14 KiB
14 KiB
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::HTTPwith custom streaming; GoChat usesresty.Client - Chatwoot: Environment variables for API keys; GoChat uses
CaptainConfigstruct via viper - Chatwoot: Provider selection via feature flags; GoChat uses
captain.llm_providerconfig 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/JSONhelpers; Chatwoot uses Railsrender json: - GoChat: Uses
parseUintParam/getAccountID/getUserIDhelpers 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
- Soft delete on all models — GoChat uses
gorm.DeletedAtuniversally; Chatwoot Captain models don't soft-delete - json.RawMessage instead of native jsonb — GoChat avoids the GORM datatypes.JSON dependency; lazy parsing in Go
- Inbox binding folded into assistant service — GoChat merges inbox binding/unbinding into CaptainAssistantService rather than a separate service
- Single CopilotHandler for all copilot actions — GoChat unifies threads, messages, suggest/summarize/translate under one handler
- LLMProvider as Go interface — Chatwoot uses Ruby class hierarchy; GoChat uses interface for polymorphic provider support
- Config struct for Captain settings — Chatwoot uses ENV vars; GoChat uses typed CaptainConfig in YAML
- Separate CopilotService — GoChat extracts Copilot into its own service rather than nesting under Captain::AssistantService
- 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 |