003-ticketing's ticket-state-machine.ts already defines AI_ANALYZING/ AI_TROUBLESHOOTING/AI_VERIFYING/AI_RESOLVED/HUMAN_ESCALATION ticket statuses, clearly authored anticipating this feature. Corrects the plan before implementation: AISupportSession.status now drives Ticket.status through the existing ticketsService.updateStatus (reusing its optimistic concurrency), instead of an isolated status field the rest of the system never sees. Also clarifies that knowledge retrieval is an in-process service call through knowledge's index.ts, not an HTTP loopback. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
123 lines
6.9 KiB
Markdown
123 lines
6.9 KiB
Markdown
# Phase 1 Data Model: AI Support Agent
|
||
|
||
All new models use `cuid()` ids. Refines `docs/06-database-schema.md`'s conceptual
|
||
`AISupportSession`/`AIDiagnosis`/`AIInteraction`/`AIAction`/`AIActionResult`/
|
||
`AIKnowledgeReference` shapes; adds `AIConfidencePolicy` (research.md — not in doc 06, required
|
||
by FR-005).
|
||
|
||
## AISupportSession
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| id | String @id @default(cuid()) | |
|
||
| ticketId | String | FK → `Ticket`. One **active** session per ticket at a time (FR-001) — enforced in the repository (partial-condition check, not a DB constraint, since a ticket accumulates multiple ended sessions over time if re-escalated and re-opened) |
|
||
| status | String | `analyzing` \| `troubleshooting` \| `verifying` \| `resolved` \| `escalated` \| `ended_by_agent` (doc 06's enum, plus `ended_by_agent` for FR-023). Every transition except `ended_by_agent` is mirrored onto `Ticket.status` via the *existing* 003 state machine (`AI_ANALYZING`/`AI_TROUBLESHOOTING`/`AI_VERIFYING`/`AI_RESOLVED`/`HUMAN_ESCALATION`) through `ticketsService.updateStatus(..., 'ai')` — research.md "AISupportSession.status drives Ticket.status" |
|
||
| activeRunbookKey | String? | Set when a diagnosis matches a runbook (research.md "Runbook engine") |
|
||
| currentStepIndex | Int? | App-owned index into the active runbook's `steps`; null when no runbook is active |
|
||
| clarifyingQuestionsAsked | Int @default(0) | Counted against `AIConfidencePolicy.maxClarifyingQuestions` (FR-009) |
|
||
| toolCallCount | Int @default(0) | Counted against the per-session hard cap (doc 11 §B2 — Assumptions) |
|
||
| startedAt | DateTime @default(now()) | |
|
||
| endedAt | DateTime? | |
|
||
|
||
**Relations**: `diagnoses AIDiagnosis[]`, `interactions AIInteraction[]`, `actions AIAction[]`,
|
||
`knowledgeRefs AIKnowledgeReference[]`.
|
||
|
||
**Index**: `(ticketId, status)` — the exact shape the "one active session per ticket" check and
|
||
the ticket-detail view both query on.
|
||
|
||
## AIDiagnosis
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| id | String @id @default(cuid()) | |
|
||
| sessionId | String | FK → `AISupportSession` |
|
||
| product | String | Echoes the ticket's product for readability; not a second source of truth for scoping (the session's `ticketId` → `Ticket.productId` remains authoritative) |
|
||
| feature | String? | |
|
||
| problemType | String | Matched against `Runbook.key` for the runbook-engine trigger (research.md) |
|
||
| severity | String | |
|
||
| confidence | Float | 0–1; the value the confidence-band policy (FR-004) is applied to |
|
||
| possibleCauses | String[] | |
|
||
| createdAt | DateTime @default(now()) | |
|
||
|
||
Never updated in place — a session accumulates one row per diagnosis attempt (initial + each
|
||
re-diagnosis after a customer reply), matching FR-002's "never overwriting a prior one."
|
||
|
||
## AIInteraction
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| id | String @id @default(cuid()) | |
|
||
| sessionId | String | FK → `AISupportSession` |
|
||
| role | String | `customer` \| `ai` |
|
||
| content | String | |
|
||
| createdAt | DateTime @default(now()) | |
|
||
|
||
An `AIInteraction` with `role: ai` that's a clarifying question or guided-step message is *also*
|
||
written as a `TicketMessage` (`type: AI_MESSAGE`) via the existing messages module — `AIInteraction`
|
||
is the session's own ordered transcript for reasoning-call context; `TicketMessage` is the
|
||
customer-visible record. They're intentionally two records: the session transcript may include
|
||
turns not meant to duplicate onto the ticket (e.g., an internal re-diagnosis triggered by a tool
|
||
result, with no new customer-facing text).
|
||
|
||
## AIAction / AIActionResult
|
||
|
||
| Field (`AIAction`) | Type | Notes |
|
||
|---|---|---|
|
||
| id | String @id @default(cuid()) | |
|
||
| sessionId | String | FK → `AISupportSession` |
|
||
| toolName | String | Must match a name in the code-defined registry (research.md) |
|
||
| input | Json | The model's proposed input, before any validation |
|
||
| riskLevel | String | Copied from the registry at evaluation time (`low`/`medium`/`high`) — a durable record of what risk tier applied, independent of later registry changes |
|
||
| evaluationOutcome | String | `approved` \| `pending_approval` \| `refused` — the deterministic gate's decision (research.md), always recorded even when nothing executes |
|
||
| refusalReason | String? | Set when `evaluationOutcome = refused` (unknown tool, product not in scope, etc.) |
|
||
| approvedBy | String? | `system-policy` for auto-approved low-risk; null while `pending_approval`; an agent id if a future approval UI fills it in |
|
||
| createdAt | DateTime @default(now()) | |
|
||
|
||
| Field (`AIActionResult`) | Type | Notes |
|
||
|---|---|---|
|
||
| id | String @id @default(cuid()) | |
|
||
| actionId | String @unique | FK → `AIAction` — only exists when `evaluationOutcome = approved` and execution actually ran |
|
||
| output | Json | |
|
||
| status | String | `success` \| `failed` |
|
||
| createdAt | DateTime @default(now()) | |
|
||
|
||
A `pending_approval` or `refused` `AIAction` has no `AIActionResult` row — the absence itself is
|
||
the record of "never executed" (FR-013 requires the proposal+evaluation to be recorded either
|
||
way, not that every proposal produces a result).
|
||
|
||
## AIKnowledgeReference
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| id | String @id @default(cuid()) | |
|
||
| sessionId | String | FK → `AISupportSession` |
|
||
| knowledgeId | String | `KnowledgeEntry.id` (004) — not a DB-level FK across module boundaries per this codebase's convention of modules only depending on each other's `index.ts`, but a plain string reference resolved through `knowledge`'s exported repository |
|
||
| relevanceScore | Float? | Null for now — 004's retrieval doesn't emit a numeric score (structured filtering + validation-status ranking, not a similarity score); reserved for a future semantic-retrieval layer (spec.md Assumptions) |
|
||
| createdAt | DateTime @default(now()) | |
|
||
|
||
One row per knowledge entry actually included in a reasoning call's context — the durable record
|
||
of what the AI was actually shown, satisfying doc 03 §9's "AI must never invent... expose
|
||
internal notes" concern from the audit side (you can always answer "what knowledge did the AI
|
||
see for this session").
|
||
|
||
## AIConfidencePolicy
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| id | String @id @default(cuid()) | |
|
||
| productId | String? | Null = system-wide default row (optional; env-var defaults cover the no-row case too — research.md) |
|
||
| categoryId | String? | Null = applies to every category of `productId` |
|
||
| highThreshold | Float | `confidence >= highThreshold` → proceed |
|
||
| lowThreshold | Float | `confidence < lowThreshold` → escalate; between the two → ask |
|
||
| maxClarifyingQuestions | Int | FR-009's cap |
|
||
| updatedAt | DateTime @updatedAt | |
|
||
|
||
**Constraints**: `@@unique([productId, categoryId])`. `highThreshold > lowThreshold` is validated
|
||
at the service layer (Zod refinement), not the DB.
|
||
|
||
## Ticket (relation added by this feature)
|
||
|
||
`aiSessions AISupportSession[]` — the forward relation doc 06 already specified on `Ticket` but
|
||
that couldn't be added until `AISupportSession` existed (same pattern 004 used for `Product`'s
|
||
relations).
|