Files
support_backend/specs/005-ai-support/data-model.md
T

123 lines
6.9 KiB
Markdown
Raw Normal View History

# 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 | 01; 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).