Files
descrybe/docs/support-auto/02-contract.md
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

20 KiB
Raw Blame History

02 — Support auto-response + rich ticket contract

Status: Design only — no application code in this document.
Agent: 2/10 (support-auto)
Date: 2026-08-05
Repo: F:/laragon/www/_MY/descrybe-v2
Machine contract: 02-contract.json


0. North star

Give support tickets richer customer context, then on create (first response only by default) attempt:

  1. Deterministic match against FAQ / knowledge articles or reply templates (keyword + intent).
  2. If match confidence is below threshold → AI assist using the platform admin support AI role.
  3. If AI fails / is low-confidence / customer replies again → human handoff (queue + claim desk from 11-support-design).

Auto replies are always clearly labeled (author_role=system + auto_* metadata). Staff may disable auto per ticket.

Customer Create ticket (+ tags / category / optional SKU / context snapshot)
        │
        ▼
  [sync] FAQ / template matcher ──confidence ≥ T_match──► post labeled system reply
        │ below threshold
        ▼
  [async job] AI assist (AIRoleSupport) ──ok──► draft internal note OR auto-send (admin)
        │ fail / low conf
        ▼
  Human handoff (open/unassigned queue; no further auto unless staff re-enables)

1. Inventory basis

Source Status
01-inventory.md / 01-extension-points.json Authoritative desk + AI inventory (agent 1). Confirms stub gate, ai_roles.support, no FAQ matcher, Create post-commit as natural hook.
11-support-design.md / 11-support-api-contract.json Desk roles, claim queue, CSAT, message author_role ∈ user|agent|system.
Schema 025 + 027/029/030 Tickets/messages/notifications + desk indexes, staff_role, CSAT. No tags / SKU / KB / auto flags.
support.Ticket / CreateInput (apps/api/internal/support/types.go) Priority + category already on create; no tags/SKU/context.
support.Create (tickets.go) Post-commit extension point for matcher → optional TryAutoReplyLLM (never Completer in Create).
support.TryAutoReplyLLM (ai_auto_reply.go) Only allowed LLM entry for tickets; currently returns ErrAIAutoReplyDisabled.
platformsettings.AIRoleSupport = "support" Admin-configurable slot in ai_roles (GET/PUT /api/admin/settings via admin_settings_handlers.go).
aiprovider.ResolveCompleterForRole(..., RoleSupport) Platform completer for support role; no env fallback when unset (tests assert).
Web admin apps/web/src/lib/admin-ai-roles.ts — role support; UI /admin/settings → AI roles tab.
Docs Ask Rule tree (DocsAskGuide / docs-guide) — pattern for content match before LLM; must not use AIRoleSupport.
FAQ today Marketing FaqSection only — no support knowledge base / canned ticket replies.
Jobs Postgres poller / jobs.Queue pattern (River deferred) — prefer same style for AI auto jobs.
Plan keys `support.center

ASSUMPTION: Extending TryAutoReplyLLM remains the single AI gate; FAQ matching is a sibling TryAutoReplyMatch (or internal stages of one orchestrator) that never calls an LLM. Explicit product gate = support_auto.enabled and ai_roles.support.enabled (inventory §4.3).


2. Richer ticket fields

2.1 Additive fields on support_tickets

Field Type Required Notes
priority enum low|normal|high yes (default normal) Exists — preserve.
tags TEXT[] or join table no Max 10 tags; each ≤ 40 runes; lowercase slug-ish; staff + customer may set on create; staff may PATCH.
related_product_id UUID nullable no Company-scoped catalog product when resolvable. Validate company_id match — never cross-tenant.
related_sku TEXT nullable no Free-text SKU (≤ 128). Stored even if product id unknown; used for match features + staff UI.
category taxonomy slug yes See §2.2.
customer_context JSONB no (server-filled) Snapshot at create — see §2.3. Immutable after create (staff-visible; customers see limited subset).
auto_reply_disabled BOOLEAN NOT NULL DEFAULT false Staff toggle; also set automatically on human handoff / customer follow-up after auto.
auto_reply_status enum none|matched|ai_draft|ai_sent|skipped|failed|handed_off.
auto_reply_attempted_at TIMESTAMPTZ Last orchestrator run.
auto_reply_message_id UUID nullable Public auto message when posted.

Contracts preserved: existing create body (subject, category, priority, body) remains valid. New fields are additive optional.

2.2 Category taxonomy

Todays CHECK: billing | bug | account | other.

Chosen approach: keep seed slugs for backward compatibility; move allow-list to support_categories (platform-scoped admin taxonomy):

Column Purpose
slug Stable API value (PK)
label Admin/UI display
parent_slug Optional hierarchy (1 level deep MVP)
sort_order List order
is_active Soft-disable without breaking historical tickets
match_intents Optional string[] of intent keys for matcher

Seed rows: existing four categories. Admin may add e.g. integrations, processing, export, billing_credits.

BREAKING (soft): create/update must reject unknown active slugs; historical tickets may retain inactive slugs (read-only display).

Create API: category still a string slug (default other).

2.3 Customer context snapshot

Filled server-side on Create from the authenticated company/user — never trust client JSON for authz-sensitive facts.

{
  "captured_at": "RFC3339",
  "company_id": "uuid",
  "company_name": "string",
  "plan_slug": "string|null",
  "user_id": "uuid",
  "user_email": "string",
  "user_name": "string|null",
  "locale": "string|null",
  "timezone": "string|null",
  "related_product": {
    "id": "uuid|null",
    "sku": "string|null",
    "title": "string|null"
  },
  "signals": {
    "open_ticket_count": 0,
    "last_ticket_category": "string|null"
  }
}

Rules:

  • Snapshot is denormalized; later plan changes do not rewrite it.
  • Customer GET may omit signals / email of others; staff GET sees full snapshot.
  • No API keys, SMTP secrets, BYOK material, or raw payment tokens in snapshot.
  • Cap JSON size (~4 KiB); drop optional signals if over budget.

3. Knowledge + templates (match corpus)

New platform-scoped content (not per-tenant KB for MVP — support is platform desk):

3.1 support_kb_articles

Field Notes
id, slug, title, body_md Public-safe answer text
category_slugs Overlap boost with ticket category
keywords TEXT[] — lowercase tokens
intent_keys TEXT[] — e.g. password_reset, stripe_invoice
is_published Matcher ignores unpublished
priority_weight Tie-break

3.2 support_reply_templates

Field Notes
id, name, body Staff-authored reply (may include {{subject}} placeholders — allowlist only)
keywords, intent_keys, category_slugs Same as KB
is_active

ASSUMPTION: MVP matcher is lexical (keyword overlap + intent + category boost), not embeddings. Vector search may reuse AIRoleVectorization later — out of scope here.


4. Auto-response pipeline

4.1 Triggers

Event Behavior
Ticket create Primary: run orchestrator once.
Customer first reply after create with no prior auto Optional admin flag retry_on_first_customer_reply (default false).
Customer reply after an auto public message Handoff — set auto_reply_disabled=true, auto_reply_status=handed_off; do not re-run AI.
Staff public reply Disable further auto on that ticket.
Staff sets auto_reply_disabled=true Skip forever until staff clears flag.

Never invoke LLM from Create / ReplyAsUser / ReplyAsAgent directly — only via TryAutoReplyLLM (AI stage) after FAQ stage.

4.2 Stage A — FAQ / template match (sync, cheap)

Input: subject + body + category + tags + related_sku + intent heuristics (simple regex/keyword map).

Output:

{
  "kind": "kb_article|template|none",
  "ref_id": "uuid|null",
  "confidence": 0.0,
  "body": "string|null",
  "label": "string"
}
  • If confidence >= settings.match_confidence_threshold and auto enabled → post public message:
    • author_role = "system"
    • author_user_id = null
    • is_internal_note = false
    • metadata: auto_source, auto_confidence, auto_ref_type, auto_ref_id
    • Prefix or footer (localized): clearly states automated reply (e.g. “Automated answer from help center”).
  • Set ticket statuspending (waiting on customer), auto_reply_status=matched.
  • Notify customer (agent_reply or new kind auto_reply — prefer new auto_reply for analytics; additive CHECK).
  • Do not assign an agent on FAQ match (stays unassigned for claim).

Performance: sync inside create transaction after commit of ticket+first message is preferred: commit ticket → match → insert system message in a short follow-up tx so create latency stays low. Matcher must be O(corpus) with small corpus + indexes on keywords GIN; hard cap ≤ 50 ms CPU budget or abort to AI stage.

4.3 Confidence threshold → Stage B

If no match or confidence < T_match:

  • If settings.ai_enabled and support AI configured → enqueue async AI job (§5).
  • Else → auto_reply_status=skipped (or handed_off if desk expects human-only).

Default match_confidence_threshold = 0.78 (admin-configurable 0.500.95).

4.4 Message labeling (customer + staff)

Audience Presentation
Customer System bubble with badge “Automated” / “AI-assisted”; never look like a named human agent.
Staff Same + auto_source, confidence, KB/template link; AI drafts as internal notes when delivery=draft.

Extend support_messages (additive):

auto_source TEXT CHECK (auto_source IS NULL OR auto_source IN ('kb','template','ai')),
auto_confidence REAL,
auto_ref_type TEXT,
auto_ref_id UUID,
is_auto_reply BOOLEAN NOT NULL DEFAULT false

5. AI assist

5.1 Provider resolution (discovered)

Symbol / path Role
platformsettings.AIRoleSupport ("support") Admin slot in ai_roles.
platformsettings.ResolveAIConfig(ctx, AIRoleSupport) Decrypt key; provider/base_url/model/enabled.
aiprovider.RoleSupport / ResolveCompleterForRole(ctx, companyID, RoleSupport) Completer; platform-only for support (no env fallback).
support.TryAutoReplyLLM(ctx, ticketID) Sole product gate for ticket LLM sends/drafts.
Admin UI /admin/settings + admin-ai-roles.ts role support.
API GET/PUT /api/admin/settingsai_roles.support.

ASSUMPTION: Support auto uses platform ai_roles.support, never tenant BYOK. companyID passed to resolver is the tickets company for future bindings only; MVP ignores company BYOK for RoleSupport (already the case in ResolveCompleterForRole switch).

5.2 Admin AI override vs global

Under support-auto settings (§8):

Setting Behavior
ai_use_global_support_role: true (default) Use ResolveCompleterForRole(..., RoleSupport) as configured in admin.
ai_model_override / ai_provider_override / ai_base_url_override Optional; applied only when ai_use_global_support_role is true and overrides non-empty — still uses the same API key from AIRoleSupport (never a second secret store).
Role disabled / missing key AI stage → failed → human handoff; log without secrets.

5.3 Prompt contract

System prompt (server-owned, not admin free-text MVP):

  • Answer only from provided KB snippets + ticket text.
  • No inventing billing credits, invoices, or other tenants data.
  • If unsure, say so and ask one clarifying question OR signal handoff=true.
  • Output JSON: { "body": "...", "confidence": 0-1, "handoff": bool, "citations": ["kb:slug"] }.

User/content payload:

  • Redacted ticket subject + body (§6).
  • Category, tags, related_sku (company-scoped).
  • Top-N KB snippets from cheap lexical retrieval (even if below FAQ auto-send threshold).
  • Customer context snapshot sanitized (no emails of other users; plan slug ok).

5.4 Delivery modes

ai_delivery Action
draft (default, safer) Insert internal note (is_internal_note=true, author_role=system, is_auto_reply=true); notify assignee or unassigned queue via staff notification; auto_reply_status=ai_draft. Customer sees nothing yet.
auto_send Post public system message (labeled AI-assisted); status → pending; auto_reply_status=ai_sent.

Require ai_confidence >= settings.ai_confidence_threshold (default 0.65) and handoff=false to send/draft; else handoff.

5.5 Async job (preferred)

Sync path forbidden for LLM on the HTTP create request.

Job sketch (support_auto_jobs or reuse processing-style pending table):

Column Notes
id, ticket_id, company_id Tenant key for isolation
status pending|running|done|failed
attempt, last_error No PII in error; truncate
created_at, updated_at

Worker:

  1. Skip if auto_reply_disabled or ticket closed/resolved or already matched/sent.
  2. Rate-limit check (§6).
  3. Call TryAutoReplyLLM (implementation loads ticket, snippets, completer, posts draft/send).
  4. Idempotent: unique partial index on (ticket_id) WHERE status IN ('pending','running') or ticket-level lock.

ASSUMPTION: Follow existing jobs.Queue + worker poller pattern (apps/api/internal/jobs/river.go comment: River deferred). Name the worker kind support_auto_reply.


6. Security

Control Rule
Tenant isolation All ticket reads/writes filter company_id. KB is platform-global content but never includes other companies tickets in prompts. Related product id validated against ticket company_id.
Prompt isolation Completer called with one tickets redacted text only; never batch multi-tenant prompts.
Secret redaction Before match features + AI: strip patterns for API keys, Bearer tokens, sk-, AWS keys, private key PEM, connection strings; replace with [REDACTED]. Do not store raw secrets in customer_context or auto job logs.
Rate limits Per company_id: default 10 AI jobs / hour; platform global: 30 AI jobs / minute. FAQ match not counted toward AI limits. Reuse sliding-window pattern (httpapi/ratelimit.go). Exceed → skipped/failed + handoff, HTTP create still 201.
Disable per ticket Staff PATCH { "auto_reply_disabled": true }; also auto-set on handoff / customer follow-up after auto.
AuthZ Settings: RequirePlatformAdmin. Disable flag: RequireSupportDesk on assigned/visible ticket. KB CRUD: platform admin.
Customer visibility Never expose internal AI drafts, confidence, or admin settings.
Logging No API keys, no full bodies at info level; structured ticket_id, company_id, auto_source, confidence.
Docs Ask Remains rule-based; must not use AIRoleSupport / AIRoleDocsAPI (existing invariant).

7. Performance

Path Guidance
FAQ/template match Sync, bounded corpus, GIN on keywords; target < 50 ms; no external I/O.
AI assist Async job only; HTTP returns before LLM.
Create API Do not hold open DB transactions across LLM.
Indexes (auto_reply_status), GIN tags, FK related_product_id, job (status, created_at).
List endpoints Do not embed full customer_context in list — detail only (or truncated).
N+1 Matcher loads published KB once per process with short TTL cache (e.g. 60s) — invalidate on admin KB write.

8. Admin settings

Stored under platform settings (JSON key support_auto in values or first-class object on PublicView — implementers pick one; prefer first-class for typed admin UI).

Key Type Default Purpose
enabled bool false Master switch for any auto first response.
faq_enabled bool true Stage A.
ai_enabled bool false Stage B (still requires ai_roles.support configured + enabled).
match_confidence_threshold number 0.78 FAQ/template auto-send.
ai_confidence_threshold number 0.65 Min model confidence.
ai_delivery draft|auto_send draft
ai_use_global_support_role bool true Use admin ai_roles.support.
ai_provider_override string? null Optional; key still from support role.
ai_model_override string? null
ai_base_url_override string? null
rate_limit_per_company_per_hour int 10 AI jobs.
rate_limit_platform_per_minute int 30 AI jobs.
retry_on_first_customer_reply bool false

API (additive):

Method Path Auth
GET/PUT /api/admin/settings (embed support_auto) Platform admin
CRUD /api/admin/support/kb/articles* Platform admin
CRUD /api/admin/support/templates* Platform admin
CRUD /api/admin/support/categories* Platform admin
PATCH /api/admin/support/tickets/{id} Support desk — auto_reply_disabled, tags, related_*, priority, category

Customer create (additive body fields): tags?, related_product_id?, related_sku? (category taxonomy as today).


9. Human handoff

Set auto_reply_status=handed_off, auto_reply_disabled=true when:

  1. AI returns handoff=true or confidence below threshold.
  2. AI provider error / timeout / unset completer.
  3. Rate limit exceeded for AI.
  4. Customer sends another message after an automated public reply.
  5. Staff claims/replies or explicitly disables auto.

Effects: ticket remains open (or current status); stays claimable; optional internal system note “Needs human review” (not customer-visible). No further auto jobs for that ticket unless staff sets auto_reply_disabled=false (rare; audit).


10. Alignment with support desk (agent 11)

  • Auto does not assign assignee_admin_user_id (preserves unassigned queue + claim).
  • Public auto reply may move status openpending (same as agent public reply default).
  • Internal AI drafts do not change status / do not notify customer.
  • CSAT / claim / staff roles unchanged.
  • TryAutoReplyLLM opt-in replaces the “keep disabled” note in 11-support §11.9 when this feature ships behind support_auto.enabled.

11. Implementation phasing (later agents)

  1. Migration: ticket rich fields + message auto columns + categories + KB + templates + auto jobs.
  2. Admin settings + KB/template/category CRUD.
  3. Sync matcher + labeled system post on create.
  4. Implement TryAutoReplyLLM + async worker + redaction + rate limits.
  5. Staff UI: disable auto, show auto metadata, AI draft notes.
  6. Customer UI: tags/SKU/category; automated badge on messages.
  7. Tests: tenant isolation, redaction, threshold branches, handoff on user reply, AI disabled stub, rate limit.

Out of scope: per-tenant KB, embedding retrieval, multi-turn AI bot, attachments, SLA clocks.


12. Contracts preserved / breaking

Preserved

  • Existing customer/staff ticket JSON fields; additive only.
  • author_role enum values; system used for auto.
  • Admin ai_roles.support shape; no new secret store.
  • Desk visibility / claim rules from agent 11.

BREAKING (soft)

  • Category allow-list may grow; clients must tolerate unknown historical categories.
  • New notification kind auto_reply requires CHECK constraint migration.

Reconciled with 01-inventory: symbols/paths match; Create post-commit hook preferred; settings stay in platform JSON (support_auto + existing ai_roles.support); no plan-permission key for auto-reply in MVP.