feat(captain): align preferences payloads
This commit is contained in:
@@ -16,9 +16,9 @@ Build GoChat as a Go backend that can directly reuse the frontend from `referenc
|
||||
|
||||
## Current Baseline
|
||||
|
||||
- Latest implementation checkpoint: `feat(copilot): align thread message payloads`.
|
||||
- Latest documentation checkpoint: this checkpoint, recorded with the B11.2 Copilot thread/message persistence and payload implementation.
|
||||
- Worktree status at this implementation checkpoint: B11.1a aligns Captain assistant CRUD/tools/inbox bindings; B11.1b aligns Captain scenarios and custom tools; B11.1c aligns Captain documents, assistant responses, bulk actions, and custom-tool test payloads; B11.2 aligns Copilot thread/message create/list/get/delete payloads, account/user scoping, and no-LLM fallback persistence. Next active slice is B11.3 document sync, embedding, Meilisearch, and external LLM gates.
|
||||
- Latest implementation checkpoint: `feat(captain): align preferences payloads`.
|
||||
- Latest documentation checkpoint: this checkpoint, recorded with the B11.3a Captain preferences account-level payload implementation.
|
||||
- Worktree status at this implementation checkpoint: B11.1a aligns Captain assistant CRUD/tools/inbox bindings; B11.1b aligns Captain scenarios and custom tools; B11.1c aligns Captain documents, assistant responses, bulk actions, and custom-tool test payloads; B11.2 aligns Copilot thread/message create/list/get/delete payloads, account/user scoping, and no-LLM fallback persistence; B11.3a aligns Captain preferences show/update payloads and account-level model/feature storage. Next active slice is B11.3b document sync, embedding, Meilisearch, and external LLM gates.
|
||||
- `go test ./...` passes.
|
||||
- Route dump succeeds with `TOTAL: 830` after adding the Chatwoot-compatible applied-SLA index route.
|
||||
- Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`.
|
||||
@@ -55,7 +55,7 @@ Next ordered checkpoints:
|
||||
| Phase 1 | Meilisearch search engine | Review | B6 payload parity, optional live gate, and DB-fallback hardening are implemented; an actual live Meilisearch run is optional and environment-dependent |
|
||||
| Phase 2 | Route and controller parity audit | Doing | Ruby/Bundler unavailable, so Chatwoot route extraction currently uses static `routes.rb` fallback |
|
||||
| Phase 3 | Data and serializer parity | Doing | JSON fixture coverage is partial and still endpoint-family based |
|
||||
| Phase 4 | Enterprise feature completion | Doing | B7, B8, B9, and B10 are in Review; B11 Captain resources and Copilot thread/message payloads are landed, while Copilot tasks/preferences/tool-call depth and document/LLM/Meilisearch gates remain active |
|
||||
| Phase 4 | Enterprise feature completion | Doing | B7, B8, B9, and B10 are in Review; B11 Captain resources, preferences, and Copilot thread/message payloads are landed, while Copilot tasks/tool-call depth and document/LLM/Meilisearch gates remain active |
|
||||
| Phase 5 | Background jobs and integrations | Planned | durable worker choice and job parity are open |
|
||||
| Phase 6 | Core placeholder burn-down | Doing | account/contact/conversation/message/inbox placeholder groups remain broad |
|
||||
| Phase 7 | Verification harness | Planned | search live gate and reused-frontend smoke harness are not complete |
|
||||
@@ -150,12 +150,13 @@ This ledger records the committed parity checkpoints that future slices should b
|
||||
| `feat(captain): align scenario and tool payloads` | Advanced B11.1b for Captain scenarios and custom tools: scenario create/list/show/update/delete now accept nested `{ scenario }`, enforce account/assistant scope, return Chatwoot raw scenario serializers and `{ payload, meta }` lists, hide disabled scenarios from index, and return `204` on delete; custom tools now accept nested `{ custom_tool }`, auto-generate slugs from title, enforce account scope, and return raw tool serializers or `{ payload, meta }` lists. | `go test ./internal/handler/api/v1 -run 'CaptainScenario\|CaptainCustomTool\|CaptainResource' -count=1`; `go test ./internal/handler/api/v1 -run 'Captain\|Copilot' -count=1`; `go test ./internal/service -run 'Captain\|Copilot' -count=1`; focused fixtures cover nested frontend bodies, account scoping, disabled scenario filtering, raw payloads, custom-tool slug generation, and no local envelopes. | Continue B11.1c with Captain documents, assistant responses, and bulk actions; custom-tool `test` still needs the reference unsaved-tool execution contract and safe network boundary before B11 leaves Review. |
|
||||
| `feat(captain): align document response actions` | Completes B11.1c frontend payload depth for Captain documents, assistant responses, bulk actions, and custom-tool test: document list/create/show/sync/delete now use account-scoped raw serializers and `{ payload, meta }`; assistant responses use nested `{ assistant_response }`, account-scoped raw serializers, filters, edited flag, and `204` delete; bulk actions accept Chatwoot `{ type, ids, fields.status }` for `AssistantResponse` and `AssistantDocument`; custom-tool test accepts unsaved nested `{ custom_tool }` configs and returns raw `{ status, body }` with a fakeable HTTP boundary. | Focused B11.1c handler tests cover document list/create/show/sync/delete, response list/create/show/update/delete, bulk approve/delete/sync/delete shapes, custom-tool test success/error contract, account scoping, sync status, and no local envelopes. Verification passed: focused B11.1c handler tests, Captain/Copilot service tests, Captain repository tests, handler/service package tests, escalated full `go test ./...`, and `git diff --check`. | Continue B11.2 with Copilot persistence, disabled-state payloads, and document/LLM/Meilisearch gates. B11 remains Doing until Copilot and external-provider gates are tracked and tested. |
|
||||
| `feat(copilot): align thread message payloads` | Completes the B11.2 Copilot thread/message frontend contract: thread create accepts Chatwoot `{ message, assistant_id, conversation_id }`, creates the initial user message plus deterministic assistant fallback when no LLM provider is configured, validates assistant account scope, and returns raw thread payloads. Thread list returns `{ payload }` scoped to current account/user with Chatwoot page size/order. Nested `copilot_messages` list/create returns raw message payloads with embedded thread/user/assistant push data and `204` delete remains available for local compatibility. | `go test ./internal/handler/api/v1 -run 'Copilot' -count=1`; `go test ./internal/handler/api/v1 -run 'Captain\|Copilot' -count=1`; `go test ./internal/service -run 'Captain\|Copilot' -count=1`; `go test ./internal/repository -run 'Copilot' -count=1`; handler/service/repository package tests; escalated full `go test ./...`; `git diff --check`. Focused handler fixtures cover no local envelopes, account/user isolation, assistant scope, nested message order, and no-LLM assistant fallback persistence. | Continue B11.3 with Captain document sync/indexing, Meilisearch/embedding gates, Copilot tasks/preferences/tool-call/playground depth, streaming/realtime compatibility, and reused frontend smoke coverage. |
|
||||
| `feat(captain): align preferences payloads` | Starts B11.3a by replacing the legacy local `captain_preferences` CRUD response with Chatwoot's account-level Captain preferences contract: `GET/PUT /captain/preferences` now return raw `{ providers, models, features }`, `captain_models` and `captain_features` persist on accounts, updates merge with existing account settings, invalid model selections return `422`, and update is administrator-gated. Route artifacts were regenerated after removing frontend-unused POST/DELETE preference routes and adding exact no-trailing-slash GET/PUT paths. | `go test ./internal/handler/api/v1 -run 'CaptainPreference' -count=1`; `go test ./internal/service -run 'CaptainPreference' -count=1`; `go test ./internal/handler/api/v1 -run 'Captain\|Copilot' -count=1`; `go test ./internal/service -run 'Captain\|Copilot' -count=1`; `go test ./internal/handler/api/v1 -count=1`; `go test ./internal/service -count=1`; `go test ./internal/model -count=1`; `go test ./internal/router -count=1`; `go test ./cmd/migrate -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; escalated full `go test ./...`; `git diff --check`. | Continue B11.3b with document sync/indexing, Meilisearch/embedding gates, remaining Copilot task/tool-call/playground depth, streaming/realtime compatibility, and reused frontend smoke coverage. |
|
||||
|
||||
## Next Slice Contract
|
||||
|
||||
Completed implementation slice: B11.2 now aligns Copilot thread and nested message persistence/payloads with the reused Chatwoot frontend, building on B11.1a-B11.1c Captain resource contracts.
|
||||
Completed implementation slice: B11.3a now aligns Captain preferences show/update payloads with the reused Chatwoot frontend, building on B11.1a-B11.2 Captain/Copilot resource contracts.
|
||||
|
||||
Next implementation slice: continue B11.3 document sync/indexing, Meilisearch/embedding gates, Copilot tasks/preferences/tool-call/playground depth, and streaming/realtime compatibility. B9.3 delayed/durable worker scheduling and B9.4 macro attachment depth remain named Phase 5 follow-ups.
|
||||
Next implementation slice: continue B11.3b document sync/indexing, Meilisearch/embedding gates, Copilot tasks/tool-call/playground depth, and streaming/realtime compatibility. B9.3 delayed/durable worker scheduling and B9.4 macro attachment depth remain named Phase 5 follow-ups.
|
||||
|
||||
| Step | Required result | Reference source | Verification |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -179,7 +180,8 @@ Next implementation slice: continue B11.3 document sync/indexing, Meilisearch/em
|
||||
| N18 | Keep B11.1b Captain scenario/custom-tool resources as current Captain baseline. | Captain scenarios/custom tools controllers, Jbuilder views, and dashboard clients. | Done by `feat(captain): align scenario and tool payloads`; scenarios and custom tools use nested frontend bodies, account scoping, raw payloads, and Chatwoot list meta. |
|
||||
| N19 | Keep B11.1c Captain document/response/bulk/test resources as current Captain baseline. | Captain documents, assistant responses, bulk actions, custom-tool test controllers, Jbuilder views, and dashboard clients. | Done by `feat(captain): align document response actions`; documents, assistant responses, bulk resource actions, and unsaved custom-tool test use account-scoped Chatwoot payloads with a fakeable HTTP boundary. |
|
||||
| N20 | Keep B11.2 Copilot thread/message resources as current Copilot baseline. | `copilot_threads_controller.rb`, `copilot_messages_controller.rb`, Copilot Jbuilder partials, and dashboard `copilotThreads.js`/`copilotMessages.js` clients. | Done by `feat(copilot): align thread message payloads`; threads/messages persist account/user-scoped Chatwoot payloads and no-LLM fallback assistant messages. |
|
||||
| N21 | Update this tracker after every implementation checkpoint. | This document. | `git diff --check`; `go test ./...` for Go changes. |
|
||||
| N21 | Keep B11.3a Captain preferences as current account-level Captain config baseline. | `preferences_controller.rb`, `CaptainFeaturable`, `config/llm.yml`, dashboard `preferences.js` API/store. | Done by `feat(captain): align preferences payloads`; raw model/provider/feature payloads, merge update behavior, account storage, admin gate, and exact preference routes are covered. |
|
||||
| N22 | Update this tracker after every implementation checkpoint. | This document. | `git diff --check`; `go test ./...` for Go changes. |
|
||||
|
||||
Current B2 profile checkpoint:
|
||||
|
||||
@@ -410,7 +412,7 @@ Upcoming enterprise task boards:
|
||||
| B10 | B10.4 | Review remaining InboxLimit/account-limit create-path enforcement outside capacity policies. | Chatwoot enterprise account/inbox limit policies. | Inbox/channel create tests for over-limit behavior and frontend-readable errors. | Done by `feat(inboxes): enforce chatwoot inbox limits` |
|
||||
| B11 | B11.1 | Align Captain assistant CRUD, inbox bindings, responses, documents, scenarios, and custom tools payloads. | Captain controllers/services/frontend clients under `reference/chatwoot`. | Handler/service fixtures for every Captain dashboard client path. | Review; assistant CRUD/tools/inbox binding, scenarios, custom tools, documents, assistant responses, bulk actions, and custom-tool test payloads are landed |
|
||||
| B11 | B11.2 | Align Copilot threads, messages, tasks, preferences, playground/tool-call behavior, and disabled-state feature gates. | Copilot controllers/services/frontend clients under `reference/chatwoot`. | Copilot handler/service tests for persistence, disabled LLM state, and frontend payloads. | Review; thread/message payloads, account/user scoping, assistant scope, and no-LLM fallback are landed; tasks/preferences/tool-call/playground depth remains in B11.3 follow-up |
|
||||
| B11 | B11.3 | Add document sync/embedding/LLM job boundaries where external dependencies are required and finish remaining Copilot task/preference/tool-call/streaming depth. | Captain/Copilot jobs, document services, Copilot controllers/services/frontend clients. | Worker tests or explicit feature-gated fallback tests plus Copilot task/preference/tool-call fixtures. | Todo |
|
||||
| B11 | B11.3 | Add document sync/embedding/LLM job boundaries where external dependencies are required and finish remaining Copilot task/preference/tool-call/streaming depth. | Captain/Copilot jobs, document services, Copilot controllers/services/frontend clients. | Worker tests or explicit feature-gated fallback tests plus Copilot task/preference/tool-call fixtures. | Doing; B11.3a Captain preferences show/update payloads are landed, while document sync/indexing and remaining Copilot task/tool-call depth remain active |
|
||||
| B12 | B12.1 | Add a repeatable command to run the reused Chatwoot frontend against GoChat. | `reference/chatwoot` frontend boot/auth/API clients. | Smoke command documented and runnable locally. | Todo |
|
||||
| B12 | B12.2 | Cover login, inbox list/settings, conversation list/detail/message send, contact/company views, widget init/message, public CSAT, SLA/CSAT reports, and enterprise admin screens. | Dashboard route usage and frontend stores/API modules. | Smoke report checked into `docs/parity/` with pass/fail gaps. | Todo |
|
||||
|
||||
@@ -771,7 +773,8 @@ B11 Captain/Copilot breakdown:
|
||||
| B11.1b | Align Captain scenario and custom-tool CRUD/list payloads. | `scenarios_controller.rb`, `custom_tools_controller.rb`, scenario/custom-tool Jbuilder views, dashboard Captain clients. | Handler fixtures for nested bodies, list/show/create/update/delete, account scoping, disabled scenario filtering, and no local response envelopes. | Done by `feat(captain): align scenario and tool payloads` |
|
||||
| B11.1c | Align Captain documents, assistant responses, bulk actions, and custom-tool test execution. | `documents_controller.rb`, `assistant_responses_controller.rb`, `bulk_actions_controller.rb`, `custom_tools_controller#test`, dashboard Captain clients. | Handler/service fixtures for document list/create/show/sync/delete, response list/show/create/update/delete, bulk-action payloads, test execution errors, account scoping, sync status, and no local response envelopes. | Done by `feat(captain): align document response actions` |
|
||||
| B11.2 | Align Copilot thread/message APIs, account/user scoping, assistant scope, frontend payloads, and no-LLM fallback persistence. | `copilot_threads_controller.rb`, `copilot_messages_controller.rb`, Copilot Jbuilder partials, dashboard `copilotThreads.js` and `copilotMessages.js`. | Handler tests cover thread/message create/list/get/delete, nested message ordering, assistant/account isolation, user isolation, and deterministic disabled LLM fallback. | Done by `feat(copilot): align thread message payloads` |
|
||||
| B11.3 | Implement document sync/indexing gates for Meilisearch or the chosen embedding/search backend, and finish Copilot task/preference/playground/tool-call/streaming depth without blocking the frontend when LLM config is absent. | Captain document/embedding services plus remaining Copilot controllers/services/frontend clients. | Tests cover disabled state, failed sync observability, successful fake backend indexing, Copilot task/preference/tool-call persistence, and non-streaming fallback. | Todo |
|
||||
| B11.3a | Align Captain preferences show/update payloads, account-level model/feature storage, and admin update gate. | `preferences_controller.rb`, `CaptainFeaturable`, `config/llm.yml`, dashboard `preferences.js` API/store. | Handler tests cover raw `{ providers, models, features }`, default selected models, merge update behavior, account persistence, invalid model rejection, admin gate, and exact preference routes. | Done by `feat(captain): align preferences payloads` |
|
||||
| B11.3b | Implement document sync/indexing gates for Meilisearch or the chosen embedding/search backend, and finish Copilot task/playground/tool-call/streaming depth without blocking the frontend when LLM config is absent. | Captain document/embedding services plus remaining Copilot controllers/services/frontend clients. | Tests cover disabled state, failed sync observability, successful fake backend indexing, Copilot task/tool-call persistence, and non-streaming fallback. | Todo |
|
||||
|
||||
B11.1a current checkpoint:
|
||||
|
||||
@@ -809,13 +812,22 @@ B11.2 current checkpoint:
|
||||
- `GetThread`/`DeleteThread` remain available for local compatibility but now use the same account/user scoping; delete removes thread messages and returns `204`.
|
||||
- Focused tests cover raw payloads without local `{ success, data }`, assistant/account isolation, user isolation, nested message persistence/order, and the safe no-LLM assistant fallback message.
|
||||
|
||||
B11.3a current checkpoint:
|
||||
|
||||
- Reference files inspected for this slice: `reference/chatwoot/app/controllers/api/v1/accounts/captain/preferences_controller.rb`, `app/models/concerns/captain_featurable.rb`, `config/llm.yml`, dashboard `api/captain/preferences.js`, and store `captain/preferences.js`.
|
||||
- Captain preferences now follow Chatwoot `resource :preferences, only: [:show, :update]`: exact no-trailing-slash GET/PUT routes are registered and the frontend-unused local POST/DELETE preference routes are no longer mounted.
|
||||
- Preference responses now return raw `{ providers, models, features }` instead of local `{ success, data }` or tone/language rows. Feature payloads include configured models, default model, account-selected model, and enabled state.
|
||||
- Account-level `captain_models` and `captain_features` JSON fields now persist the same settings Chatwoot stores through `Account#settings`; updates merge partial incoming maps with existing account settings.
|
||||
- Invalid model selections return `422 { error }`, non-admin updates return `401`, and agents can still read the preferences payload.
|
||||
- Route dump was regenerated and remains `TOTAL: 830`; route parity remains `270 exact, 7 parameter-compatible, 0 missing`.
|
||||
|
||||
B11 landing rules:
|
||||
|
||||
| Area | Landing requirement | Done signal |
|
||||
| --- | --- | --- |
|
||||
| Captain assistant resources | Audit the current `reference/chatwoot` Captain route/controller/frontend client set before code changes, then implement account-scoped CRUD and nested assistant resources with raw frontend-compatible payloads. | Handler tests cover list/show/create/update/delete, inbox binding, response/scenario/document/custom-tool paths, and disabled-state responses. |
|
||||
| Captain document sync | External embedding/LLM work must sit behind fakeable interfaces and config gates; missing provider config must not break the reused frontend. | Tests cover disabled config, fake successful sync, failure metadata, and no unhandled external call in default test mode. |
|
||||
| Copilot persistence | Threads/messages are now persisted and frontend-shaped; tasks, preferences, playground state, and tool-call records must still persist enough data for frontend reloads. | B11.2 tests cover thread/message create/list/get/delete and account/user scoping; B11.3 must add tool-call serialization and task/preference lifecycle tests. |
|
||||
| Copilot persistence | Threads/messages and Captain preferences are now persisted and frontend-shaped; tasks, playground state, and tool-call records must still persist enough data for frontend reloads. | B11.2 tests cover thread/message create/list/get/delete and account/user scoping; B11.3a tests cover preferences; B11.3b must add tool-call serialization and task lifecycle tests. |
|
||||
| Streaming fallback | If Chatwoot streams a response but GoChat cannot yet stream safely, return a documented frontend-compatible non-streaming or disabled state rather than a placeholder success. | Tests prove the frontend API path receives a deterministic payload/status. |
|
||||
| Deferred external depth | Model/provider-specific LLM behavior may be feature-gated, but every gate must be visible in this tracker and covered by tests. | B11 stays `Review`, not `Done`, while any external-provider depth remains deferred. |
|
||||
|
||||
@@ -1207,7 +1219,7 @@ Included checklist:
|
||||
- [x] Custom roles and permission checks.
|
||||
- [x] Agent capacity and assignment limits.
|
||||
- [ ] Assignment policies and auto-assignment compatibility.
|
||||
- [ ] Captain/Copilot assistant, custom tools, scenarios, documents, responses, inbox bindings, and Copilot threads/messages. Captain assistant CRUD/tools/inbox binding, scenario/custom-tool CRUD, documents, assistant responses, bulk actions, custom-tool test payloads, and Copilot thread/message payloads are landed; external document/LLM gates and remaining Copilot task/tool-call depth remain active.
|
||||
- [ ] Captain/Copilot assistant, custom tools, scenarios, documents, responses, inbox bindings, preferences, and Copilot threads/messages. Captain assistant CRUD/tools/inbox binding, scenario/custom-tool CRUD, documents, assistant responses, bulk actions, custom-tool test payloads, preferences, and Copilot thread/message payloads are landed; external document/LLM gates and remaining Copilot task/tool-call depth remain active.
|
||||
- [x] CSAT survey response flow, metrics, filters, and review notes.
|
||||
- [x] Inbox limits and account/inbox usage enforcement.
|
||||
- [x] Automation rules, macros, execution logs, and action side effects.
|
||||
@@ -1226,7 +1238,7 @@ Enterprise tracking table:
|
||||
| P4.3 | Custom roles/permissions | `internal/model/custom_role.go`, `internal/service/custom_role_service.go`, `internal/middleware/role_check.go`, `internal/handler/api/v1/custom_role_handler.go` | Permission arrays, raw admin payloads, administrator gates, account-user resolution, profile permissions, and delete nullification are implemented; keep in Review until frontend smoke covers enterprise settings. | Review |
|
||||
| P4.4 | Agent capacity | `internal/model/agent_capacity_policy.go`, `internal/service/agent_capacity_policy_service.go`, `internal/handler/api/v1/agent_capacity_handler.go`, `internal/autoassignment/*` | First align AgentCapacityPolicy API, serializers, nested users, and `InboxCapacityLimit` data contracts; then enforce capacity in manual/auto assignment. | Review |
|
||||
| P4.5 | Inbox limits | `internal/model/agent_capacity_policy.go` `InboxCapacityLimit`, plus legacy `internal/model/inbox_limit.go`, `internal/service/inbox_limit_service.go`, `internal/repository/inbox_limit_repo.go`, `internal/handler/api/v1/inbox_limit_handler.go` | Chatwoot enterprise nested `agent_capacity_policies/:policy_id/inbox_limits`, assignment-path enforcement, and account-level `usage_limits[:inboxes]` create-path enforcement are covered. | Review |
|
||||
| P4.6 | Captain/Copilot | `internal/model/captain_models.go`, `internal/model/copilot_models.go`, `internal/service/captain_*`, `internal/service/copilot_*`, `internal/handler/api/v1/captain_*`, `internal/handler/api/v1/copilot_handler.go` | Captain resource payloads through B11.1c and Copilot thread/message payloads through B11.2 are aligned; complete Copilot tasks/preferences/tool-call depth, suggestions, document/LLM gates, and streaming compatibility. | Doing |
|
||||
| P4.6 | Captain/Copilot | `internal/model/captain_models.go`, `internal/model/copilot_models.go`, `internal/service/captain_*`, `internal/service/copilot_*`, `internal/handler/api/v1/captain_*`, `internal/handler/api/v1/copilot_handler.go` | Captain resource payloads through B11.1c, Copilot thread/message payloads through B11.2, and Captain preferences through B11.3a are aligned; complete Copilot tasks/tool-call depth, suggestions, document/LLM gates, and streaming compatibility. | Doing |
|
||||
| P4.7 | CSAT | `internal/csat/*`, `internal/automation/csat_survey_*`, `internal/handler/api/v1/csat_*`, `internal/service/csat_metrics_service.go` | Account-side list/metrics/review-note payloads, public update depth, resolve-triggered generic survey send, and download CSV are aligned; channel-specific template send hooks remain Phase 5 integration work. | Review |
|
||||
| P4.8 | Automation and macros | `internal/automation/*`, `internal/handler/api/v1/automation_rule_handler.go`, `internal/handler/api/v1/macro_handler.go` | Automation-rule CRUD/listener/log/external-action delivery and macro CRUD/display-ID execution parity are implemented; durable delayed actions and deeper macro attachment/file parity remain B9.3/B9.4 follow-ups. | Review |
|
||||
| P4.9 | Assignment policies | `internal/autoassignment/*`, `internal/automation/agent_bot_rule_listener.go` | Match Chatwoot assignment policy behavior and availability/capacity rules. | Doing |
|
||||
@@ -1243,7 +1255,7 @@ Enterprise work package breakdown:
|
||||
| Audit | Audit model parity, mutating action coverage, request metadata, filters/pagination, admin endpoint payloads. | List/admin payload parity is covered by `feat(audit): align chatwoot audit log payloads`; named representative writer coverage is covered by `feat(audit): record enterprise mutations` and `feat(audit): cover operational mutations`. | Review |
|
||||
| Custom roles | Permission-key parity, account-user role resolution, policy middleware, create/update/delete behavior. | Permission arrays, raw admin payloads, admin gates, RBAC/account-scope/profile resolution, and delete nullification are covered by `feat(custom-roles): align chatwoot permissions`. | Review |
|
||||
| Inbox limits | Account/inbox limit models, create/update enforcement, UI-readable limit responses, admin overrides. | Chatwoot nested capacity-limit API, assignment-path enforcement, and account-level create-limit `402` behavior are covered by B5/B7/B10.4. | Review |
|
||||
| Captain/Copilot | Assistants, inbox bindings, scenarios, responses, documents, tools, copilot threads/messages, tasks, streaming/tool-call behavior. | Assistant CRUD/tools/inbox binding fixtures are covered by `feat(captain): align assistant resources`; scenario/custom-tool fixtures are covered by `feat(captain): align scenario and tool payloads`; document/assistant-response/bulk/custom-tool-test fixtures are covered by `feat(captain): align document response actions`; Copilot thread/message fixtures are covered by `feat(copilot): align thread message payloads`; feature gates for external LLM dependencies, remaining Copilot tasks/tool-calls, and frontend smoke screens remain. | Doing |
|
||||
| Captain/Copilot | Assistants, inbox bindings, scenarios, responses, documents, tools, preferences, copilot threads/messages, tasks, streaming/tool-call behavior. | Assistant CRUD/tools/inbox binding fixtures are covered by `feat(captain): align assistant resources`; scenario/custom-tool fixtures are covered by `feat(captain): align scenario and tool payloads`; document/assistant-response/bulk/custom-tool-test fixtures are covered by `feat(captain): align document response actions`; Copilot thread/message fixtures are covered by `feat(copilot): align thread message payloads`; preferences fixtures are covered by `feat(captain): align preferences payloads`; feature gates for external LLM dependencies, remaining Copilot tasks/tool-calls, and frontend smoke screens remain. | Doing |
|
||||
|
||||
Enterprise acceptance gates:
|
||||
|
||||
@@ -1482,3 +1494,4 @@ Verification milestone gates:
|
||||
- 2026-06-05: B11.1b Captain scenario/custom-tool checkpoint prepared as `feat(captain): align scenario and tool payloads`; scenarios now accept nested frontend bodies, enforce account/assistant scope, list only enabled scenarios, and return raw scenario/list/204 payloads. Custom tools now accept nested frontend bodies, auto-generate slugs, enforce account scope, and return raw tool/list/204 payloads. Focused CaptainScenario/CaptainCustomTool/CaptainResource tests plus Captain/Copilot focused handler/service tests passed. Next slice is B11.1c documents, assistant responses, bulk actions, and custom-tool test execution.
|
||||
- 2026-06-05: B11.1c Captain document/response/action checkpoint prepared as `feat(captain): align document response actions`; documents now return Chatwoot raw/list payloads with account scope and `202` sync marking, assistant responses now support nested bodies, filters, raw serializers, edited tracking, and `204` delete, bulk actions now accept Chatwoot `AssistantResponse`/`AssistantDocument` resource payloads, and custom-tool test now executes unsaved configs through a fakeable HTTP boundary returning `{ status, body }`. Focused B11.1c handler tests, Captain/Copilot service tests, Captain repository tests, handler/service package tests, escalated full `go test ./...`, and `git diff --check` passed. Next slice is B11.2 Copilot persistence and safe LLM/document gates.
|
||||
- 2026-06-05: B11.2 Copilot thread/message checkpoint prepared as `feat(copilot): align thread message payloads`; Copilot threads now accept Chatwoot `{ message, assistant_id, conversation_id }`, validate account-scoped assistants, create initial user plus safe no-LLM assistant messages, and return raw thread serializers. Nested Copilot messages now list/create raw message payloads with embedded thread push data, current account/user scoping, ascending message order, and no local envelopes. Focused Copilot handler/service/repository tests, Captain/Copilot handler/service tests, handler/service/repository package tests, escalated full `go test ./...`, and `git diff --check` passed. Next slice is B11.3 document sync/indexing, Meilisearch/embedding gates, Copilot tasks/preferences/tool-call depth, and streaming/realtime compatibility.
|
||||
- 2026-06-05: B11.3a Captain preferences checkpoint prepared as `feat(captain): align preferences payloads`; Captain preferences now use Chatwoot's raw `{ providers, models, features }` show/update payload, persist `captain_models`/`captain_features` on accounts, merge partial preference updates, reject invalid model selections with `422`, gate updates to administrators, and expose exact no-trailing-slash GET/PUT preference routes. Focused CaptainPreference handler/service tests, Captain/Copilot handler/service tests, handler/service/model/router package tests, migration tests, route dump/parity regeneration, escalated full `go test ./...`, and `git diff --check` passed. Next slice is B11.3b document sync/indexing, Meilisearch/embedding gates, Copilot tasks/tool-call depth, and streaming/realtime compatibility.
|
||||
|
||||
@@ -24,7 +24,6 @@ DELETE /api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/:
|
||||
DELETE /api/v1/accounts/:account_id/captain/copilot_threads/:thread_id
|
||||
DELETE /api/v1/accounts/:account_id/captain/custom_tools/:tool_id
|
||||
DELETE /api/v1/accounts/:account_id/captain/documents/:document_id
|
||||
DELETE /api/v1/accounts/:account_id/captain/preferences/
|
||||
DELETE /api/v1/accounts/:account_id/captain/scenarios/:scenario_id
|
||||
DELETE /api/v1/accounts/:account_id/channels/facebook_channel/:fb_id
|
||||
DELETE /api/v1/accounts/:account_id/companies/:company_id
|
||||
@@ -169,6 +168,7 @@ GET /api/v1/accounts/:account_id/captain/custom_tools/
|
||||
GET /api/v1/accounts/:account_id/captain/custom_tools/:tool_id
|
||||
GET /api/v1/accounts/:account_id/captain/documents/
|
||||
GET /api/v1/accounts/:account_id/captain/documents/:document_id
|
||||
GET /api/v1/accounts/:account_id/captain/preferences
|
||||
GET /api/v1/accounts/:account_id/captain/preferences/
|
||||
GET /api/v1/accounts/:account_id/captain/scenarios/
|
||||
GET /api/v1/accounts/:account_id/captain/scenarios/:scenario_id
|
||||
@@ -535,7 +535,6 @@ POST /api/v1/accounts/:account_id/captain/custom_tools/
|
||||
POST /api/v1/accounts/:account_id/captain/custom_tools/test
|
||||
POST /api/v1/accounts/:account_id/captain/documents/
|
||||
POST /api/v1/accounts/:account_id/captain/documents/:document_id/sync
|
||||
POST /api/v1/accounts/:account_id/captain/preferences/
|
||||
POST /api/v1/accounts/:account_id/captain/scenarios/
|
||||
POST /api/v1/accounts/:account_id/captain/tasks/follow_up
|
||||
POST /api/v1/accounts/:account_id/captain/tasks/label_suggestion
|
||||
@@ -771,6 +770,7 @@ PUT /api/v1/accounts/:account_id/captain/assistant_responses/:response_id
|
||||
PUT /api/v1/accounts/:account_id/captain/assistants/:assistant_id
|
||||
PUT /api/v1/accounts/:account_id/captain/assistants/:assistant_id/scenarios/:scenario_id
|
||||
PUT /api/v1/accounts/:account_id/captain/custom_tools/:tool_id
|
||||
PUT /api/v1/accounts/:account_id/captain/preferences
|
||||
PUT /api/v1/accounts/:account_id/captain/preferences/
|
||||
PUT /api/v1/accounts/:account_id/captain/scenarios/:scenario_id
|
||||
PUT /api/v1/accounts/:account_id/companies/:company_id
|
||||
|
||||
@@ -567,7 +567,7 @@ func Bootstrap(env string) (*App, error) {
|
||||
copilotContextService := service.NewCopilotContextService(messageRepo, conversationRepo, contactRepo, llmProvider)
|
||||
captainTaskService := service.NewCaptainTaskService(captainAssistantRepo, captainAssistantResponseRepo, captainCustomToolRepo, conversationRepo, messageRepo, llmProvider, copilotContextService)
|
||||
conversationInsightService := service.NewConversationInsightService(conversationRepo, messageRepo, captainAssistantRepo, llmProvider)
|
||||
captainPreferenceService := service.NewCaptainPreferenceService(captainPreferenceRepo)
|
||||
captainPreferenceService := service.NewCaptainPreferenceService(captainPreferenceRepo, accountRepo)
|
||||
captainTaskExtendedService := service.NewCaptainTaskExtendedService(conversationRepo, messageRepo, captainAssistantRepo, captainPreferenceRepo, llmProvider)
|
||||
captainAssistantResponseService := service.NewCaptainAssistantResponseService(captainAssistantRepo, captainAssistantResponseRepo, conversationRepo, messageRepo, captainPreferenceRepo, llmProvider)
|
||||
captainBulkActionService := service.NewCaptainBulkActionService(conversationRepo, messageRepo, captainAssistantRepo, captainPreferenceRepo, llmProvider, captainTaskExtendedService, captainAssistantResponseService)
|
||||
|
||||
@@ -2,7 +2,6 @@ package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
@@ -23,8 +22,8 @@ func NewCaptainPreferenceHandler(svc *service.CaptainPreferenceService) *Captain
|
||||
// Create creates a new captain preference for an account.
|
||||
// POST /api/v1/accounts/:id/captain/preferences
|
||||
func (h *CaptainPreferenceHandler) Create(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
@@ -35,7 +34,7 @@ func (h *CaptainPreferenceHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
pref, err := h.svc.Create(c.Request.Context(), uint(accountID), &req)
|
||||
pref, err := h.svc.Create(c.Request.Context(), accountID, &req)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Create captain preference: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create preference")
|
||||
@@ -48,57 +47,61 @@ func (h *CaptainPreferenceHandler) Create(c *gin.Context) {
|
||||
// Get retrieves the captain preference for an account.
|
||||
// GET /api/v1/accounts/:id/captain/preferences
|
||||
func (h *CaptainPreferenceHandler) Get(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
|
||||
pref, err := h.svc.Get(c.Request.Context(), uint(accountID))
|
||||
pref, err := h.svc.GetConfig(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Get captain preference: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "preference not found")
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, pref)
|
||||
c.JSON(http.StatusOK, pref)
|
||||
}
|
||||
|
||||
// Update updates the captain preference for an account.
|
||||
// PUT /api/v1/accounts/:id/captain/preferences
|
||||
func (h *CaptainPreferenceHandler) Update(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
if !captainPreferencesCanUpdate(c) {
|
||||
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "administrator role required")
|
||||
return
|
||||
}
|
||||
|
||||
var req service.UpdatePreferenceRequest
|
||||
var req service.UpdateCaptainConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
pref, err := h.svc.Update(c.Request.Context(), uint(accountID), &req)
|
||||
pref, err := h.svc.UpdateConfig(c.Request.Context(), accountID, &req)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Update captain preference: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to update preference")
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, pref)
|
||||
c.JSON(http.StatusOK, pref)
|
||||
}
|
||||
|
||||
// Delete removes the captain preference for an account.
|
||||
// DELETE /api/v1/accounts/:id/captain/preferences
|
||||
func (h *CaptainPreferenceHandler) Delete(c *gin.Context) {
|
||||
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
accountID := parseAccountIDParam(c)
|
||||
if accountID == 0 {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.svc.Delete(c.Request.Context(), uint(accountID)); err != nil {
|
||||
if err := h.svc.Delete(c.Request.Context(), accountID); err != nil {
|
||||
applogger.L().Errorf("Delete captain preference: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to delete preference")
|
||||
return
|
||||
@@ -106,3 +109,8 @@ func (h *CaptainPreferenceHandler) Delete(c *gin.Context) {
|
||||
|
||||
response.OK(c, gin.H{"message": "preference deleted"})
|
||||
}
|
||||
|
||||
func captainPreferencesCanUpdate(c *gin.Context) bool {
|
||||
role := getRole(c)
|
||||
return role == "administrator" || role == "super_admin"
|
||||
}
|
||||
|
||||
@@ -3,131 +3,149 @@ package v1
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type CaptainPreferenceHandlerTestSuite struct {
|
||||
suite.Suite
|
||||
type captainPreferenceFixture struct {
|
||||
db *gorm.DB
|
||||
handler *CaptainPreferenceHandler
|
||||
router *gin.Engine
|
||||
account *model.Account
|
||||
}
|
||||
|
||||
func (s *CaptainPreferenceHandlerTestSuite) SetupSuite() {
|
||||
func newCaptainPreferenceFixture(t *testing.T) *captainPreferenceFixture {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.CaptainPreference{}))
|
||||
|
||||
account := &model.Account{Name: "Captain Preferences", Active: true, CaptainModels: datatypes.JSON(`{}`), CaptainFeatures: datatypes.JSON(`{}`)}
|
||||
require.NoError(t, db.Create(account).Error)
|
||||
|
||||
prefRepo := repository.NewCaptainPreferenceRepo(db)
|
||||
accountRepo := repository.NewAccountRepo(db)
|
||||
handler := NewCaptainPreferenceHandler(service.NewCaptainPreferenceService(prefRepo, accountRepo))
|
||||
router := gin.New()
|
||||
router.GET("/api/v1/accounts/:account_id/captain/preferences", handler.Get)
|
||||
router.PUT("/api/v1/accounts/:account_id/captain/preferences", func(c *gin.Context) {
|
||||
c.Set("role", "administrator")
|
||||
handler.Update(c)
|
||||
})
|
||||
router.PUT("/api/v1/accounts/:account_id/captain/preferences/as-agent", func(c *gin.Context) {
|
||||
c.Set("role", "agent")
|
||||
handler.Update(c)
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
s.Require().NoError(db.AutoMigrate(&model.Account{}, &model.CaptainPreference{}))
|
||||
s.db = db
|
||||
|
||||
repo := repository.NewCaptainPreferenceRepo(db)
|
||||
svc := service.NewCaptainPreferenceService(repo)
|
||||
s.handler = NewCaptainPreferenceHandler(svc)
|
||||
|
||||
s.account = &model.Account{Name: "test-captain-pref-account"}
|
||||
s.Require().NoError(db.Create(s.account).Error)
|
||||
t.Cleanup(func() {
|
||||
sqlDB, dbErr := db.DB()
|
||||
require.NoError(t, dbErr)
|
||||
require.NoError(t, sqlDB.Close())
|
||||
})
|
||||
return &captainPreferenceFixture{db: db, router: router, account: account}
|
||||
}
|
||||
|
||||
func (s *CaptainPreferenceHandlerTestSuite) TearDownSuite() {
|
||||
if s.db != nil {
|
||||
sqlDB, _ := s.db.DB()
|
||||
sqlDB.Close()
|
||||
func (f *captainPreferenceFixture) path(suffix string) string {
|
||||
return "/api/v1/accounts/" + strconv.FormatUint(uint64(f.account.ID), 10) + "/captain/preferences" + suffix
|
||||
}
|
||||
|
||||
func (f *captainPreferenceFixture) request(method, path string, body any) *httptest.ResponseRecorder {
|
||||
var raw []byte
|
||||
if body != nil {
|
||||
raw, _ = json.Marshal(body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptainPreferenceHandlerSuite(t *testing.T) {
|
||||
suite.Run(t, new(CaptainPreferenceHandlerTestSuite))
|
||||
}
|
||||
|
||||
func (s *CaptainPreferenceHandlerTestSuite) TestCreate_BadRequest_InvalidAccountID() {
|
||||
r := gin.New()
|
||||
r.POST("/api/v1/accounts/:id/captain/preferences", s.handler.Create)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/captain/preferences", nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func (s *CaptainPreferenceHandlerTestSuite) TestCreate_BadRequest_EmptyBody() {
|
||||
r := gin.New()
|
||||
r.POST("/api/v1/accounts/:id/captain/preferences", s.handler.Create)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/captain/preferences", s.account.ID), nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func (s *CaptainPreferenceHandlerTestSuite) TestCreate_Success() {
|
||||
r := gin.New()
|
||||
r.POST("/api/v1/accounts/:id/captain/preferences", s.handler.Create)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"tone": "professional",
|
||||
"language": "en",
|
||||
"auto_label_enabled": true,
|
||||
"auto_follow_up_enabled": false,
|
||||
recorder := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(method, path, bytes.NewReader(raw))
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
b, _ := json.Marshal(body)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/captain/preferences", s.account.ID), bytes.NewBuffer(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
f.router.ServeHTTP(recorder, req)
|
||||
return recorder
|
||||
}
|
||||
|
||||
func (s *CaptainPreferenceHandlerTestSuite) TestGet_BadRequest_InvalidAccountID() {
|
||||
r := gin.New()
|
||||
r.GET("/api/v1/accounts/:id/captain/preferences", s.handler.Get)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/captain/preferences", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
func decodeCaptainPreferencePayload(t *testing.T, recorder *httptest.ResponseRecorder) map[string]any {
|
||||
t.Helper()
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &payload))
|
||||
return payload
|
||||
}
|
||||
|
||||
func (s *CaptainPreferenceHandlerTestSuite) TestUpdate_BadRequest_InvalidAccountID() {
|
||||
r := gin.New()
|
||||
r.PUT("/api/v1/accounts/:id/captain/preferences", s.handler.Update)
|
||||
func TestCaptainPreferencesGetReturnsRawChatwootConfig(t *testing.T) {
|
||||
f := newCaptainPreferenceFixture(t)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("PUT", "/api/v1/accounts/abc/captain/preferences", nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
w := f.request(http.MethodGet, f.path(""), nil)
|
||||
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
||||
payload := decodeCaptainPreferencePayload(t, w)
|
||||
require.Nil(t, payload["success"])
|
||||
require.Contains(t, payload, "providers")
|
||||
require.Contains(t, payload, "models")
|
||||
require.Contains(t, payload, "features")
|
||||
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
features := payload["features"].(map[string]any)
|
||||
editor := features["editor"].(map[string]any)
|
||||
require.Equal(t, false, editor["enabled"])
|
||||
require.Equal(t, "gpt-4.1-mini", editor["default"])
|
||||
require.Equal(t, "gpt-4.1-mini", editor["selected"])
|
||||
require.NotEmpty(t, editor["models"].([]any))
|
||||
}
|
||||
|
||||
func (s *CaptainPreferenceHandlerTestSuite) TestDelete_BadRequest_InvalidAccountID() {
|
||||
r := gin.New()
|
||||
r.DELETE("/api/v1/accounts/:id/captain/preferences", s.handler.Delete)
|
||||
func TestCaptainPreferencesUpdateMergesAccountModelsAndFeatures(t *testing.T) {
|
||||
f := newCaptainPreferenceFixture(t)
|
||||
require.NoError(t, f.db.Model(&model.Account{}).Where("id = ?", f.account.ID).Updates(map[string]any{
|
||||
"captain_models": datatypes.JSON(`{"editor":"gpt-4.1-mini","assistant":"gpt-5.1"}`),
|
||||
"captain_features": datatypes.JSON(`{"editor":true,"assistant":false}`),
|
||||
}).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/abc/captain/preferences", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
w := f.request(http.MethodPut, f.path(""), map[string]any{
|
||||
"captain_models": map[string]any{"editor": "gpt-4.1"},
|
||||
"captain_features": map[string]any{"editor": false},
|
||||
})
|
||||
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
||||
payload := decodeCaptainPreferencePayload(t, w)
|
||||
features := payload["features"].(map[string]any)
|
||||
require.Equal(t, "gpt-4.1", features["editor"].(map[string]any)["selected"])
|
||||
require.Equal(t, false, features["editor"].(map[string]any)["enabled"])
|
||||
require.Equal(t, "gpt-5.1", features["assistant"].(map[string]any)["selected"])
|
||||
require.Equal(t, false, features["assistant"].(map[string]any)["enabled"])
|
||||
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
}
|
||||
var account model.Account
|
||||
require.NoError(t, f.db.First(&account, f.account.ID).Error)
|
||||
var models map[string]string
|
||||
var featureValues map[string]bool
|
||||
require.NoError(t, json.Unmarshal(account.CaptainModels, &models))
|
||||
require.NoError(t, json.Unmarshal(account.CaptainFeatures, &featureValues))
|
||||
require.Equal(t, "gpt-4.1", models["editor"])
|
||||
require.Equal(t, "gpt-5.1", models["assistant"])
|
||||
require.False(t, featureValues["editor"])
|
||||
require.False(t, featureValues["assistant"])
|
||||
}
|
||||
|
||||
func TestCaptainPreferencesUpdateRejectsNonAdminAndInvalidModel(t *testing.T) {
|
||||
f := newCaptainPreferenceFixture(t)
|
||||
|
||||
w := f.request(http.MethodPut, f.path("/as-agent"), map[string]any{"captain_models": map[string]any{"editor": "gpt-4.1"}})
|
||||
require.Equal(t, http.StatusUnauthorized, w.Code, w.Body.String())
|
||||
|
||||
w = f.request(http.MethodPut, f.path(""), map[string]any{"captain_models": map[string]any{"editor": "not-a-model"}})
|
||||
require.Equal(t, http.StatusUnprocessableEntity, w.Code, w.Body.String())
|
||||
require.Contains(t, decodeCaptainPreferencePayload(t, w)["error"], "not a valid model")
|
||||
}
|
||||
|
||||
func TestCaptainPreferencesInvalidAccountID(t *testing.T) {
|
||||
f := newCaptainPreferenceFixture(t)
|
||||
|
||||
w := f.request(http.MethodGet, "/api/v1/accounts/abc/captain/preferences", nil)
|
||||
require.Equal(t, http.StatusBadRequest, w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
+15
-11
@@ -1,19 +1,23 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/datatypes"
|
||||
|
||||
// Account represents a tenant/organization in the system.
|
||||
type Account struct {
|
||||
Base
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Domain string `gorm:"size:255" json:"domain,omitempty"`
|
||||
Locale string `gorm:"size:10;default:en" json:"locale"`
|
||||
Timezone string `gorm:"size:50;default:UTC" json:"timezone"`
|
||||
Active bool `gorm:"default:true" json:"active"`
|
||||
Status string `gorm:"size:50;default:active" json:"status"`
|
||||
OnboardingStep string `gorm:"size:100" json:"onboarding_step,omitempty"`
|
||||
FeatureFlags string `gorm:"type:text" json:"feature_flags,omitempty"` // JSON-encoded feature flags
|
||||
AutoResolveDuration int `gorm:"default:0" json:"auto_resolve_duration,omitempty"` // days
|
||||
AgentLimit int `gorm:"default:0" json:"agent_limit,omitempty"` // max agents allowed (0 = unlimited), Chatwoot usage_limits[:agents]
|
||||
InboxLimit int `gorm:"default:0" json:"inbox_limit,omitempty"` // max inboxes allowed (0 = unlimited), Chatwoot usage_limits[:inboxes]
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Domain string `gorm:"size:255" json:"domain,omitempty"`
|
||||
Locale string `gorm:"size:10;default:en" json:"locale"`
|
||||
Timezone string `gorm:"size:50;default:UTC" json:"timezone"`
|
||||
Active bool `gorm:"default:true" json:"active"`
|
||||
Status string `gorm:"size:50;default:active" json:"status"`
|
||||
OnboardingStep string `gorm:"size:100" json:"onboarding_step,omitempty"`
|
||||
FeatureFlags string `gorm:"type:text" json:"feature_flags,omitempty"` // JSON-encoded feature flags
|
||||
AutoResolveDuration int `gorm:"default:0" json:"auto_resolve_duration,omitempty"` // days
|
||||
AgentLimit int `gorm:"default:0" json:"agent_limit,omitempty"` // max agents allowed (0 = unlimited), Chatwoot usage_limits[:agents]
|
||||
InboxLimit int `gorm:"default:0" json:"inbox_limit,omitempty"` // max inboxes allowed (0 = unlimited), Chatwoot usage_limits[:inboxes]
|
||||
CaptainModels datatypes.JSON `gorm:"type:jsonb;default:'{}'" json:"captain_models,omitempty"`
|
||||
CaptainFeatures datatypes.JSON `gorm:"type:jsonb;default:'{}'" json:"captain_features,omitempty"`
|
||||
}
|
||||
|
||||
func (Account) TableName() string { return "accounts" }
|
||||
|
||||
@@ -1265,10 +1265,10 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) {
|
||||
// Reference: Chatwoot Captain::PreferencesController
|
||||
preferences := captain.Group("/preferences")
|
||||
{
|
||||
preferences.POST("/", h.CaptainPreference.Create)
|
||||
preferences.GET("", h.CaptainPreference.Get)
|
||||
preferences.GET("/", h.CaptainPreference.Get)
|
||||
preferences.PUT("", h.CaptainPreference.Update)
|
||||
preferences.PUT("/", h.CaptainPreference.Update)
|
||||
preferences.DELETE("/", h.CaptainPreference.Delete)
|
||||
}
|
||||
|
||||
// M12: Label Suggestion + Follow Up (GET endpoints)
|
||||
|
||||
@@ -2,22 +2,30 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
pkgvalidator "github.com/gochat/gochat/pkg/validator"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
pkgvalidator "github.com/gochat/gochat/pkg/validator"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// CaptainPreferenceService implements business logic for CaptainPreference operations.
|
||||
// Reference: Chatwoot enterprise/app/controllers/api/v1/captain/preferences_controller.rb
|
||||
type CaptainPreferenceService struct {
|
||||
repo *repository.CaptainPreferenceRepo
|
||||
repo *repository.CaptainPreferenceRepo
|
||||
accountRepo *repository.AccountRepo
|
||||
}
|
||||
|
||||
func NewCaptainPreferenceService(repo *repository.CaptainPreferenceRepo) *CaptainPreferenceService {
|
||||
return &CaptainPreferenceService{repo: repo}
|
||||
func NewCaptainPreferenceService(repo *repository.CaptainPreferenceRepo, accountRepo ...*repository.AccountRepo) *CaptainPreferenceService {
|
||||
svc := &CaptainPreferenceService{repo: repo}
|
||||
if len(accountRepo) > 0 {
|
||||
svc.accountRepo = accountRepo[0]
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
// --- Request DTOs ---
|
||||
@@ -25,29 +33,106 @@ func NewCaptainPreferenceService(repo *repository.CaptainPreferenceRepo) *Captai
|
||||
// CreatePreferenceRequest is the DTO for creating a preference.
|
||||
// Reference: Chatwoot enterprise/app/models/captain/preference.rb
|
||||
type CreatePreferenceRequest struct {
|
||||
Tone string `json:"tone" validate:"omitempty,oneof=professional casual friendly formal"`
|
||||
Language string `json:"language" validate:"omitempty,min=1,max=10"`
|
||||
ResponseGuidelines string `json:"response_guidelines" validate:"omitempty,max=2000"`
|
||||
AutoLabelEnabled bool `json:"auto_label_enabled"`
|
||||
AutoFollowUpEnabled bool `json:"auto_follow_up_enabled"`
|
||||
AutoReplyEnabled bool `json:"auto_reply_enabled"`
|
||||
MaxResponseLength int `json:"max_response_length" validate:"omitempty,min=50,max=5000"`
|
||||
CustomPromptSuffix string `json:"custom_prompt_suffix" validate:"omitempty,max=1000"`
|
||||
Tone string `json:"tone" validate:"omitempty,oneof=professional casual friendly formal"`
|
||||
Language string `json:"language" validate:"omitempty,min=1,max=10"`
|
||||
ResponseGuidelines string `json:"response_guidelines" validate:"omitempty,max=2000"`
|
||||
AutoLabelEnabled bool `json:"auto_label_enabled"`
|
||||
AutoFollowUpEnabled bool `json:"auto_follow_up_enabled"`
|
||||
AutoReplyEnabled bool `json:"auto_reply_enabled"`
|
||||
MaxResponseLength int `json:"max_response_length" validate:"omitempty,min=50,max=5000"`
|
||||
CustomPromptSuffix string `json:"custom_prompt_suffix" validate:"omitempty,max=1000"`
|
||||
}
|
||||
|
||||
// UpdatePreferenceRequest is the DTO for updating a preference.
|
||||
// Reference: Chatwoot enterprise/app/models/captain/preference.rb
|
||||
type UpdatePreferenceRequest struct {
|
||||
Tone string `json:"tone,omitempty" validate:"omitempty,oneof=professional casual friendly formal"`
|
||||
Language string `json:"language,omitempty" validate:"omitempty,min=1,max=10"`
|
||||
ResponseGuidelines string `json:"response_guidelines,omitempty" validate:"omitempty,max=2000"`
|
||||
AutoLabelEnabled *bool `json:"auto_label_enabled,omitempty"`
|
||||
AutoFollowUpEnabled *bool `json:"auto_follow_up_enabled,omitempty"`
|
||||
AutoReplyEnabled *bool `json:"auto_reply_enabled,omitempty"`
|
||||
MaxResponseLength *int `json:"max_response_length,omitempty" validate:"omitempty,min=50,max=5000"`
|
||||
CustomPromptSuffix string `json:"custom_prompt_suffix,omitempty" validate:"omitempty,max=1000"`
|
||||
Tone string `json:"tone,omitempty" validate:"omitempty,oneof=professional casual friendly formal"`
|
||||
Language string `json:"language,omitempty" validate:"omitempty,min=1,max=10"`
|
||||
ResponseGuidelines string `json:"response_guidelines,omitempty" validate:"omitempty,max=2000"`
|
||||
AutoLabelEnabled *bool `json:"auto_label_enabled,omitempty"`
|
||||
AutoFollowUpEnabled *bool `json:"auto_follow_up_enabled,omitempty"`
|
||||
AutoReplyEnabled *bool `json:"auto_reply_enabled,omitempty"`
|
||||
MaxResponseLength *int `json:"max_response_length,omitempty" validate:"omitempty,min=50,max=5000"`
|
||||
CustomPromptSuffix string `json:"custom_prompt_suffix,omitempty" validate:"omitempty,max=1000"`
|
||||
}
|
||||
|
||||
// UpdateCaptainConfigRequest matches Chatwoot's Captain::PreferencesController params.
|
||||
type UpdateCaptainConfigRequest struct {
|
||||
CaptainModels map[string]string `json:"captain_models"`
|
||||
CaptainFeatures map[string]bool `json:"captain_features"`
|
||||
}
|
||||
|
||||
// CaptainConfigPayload is the raw payload returned by Chatwoot preferences show/update.
|
||||
type CaptainConfigPayload struct {
|
||||
Providers map[string]map[string]string `json:"providers"`
|
||||
Models map[string]CaptainModelConfig `json:"models"`
|
||||
Features map[string]CaptainFeatureConfig `json:"features"`
|
||||
}
|
||||
|
||||
type CaptainModelConfig struct {
|
||||
Provider string `json:"provider"`
|
||||
DisplayName string `json:"display_name"`
|
||||
ComingSoon *bool `json:"coming_soon,omitempty"`
|
||||
CreditMultiplier int `json:"credit_multiplier"`
|
||||
}
|
||||
|
||||
type CaptainFeatureConfig struct {
|
||||
Models []CaptainFeatureModel `json:"models"`
|
||||
Default string `json:"default"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Selected string `json:"selected"`
|
||||
}
|
||||
|
||||
type CaptainFeatureModel struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Provider string `json:"provider"`
|
||||
ComingSoon *bool `json:"coming_soon"`
|
||||
CreditMultiplier int `json:"credit_multiplier"`
|
||||
}
|
||||
|
||||
var captainProviders = map[string]map[string]string{
|
||||
"openai": {"display_name": "OpenAI"},
|
||||
"anthropic": {"display_name": "Anthropic"},
|
||||
"gemini": {"display_name": "Gemini"},
|
||||
}
|
||||
|
||||
var captainModels = map[string]CaptainModelConfig{
|
||||
"gpt-4.1": {Provider: "openai", DisplayName: "GPT-4.1", CreditMultiplier: 3},
|
||||
"gpt-4.1-mini": {Provider: "openai", DisplayName: "GPT-4.1 Mini", CreditMultiplier: 1},
|
||||
"gpt-4.1-nano": {Provider: "openai", DisplayName: "GPT-4.1 Nano", CreditMultiplier: 1},
|
||||
"gpt-5.1": {Provider: "openai", DisplayName: "GPT-5.1", CreditMultiplier: 2},
|
||||
"gpt-5-mini": {Provider: "openai", DisplayName: "GPT-5 Mini", CreditMultiplier: 1},
|
||||
"gpt-5-nano": {Provider: "openai", DisplayName: "GPT-5 Nano", CreditMultiplier: 1},
|
||||
"gpt-5.2": {Provider: "openai", DisplayName: "GPT-5.2", CreditMultiplier: 3},
|
||||
"claude-haiku-4.5": {Provider: "anthropic", DisplayName: "Claude Haiku 4.5", ComingSoon: boolPtr(true), CreditMultiplier: 2},
|
||||
"claude-sonnet-4.5": {Provider: "anthropic", DisplayName: "Claude Sonnet 4.5", ComingSoon: boolPtr(true), CreditMultiplier: 3},
|
||||
"gemini-3-flash": {Provider: "gemini", DisplayName: "Gemini 3 Flash", ComingSoon: boolPtr(true), CreditMultiplier: 1},
|
||||
"gemini-3-pro": {Provider: "gemini", DisplayName: "Gemini 3 Pro", ComingSoon: boolPtr(true), CreditMultiplier: 3},
|
||||
"whisper-1": {Provider: "openai", DisplayName: "Whisper", CreditMultiplier: 1},
|
||||
"text-embedding-3-small": {Provider: "openai", DisplayName: "Text Embedding 3 Small", CreditMultiplier: 1},
|
||||
}
|
||||
|
||||
var captainFeatureModels = map[string][]string{
|
||||
"editor": {"gpt-4.1-mini", "gpt-4.1-nano", "gpt-5-mini", "gpt-4.1", "gpt-5.1", "gpt-5.2", "claude-haiku-4.5", "gemini-3-flash", "gemini-3-pro"},
|
||||
"assistant": {"gpt-5-mini", "gpt-4.1", "gpt-5.1", "gpt-5.2", "claude-haiku-4.5", "claude-sonnet-4.5", "gemini-3-flash", "gemini-3-pro"},
|
||||
"copilot": {"gpt-5-mini", "gpt-4.1", "gpt-5.1", "gpt-5.2", "claude-haiku-4.5", "claude-sonnet-4.5", "gemini-3-flash", "gemini-3-pro"},
|
||||
"label_suggestion": {"gpt-4.1-nano", "gpt-4.1-mini", "gpt-5-mini", "gemini-3-flash", "claude-haiku-4.5"},
|
||||
"audio_transcription": {"whisper-1"},
|
||||
"help_center_search": {"text-embedding-3-small"},
|
||||
}
|
||||
|
||||
var captainFeatureDefaults = map[string]string{
|
||||
"editor": "gpt-4.1-mini",
|
||||
"assistant": "gpt-5.1",
|
||||
"copilot": "gpt-5.1",
|
||||
"label_suggestion": "gpt-4.1-nano",
|
||||
"audio_transcription": "whisper-1",
|
||||
"help_center_search": "text-embedding-3-small",
|
||||
}
|
||||
|
||||
var captainFeatureOrder = []string{"editor", "assistant", "copilot", "label_suggestion", "audio_transcription", "help_center_search"}
|
||||
|
||||
// --- Business logic ---
|
||||
|
||||
// validTones are the allowed tone values for captain preferences.
|
||||
@@ -64,6 +149,138 @@ func isValidTone(tone string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetConfig returns Chatwoot's raw Captain preferences payload for an account.
|
||||
func (s *CaptainPreferenceService) GetConfig(ctx context.Context, accountID uint) (*CaptainConfigPayload, error) {
|
||||
account, err := s.findAccount(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return captainConfigPayload(account), nil
|
||||
}
|
||||
|
||||
// UpdateConfig merges captain_models/captain_features into account settings and returns the raw payload.
|
||||
func (s *CaptainPreferenceService) UpdateConfig(ctx context.Context, accountID uint, req *UpdateCaptainConfigRequest) (*CaptainConfigPayload, error) {
|
||||
account, err := s.findAccount(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
models := jsonMapString(account.CaptainModels)
|
||||
features := jsonMapBool(account.CaptainFeatures)
|
||||
|
||||
for key, value := range req.CaptainModels {
|
||||
if !isCaptainFeature(key) {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(value) == "" {
|
||||
models[key] = ""
|
||||
continue
|
||||
}
|
||||
if !validCaptainModelFor(key, value) {
|
||||
return nil, fmt.Errorf("'%s' is not a valid model for %s. Allowed: %s", value, key, strings.Join(captainFeatureModels[key], ", "))
|
||||
}
|
||||
models[key] = value
|
||||
}
|
||||
for key, value := range req.CaptainFeatures {
|
||||
if !isCaptainFeature(key) {
|
||||
continue
|
||||
}
|
||||
features[key] = value
|
||||
}
|
||||
|
||||
account.CaptainModels = marshalJSONMap(models)
|
||||
account.CaptainFeatures = marshalJSONMap(features)
|
||||
if err := s.accountRepo.Update(ctx, account); err != nil {
|
||||
return nil, fmt.Errorf("update captain preferences: %w", err)
|
||||
}
|
||||
return captainConfigPayload(account), nil
|
||||
}
|
||||
|
||||
func (s *CaptainPreferenceService) findAccount(ctx context.Context, accountID uint) (*model.Account, error) {
|
||||
if s.accountRepo == nil {
|
||||
return nil, fmt.Errorf("account repository is required for captain preferences")
|
||||
}
|
||||
account, err := s.accountRepo.FindByID(ctx, accountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("account not found: %w", err)
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func captainConfigPayload(account *model.Account) *CaptainConfigPayload {
|
||||
accountModels := jsonMapString(account.CaptainModels)
|
||||
accountFeatures := jsonMapBool(account.CaptainFeatures)
|
||||
features := make(map[string]CaptainFeatureConfig, len(captainFeatureOrder))
|
||||
for _, key := range captainFeatureOrder {
|
||||
models := make([]CaptainFeatureModel, 0, len(captainFeatureModels[key]))
|
||||
for _, modelName := range captainFeatureModels[key] {
|
||||
modelConfig := captainModels[modelName]
|
||||
models = append(models, CaptainFeatureModel{
|
||||
ID: modelName,
|
||||
DisplayName: modelConfig.DisplayName,
|
||||
Provider: modelConfig.Provider,
|
||||
ComingSoon: modelConfig.ComingSoon,
|
||||
CreditMultiplier: modelConfig.CreditMultiplier,
|
||||
})
|
||||
}
|
||||
selected := accountModels[key]
|
||||
if !validCaptainModelFor(key, selected) {
|
||||
selected = captainFeatureDefaults[key]
|
||||
}
|
||||
features[key] = CaptainFeatureConfig{
|
||||
Models: models,
|
||||
Default: captainFeatureDefaults[key],
|
||||
Enabled: accountFeatures[key],
|
||||
Selected: selected,
|
||||
}
|
||||
}
|
||||
return &CaptainConfigPayload{Providers: captainProviders, Models: captainModels, Features: features}
|
||||
}
|
||||
|
||||
func isCaptainFeature(key string) bool {
|
||||
_, ok := captainFeatureModels[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func validCaptainModelFor(feature, modelName string) bool {
|
||||
if modelName == "" {
|
||||
return false
|
||||
}
|
||||
for _, allowed := range captainFeatureModels[feature] {
|
||||
if allowed == modelName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func jsonMapString(raw datatypes.JSON) map[string]string {
|
||||
result := map[string]string{}
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return result
|
||||
}
|
||||
_ = json.Unmarshal(raw, &result)
|
||||
return result
|
||||
}
|
||||
|
||||
func jsonMapBool(raw datatypes.JSON) map[string]bool {
|
||||
result := map[string]bool{}
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return result
|
||||
}
|
||||
_ = json.Unmarshal(raw, &result)
|
||||
return result
|
||||
}
|
||||
|
||||
func marshalJSONMap(value any) datatypes.JSON {
|
||||
raw, _ := json.Marshal(value)
|
||||
return datatypes.JSON(raw)
|
||||
}
|
||||
|
||||
func boolPtr(value bool) *bool {
|
||||
return &value
|
||||
}
|
||||
|
||||
// Create creates a new preference for an account.
|
||||
func (s *CaptainPreferenceService) Create(ctx context.Context, accountID uint, req *CreatePreferenceRequest) (*model.CaptainPreference, error) {
|
||||
// Validate request
|
||||
@@ -78,15 +295,15 @@ func (s *CaptainPreferenceService) Create(ctx context.Context, accountID uint, r
|
||||
}
|
||||
|
||||
pref := &model.CaptainPreference{
|
||||
AccountID: accountID,
|
||||
Tone: req.Tone,
|
||||
Language: req.Language,
|
||||
ResponseGuidelines: req.ResponseGuidelines,
|
||||
AutoLabelEnabled: req.AutoLabelEnabled,
|
||||
AccountID: accountID,
|
||||
Tone: req.Tone,
|
||||
Language: req.Language,
|
||||
ResponseGuidelines: req.ResponseGuidelines,
|
||||
AutoLabelEnabled: req.AutoLabelEnabled,
|
||||
AutoFollowUpEnabled: req.AutoFollowUpEnabled,
|
||||
AutoReplyEnabled: req.AutoReplyEnabled,
|
||||
MaxResponseLength: req.MaxResponseLength,
|
||||
CustomPromptSuffix: req.CustomPromptSuffix,
|
||||
AutoReplyEnabled: req.AutoReplyEnabled,
|
||||
MaxResponseLength: req.MaxResponseLength,
|
||||
CustomPromptSuffix: req.CustomPromptSuffix,
|
||||
}
|
||||
|
||||
// Apply defaults
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE accounts
|
||||
DROP COLUMN IF EXISTS captain_features,
|
||||
DROP COLUMN IF EXISTS captain_models;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Chatwoot stores Captain model/feature preferences in account settings.
|
||||
|
||||
ALTER TABLE accounts
|
||||
ADD COLUMN IF NOT EXISTS captain_models JSONB NOT NULL DEFAULT '{}',
|
||||
ADD COLUMN IF NOT EXISTS captain_features JSONB NOT NULL DEFAULT '{}';
|
||||
Reference in New Issue
Block a user