280 lines
14 KiB
Markdown
280 lines
14 KiB
Markdown
# 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 | |