622 lines
20 KiB
JSON
622 lines
20 KiB
JSON
{
|
|||
|
|
"$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"
|
||
|
|
]
|
||
|
|
}
|