RFC: AI Agent Knowledge — Conversation History (Phase 1)
Document Conventions (do not remove)
This RFC follows the Qontak RFC Template format for governance — the metadata table, Confluence sections 1–6, and Comment logs are mandatory. Sections marked
N/A — reasonare intentional, not omissions.It is agent-execution-ready: §1 Design References (FE half) + §1 PRD-to-Schema Derivation (BE half), §2 Repo Reading Guide (Detail 2.0) for both layers, mermaid diagrams, §2.G Cross-Layer Contract Verification, and §4 Agent Execution Plan + Verification & Rollback Recipe are filled to the extent the current repository and the (still-open) cross-team contracts allow. Unresolved blockers are listed in §5 and gate §7 to
no.Delivery & project management live elsewhere. This is the technical artifact only. Staffing, effort, and schedule live in the initiative's
delivery/folder. Until handed to delivery, the Delivery row readsnot yet handed to delivery.The YAML frontmatter is the machine-readable index; the Metadata table is the human-readable governance record. Both must agree on every shared field.
Metadata
| Field | Value | Notes |
|---|---|---|
| Status | IDEA | Human label; YAML status: carries the remapped linter enum draft |
| DRI | Dimas Fauzi Hidayat | Accountable owner carried from the PRD (PM). Engineering DRI assigned at delivery handoff. |
| Team | chatbot | Advisory squad slug; squad = BOT (Hadiningbot) |
| Author(s) | BOT squad (Hadiningbot) | Eng author(s) to be named |
| Reviewers | BOT tech lead · Data & AI team · chatbot-fe lead | Data & AI owns masking + vector + webhook |
| Approver(s) | Engineering Lead · InfoSec approver | Names TBD; InfoSec required (PII handling) |
| Submitted Date | 2026-06-20 | ISO-8601 |
| Last Updated | 2026-06-20 | ISO-8601 |
| Target Release | 2026-Q3 | From initiative target_quarter |
| Target Quarter | 2026-Q3 | Advisory; from PRD / initiative README |
| Delivery | not yet handed to delivery | Pointer added once handed to delivery |
| Related | ../prds/conversation-history.md · ../ai-agent-knowledge-anchor.md | PRD + initiative ANCHOR |
| Discussion | #bot-squad — RFC thread TBD | Slack |
Type: full-stack
Frontend sub-type: new-feature
Backend sub-type: enhancement (extends existing agent_conversation_history scaffolding)
Sections at a Glance
- Overview (Design References — FE half; PRD-to-Schema Derivation — BE half)
- Technical Design (Repo Reading Guide both layers → end-to-end mermaid → DDL → APIs → cross-layer contract verification)
- High-Availability & Security
- Backwards Compatibility and Rollout Plan
- Concern, Questions, or Known Limitations
- Comment logs
- Ready for agent execution
1. Overview
This RFC designs Phase 1 of the AI Agent Knowledge initiative: Conversation History — a new dynamic knowledge source that lets the Qontak AI Agent (Airene Copilot) learn from the resolved chat conversations of selected expert agents. An Admin picks a Division and up to 15 expert agents; the system ingests the last ~90 days of those agents' resolved, customer-facing, text-only conversations (PII-masked by the Data & AI pipeline), indexes them into the AI vector store, keeps them fresh with a daily rolling-window sync, and surfaces a per-source training status (Training in Progress / Active / Error) and "Last Updated" timestamp in the existing Knowledge / AI Resources UI.
Grounding note — this is an enhancement on a partial existing build, not greenfield. The backend already ships an
agent_conversation_historyknowledge-source type, theai_knowledge_source_omnichannel_agent_conversationstable, a create endpoint (POST /v1/gpt/ai_knowledge_sources/omnichannel-agent-conversations), a detail endpoint, a soft-delete, a usages/quota endpoint, an AI-Servicetype: 'conversation'post path, and a poll-based status updater. The frontend already declares a "Conversation history" source tile but does not wire it to a working flow. This RFC therefore mostly extends verified backend contracts and builds the frontend configuration surface plus the missing daily-sync, division-scoping, 15-agent-cap, and status-webhook pieces. See Detail 2.0.
Success Criteria
- Adoption (PRD §13 ⭐): ≥ 30% of AI-Agent-active Qontak360 companies configure ≥1 Conversation History source within 90 days of GA.
- Training latency (PRD §6 / §14): Initial training of a 15-agent source reaches
Activewithin ~1 hour; daily sync success rate ≥ 99%. - Freshness: rolling 90-day window maintained — data older than 90 days is purged each daily sync; "Last Updated" advances on every successful sync.
- Privacy: 100% masking of standard phone/email formats before indexing (Internal Alpha gate, PRD §14); no raw PII persisted post-processing.
- Isolation: conversation ingestion runs in the Data & AI-owned isolated service (no "noisy neighbor" load on the chatbot app).
Out of Scope
Mirrors PRD §5 Non-Goals: automated/CSAT-based agent selection; real-time ingestion; multi-media (image/file/voice) indexing; cross-division knowledge pooling; new reporting dashboards. Plus: this RFC does not specify the internals of the Data & AI ingestion/masking/vector service (owned by that team) — it specifies only the contract the chatbot BE/FE depend on.
Related Documents
- PRD:
../prds/conversation-history.md(v1.2) — source of all ACs. - Initiative ANCHOR:
../ai-agent-knowledge-anchor.md. - Confluence: https://jurnal.atlassian.net/wiki/spaces/QON/pages/51092521518.
- Figma master (per PRD HEADER): ✨ Bot - AI node 18883-4459 · ✨ AI Resources node 10853-57617.
- Repos reconciled:
chatbot(Rails BE),chatbot-fe(Nuxt 3 FE),qontak-designer(design prototype).
Assumptions
- The Data & AI team owns and delivers: PII masking, the isolated conversation-training/vector service, and the rolling-90-day ingestion+purge. The chatbot BE only configures, triggers, and reflects status. (PRD §15 — all marked blocking.)
- The existing AI-Service contract
POST …/knowledge-base/vectorsaccepting{ id, type: 'conversation', participant_ids: [...] }andGET …/vectors/status/:company_idremains stable (verified inpost_knowledge_sources.rb/update_status.rb). - "Resolved" room status + ≥5 bubble messages is an acceptable proxy for a useful resolution (PRD OQ#6 — owned by PM, not yet confirmed).
- The division→agent directory needed by the config modal is served by the existing
Division/AgentsDivisionmodels (no new directory service). - Qontak360 plan/tier gating is expressed via the existing
SystemPreferenceflag mechanism.
Dependencies
| Dependency | Owning team | Deliverable | Blocking? | Status |
|---|---|---|---|---|
| PII masking engine | Data & AI | NLP redaction → generic tags before indexing | YES | Contract/SLA open (PRD OQ#1) |
| Isolated conversation training + vector indexing | Data & AI | Process heavy conversation data; expose type:'conversation' vector create + status | YES | Create + poll status verified live; rolling-90d window behavior to confirm |
| Training-status webhook | Data & AI | Inbound webhook: in_progress/completed/failed payload + retry semantics | YES | Contract not defined (PRD OQ#4) — see Decision 6 |
| Agent–Division directory | BOT (chatbot) | GET list divisions + agents for the config modal | YES | New endpoint — verified no GET directory exists (only internal sync PATCH /v1/chat/divisions); build from Division/AgentsDivision (chunk 2b) |
| Daily sync cron | BOT (chatbot) | 24h job: re-trigger ingest, purge >90d, advance "Last Updated" | YES | New — config/schedule.yml + worker (this RFC) |
| Resolved-room fetch (90-day window) | Inbox / Platform (ChatService) | Fetch resolved rooms by agent over 90 days | YES | Hub::ChatService::Rooms::List exists; consumed by Data & AI service |
| Onboarding consent | Legal / Ops | Existing consent covers internal AI training use | NO | Reuse existing |
Design References (frontend half — required)
Design status is DRAFT (PRD §8 + PRD OQ#7). The only located UI is a throwaway design prototype in
qontak-designer(README: "playground for designers to bring ideas to prototype"). It is not the Figma master and omits several PRD requirements. Productionchatbot-fehas only a non-wired "Conversation history" tile. Several frames are thereforen/a — design pendingand listed in §5 as blockers.
| PRD-named surface | Figma / design link | Frame name | Design system version | Design QA contact | Notes |
|---|---|---|---|---|---|
| Add Knowledge → source-type picker (+ "Conversation history") | Bot - AI 20840-30474 / 20840-32079 / 20839-31336 | Add source flow | @mekari/pixel3@1.0.12 (chatbot-fe package.json) | Wulan Febyazzahra Putri | Prototype: qontak-designer/app/components/bot-automation/sources/AddKnowledgeDrawer.vue |
| Conversation History config form (Division + ≤15 agents + tooltip + 15-cap validation) | n/a — design pending | — | @mekari/pixel3@1.0.12 | Wulan | Gap (PRD OQ#7): prototype uses a "Use as source" toggle + fixed 9-agent read-only list; no division-first multiselect, no 15-cap, no expertise tooltip. Blocker → §5. |
| Source detail (Agent · Division table) | AI Resources 10859-83239 | Source detail | @mekari/pixel3@1.0.12 | Wulan | Prototype: qontak-designer/app/components/bot-automation/sources/SourceDetailDrawer.vue (read-only Agent·Division table present) |
| AI Resources list (Status, Last updated, remove/delete) | Bot - AI 20671-248358 | Resources list | @mekari/pixel3@1.0.12 | Wulan | Prototype qontak-designer/app/pages/bot-automation/resources/index.vue shows only Active/Inactive — no Training-in-Progress / Error states (PRD OQ#7). Blocker → §5. |
PRD-to-Schema Derivation (backend half — required)
| PRD-described entity / attribute / rule | Persisted as (table.column) | Exposed via (endpoint / event) | Enforced where | Source (PRD §) |
|---|---|---|---|---|
| A Conversation History source belongs to a company and one source type | ai_knowledge_sources.company_id, .organization_id, .ai_knowledge_source_type_id (type code agent_conversation_history), .status, .name | POST …/omnichannel-agent-conversations (create), GET …/ (list) | CreateOmnichannelAgentConversationKnowledgeSource use case | §7, §8, S01 |
| Selected expert agents (≤15) feed the source | ai_knowledge_source_omnichannel_agent_conversations (agent_id, agent_name, company_id, ai_knowledge_source_id) | create payload agents:[{id,name}]; AI-Service participant_ids | AiKnowledgeSourceOmnichannelAgentConversations::Create + PostKnowledgeSources | §6, S01, S04 |
One Division = one source + division_id recorded | NEW ai_knowledge_sources.division_id (nullable; + partial-unique (company_id, division_id, type_id)) | create payload division_id | create use case existence guard (extended to be division-aware) + DB partial-unique index | §6, S05, NEG-3 — see Decision 1 |
| Max 15 agents per source | contract rule on agents array length ≤ 15 | create payload validation (422) | create use case contract (extended) + FE counter | §6, S04 |
| Training status lifecycle (in_progress / active / error) | ai_knowledge_sources.status (IN_PROGRESS/ACTIVE/FAILED) | status poll worker + (future) inbound webhook; surfaced in list/detail | UpdateStatus repo (poll) → status webhook (new, blocked) | §7, S06, S10 |
| "Last Updated" freshness | ai_knowledge_sources.updated_at (advances on sync) | list/detail serializer | daily sync worker (new) | §7, S10 |
| Daily rolling 90-day sync + purge | (window + purge enforced inside Data & AI vector store) | ConversationHistoryDailySyncWorker re-posts via PostKnowledgeSources | new sidekiq-cron job @ 0 1 * * * Asia/Jakarta | §9#4, S10, S12 |
| PII masking; quality gate (≥5 msgs); whisper exclusion; text-only; recency | (enforced inside Data & AI ingestion; not in chatbot) | AI-Service ingestion contract | Data & AI pipeline | S06, S07, S08, S09, S13, NEG-2/4 |
| Reference room-id link, permission-scoped | ask_airene_history_references.ai_knowledge_source_id, room_id (link source) | answer reference render + room-view permission check | room authorization on open | S11, OQ#5 |
| Soft-delete (remove from agent) vs hard-delete (delete source) | ai_agent_knowledges.deleted_at (unlink) vs ai_knowledge_sources.deleted_at + vector hard-delete | DELETE :id + delete_vector_db | delete use case (guarded by in-use check) | S02, S03 |
Every §2.3 DDL row and §2.4 endpoint traces to a row here (or to a Figma frame in Design References). Rows owned by Data & AI are explicitly out of chatbot's enforcement scope.
Detail 1.A — PRD Traceability (cross-layer)
Composite AC ids per PRD §10.2 (CONVHIST-Sxx/AC-n, ERR-n, NEG-n).
Forward (PRD AC → RFC):
| PRD composite AC id | FE section / component | BE section / endpoint |
|---|---|---|
| CONVHIST-S01/AC-1..3, ERR-1 | ConversationHistoryForm in AddKnowledgeDrawer (§2.A) | POST …/omnichannel-agent-conversations (§2.4, extended) |
| CONVHIST-S02/AC-1..3, ERR-1 | Row action "−" on Resources list (§2.A) | unlink ai_agent_knowledges (§2.4) |
| CONVHIST-S03/AC-1..3, ERR-1 | Delete action + in-use confirm dialog (§2.A) | DELETE :id + in-use guard + delete_vector_db (§2.4) |
| CONVHIST-S04/AC-1..3, ERR-1 | 15-cap counter + validation (§2.A, §2.C) | contract agents ≤15 (§2.4) |
| CONVHIST-S05/AC-1..3, ERR-1 | Division-first multi-select (§2.A) | divisions+agents read (§2.4); division_id persist (§2.3) |
| CONVHIST-S06/AC-1..3, ERR-1 | status surfacing only | Data & AI masking (out of scope); status Error on failure (§2.F.2) |
| CONVHIST-S07, S08, S09, S13 | n/a — backend/pipeline | Data & AI ingestion contract (§2.D resp. boundary) |
| CONVHIST-S10/AC-1..3, ERR-1 | "Last Updated" cell + status badge (§2.C) | ConversationHistoryDailySyncWorker (§2.F) |
| CONVHIST-S11/AC-1..3, ERR-1 | reference link render (§2.A) | room-view permission check (§3 authz) |
| CONVHIST-S12/AC-1..3, ERR-1 | n/a | sync skips deactivated agents; ages out (§2.F) |
| NEG-1 | hide tile when not Qontak360 (§2.A) | plan flag gate (§3) |
| NEG-3 | block 2nd source per division (§2.A error) | one-per-division unique guard (§2.3) |
Reverse (RFC → PRD AC):
| New FE component / BE endpoint / dependency | PRD composite AC id it serves |
|---|---|
ConversationHistoryForm (division + ≤15 agents) | CONVHIST-S01/AC-1, S04/AC-1, S05/AC-1 |
division_id column + one-per-division unique index | CONVHIST-S05/AC-2, NEG-3 |
ConversationHistoryDailySyncWorker + schedule.yml entry | CONVHIST-S10/AC-1, S12/AC-2 |
| in-use delete guard | CONVHIST-S03/ERR-1 |
| training-status webhook (blocked) | CONVHIST-S06/ERR-1, S10/ERR-1 |
UI / Consumer Surface Coverage
| PRD-named surface | Consumer | Required reads (BE) | Required writes (BE) | FE component | Status surface |
|---|---|---|---|---|---|
| Add Knowledge modal / drawer | web (Admin) | GET …/usages (plan/quota), divisions+agents | POST …/omnichannel-agent-conversations | AddKnowledgeDrawer → ConversationHistoryForm | ai_knowledge_sources.status |
| Knowledge Index / AI Resources list | web (Admin) | GET …/ (list) | DELETE :id (delete), unlink | pages/ai-knowledge/training-sources/index.vue + knowledge-list.vue | status badge + Last Updated |
| Source detail drawer | web (Admin) | GET …/omnichannel-agent-conversation/:id | — | SourceDetailDrawer (new in chatbot-fe) | status badge |
| Copilot answer reference (Inbox) | web (Agent) | room-by-id (permission-scoped) | — | Inbox Copilot reference link | n/a — read only |
Role Coverage
| PRD role | Authorization mechanism | Endpoints permitted (BE) | UI surface visibility (FE) | Cross-tenant? | Audit trail |
|---|---|---|---|---|---|
| Admin / Super Admin (owner/supervisor/admin) | Grape set_role(%w[owner supervisor admin]) (authorization_helpers.rb) | create / list / detail / delete / usages | full config + management | no — scoped by company_id/organization_id | PaperTrail on ai_knowledge_sources / agent-conversation rows |
| Agent (front-line) | session role | none (config); room-scoped read for reference link | consumes Copilot suggestions; sees reference link only if room-permitted | no | n/a |
| Non-Qontak360 / lower tier | plan flag (SystemPreference) | none | "Conversation history" tile hidden (NEG-1) | n/a | n/a |
| Data & AI pipeline (service) | service auth (webhook contract TBD) | status webhook (inbound, blocked) | n/a | n/a — operates per company_id | sync logs |
PRD Section Coverage
| PRD § | Title | Where covered |
|---|---|---|
| 2 | Phase Context | §1 Overview |
| 3 | One-liner + Problem | §1 Overview |
| 4 | Target Users + Persona | §1 (Success Criteria / personas) |
| 5 | Non-Goals | §1 Out of Scope |
| 6 | Constraints | §1, §3 (limits, tier, data limits) |
| 6.1 | Data Lifecycle | §2.3 per-status lifecycle + §3.D compliance |
| 7 | Feature Changes | §2.A UI Contract + §2.4 APIs |
| 8 | New Features | §1 Design References + §2.A + §5 (design gaps) |
| 9 | API & Webhook Behavior | §2.4 APIs + §2.F async + Decision 6 (webhook) |
| 10.1 | System Flow | §2.2 sequence + §2.1 branch/skip |
| 10.2 | User Stories + ACs | Detail 1.A + Detail 1.C |
| 11 | Rollout | §4 Rollout Strategy |
| 12 | Observability | §3 Monitoring & Alerting |
| 13 | Success Metrics | §1 Success Criteria |
| 14 | Launch Plan & Stage Gates | §4 Rollout (stage gates) |
| 15 | Dependencies | §1 Dependencies |
| 16 | Key Decisions | §2 Technical Decisions (ADR) + Detail 1.B |
| 17 | Open Questions | §5 Concerns / Open Questions |
Detail 1.B — Decisions Closed (cross-layer)
| Decision | Chosen option | Alternatives rejected | Why rejected | Layer |
|---|---|---|---|---|
| 1. Source scoping | Extend to division-scoped (division_id + one-per-division) | Keep existing one-per-company | PRD §6/S05/NEG-3 mandate one-Division-one-source; current code only guards one-per-company | both |
| 2. Storage | Reuse existing ai_knowledge_sources + agent-conversation table (ChatbotGpt DB) | New bespoke tables | Type, tables, serializers already exist and are wired to AI-Service | BE |
| 3. Ingest sync | Async daily sidekiq-cron worker re-posting to AI-Service | Sync on request / real-time | Heavy work; PRD §5 defers real-time; matches existing nightly job pattern | BE |
| 4. Status propagation | Poll (get_vector_status) as baseline; webhook as enhancement | Webhook-only | Webhook contract undefined (OQ#4); poll already works | BE |
| 5. Vector indexing | Reuse AI-Service type:'conversation' contract | Build vectors in chatbot | No local vector DB; Data & AI owns it | BE/Data |
| 6. 15-agent cap enforcement | Both FE (counter+validation) and BE (contract ≤15) | FE-only | Defense-in-depth; BE is source of truth | both |
| 7. Delete semantics | Hard-delete vectors only when not in use; unlink = soft-delete join row | Always hard-delete | PRD S03/ERR-1 protects in-use sources | both |
| 8. PII masking ownership | Data & AI pipeline (no chatbot masking) | Mask in chatbot | Team boundary; isolated service requirement (PRD §6/§15) | Data |
| 9. Plan/tier gating | Reuse SystemPreference flag | New flag system | Existing rollout-flag pattern (knowledge_stores/create.rb:36) | BE |
| 10. FE source-type wiring | Wire the existing "conversation-history" tile + new form | New drawer | Tile already declared in AddKnowledgeDrawer.vue | FE |
FE/BE alignment notes: API is snake_case JSON via Grape; FE consumes via
$apiMainwrapper. No REST-vs-gRPC or pagination disagreement (offset list reused). Error envelope is the existingErrorExceptionshape.
Detail 1.C — Per-Story Change Map
| Story id | Title | Layer scope | FE changes | BE changes | Composite AC ids | Acceptance criteria (verifiable) | RFC anchors |
|---|---|---|---|---|---|---|---|
| CONVHIST-S01 | Source configuration | FE + BE | ConversationHistoryForm; service add_conversation(); wire tile | extend create contract (division_id); reuse POST …/omnichannel-agent-conversations | S01/AC-1..3, ERR-1 | rspec on create use case passes; row appears with IN_PROGRESS; vitest on form; toast shown | §2.4 · §2.A · §4.D ch.3,6 · §1 PRD-to-Schema r1 |
| CONVHIST-S02 | Remove from agent (unlink) | FE + BE | "−" row action + confirm | soft-delete ai_agent_knowledges join (no vector delete) | S02/AC-1..3, ERR-1 | rspec: join row deleted_at set, source row intact; agent stops referencing | §2.4 · §2.A · §4.D ch.7 |
| CONVHIST-S03 | Delete source | FE + BE | Delete + in-use guard dialog | DELETE :id; if in-use → 422; else delete_vector_db + soft-delete | S03/AC-1..3, ERR-1 | rspec: in-use delete blocked 422; unused delete purges vectors; no restore | §2.4 · §3.B · §4.D ch.7 |
| CONVHIST-S04 | 15-agent cap | FE + BE | live "N/15" counter + validation message + Save disable | contract agents length ≤15 → 422 | S04/AC-1..3, ERR-1 | vitest: 16th selection disables Save + message; rspec: 16 agents → 422 | §2.A · §2.C · §2.4 · §4.D ch.4 |
| CONVHIST-S05 | Select whole division | FE + BE | division-first multiselect; select-all (cap 15); deselect individuals | divisions+agents read; persist division_id | S05/AC-1..3, ERR-1 | vitest: selecting division marks ≤15 agents; rspec: 2nd source/division blocked (NEG-3) | §2.A · §2.4 · §2.3 · §4.D ch.2,4 |
| CONVHIST-S06 | PII masking | Cross-squad | n/a — status surfaced only | n/a — Data & AI pipeline; chatbot sets Error on failure signal | S06/AC-1..3, ERR-1 | Data & AI alpha gate: 100% mask standard phone/email; chatbot shows Error on fail | §2.D · §2.F.2 · §3.D |
| CONVHIST-S07 | Quality gate (≥5 msgs) | Cross-squad | n/a | n/a — Data & AI ingestion filter | S07/AC-1..3, ERR-1 | Data & AI: 3-msg room skipped; ≥5 customer-facing indexed | §2.D resp. boundary |
| CONVHIST-S08 | Exclude whispers | Cross-squad | n/a | n/a — Data & AI ingestion filter | S08/AC-1..3, ERR-1 | Data & AI: only customer-facing bubbles indexed | §2.D · §3.D |
| CONVHIST-S09 | Recency prioritization | Cross-squad | n/a | n/a — Data & AI ranking | S09/AC-1..3, ERR-1 | Data & AI: latest resolution wins; deterministic tie-break | §2.D |
| CONVHIST-S10 | Daily maintenance sync | BE | status badge + Last Updated refresh | ConversationHistoryDailySyncWorker + schedule.yml 0 1 * * * Asia/Jakarta | S10/AC-1..3, ERR-1 | rspec-sidekiq: worker enqueues per active source; on fail prior index retained + alert | §2.F · §4.D ch.5 |
| CONVHIST-S11 | Reference room-id link | FE + BE | render reference link | room-view permission check on open | S11/AC-1..3, ERR-1 | cross-division open → 403; authorized → room opens | §3 authz · §2.4 |
| CONVHIST-S12 | Agent deactivation | BE | n/a | sync skips deactivated agents; data ages out at 90d | S12/AC-1..3, ERR-1 | rspec: deactivated agent → no new ingest; reactivation resumes without dup | §2.F |
| CONVHIST-S13 | Text-only | Cross-squad | n/a | n/a — Data & AI content filter | S13/AC-1..3, ERR-1 | Data & AI: non-text excluded; text caption indexed | §2.D |
Stories S06–S09, S13 are
Cross-squad(Data & AI pipeline) — chatbot's responsibility is to configure, trigger, and reflect status, per the §2.D Responsibility Boundary Matrix.
2. Technical Design
Detail 2.0 — Repo Reading Guide (read this first)
Repo Map (mermaid, both layers)
flowchart LR
subgraph fe["chatbot-fe (Nuxt 3 / Vue 3)"]
drawer["components/AddKnowledgeDrawer.vue"]
reslist["pages/ai-knowledge/training-sources/index.vue"]
klist["modules/ai-knowledge/views/training-source/knowledge-list.vue"]
svc["common/services/main/v1/ai-assist.ts"]
ep["common/services/main/endpoint.ts"]
end
subgraph be["chatbot (Rails / Grape)"]
api["app/api/frontend_service/v1/gpt/ai_knowledge_sources.rb"]
uc["use_cases/.../ai_knowledge_source/*"]
repo["repositories/gpt/ai_knowledge_sources/*"]
worker["workers/* (sidekiq) + config/schedule.yml"]
aisvc["lib/ai_service/gpt/knowledge_base.rb"]
end
subgraph infra
pg[("ChatbotGpt DB\nai_knowledge_sources +\nomnichannel_agent_conversations")]
redis[["Redis / Sidekiq"]]
extai(["Data & AI vector + masking service"])
end
drawer --> svc --> ep --> api
reslist --> svc
klist --> reslist
api --> uc --> repo --> pg
repo --> aisvc --> extai
worker --> repo
redis --> worker
Existing Code Anchors
| Layer | Path | Why the agent reads it | What pattern it teaches |
|---|---|---|---|
| BE | app/api/frontend_service/v1/gpt/ai_knowledge_sources.rb | The Grape resource hosting all knowledge endpoints incl. omnichannel-agent-conversations | Grape route + set_role + Dry::Matcher::ResultMatcher response shape |
| BE | app/core/use_cases/api/frontend_service/v1/gpt/ai_knowledge_source/create_omnichannel_agent_conversation_knowledge_source.rb | The create use case to extend (add division_id, ≤15 cap) | APIAbstractUseCase + contract do params + Dry::Monads::Do; one-per-company guard at L30–33; returns 202 |
| BE | app/core/repositories/gpt/ai_service/post_knowledge_sources.rb | Builds the AI-Service payload { id, type:'conversation', participant_ids } (L35–39) | how participant_ids are derived from the agent-conversation rows |
| BE | app/core/repositories/gpt/ai_knowledge_sources/update_status.rb | Status poll → maps AI-Service status to ACTIVE/uppercased (L32–40, L51–53) | poll-based status sync; conversation type handling |
| BE | app/core/use_cases/api/frontend_service/v1/gpt/ai_knowledge_source/auto_train_agent_conversation.rb | Existing manual "auto train" (rails_admin) — bails if source exists | shows there is no rolling daily sync yet |
| BE | lib/ai_service/gpt/knowledge_base.rb | AI-Service client: create_vector_db (L16), get_vector_status (L36) | external HTTP call shape + base path |
| BE | app/controllers/webhook_room_resolved_controller.rb | Inbound webhook pattern (for a future status webhook) | skip_before_action :verify_authenticity_token + UseCases::System::ReceiveWebhook* |
| BE | config/schedule.yml (+ config/initializers/sidekiq.rb:44) | Existing sidekiq-cron daily jobs at 0 1 * * * Asia/Jakarta | exact pattern for the new daily sync entry |
| BE | app/api/frontend_service/helpers/authorization_helpers.rb | set_role(roles) 403 enforcement | role authz (no Pundit) |
| BE | app/api/frontend_service/helpers/current_user_helpers.rb | current_user['organization_id'/'company_id'] | multi-tenancy scoping |
| FE | components/AddKnowledgeDrawer.vue | The drawer with a declared (un-wired) conversation-history tile (L440–443) | <script setup>, Pixel MpDrawer*, sourceTypeTiles, toast.notify |
| FE | modules/ai-knowledge/views/training-source/knowledge-list.vue | Status badge rendering (ACTIVE green / IN_PROGRESS orange / FAILED red) | MpBadge variant-color per status |
| FE | common/services/main/v1/ai-assist.ts | Service wrapper (get/add/add_url) | $apiMain(endpoint, {method,body,signal,lazy}) |
| FE | common/services/main/endpoint.ts | Endpoint registry | where to add the conversation endpoint entry |
Existing Contracts to Reuse, Extend, or Replace (BE)
| Contract | Status | Justification | Owner |
|---|---|---|---|
ai_knowledge_sources table + type agent_conversation_history | reuse | already models the source | BOT |
ai_knowledge_source_omnichannel_agent_conversations table | extend | add division_id (+ unique guard) | BOT |
POST …/omnichannel-agent-conversations | extend | add division_id + ≤15 cap to contract | BOT |
GET …/omnichannel-agent-conversation/:id | reuse | detail already serves agent list | BOT |
DELETE :id | extend | add in-use guard + conditional delete_vector_db | BOT |
GET …/usages | reuse | quota (currently quota=1 source) | BOT |
AI-Service type:'conversation' vector create + get_vector_status | reuse | Data & AI owns; contract verified live | Data & AI |
| Daily sync cron worker | new-with-justification | no rolling-window job exists today (only manual auto_train) | BOT |
| Inbound training-status webhook | new-with-justification | poll exists but PRD §9#3 wants webhook; contract undefined (OQ#4) | Data & AI + BOT |
Patterns to Follow
| Layer | Concern | Pattern in repo | Reference file | Deviation? |
|---|---|---|---|---|
| BE | HTTP handler shape | Grape resource + set_role + ResultMatcher | app/api/frontend_service/v1/gpt/ai_knowledge_sources.rb | none |
| BE | Use case | APIAbstractUseCase + contract + Dry::Monads::Do | create_omnichannel_agent_conversation_knowledge_source.rb | none |
| BE | Repository / DB | AbstractRepository, Success/Failure | repositories/gpt/ai_knowledge_sources/update_status.rb | none |
| BE | Worker / cron | Sidekiq::Worker + config/schedule.yml | app/workers/refresh_ai_agent_vector_status_worker.rb + config/schedule.yml | none |
| BE | Serializer | Grape::Entity | app/api/frontend_service/v1/entities/gpt/ai_knowledge_source/knowledge_source.rb | none |
| BE | Error shape | FrontendService::Exceptions::ErrorException | app/api/frontend_service/helpers/error_response_helpers.rb | none |
| BE | Feature flag | SystemPreference.find_by(group_code:, code:, enabled:) | app/core/repositories/knowledge_stores/create.rb:36 | none |
| BE | Logging | Rollbar.error/info | (repo-wide; e.g. create use case L42) | none |
| FE | Component | <script setup> + Pixel Mp* | components/AddKnowledgeDrawer.vue | none |
| FE | State | Pinia (store/ai-assist, store/ai-agent) | store/ai-assist/* | none |
| FE | API client | $apiMain wrapper | common/services/main/v1/ai-assist.ts | none |
| FE | Toast / error | toast.notify({position,variant,title}) | components/AddKnowledgeDrawer.vue (~L632–646) | none |
| FE | Status badge | MpBadge variant-color green/orange/red | modules/ai-knowledge/views/training-source/knowledge-list.vue | none |
| Cross | snake_case API → FE | JSON snake_case consumed directly via $apiMain | common/services/main/v1/ai-assist.ts | none |
| FE | i18n | none (no i18n framework) | — | deviation: strings hard-coded inline (PRD validation copy) |
Reading Order for the Agent
app/api/frontend_service/v1/gpt/ai_knowledge_sources.rb— endpoint surface + auth.…/create_omnichannel_agent_conversation_knowledge_source.rb— the use case to extend.app/core/repositories/gpt/ai_service/post_knowledge_sources.rb— AI-Service payload.app/core/repositories/gpt/ai_knowledge_sources/update_status.rb— status poll.config/schedule.yml+config/initializers/sidekiq.rb— cron pattern for daily sync.app/controllers/webhook_room_resolved_controller.rb— inbound webhook pattern.db/chatbot_gpt_schema.rb(L137, L150, L173) — tables to migrate.components/AddKnowledgeDrawer.vue— the tile to wire + drawer pattern.common/services/main/v1/ai-assist.ts+endpoint.ts— FE service wiring.modules/ai-knowledge/views/training-source/knowledge-list.vue— status badge.
Source Verification (anti-hallucination — required)
| Layer | Anchor / pattern / contract | Verified by | Evidence |
|---|---|---|---|
| BE | ai_knowledge_source_omnichannel_agent_conversations table | grep schema | db/chatbot_gpt_schema.rb:137 create_table "…omnichannel_agent_conversations"; FK to ai_knowledge_sources at L492 |
| BE | ai_knowledge_source_types (code unique) | grep schema | db/chatbot_gpt_schema.rb:150; unique index on code (L159) |
| BE | ai_knowledge_sources | grep schema | db/chatbot_gpt_schema.rb:173; FK to types L494 |
| BE | type code agent_conversation_history | grep | used in search.rb:39, update_status.rb:36, create_…rb:27, post_knowledge_sources.rb:23/35 |
| BE | create endpoint | read | ai_knowledge_sources.rb:259 post '/omnichannel-agent-conversations'; set_role %w[owner supervisor admin] L260 |
| BE | detail / delete / usages endpoints | read | :229 get '/omnichannel-agent-conversation/:id', :80 delete ':id', :281 get '/usages' |
| BE | create use case (no cap, no division, one-per-company guard) | read | contract required(:agents).array(:hash){ required(:id), required(:name) } + company_id (L16–21); guard L30–33; no division_id, no length cap; returns 202 (L53) |
| BE | AI-Service conversation payload | read | post_knowledge_sources.rb:39 knowledges << { id:…, type:'conversation', participant_ids: agent_ids } |
| BE | status poll | read | update_status.rb:14 get_vector_status; L32 find type=='conversation'; L52 success→ACTIVE else upcase |
| BE | manual auto-train (not cron) | read + grep | auto_train_agent_conversation.rb bails "already exist" L26; caller is config/initializers/rails_admin.rb:189 (rails_admin form), not schedule.yml |
| BE | sidekiq-cron daily pattern | read | config/schedule.yml L26–29 (0 1 * * * Asia/Jakarta), L61–63; loaded by config/initializers/sidekiq.rb:44–46 |
| BE | inbound webhook pattern | read | app/controllers/webhook_room_resolved_controller.rb skip_before_action :verify_authenticity_token + UseCases::System::ReceiveWebhookRoomResolved |
| BE | multi-tenancy + authz | (agent) read | current_user_helpers.rb:6 set_current_user; authorization_helpers.rb:6 set_role |
| BE | feature flag | (agent) read | SystemPreference.find_by(group_code:'rollout', code:…, enabled:true) at knowledge_stores/create.rb:36 |
| BE | test/build commands | read AGENTS.md | rspec, rubocop, brakeman, rails db:migrate, bundle exec sidekiq (AGENTS.md test section) |
| FE | conversation-history tile declared but un-wired | read | components/AddKnowledgeDrawer.vue:440–443 tile id:"conversation-history"; active SOURCE_TYPE_OPTIONS L471–474 + sourceTypeOptions L478–492 exclude it |
| FE | service wrapper | read | common/services/main/v1/ai-assist.ts get/add/add_url/save_url via $apiMain(endpoint.v1.ai_assist.*, …) |
| FE | status badge | (agent) read | knowledge-list.vue MpBadge green/orange/red for ACTIVE/IN_PROGRESS/FAILED |
| FE | stack + DS | (agent) read | Nuxt 3.12.2, Vue 3, Pinia; @mekari/pixel3@1.0.12 (package.json) |
| FE | no i18n | (agent) grep | no vue-i18n / locales dir |
| FE | build/test | (agent) read package.json | pnpm dev/build/test (vitest)/test:e2e (playwright)/lint:ts/lint:prettier |
| Design | prototype files exist + gaps | (agent) read | qontak-designer/app/components/bot-automation/sources/AddKnowledgeDrawer.vue (toggle + 9 hardcoded agents, no cap/tooltip), SourceDetailDrawer.vue (Active/Inactive only), app/pages/bot-automation/resources/index.vue |
Design ↔ Code Mapping (frontend half)
| Figma frame / component | Implementing file (path) | Reuse vs new | Tokens used | Backing API endpoint(s) | Deviation |
|---|---|---|---|---|---|
| Add source picker | chatbot-fe/components/AddKnowledgeDrawer.vue | extended (wire tile L440) | Pixel MpDrawer*, color.icon.brand, text.default | POST …/omnichannel-agent-conversations | none — tile exists |
| Conversation History config form | chatbot-fe/components/.../ConversationHistoryForm.vue (new) | new | MpSelect, MpCheckbox, MpBadge, MpTooltip | divisions+agents read; POST …/omnichannel-agent-conversations | design pending (OQ#7) |
| Resources list | chatbot-fe/pages/ai-knowledge/training-sources/index.vue + knowledge-list.vue | extended | MpTable, MpBadge | GET …/, DELETE :id | training/error states need design |
| Source detail | chatbot-fe/components/.../SourceDetailDrawer.vue (new) | new | MpDrawer*, MpTable | GET …/omnichannel-agent-conversation/:id | mirror prototype read-only table |
Detail 2.1 — Architecture (mermaid)
End-to-end component diagram
flowchart TB
admin([Admin / Super Admin]) --> drawer["AddKnowledgeDrawer → ConversationHistoryForm (FE)"]
drawer --> svc["ai-assist.ts ($apiMain)"]
svc --> api["/v1/gpt/ai_knowledge_sources/omnichannel-agent-conversations/"]
api --> uc["CreateOmnichannelAgentConversationKnowledgeSource"]
uc --> repo["AiKnowledgeSourceOmnichannelAgentConversations::Create"]
repo --> pg[("ChatbotGpt DB")]
uc --> post["AiService::PostKnowledgeSources"]
post --> aisvc["AiService::Gpt::KnowledgeBase"]
aisvc --> extai(["Data & AI: mask + index (type:conversation)"])
cron[["schedule.yml 0 1 * * *"]] --> dsync["ConversationHistoryDailySyncWorker (new)"]
dsync --> post
poll["ConversationHistoryStatusSyncWorker / UpdateStatus"] --> aisvc
poll --> pg
wh["/webhooks/.../conversation-training (new, blocked OQ#4)/"] -.-> pg
Data model (mermaid erDiagram)
erDiagram
AI_KNOWLEDGE_SOURCE_TYPES ||--o{ AI_KNOWLEDGE_SOURCES : classifies
AI_KNOWLEDGE_SOURCES ||--o{ AI_KS_OMNI_AGENT_CONVERSATIONS : has
AI_KNOWLEDGE_SOURCES ||--o{ AI_AGENT_KNOWLEDGES : linked_to_agent
AI_KNOWLEDGE_SOURCE_TYPES {
bigint id PK
string code "agent_conversation_history"
string name
int sequence
}
AI_KNOWLEDGE_SOURCES {
bigint id PK
string company_id
string organization_id
bigint ai_knowledge_source_type_id FK
string division_id "NEW (Decision 1) — one source per company+division"
string status "IN_PROGRESS|ACTIVE|FAILED"
string name
timestamptz updated_at "Last Updated"
timestamptz deleted_at
}
AI_KS_OMNI_AGENT_CONVERSATIONS {
bigint id PK
bigint ai_knowledge_source_id FK
string agent_id
string agent_name
string company_id
timestamptz deleted_at
}
AI_AGENT_KNOWLEDGES {
bigint id PK
bigint ai_knowledge_id FK
bigint ai_agent_id
timestamptz deleted_at "unlink = soft delete"
}
State machine — source status
stateDiagram-v2
[*] --> IN_PROGRESS: create (POST omnichannel-agent-conversations)
IN_PROGRESS --> ACTIVE: AI-Service status=success (poll/webhook)
IN_PROGRESS --> FAILED: masking/index fail
FAILED --> IN_PROGRESS: retry / next daily sync
ACTIVE --> ACTIVE: daily sync (ingest new, purge >90d, Last Updated advances)
ACTIVE --> [*]: delete source (not in use) + hard-delete vectors
FAILED --> [*]: delete source
Branch & skip flow — plan gate + one-per-division
flowchart TD
start([Admin opens Add Knowledge]) --> q1{Qontak360 plan flag on?}
q1 -- no --> hide[Hide Conversation history tile NEG-1]
q1 -- yes --> q2{Division already has a source?}
q2 -- yes --> block[Block: one division = one source NEG-3]
q2 -- no --> q3{agents count > 15?}
q3 -- yes --> cap[Validation; Save disabled S04]
q3 -- no --> save[Create source → IN_PROGRESS]
Detail 2.2 — Sequence (mermaid, incl. failure paths)
Happy path — create + train
sequenceDiagram
actor A as Admin (FE)
participant LB as Load Balancer
participant API as chatbot Grape API
participant UC as CreateOmni…UseCase
participant DBW as ChatbotGpt DB (primary)
participant POST as PostKnowledgeSources
participant EXT as Data & AI vector/mask svc
participant POLL as StatusSync (poll)
A->>LB: POST /v1/gpt/.../omnichannel-agent-conversations {division_id, agents[≤15]}
LB->>API: HTTP
API->>API: set_role(owner/supervisor/admin)
API->>UC: handle(params)
UC->>UC: validate ≤15 agents; division not already sourced
UC->>DBW: BEGIN; upsert source (division_id) + insert agent-conversation rows; COMMIT
UC->>POST: build {id, type:'conversation', participant_ids}
POST->>EXT: POST /knowledge-base/vectors
Note right of EXT: ingestion+mask+index async (~1h, isolated svc)
EXT-->>POST: 202 accepted
UC-->>API: 202 (status IN_PROGRESS)
API-->>A: 202 row "Training in Progress"
POLL->>EXT: GET /vectors/status/:company_id (periodic)
EXT-->>POLL: status=success
POLL->>DBW: UPDATE status=ACTIVE; updated_at=now (Last Updated)
Failure path — masking/index fails
sequenceDiagram
participant POLL as StatusSync (poll)
participant EXT as Data & AI svc
participant DBW as ChatbotGpt DB
participant ALERT as Rollbar / Slack BOT alert
POLL->>EXT: GET /vectors/status/:company_id
EXT-->>POLL: status=failed (masking/index error)
POLL->>DBW: UPDATE status=FAILED (retain prior index)
POLL->>ALERT: alert (3 consecutive failures → Slack BOT)
Note over DBW: "Last Successful Sync" preserved; Admin notified (S06/ERR-1, S10/ERR-1)
Daily sync (rolling 90 days)
sequenceDiagram
participant CRON as sidekiq-cron 0 1 * * * Asia/Jakarta
participant W as ConversationHistoryDailySyncWorker (new)
participant DBW as ChatbotGpt DB
participant POST as PostKnowledgeSources
participant EXT as Data & AI svc
CRON->>W: enqueue
W->>DBW: list ACTIVE agent_conversation_history sources
loop per source
W->>POST: re-post {type:'conversation', participant_ids}
POST->>EXT: POST /vectors (ingest prior 24h, purge >90d)
EXT-->>POST: 202
end
W->>DBW: on success advance updated_at; on fail retain + alert (S10/ERR-1)
Detail 2.3 — Database Model (DDL)
Tooling: Rails migrations against the
chatbot_gptdatabase — migrations live indb/chatbot_gpt_migrate/(perconfig/database.yml:43 migrations_paths), via theChatbotGptRecordconnection (app/models/chatbot_gpt_record.rb). Schema dump:db/chatbot_gpt_schema.rb. The source/type/agent-conversation tables already exist — this RFC adds one column + one partial-unique guard. Match the verified existing partial-index syntax (where: 'deleted_at is null', e.g.db/migrate/20240724144048_unique_knowledge_store.rb:4).
Where
division_idlives — Decision 1 (canonical = the source row). The "one Division = one source" rule (NEG-3) is a property of the source, sodivision_idis added toai_knowledge_sources(the source the existing one-per-company guard already keys on), not the agent-conversation rows. Nullable: onlyagent_conversation_historysources set it; FILE/URL sources leave it null. This makes the existing existence guard division-aware with a DB backstop.
# db/chatbot_gpt_migrate/2026XXXX_add_division_id_to_ai_knowledge_sources.rb
class AddDivisionIdToAiKnowledgeSources < ActiveRecord::Migration[7.1]
def change
add_column :ai_knowledge_sources, :division_id, :string # nullable; set only for conversation sources
# One conversation_history source per (company, division). Partial-unique (paranoid soft-delete),
# matching the verified repo pattern (where: 'deleted_at is null').
add_index :ai_knowledge_sources,
%i[company_id division_id ai_knowledge_source_type_id],
unique: true,
where: 'deleted_at IS NULL AND division_id IS NOT NULL',
name: 'index_ai_knowledge_sources_on_company_division_type'
end
end
- No new tables. Reuses
ai_knowledge_sources(schema L173),ai_knowledge_source_types(L150),ai_knowledge_source_omnichannel_agent_conversations(L137). Onlyai_knowledge_sources.division_idis added. The agent-conversation rows (agent_id,agent_name,company_id,ai_knowledge_source_id) are unchanged — they remain the per-agent membership of the (now division-scoped) source. - Cardinality: ≤1 conversation source per (company, division); ≤15 agent rows per source.
- PII classification per column:
agent_id/agent_name/company_id/division_id= internal identifiers (not customer PII). No raw customer chat content is stored in chatbot — masked conversation vectors live only in the Data & AI vector store. - Retention: rolling 90-day vectors (Data & AI store); source/agent rows retained until source
delete (
acts_as_paranoidsoft delete, then hard vector delete on confirmed delete).
Per-status lifecycle (ai_knowledge_sources.status):
| Status | Visibility | Retention | Restore semantics | Transitions allowed |
|---|---|---|---|---|
IN_PROGRESS | list row "Training in Progress" | until status changes | n/a | → ACTIVE, → FAILED |
ACTIVE | list row "Active" + Last Updated | rolling 90d (vectors) | n/a | → ACTIVE (sync), → deleted |
FAILED | list row "Error" / "Last Successful Sync" | prior index retained | retry/next sync | → IN_PROGRESS, → deleted |
deleted (deleted_at) | hidden | hard delete vectors | no restore (re-create) | terminal |
Detail 2.4 — APIs
Outbound endpoints (consumers call us)
| Endpoint | Method | AuthN/AuthZ | Request schema | Response schema | Status codes | Idempotency | Versioning | Reuse? |
|---|---|---|---|---|---|---|---|---|
/v1/gpt/ai_knowledge_sources/omnichannel-agent-conversations | POST | set_role(owner/supervisor/admin); company_id from current_user (not body) | { division_id (NEW), agents:[{id,name}] (≤15 NEW) } | { status, code, message, data:[{id, agent_id, agent_name, ai_knowledge_source_id}] } | 202, 422 (>15 / dup-division / type-not-found), 403 | division-aware existence guard (NEG-3) | v1 | extended |
/v1/gpt/ai_knowledge_sources/omnichannel-agent-conversation/:id | GET | same | path :id | { data:[{agent_id, agent_name}], message, status_code } (built in use case; no Grape entity) | 200, 404, 403 | n/a | v1 | reused |
/v1/gpt/ai_knowledge_sources | GET | same | filters incl. source_type | { status, code, message, data:[{id, name, type:{code,name}, status, updated_at, url}], meta:{page,limit,total_data,total_pages,last_page} } | 200, 403 | n/a | v1 | reused |
/v1/gpt/ai_knowledge_sources/:id | DELETE | same | path :id | { status, code, message } | 200, 422 (in-use S03/ERR-1), 403 | n/a | v1 | extended (in-use guard + conditional vector delete) |
/v1/gpt/ai_knowledge_sources/usages | GET | same | — | { status, code, message, data:[{type, usage, qouta, is_available}], meta } — note literal field is qouta (sic) | 200, 403 | n/a | v1 | reused |
GET /v1/divisions + GET /v1/divisions/:id/agents (config modal directory) | GET | set_role(owner/supervisor/admin) + company scope | path/query | { data:[{division_id, name, agents:[{id, name}]}] } (proposed) | 200, 403 | n/a | v1 | new-with-justification — verified: no GET directory endpoint exists in frontend_service (only internal sync PATCH /v1/chat/divisions, role qontak_chat, app/api/internal_service/v1/chat/division.rb:20). Build from Division/AgentsDivision models, or proxy the Omnichannel directory. |
Grounded payload examples (field names verified against the Grape entities; see Source
Verification). company_id is always taken from current_user, never the request body.
Create — request (POST omnichannel-agent-conversations):
{ "division_id": "div-123", "agents": [ { "id": "agent-1", "name": "Alfian Ramadhan" }, { "id": "agent-2", "name": "Christin Purnama Sari" } ] }
Create — response 202 (Entities::Gpt::AiKnowledgeSource::CreateOmnichannelAgentConversationKnowledgeSource):
{ "status": "success", "code": 202, "message": "Success to create AI Knowledge Source Omnichannel Agent Conversation",
"data": [ { "id": 10, "agent_id": "agent-1", "agent_name": "Alfian Ramadhan", "ai_knowledge_source_id": 55 } ] }
List — response 200 (…/get_response.rb → KnowledgeSource + MetaPaginate):
{ "status": "success", "code": 200, "message": "OK",
"data": [ { "id": 55, "name": "Conversation History — Sales", "type": { "code": "agent_conversation_history", "name": "Conversation History" }, "status": "IN_PROGRESS", "updated_at": "2026-06-20T01:00:00Z", "url": null } ],
"meta": { "page": 1, "limit": 10, "total_data": 1, "total_pages": 1, "last_page": true } }
Usages — response 200 (…/get_usage_response.rb → Usage; quota hard-coded 1 for this type):
{ "status": "success", "code": 200, "message": "OK",
"data": [ { "type": "agent_conversation_history", "usage": 1, "qouta": 1, "is_available": false } ], "meta": {} }
⚠️ FE must read the misspelled
qoutakey (verified inentities/.../usage.rb) — do not "correct" it toquotaon the client. Rate limits / pagination follow the existing list endpoint (offset).
Inbound webhooks (other services call us)
| Endpoint | Method | AuthN/AuthZ | Source service | Request schema | Response | Status | Idempotency | Versioning |
|---|---|---|---|---|---|---|---|---|
/webhooks/v1/conversation-training-status (NEW, BLOCKED) | POST | TBD (Data & AI contract, OQ#4) — follow WebhookRoomResolvedController UUID pattern or HMAC | Data & AI training pipeline | { company_id, status: in_progress|completed|failed, last_updated, error? } | { ok } | 200, 401, 422 | by (company_id, source_id, status) | v1 |
Webhook is not buildable until Data & AI publishes the contract (OQ#4). The poll-based
UpdateStatuspath is the shipping baseline; the webhook is an enhancement (Decision 4/6).
Detail 2.A — UI Contract
For ConversationHistoryForm.vue (new) and SourceDetailDrawer.vue (new):
- Figma frame URL:
n/a — design pending(config form) / AI Resources 10859-83239 (detail). - Implementation file:
chatbot-fe/components/ai-knowledge/ConversationHistoryForm.vue,…/SourceDetailDrawer.vue. - Props type:
interface ConversationHistoryFormProps {
visible: boolean;
maxAgents?: number; // default 15
}
interface ConversationHistoryFormState {
divisionId: string | null;
selectedAgentIds: Set<string>; // capped at maxAgents
}
- State ownership: local
<script setup>refs; persisted viastore/ai-assistactionADD_CONVERSATION_HISTORYcallingai-assist.ts add_conversation(). - Event payloads (analytics, PRD §12):
knowledge_source_add_click {source_type:'conversation_history'};conv_history_config_save {division_id, agent_count(1–15), source_id}. - Conditional rendering: tile hidden when plan flag off (NEG-1); Save disabled until
divisionId && 1≤count≤15; validation message at 16th selection. - A11y:
MpSelect/MpCheckboxkeyboard nav; counter hasaria-live="polite"; validation message linked viaaria-describedby.
Detail 2.B — Data-Fetching Strategy
- Library:
$apiMainwrapper (NuxtuseFetch-based) viacommon/services/main/v1/ai-assist.ts. - Cache key: per existing list (no custom SWR cache); refetch on drawer open + after save/delete.
- Optimistic updates: no — Save awaits 202, then refetch list (status starts
IN_PROGRESS). - Status polling on the list view: re-fetch list on interval/visit while any row is
IN_PROGRESS(matches existing knowledge-list refresh).
Detail 2.C — UI State Matrix
| Surface | Loading | Empty | Error | Partial | Success |
|---|---|---|---|---|---|
| Config form (division/agents) | directory fetch spinner | no divisions/agents → disabled Save | directory load error + Retry (ERR-1) | division chosen, agents loading | ≤15 selected → Save enabled |
| 15-cap counter | n/a | "0/15 selected" | "Please limit selection to your top 15 experts" + Save disabled | "N/15 selected" | "15/15 selected" valid |
| Resources list | list fetch skeleton | empty-state + "Add source" CTA | list load error + Retry | rows w/ mixed status | rows w/ ACTIVE + Last Updated |
| Status badge | — | — | Error/"Last Successful Sync" (red) | Training in Progress (orange) | Active (green) |
| Source detail | detail fetch | n/a | load error | — | agent·division read-only table |
Detail 2.D — Data Integrity Matrix
| Write path | Transaction scope | Partial failure behavior | Idempotency key + TTL | Consistency | Duplicate handling | Stale-read |
|---|---|---|---|---|---|---|
| Create source + agent rows | ChatbotGptRecord.transaction (rows atomic) | rollback rows; 422; AI-Service post outside trx → retry via sync | one-per-division guard | strong (DB) for rows; eventual for vectors | dup source → 422 already_exist | list may briefly show IN_PROGRESS before vectors ready |
| Daily sync re-post | per-source (no cross-source trx) | one source fails → others proceed; alert | source_id + sync date | eventual (Data & AI store) | re-post is upsert by participant_ids | "Last Updated" advances only on success |
| Delete source | soft-delete row + vector hard-delete | if vector delete fails → keep soft-deleted, retry | source_id | eventual | re-delete is no-op | row hidden immediately |
Detail 2.E — Concurrency Collision Map
| Resource | Writers | Collision | Resolution | On conflict |
|---|---|---|---|---|
ai_knowledge_sources (one per division) | two Admins creating same division source | both pass pre-check, both insert | DB partial-unique guard (Decision 1) | 2nd insert → 422 already_exist (NEG-3) |
| Same source: daily sync vs Admin delete | cron worker + Admin | sync re-posts a deleting source | worker skips soft-deleted rows; check deleted_at before post | sync no-ops |
| Status: poll vs webhook (future) | UpdateStatus + webhook | both write status | last-writer-wins on updated_at; webhook authoritative when present | idempotent by terminal status |
Detail 2.F — Async Job / Event Consumer Spec
| Job/Consumer | Trigger | Input | Retry | DLQ/retention | Concurrency | Idempotency | Timeout | Poison handling |
|---|---|---|---|---|---|---|---|---|
ConversationHistoryDailySyncWorker (new) | sidekiq-cron 0 1 * * * Asia/Jakarta | none (queries active sources) | sidekiq_options retry: 3 (match workers) | Sidekiq dead set (default) | queue application_maintenance (per schedule.yml peers) | per source_id + date | per-source AI-Service call timeout (follow AiService client) | log + Rollbar; skip source, continue |
ConversationHistoryStatusSyncWorker (poll, reuses UpdateStatus) | periodic while any IN_PROGRESS | company_id | retry: 1 (match update_status rescue) | n/a (rescues + returns true) | low | terminal-status idempotent | client timeout | swallow + Rollbar (existing behavior) |
| training-status webhook consumer (new, blocked OQ#4) | inbound POST | {company_id,status,...} | per webhook contract | per contract | n/a | (company_id,source_id,status) | n/a | reject 422 on unknown company |
Detail 2.F.1 — Responsibility Boundary Matrix
| Step (execution order) | Owning squad / service | Inbound trigger | Outbound effect | Failure handler | PRD anchor |
|---|---|---|---|---|---|
| 1. Config + persist source/agents/division | BOT (chatbot) | Admin Save | DB rows + AI-Service post | 422 + rollback | S01, S05, NEG-3 |
| 2. Fetch resolved rooms (90d) by agent | Inbox/Platform (ChatService) | Data & AI ingestion | room data to Data & AI | retry in ingestion | §15, S10 |
| 3. Quality gate / whisper exclude / text-only / recency | Data & AI | ingestion | filtered content | skip non-qualifying | S07, S08, S09, S13 |
| 4. PII masking | Data & AI | ingestion | masked text | room skipped + status fail | S06, NEG-4 |
| 5. Vector index (type:conversation) | Data & AI | ingestion | vectors + status | status=failed | S06 |
| 6. Status reflect (poll/webhook) | BOT (chatbot) | poll / webhook | update status/updated_at | retain prior; alert | S06/ERR-1, S10/ERR-1 |
| 7. Daily rolling sync + purge >90d | BOT trigger + Data & AI purge | cron | re-ingest/purge | retain index; alert | S10, S12 |
| 8. Reference room-id link permission | BOT (chatbot) | answer render | room link if permitted | 403 cross-division | S11/ERR-1, OQ#5 |
The PRD allocates masking/training/webhook to Data & AI (§15) — this matrix matches it. The
division_idscoping (step 1) is the chatbot-side change vs the existing one-per-company guard.
Detail 2.F.2 — State Surface Contract
| Entity | State field / event | Default | Updated by | Read via | Stale window |
|---|---|---|---|---|---|
| Conversation History source | ai_knowledge_sources.status | IN_PROGRESS on create | StatusSync poll (+ future webhook) | GET …/ list, detail | up to poll interval (~minutes) |
| Source freshness | ai_knowledge_sources.updated_at ("Last Updated") | create time | daily sync worker on success | list/detail | ≤ 24h |
Detail 2.G — Cross-Layer Contract Verification
| Endpoint | BE response schema | FE expected schema | Match? | Gaps |
|---|---|---|---|---|
POST …/omnichannel-agent-conversations | { status, code, message, data:[{id, agent_id, agent_name, ai_knowledge_source_id}] } snake_case | form sends {division_id, agents[]}; reads data[].id | yes | company_id server-side (not body); add division_id to params; 422 codes for >15 / dup-division specified (§3.B) |
GET …/ list | rows {id,name,source_type,status,updated_at} | list expects status enum + updated_at | yes | confirm status string mapping ACTIVE/IN_PROGRESS/FAILED ↔ badge |
DELETE :id | {message} / 422 in-use | dialog expects in-use error code | partial | define in-use error code/message envelope |
GET /v1/divisions(/:id/agents) (directory) | proposed { data:[{division_id, name, agents:[{id,name}]}] } | division-first multiselect | n/a — new endpoint | verified: no existing GET directory (only internal PATCH /v1/chat/divisions); build in chunk 2b — no contract mismatch, net-new |
Any "no"/"partial" is a blocker — resolved during chunk 1 (contract confirmation) or moved to §5.
Detail 2.H — End-to-End Data Flow
Admin selects division + ≤15 agents → ConversationHistoryForm → POST omnichannel-agent-conversations → CreateUseCase (validate, persist rows+division_id, transaction) → PostKnowledgeSources → AI-Service (mask+index, async) → 202 IN_PROGRESS → list shows "Training in Progress" → StatusSync poll → status=ACTIVE + Last Updated → daily sync maintains 90d window → Agent asks Airene → indexed conversation answer (+ permission-scoped room reference).
- Side effects: analytics events (§12); Rollbar/Slack alerts on failure; PaperTrail audit on source rows.
- Ownership per step: see §2.F.1.
Detail 2.I — Scope Boundaries
- BE create:
app/core/use_cases/.../create_omnichannel_agent_conversation_knowledge_source.rb(extend contract); migration fordivision_id; newConversationHistoryDailySyncWorker+config/schedule.ymlentry; extendDELETEuse case in-use guard. - BE NOT touched: AI-Service internals; PII masking; vector store;
auto_train_agent_conversation(manual tool stays). - FE create:
ConversationHistoryForm.vue,SourceDetailDrawer.vue;ai-assist.ts add_conversation();endpoint.tsentry; wire tile inAddKnowledgeDrawer.vue. - FE NOT touched: existing PDF/URL/text flows; Inbox Copilot rendering (reference link is additive).
- Shared modules:
AddKnowledgeDrawer.vueis shared with PDF/URL/text — additive wiring only (impact: low; existing types unaffected).
Detail 2.J — Asset Inventory
| Asset | Type | Source | Format & sizes | Path |
|---|---|---|---|---|
| "employee" source tile icon | icon | @mekari/pixel3 (already referenced L442) | Pixel icon font/SVG | bundled |
| status badge dot | (DS) | @mekari/pixel3 MpBadge | n/a | bundled |
No new bespoke assets; reuse Pixel design-system primitives. Any new illustration → design review.
3. High-Availability & Security
HA: the heavy ingestion runs in the Data & AI isolated service (PRD §6) — no chatbot pod load.
Chatbot stays stateless; create/list/delete are standard Grape requests; the daily sync is a
sidekiq-cron worker on the existing application_maintenance queue. If the AI-Service is down,
create returns IN_PROGRESS and the sync/poll retries; prior index is retained on sync failure.
Performance Requirement
- Frontend: drawer interactions < 100ms; list render reuses existing virtualization; no measurable bundle delta beyond one form component + Pixel imports.
- Backend: create is a small transactional write + one async AI-Service post; reuses existing endpoint capacity. Initial 15-agent training reaches ACTIVE within ~1h (Data & AI SLA, PRD §6).
- Load: daily sync iterates ACTIVE sources (bounded; ≤1 per division per company) at 01:00 Jakarta
off-peak — matches
delete_old_logs_job/partition_chatbot_gpt_maintenancewindow.
Monitoring & Alerting (PRD §12)
- Events:
knowledge_source_add_click,conv_history_config_save,conv_history_training_status,conv_history_daily_sync. - BE: Rollbar on use-case/worker exceptions (existing
Rollbar.error); Sidekiq queue latency metric already shipped (SendSidekiqQueueLatencyToDatadogWorker). - Alerts:
conv_history_training_status=failed3× consecutive for one source → Slack BOT channel;conv_history_daily_sync=failed→ Slack + escalate to Data & AI (PRD §12). - Dashboard owner: BOT squad (Hadiningbot).
Logging
- BE structured logs via existing Rollbar/Papertrail; log
company_id,source_id,status,agent_count. Never log raw chat content oragentPII payloads. - FE: no PII in client logs.
Security Implications
- Threat model: (a) cross-tenant data leak via reference room links; (b) over-broad agent selection; (c) PII leakage into vectors (owned by Data & AI masking); (d) unauthorized config by non-admins.
- AuthZ at every boundary via
set_role(%w[owner supervisor admin])+ company/organization scope.
Role × Endpoint Authorization Matrix
| Role | Endpoint(s) | Methods | Tenant scope | UI visibility | Constraint | Audit |
|---|---|---|---|---|---|---|
| Admin/Super Admin | all conversation endpoints | POST/GET/DELETE | own company/org | full config | one-per-division; ≤15 | PaperTrail |
| Agent | room-by-id (reference) | GET (read) | own permitted rooms | reference link only | room-view permission (S11) | n/a |
| Non-Qontak360 | none | — | — | tile hidden (NEG-1) | plan flag | n/a |
| Data & AI service | status webhook (future) | POST | per company_id | — | webhook auth (OQ#4) | sync logs |
- Ownership validation:
ai_knowledge_sources.company_id == current_user['company_id']enforced in use case/repository (current_user_helpers.rb). - Input validation:
agentsarray ≤15, each{id,name}filled (Dry contract);division_idfilled. - Injection: parameterized AR queries; outbound AI-Service URL is fixed config (no SSRF on user input).
- Secrets: AI-Service base URL/keys via existing config/env (no hard-coding).
- Reference-link permission (PRD OQ#5/S11): validate requesting user's room-view permission on every reference open; deny cross-division (403).
- Static analysis:
brakeman(BE),lint:ts(FE). - Compliance: see §3.D.
Detail 3.A — Failure Mode Catalog (merged)
| Surface | FE behavior on failure | BE response on failure | Codes consistent? |
|---|---|---|---|
| Directory load | load error + Retry, Save disabled (ERR-1) | 4xx from directory read | yes |
| Create > 15 agents | inline validation, Save disabled (S04/ERR-1) | 422 | yes |
| Create dup division | inline error (NEG-3) | 422 already_exist | yes |
| Delete in-use | dialog blocks + message (S03/ERR-1) | 422 in-use | yes |
| Training/masking fail | row shows Error / Last Successful Sync (S06/ERR-1) | status=FAILED (poll/webhook) | yes |
| Daily sync fail | Last Updated unchanged | retain prior index + alert (S10/ERR-1) | yes |
| Cross-division reference | permission error toast (S11/ERR-1) | 403 | yes |
Detail 3.A.1 — Branch & Skip Catalog
| Branch trigger | Where checked | Downstream effect | Audit | User-visible? |
|---|---|---|---|---|
| Not Qontak360 (plan flag off) | FE tile gate + BE plan check | tile hidden / create rejected (NEG-1) | n/a | yes (hidden) |
| Division already sourced | BE unique guard + FE pre-check | block create (NEG-3) | source row | yes (error) |
| Agent has 0 qualifying rooms | Data & AI ingestion | agent ignored, others proceed (NEG-2) | sync log | no |
| Internal whisper / non-text | Data & AI ingestion | excluded (NEG-4, S08, S13) | sync log | no |
| Agent deactivated | daily sync | no new ingest; ages out 90d (S12) | sync log | no |
Detail 3.B — Error Response Catalog (BE)
Shape: { "message": [...], "code": <http>, "errors": {...} } (existing ErrorException).
| Endpoint | Code | HTTP | Message | When | User-facing? |
|---|---|---|---|---|---|
| create | agent_limit_exceeded | 422 | "Please limit selection to your top 15 experts" | >15 agents (S04) | yes |
| create | already_exist | 422 | division already has a source | NEG-3 | yes |
| create | not_found (type) | 422 | source type missing | misconfig | no |
| delete | in_use | 422 | remove from AI Agents first | S03/ERR-1 | yes |
| reference | forbidden | 403 | no permission for this room | S11/ERR-1 | yes |
Detail 3.C — Error Message Catalog (FE)
| Error | User-facing message | Surface | i18n key |
|---|---|---|---|
| cap exceeded | "Please limit selection to your top 15 experts" | inline + Save disabled | hard-coded (no i18n) |
| directory load | "Couldn't load divisions/agents. Retry." | inline + Retry | hard-coded |
| save failed | "Failed to add Conversation History source." | toast (variant error) | hard-coded |
| delete in-use | "Remove this source from all AI Agents before deleting." | dialog | hard-coded |
Detail 3.D — Compliance & Data Governance (triggered: PII)
| Field | Classification | Legal basis | Retention | Encryption | Access audit | Right-to-delete |
|---|---|---|---|---|---|---|
| Raw chat content (pre-mask) | PII (customer) | UU PDP / existing consent (PRD §15) | not persisted post-processing (transient) | in transit (HTTPS) | Data & AI logs | discarded after masking |
| Masked conversation vectors | de-identified | legitimate interest / consent | rolling 90d | at rest (vector store) | Data & AI | purged on source delete (hard) |
agent_id/name/division_id | internal identifier | operational | until source delete | DB at rest | PaperTrail | soft→hard delete on source delete |
Masking correctness (100% standard phone/email) is the Internal Alpha gate (PRD §14) and an open risk owned by Data & AI (OQ#1).
Detail 3.E — Accessibility
WCAG AA: keyboard nav through division select + agent checkboxes; counter aria-live; validation
message aria-describedby; focus trap in drawer; color-independent status (badge text + color).
4. Backwards Compatibility and Rollout Plan
Compatibility
- BE: additive — new column (nullable, backfilled), extended contract (additive params), reused endpoints. Existing PDF/URL/text sources unaffected. No API version bump.
- FE: additive — wires an existing tile + new components; existing flows untouched.
- Migration: none for data (new
conversation_historycapability; no backfill of vectors). Thedivision_idcolumn backfills from existing agent rows where determinable, else stays null until re-config.
Rollout Strategy
- Deploy order: BE first (migration + extended create + sync worker behind flag), then FE
(wire tile). FE depends on BE create accepting
division_id. - Feature flag:
SystemPreference(group_code:'rollout', code:'conversation_history')default OFF, enabled per Qontak360 account. Single flag gates both BE create acceptance and FE tile. - Stages (PRD §11/§14): Internal Alpha (Mekari CS) → Closed Beta (5–10 Qontak360) → Open Beta/Limited GA (Service Suite + Pro-above) → GA.
- Stage gates: Alpha: 100% mask standard phone/email + <1h/15-agent training; Beta: >70% suggestion acceptance; Limited GA: no perf degradation in 24h sync window.
- Rollback: toggle flag OFF (hides tile, rejects new creates); existing sources stay until explicitly deleted. Sync worker no-ops when flag off.
- Stop conditions: widespread masking/index failures unresolved within SLA → PM disables flag for affected accounts (PRD §12).
Detail 4.A — Cross-Layer Rollout Compatibility Matrix
| Scenario | FE | BE | Works? | Mitigation |
|---|---|---|---|---|
| Pre-deploy | Old | Old | yes | baseline (tile un-wired) |
| Backend first | Old | New | yes | new column nullable; old FE never sends division_id |
| Frontend first | New | Old | no | avoid — deploy BE first (FE needs division_id accept) |
| Both deployed | New | New | yes | target |
| Backend rollback | New | Old | partial | flag OFF hides FE tile; FE degrades to no-conv-history |
| Frontend rollback | Old | New | yes | BE accepts but no UI creates; harmless |
Detail 4.B — Configuration Contract
| Layer | Env var / flag | Type | Default | Required | Provisioner | Secret? |
|---|---|---|---|---|---|---|
| BE | SystemPreference rollout/conversation_history | flag (bool) | OFF | yes | DB seed / admin | no |
| BE | AI-Service base URL/keys (existing) | env | existing | yes | config/Vault | yes |
| BE | daily sync schedule (config/schedule.yml) | cron | 0 1 * * * Asia/Jakarta | yes | repo | no |
| FE | reads plan flag via API | n/a | — | — | — | no |
Detail 4.C — Test Plan (commands sourced from repo)
| Layer | Command | What it must prove |
|---|---|---|
| BE unit/use-case | rspec spec/core/use_cases/.../create_omnichannel_agent_conversation_knowledge_source_spec.rb (AGENTS.md test section) | ≤15 cap, division guard, 202 |
| BE worker | rspec spec/workers/conversation_history_daily_sync_worker_spec.rb (rspec-sidekiq) | enqueues per active source; failure isolation |
| BE lint/security | rubocop ; brakeman (AGENTS.md) | style + no security regression |
| BE migration | rails db:migrate then rails db:rollback (AGENTS.md) | division_id add + index up/down |
| FE unit | pnpm test (vitest) (package.json scripts) | counter/validation; tile wiring |
| FE e2e | pnpm test:e2e (playwright) | add → IN_PROGRESS row; delete in-use blocked |
| FE lint | pnpm lint:ts && pnpm lint:prettier | type + format |
| FE build | pnpm build | bundles |
Detail 4.D — Agent Execution Plan
| Order | Layer | Chunk | Files | Commands | Acceptance criteria |
|---|---|---|---|---|---|
| 1 | BE | Confirm contracts & entities (read-only) | ai_knowledge_sources.rb, entities/.../knowledge_source.rb, post_knowledge_sources.rb | rspec spec/api/...ai_knowledge_sources_spec.rb | existing create/list/detail/delete specs green (baseline) |
| 2 | BE | Migration: add ai_knowledge_sources.division_id + partial-unique index (chatbot_gpt DB) | db/chatbot_gpt_migrate/2026XXXX_add_division_id_to_ai_knowledge_sources.rb, db/chatbot_gpt_schema.rb | rails db:migrate && rails db:rollback && rails db:migrate (chatbot_gpt) | column + index_ai_knowledge_sources_on_company_division_type present; rollback clean |
| 2b | BE | Directory endpoint: GET /v1/divisions + GET /v1/divisions/:id/agents (build from Division/AgentsDivision) | new Grape resource under app/api/frontend_service/v1/, new entity | rspec spec/api/...divisions_spec.rb | returns divisions + nested agents, company-scoped, set_role enforced |
| 3 | BE | Extend create use case: division_id param + ≤15 cap + division-aware existence guard | …/create_omnichannel_agent_conversation_knowledge_source.rb | rspec …create_omnichannel…_spec.rb | 16 agents→422; dup (company,division)→422; valid→202 |
| 4 | BE | Extend DELETE in-use guard + conditional delete_vector_db | …/ai_knowledge_source/delete.rb, lib/ai_service/... | rspec …delete_spec.rb | in-use→422; unused→vectors deleted |
| 5 | BE | Daily sync worker + schedule.yml entry | app/workers/conversation_history_daily_sync_worker.rb, config/schedule.yml | rspec spec/workers/... | worker re-posts per ACTIVE source; failure isolated + alert |
| 6 | FE | Service + endpoint + wire tile | common/services/main/v1/ai-assist.ts, endpoint.ts, components/AddKnowledgeDrawer.vue | pnpm lint:ts && pnpm test | tile selectable; service hits create endpoint |
| 7 | FE | ConversationHistoryForm.vue (division-first, ≤15 counter, tooltip) — consumes chunk 2b directory endpoint | new component + store/ai-assist action | pnpm test | counter 0–15; 16th disables Save + message; save → IN_PROGRESS (needs Figma frame — §5 blocker 3) |
| 8 | FE | Resources list: status states + Last Updated + remove/delete | pages/ai-knowledge/training-sources/index.vue, knowledge-list.vue, SourceDetailDrawer.vue | pnpm test:e2e | badges per status; delete in-use blocked |
| 9 | both | Reference room-id link + permission (S11, Could-Have) | Inbox Copilot ref render + room authz | rspec + pnpm test:e2e | cross-division open → 403 |
| 10 | BE | (Deferred/blocked) training-status webhook | new controller + use case | rspec | only when OQ#4 contract published |
Detail 4.E — Verification & Rollback Recipe
- Pre-merge (per layer):
- BE: 1)
rubocop2)brakeman3)rspec4)rails db:migrate(+ rollback check) - FE: 1)
pnpm lint:ts && pnpm lint:prettier2)pnpm test3)pnpm test:e2e4)pnpm build
- BE: 1)
- Post-deploy signals: Datadog Sidekiq queue latency for
application_maintenancestable;conv_history_training_statusfailure rate < threshold over first 24h; create endpoint 5xx < 0.1%. - Rollback (deploy-order-aware):
- Toggle
SystemPreference rollout/conversation_historyOFF (hides FE tile + rejects creates). - If migration must be reverted:
rails db:rollback(only thedivision_idmigration; safe — nullable). - Confirm existing PDF/URL sources unaffected and Sidekiq sync worker no-ops.
- Toggle
Detail 4.F — Resource & Cost Notes
- Compute: negligible — one daily off-peak worker; create/list reuse existing endpoints.
- DB: one nullable column + one partial index; minimal growth (≤1 source/division, ≤15 agent rows).
- Network: AI-Service calls already in use; daily sync adds N (active sources) posts/day.
- Storage: masked vectors live in Data & AI store (rolling 90d), not chatbot.
5. Concern, Questions, or Known Limitations
Blockers (gate §7 to no):
- [REV-3] Training-status webhook contract (PRD OQ#4, Data & AI). States, payload, retry/auth undefined.
Baseline ships on the existing poll (
UpdateStatus); the webhook (chunk 10) is blocked. - [REV-4] PII masking spec/SLA (PRD OQ#1, Data & AI). 100%-mask gate for standard phone/email is the Internal Alpha gate; masking lives entirely in the Data & AI pipeline.
- [REV-2] Design DRAFT (PRD OQ#7, Design). No Figma frame for the config form (division-first
multiselect, 15-cap validation, expertise tooltip) or the training-status list states. The
qontak-designerprototype uses a "Use as source" toggle + fixed read-only agent list and only Active/Inactive states — it does not match the PRD. Do not build chunk 7/8 UI against it. Divisions+agents directory endpoint unverifiedRESOLVED (R1 review; re-confirmed R2, REV-1). Verified there is no GET directory endpoint (only internal syncPATCH /v1/chat/divisions,app/api/internal_service/v1/chat/division.rb:20). Now specified as a new endpoint (GET /v1/divisions(/:id/agents)) with a proposed contract in §2.4 and execution-plan chunk 2b. No longer a contract mismatch.- [REV-5] Source-scoping decision (Decision 1) — needs PM sign-off (technical spec now concrete). The
PRD mandates one-Division-one-source; existing code enforces one-per-company. The technical
resolution is specified (add nullable
ai_knowledge_sources.division_id+ partial-unique(company_id, division_id, type_id), §2.3 DDL; division-aware existence guard, chunk 3). The only open item is PM + eng confirmation of the scoping choice before chunk 2/3 land.
Open questions (non-blocking but tracked): Copilot quota model (PRD OQ#3 — usages currently
quota=1); Jira epic/keys (PRD OQ#2 — stories are TEMP placeholders); "resolved + ≥5 msgs" as a
useful-resolution proxy (PRD OQ#6); [REV-11] create posts to AI-Service outside the DB
transaction — a crash between COMMIT and post can leave a source IN_PROGRESS until the next 01:00
sync (≤24h); consider an immediate post-retry on create (rfc-reviewer R2).
Known limitations: text-only this phase (S13); daily (not real-time) freshness; 15-agent cap; reference link is Could-Have (S11) and may slip; recency/quality/whisper filters are Data & AI's and not independently testable from chatbot.
6. Comment logs
| Date | Comment(s) From | Action Item(s) |
|---|---|---|
| 2026-06-20 | RFC drafted (rfc-starter) | Circulate to BOT + Data & AI + FE; resolve §5 blockers |
| 2026-06-20 | rfc-reviewer R1 (6.5/10, HOLD) — see conversation-history-review.md | Acted on in-control items: §2.4 grounded JSON examples + verified directory endpoint is net-new (REV: ACV); §2.3 concrete one-per-division DDL on ai_knowledge_sources (REV: scoping). Cross-team items (design frames, webhook, masking SLA) remain. |
| 2026-06-20 | rfc-reviewer R2 (7.5/10, HOLD) — see conversation-history-review.md | Delta re-review at chatbot fa6dd8b79 / chatbot-fe f5b80d5a. Re-grounded every citation — all valid. Confirmed R1 fixes hold (REV-1/6/7/8 closed); cross-layer mismatch resolved → 6.5 cap lifted, ACV 5.5→7.0. New finding REV-11 (non-atomic create→AI-Service post) tracked above. Remaining blockers all cross-team/sign-off: design frames (REV-2), webhook (REV-3), masking SLA (REV-4), scoping sign-off (REV-5). |
7. Ready for agent execution
- no
- Missing execution-readiness gates (must close before
yes):- Design References (FE half): config-form + training-status-state Figma frames missing (§5 blocker 3) — cannot build chunks 7–8 UI against the divergent prototype.
- Inbound webhook contract: undefined (§5 blocker 1) — chunk 10 blocked; poll baseline is fine.
- Source-scoping decision: technical spec now concrete (§2.3 DDL); needs PM+eng sign-off on the scoping choice (§5 blocker 5).
- Compliance dependency: masking SLA owned by Data & AI (§5 blocker 2).
- Closed in R1 review: cross-layer contract gaps — §2.4 now carries grounded request/response
JSON for create/list/detail/usages (verified field names incl. the
qoutatypo); the divisions+agents directory is verified-absent and specified as a new endpoint (§2.4 + chunk 2b), removing the §2.G "unverified" row; the one-per-division DDL is now concrete (§2.3). - Already green: PRD-to-Schema derivation, Repo Reading Guide + Source Verification (verified against real files), DDL, reused/extended/new API tags + payload examples, mermaid diagrams (topology via component + sequence + state + branch), state surface contract, failure/branch catalogs, rollout + rollback recipe, agent execution plan (chunks 1–9 + 2b buildable; FE chunks 7–8 need the design frames; chunk 10 deferred to the webhook contract).
Remaining blockers: 1 (webhook contract, Data & AI), 2 (masking SLA, Data & AI), 3 (design frames, Design), 5 (scoping sign-off, PM+eng). Once closed, re-run
checklist.md/rfc-reviewer.