# P10 (M10) Captain AI + Copilot — Chatwoot vs GoChat Comparison This document maps every Chatwoot Enterprise Captain/Copilot feature to its GoChat port, highlighting structural differences, naming changes, and intentional deviations. --- ## 1. Architecture Overview | Aspect | Chatwoot (Ruby/Rails) | GoChat (Go/Gin) | |--------|----------------------|------------------| | Language | Ruby on Rails | Go + Gin framework | | ORM | ActiveRecord | GORM | | Vector DB | pgvector via ActiveRecord | pgvector via pgvector-go | | HTTP client | Net::HTTP / HTTParty | resty/v2 | | DI pattern | Rails autoloading | Manual constructor injection (repos→services→handlers) | | Routing | config/routes.rb (327+ routes) | internal/router/router.go (group-based) | | Config | ENV vars + features.yml | CaptainConfig in config.yaml (viper) | --- ## 2. Models ### Captain::Assistant → CaptainAssistant | Chatwoot Field | GoChat Field | Notes | |----------------|-------------|-------| | `id` (bigint) | `ID` (uint, GORM auto) | Same semantics | | `account_id` | `AccountID` (uint) | FK to accounts | | `name` (string) | `Name` (string) | Identical | | `status` (enum: active/draft/archived) | `Status` (AssistantStatus string enum) | Same values, Go uses typed string | | `config` (jsonb) | `Config` (json.RawMessage) | GoChat uses json.RawMessage for lazy parsing; Chatwoot stores as native jsonb | | `created_at/updated_at` | `CreatedAt/UpdatedAt` | GORM auto-managed | | — | `DeletedAt` (gorm.DeletedAt) | GoChat uses soft delete; Chatwoot has no soft delete on this model | ### Captain::Document → CaptainDocument | Chatwoot Field | GoChat Field | Notes | |----------------|-------------|-------| | `id` | `ID` (uint) | Same | | `assistant_id` | `AssistantID` (uint) | FK to captain_assistant | | `account_id` | `AccountID` (uint) | Account scope | | `url` (string) | `URL` (string) | External URL | | `external_url` | `ExternalURL` (string) | Chatwoot distinguishes url vs external_url | | `status` (enum: active/processing/failed) | `Status` (DocumentStatus string enum) | Same values | | `content` (text) | `Content` (string) | Raw document text | | `embedding` (vector(1536)) | `Embedding` (pgvector.Vector) | Both use pgvector 1536-dim cosine | | — | `DeletedAt` | GoChat soft delete | ### Captain::Scenario → CaptainScenario | Chatwoot Field | GoChat Field | Notes | |----------------|-------------|-------| | `id` | `ID` (uint) | Same | | `assistant_id` | `AssistantID` (uint) | FK | | `account_id` | `AccountID` (uint) | Account scope | | `name` | `Name` (string) | Identical | | `description` | `Description` (string) | Identical | | `scenario_type` (enum: greeting/farewell/faq/custom) | `ScenarioType` (ScenarioType string enum) | Same values | | `trigger_keywords` (jsonb) | `TriggerKeywords` (json.RawMessage) | GoChat uses json.RawMessage | | `response_template` (text) | `ResponseTemplate` (string) | Identical | | — | `DeletedAt` | Soft delete | ### Captain::InboxAssistant → CaptainInbox | Chatwoot Field | GoChat Field | Notes | |----------------|-------------|-------| | `id` | `ID` (uint) | Same | | `inbox_id` | `InboxID` (uint) | FK to inbox | | `assistant_id` | `AssistantID` (uint) | FK to captain_assistant | | `account_id` | `AccountID` (uint) | Account scope | ### Captain::Response → CaptainResponse Chatwoot model is a join/pivot; GoChat keeps it as a standalone model for audit trail. | Chatwoot Field | GoChat Field | Notes | |----------------|-------------|-------| | `id` | `ID` (uint) | Same | | `assistant_id` | `AssistantID` (uint) | FK | | `conversation_id` | `ConversationID` (uint) | FK | | `account_id` | `AccountID` (uint) | Account scope | | `content` (text) | `Content` (string) | The AI-generated response | | `source` (enum) | `Source` (string) | "captain" / "copilot" | --- ## 3. Copilot Models ### Captain::CopilotThread → CopilotThread | Chatwoot Field | GoChat Field | Notes | |----------------|-------------|-------| | `id` | `ID` (uint) | Same | | `account_id` | `AccountID` (uint) | Account scope | | `user_id` | `UserID` (uint) | Who owns this thread | | `title` | `Title` (string) | Thread display name | | `status` | `Status` (string) | active/archived | | `metadata` (jsonb) | `Metadata` (json.RawMessage) | Flexible metadata | | — | `DeletedAt` | Soft delete | ### Captain::CopilotMessage → CopilotMessage | Chatwoot Field | GoChat Field | Notes | |----------------|-------------|-------| | `id` | `ID` (uint) | Same | | `thread_id` | `ThreadID` (uint) | FK to copilot_thread | | `role` (enum: user/assistant/system) | `Role` (string) | Same values | | `content` (text) | `Content` (string) | Message body | --- ## 4. LLM Provider Abstraction | Chatwoot | GoChat | Notes | |----------|--------|-------| | `Captain::BaseAiService` | `llm.LLMProvider` (interface) | GoChat uses Go interface; Chatwoot uses Ruby class hierarchy | | `Captain::OpenAiService` | `llm.OpenAIProvider` (struct) | Both implement OpenAI chat+embeddings | | — | `llm.ProviderConfig` | Config struct mapped from CaptainConfig in config.yaml | | — | `llm.ChatRequest/ChatResponse` | Unified request/response types | | — | `llm.EmbeddingRequest/EmbeddingResponse` | Embedding types | | — | `llm.StreamChunk` | Streaming support (Chatwoot uses SSE via ActionController::Live) | | — | `llm.ToolCall/ToolResult` | Structured tool call interface | ### Key Differences - **Chatwoot**: Uses `Net::HTTP` with custom streaming; GoChat uses `resty.Client` - **Chatwoot**: Environment variables for API keys; GoChat uses `CaptainConfig` struct via viper - **Chatwoot**: Provider selection via feature flags; GoChat uses `captain.llm_provider` config field --- ## 5. Tool Registry | Chatwoot | GoChat | Notes | |----------|--------|-------| | `Captain::ToolRegistry` (Ruby module) | `llm.ToolRegistry` (Go struct) | Both register named tools | | `Captain::SearchDocumentationService` | `llm.SearchDocumentationService` | Vector similarity search as a tool | | `Captain::CustomHttpTool` | `llm.CustomHttpTool` | HTTP call tool with headers/body config | | — | `llm.BaseTool` (interface) | Go interface for extensible tools; Chatwoot uses module mixins | --- ## 6. Services | Chatwoot Service | GoChat Service | Notes | |-------------------|---------------|-------| | `Captain::AssistantService` | `CaptainAssistantService` | CRUD + config update + list by account | | `Captain::DocumentService` | `CaptainDocumentService` | CRUD + search similar (vector) + list by assistant | | `Captain::ScenarioService` | `CaptainScenarioService` | CRUD | | `Captain::InboxAssistantService` | (embedded in CaptainAssistantService.BindInbox/UnbindInbox) | GoChat folds inbox binding into assistant service | | `Captain::CopilotService` | `CopilotService` | Thread CRUD + message CRUD + LLM calls | | — | `ReplySuggestionService` | Extracted as sub-service within CopilotService | | — | `ConversationSummaryService` | Extracted as sub-service within CopilotService | | — | `TranslationService` | Extracted as sub-service within CopilotService | ### Key Differences - **Chatwoot**: Inbox binding is a separate service; GoChat folds it into CaptainAssistantService - **Chatwoot**: Copilot features are separate controller actions; GoChat uses a single CopilotService with sub-methods - **GoChat**: Each service takes its repo(s) via constructor injection (newer pattern) --- ## 7. Repositories | Chatwoot | GoChat | Notes | |----------|--------|-------| | ActiveRecord scopes (`.where`, `.order`) | GORM `.Where`, `.Order`, `.Limit` | Equivalent query patterns | | `Captain::Assistant.where(account_id: id)` | `CaptainAssistantRepo.ListByAccount(accountID)` | Named methods | | Vector search: raw SQL `ORDER BY embedding <=> $1` | `CaptainDocumentRepo.SearchSimilar(embedding, limit)` using pgvector-go cosine distance | Both use pgvector cosine distance operator | | — | `BaseRepository[T]` generic | GoChat has a generic base repo pattern | --- ## 8. Handlers / Controllers | Chatwoot Controller | GoChat Handler | Routes | |---------------------|---------------|--------| | `Captain::AssistantsController` | `CaptainAssistantHandler` | `/accounts/:id/captain/assistants` | | `Captain::DocumentsController` | `CaptainDocumentHandler` | `/accounts/:id/captain/assistants/:aid/documents` | | `Captain::ScenariosController` | `CaptainScenarioHandler` | `/accounts/:id/captain/assistants/:aid/scenarios` | | `Captain::CustomToolsController` | `CaptainCustomToolHandler` | `/accounts/:id/captain/custom_tools` | | `Captain::CopilotThreadsController` | `CopilotHandler.ListThreads/CreateThread/GetThread/DeleteThread` | `/accounts/:id/captain/copilot_threads` | | `Captain::CopilotMessagesController` | `CopilotHandler.SendMessage` | `/accounts/:id/captain/copilot_threads/:id/messages` | | — | `CopilotHandler.SuggestReplies` | `/accounts/:id/captain/copilot/suggest_replies` | | — | `CopilotHandler.SummarizeConversation` | `/accounts/:id/captain/copilot/summarize` | | — | `CopilotHandler.TranslateMessage` | `/accounts/:id/captain/copilot/translate` | ### Key Differences - **Chatwoot**: Separate controllers per resource; GoChat uses grouped handlers with method-per-action - **Chatwoot**: Copilot threads/messages are separate controllers; GoChat unifies under single CopilotHandler - **GoChat**: Uses `response.Success/Error/JSON` helpers; Chatwoot uses Rails `render json:` - **GoChat**: Uses `parseUintParam/getAccountID/getUserID` helpers for param extraction --- ## 9. Route Structure | Chatwoot Route Pattern | GoChat Route Pattern | Notes | |------------------------|---------------------|-------| | `namespace :api, scope :v1` → `namespace :accounts` → `namespace :captain` | `/api/v1/accounts/:account_id/captain/...` | Same nesting structure | | `resources :assistants` | `captain.Group("/assistants")` | Identical CRUD | | `assistants/:id/inboxes` (POST/DELETE) | `assistants.POST("/:id/inboxes")` | Inbox binding routes | | `assistants/:assistant_id/documents` | `assistants.Group("/:assistant_id/documents")` | Nested documents | | `assistants/:assistant_id/scenarios` | `assistants.Group("/:assistant_id/scenarios")` | Nested scenarios | | `resources :custom_tools` | `captain.Group("/custom_tools")` | Account-level tools | | `resources :copilot_threads` | `captain.Group("/copilot_threads")` | Thread CRUD | | `copilot/suggest_replies` | `captain.POST("/copilot/suggest_replies")` | AI action endpoints | | `copilot/summarize` | `captain.POST("/copilot/summarize")` | Summary endpoint | | `copilot/translate` | `captain.POST("/copilot/translate")` | Translation endpoint | --- ## 10. Bootstrap / Dependency Wiring | Chatwoot | GoChat | Notes | |----------|--------|-------| | Rails autoloading (no explicit DI) | Manual constructor chain in `bootstrap.go` | Explicit wiring: repos→services→handlers | | `config/initializers/` auto-run | `Bootstrap()` function step-by-step | Ordered dependency creation | | Feature flags (`CAPTAIN_ENABLED`) | `captain.enabled` in config.yaml | Toggle feature availability | | ENV: `OPENAI_API_KEY` | `captain.llm_api_key` | Same concept, different naming | --- ## 11. File Inventory ### GoChat Files Created (P10 M10) | File | Lines | Purpose | |------|-------|---------| | `internal/model/captain_models.go` | 237 | 6 Captain models + enums | | `internal/model/copilot_models.go` | ~70 | CopilotThread + CopilotMessage | | `internal/repository/captain_repo.go` | 295 | 6 repos with vector search | | `internal/repository/copilot_repo.go` | 84 | 2 repos | | `internal/service/captain_service.go` | 225 | 4 Captain services | | `internal/service/copilot_service.go` | 238 | CopilotService (CRUD + AI actions) | | `internal/service/llm/provider.go` | 322 | LLMProvider interface + OpenAIProvider | | `internal/service/llm/prompts.go` | 159 | System prompt builders | | `internal/service/llm/tools.go` | ~300 | ToolRegistry + tools | | `internal/handler/api/v1/captain_handler.go` | 624 | 4 Captain handlers | | `internal/handler/api/v1/copilot_handler.go` | 218 | CopilotHandler | | `internal/handler/api/v1/helpers.go` | 69 | parseUintParam, getUserID, getPaginationParams | | `internal/config/config.go` | +17 | CaptainConfig struct | | `internal/router/router.go` | +63 | Captain route group | | `internal/app/bootstrap.go` | +30 | Captain repos, services, LLM, handlers wiring | ### Modified Existing Files | File | Change | |------|--------| | `internal/config/config.go` | Added `Captain CaptainConfig` to Config struct | | `internal/router/router.go` | Added 5 handler fields to Handlers struct; Captain route group | | `internal/app/bootstrap.go` | Added llm import; 8 Captain repos; 4 services + LLM provider + copilot; 5 handler wires | --- ## 12. Intentional Deviations from Chatwoot 1. **Soft delete on all models** — GoChat uses `gorm.DeletedAt` universally; Chatwoot Captain models don't soft-delete 2. **json.RawMessage instead of native jsonb** — GoChat avoids the GORM datatypes.JSON dependency; lazy parsing in Go 3. **Inbox binding folded into assistant service** — GoChat merges inbox binding/unbinding into CaptainAssistantService rather than a separate service 4. **Single CopilotHandler for all copilot actions** — GoChat unifies threads, messages, suggest/summarize/translate under one handler 5. **LLMProvider as Go interface** — Chatwoot uses Ruby class hierarchy; GoChat uses interface for polymorphic provider support 6. **Config struct for Captain settings** — Chatwoot uses ENV vars; GoChat uses typed CaptainConfig in YAML 7. **Separate CopilotService** — GoChat extracts Copilot into its own service rather than nesting under Captain::AssistantService 8. **No Captain::ResponseService** — GoChat has the model but no dedicated response service (responses are embedded in assistant service flow) --- ## 13. Missing / Deferred Items | Feature | Status | Notes | |---------|--------|-------| | Document processing (ingestion, chunking) | Deferred | Needs document processor service | | Embedding generation pipeline | Deferred | Needs background job to call OpenAI embeddings API | | Streaming responses (SSE) | Stub only | Chatwoot uses ActionController::Live; GoChat needs Gin SSE implementation | | Captain webhook for inbox events | Deferred | Needs P7 channel integration | | Response quality scoring | Deferred | Chatwoot has response rating; GoChat model has fields but no scoring service | | Multiple LLM providers (Azure, Anthropic, Ollama) | Interface ready | OpenAIProvider implemented; others stubs | | Admin UI for Captain config | Deferred | Front-end work | | Document upload endpoint | Missing | Chatwoot has file upload; GoChat needs multipart handler |