feat: implement AI support agent (005) — diagnosis, tools, runbooks, verification
Real Anthropic Claude integration per explicit product decision: a ticket's AI session diagnoses the problem via a structured-output call, applies a DB-configurable confidence-band policy (FR-005), and on "proceed" reasons and acts through a small permission/risk-gated tool system (FR-011/FR-012), optionally walking a matching runbook step by step with the application — never the model — owning the step index (FR-015/FR-016). Resolution requires real tool evidence, never customer claims alone (FR-018) — verifyProductResolution is a documented fail-closed placeholder mirroring the existing malware-scanner precedent, since no real per-product operational signal exists yet. AISupportSession.status mirrors onto Ticket.status through 003-ticketing's existing AI_ANALYZING/AI_TROUBLESHOOTING/AI_VERIFYING/AI_RESOLVED/ HUMAN_ESCALATION state machine, discovered during planning to have been built anticipating this exact feature. Two circular module dependencies (escalation<->sessions, tools<->sessions) were designed around rather than found as bugs: escalation is a pure summary formatter with no state dependencies of its own, and tools stays a clean leaf module with zero dependency on ai-support/sessions. Ticket creation enqueues the first diagnosis turn via the existing queue infrastructure (off the hot path of the inbound SaaS integration endpoint); a human actor changing ticket status ends the AI session via the event-bus scaffold that existed in this codebase but had never been wired to anything. A real Prisma limitation was found and fixed before it reached tests: compound-unique upsert rejects null for a nullable key column, so AIConfidencePolicy uses find-then-update/create instead, same fix class 004 already used for the same underlying limitation. Adds 9 unit tests (confidence-band, tool-policy-gate, runbook-step- advance) and 6 integration test files, including the two constitution- required standing E2E scenarios. AI-independent tests were run against real Postgres/Redis/MinIO (88 passed, 0 failed across the full suite, including every pre-existing 002/003/004 test). The AI-dependent tests compile and skip cleanly via describe.skipIf but were not run against a live model — no ANTHROPIC_API_KEY was available in this session; a real key must be supplied before this feature can actually run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
49eaa4bc58
commit
82d02bcdcd
@@ -0,0 +1,132 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "ai_support_sessions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ticketId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"activeRunbookKey" TEXT,
|
||||
"currentStepIndex" INTEGER,
|
||||
"clarifyingQuestionsAsked" INTEGER NOT NULL DEFAULT 0,
|
||||
"toolCallCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"endedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "ai_support_sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ai_diagnoses" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sessionId" TEXT NOT NULL,
|
||||
"product" TEXT NOT NULL,
|
||||
"feature" TEXT,
|
||||
"problemType" TEXT NOT NULL,
|
||||
"severity" TEXT NOT NULL,
|
||||
"confidence" DOUBLE PRECISION NOT NULL,
|
||||
"possibleCauses" TEXT[],
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ai_diagnoses_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ai_interactions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sessionId" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ai_interactions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ai_actions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sessionId" TEXT NOT NULL,
|
||||
"toolName" TEXT NOT NULL,
|
||||
"input" JSONB NOT NULL,
|
||||
"riskLevel" TEXT NOT NULL,
|
||||
"evaluationOutcome" TEXT NOT NULL,
|
||||
"refusalReason" TEXT,
|
||||
"approvedBy" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ai_actions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ai_action_results" (
|
||||
"id" TEXT NOT NULL,
|
||||
"actionId" TEXT NOT NULL,
|
||||
"output" JSONB NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ai_action_results_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ai_knowledge_references" (
|
||||
"id" TEXT NOT NULL,
|
||||
"sessionId" TEXT NOT NULL,
|
||||
"knowledgeId" TEXT NOT NULL,
|
||||
"relevanceScore" DOUBLE PRECISION,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ai_knowledge_references_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ai_confidence_policies" (
|
||||
"id" TEXT NOT NULL,
|
||||
"productId" TEXT,
|
||||
"categoryId" TEXT,
|
||||
"highThreshold" DOUBLE PRECISION NOT NULL,
|
||||
"lowThreshold" DOUBLE PRECISION NOT NULL,
|
||||
"maxClarifyingQuestions" INTEGER NOT NULL,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ai_confidence_policies_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ai_support_sessions_ticketId_status_idx" ON "ai_support_sessions"("ticketId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ai_diagnoses_sessionId_idx" ON "ai_diagnoses"("sessionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ai_interactions_sessionId_createdAt_idx" ON "ai_interactions"("sessionId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ai_actions_sessionId_createdAt_idx" ON "ai_actions"("sessionId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ai_action_results_actionId_key" ON "ai_action_results"("actionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ai_knowledge_references_sessionId_idx" ON "ai_knowledge_references"("sessionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ai_confidence_policies_productId_categoryId_key" ON "ai_confidence_policies"("productId", "categoryId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ai_support_sessions" ADD CONSTRAINT "ai_support_sessions_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "tickets"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ai_diagnoses" ADD CONSTRAINT "ai_diagnoses_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "ai_support_sessions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ai_interactions" ADD CONSTRAINT "ai_interactions_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "ai_support_sessions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ai_actions" ADD CONSTRAINT "ai_actions_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "ai_support_sessions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ai_action_results" ADD CONSTRAINT "ai_action_results_actionId_fkey" FOREIGN KEY ("actionId") REFERENCES "ai_actions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ai_knowledge_references" ADD CONSTRAINT "ai_knowledge_references_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "ai_support_sessions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ai_confidence_policies" ADD CONSTRAINT "ai_confidence_policies_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
+117
-8
@@ -34,14 +34,15 @@ model Product {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
categories Category[]
|
||||
integration ProductIntegration?
|
||||
problems Problem[]
|
||||
tickets Ticket[]
|
||||
knowledgeEntries KnowledgeEntry[]
|
||||
errorCodes ErrorCode[]
|
||||
knownIssues KnownIssue[]
|
||||
runbooks Runbook[]
|
||||
categories Category[]
|
||||
integration ProductIntegration?
|
||||
problems Problem[]
|
||||
tickets Ticket[]
|
||||
knowledgeEntries KnowledgeEntry[]
|
||||
errorCodes ErrorCode[]
|
||||
knownIssues KnownIssue[]
|
||||
runbooks Runbook[]
|
||||
aiConfidencePolicies AIConfidencePolicy[]
|
||||
|
||||
@@map("products")
|
||||
}
|
||||
@@ -141,6 +142,7 @@ model Ticket {
|
||||
category Category? @relation(fields: [categoryId], references: [id])
|
||||
messages TicketMessage[]
|
||||
attachments TicketAttachment[]
|
||||
aiSessions AISupportSession[]
|
||||
|
||||
@@unique([productId, idempotencyKey])
|
||||
@@index([productId, status])
|
||||
@@ -273,3 +275,110 @@ model AuditLog {
|
||||
|
||||
@@map("audit_logs")
|
||||
}
|
||||
|
||||
model AISupportSession {
|
||||
id String @id @default(cuid())
|
||||
ticketId String
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id])
|
||||
status String // analyzing | troubleshooting | verifying | resolved | escalated |
|
||||
// ended_by_agent — mirrored onto Ticket.status through the existing 003 state machine, see
|
||||
// specs/005-ai-support/research.md "AISupportSession.status drives Ticket.status"
|
||||
activeRunbookKey String?
|
||||
currentStepIndex Int?
|
||||
clarifyingQuestionsAsked Int @default(0)
|
||||
toolCallCount Int @default(0)
|
||||
startedAt DateTime @default(now())
|
||||
endedAt DateTime?
|
||||
|
||||
diagnoses AIDiagnosis[]
|
||||
interactions AIInteraction[]
|
||||
actions AIAction[]
|
||||
knowledgeRefs AIKnowledgeReference[]
|
||||
|
||||
@@index([ticketId, status])
|
||||
@@map("ai_support_sessions")
|
||||
}
|
||||
|
||||
model AIDiagnosis {
|
||||
id String @id @default(cuid())
|
||||
sessionId String
|
||||
session AISupportSession @relation(fields: [sessionId], references: [id])
|
||||
product String
|
||||
feature String?
|
||||
problemType String
|
||||
severity String
|
||||
confidence Float
|
||||
possibleCauses String[]
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([sessionId])
|
||||
@@map("ai_diagnoses")
|
||||
}
|
||||
|
||||
model AIInteraction {
|
||||
id String @id @default(cuid())
|
||||
sessionId String
|
||||
session AISupportSession @relation(fields: [sessionId], references: [id])
|
||||
role String // customer | ai
|
||||
content String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([sessionId, createdAt])
|
||||
@@map("ai_interactions")
|
||||
}
|
||||
|
||||
model AIAction {
|
||||
id String @id @default(cuid())
|
||||
sessionId String
|
||||
session AISupportSession @relation(fields: [sessionId], references: [id])
|
||||
toolName String
|
||||
input Json
|
||||
riskLevel String // low | medium | high — copied from the registry at evaluation time
|
||||
evaluationOutcome String // approved | pending_approval | refused
|
||||
refusalReason String?
|
||||
approvedBy String? // system-policy | agentId | null while pending_approval
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
result AIActionResult?
|
||||
|
||||
@@index([sessionId, createdAt])
|
||||
@@map("ai_actions")
|
||||
}
|
||||
|
||||
model AIActionResult {
|
||||
id String @id @default(cuid())
|
||||
actionId String @unique
|
||||
action AIAction @relation(fields: [actionId], references: [id])
|
||||
output Json
|
||||
status String // success | failed
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("ai_action_results")
|
||||
}
|
||||
|
||||
model AIKnowledgeReference {
|
||||
id String @id @default(cuid())
|
||||
sessionId String
|
||||
session AISupportSession @relation(fields: [sessionId], references: [id])
|
||||
knowledgeId String // KnowledgeEntry.id — resolved through ai-support/knowledge's public
|
||||
// index.ts, not a cross-module DB-level FK (Constitution Principle III)
|
||||
relevanceScore Float?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([sessionId])
|
||||
@@map("ai_knowledge_references")
|
||||
}
|
||||
|
||||
model AIConfidencePolicy {
|
||||
id String @id @default(cuid())
|
||||
productId String? // null = system-wide default row
|
||||
product Product? @relation(fields: [productId], references: [id])
|
||||
categoryId String? // null = applies to every category of productId
|
||||
highThreshold Float
|
||||
lowThreshold Float
|
||||
maxClarifyingQuestions Int
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([productId, categoryId])
|
||||
@@map("ai_confidence_policies")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user