Files
gochat/docs/comparison/M10-captain-chatwoot-vs-gochat.md
T
2026-06-04 15:44:48 +08:00

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 |