Files

622 lines
20 KiB
JSON
Raw Permalink Normal View History

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Descrybe v2 Support Auto-Response + Rich Ticket Contract",
"version": "02.0.0-design",
"status": "design-only",
"related": [
"docs/support-auto/02-contract.md",
"docs/support-auto/01-inventory.md",
"docs/support-auto/01-extension-points.json",
"docs/admin-roles-support/11-support-design.md",
"docs/admin-roles-support/11-support-api-contract.json"
],
"inventory_note": "Reconciled with agent-1 01-inventory + 01-extension-points; Create post-commit is the preferred orchestrator hook.",
"discovered_symbols": {
"ai_gate": {
"symbol": "TryAutoReplyLLM",
"path": "apps/api/internal/support/ai_auto_reply.go",
"behavior_today": "returns ErrAIAutoReplyDisabled"
},
"create_hook": {
"symbol": "Create",
"path": "apps/api/internal/support/tickets.go",
"line": 252,
"note": "Post-commit: content matcher then optional TryAutoReplyLLM; never Completer directly"
},
"ai_role_constant": {
"symbol": "AIRoleSupport",
"value": "support",
"path": "apps/api/internal/platformsettings/types.go"
},
"ai_resolve": {
"symbol": "ResolveAIConfig",
"path": "apps/api/internal/platformsettings/ai_configs.go"
},
"completer": {
"symbol": "ResolveCompleterForRole",
"role": "RoleSupport",
"path": "apps/api/internal/aiprovider/roles.go",
"notes": "Platform role only; no env fallback when unset"
},
"admin_api": {
"path": "/api/admin/settings",
"field": "ai_roles.support",
"handlers": "apps/api/internal/httpapi/admin_settings_handlers.go",
"web": [
"apps/web/src/lib/admin-ai-roles.ts",
"apps/web/src/lib/admin-platform-settings.ts",
"apps/web/src/routes/admin/settings/+page.svelte"
]
},
"ticket_type": {
"symbol": "Ticket",
"path": "apps/api/internal/support/types.go"
},
"schema": [
"apps/api/sql/schema/025_support_center.sql",
"apps/api/sql/schema/027_capabilities_support_perf.sql",
"apps/api/sql/schema/029_staff_roles.sql",
"apps/api/sql/schema/030_support_desk.sql"
],
"docs_ask_pattern": {
"component": "apps/web/src/lib/components/docs/DocsAskGuide.svelte",
"rule": "content match pattern only; never AIRoleSupport"
},
"jobs_pattern": {
"symbol": "EnqueueProcessingJob",
"path": "apps/api/internal/jobs/river.go",
"notes": "River deferred; Postgres pending + worker poller"
},
"rate_limit_pattern": {
"path": "apps/api/internal/httpapi/ratelimit.go"
}
},
"conventions": {
"timestamps": "RFC3339 UTC",
"ids": "UUID string",
"errors": {
"shape": { "error": "string", "code": "string?" }
},
"additive_only": true
},
"enums": {
"ticket_status": ["open", "pending", "resolved", "closed"],
"ticket_priority": ["low", "normal", "high"],
"ticket_category_seed": ["billing", "bug", "account", "other"],
"author_role": ["user", "agent", "system"],
"auto_source": ["kb", "template", "ai"],
"auto_reply_status": [
"none",
"matched",
"ai_draft",
"ai_sent",
"skipped",
"failed",
"handed_off"
],
"ai_delivery": ["draft", "auto_send"],
"match_kind": ["kb_article", "template", "none"],
"notification_kind_additive": ["auto_reply"],
"support_auto_job_status": ["pending", "running", "done", "failed"]
},
"types": {
"CustomerContextSnapshot": {
"type": "object",
"required": ["captured_at", "company_id", "user_id"],
"properties": {
"captured_at": { "type": "string", "format": "date-time" },
"company_id": { "type": "string", "format": "uuid" },
"company_name": { "type": "string" },
"plan_slug": { "type": ["string", "null"] },
"user_id": { "type": "string", "format": "uuid" },
"user_email": { "type": "string" },
"user_name": { "type": ["string", "null"] },
"locale": { "type": ["string", "null"] },
"timezone": { "type": ["string", "null"] },
"related_product": {
"type": "object",
"properties": {
"id": { "type": ["string", "null"], "format": "uuid" },
"sku": { "type": ["string", "null"], "maxLength": 128 },
"title": { "type": ["string", "null"] }
}
},
"signals": {
"type": "object",
"properties": {
"open_ticket_count": { "type": "integer", "minimum": 0 },
"last_ticket_category": { "type": ["string", "null"] }
}
}
},
"constraints": {
"filled_server_side": true,
"immutable_after_create": true,
"max_bytes": 4096,
"forbidden": [
"api_keys",
"smtp_secrets",
"byok_material",
"payment_tokens",
"other_tenant_data"
]
}
},
"SupportTicketRich": {
"description": "Additive fields on SupportTicket from 11-contract",
"extends": "docs/admin-roles-support/11-support-api-contract.json#/types/SupportTicket",
"properties": {
"tags": {
"type": "array",
"items": { "type": "string", "maxLength": 40 },
"maxItems": 10
},
"related_product_id": { "type": ["string", "null"], "format": "uuid" },
"related_sku": { "type": ["string", "null"], "maxLength": 128 },
"category": {
"type": "string",
"description": "Taxonomy slug; seeds billing|bug|account|other; admin may add active slugs"
},
"customer_context": {
"oneOf": [
{ "$ref": "#/types/CustomerContextSnapshot" },
{ "type": "null" }
],
"description": "Staff detail full; customer detail may omit signals"
},
"auto_reply_disabled": { "type": "boolean", "default": false },
"auto_reply_status": { "$ref": "#/enums/auto_reply_status" },
"auto_reply_attempted_at": {
"type": ["string", "null"],
"format": "date-time"
},
"auto_reply_message_id": {
"type": ["string", "null"],
"format": "uuid"
}
}
},
"SupportMessageAuto": {
"description": "Additive fields on SupportMessage",
"properties": {
"is_auto_reply": { "type": "boolean", "default": false },
"auto_source": {
"oneOf": [{ "$ref": "#/enums/auto_source" }, { "type": "null" }]
},
"auto_confidence": {
"type": ["number", "null"],
"minimum": 0,
"maximum": 1
},
"auto_ref_type": {
"type": ["string", "null"],
"enum": ["kb_article", "template", null]
},
"auto_ref_id": { "type": ["string", "null"], "format": "uuid" }
},
"labeling": {
"author_role": "system",
"author_user_id": null,
"customer_badge": "Automated|AI-assisted",
"never_impersonate_human_agent": true
}
},
"SupportCategory": {
"type": "object",
"required": ["slug", "label", "is_active"],
"properties": {
"slug": { "type": "string", "maxLength": 32 },
"label": { "type": "string", "maxLength": 80 },
"parent_slug": { "type": ["string", "null"] },
"sort_order": { "type": "integer" },
"is_active": { "type": "boolean" },
"match_intents": {
"type": "array",
"items": { "type": "string" }
}
}
},
"SupportKbArticle": {
"type": "object",
"required": ["id", "slug", "title", "body_md", "is_published"],
"properties": {
"id": { "type": "string", "format": "uuid" },
"slug": { "type": "string" },
"title": { "type": "string", "maxLength": 200 },
"body_md": { "type": "string", "maxLength": 20000 },
"category_slugs": { "type": "array", "items": { "type": "string" } },
"keywords": { "type": "array", "items": { "type": "string" } },
"intent_keys": { "type": "array", "items": { "type": "string" } },
"is_published": { "type": "boolean" },
"priority_weight": { "type": "integer", "default": 0 }
}
},
"SupportReplyTemplate": {
"type": "object",
"required": ["id", "name", "body", "is_active"],
"properties": {
"id": { "type": "string", "format": "uuid" },
"name": { "type": "string", "maxLength": 120 },
"body": { "type": "string", "maxLength": 10000 },
"category_slugs": { "type": "array", "items": { "type": "string" } },
"keywords": { "type": "array", "items": { "type": "string" } },
"intent_keys": { "type": "array", "items": { "type": "string" } },
"is_active": { "type": "boolean" },
"placeholders_allowlist": ["subject", "related_sku", "company_name"]
}
},
"MatchResult": {
"type": "object",
"required": ["kind", "confidence"],
"properties": {
"kind": { "$ref": "#/enums/match_kind" },
"ref_id": { "type": ["string", "null"], "format": "uuid" },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
"body": { "type": ["string", "null"] },
"label": { "type": "string" }
}
},
"AiAssistResult": {
"type": "object",
"required": ["body", "confidence", "handoff"],
"properties": {
"body": { "type": "string", "maxLength": 10000 },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
"handoff": { "type": "boolean" },
"citations": {
"type": "array",
"items": { "type": "string" }
}
}
},
"SupportAutoSettings": {
"type": "object",
"required": ["enabled"],
"properties": {
"enabled": { "type": "boolean", "default": false },
"faq_enabled": { "type": "boolean", "default": true },
"ai_enabled": { "type": "boolean", "default": false },
"match_confidence_threshold": {
"type": "number",
"default": 0.78,
"minimum": 0.5,
"maximum": 0.95
},
"ai_confidence_threshold": {
"type": "number",
"default": 0.65,
"minimum": 0.5,
"maximum": 0.95
},
"ai_delivery": {
"$ref": "#/enums/ai_delivery",
"default": "draft"
},
"ai_use_global_support_role": { "type": "boolean", "default": true },
"ai_provider_override": { "type": ["string", "null"], "maxLength": 64 },
"ai_model_override": { "type": ["string", "null"], "maxLength": 128 },
"ai_base_url_override": {
"type": ["string", "null"],
"maxLength": 512
},
"rate_limit_per_company_per_hour": {
"type": "integer",
"default": 10,
"minimum": 1
},
"rate_limit_platform_per_minute": {
"type": "integer",
"default": 30,
"minimum": 1
},
"retry_on_first_customer_reply": {
"type": "boolean",
"default": false
}
},
"notes": "Overrides change provider/model/base_url only; API key always from ai_roles.support"
}
},
"pipeline": {
"name": "support_auto_first_response",
"entry_triggers": [
{
"event": "ticket_created",
"required": true
},
{
"event": "customer_first_reply_without_prior_auto",
"required": false,
"gate": "settings.retry_on_first_customer_reply"
}
],
"preconditions": [
"settings.enabled == true",
"ticket.auto_reply_disabled == false",
"ticket.status in open|pending",
"no prior public is_auto_reply message OR retry flag allows"
],
"stages": [
{
"id": "A_faq_template_match",
"mode": "sync",
"max_cpu_ms": 50,
"llm": false,
"when": "settings.faq_enabled",
"action_on_hit": {
"condition": "confidence >= settings.match_confidence_threshold",
"post_message": {
"author_role": "system",
"is_internal_note": false,
"is_auto_reply": true,
"auto_source": "kb|template"
},
"ticket_status": "pending",
"auto_reply_status": "matched",
"assign_agent": false,
"notify_kind": "auto_reply"
},
"action_on_miss": "continue_to_B"
},
{
"id": "B_ai_assist",
"mode": "async_job",
"job_kind": "support_auto_reply",
"llm_gate": "support.TryAutoReplyLLM",
"completer": "aiprovider.ResolveCompleterForRole(RoleSupport)",
"when": "settings.ai_enabled && ai_roles.support configured",
"prompt": {
"inputs": [
"redacted_subject",
"redacted_body",
"category",
"tags",
"related_sku",
"kb_snippets_top_n",
"sanitized_customer_context"
],
"output_schema": "AiAssistResult",
"rules": [
"answer only from snippets + ticket",
"no cross-tenant data",
"no inventing billing facts",
"handoff=true when unsure"
]
},
"delivery": {
"draft": {
"is_internal_note": true,
"auto_reply_status": "ai_draft",
"notify_customer": false,
"notify_staff": true
},
"auto_send": {
"is_internal_note": false,
"auto_reply_status": "ai_sent",
"ticket_status": "pending",
"notify_kind": "auto_reply",
"condition": "confidence >= ai_confidence_threshold && handoff == false"
}
},
"on_failure": "human_handoff"
}
],
"human_handoff": {
"set": {
"auto_reply_status": "handed_off",
"auto_reply_disabled": true
},
"triggers": [
"ai_handoff_true_or_low_confidence",
"ai_provider_error_or_unset",
"ai_rate_limited",
"customer_reply_after_public_auto",
"staff_public_reply_or_explicit_disable"
],
"optional_internal_note": "Needs human review",
"keep_claimable": true,
"assign_agent": false
}
},
"schema_deltas_sketch": {
"support_tickets_additive": [
"tags TEXT[] NOT NULL DEFAULT '{}'",
"related_product_id UUID NULL",
"related_sku TEXT NULL",
"customer_context JSONB NULL",
"auto_reply_disabled BOOLEAN NOT NULL DEFAULT false",
"auto_reply_status TEXT NOT NULL DEFAULT 'none'",
"auto_reply_attempted_at TIMESTAMPTZ NULL",
"auto_reply_message_id UUID NULL"
],
"support_messages_additive": [
"is_auto_reply BOOLEAN NOT NULL DEFAULT false",
"auto_source TEXT NULL",
"auto_confidence REAL NULL",
"auto_ref_type TEXT NULL",
"auto_ref_id UUID NULL"
],
"new_tables": [
"support_categories",
"support_kb_articles",
"support_reply_templates",
"support_auto_jobs"
],
"category_check": "Replace hard CHECK with taxonomy table + app validation; seed billing/bug/account/other"
},
"endpoints_additive": [
{
"id": "customer.create_ticket_rich",
"method": "POST",
"path": "/api/support/tickets",
"auth": "customer",
"body_additive": {
"tags": { "type": "string[]", "optional": true, "maxItems": 10 },
"related_product_id": { "type": "uuid", "optional": true },
"related_sku": { "type": "string", "optional": true, "maxLength": 128 }
},
"side_effects": [
"server fills customer_context",
"after commit: Stage A sync; maybe enqueue Stage B",
"HTTP must not await LLM"
]
},
{
"id": "staff.patch_auto_fields",
"method": "PATCH",
"path": "/api/admin/support/tickets/{id}",
"auth": "support_desk",
"body_additive": {
"auto_reply_disabled": { "type": "boolean", "optional": true },
"tags": { "optional": true },
"related_product_id": { "optional": true },
"related_sku": { "optional": true },
"category": { "optional": true }
}
},
{
"id": "admin.support_auto_settings",
"method": "GET|PUT",
"path": "/api/admin/settings",
"auth": "platform_admin",
"field": "support_auto",
"schema_ref": "#/types/SupportAutoSettings"
},
{
"id": "admin.kb_crud",
"method": "GET|POST|PATCH|DELETE",
"path": "/api/admin/support/kb/articles",
"auth": "platform_admin"
},
{
"id": "admin.templates_crud",
"method": "GET|POST|PATCH|DELETE",
"path": "/api/admin/support/templates",
"auth": "platform_admin"
},
{
"id": "admin.categories_crud",
"method": "GET|POST|PATCH|DELETE",
"path": "/api/admin/support/categories",
"auth": "platform_admin"
}
],
"security": {
"tenant_isolation": [
"All ticket queries filter company_id",
"related_product_id must belong to ticket company_id",
"Prompts never include other companies' tickets",
"KB is platform content only — not tenant data"
],
"redaction": {
"before_match_and_ai": true,
"patterns": [
"api_keys",
"bearer_tokens",
"sk-_openai_style",
"aws_keys",
"pem_private_keys",
"connection_strings"
],
"replacement": "[REDACTED]"
},
"rate_limits": {
"faq_match": "unlimited_within_cpu_budget",
"ai_per_company_per_hour": "settings.rate_limit_per_company_per_hour",
"ai_platform_per_minute": "settings.rate_limit_platform_per_minute",
"on_exceed": "skip_ai_handoff_create_still_201"
},
"per_ticket_disable": {
"field": "auto_reply_disabled",
"actors": ["support_desk", "auto_on_handoff"]
},
"secrets": [
"Never log ResolveAIConfig API keys",
"Overrides do not store a second API key",
"customer_context must not contain secrets"
],
"docs_ask_invariant": "Guided /docs Ask must never use AIRoleSupport or AIRoleDocsAPI"
},
"performance": {
"faq_match": "sync_bounded",
"ai_assist": "async_job_only",
"create_http": "no_llm_await",
"kb_cache_ttl_seconds": 60,
"list_endpoints": "omit_full_customer_context",
"indexes": [
"GIN tags",
"auto_reply_status",
"support_auto_jobs (status, created_at)",
"kb keywords GIN"
]
},
"contracts_preserved": [
"Existing customer ticket create fields remain valid without tags/SKU",
"author_role enum unchanged; system used for auto",
"ai_roles.support admin shape unchanged",
"Desk claim/visibility from agent 11 unchanged",
"TryAutoReplyLLM remains sole LLM ticket gate"
],
"breaking_soft": [
{
"change": "Category taxonomy expands beyond CHECK four",
"migration": "Seed four; app validates active slugs; historical inactive slugs readable"
},
{
"change": "notification kind auto_reply",
"migration": "Widen support_notifications kind CHECK"
}
],
"test_matrix": [
{
"case": "faq_above_threshold",
"expect": "public system auto message; status pending; no AI job"
},
{
"case": "faq_below_threshold_ai_draft",
"expect": "async job; internal note; customer no public auto"
},
{
"case": "ai_auto_send",
"expect": "public labeled AI message when conf ok"
},
{
"case": "ai_unset_role",
"expect": "handoff; TryAutoReplyLLM fail closed"
},
{
"case": "cross_tenant_product_id",
"expect": "400/ignore product; never leak"
},
{
"case": "secret_in_body",
"expect": "redacted before AI; not in logs"
},
{
"case": "customer_reply_after_auto",
"expect": "auto_reply_disabled; no second AI"
},
{
"case": "staff_disable_auto",
"expect": "orchestrator no-op"
},
{
"case": "rate_limit_ai",
"expect": "handoff; create 201"
},
{
"case": "master_switch_off",
"expect": "no match post; no job"
}
],
"assumptions": [
"MVP matcher is lexical not embeddings (Docs Ask is pattern inspiration only)",
"Support AI uses platform ai_roles.support only (no tenant BYOK)",
"FAQ match does not auto-assign agents",
"Extend TryAutoReplyLLM rather than a parallel LLM entry point",
"Product gate is support_auto.enabled AND ai_roles.support.enabled (inventory 4.3)",
"No plan-permission key for auto-reply in MVP (platform admin settings only)",
"support_auto settings stored in platform JSON alongside ai_roles"
]
}