From 18a2cc5e83243d0b8d1309cf69d45e43fd04430e Mon Sep 17 00:00:00 2001 From: Rogee Date: Sat, 6 Jun 2026 15:07:08 +0800 Subject: [PATCH] feat(conversations): expose inbox assistant --- cmd/route_parity/main.go | 1 + docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md | 26 +++++---- docs/parity/gochat_routes.txt | 3 +- docs/parity/route_parity.md | 3 +- .../handler/api/v1/conversation_handler.go | 28 ++++++++++ .../api/v1/conversation_handler_crud_test.go | 55 +++++++++++++++++++ internal/router/router.go | 1 + internal/service/conversation_service.go | 23 ++++++++ 8 files changed, 127 insertions(+), 13 deletions(-) diff --git a/cmd/route_parity/main.go b/cmd/route_parity/main.go index b0901b70..127a953a 100644 --- a/cmd/route_parity/main.go +++ b/cmd/route_parity/main.go @@ -102,6 +102,7 @@ var criticalRoutes = []route{ {Method: "POST", Path: "/api/v1/accounts/:account_id/conversations/:conversation_id/toggle_status", Controller: "api/v1/accounts/conversations#toggle_status", Source: "routes.rb:158"}, {Method: "POST", Path: "/api/v1/accounts/:account_id/conversations/:conversation_id/toggle_priority", Controller: "api/v1/accounts/conversations#toggle_priority", Source: "routes.rb:159"}, {Method: "POST", Path: "/api/v1/accounts/:account_id/conversations/:conversation_id/transcript", Controller: "api/v1/accounts/conversations#transcript", Source: "routes.rb:157"}, + {Method: "GET", Path: "/api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant", Controller: "api/v1/accounts/conversations#inbox_assistant", Source: "routes.rb:165"}, {Method: "GET", Path: "/api/v1/accounts/:account_id/conversations/:conversation_id/messages/", Controller: "api/v1/accounts/conversations/messages#index", Source: "routes.rb:142"}, {Method: "POST", Path: "/api/v1/accounts/:account_id/conversations/:conversation_id/messages/", Controller: "api/v1/accounts/conversations/messages#create", Source: "routes.rb:142"}, diff --git a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md index 860c9327..822c3c08 100644 --- a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md +++ b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md @@ -49,15 +49,15 @@ Hermes task landing checklist: ## Current Baseline -- Current tracking checkpoint: 2026-06-06 conversation direct uploads checkpoint, prepared as `feat(conversations): align direct upload routes`. -- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): align direct upload routes`. -- Latest documentation/tooling checkpoint: this tracker landing update plus `e8d08bb docs: track canned response parity`; this document is the active follow-up plan and supersedes `.hermes/plans/*`. +- Current tracking checkpoint: 2026-06-06 conversation inbox assistant checkpoint, prepared as `feat(conversations): expose inbox assistant`. +- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): expose inbox assistant`. +- Latest documentation/tooling checkpoint: this tracker update for P3.19 plus the landed parity tracker history; this document is the active follow-up plan and supersedes `.hermes/plans/*`. - Plan landing status: complete for the current known Hermes plans and user-confirmed scope. Future work should update this file directly instead of opening a parallel tracker. -- Worktree status at this implementation checkpoint: conversation direct uploads from `reference/chatwoot/config/routes.rb:151`, `DirectUploadsController`, and reused dashboard `useFileUpload.js`/`fileUploadMixin.js` are implemented for the ActiveStorage metadata + PUT upload flow. `POST /api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads` is now tracked and routed, the local `PUT /api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads/:upload_uuid` target stores the bytes, uploads are account-scoped, and conversation lookup follows Chatwoot display-ID semantics with legacy ID fallback. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack. +- Worktree status at this implementation checkpoint: conversation inbox assistant from `reference/chatwoot/config/routes.rb:165`, enterprise `Api::V1::Accounts::ConversationsController#inbox_assistant`, and reused dashboard `api/inbox/conversation.js` / conversation store callers is implemented. `GET /api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant` is now tracked and routed, resolves conversations by display ID with legacy ID fallback, joins inboxes to Captain assistants through account-scoped `captain_inboxes`, and returns `{ assistant: { id, name } }` or `{ assistant: null }` exactly for the frontend Copilot assistant state. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack. - `go test ./...` passes. -- Route dump succeeds with `TOTAL: 927` after adding conversation nested direct upload create and local PUT upload target routes. +- Route dump succeeds with `TOTAL: 928` after adding the conversation inbox assistant route. - Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`. -- Tracked frontend-critical route audit covers 380 Chatwoot routes: 367 exact, 0 method-compatible, 13 parameter-compatible, 0 missing. The 13 parameter-compatible routes are Gin-internal parameter-name differences for nested AgentCapacityPolicy users/inbox limits, dashboard app member `:id` names, plus the public article `.md`/`.png` suffixes served through the same external article route dispatcher. +- Tracked frontend-critical route audit covers 381 Chatwoot routes: 368 exact, 0 method-compatible, 13 parameter-compatible, 0 missing. The 13 parameter-compatible routes are Gin-internal parameter-name differences for nested AgentCapacityPolicy users/inbox limits, dashboard app member `:id` names, plus the public article `.md`/`.png` suffixes served through the same external article route dispatcher. - `/api/v1/widget` stubs are burned down and public inbox/contact/conversation/message core flows are backed by real handlers. - Handler test stability fixes are committed into the baseline before feature parity work continues. - `.codegraph/` is generated indexing output and is not part of tracked product code. @@ -140,7 +140,7 @@ This table is the shortest authoritative handoff view. If an older lower section | Priority | Workstream | Current state | Next checkpoint | Commit close rule | | --- | --- | --- | --- | --- | | 1 | P3.2a invitation/confirmation mail parity | Implemented for current non-SSO reference behavior: profile resend is no longer a TODO-only log, new invited agents and unconfirmed invited profile resends generate reset-password invitation links, normal unconfirmed profile resends generate confirmation links, `users.unconfirmed_email` is modeled for email-update routing, and fakeable/environment SMTP mailers keep default tests offline. | Keep in Review; reopen only if reused frontend smoke or fresh reference evidence exposes additional Devise confirmation states outside excluded SSO/SAML/LDAP/OIDC variants. | Focused profile and agent invitation tests, combined handler/service/repository/router/migrate/app tests, full `go test ./...`, and `git diff --check` passed. | -| 2 | Phase 2/3 drift | Tracked route parity is 0 missing for the current 380-route critical set; dashboard `/app` shell routes from `routes.rb:19-20`, `.well-known` app association and custom-domain challenge routes from `routes.rb:657-660`, Twilio callback routes from `routes.rb:639-640`, enterprise Twilio voice routes from `routes.rb:643-646`, root Linear/Shopify/Notion OAuth callback routes from `routes.rb:630/634/654`, root Twitter/Google/Microsoft/Instagram/TikTok callback routes from `routes.rb:626/649-652`, assignment policy routes from `routes.rb:306-313`, help-center portal/category/article routes from `routes.rb:385-404`, public help-center portal/sitemap/article/category/search/article-detail routes from `routes.rb:590-601`, enterprise contact outbound voice call from `routes.rb:216`, account agent-bot routes from `routes.rb:94-97`, account webhook routes from `routes.rb:342`, account integration app/hook routes from `routes.rb:345-348`, account Dyte routes from `routes.rb:357-358`, dashboard app routes from `routes.rb:130`, canned response routes from `routes.rb:114`, notification subscription routes from `routes.rb:440`, team/team-member routes from `routes.rb:296-300`, conversation participant routes from `routes.rb:150`, and conversation direct upload route from `routes.rb:151` are now explicitly tracked. Notification list/action serializers, user notification-settings raw payloads, campaigns raw payload/display-id routes, Devise password reset/confirmation payloads, CRM shared attachment payloads plus fixed 100-row attachment pagination, account/settings payloads, assignable-agent payloads, agent index full-list behavior, agent create/update/delete defaults/errors/scope, account agent-bot route/payload/mutation behavior, account webhook payload/mutation behavior, integration app/hook payload behavior, account Dyte create/join payload behavior, dashboard app raw payload/serializer behavior, canned response raw payload/search/delete behavior, notification subscription payload behavior, team update/frontend route behavior, conversation participant route/payload/final-set update behavior, conversation direct upload ActiveStorage behavior, label CRUD payloads, custom filters, custom attribute definitions, contact outbound voice calls, assignment policy CRUD and inbox binding payloads, Twilio inbound/status callbacks, enterprise Twilio voice callbacks, Linear/Shopify/Notion root integration callbacks, Shopify OAuth auth redirects, root channel OAuth callbacks, help-center portal/category/article payloads, dashboard app shell route behavior, app association JSON payloads, Cloudflare custom hostname verification, public widget popular-article lists, public help-center category list/show payloads, public portal show/default-locale payloads, public portal search payloads, public article show/markdown/tracking routes, and public sitemap XML now match the inspected Chatwoot contract. | Continue the next evidence-backed route/controller/serializer drift after conversation direct upload parity or from B12 findings. | Regenerate parity artifacts when routes change and add endpoint-family fixture tests. | +| 2 | Phase 2/3 drift | Tracked route parity is 0 missing for the current 381-route critical set; dashboard `/app` shell routes from `routes.rb:19-20`, `.well-known` app association and custom-domain challenge routes from `routes.rb:657-660`, Twilio callback routes from `routes.rb:639-640`, enterprise Twilio voice routes from `routes.rb:643-646`, root Linear/Shopify/Notion OAuth callback routes from `routes.rb:630/634/654`, root Twitter/Google/Microsoft/Instagram/TikTok callback routes from `routes.rb:626/649-652`, assignment policy routes from `routes.rb:306-313`, help-center portal/category/article routes from `routes.rb:385-404`, public help-center portal/sitemap/article/category/search/article-detail routes from `routes.rb:590-601`, enterprise contact outbound voice call from `routes.rb:216`, account agent-bot routes from `routes.rb:94-97`, account webhook routes from `routes.rb:342`, account integration app/hook routes from `routes.rb:345-348`, account Dyte routes from `routes.rb:357-358`, dashboard app routes from `routes.rb:130`, canned response routes from `routes.rb:114`, notification subscription routes from `routes.rb:440`, team/team-member routes from `routes.rb:296-300`, conversation participant routes from `routes.rb:150`, conversation direct upload route from `routes.rb:151`, and conversation inbox assistant route from `routes.rb:165` are now explicitly tracked. Notification list/action serializers, user notification-settings raw payloads, campaigns raw payload/display-id routes, Devise password reset/confirmation payloads, CRM shared attachment payloads plus fixed 100-row attachment pagination, account/settings payloads, assignable-agent payloads, agent index full-list behavior, agent create/update/delete defaults/errors/scope, account agent-bot route/payload/mutation behavior, account webhook payload/mutation behavior, integration app/hook payload behavior, account Dyte create/join payload behavior, dashboard app raw payload/serializer behavior, canned response raw payload/search/delete behavior, notification subscription payload behavior, team update/frontend route behavior, conversation participant route/payload/final-set update behavior, conversation direct upload ActiveStorage behavior, conversation inbox assistant Copilot payload behavior, label CRUD payloads, custom filters, custom attribute definitions, contact outbound voice calls, assignment policy CRUD and inbox binding payloads, Twilio inbound/status callbacks, enterprise Twilio voice callbacks, Linear/Shopify/Notion root integration callbacks, Shopify OAuth auth redirects, root channel OAuth callbacks, help-center portal/category/article payloads, dashboard app shell route behavior, app association JSON payloads, Cloudflare custom hostname verification, public widget popular-article lists, public help-center category list/show payloads, public portal show/default-locale payloads, public portal search payloads, public article show/markdown/tracking routes, and public sitemap XML now match the inspected Chatwoot contract. | Continue the next evidence-backed route/controller/serializer drift after conversation inbox assistant parity or from B12 findings. | Regenerate parity artifacts when routes change and add endpoint-family fixture tests. | | 3 | Phase 6 placeholder audit | Widget/public/webhook critical placeholders are burned down; inbox WhatsApp health/register-webhook and sync-template drift are closed; refreshed `docs/parity/placeholder_audit.md` shows only webhook nil-handler fallbacks still call `chatwootParityStub`; dashboard conversation transcript/custom-attribute response drift and message retry status drift are closed. | Keep in Review; reopen only if fresh `rg`, route smoke, or B12 finds a frontend-reachable placeholder/stub in account/contact/conversation/message/inbox/widget/public paths. | `rg` placeholder audit and `scripts/parity_frontend_smoke.sh --check` are recorded; no reused-frontend blocker is ownerless. | | 4 | P3.9 account agent-bot API | Implemented for the reused dashboard AgentBots settings route with no-trailing-slash routes, PATCH update, raw Jbuilder-style payloads, account mutation scope, system-bot show/list visibility, empty `200 OK` delete, and full reset/avatar action payloads. | Keep in Review; reopen only if live settings smoke exposes avatar upload storage or administrator-secret gating drift. | Focused AgentBot handler tests, service/router focused tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | | 5 | P3.10 account webhooks API | Implemented for the reused dashboard Webhooks settings route with PATCH update, Chatwoot `{ payload }` list/mutation serializers, nested `{ webhook: ... }` bodies, generated secret, account-scoped mutations, URL/subscription validation, optional inbox serialization, and empty `200 OK` delete. | Keep in Review; reopen only if live settings smoke exposes audit writer or delivery-signature drift beyond the existing delivery service boundary. | Focused webhook handler/service/router tests, migration test, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | @@ -152,10 +152,11 @@ This table is the shortest authoritative handoff view. If an older lower section | 11 | P3.16 teams frontend routes | Implemented for reused dashboard team settings: no-trailing-slash `GET/POST /teams` and `GET/POST/PATCH/DELETE /teams/:team_id/team_members` aliases are registered, and frontend `PATCH /teams/:team_id` update now reaches the Chatwoot team serializer response. | Keep in Review; reopen only if live team settings smoke exposes team show/store payload drift or team-member authorization/status-code differences beyond the inspected controller/Jbuilder contract. | Focused TeamHandler PATCH test, router/route-parity tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | | 12 | P3.17 conversation participants API | Implemented for reused dashboard conversation sidebar calls: singleton `GET/POST/PATCH/PUT/DELETE /conversations/:conversation_id/participants` routes are tracked and registered without the trailing slash, list/create/update return raw Chatwoot agent arrays, create accepts `user_ids`, update treats `user_ids` as the final participant set, and destroy returns empty `200 OK`. | Keep in Review; reopen only if live conversation sidebar smoke exposes participant authorization or agent serializer drift beyond the inspected controller/Jbuilder contract. | Focused ConversationParticipant handler/service tests, router/route-parity tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | | 13 | P3.18 conversation direct uploads API | Implemented for reused dashboard composer uploads: `POST /conversations/:conversation_id/direct_uploads` is tracked and registered, ActiveStorage blob metadata returns raw `signed_id/direct_upload` JSON, the returned local PUT URL stores bytes under account-scoped direct uploads, and conversation lookup uses display ID with legacy ID fallback. | Keep in Review; reopen only if live composer upload smoke exposes ActiveStorage header/status/storage drift beyond the inspected controller/frontend contract. | Focused upload handler/service/router/route-parity tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | -| 14 | P6.8 contact outbound voice call | Implemented for the reused dashboard route with Twilio voice-enabled inbox validation, assigned-inbox check, open-conversation reuse, ContactInbox/conversation/call/message persistence, and Chatwoot response shape. | Keep in Review; reopen only if live smoke exposes provider initiation/status-update drift beyond the fakeable persisted boundary. | Focused contact call tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | -| 15 | B12 optional live smoke | API/browser/enterprise smoke commands are checked in; live runs need PostgreSQL, Redis, Meilisearch, Vite, and Chrome. | Run full live smoke when environment is available and map failures to the board. | `docs/parity/frontend_smoke_report.md` records pass/fail and linked owners. | -| 16 | B9.3 delayed automation actions | Current reference exposes no delayed automation action params; scheduled-item work is already P5.12; `send_email_to_team` is durable and `add_sla` mutates conversation/applied SLA state. | Keep automation drift closed if future reference/smoke exposes delayed params or unsupported action shapes. | Automation worker/action fixtures verify queued team email replay, retry visibility through worker jobs, and SLA action idempotency. | -| 17 | P5.13 reports/analytics | P5.13a derives visible report aggregates from persisted rows; P5.13b adds lazy rollup freshness, durable day rollup jobs, and `/reports` metric timeseries. | Keep report drift closed as frontend smoke or reference inspection exposes additional metrics. | Report fixtures verify timeseries values, cache/freshness behavior, and no hidden placeholder JSON. | +| 14 | P3.19 conversation inbox assistant API | Implemented for reused dashboard Copilot state: `GET /conversations/:conversation_id/inbox_assistant` is tracked and registered, conversation lookup uses display ID with legacy ID fallback, account-scoped `captain_inboxes` binds the inbox to a Captain assistant, and responses are exactly `{ assistant: { id, name } }` or `{ assistant: null }`. | Keep in Review; reopen only if live conversation/Copilot smoke exposes feature-flag, authorization, or serializer drift beyond the inspected enterprise controller/store contract. | Focused ConversationCrud handler tests, service/router/route-parity tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | +| 15 | P6.8 contact outbound voice call | Implemented for the reused dashboard route with Twilio voice-enabled inbox validation, assigned-inbox check, open-conversation reuse, ContactInbox/conversation/call/message persistence, and Chatwoot response shape. | Keep in Review; reopen only if live smoke exposes provider initiation/status-update drift beyond the fakeable persisted boundary. | Focused contact call tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | +| 16 | B12 optional live smoke | API/browser/enterprise smoke commands are checked in; live runs need PostgreSQL, Redis, Meilisearch, Vite, and Chrome. | Run full live smoke when environment is available and map failures to the board. | `docs/parity/frontend_smoke_report.md` records pass/fail and linked owners. | +| 17 | B9.3 delayed automation actions | Current reference exposes no delayed automation action params; scheduled-item work is already P5.12; `send_email_to_team` is durable and `add_sla` mutates conversation/applied SLA state. | Keep automation drift closed if future reference/smoke exposes delayed params or unsupported action shapes. | Automation worker/action fixtures verify queued team email replay, retry visibility through worker jobs, and SLA action idempotency. | +| 18 | P5.13 reports/analytics | P5.13a derives visible report aggregates from persisted rows; P5.13b adds lazy rollup freshness, durable day rollup jobs, and `/reports` metric timeseries. | Keep report drift closed as frontend smoke or reference inspection exposes additional metrics. | Report fixtures verify timeseries values, cache/freshness behavior, and no hidden placeholder JSON. | ## Open Checkpoint Contracts @@ -185,6 +186,7 @@ These rows are the executable development plan from this point forward. A checkp | P3.16 teams frontend route parity | `internal/router/router.go`, `internal/handler/api/v1/team_handler.go`, team handler/router tests, `cmd/route_parity` | `reference/chatwoot/config/routes.rb:296-300`, `reference/chatwoot/app/controllers/api/v1/accounts/teams_controller.rb`, `reference/chatwoot/app/controllers/api/v1/accounts/team_members_controller.rb`, `reference/chatwoot/app/views/api/v1/accounts/teams/*.json.jbuilder`, `reference/chatwoot/app/views/api/v1/accounts/team_members/*.json.jbuilder`, dashboard `api/teams.js`, `store/modules/teams/actions.js`, `store/modules/teamMembers.js` | Team routes now match reused dashboard call sites: no-trailing-slash `GET/POST /teams` aliases are registered for `CacheEnabledApiClient`, frontend `PATCH /teams/:team_id` update is registered alongside Rails `PUT`, and no-trailing-slash team-member list/create/update/delete aliases are registered for `TeamsAPI.getAgents/addAgents/updateAgents`. Team update continues to accept raw frontend bodies and `{ team: ... }` wrappers and returns raw Chatwoot team fields. | Review by `feat(teams): align frontend update routes`; focused TeamHandler PATCH test, router/route-parity tests, route dump/parity regeneration to `TOTAL: 919` and `361 exact, 13 parameter-compatible, 0 missing out of 374`, full `go test ./...`, and `git diff --check` passed. | | P3.17 conversation participants API parity | `internal/router/router.go`, `internal/handler/api/v1/conversation_participant_handler.go`, `internal/service/conversation_participant_service.go`, `internal/repository/conversation_participant_repo.go`, conversation participant handler/service tests, `cmd/route_parity` | `reference/chatwoot/config/routes.rb:150`, `reference/chatwoot/app/controllers/api/v1/accounts/conversations/participants_controller.rb`, `reference/chatwoot/app/views/api/v1/accounts/conversations/participants/*.json.jbuilder`, `reference/chatwoot/app/views/api/v1/models/_agent.json.jbuilder`, dashboard `api/inbox/conversation.js` | Conversation participants now match the reused dashboard sidebar contract: singleton participant routes are tracked and registered without trailing slashes; trailing slash and legacy per-user routes remain compatibility aliases; list/create/update return raw agent arrays rendered through the Chatwoot agent serializer shape; create accepts frontend `user_ids` and legacy `user_id`; update treats `user_ids` as the final participant set, adding missing users and removing absent users; destroy accepts `{ user_ids: [...] }` and returns empty `200 OK`. | Review by `feat(conversations): align participant payloads`; focused ConversationParticipant handler/service tests, router/route-parity tests, route dump/parity regeneration to `TOTAL: 925` and `366 exact, 13 parameter-compatible, 0 missing out of 379`, full `go test ./...`, and `git diff --check` passed. | | P3.18 conversation direct uploads API parity | `internal/router/router.go`, `internal/handler/api/v1/upload_handler.go`, `internal/service/upload_service.go`, `internal/app/bootstrap.go`, upload handler tests, `cmd/route_parity` | `reference/chatwoot/config/routes.rb:151`, `reference/chatwoot/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb`, `reference/chatwoot/app/javascript/dashboard/composables/useFileUpload.js`, `reference/chatwoot/app/javascript/dashboard/mixins/fileUploadMixin.js` | Conversation direct uploads now match the reused dashboard composer boundary: Chatwoot's nested `POST /api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads` route is tracked and registered, JSON ActiveStorage blob metadata returns raw `signed_id`, `direct_upload.url`, and headers, the returned local PUT target writes bytes to account-scoped storage, direct-upload rows keep `Source=account` and current account scope, and conversation resolution uses display ID with legacy ID fallback. | Review by `feat(conversations): align direct upload routes`; focused upload handler/service/router/route-parity tests, route dump/parity regeneration to `TOTAL: 927` and `367 exact, 13 parameter-compatible, 0 missing out of 380`, full `go test ./...`, and `git diff --check` passed. | +| P3.19 conversation inbox assistant API parity | `internal/router/router.go`, `internal/handler/api/v1/conversation_handler.go`, `internal/service/conversation_service.go`, conversation CRUD/service tests, `cmd/route_parity` | `reference/chatwoot/config/routes.rb:165`, `reference/chatwoot/enterprise/app/controllers/enterprise/api/v1/accounts/conversations_controller.rb`, `reference/chatwoot/app/javascript/dashboard/api/inbox/conversation.js`, `reference/chatwoot/app/javascript/dashboard/store/modules/conversations/actions.js`, conversation store mutation that writes `copilotAssistant` | Conversation inbox assistant now matches the reused dashboard Copilot sidebar contract: `GET /api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant` is tracked and registered, resolves conversations by account display ID with legacy ID fallback, joins `captain_inboxes` to `captain_assistants` with account and inbox scope, serializes only assistant `id/name`, and returns `{ assistant: null }` when no assistant is bound. | Review by `feat(conversations): expose inbox assistant`; focused ConversationCrud handler tests cover bound, unbound, and display-ID lookup paths; service/router/route-parity tests pass; route dump/parity regenerated to `TOTAL: 928` and `368 exact, 13 parameter-compatible, 0 missing out of 381`; full `go test ./...` and `git diff --check` passed. | | P6.8 contact outbound voice call parity | `internal/router/router.go`, `internal/handler/api/v1/contact_handler.go`, `internal/service/contact_service.go`, `internal/model/call.go`, contact handler tests | `reference/chatwoot/config/routes.rb:216`, `reference/chatwoot/enterprise/app/controllers/api/v1/accounts/contacts/calls_controller.rb`, `reference/chatwoot/enterprise/app/services/voice/outbound_call_builder.rb`, `reference/chatwoot/enterprise/app/services/voice/call_message_builder.rb`, `reference/chatwoot/enterprise/app/models/call.rb`, dashboard `api/contacts.js`, `store/modules/contacts/actions.js`, `api/channel/voice/voiceAPIClient.js` | Contact outbound calls now match the Chatwoot enterprise route boundary: account contact lookup, current user's assigned `Channel::TwilioSms` inbox lookup, `voice_enabled` guard, phone-number guard, open display-ID conversation reuse only for same inbox/contact, new ContactInbox/open conversation creation when needed, persisted outgoing Twilio call metadata, linked `voice_call` message content attributes, and raw `{ conversation_id, inbox_id, call_sid, conference_sid }` response. | Review by `feat(contacts): initiate voice calls`; focused contact call tests cover success/reuse/resolved-hint ignored/no-phone/non-voice/unassigned cases; route dump/parity regenerated to `TOTAL: 861` and `299 exact, 7 parameter-compatible, 0 missing out of 306`; full `go test ./...` and `git diff --check` passed. | | P6 conversation transcript response parity | `internal/handler/api/v1/conversation_handler.go`, conversation handler tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb`, dashboard `api/inbox/conversation.js` | Account conversation transcript now follows Chatwoot's controller contract: missing email returns `422 { error: "email param missing" }`, nonblank email schedules through the existing service boundary and returns empty `200 OK`, and invalid-looking but nonblank email values are not rejected by local email format validation. | Review by `feat(conversations): align transcript responses`; focused transcript handler/service tests, combined handler/service/router tests, full `go test ./...`, and `git diff --check` must pass. | | P6 conversation custom attributes response parity | `internal/handler/api/v1/conversation_handler.go`, conversation handler tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb`, `app/views/api/v1/accounts/conversations/custom_attributes.json.jbuilder`, dashboard `api/inbox/conversation.js`, conversation store action | Account conversation custom attribute updates now return Chatwoot `{ custom_attributes: ... }` only, matching the store action that reads `response.data.custom_attributes`, instead of returning the full conversation serializer with unrelated fields. Empty/null JSON serializes as `{}`. | Review by `feat(conversations): align custom attribute response`; focused handler tests, combined handler/service/router tests, full `go test ./...`, and `git diff --check` passed. | @@ -226,6 +228,7 @@ This ledger records the committed parity checkpoints that future slices should b | Commit | Scope | Verification summary | Follow-up state | | --- | --- | --- | --- | +| `feat(conversations): expose inbox assistant` | Advances P3.19 conversation inbox assistant parity by matching Chatwoot enterprise `Api::V1::Accounts::ConversationsController#inbox_assistant`, route `165`, and reused dashboard `api/inbox/conversation.js` plus conversation store Copilot assistant state. GoChat now registers and tracks `GET /api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant`, resolves conversations by display ID with legacy ID fallback, account-scopes the inbox-to-Captain-assistant join through `captain_inboxes` and `captain_assistants`, returns only assistant `id/name`, and returns `{ assistant: null }` for unbound inboxes. | `go test ./internal/handler/api/v1 ./internal/service ./internal/router ./cmd/route_parity -run 'ConversationCrud\|Conversation\|Router\|RouteParity' -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; full `go test ./...`; `git diff --check`. Route dump is `TOTAL: 928`; tracked route parity is `368 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 381`. | P3.19 moves to Review for current conversation Copilot assistant evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. | | `feat(conversations): align direct upload routes` | Advances P3.18 conversation direct upload parity by matching Chatwoot nested `Conversations::DirectUploadsController`, route `151`, and the reused dashboard ActiveStorage upload callers. GoChat now registers and tracks `POST /api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads`, returns raw ActiveStorage `signed_id/direct_upload` metadata, validates the account-scoped conversation by display ID with legacy ID fallback, stores upload rows as account direct uploads, and exposes the local PUT URL used by the metadata response to persist bytes. | `go test ./internal/handler/api/v1 ./internal/service ./internal/router ./cmd/route_parity -run 'Upload\|Router\|RouteParity' -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; full `go test ./...`; `git diff --check`. Route dump is `TOTAL: 927`; tracked route parity is `367 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 380`. | P3.18 moves to Review for current composer upload evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. | | `feat(conversations): align participant payloads` | Advances P3.17 conversation participant parity by matching Chatwoot `Conversations::ParticipantsController`, participant Jbuilder views, the `_agent` serializer, routes `150`, and reused dashboard `api/inbox/conversation.js` calls. GoChat now registers and tracks singleton `GET/POST/PATCH/PUT/DELETE /api/v1/accounts/:account_id/conversations/:conversation_id/participants` routes without trailing slashes, keeps trailing slash and per-user compatibility aliases, returns raw agent arrays instead of local `{ success, data }` envelopes, accepts frontend `user_ids`, treats update `user_ids` as the final participant set, and returns empty `200 OK` for participant destroy. | `go test ./internal/handler/api/v1 ./internal/service ./internal/router ./cmd/route_parity -run 'ConversationParticipant\|Router\|RouteParity' -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; full `go test ./...`; `git diff --check`. Route dump is `TOTAL: 925`; tracked route parity is `366 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 379`. | P3.17 moves to Review for current conversation sidebar participant evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. | | `feat(teams): align frontend update routes` | Advances P3.16 team route parity by matching Chatwoot `TeamsController`, `TeamMembersController`, their Jbuilder serializers, routes `296-300`, and reused dashboard `api/teams.js`/team store callers. GoChat now registers frontend no-trailing-slash aliases for team index/create and team-member list/create/update/delete, tracks the Rails `PATCH /api/v1/accounts/:account_id/teams/:team_id` route, routes frontend team updates through the existing Chatwoot-shaped update serializer, and keeps raw frontend bodies plus `{ team: ... }` wrappers accepted. | `go test ./internal/handler/api/v1 -run TeamHandler -count=1`; `go test ./internal/handler/api/v1 ./internal/router ./cmd/route_parity -run 'TeamHandler\|Router\|RouteParity' -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; full `go test ./...`; `git diff --check`. Route dump is `TOTAL: 919`; tracked route parity is `361 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 374`. | P3.16 moves to Review for current team settings route evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. | @@ -2413,3 +2416,4 @@ Verification milestone gates: - 2026-06-06: P3.16 teams frontend route checkpoint prepared as `feat(teams): align frontend update routes`; audited Chatwoot teams/team-members controllers, Jbuilder serializers, routes `296-300`, and reused dashboard teams API/store callers. GoChat now exposes the frontend `PATCH /api/v1/accounts/:account_id/teams/:team_id` update path, registers no-trailing-slash team index/create aliases, and registers no-trailing-slash team-member list/create/update/delete aliases used by the dashboard. Focused TeamHandler PATCH test, router/route-parity tests, route dump/parity regeneration (`TOTAL: 919`, `361 exact`, `13 parameter-compatible`, `0 missing out of 374`), full `go test ./...`, and `git diff --check` passed; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. - 2026-06-06: P3.17 conversation participant checkpoint prepared as `feat(conversations): align participant payloads`; audited Chatwoot participant routes/controller/Jbuilder, `_agent` serializer, and reused dashboard conversation API calls. GoChat now exposes singleton no-trailing-slash participant routes, tracks `GET/POST/PATCH/PUT/DELETE /conversations/:conversation_id/participants`, returns raw agent arrays, accepts frontend `user_ids`, applies update as final-set synchronization, and returns empty `200 OK` destroy responses. Focused ConversationParticipant handler/service tests, router/route-parity tests, route dump/parity regeneration (`TOTAL: 925`, `366 exact`, `13 parameter-compatible`, `0 missing out of 379`), full `go test ./...`, and `git diff --check` passed; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. - 2026-06-06: P3.18 conversation direct upload checkpoint prepared as `feat(conversations): align direct upload routes`; audited Chatwoot nested direct upload route/controller and reused dashboard ActiveStorage upload callers. GoChat now exposes and tracks `POST /api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads`, returns raw ActiveStorage `signed_id/direct_upload` metadata, validates account-scoped conversations by display ID with legacy ID fallback, and supports the returned local PUT upload target for account-scoped bytes. Focused upload handler/service/router/route-parity tests, route dump/parity regeneration (`TOTAL: 927`, `367 exact`, `13 parameter-compatible`, `0 missing out of 380`), full `go test ./...`, and `git diff --check` passed; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. +- 2026-06-06: P3.19 conversation inbox assistant checkpoint prepared as `feat(conversations): expose inbox assistant`; audited Chatwoot enterprise conversation `inbox_assistant`, route `165`, and reused dashboard conversation API/store Copilot assistant state. GoChat now exposes and tracks `GET /api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant`, resolves conversations by display ID with legacy ID fallback, looks up the account-scoped Captain assistant bound to the conversation inbox, and returns `{ assistant: { id, name } }` or `{ assistant: null }`. Focused ConversationCrud handler/service/router/route-parity tests, route dump/parity regeneration (`TOTAL: 928`, `368 exact`, `13 parameter-compatible`, `0 missing out of 381`), full `go test ./...`, and `git diff --check` passed; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. diff --git a/docs/parity/gochat_routes.txt b/docs/parity/gochat_routes.txt index c385b993..9f8ea439 100644 --- a/docs/parity/gochat_routes.txt +++ b/docs/parity/gochat_routes.txt @@ -218,6 +218,7 @@ GET /api/v1/accounts/:account_id/conversations/:conversation_id GET /api/v1/accounts/:account_id/conversations/:conversation_id/attachments GET /api/v1/accounts/:account_id/conversations/:conversation_id/draft_messages/ GET /api/v1/accounts/:account_id/conversations/:conversation_id/draft_messages/:draft_id +GET /api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant GET /api/v1/accounts/:account_id/conversations/:conversation_id/labels GET /api/v1/accounts/:account_id/conversations/:conversation_id/messages/ GET /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id @@ -925,4 +926,4 @@ PUT /public/api/v1/csat_survey/:id PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id PUT /widget/direct_uploads/:upload_uuid -TOTAL: 927 +TOTAL: 928 diff --git a/docs/parity/route_parity.md b/docs/parity/route_parity.md index 442cab62..6c4b83a5 100644 --- a/docs/parity/route_parity.md +++ b/docs/parity/route_parity.md @@ -7,7 +7,7 @@ Generated from: This report covers tracked frontend-critical Chatwoot routes from `reference/chatwoot/config/routes.rb`, including API v1 account routes, Captain/Copilot, assignment policies, widget/public APIs, and API v2 reports. Ruby is not installed in the workspace, so Chatwoot routes are sourced from static route declarations instead of `bin/rails routes`. -Summary: 367 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 380 tracked critical routes. +Summary: 368 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 381 tracked critical routes. ## Missing Critical Routes @@ -144,6 +144,7 @@ These routes exist with equivalent method and path shape but different parameter | GET | `/api/v1/accounts/:account_id/contacts/search` | `/api/v1/accounts/:account_id/contacts/search` | `api/v1/accounts/contacts#search` | `routes.rb:201` | exact | | GET | `/api/v1/accounts/:account_id/conversations/` | `/api/v1/accounts/:account_id/conversations/` | `api/v1/accounts/conversations#index` | `routes.rb:134` | exact | | GET | `/api/v1/accounts/:account_id/conversations/:conversation_id` | `/api/v1/accounts/:account_id/conversations/:conversation_id` | `api/v1/accounts/conversations#show` | `routes.rb:134` | exact | +| GET | `/api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant` | `/api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant` | `api/v1/accounts/conversations#inbox_assistant` | `routes.rb:165` | exact | | GET | `/api/v1/accounts/:account_id/conversations/:conversation_id/labels` | `/api/v1/accounts/:account_id/conversations/:conversation_id/labels` | `api/v1/accounts/conversations/labels#index` | `routes.rb:149` | exact | | GET | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/` | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/` | `api/v1/accounts/conversations/messages#index` | `routes.rb:142` | exact | | GET | `/api/v1/accounts/:account_id/conversations/:conversation_id/participants` | `/api/v1/accounts/:account_id/conversations/:conversation_id/participants` | `api/v1/accounts/conversations/participants#show` | `routes.rb:150` | exact | diff --git a/internal/handler/api/v1/conversation_handler.go b/internal/handler/api/v1/conversation_handler.go index b222c969..e46e75ff 100644 --- a/internal/handler/api/v1/conversation_handler.go +++ b/internal/handler/api/v1/conversation_handler.go @@ -783,6 +783,34 @@ func (h *ConversationHandler) ListAttachments(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"meta": gin.H{"total_count": total}, "payload": payload}) } +// InboxAssistant returns the Captain assistant connected to the conversation inbox. +// GET /api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant +// Reference: Chatwoot enterprise conversations_controller#inbox_assistant. +func (h *ConversationHandler) InboxAssistant(c *gin.Context) { + accountID, err := parseUintParam(c, "account_id") + if err != nil { + response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") + return + } + + conversationID, err := parseUintParam(c, "conversation_id") + if err != nil { + response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation_id") + return + } + + assistant, svcErr := h.conversationSvc.GetInboxAssistant(c.Request.Context(), accountID, conversationID) + if svcErr != nil { + handleServiceError(c, svcErr) + return + } + if assistant == nil { + c.JSON(http.StatusOK, gin.H{"assistant": nil}) + return + } + c.JSON(http.StatusOK, gin.H{"assistant": gin.H{"id": assistant.ID, "name": assistant.Name}}) +} + // ToggleTyping toggles the typing status for an agent in a conversation. // POST /api/v1/accounts/:account_id/conversations/:id/toggle_typing_status // Reference: Chatwoot conversations_controller#toggle_typing_status (member route) diff --git a/internal/handler/api/v1/conversation_handler_crud_test.go b/internal/handler/api/v1/conversation_handler_crud_test.go index c37c21f0..7f492224 100644 --- a/internal/handler/api/v1/conversation_handler_crud_test.go +++ b/internal/handler/api/v1/conversation_handler_crud_test.go @@ -85,6 +85,8 @@ func (s *ConversationCrudTestSuite) SetupSuite() { &model.SlaPolicy{}, &model.AppliedSLA{}, &model.SlaEvent{}, + &model.CaptainAssistant{}, + &model.CaptainInbox{}, ) s.Require().NoError(err) @@ -127,6 +129,7 @@ func (s *ConversationCrudTestSuite) SetupSuite() { conversations.POST("/filter", handler.Filter) conversations.POST("/:conversation_id/priority", handler.UpdatePriority) conversations.POST("/:conversation_id/assignments", handler.AssignTeam) + conversations.GET("/:conversation_id/inbox_assistant", handler.InboxAssistant) } } } @@ -182,6 +185,8 @@ func (s *ConversationCrudTestSuite) TearDownTest() { s.db.Exec("DELETE FROM teams") s.db.Exec("DELETE FROM team_members") s.db.Exec("DELETE FROM sla_policies") + s.db.Exec("DELETE FROM captain_inboxes") + s.db.Exec("DELETE FROM captain_assistants") } func (s *ConversationCrudTestSuite) accountURL() string { @@ -340,6 +345,56 @@ func (s *ConversationCrudTestSuite) TestGet_NotFound() { assert.Equal(s.T(), http.StatusNotFound, w.Code) } +func (s *ConversationCrudTestSuite) TestInboxAssistant_ReturnsBoundAssistant() { + assistant := &model.CaptainAssistant{AccountID: s.testAccount.ID, Name: "Inbox Helper", Description: "Helps this inbox", Config: json.RawMessage(`{}`), Status: model.AssistantStatusActive} + s.Require().NoError(s.db.Create(assistant).Error) + s.Require().NoError(s.db.Create(&model.CaptainInbox{AccountID: s.testAccount.ID, AssistantID: assistant.ID, InboxID: s.testInbox.ID}).Error) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", s.convURL(s.testConv.ID)+"/inbox_assistant", nil) + s.router.ServeHTTP(w, req) + + assert.Equal(s.T(), http.StatusOK, w.Code) + var resp map[string]interface{} + s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + assistantResp, ok := resp["assistant"].(map[string]interface{}) + s.Require().True(ok) + assert.Equal(s.T(), float64(assistant.ID), assistantResp["id"]) + assert.Equal(s.T(), "Inbox Helper", assistantResp["name"]) + assert.NotContains(s.T(), assistantResp, "description") +} + +func (s *ConversationCrudTestSuite) TestInboxAssistant_ReturnsNilWhenUnbound() { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", s.convURL(s.testConv.ID)+"/inbox_assistant", nil) + s.router.ServeHTTP(w, req) + + assert.Equal(s.T(), http.StatusOK, w.Code) + var resp map[string]interface{} + s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Contains(s.T(), resp, "assistant") + assert.Nil(s.T(), resp["assistant"]) +} + +func (s *ConversationCrudTestSuite) TestInboxAssistant_UsesDisplayIDRoute() { + displayID := uint(77) + s.testConv.DisplayID = &displayID + s.Require().NoError(s.db.Save(s.testConv).Error) + assistant := &model.CaptainAssistant{AccountID: s.testAccount.ID, Name: "Display Helper", Description: "Display route", Config: json.RawMessage(`{}`), Status: model.AssistantStatusActive} + s.Require().NoError(s.db.Create(assistant).Error) + s.Require().NoError(s.db.Create(&model.CaptainInbox{AccountID: s.testAccount.ID, AssistantID: assistant.ID, InboxID: s.testInbox.ID}).Error) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/77/inbox_assistant", nil) + s.router.ServeHTTP(w, req) + + assert.Equal(s.T(), http.StatusOK, w.Code) + var resp map[string]interface{} + s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + assistantResp := resp["assistant"].(map[string]interface{}) + assert.Equal(s.T(), float64(assistant.ID), assistantResp["id"]) +} + // ========== Update Handler Tests ========== func (s *ConversationCrudTestSuite) TestUpdate_Success() { diff --git a/internal/router/router.go b/internal/router/router.go index ac20c0be..1d572b7e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -976,6 +976,7 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) { // Additional Conversation endpoints matching Chatwoot member routes conversations.GET("/:conversation_id/attachments", h.Conversation.ListAttachments) + conversations.GET("/:conversation_id/inbox_assistant", h.Conversation.InboxAssistant) conversations.POST("/:conversation_id/direct_uploads", h.Upload.ConversationDirectUpload) conversations.PUT("/:conversation_id/direct_uploads/:upload_uuid", h.Upload.CompleteConversationDirectUpload) conversations.POST("/:conversation_id/toggle_typing_status", h.Conversation.ToggleTyping) diff --git a/internal/service/conversation_service.go b/internal/service/conversation_service.go index d7a7b96f..875dc729 100644 --- a/internal/service/conversation_service.go +++ b/internal/service/conversation_service.go @@ -161,6 +161,29 @@ func (s *ConversationService) GetByAccountAndDisplayIDOrID(ctx context.Context, return s.repo.FindByAccountAndDisplayIDOrID(ctx, accountID, routeID) } +// GetInboxAssistant returns the Captain assistant connected to the conversation's inbox. +// Reference: Chatwoot enterprise ConversationsController#inbox_assistant. +func (s *ConversationService) GetInboxAssistant(ctx context.Context, accountID, routeID uint) (*model.CaptainAssistant, error) { + conversation, err := s.repo.FindByAccountAndDisplayIDOrID(ctx, accountID, routeID) + if err != nil { + return nil, err + } + + var assistant model.CaptainAssistant + err = s.repo.DB().WithContext(ctx). + Joins("JOIN captain_inboxes ON captain_inboxes.captain_assistant_id = captain_assistants.id"). + Where("captain_inboxes.account_id = ? AND captain_inboxes.inbox_id = ? AND captain_assistants.account_id = ?", accountID, conversation.InboxID, accountID). + Order("captain_inboxes.id DESC"). + First(&assistant).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + if err != nil { + return nil, err + } + return &assistant, nil +} + // CreateConversationRequest is the DTO for creating a conversation. // Reference: Chatwoot app/controllers/api/v1/accounts/conversations_controller.rb #create // Supports creating conversation with an initial message (like Chatwoot's ConversationBuilder).