{ "$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" ] }