{"openapi":"3.1.0","info":{"title":"Vector Storage","description":"High-performance vector embedding storage and search microservice with a built-in\nRAG pipeline, hybrid search (FAISS + BM25), multi-provider embeddings, and LLM\nmetadata enrichment.\n\n**[Full Documentation](/docs)** — Architecture, API reference, data providers, deployment, and more.\n| **[Integration Guide](/integration-guide)** — Quick start, code examples, RAG pattern.\n| **[OpenAPI JSON](/openapi.json)** — Machine-readable schema.\n\n## Authentication\n\nAll endpoints (except `/v1/health`) require a Bearer token in the `Authorization`\nheader:\n\n```\nAuthorization: Bearer <api_key>\n```\n\nAPI keys are issued via the Admin Panel. Use the **Authorize** button at the top\nof this page to set your token once for all interactive requests.\n\n## Concepts\n\n- **Namespace** (`{ns}`) — top-level isolation unit. Each namespace has its own\n  FAISS + BM25 indexes, document storage, and stats.\n- **Client ID** (`{client_id}`) — secondary isolation key inside a namespace,\n  supplied in the URL path: `/v1/client/{client_id}/ns/{ns}/...`. Internally\n  composed as `{ns}__{client_id}` for separate FAISS + BM25 indexes per tenant.\n  Endpoints that don't need tenant scoping (health, warmup, rebuild, provider\n  tools) stay at `/v1/ns/{ns}/...`.\n- **Document ID** (`doc_id`) — logical document inside a client. One document\n  produces N chunks (vectors). Deleting a document removes all its chunks.\n- **Ingest job** (`ingest_id`) — async background pipeline that pulls data from\n  an external provider (e.g. LiveHelpNow), aggregates, and indexes it.\n- **Crawl job** (`crawl_id`) — async website crawl that turns each fetched page\n  into a regular document.\n\n## Error format\n\nEvery non-2xx response uses a single shape:\n\n```json\n{\n  \"error\": \"snake_case_code\",\n  \"message\": \"Human readable message\",\n  \"details\": { \"...optional context...\" }\n}\n```\n\nThe `error` field is the canonical machine-readable code (`not_found`,\n`invalid_request`, `validation_error`, `rate_limited`, `upstream_error`, etc.).\n","version":"0.7.0"},"paths":{"/v1/ns/{ns}/warmup":{"post":{"tags":["management"],"summary":"Warmup namespace","description":"Load a namespace's FAISS + BM25 indexes into the in-memory cache so that subsequent queries skip the cold-load latency.\n\n**When to use:** call after deploy or before a known traffic burst on a namespace that hasn't been queried recently.","operationId":"warmup_v1_ns__ns__warmup_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WarmupResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/rebuild":{"post":{"tags":["management"],"summary":"Rebuild namespace index","description":"Trigger an **asynchronous** rebuild of a namespace's FAISS index. Compacts the WAL, removes tombstoned vectors, and reselects the optimal index type (Flat / IVF / IVFPQ) for the current vector count.\n\n**Side-effects:** locks the namespace from new upserts during the rebuild (usually seconds to minutes). Safe to call on a healthy namespace.\n\n**Rate-limited:** 5/minute.","operationId":"rebuild_v1_ns__ns__rebuild_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RebuildResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/stats":{"get":{"tags":["management"],"summary":"Namespace statistics","description":"Detailed statistics for a namespace (without client_id scoping). Use `/client/{client_id}/ns/{ns}/stats` for tenant-isolated view.","operationId":"stats_v1_ns__ns__stats_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/stats":{"get":{"tags":["management"],"summary":"Namespace statistics","description":"Detailed statistics for a namespace from Turbopuffer + Supabase:\n\n**1. Turbopuffer view** — `vector_count`, `index_size_bytes`, `index_status`, `last_write_at`, `attributes` (schema). Namespace = `{ns}__{client_id}` for tenant isolation.\n\n**2. Source data** (`source_data`) — counts from Supabase: tickets, chats, kb_articles, departments.\n\n**3. Aggregated** (`aggregated`) — total/indexed/pending counts from vs_lhn_aggregated.\n\n**4. Documents** (`documents`) — legacy StatsStore counts.","operationId":"stats_v1_client__client_id__ns__ns__stats_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}":{"delete":{"tags":["management"],"summary":"Delete entire namespace","description":"**Destructive.** Permanently delete a namespace and all its data: vectors, documents, metadata, WAL segments, and crawl/ingest job history from S3.\n\nAlso clears the matching `vs_kb_chunks` projection rows so the next indexer pass doesn't try to re-upsert ghost rows. The namespace is resolved through `VS_TURBOPUFFER_PREFIX` first — to delete the legacy generation, run with the env var unset.\n\n**Cannot be undone.** Returns counts for verification.","operationId":"delete_namespace_v1_ns__ns__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteNamespaceResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/health":{"get":{"tags":["health"],"summary":"Health check","description":"Check service health including S3 connectivity, cache status, and uptime.","operationId":"health_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/v1/ns/{ns}/documents/providers/{provider}/validate":{"post":{"tags":["ingest"],"summary":"Validate provider credentials","description":"Test provider API credentials **without** starting ingestion. Returns per-data-type availability counts and access status. Use this from a UI credentials form to give the user immediate feedback before persisting.","operationId":"validate_credentials_v1_ns__ns__documents_providers__provider__validate_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateCredentialsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateCredentialsResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Unknown provider","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/ingest":{"post":{"tags":["ingest"],"summary":"Start data ingestion from external provider","description":"Queue an asynchronous ingestion job that fetches data from an external provider (e.g. LiveHelpNow), aggregates similar items into clusters, and indexes them as documents in this namespace.\n\n**Pipeline phases:** fetching → aggregating → indexing → finalizing.\n\n**Side-effects:** writes raw data to S3 (`{ns}/providers/{provider}/data.sqlite`) for incremental syncs and stores credentials in the admin config store on first successful run. Webhook events: `ingest.progress`, `ingest.done`, `ingest.error`.\n\n**Follow-up:** poll `GET /ingest/{ingest_id}/status` or watch the webhook.","operationId":"start_ingest_v1_client__client_id__ns__ns__documents_ingest_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Unknown provider, invalid credentials, or no credentials configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/ingest/{ingest_id}/status":{"get":{"tags":["ingest"],"summary":"Get ingestion job status","description":"Get the current status, phase, and progress of an ingestion job. Same response shape as items in `GET /ingest/history`.","operationId":"get_ingest_status_v1_ns__ns__documents_ingest__ingest_id__status_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"ingest_id","in":"path","required":true,"schema":{"type":"string","title":"Ingest Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestStatusResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Ingest job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/ingest/{ingest_id}/stop":{"post":{"tags":["ingest"],"summary":"Stop an active ingestion job","description":"Cancel a pending or running ingest job. Already-indexed documents remain. Status transitions to `cancelled`.","operationId":"stop_ingest_v1_ns__ns__documents_ingest__ingest_id__stop_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"ingest_id","in":"path","required":true,"schema":{"type":"string","title":"Ingest Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Ingest job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Job is not in a cancellable state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/ingest/{ingest_id}":{"delete":{"tags":["ingest"],"summary":"Delete ingestion job from history","description":"Remove a completed, failed, or cancelled ingest job from the history table. Running or pending jobs must be stopped first via `POST .../stop`. Already-indexed documents are **not** removed by this call.","operationId":"delete_ingest_v1_ns__ns__documents_ingest__ingest_id__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"ingest_id","in":"path","required":true,"schema":{"type":"string","title":"Ingest Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Ingest job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Job is still running or pending","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/ingest/history":{"get":{"tags":["ingest"],"summary":"List ingestion history for namespace","description":"List recent ingestion jobs for a namespace, newest first. Each item has the same shape as `GET /ingest/{ingest_id}/status`. Pagination via `limit` (default 50, max 200).","operationId":"ingest_history_v1_ns__ns__documents_ingest_history_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Maximum number of jobs to return","default":50,"title":"Limit"},"description":"Maximum number of jobs to return"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestHistoryResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/providers":{"get":{"tags":["ingest"],"summary":"List available data providers","description":"List all data providers registered on this server, with their data types and MCP capability flag. Use to populate the provider picker in your UI.","operationId":"list_providers_v1_ns__ns__documents_providers_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvidersListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/providers/{provider}/tools":{"get":{"tags":["ingest"],"summary":"Get MCP tools for a provider","description":"List the MCP tools exposed by a provider. Tools let an LLM call live provider APIs (e.g. fetch a fresh ticket) when search results indicate `has_mcp=true`.","operationId":"get_provider_tools_v1_ns__ns__documents_providers__provider__tools_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPToolsResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Provider not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Provider does not support MCP","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/providers/{provider}/schedules":{"get":{"tags":["ingest"],"summary":"List recurring update schedules for a provider","description":"List all schedules for the given provider in this namespace (all tenants). Use `/client/{client_id}/ns/{ns}/...` to scope to one tenant.","operationId":"list_schedules_v1_ns__ns__documents_providers__provider__schedules_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"client_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Provider not registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/providers/{provider}/schedules":{"get":{"tags":["ingest"],"summary":"List recurring update schedules for a provider","description":"List schedules for the given provider in this namespace, scoped to `client_id` from the path. Each entry has the full state (enabled, interval, last run, next run, last status).","operationId":"list_schedules_v1_client__client_id__ns__ns__documents_providers__provider__schedules_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"client_id","in":"path","required":true,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Provider not registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"put":{"tags":["ingest"],"summary":"Create or replace schedules for a provider's data_types","description":"Upsert recurring update schedules for one or more data_types of a provider. Existing schedules for the same `(provider, data_type)` are replaced atomically.\n\n**Validation:** the provider must support scheduling for each requested data_type, and `interval_hours` must respect the data_type's `min_interval_hours`. The first run is scheduled `interval_hours` from creation — call `POST .../run-now` if you want an immediate sync.","operationId":"upsert_schedules_v1_client__client_id__ns__ns__documents_providers__provider__schedules_put","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertSchedulesRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Provider does not support scheduling for the given data_type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Provider not registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/providers/{provider}/schedules/{data_type}":{"delete":{"tags":["ingest"],"summary":"Remove a single recurring schedule","description":"Delete one schedule by `(provider, data_type, client_id)`. The currently running ingest job (if any) is **not** cancelled — it will finish and report its outcome to the deleted schedule (which is then a no-op). Use `POST .../stop` on the ingest job to abort.","operationId":"delete_schedule_v1_client__client_id__ns__ns__documents_providers__provider__schedules__data_type__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"data_type","in":"path","required":true,"schema":{"type":"string","title":"Data Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Schedule not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/providers/{provider}/schedules/{data_type}/pause":{"post":{"tags":["ingest"],"summary":"Pause a recurring schedule","description":"Set `enabled=false` so the IngestionScheduler skips this schedule on its next tick. The next_run_at value is preserved — call `resume` to continue from where it left off.","operationId":"pause_schedule_v1_client__client_id__ns__ns__documents_providers__provider__schedules__data_type__pause_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"data_type","in":"path","required":true,"schema":{"type":"string","title":"Data Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Schedule not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/providers/{provider}/schedules/{data_type}/resume":{"post":{"tags":["ingest"],"summary":"Resume a paused schedule","description":"Set `enabled=true`. The schedule fires on the next scheduler tick if `next_run_at <= now`, otherwise at `next_run_at`.","operationId":"resume_schedule_v1_client__client_id__ns__ns__documents_providers__provider__schedules__data_type__resume_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"data_type","in":"path","required":true,"schema":{"type":"string","title":"Data Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Schedule not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/providers/{provider}/schedules/{data_type}/run-now":{"post":{"tags":["ingest"],"summary":"Trigger a scheduled update immediately","description":"Bypass the timer and enqueue an ingest job for this single data_type right now. The schedule's `last_run_at` is updated as if the timer had fired naturally; `next_run_at` is recomputed only after the job finishes (via the standard worker hook).\n\nReturns the same shape as `POST /ingest`.","operationId":"run_schedule_now_v1_client__client_id__ns__ns__documents_providers__provider__schedules__data_type__run_now_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"data_type","in":"path","required":true,"schema":{"type":"string","title":"Data Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Schedule not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"No credentials configured for this provider","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/providers/{provider}/call":{"post":{"tags":["ingest"],"summary":"Call an MCP tool on a provider","description":"Execute an MCP tool on a provider using the credentials stored for this namespace. Returns the raw tool output (shape is tool-specific). Rate-limited: 60/minute.","operationId":"call_provider_tool_v1_client__client_id__ns__ns__documents_providers__provider__call_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPCallRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPCallResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Provider not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Provider does not support MCP, no credentials, or unknown tool","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"Upstream provider error during MCP call","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/upload":{"post":{"tags":["documents"],"summary":"Upload document for processing","description":"Upload a file (PDF, DOCX, TXT, CSV, XLSX, HTML). The document is parsed, chunked, embedded, and indexed **asynchronously** — the response is returned as soon as the file is staged.\n\n**Side-effects:** spawns a background processing task. Original file and metadata are stored under `{namespace}/documents/{doc_id}/` in S3.\n\n**Follow-up:**\n- Poll `GET /v1/ns/{ns}/documents/{doc_id}` until `status` is `ready` or `error`\n- Or configure a webhook on the API key to receive `{doc_id, status, chunk_count, error, filename}`\n\n**Limits:** see `max_file_size_mb` (default 50 MB) and the `supported_extensions` setting.","operationId":"upload_document_v1_client__client_id__ns__ns__documents_upload_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_document_v1_client__client_id__ns__ns__documents_upload_post"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentUploadResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"File exceeds the configured size limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"Unsupported file extension","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/{doc_id}":{"get":{"tags":["documents"],"summary":"Get document status","description":"Retrieve a single document's metadata and processing status.\n\n**Status values:** pending → processing → ready (or error / empty).\n\nUse this to poll for completion after `POST /upload`, `POST /fetch-url`, `POST /reprocess`, or `POST /chunks/upsert`.","operationId":"get_document_v1_ns__ns__documents__doc_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"doc_id","in":"path","required":true,"schema":{"type":"string","title":"Doc Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Document not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["documents"],"summary":"Delete document and its vectors","description":"Delete a document. Removes the original file and metadata from S3 and all associated chunks from FAISS + BM25 + WAL.\n\n**Side-effects:** decrements namespace document stats; safe to call on an already-empty document. Cannot be undone — re-upload to restore.","operationId":"delete_document_v1_ns__ns__documents__doc_id__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"doc_id","in":"path","required":true,"schema":{"type":"string","title":"Doc Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentDeleteResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Document not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents":{"get":{"tags":["documents"],"summary":"List documents in namespace","description":"List documents in a namespace, sorted by creation date (newest first). Use `/client/{client_id}/ns/{ns}/documents` for tenant-scoped listing.\n\n**Pagination:** controlled via `limit` and `offset`. The response includes `total` (full count after filtering) plus the echoed `limit`/`offset`. Default page size is 100; max is 500.","operationId":"list_documents_v1_ns__ns__documents_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Page size (1-500)","default":100,"title":"Limit"},"description":"Page size (1-500)"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Offset for pagination","default":0,"title":"Offset"},"description":"Offset for pagination"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents":{"get":{"tags":["documents"],"summary":"List documents in namespace","description":"List documents in a namespace for a specific client, sorted by creation date (newest first).\n\n**Pagination:** controlled via `limit` and `offset`. The response includes `total` (full count after filtering) plus the echoed `limit`/`offset`. Default page size is 100; max is 500.","operationId":"list_documents_v1_client__client_id__ns__ns__documents_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Page size (1-500)","default":100,"title":"Limit"},"description":"Page size (1-500)"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Offset for pagination","default":0,"title":"Offset"},"description":"Offset for pagination"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/{doc_id}/reprocess":{"post":{"tags":["documents"],"summary":"Reprocess document","description":"Delete old vectors and re-run the processing pipeline on the original file.\n\n**When to use:** after changing chunk size, embedding provider, enrichment prompts, or pipeline config — to bring an existing document into line with the new settings.\n\n**Side-effects:** removes old chunks from FAISS, runs parse → chunk → enrich → embed → upsert pipeline asynchronously. Returns immediately.\n\n**Follow-up:** poll `GET /documents/{doc_id}` or use the webhook callback.","operationId":"reprocess_document_v1_ns__ns__documents__doc_id__reprocess_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"doc_id","in":"path","required":true,"schema":{"type":"string","title":"Doc Id"}}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentUploadResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Document not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/fetch-url":{"post":{"tags":["documents"],"summary":"Fetch and process a web page","description":"Download a web page by URL, extract content via trafilatura, chunk, embed, and index it. The page becomes a regular document accessible via `GET /documents/{doc_id}` and `DELETE /documents/{doc_id}`.\n\n**SSRF protection:** private/loopback IPs and non-http(s) schemes are rejected.\n\n**Side-effects:** spawns a background processing task; webhook callback sent on completion.","operationId":"fetch_url_v1_client__client_id__ns__ns__documents_fetch_url_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchUrlRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchUrlResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Invalid URL or fetch failed (HTTP error / non-HTML content / size limit)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/crawl":{"post":{"tags":["documents","crawl"],"summary":"Start website crawl","description":"Crawl a website starting from the given URL. Pages are fetched via BFS with configurable depth, page limits, URL filters, robots.txt and sitemap support. Each fetched page becomes a regular document.\n\n**Webhook callbacks** (sent to the webhook URL configured for the API key):\n- Per-page: `{event: 'crawl.page', crawl_id, doc_id, url, status, chunk_count, filename, error}`\n- Final: `{event: 'crawl.done', crawl_id, status, pages_found, pages_processed, pages_error, total_chunks, finished_at}`\n\n**Crawl statuses**: pending, running, done, stopped, error.\n**Page statuses**: pending, processing, ready, error, skipped, empty.\n\n**Follow-up:** `GET /documents/crawl/{crawl_id}` for status, `POST .../stop` to abort, `POST .../reprocess` to re-run with the same params.","operationId":"start_crawl_v1_client__client_id__ns__ns__documents_crawl_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Invalid start_url or regex pattern","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/crawl/{crawl_id}":{"get":{"tags":["documents","crawl"],"summary":"Get crawl job status","description":"Get the current status and progress of a crawl job, including per-page results (URL, depth, doc_id, status, chunk count). Use this for progress UIs while the job is running.","operationId":"get_crawl_status_v1_ns__ns__documents_crawl__crawl_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"crawl_id","in":"path","required":true,"schema":{"type":"string","title":"Crawl Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlStatusResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Crawl job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/crawl/{crawl_id}/stop":{"post":{"tags":["documents","crawl"],"summary":"Stop a running crawl","description":"Stop an active crawl job. Already-processed pages remain in the index. Status transitions to `stopped`.","operationId":"stop_crawl_v1_ns__ns__documents_crawl__crawl_id__stop_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"crawl_id","in":"path","required":true,"schema":{"type":"string","title":"Crawl Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlStopResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Active crawl job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/crawl/{crawl_id}/reprocess":{"post":{"tags":["documents","crawl"],"summary":"Re-crawl a completed crawl job","description":"Delete all documents/chunks from the old crawl and re-run with the same parameters (start URL, max_depth, max_pages, filters). The `crawl_id` is preserved.\n\n**Constraints:** only works for completed crawls (done/stopped/error). Active crawls must be stopped first.","operationId":"reprocess_crawl_v1_ns__ns__documents_crawl__crawl_id__reprocess_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"crawl_id","in":"path","required":true,"schema":{"type":"string","title":"Crawl Id"}}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlReprocessResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Crawl job not found, still active, or missing saved config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/chunks/upsert":{"post":{"tags":["chunks"],"summary":"Upsert pre-chunked text","description":"Accept pre-chunked text with caller-supplied IDs and metadata. Vector Storage generates embeddings via the active provider and indexes the chunks in FAISS + BM25 + WAL. Existing `chunk_id`s are overwritten (returned with `status='updated'`); new IDs return `status='created'`.\n\n**Grouping:** chunks are grouped under a logical `doc_id`. Delete all chunks for a document via `DELETE /documents/{doc_id}`.\n\n**Reserved metadata keys** set by Vector Storage on every chunk and never to be supplied by callers: `chunk_text`, `doc_id`, `client_id`, `doc_type`, `source`.\n\n**Limits:** 1-500 chunks per request, text 1-10 000 chars, metadata 10 KB.","operationId":"upsert_chunks_v1_client__client_id__ns__ns__chunks_upsert_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChunkUpsertRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChunkUpsertResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"No embedding provider configured / provider error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/pipeline/{config_name}":{"put":{"tags":["pipeline"],"summary":"Create or update pipeline config","description":"Create or update a named pipeline configuration for a client. Configs control LLM enrichment prompts, JSON output schema, vector strategy (single/multi), and chunking parameters.\n\n**Multi-vector strategy:** when `vector_strategy='multi'`, each chunk is expanded into N vectors via `vector_templates` (Jinja2 templates rendered from enrichment data). Useful for SOPs where one chunk contributes trigger/steps/keywords vectors.","operationId":"put_pipeline_config_v1_client__client_id__ns__ns__pipeline__config_name__put","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"config_name","in":"path","required":true,"schema":{"type":"string","title":"Config Name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineConfigRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineConfigResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Invalid pipeline config (validation errors)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["pipeline"],"summary":"Get pipeline config by name","description":"Fetch a single pipeline config by `client_id` (in path) + `config_name`.","operationId":"get_pipeline_config_v1_client__client_id__ns__ns__pipeline__config_name__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"config_name","in":"path","required":true,"schema":{"type":"string","title":"Config Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineConfigResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Pipeline config not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["pipeline"],"summary":"Delete pipeline config","description":"Delete a named pipeline config. Documents that referenced this config are not affected — they keep their already-indexed vectors. Re-process them to switch to the default pipeline.","operationId":"delete_pipeline_config_v1_client__client_id__ns__ns__pipeline__config_name__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"config_name","in":"path","required":true,"schema":{"type":"string","title":"Config Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineConfigDeleteResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Pipeline config not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/pipeline":{"get":{"tags":["pipeline"],"summary":"List pipeline configs for a client","description":"List all named pipeline configs registered for a given `client_id` (in path).","operationId":"list_pipeline_configs_v1_client__client_id__ns__ns__pipeline_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineConfigListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/admin/pipeline/configs":{"get":{"tags":["pipeline"],"summary":"List all pipeline configs (admin)","description":"Admin-only: list all pipeline configs across all clients in the system.","operationId":"admin_list_configs_v1_admin_pipeline_configs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientConfigsListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"BearerAuth":[]}]}},"/v1/client/{client_id}/ns/{ns}/query":{"post":{"tags":["query"],"summary":"Text query (embed + search)","description":"Send a text query. The service embeds it via the active embedding provider and performs hybrid search (Turbopuffer ANN + BM25) across the client-isolated namespace.\n\n**How it works:**\n1. Text is embedded using the active embedding provider's query model\n2. Turbopuffer ANN finds semantically similar vectors (dense search)\n3. Turbopuffer BM25 finds keyword matches in chunk text (sparse search)\n4. Metadata filters are applied (pre-ANN, attribute-indexed)\n5. Results are combined via Reciprocal Rank Fusion (RRF)\n6. **Source Priority Engine** boosts/penalises each result by `metadata.priority` — the final `score` is `base_score * priority_boost_applied`. Both are surfaced on each result for transparency (`base_score` and `priority_boost_applied` are `null` when no non-trivial boost was applied, e.g. in `dense`/`sparse` modes)\n7. **Cross-encoder re-ranking** (optional) — if `rerank=true` or `VS_RERANK_ENABLED=true`, results are re-scored by a cross-encoder model (Bedrock or Voyage) for higher precision; the pre-rerank retrieval score is preserved as `original_score` and the cross-encoder output as `rerank_score`\n8. Top-k results are returned with chunk text and metadata\n\n**Re-ranking:** When enabled, adds ~50-200ms latency but significantly improves relevance. Configure rerank model in credentials (`rerank_model` field in bedrock/voyage).\n\n**Metadata filters** support exact match, IN, gte/lte, contains, and boolean operators, plus boolean combinators `_and` / `_or` / `_not` for arbitrary filter trees. Multiple top-level keys without a combinator are implicit AND (backwards-compatible). See the `filters` field description for the full grammar.\n\n**`X-Active-Department` header** — optional request-scope narrowing. When absent or blank, `body.filters` is passed through unchanged. When present, the header **overrides** any `department` constraint in `body.filters`: every leaf constraint on the `department` attribute (exact match or any `department_*` suffix operator) is stripped from the caller's tree — inside `_or` branches too — and the header value is then AND-ed in as a single `{\"department\": <value>}` equality.","operationId":"query_v1_client__client_id__ns__ns__query_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"X-Active-Department","in":"header","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"description":"Optional request-scope department filter. When provided, overrides any `department` constraint in `body.filters`: all department leaf constraints are stripped from the caller's tree and replaced by a single `{\"department\": <value>}` equality AND-ed on top. Empty/whitespace values are ignored.","title":"X-Active-Department"},"description":"Optional request-scope department filter. When provided, overrides any `department` constraint in `body.filters`: all department leaf constraints are stripped from the caller's tree and replaced by a single `{\"department\": <value>}` equality AND-ed on top. Empty/whitespace values are ignored."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Malformed filter tree (e.g. combinator sibling to a leaf)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"No embedding provider configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/aggregate":{"post":{"tags":["aggregate"],"summary":"Aggregate metadata distributions","description":"Group vectors by one or more metadata fields and return value counts. Useful for building dashboards and understanding data composition.\n\nOptional filters narrow the scope before aggregation.","operationId":"aggregate_v1_client__client_id__ns__ns__aggregate_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AggregateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AggregateResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/ns/{ns}/vectors":{"get":{"tags":["vectors"],"summary":"List vectors with cursor pagination","description":"List all active vectors in a namespace with their metadata. Uses cursor pagination for efficient iteration over large namespaces.\n\nPass `starting_after` from the previous response's `next_cursor` to get the next page.\n\nOptional `filters` parameter accepts a JSON object with metadata filter conditions (e.g. `{\"source_type\": \"curated_kb\"}`, `{\"channel_ne\": \"\"}`).","operationId":"list_vectors_v1_ns__ns__vectors_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}},{"name":"starting_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Cursor from previous response","title":"Starting After"},"description":"Cursor from previous response"},{"name":"client_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional client_id to scope to a specific client namespace","title":"Client Id"},"description":"Optional client_id to scope to a specific client namespace"},{"name":"filters","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"JSON-encoded metadata filters (e.g. {\"source_type\": \"curated_kb\"})","title":"Filters"},"description":"JSON-encoded metadata filters (e.g. {\"source_type\": \"curated_kb\"})"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VectorListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/ns/{ns}/images/{signed_token}":{"get":{"tags":["images"],"summary":"Get image by signed token","description":"Serve an image from S3 using a signed token. No auth header required. Token includes doc_id, image_id, and expiry. Generated by the query endpoint. Image is streamed directly from S3 without buffering in memory.","operationId":"get_image_v1_ns__ns__images__signed_token__get","parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"signed_token","in":"path","required":true,"schema":{"type":"string","title":"Signed Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}},"image/jpeg":{},"image/png":{}}},"403":{"description":"Invalid or expired token"},"404":{"description":"Image not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/client/{client_id}/ns/{ns}/lhn/conversations":{"get":{"tags":["lhn"],"summary":"List LHN conversations (tickets + chats)","description":"Paginated merged list of LiveHelpNow tickets and chats, ordered by creation time descending. Used by cortex-hub's QA Chat UI.\n\n**Filters:**\n- `type` — `ticket` | `chat` | `all` (default `all`)\n- `q` — case-insensitive substring match on subject / customer name / problem\n- `operator` — exact operator match\n- `from_ts`, `to_ts` — ISO timestamps on `created_time` (tickets) / `start_time` (chats)\n\nPage-based pagination (1-indexed).","operationId":"list_conversations_v1_client__client_id__ns__ns__lhn_conversations_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"type","in":"query","required":false,"schema":{"enum":["ticket","chat","all"],"type":"string","default":"all","title":"Type"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Q"}},{"name":"operator","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Operator"}},{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"From"}},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"To"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"exclude_external_ids","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated list of external_ids to skip. cortex-hub uses this for the \"To Review\" tab: it collects already-reviewed IDs from ch_qa_evaluations and excludes them so pagination counts stay consistent. Max 1000 ids — larger sets should use a different approach.","title":"Exclude External Ids"},"description":"Comma-separated list of external_ids to skip. cortex-hub uses this for the \"To Review\" tab: it collects already-reviewed IDs from ch_qa_evaluations and excludes them so pagination counts stay consistent. Max 1000 ids — larger sets should use a different approach."},{"name":"has_agent_response","in":"query","required":false,"schema":{"type":"boolean","description":"When true, only returns conversations that already have at least one human agent reply. For tickets: EXISTS a non-internal action whose actor is not the customer. For chats: EXISTS a message with a non-visitor sender_type. Reviewers default this on so they don't waste time on open/unanswered threads.","default":false,"title":"Has Agent Response"},"description":"When true, only returns conversations that already have at least one human agent reply. For tickets: EXISTS a non-internal action whose actor is not the customer. For chats: EXISTS a message with a non-visitor sender_type. Reviewers default this on so they don't waste time on open/unanswered threads."},{"name":"substantive","in":"query","required":false,"schema":{"type":"boolean","description":"When true, only returns conversations with substantive back-and-forth that's useful for QA comparison. For tickets this means a `body` longer than 500 chars AND containing an email-thread marker (`wrote:`) so we know the customer and agent actually exchanged meaningful content. For chats the heuristic is `wait_time > 0` AND duration ≥ 30s (skip abandoned sessions). Default off — set from the UI checkbox.","default":false,"title":"Substantive"},"description":"When true, only returns conversations with substantive back-and-forth that's useful for QA comparison. For tickets this means a `body` longer than 500 chars AND containing an email-thread marker (`wrote:`) so we know the customer and agent actually exchanged meaningful content. For chats the heuristic is `wait_time > 0` AND duration ≥ 30s (skip abandoned sessions). Default off — set from the UI checkbox."},{"name":"department","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"description":"Exact-match filter on the LiveHelpNow department name for both tickets and chats. The QA UI in cortex-hub uses this to isolate a reviewer to one team at a time. Value must match exactly (case-sensitive); pull from `/providers/livehelpnow/departments` rather than typing by hand. Trailing whitespace is stripped both on ingest (store.py) and in the comparison here.","title":"Department"},"description":"Exact-match filter on the LiveHelpNow department name for both tickets and chats. The QA UI in cortex-hub uses this to isolate a reviewer to one team at a time. Value must match exactly (case-sensitive); pull from `/providers/livehelpnow/departments` rather than typing by hand. Trailing whitespace is stripped both on ingest (store.py) and in the comparison here."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/lhn/customer-history":{"get":{"tags":["lhn"],"summary":"All tickets + chats for one customer email","description":"Given an email, returns every LHN ticket and chat that shares it (case-insensitive match on `customer_email` / `visitor_email`). Drives the QA 'Customer history' panel so a reviewer can jump between the selected conversation and sibling conversations from the same customer. Matches the existing `customer_history` MCP tool 1:1.\n\nNote: some tenants don't populate chat `visitor_email`; in that case only tickets appear.","operationId":"customer_history_v1_client__client_id__ns__ns__lhn_customer_history_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"email","in":"query","required":true,"schema":{"type":"string","minLength":3,"maxLength":320,"description":"Customer email. Case-insensitive match.","title":"Email"},"description":"Customer email. Case-insensitive match."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"department","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"description":"Optional exact-match department filter. When set, restricts both the ticket and chat sub-queries to rows where `department = $X`. Used by the QA panel to keep customer history scoped to the currently-selected team.","title":"Department"},"description":"Optional exact-match department filter. When set, restricts both the ticket and chat sub-queries to rows where `department = $X`. Used by the QA panel to keep customer history scoped to the currently-selected team."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__api__lhn__CustomerHistoryResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/lhn/conversations/{source_type}/{external_id}":{"get":{"tags":["lhn"],"summary":"LHN conversation detail","description":"Full thread for a single ticket or chat with derived `customer_message` (first customer content) and `agent_response` (first operator reply). The `messages` array contains every visible turn in chronological order.","operationId":"get_conversation_v1_client__client_id__ns__ns__lhn_conversations__source_type___external_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"source_type","in":"path","required":true,"schema":{"enum":["ticket","chat"],"type":"string","title":"Source Type"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationDetailResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/overview":{"get":{"tags":["lhn-data"],"summary":"High-level row counts for this tenant","description":"Row counts per entity type (tickets, chats, kb_articles) scoped to the caller's namespace and client_id. Best first call when exploring a tenant; use the other endpoints to drill into each bucket.","operationId":"overview_v1_client__client_id__ns__ns__providers_livehelpnow_overview_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OverviewResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/schema":{"get":{"tags":["lhn-data"],"summary":"Schema reflection for vs_lhn_* tables","description":"Returns columns, row counts for this tenant, sample rows, and (for JSONB columns) a union of top-level keys observed in the sample. Useful for building UI tables and for LLM prompt priming before a custom query.","operationId":"schema_v1_client__client_id__ns__ns__providers_livehelpnow_schema_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"sample_rows","in":"query","required":false,"schema":{"type":"integer","maximum":10,"minimum":0,"description":"How many sample rows to fetch per table (≤10).","default":3,"title":"Sample Rows"},"description":"How many sample rows to fetch per table (≤10)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchemaResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/analytics/timeseries":{"get":{"tags":["lhn-data"],"summary":"Tickets / chats volume over time","description":"Time-bucketed row counts via PostgreSQL `date_trunc`. Tickets bucket by `created_time`, chats by `start_time`. Newest bucket first. Supports `limit`/`offset` pagination to walk across long history windows.","operationId":"analytics_timeseries_v1_client__client_id__ns__ns__providers_livehelpnow_analytics_timeseries_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"entity","in":"query","required":true,"schema":{"enum":["tickets","chats"],"type":"string","description":"Entity to count.","title":"Entity"},"description":"Entity to count."},{"name":"bucket","in":"query","required":false,"schema":{"enum":["day","week","month"],"type":"string","description":"Bucket granularity.","default":"day","title":"Bucket"},"description":"Bucket granularity."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":365,"minimum":1,"description":"Max number of buckets per page (≤365).","default":90,"title":"Limit"},"description":"Max number of buckets per page (≤365)."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Number of buckets to skip from the newest end.","default":0,"title":"Offset"},"description":"Number of buckets to skip from the newest end."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeseriesResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/analytics/top":{"get":{"tags":["lhn-data"],"summary":"Top groups by row count","description":"Top-N group-by aggregation over tickets or chats. Common use: 'which category has the most tickets?', 'which operator handles the most chats?'. Paginate to walk the long tail.","operationId":"analytics_top_v1_client__client_id__ns__ns__providers_livehelpnow_analytics_top_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"entity","in":"query","required":true,"schema":{"enum":["tickets","chats"],"type":"string","title":"Entity"}},{"name":"group_by","in":"query","required":true,"schema":{"enum":["category","department","operator","status","priority","source_type"],"type":"string","title":"Group By"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopEntitiesResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/analytics/distribution":{"get":{"tags":["lhn-data"],"summary":"Numeric distribution for one field","description":"Summary statistics (count, min, max, avg, median, p95) for a numeric column — useful for answering 'what's the typical wait time' or 'how fat is the response-time tail'.","operationId":"analytics_distribution_v1_client__client_id__ns__ns__providers_livehelpnow_analytics_distribution_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"entity","in":"query","required":true,"schema":{"enum":["tickets","chats"],"type":"string","title":"Entity"}},{"name":"field","in":"query","required":true,"schema":{"enum":["wait_time","messages_count","sentiment","actions_count"],"type":"string","title":"Field"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DistributionResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/analytics/clusters/{cluster_id}":{"get":{"tags":["lhn-data"],"summary":"Drill into one aggregated cluster","description":"Return a single cluster from `vs_lhn_aggregated` along with its representative ticket or chat (inlined with actions/messages when still present). Paired with `/analytics/top` on the `category` dimension to 'open' a frequent bucket.","operationId":"cluster_details_v1_client__client_id__ns__ns__providers_livehelpnow_analytics_clusters__cluster_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"cluster_id","in":"path","required":true,"schema":{"type":"string","title":"Cluster Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClusterDetailResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Cluster not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/similar-resolved":{"post":{"tags":["lhn-data"],"summary":"Full-text search over closed tickets","description":"FTS over the `vs_lhn_tickets` GIN tsvector index (title + body + problem), restricted to `status='Closed'` — useful for surfacing how operators historically resolved similar problems. POST body so the query itself stays out of the URL.","operationId":"similar_resolved_v1_client__client_id__ns__ns__providers_livehelpnow_similar_resolved_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimilarResolvedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimilarResolvedResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/threads/tickets/{ticket_id}":{"get":{"tags":["lhn-data"],"summary":"Full ticket: metadata + audit journal + dialogue","description":"Single ticket with **both** inline arrays — the canonical drill-in for a single thread:\n\n• `actions[]` — audit journal rows from `vs_lhn_ticket_actions` (state transitions: Created, Assigned, Status changed, Tag added, …). These are **not** reply text — they're what LHN logs as operator actions.\n• `comments[]` — the actual customer↔agent dialogue from `vs_lhn_ticket_comments`: every reply-like row with `message` (the text, often multi-KB on email-forwarded tickets), `created_by` (system | operator email), `type` (CommentFromUnknown | CommentFromAgent | …), `customer_notified`, `visible`, `created_time`.\n\nNeither array is available from the lightweight `GET /tickets` list or the bulk `GET /threads?entity=tickets` (the latter inlines `actions[]` only, not `comments[]`) — per-ticket call is the only path to the reply text.\n\nSet `include_related=true` to also inline a compact list of the same customer's chats, joined by visitor_id OR customer_email, whichever this ticket carries.","operationId":"ticket_detail_v1_client__client_id__ns__ns__providers_livehelpnow_threads_tickets__ticket_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"ticket_id","in":"path","required":true,"schema":{"type":"string","title":"Ticket Id"}},{"name":"include_related","in":"query","required":false,"schema":{"type":"boolean","description":"If true, inline related_chats[] for the same customer.","default":false,"title":"Include Related"},"description":"If true, inline related_chats[] for the same customer."},{"name":"related_limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Max related chats to inline (≤100).","default":20,"title":"Related Limit"},"description":"Max related chats to inline (≤100)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketDetailResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Ticket not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/threads/chats/{chat_id}":{"get":{"tags":["lhn-data"],"summary":"Full chat with every message","description":"Returns the chat's metadata and all `vs_lhn_chat_messages` rows attached. Set `include_related=true` to inline the same customer's tickets (joined by visitor_id OR visitor_email).","operationId":"chat_detail_v1_client__client_id__ns__ns__providers_livehelpnow_threads_chats__chat_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"chat_id","in":"path","required":true,"schema":{"type":"string","title":"Chat Id"}},{"name":"include_related","in":"query","required":false,"schema":{"type":"boolean","description":"If true, inline related_tickets[] for the same customer.","default":false,"title":"Include Related"},"description":"If true, inline related_tickets[] for the same customer."},{"name":"related_limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Max related tickets to inline (≤100).","default":20,"title":"Related Limit"},"description":"Max related tickets to inline (≤100)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatDetailResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Chat not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/customers/history":{"get":{"tags":["lhn-data"],"summary":"All tickets + chats for a customer email","description":"Joins tickets (by `customer_email`) and chats (by `visitor_email`) on the same canonical identity. Supports partial-match ILIKE (e.g. `email=john@`). Offset walks both sides in lockstep.","operationId":"customer_history_v1_client__client_id__ns__ns__providers_livehelpnow_customers_history_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"email","in":"query","required":true,"schema":{"type":"string","minLength":1,"description":"Customer email (exact or partial ILIKE pattern).","title":"Email"},"description":"Customer email (exact or partial ILIKE pattern)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/app__api__lhn_provider__CustomerHistoryResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/kb/search":{"get":{"tags":["lhn-data"],"summary":"Full-text search over DynamicAnswers (AI-generated KB)","description":"FTS over the AI-generated half of `vs_lhn_kb_articles` (`ai_answer = TRUE`) — LiveHelpNow DynamicAnswers only. For the human-authored FAQ use `/faq/search`.\n\nThe search runs against the GIN tsvector index on `title + question + answer`. When `X-Active-Department` is set, the result set is scoped permissively: DynamicAnswers tagged with that department **plus** those with no department at all. The `ai_answer = TRUE` filter is applied server-side and cannot be overridden.","operationId":"kb_search_v1_client__client_id__ns__ns__providers_livehelpnow_kb_search_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"description":"Search terms (matched against title + question + answer).","title":"Q"},"description":"Search terms (matched against title + question + answer)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":10,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBSearchResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/kb/{external_id}":{"get":{"tags":["lhn-data"],"summary":"Single DynamicAnswer by external_id","description":"Fetches one DynamicAnswer (AI-generated KB entry) with its full body, categories, tags, rating, and counters. Returns 404 when the row is missing **or** when `external_id` actually belongs to a human-authored FAQ — use `/faq/{id}` for those.","operationId":"kb_detail_v1_client__client_id__ns__ns__providers_livehelpnow_kb__external_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBArticleDetail"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"DynamicAnswer not found for this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/operators/stats":{"get":{"tags":["lhn-data"],"summary":"Per-operator ticket workload","description":"Ticket counts (total / closed / open) per operator, ordered by total DESC. Handy for routing decisions or sizing coverage — paginate to see the long tail of low-volume operators.","operationId":"operator_stats_v1_client__client_id__ns__ns__providers_livehelpnow_operators_stats_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperatorStatsResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/categories/{category}/sop":{"get":{"tags":["lhn-data"],"summary":"Top clusters (SOP templates) for a category","description":"Returns the top clusters from `vs_lhn_aggregated` for the given category, ordered by `cluster_size` DESC. Each cluster carries a summary and keywords — think of the top few as a category SOP.","operationId":"category_sop_v1_client__client_id__ns__ns__providers_livehelpnow_categories__category__sop_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"category","in":"path","required":true,"schema":{"type":"string","title":"Category"}},{"name":"entity","in":"query","required":false,"schema":{"enum":["tickets","chats"],"type":"string","default":"tickets","title":"Entity"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":5,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategorySOPResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/threads":{"get":{"tags":["lhn-data"],"summary":"Bulk tickets or chats with transcripts inlined","description":"Bulk fetch of headers with nested transcripts inlined. Heavy — prefer `/tickets` or `/chats` for lightweight UI lists.\n\n**What gets inlined per item:**\n• `entity=chats`  → `items[].messages[]` — the full chat transcript (every `vs_lhn_chat_messages` row). This is the real customer↔operator dialogue.\n• `entity=tickets` → `items[].actions[]` — the **audit journal only** (`Ticket Created`, `Status changed`, `Tag added`, …). The actual reply text (`vs_lhn_ticket_comments.message`) is **NOT** included here — to read what the agent wrote back to the customer use the per-ticket detail `GET /threads/tickets/{id}`, which inlines both `actions[]` and `comments[]`.\n\n**Grouping & filters:** group-by-visitor via `visitor_id=…`; filter by date, operator, category, `customer_email`, or explicit `ids[]` (max 50 — uses request order and ignores other filters).\n\n**`with_conversation=true` by default** — drops tickets/chats with zero real dialogue (no rows in `vs_lhn_ticket_comments` for tickets, no rows in `vs_lhn_chat_messages` for chats). Response carries `total_all` and `total_with_conversation` so the caller sees how much was filtered out. Set `with_conversation=false` to include everything.","operationId":"list_threads_v1_client__client_id__ns__ns__providers_livehelpnow_threads_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"entity","in":"query","required":true,"schema":{"enum":["tickets","chats"],"type":"string","description":"Which thread type to fetch.","title":"Entity"},"description":"Which thread type to fetch."},{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Inclusive lower bound on start timestamp (ISO-8601). Tickets→created_time, chats→start_time.","title":"Date From"},"description":"Inclusive lower bound on start timestamp (ISO-8601). Tickets→created_time, chats→start_time."},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exclusive upper bound (ISO-8601).","title":"Date To"},"description":"Exclusive upper bound (ISO-8601)."},{"name":"operator","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact match on operator name.","title":"Operator"},"description":"Exact match on operator name."},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact match on category (tickets only).","title":"Category"},"description":"Exact match on category (tickets only)."},{"name":"customer_email","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Case-insensitive exact match on customer_email / visitor_email.","title":"Customer Email"},"description":"Case-insensitive exact match on customer_email / visitor_email."},{"name":"visitor_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact match on LHN visitor UUID (group-by-visitor).","title":"Visitor Id"},"description":"Exact match on LHN visitor UUID (group-by-visitor)."},{"name":"ids","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"description":"Explicit external_ids; max 50. Date/filters ignored when set.","title":"Ids"},"description":"Explicit external_ids; max 50. Date/filters ignored when set."},{"name":"with_conversation","in":"query","required":false,"schema":{"type":"boolean","description":"When true (default), return only threads carrying at least one row of real dialogue (`vs_lhn_ticket_comments` for tickets, `vs_lhn_chat_messages` for chats). Set `false` to include threads with no reply (auto-closed / header-only chats).","default":true,"title":"With Conversation"},"description":"When true (default), return only threads carrying at least one row of real dialogue (`vs_lhn_ticket_comments` for tickets, `vs_lhn_chat_messages` for chats). Set `false` to include threads with no reply (auto-closed / header-only chats)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThreadListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/departments":{"get":{"tags":["lhn-data"],"summary":"Distinct departments across all entities","description":"Union of distinct department values observed on `vs_lhn_tickets.department`, `vs_lhn_chats.department`, and `vs_lhn_kb_articles.department`. Includes per-source row counts so a UI can show usage weight next to each department.","operationId":"list_departments_v1_client__client_id__ns__ns__providers_livehelpnow_departments_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepartmentListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/tickets":{"get":{"tags":["lhn-data"],"summary":"Lightweight ticket list (headers only)","description":"Ticket **headers only** — intended for UI tables and index pages where drill-in happens on click.\n\n**Not returned here:**\n• `actions[]` — the per-ticket audit journal (`Ticket Created`, `Status changed`, `Tag added`, …).\n• `comments[]` — the actual customer↔agent reply text (`vs_lhn_ticket_comments.message`).\n\nFor either of those, call `GET /threads/tickets/{id}` — the detail endpoint inlines **both** arrays. For batch scenarios where you need several full transcripts at once see `GET /threads` (note: that bulk endpoint carries `actions[]` but **not** `comments[]` — per-ticket call is required for reply text).\n\n**Filters:** `date_from`/`date_to`, `operator`, `category`, `status`, `customer_email`, `visitor_id`. `X-Active-Department` header restricts to one department.\n\n**`with_conversation=true` by default** — drops tickets with no rows in `vs_lhn_ticket_comments` (auto-closed / offline-form-only). Response carries `total_all` and `total_with_conversation` so callers see how much was filtered.","operationId":"list_tickets_v1_client__client_id__ns__ns__providers_livehelpnow_tickets_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Inclusive ISO-8601 lower bound on created_time.","title":"Date From"},"description":"Inclusive ISO-8601 lower bound on created_time."},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exclusive ISO-8601 upper bound on created_time.","title":"Date To"},"description":"Exclusive ISO-8601 upper bound on created_time."},{"name":"operator","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact match on operator.","title":"Operator"},"description":"Exact match on operator."},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact match on category.","title":"Category"},"description":"Exact match on category."},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact match on status (e.g. `Open`, `Closed`).","title":"Status"},"description":"Exact match on status (e.g. `Open`, `Closed`)."},{"name":"customer_email","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Case-insensitive exact match on customer_email.","title":"Customer Email"},"description":"Case-insensitive exact match on customer_email."},{"name":"visitor_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact match on LHN visitor UUID (group-by-visitor).","title":"Visitor Id"},"description":"Exact match on LHN visitor UUID (group-by-visitor)."},{"name":"with_conversation","in":"query","required":false,"schema":{"type":"boolean","description":"When true (default), list only tickets that have ≥1 row in `vs_lhn_ticket_comments` (real customer↔agent dialogue). Set `false` to include tickets without any reply.","default":true,"title":"With Conversation"},"description":"When true (default), list only tickets that have ≥1 row in `vs_lhn_ticket_comments` (real customer↔agent dialogue). Set `false` to include tickets without any reply."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/chats":{"get":{"tags":["lhn-data"],"summary":"Lightweight chat list","description":"Chat headers only (no `messages[]`) — intended for UI tables. Filters: date window, `operator`, `terminated_by`, `customer_email`, `visitor_id`. Use `GET /threads/chats/{id}` to fetch messages on demand.\n\n**`with_conversation=true` by default** — drops chats with no rows in `vs_lhn_chat_messages` (on veracity ≈31% of chats are in this state: old header-only ingests). Response carries `total_all` and `total_with_conversation` for caller visibility.","operationId":"list_chats_v1_client__client_id__ns__ns__providers_livehelpnow_chats_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Inclusive ISO-8601 lower bound on start_time.","title":"Date From"},"description":"Inclusive ISO-8601 lower bound on start_time."},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exclusive ISO-8601 upper bound on start_time.","title":"Date To"},"description":"Exclusive ISO-8601 upper bound on start_time."},{"name":"operator","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact match on operator.","title":"Operator"},"description":"Exact match on operator."},{"name":"terminated_by","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact match on terminated_by.","title":"Terminated By"},"description":"Exact match on terminated_by."},{"name":"customer_email","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Case-insensitive exact match on visitor_email.","title":"Customer Email"},"description":"Case-insensitive exact match on visitor_email."},{"name":"visitor_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact match on LHN visitor UUID.","title":"Visitor Id"},"description":"Exact match on LHN visitor UUID."},{"name":"with_conversation","in":"query","required":false,"schema":{"type":"boolean","description":"When true (default), list only chats carrying real messages (≥1 row in `vs_lhn_chat_messages`). Set `false` to include header-only chats.","default":true,"title":"With Conversation"},"description":"When true (default), list only chats carrying real messages (≥1 row in `vs_lhn_chat_messages`). Set `false` to include header-only chats."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/kb":{"get":{"tags":["lhn-data"],"summary":"Browse list of DynamicAnswers (AI-generated KB)","description":"Paginated list of DynamicAnswers (AI-generated KB entries, `ai_answer = TRUE`). For human-authored FAQ use `/faq`.\n\nNo full-text search — this is the browse surface; use `/kb/search` when the caller has a query string. Filters: `category` (exact element match inside `categories[]`) and `X-Active-Department` header (permissive: rows with that department plus rows with no department). The `ai_answer = TRUE` filter is applied server-side and cannot be overridden.","operationId":"list_kb_v1_client__client_id__ns__ns__providers_livehelpnow_kb_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Match exact value inside `categories[]`.","title":"Category"},"description":"Match exact value inside `categories[]`."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/faq/search":{"get":{"tags":["lhn-data"],"summary":"Full-text search over human-authored FAQ entries","description":"FTS over the human-authored half of `vs_lhn_kb_articles` (`ai_answer = FALSE`) — the FAQ written by support staff. For AI-generated DynamicAnswers use `/kb/search`.\n\nThe search runs against the GIN tsvector index on `title + question + answer`. When `X-Active-Department` is set, the result set is scoped permissively: FAQ entries tagged with that department **plus** the typical cross-department FAQ (rows with no department). The `ai_answer = FALSE` filter is applied server-side and cannot be overridden.","operationId":"faq_search_v1_client__client_id__ns__ns__providers_livehelpnow_faq_search_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"description":"Search terms (matched against title + question + answer).","title":"Q"},"description":"Search terms (matched against title + question + answer)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":10,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBSearchResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/faq":{"get":{"tags":["lhn-data"],"summary":"Browse list of human-authored FAQ entries","description":"Paginated list of human-authored FAQ entries (`ai_answer = FALSE`). For AI-generated DynamicAnswers use `/kb`.\n\nNo full-text search — this is the browse surface; use `/faq/search` when the caller has a query string. Filters: `category` (exact element match inside `categories[]`) and `X-Active-Department` header (permissive: rows with that department plus the typical cross-department FAQ with no department). The `ai_answer = FALSE` filter is applied server-side and cannot be overridden.","operationId":"list_faq_v1_client__client_id__ns__ns__providers_livehelpnow_faq_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Match exact value inside `categories[]`.","title":"Category"},"description":"Match exact value inside `categories[]`."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/faq/{external_id}":{"get":{"tags":["lhn-data"],"summary":"Single FAQ entry by external_id","description":"Fetches one human-authored FAQ entry with its full body, categories, tags, rating, and counters. Returns 404 when the row is missing **or** when `external_id` actually belongs to a DynamicAnswer — use `/kb/{id}` for those.","operationId":"faq_detail_v1_client__client_id__ns__ns__providers_livehelpnow_faq__external_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KBArticleDetail"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"FAQ entry not found for this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/visitors":{"get":{"tags":["lhn-data"],"summary":"Aggregated visitor rollup","description":"One row per `visitor_id` — union of tickets and chats, grouped. Columns: last known email, last known name, tickets_count, chats_count, last_seen. Ordered by `last_seen` DESC. `X-Active-Department` narrows both sides before the group by.","operationId":"list_visitors_v1_client__client_id__ns__ns__providers_livehelpnow_visitors_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VisitorListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/visitors/{visitor_id}":{"get":{"tags":["lhn-data"],"summary":"All tickets + chats for one visitor","description":"Full cross-channel history for a single visitor_id. Matches by LHN UUID only (no email heuristics). Use this when the UI has drilled into a single customer from the `/visitors` list.","operationId":"visitor_detail_v1_client__client_id__ns__ns__providers_livehelpnow_visitors__visitor_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"visitor_id","in":"path","required":true,"schema":{"type":"string","title":"Visitor Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VisitorDetailResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/sops":{"get":{"tags":["lhn-data"],"summary":"List auto-generated SOPs","description":"Paginated browse over ``vs_lhn_sops`` with filters on ``department``, ``category``, ``status`` and ``confidence_min``. Hub uses this for ``/admin/sop/drafts``.","operationId":"list_sops_v1_client__client_id__ns__ns__providers_livehelpnow_sops_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"department","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"}},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"confidence_min","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"title":"Confidence Min"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Full-text query over title + body; forces status=published.","title":"Q"},"description":"Full-text query over title + body; forces status=published."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SopListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/sops/coverage":{"get":{"tags":["lhn-data"],"summary":"SOP coverage report","description":"Gap report — how many aggregated clusters exist for this tenant vs. how many already have a draft/published SOP, plus the biggest uncovered clusters. Feeds the hub dashboard widget described in SOP-GENERATOR-HUB-INTEGRATION.md.","operationId":"sops_coverage_v1_client__client_id__ns__ns__providers_livehelpnow_sops_coverage_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"cluster_size_min","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Cluster Size Min"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"min_dialogue_chars","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Clusters whose representative_id carries less than this many chars of customer-visible text are hidden from ``top_gaps`` by default. Matches the SOP generator's pre-check so the UI doesn't surface clusters that would all be skipped.","default":200,"title":"Min Dialogue Chars"},"description":"Clusters whose representative_id carries less than this many chars of customer-visible text are hidden from ``top_gaps`` by default. Matches the SOP generator's pre-check so the UI doesn't surface clusters that would all be skipped."},{"name":"include_empty","in":"query","required":false,"schema":{"type":"boolean","description":"Return the sparse-dialogue tail as well. The response still reports ``total_empty`` separately so the UI can render a 'Show sparse clusters' toggle either way.","default":false,"title":"Include Empty"},"description":"Return the sparse-dialogue tail as well. The response still reports ``total_empty`` separately so the UI can render a 'Show sparse clusters' toggle either way."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverageResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/sops/{external_id}":{"get":{"tags":["lhn-data"],"summary":"Fetch one SOP","description":"Detail view including what_to_do, what_to_say, citation_map, source ids and — when ``?debug=true`` — the full audit trail.","operationId":"get_sop_v1_client__client_id__ns__ns__providers_livehelpnow_sops__external_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}},{"name":"debug","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Debug"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SopDetail"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/sops/generate":{"post":{"tags":["lhn-data"],"summary":"Trigger SOP generation","description":"Enqueue an ingest-style job with ``phases=['sop']``. Returns the job id so the caller can poll ``/admin/jobs/{id}`` for progress. Authorization is whatever the Bearer token grants for the ``(namespace, client_id)`` path — the caller (cortex-hub or another tenant integration) is responsible for its own role-gating. Tenant API-keys are intentionally allowed to trigger generation so product teams don't need to hand their VS master key out to every hub deployment.","operationId":"generate_sops_v1_client__client_id__ns__ns__providers_livehelpnow_sops_generate_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateSopsBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateSopsResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/sops/{external_id}/approve":{"post":{"tags":["lhn-data"],"summary":"Approve a draft SOP","description":"Flip ``status`` from ``draft`` to ``published`` and stamp ``reviewed_by``/``reviewed_at``. Idempotent — re-approving an already-published SOP returns the row unchanged.","operationId":"approve_sop_v1_client__client_id__ns__ns__providers_livehelpnow_sops__external_id__approve_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SopDetail"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/sops/{external_id}/archive":{"post":{"tags":["lhn-data"],"summary":"Archive a SOP","description":"Soft-delete: sets ``status='archived'``. The row stays for audit and the Turbopuffer chunk is kept (archive != retract). Re-approval after archive is possible.","operationId":"archive_sop_v1_client__client_id__ns__ns__providers_livehelpnow_sops__external_id__archive_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/providers/livehelpnow/sops/{external_id}/feedback":{"post":{"tags":["lhn-data"],"summary":"Record SOP usage / vote","description":"Non-admin; any tenant token may emit. ``usage=true`` bumps ``usage_count`` (called when an agent clicks through a citation in chat). ``vote='up'|'down'`` bumps the corresponding counter.","operationId":"feedback_sop_v1_client__client_id__ns__ns__providers_livehelpnow_sops__external_id__feedback_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/entailment/check":{"post":{"tags":["entailment"],"summary":"Per-step factual-consistency check for a generated SOP","description":"For each `step`, ask Bedrock Haiku whether the step is supported by the chunk it cites (or, when no `cited_source_id` is given, by the concatenated chunk context). Returns one verdict per step plus an `all_supported` summary the caller uses to gate auto-approval.\n\nFail-closed: LLM errors yield `supported=false` with `reason=llm_error: ...` so a hallucinated step never slips through because the model timed out. Cap of 50 steps and 200 chunks per request.","operationId":"check_entailment_v1_entailment_check_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntailmentRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntailmentResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"No LLM provider configured (set Bedrock credentials in Admin Panel)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"BearerAuth":[]}]}},"/v1/ns/{ns}/by-tenant/{client_id}":{"delete":{"tags":["management"],"summary":"Per-tenant vector purge (filtered)","description":"**Destructive.** Deletes every Turbopuffer vector in `ns` tagged `client_id={client_id}` and every `vs_kb_chunks` row scoped to `(namespace, client_id)`. Other tenants in the same namespace are unaffected.\n\nThe namespace is resolved through `VS_TURBOPUFFER_PREFIX` before the delete — to purge legacy data, run with the env var unset.\n\nIdempotent: running twice is harmless (second call deletes zero rows). Best-effort ordering — Turbopuffer first, Postgres second; response surfaces partial-failure detail.","operationId":"purge_by_tenant_v1_ns__ns__by_tenant__client_id__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurgeResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/faqs/stats":{"get":{"tags":["faqs"],"summary":"Aggregate counts + top-cited FAQs (admin)","operationId":"faq_stats_v1_client__client_id__ns__ns__faqs_stats_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqStatsResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/faqs/citations:batch":{"post":{"tags":["faqs"],"summary":"Bump citation_count for many FAQs in one call","description":"Cortex-hub's chat route calls this once per response with every FAQ doc_id it cited. Up to 500 ids per call; duplicates increment by their multiplicity (one chat citing the same FAQ twice bumps it by 2). Unknown ids are silently skipped — an FAQ deleted between search-time and citation-time should not fail the whole batch.\n\nRe-indexing is debounced via the dirty flag — this endpoint marks rows whose priority band changed and the FaqDirtyFlusher picks them up on its 60s cadence. So a chat storm produces at most 1 Turbopuffer write per FAQ per minute, no matter how many citations land in between.","operationId":"bump_citations_batch_v1_client__client_id__ns__ns__faqs_citations_batch_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqCitationBatchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqCitationBatchResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/faqs":{"get":{"tags":["faqs"],"summary":"List FAQs in a namespace + client_id","description":"Cursor-paginated. Pass the ``next_cursor`` from a previous response back as the ``cursor`` query param to continue. Ordering is by ``external_id`` ascending — opaque to callers but reproducible for the server.","operationId":"list_faqs_v1_client__client_id__ns__ns__faqs_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/faqs/{external_id}":{"put":{"tags":["faqs"],"summary":"Upsert (create or edit) an editorial FAQ","description":"Idempotent on ``(namespace, client_id, external_id)``. Counters (``citation_count``, etc.) survive editorial edits — only DELETE clears them. The row is marked dirty and re-indexed into Turbopuffer immediately as part of this call.","operationId":"upsert_faq_v1_client__client_id__ns__ns__faqs__external_id__put","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqUpsertRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"FAQ store / indexer not configured, or embedding provider error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["faqs"],"summary":"Read one FAQ by external_id","operationId":"get_faq_v1_client__client_id__ns__ns__faqs__external_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"FAQ not found in this (namespace, client_id)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["faqs"],"summary":"Delete an FAQ and its Turbopuffer vector","description":"Removes the Turbopuffer vector first, then the PG row. A partial failure leaves a dangling PG row pointing at no vector (recoverable by re-PUT) rather than an orphan vector (only recoverable by namespace purge).","operationId":"delete_faq_v1_client__client_id__ns__ns__faqs__external_id__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqDeleteResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"FAQ not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/faqs/{external_id}/citations":{"post":{"tags":["faqs"],"summary":"Increment a single FAQ's citation_count","description":"Useful for admin / replay flows. The chat hot-path uses the batch endpoint instead — N citations in one call instead of N round trips. Re-indexing only fires when this increment moved the priority band; otherwise the row is left dirty for the FaqDirtyFlusher to consolidate on its next sweep.","operationId":"bump_citation_v1_client__client_id__ns__ns__faqs__external_id__citations_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"external_id","in":"path","required":true,"schema":{"type":"string","title":"External Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqCitationRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqCitationResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"FAQ not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"webhooks":{"doc.processing.done":{"post":{"summary":"Document processing finished","description":"Delivered after a document upload, fetch-url, or reprocess job reaches a terminal state. Sent to the webhook URL configured on the API key. Header `X-Webhook-Signature: sha256=<hmac>` is present when a webhook secret is configured.","operationId":"doc_processing_donedoc_processing_done_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentProcessedWebhook"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"crawl.page":{"post":{"summary":"Per-page crawl event","description":"Delivered once for each crawled page after its processing is finished.","operationId":"crawl_pagecrawl_page_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlPageWebhook"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"crawl.done":{"post":{"summary":"Crawl job finished","description":"Delivered once when a crawl job reaches a terminal status.","operationId":"crawl_donecrawl_done_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlDoneWebhook"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"ingest.progress":{"post":{"summary":"Ingest job progress","description":"Delivered at each phase boundary of a running ingest job.","operationId":"ingest_progressingest_progress_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestProgressWebhook"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"ingest.done":{"post":{"summary":"Ingest job completed","description":"Delivered once when an ingest job completes successfully.","operationId":"ingest_doneingest_done_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestDoneWebhook"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"ingest.error":{"post":{"summary":"Ingest job failed","description":"Delivered once if an ingest job fails.","operationId":"ingest_erroringest_error_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestErrorWebhook"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AggregateRequest":{"properties":{"group_by":{"items":{"type":"string"},"type":"array","maxItems":20,"minItems":1,"title":"Group By","description":"Metadata fields to group by. Each field produces a separate distribution."},"filters":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Filters","description":"Optional metadata filters (same syntax as query filters: exact, IN, _gte, _lte, _ne, _contains)."}},"type":"object","required":["group_by"],"title":"AggregateRequest"},"AggregateResponse":{"properties":{"distributions":{"items":{"$ref":"#/components/schemas/FieldDistribution"},"type":"array","title":"Distributions","description":"One distribution per requested field"},"total_vectors":{"type":"integer","title":"Total Vectors","description":"Total active vectors in namespace"}},"type":"object","required":["distributions","total_vectors"],"title":"AggregateResponse"},"Body_upload_document_v1_client__client_id__ns__ns__documents_upload_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"doc_type":{"type":"string","title":"Doc Type","default":"general"},"category":{"type":"string","title":"Category","default":""},"priority":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Priority","description":"Document priority 1-10. Omit to use tier default for this doc_type."},"analyze_images":{"type":"boolean","title":"Analyze Images","description":"Extract and analyze images via Vision LLM","default":false},"pipeline":{"type":"string","title":"Pipeline","description":"Named pipeline config (enrichment/vector strategy)","default":""}},"type":"object","required":["file"],"title":"Body_upload_document_v1_client__client_id__ns__ns__documents_upload_post"},"CategorySOPCluster":{"properties":{"external_id":{"type":"string","title":"External Id","description":"Cluster external_id for drill-down via `/analytics/clusters/{id}`."},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content","description":"Aggregated cluster summary — acts as a SOP template hint."},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"cluster_size":{"type":"integer","title":"Cluster Size","default":0},"keywords":{"items":{"type":"string"},"type":"array","title":"Keywords"},"representative_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Representative Id"},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata"}},"type":"object","required":["external_id"],"title":"CategorySOPCluster"},"CategorySOPResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"category":{"type":"string","title":"Category"},"entity":{"type":"string","enum":["tickets","chats"],"title":"Entity"},"clusters":{"items":{"$ref":"#/components/schemas/CategorySOPCluster"},"type":"array","title":"Clusters"}},"type":"object","required":["limit","offset","has_more","category","entity","clusters"],"title":"CategorySOPResponse"},"ChatDetailResponse":{"properties":{"external_id":{"type":"string","title":"External Id"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"operator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operator"},"visitor_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visitor Name"},"visitor_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visitor Email"},"visitor_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visitor Id","description":"LHN visitor UUID — canonical cross-channel identity."},"sentiment":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Sentiment"},"terminated_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Terminated By"},"wait_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Wait Time"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"messages_count":{"type":"integer","title":"Messages Count","default":0},"start_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Start Time"},"end_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"End Time"},"launch_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Launch Url"},"messages":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Messages","description":"Every `vs_lhn_chat_messages` row (sender_type, sender_name, content, sent_at), ordered by sent_at."},"related_tickets":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Related Tickets","description":"Only present when `include_related=true`. Compact ticket headers for the same customer — joined on visitor_id OR visitor_email, whichever the chat carries."}},"type":"object","required":["external_id"],"title":"ChatDetailResponse"},"ChatHeader":{"properties":{"external_id":{"type":"string","title":"External Id"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"operator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operator"},"visitor_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visitor Name"},"visitor_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visitor Email"},"visitor_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visitor Id"},"sentiment":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Sentiment"},"terminated_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Terminated By"},"wait_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Wait Time"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"messages_count":{"type":"integer","title":"Messages Count","default":0},"start_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Start Time"},"end_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"End Time"},"launch_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Launch Url"}},"type":"object","required":["external_id"],"title":"ChatHeader"},"ChatListResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"items":{"items":{"$ref":"#/components/schemas/ChatHeader"},"type":"array","title":"Items","description":"Chat headers without messages; newest first."},"total_all":{"type":"integer","title":"Total All","description":"Total chats matching this tenant (no `with_conversation` filter)."},"total_with_conversation":{"type":"integer","title":"Total With Conversation","description":"Chats with ≥1 row in `vs_lhn_chat_messages`. Headers where `messages_count>0` but no message rows exist (old ingests that skipped chat history) are excluded."},"with_conversation":{"type":"boolean","title":"With Conversation","description":"Echoes whether items were filtered to chats with real messages.","default":true}},"type":"object","required":["limit","offset","has_more","items","total_all","total_with_conversation"],"title":"ChatListResponse"},"ChunkItem":{"properties":{"chunk_id":{"type":"string","maxLength":128,"minLength":1,"title":"Chunk Id","description":"Caller-supplied unique chunk identifier (logical chunk-level ID, not vector ID)."},"text":{"type":"string","maxLength":10000,"minLength":1,"title":"Text","description":"Text to embed (1-10000 chars)"},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata","description":"Arbitrary user metadata (strings, numbers, booleans, lists). Reserved keys set by Vector Storage and **never** to be supplied here: `chunk_text`, `doc_id`, `client_id`, `doc_type`, `source`. Max 10 KB serialized."}},"additionalProperties":true,"type":"object","required":["chunk_id","text"],"title":"ChunkItem","description":"A single chunk in an upsert request.\n\nThe canonical field is ``chunk_id``. The legacy ``id`` alias is still\naccepted on input for backwards compatibility but is not emitted on\noutput."},"ChunkPayload":{"properties":{"source_id":{"type":"string","maxLength":128,"minLength":1,"title":"Source Id"},"chunk_text":{"type":"string","maxLength":20000,"minLength":1,"title":"Chunk Text"},"procedure_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Procedure Name"},"step_number":{"anyOf":[{"type":"integer","maximum":999.0,"minimum":0.0},{"type":"null"}],"title":"Step Number"}},"type":"object","required":["source_id","chunk_text"],"title":"ChunkPayload"},"ChunkStatusItem":{"properties":{"chunk_id":{"type":"string","title":"Chunk Id","description":"Caller-supplied chunk ID"},"status":{"type":"string","enum":["created","updated"],"title":"Status","description":"Outcome for this chunk: 'created' (new) or 'updated' (overwrote existing)."}},"type":"object","required":["chunk_id","status"],"title":"ChunkStatusItem"},"ChunkUpsertRequest":{"properties":{"doc_id":{"type":"string","maxLength":128,"minLength":1,"title":"Doc Id","description":"Logical document ID for grouping chunks"},"doc_type":{"type":"string","maxLength":64,"title":"Doc Type","description":"Document type (sop, kb, shopify, etc.)","default":"custom"},"pipeline":{"type":"string","maxLength":64,"title":"Pipeline","description":"Named pipeline config (enrichment/vector strategy)","default":""},"priority":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":1.0},{"type":"null"}],"title":"Priority","description":"Document priority 1-10 used by Source Priority Engine for ranking boost. When omitted, the tier default for this doc_type is used (see Admin Panel → Source Priority)."},"chunks":{"items":{"$ref":"#/components/schemas/ChunkItem"},"type":"array","maxItems":500,"minItems":1,"title":"Chunks","description":"Chunks to upsert (1-500)"}},"type":"object","required":["doc_id","chunks"],"title":"ChunkUpsertRequest","examples":[{"chunks":[{"chunk_id":"sop-trigger-001","metadata":{"keywords":["tracking","order","shipping"],"sop_group":"Orders & Shipping","source_type":"sop","vector_type":"trigger"},"text":"Customer asks about order tracking, shipping status, where is my order"},{"chunk_id":"sop-steps-001","metadata":{"sop_group":"Orders & Shipping","source_type":"sop","vector_type":"steps"},"text":"Step 1: Check order status in Shopify admin. Step 2: Provide tracking number."}],"doc_id":"sop-doc-550e8400-e29b","doc_type":"sop"}]},"ChunkUpsertResponse":{"properties":{"status":{"type":"string","title":"Status","default":"ok"},"doc_id":{"type":"string","title":"Doc Id"},"upserted":{"type":"integer","title":"Upserted"},"index_total":{"type":"integer","title":"Index Total"},"chunks":{"items":{"$ref":"#/components/schemas/ChunkStatusItem"},"type":"array","title":"Chunks"}},"type":"object","required":["doc_id","upserted","index_total","chunks"],"title":"ChunkUpsertResponse","examples":[{"chunks":[{"chunk_id":"sop-trigger-001","status":"created"},{"chunk_id":"sop-steps-001","status":"created"}],"doc_id":"sop-doc-550e8400-e29b","index_total":4521,"status":"ok","upserted":2}]},"ClientConfigsListResponse":{"properties":{"clients":{"items":{"$ref":"#/components/schemas/PipelineConfigSummary"},"type":"array","title":"Clients"}},"type":"object","required":["clients"],"title":"ClientConfigsListResponse"},"ClusterDetailResponse":{"properties":{"external_id":{"type":"string","title":"External Id","description":"Cluster external_id (e.g. `ticket_cluster:Billing:42`)."},"doc_type":{"type":"string","title":"Doc Type","description":"Either `ticket_cluster` or `chat_cluster`."},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title","description":"Cluster title / summary headline."},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content","description":"Aggregated summary text indexed by the cluster."},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category","description":"Source category the cluster belongs to."},"cluster_size":{"type":"integer","title":"Cluster Size","description":"How many source rows (tickets or chats) were merged.","default":0},"keywords":{"items":{"type":"string"},"type":"array","title":"Keywords","description":"Representative keywords."},"representative_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Representative Id","description":"External_id of the representative source row."},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata","description":"Aggregator metadata (shape varies)."},"representative":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Representative","description":"When the representative ticket/chat is still in vs_lhn_*, its full body (actions or messages inline)."}},"type":"object","required":["external_id","doc_type"],"title":"ClusterDetailResponse"},"ConversationDetailResponse":{"properties":{"type":{"type":"string","enum":["ticket","chat"],"title":"Type"},"external_id":{"type":"string","title":"External Id"},"subject":{"type":"string","title":"Subject"},"customer_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Name"},"customer_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Email"},"operator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operator"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"ended_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ended At"},"wait_time":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Wait Time"},"sentiment":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Sentiment"},"terminated_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Terminated By"},"transcript_available":{"type":"boolean","title":"Transcript Available","default":true},"customer_message":{"type":"string","title":"Customer Message"},"customer_message_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Message At"},"agent_response":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Response"},"agent_response_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Response At"},"messages":{"items":{"$ref":"#/components/schemas/ConversationMessage"},"type":"array","title":"Messages"}},"type":"object","required":["type","external_id","subject","customer_message"],"title":"ConversationDetailResponse"},"ConversationListItem":{"properties":{"type":{"type":"string","enum":["ticket","chat"],"title":"Type"},"external_id":{"type":"string","title":"External Id"},"subject":{"type":"string","title":"Subject"},"customer_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Name"},"customer_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Email"},"operator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operator"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"message_count":{"type":"integer","title":"Message Count","default":0},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"}},"type":"object","required":["type","external_id","subject"],"title":"ConversationListItem"},"ConversationListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ConversationListItem"},"type":"array","title":"Items"},"page":{"type":"integer","title":"Page"},"total_pages":{"type":"integer","title":"Total Pages"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["items","page","total_pages","total"],"title":"ConversationListResponse"},"ConversationMessage":{"properties":{"role":{"type":"string","enum":["customer","agent"],"title":"Role"},"author":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Author"},"body":{"type":"string","title":"Body"},"at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"At"}},"type":"object","required":["role","body"],"title":"ConversationMessage"},"CountsByType":{"properties":{"tickets":{"type":"integer","title":"Tickets","description":"Total vs_lhn_tickets rows for this tenant."},"chats":{"type":"integer","title":"Chats","description":"Total vs_lhn_chats rows for this tenant."},"kb_articles":{"type":"integer","title":"Kb Articles","description":"Total vs_lhn_kb_articles rows for this tenant."}},"type":"object","required":["tickets","chats","kb_articles"],"title":"CountsByType"},"CoverageGap":{"properties":{"cluster_external_id":{"type":"string","title":"Cluster External Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"doc_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Doc Type"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"cluster_size":{"type":"integer","title":"Cluster Size"},"keywords":{"items":{"type":"string"},"type":"array","title":"Keywords"},"representative_dialogue_chars":{"type":"integer","title":"Representative Dialogue Chars","description":"Sum of customer-visible text (message/body/comments) on the cluster's representative_id. Clusters below ``min_dialogue_chars`` in coverage are hidden by default.","default":0}},"type":"object","required":["cluster_external_id","cluster_size"],"title":"CoverageGap"},"CoverageResponse":{"properties":{"total_clusters":{"type":"integer","title":"Total Clusters"},"clusters_with_sop":{"type":"integer","title":"Clusters With Sop"},"clusters_without_sop":{"type":"integer","title":"Clusters Without Sop"},"total_empty":{"type":"integer","title":"Total Empty","description":"Uncovered clusters that fall below ``min_dialogue_chars`` and are hidden from ``top_gaps`` unless ``include_empty=true``.","default":0},"min_dialogue_chars":{"type":"integer","title":"Min Dialogue Chars","description":"Threshold used to split rich-dialogue from empty clusters.","default":200},"top_gaps":{"items":{"$ref":"#/components/schemas/CoverageGap"},"type":"array","title":"Top Gaps"}},"type":"object","required":["total_clusters","clusters_with_sop","clusters_without_sop","top_gaps"],"title":"CoverageResponse"},"CrawlDoneWebhook":{"properties":{"event":{"type":"string","const":"crawl.done","title":"Event","default":"crawl.done"},"crawl_id":{"type":"string","title":"Crawl Id"},"status":{"type":"string","enum":["done","stopped","error"],"title":"Status"},"pages_found":{"type":"integer","title":"Pages Found"},"pages_processed":{"type":"integer","title":"Pages Processed"},"pages_error":{"type":"integer","title":"Pages Error"},"total_chunks":{"type":"integer","title":"Total Chunks"},"finished_at":{"type":"string","title":"Finished At","description":"ISO-8601 UTC timestamp"}},"type":"object","required":["crawl_id","status","pages_found","pages_processed","pages_error","total_chunks","finished_at"],"title":"CrawlDoneWebhook","description":"Sent once when a crawl job reaches a terminal state."},"CrawlPageInfo":{"properties":{"url":{"type":"string","title":"Url"},"doc_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Doc Id"},"status":{"type":"string","enum":["pending","processing","ready","error","skipped","empty"],"title":"Status","description":"Per-page status. One of: pending, processing, ready, error, skipped, empty."},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"depth":{"type":"integer","title":"Depth"},"title":{"type":"string","title":"Title","default":""},"chunk_count":{"type":"integer","title":"Chunk Count","default":0},"extracted_text_length":{"type":"integer","title":"Extracted Text Length","default":0}},"type":"object","required":["url","status","depth"],"title":"CrawlPageInfo"},"CrawlPageWebhook":{"properties":{"event":{"type":"string","const":"crawl.page","title":"Event","default":"crawl.page"},"crawl_id":{"type":"string","title":"Crawl Id"},"doc_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Doc Id","description":"Document ID, null if the page was skipped/errored before indexing"},"url":{"type":"string","title":"Url"},"status":{"type":"string","enum":["pending","processing","ready","error","skipped","empty"],"title":"Status"},"chunk_count":{"type":"integer","title":"Chunk Count","default":0},"filename":{"type":"string","title":"Filename","default":""},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["crawl_id","url","status"],"title":"CrawlPageWebhook","description":"Sent for each crawled page once its processing finishes."},"CrawlReprocessResponse":{"properties":{"crawl_id":{"type":"string","title":"Crawl Id"},"status":{"$ref":"#/components/schemas/CrawlStatus"},"message":{"type":"string","title":"Message"}},"type":"object","required":["crawl_id","status","message"],"title":"CrawlReprocessResponse"},"CrawlRequest":{"properties":{"start_url":{"type":"string","maxLength":2048,"minLength":10,"title":"Start Url"},"max_depth":{"type":"integer","maximum":10.0,"minimum":1.0,"title":"Max Depth","default":3},"max_pages":{"type":"integer","maximum":500.0,"minimum":1.0,"title":"Max Pages","default":50},"crawl_delay":{"type":"number","maximum":10.0,"minimum":0.5,"title":"Crawl Delay","default":1.0},"url_pattern":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Url Pattern"},"exclude_pattern":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Exclude Pattern"},"follow_sitemap":{"type":"boolean","title":"Follow Sitemap","default":true},"respect_robots":{"type":"boolean","title":"Respect Robots","default":true},"strip_query_params":{"type":"boolean","title":"Strip Query Params","default":true},"doc_type":{"type":"string","title":"Doc Type","default":"webpage"},"category":{"type":"string","title":"Category","default":""},"priority":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":1.0},{"type":"null"}],"title":"Priority","description":"Document priority 1-10. When omitted, the tier default for this doc_type is used."},"analyze_images":{"type":"boolean","title":"Analyze Images","description":"Extract and analyze images from crawled pages via Vision LLM","default":false}},"type":"object","required":["start_url"],"title":"CrawlRequest"},"CrawlResponse":{"properties":{"crawl_id":{"type":"string","title":"Crawl Id"},"start_url":{"type":"string","title":"Start Url"},"max_pages":{"type":"integer","title":"Max Pages"},"status":{"$ref":"#/components/schemas/CrawlStatus","description":"Initial crawl status. One of: pending, running, done, stopped, error."}},"type":"object","required":["crawl_id","start_url","max_pages","status"],"title":"CrawlResponse"},"CrawlStatus":{"type":"string","enum":["pending","running","done","stopped","error"],"title":"CrawlStatus"},"CrawlStatusResponse":{"properties":{"crawl_id":{"type":"string","title":"Crawl Id"},"status":{"$ref":"#/components/schemas/CrawlStatus"},"start_url":{"type":"string","title":"Start Url"},"pages_found":{"type":"integer","title":"Pages Found"},"pages_processed":{"type":"integer","title":"Pages Processed"},"pages_error":{"type":"integer","title":"Pages Error"},"total_chunks":{"type":"integer","title":"Total Chunks","default":0},"pages":{"items":{"$ref":"#/components/schemas/CrawlPageInfo"},"type":"array","title":"Pages"},"created_at":{"type":"string","title":"Created At"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finished At"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["crawl_id","status","start_url","pages_found","pages_processed","pages_error","pages","created_at"],"title":"CrawlStatusResponse"},"CrawlStopResponse":{"properties":{"message":{"type":"string","title":"Message"}},"type":"object","required":["message"],"title":"CrawlStopResponse"},"CustomerHistoryItem":{"properties":{"type":{"type":"string","enum":["ticket","chat"],"title":"Type"},"external_id":{"type":"string","title":"External Id"},"subject":{"type":"string","title":"Subject"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"operator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operator"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"}},"type":"object","required":["type","external_id","subject"],"title":"CustomerHistoryItem"},"CustomerHistoryRow":{"properties":{"external_id":{"type":"string","title":"External Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title","description":"Ticket title (tickets only)."},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Ticket status (tickets only)."},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"created_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created Time","description":"Ticket created_time (tickets only)."},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department","description":"Department (chats only)."},"operator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operator","description":"Operator (chats only)."},"start_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Start Time","description":"Chat start_time (chats only)."},"messages_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Messages Count","description":"Chat message count (chats only)."}},"type":"object","required":["external_id"],"title":"CustomerHistoryRow"},"DataTypeInfo":{"properties":{"name":{"type":"string","title":"Name","description":"Programmatic name of the data type"},"label":{"type":"string","title":"Label","description":"Human-readable label","default":""},"description":{"type":"string","title":"Description","description":"Short description for UI","default":""},"supports_scheduling":{"type":"boolean","title":"Supports Scheduling","description":"Whether this data_type can be polled on a recurring schedule. When false, the Schedule API rejects creation of schedules for it.","default":false},"default_interval_hours":{"anyOf":[{"type":"integer","maximum":8760.0,"minimum":1.0},{"type":"null"}],"title":"Default Interval Hours","description":"Recommended polling interval in hours. Used as the default when a client creates a schedule without specifying one. Null when supports_scheduling=false."},"min_interval_hours":{"anyOf":[{"type":"integer","maximum":8760.0,"minimum":1.0},{"type":"null"}],"title":"Min Interval Hours","description":"Hard lower bound on polling interval (rate-limit / API courtesy). The Schedule API rejects intervals below this value. Null = no enforcement."}},"type":"object","required":["name"],"title":"DataTypeInfo","description":"Single data type exposed by a provider (e.g. 'tickets', 'chats').\n\nBeyond identification fields, declares whether this data_type can be\npolled on a recurring schedule via the Pull Ingestion Scheduler. Each\ndata_type may have its own polling cadence — e.g. tickets every 6h,\ndepartments once a week."},"DeleteNamespaceResponse":{"properties":{"namespace":{"type":"string","title":"Namespace"},"deleted":{"type":"boolean","title":"Deleted"},"vectors_deleted":{"type":"integer","title":"Vectors Deleted"},"vs_kb_chunks_deleted":{"type":"integer","title":"Vs Kb Chunks Deleted","default":0},"status":{"type":"string","title":"Status","default":"ok"},"turbopuffer_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Turbopuffer Error"},"postgres_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Postgres Error"}},"type":"object","required":["namespace","deleted","vectors_deleted"],"title":"DeleteNamespaceResponse"},"DepartmentListResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"items":{"items":{"$ref":"#/components/schemas/DepartmentRow"},"type":"array","title":"Items","description":"Departments ordered by total row count DESC."}},"type":"object","required":["limit","offset","has_more","items"],"title":"DepartmentListResponse"},"DepartmentRow":{"properties":{"department":{"type":"string","title":"Department","description":"Department name as stored on tickets/chats/kb."},"tickets":{"type":"integer","title":"Tickets","description":"Number of tickets tagged with this department."},"chats":{"type":"integer","title":"Chats","description":"Number of chats tagged with this department."},"kb_articles":{"type":"integer","title":"Kb Articles","description":"Number of KB articles (typically DynamicAnswers) tagged with this department."}},"type":"object","required":["department","tickets","chats","kb_articles"],"title":"DepartmentRow"},"DistributionResponse":{"properties":{"entity":{"type":"string","enum":["tickets","chats"],"title":"Entity"},"field":{"type":"string","enum":["wait_time","messages_count","sentiment","actions_count"],"title":"Field"},"stats":{"$ref":"#/components/schemas/DistributionStats","description":"Summary statistics for the field."}},"type":"object","required":["entity","field","stats"],"title":"DistributionResponse"},"DistributionStats":{"properties":{"count":{"type":"integer","title":"Count","description":"Non-null sample size for this field."},"min":{"anyOf":[{"type":"number"},{"type":"integer"},{"type":"null"}],"title":"Min","description":"Minimum observed value."},"max":{"anyOf":[{"type":"number"},{"type":"integer"},{"type":"null"}],"title":"Max","description":"Maximum observed value."},"avg":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg","description":"Mean value."},"median":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Median","description":"Median (percentile_cont 0.5)."},"p95":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"P95","description":"95th percentile (percentile_cont 0.95)."}},"type":"object","required":["count"],"title":"DistributionStats"},"DocumentDeleteResponse":{"properties":{"doc_id":{"type":"string","title":"Doc Id"},"deleted_vectors":{"type":"integer","title":"Deleted Vectors"}},"type":"object","required":["doc_id","deleted_vectors"],"title":"DocumentDeleteResponse"},"DocumentListResponse":{"properties":{"documents":{"items":{"$ref":"#/components/schemas/DocumentResponse"},"type":"array","title":"Documents"},"total":{"type":"integer","title":"Total","description":"Total number of documents matching the filter (across all pages)"},"limit":{"type":"integer","title":"Limit","description":"Page size used for this response"},"offset":{"type":"integer","title":"Offset","description":"Offset used for this response"}},"type":"object","required":["documents","total","limit","offset"],"title":"DocumentListResponse"},"DocumentProcessedWebhook":{"properties":{"doc_id":{"type":"string","title":"Doc Id","description":"Document identifier"},"status":{"type":"string","enum":["pending","processing","ready","empty","error"],"title":"Status","description":"Terminal document status: ready / empty / error"},"chunk_count":{"type":"integer","title":"Chunk Count","description":"Number of chunks indexed (0 on error)"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if status is 'error'; otherwise null"},"filename":{"type":"string","title":"Filename","description":"Original filename or generated synthetic name"}},"type":"object","required":["doc_id","status","chunk_count","filename"],"title":"DocumentProcessedWebhook","description":"Sent when a document upload / fetch-url / reprocess job finishes\n(successfully or with an error)."},"DocumentResponse":{"properties":{"doc_id":{"type":"string","title":"Doc Id"},"client_id":{"type":"string","title":"Client Id","default":""},"filename":{"type":"string","title":"Filename"},"file_size":{"type":"integer","title":"File Size"},"doc_type":{"type":"string","title":"Doc Type"},"category":{"type":"string","title":"Category","default":""},"priority":{"type":"integer","title":"Priority","default":5},"status":{"type":"string","enum":["pending","processing","ready","empty","error"],"title":"Status","description":"Current document status. One of: pending, processing, ready, empty, error."},"chunk_count":{"type":"integer","title":"Chunk Count","default":0},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"created_at":{"type":"string","title":"Created At","default":""},"processed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Processed At"},"source":{"type":"string","title":"Source","default":"upload"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url"},"crawl_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Crawl Id"}},"type":"object","required":["doc_id","filename","file_size","doc_type","status"],"title":"DocumentResponse"},"DocumentStats":{"properties":{"documents_total":{"type":"integer","title":"Documents Total","description":"Total documents in the namespace"},"documents_ready":{"type":"integer","title":"Documents Ready","description":"Documents in 'ready' state"},"documents_processing":{"type":"integer","title":"Documents Processing","description":"Documents currently being processed"},"documents_error":{"type":"integer","title":"Documents Error","description":"Documents that failed processing"},"chunks_total":{"type":"integer","title":"Chunks Total","description":"Total chunks across all documents"},"clients_total":{"type":"integer","title":"Clients Total","description":"Number of distinct client_ids in the namespace"},"sources":{"anyOf":[{"additionalProperties":{"type":"integer"},"type":"object"},{"type":"null"}],"title":"Sources","description":"Document count grouped by source (e.g. {'upload': 12, 'crawl': 5, 'ingest': 30})."}},"type":"object","required":["documents_total","documents_ready","documents_processing","documents_error","chunks_total","clients_total"],"title":"DocumentStats","description":"Aggregated document counters for a namespace.\n\nAll counters are always returned (server-side initialised to 0). The\n``sources`` map gives a per-source breakdown of document count."},"DocumentUploadResponse":{"properties":{"doc_id":{"type":"string","title":"Doc Id","description":"Generated document identifier"},"client_id":{"type":"string","title":"Client Id"},"filename":{"type":"string","title":"Filename"},"file_size":{"type":"integer","title":"File Size"},"status":{"type":"string","enum":["pending","processing","ready","empty","error"],"title":"Status","description":"Initial status. Typically 'processing' immediately after upload; watch GET /documents/{doc_id} or the webhook callback for the terminal state."},"doc_type":{"type":"string","title":"Doc Type"}},"type":"object","required":["doc_id","client_id","filename","file_size","status","doc_type"],"title":"DocumentUploadResponse"},"EntailmentRequest":{"properties":{"steps":{"items":{"$ref":"#/components/schemas/StepPayload"},"type":"array","maxItems":50,"minItems":1,"title":"Steps"},"chunks":{"items":{"$ref":"#/components/schemas/ChunkPayload"},"type":"array","maxItems":200,"minItems":1,"title":"Chunks"}},"type":"object","required":["steps","chunks"],"title":"EntailmentRequest"},"EntailmentResponse":{"properties":{"verdicts":{"items":{"$ref":"#/components/schemas/StepVerdictOut"},"type":"array","title":"Verdicts"},"all_supported":{"type":"boolean","title":"All Supported"},"unsupported_count":{"type":"integer","title":"Unsupported Count"},"tokens":{"type":"integer","title":"Tokens"}},"type":"object","required":["verdicts","all_supported","unsupported_count","tokens"],"title":"EntailmentResponse"},"ErrorResponse":{"properties":{"error":{"type":"string","title":"Error","description":"Machine-readable snake_case error code (e.g. 'not_found', 'invalid_request', 'rate_limited')."},"message":{"type":"string","title":"Message","description":"Human-readable error message suitable for logging or end-user display."},"details":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Details","description":"Optional structured context: field-level validation errors, upstream provider response, etc. Shape depends on error type."}},"type":"object","required":["error","message"],"title":"ErrorResponse","description":"Canonical error envelope for every non-2xx response.","examples":[{"error":"not_found","message":"Document not found"},{"details":{"errors":[{"loc":["body","client_id"],"msg":"field required","type":"missing"}]},"error":"validation_error","message":"Request body failed validation"}]},"FaqCitationBatchRequest":{"properties":{"ids":{"items":{"type":"string"},"type":"array","maxItems":500,"minItems":1,"title":"Ids","description":"List of FAQ external_ids to increment by 1 each."},"source":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Source","description":"Free-form attribution. Same semantics as the single endpoint."}},"additionalProperties":false,"type":"object","required":["ids"],"title":"FaqCitationBatchRequest","description":"Body of ``POST /v1/.../faqs/citations:batch`` — chat hot-path.\n\nCortex-hub's chat route calls this once per response with every\nFAQ doc_id it cited, instead of N round-trips. Idempotent within\nthe batch — duplicate ids increment by their multiplicity."},"FaqCitationBatchResponse":{"properties":{"updated":{"type":"integer","title":"Updated","description":"Number of rows actually updated. May be less than len(ids) if some external_ids didn't exist."},"priority_changed":{"items":{"type":"string"},"type":"array","title":"Priority Changed","description":"external_ids whose priority band moved as a result of this batch. The flusher will re-index them in its next pass."}},"additionalProperties":false,"type":"object","required":["updated","priority_changed"],"title":"FaqCitationBatchResponse","description":"Result of a batch citation increment."},"FaqCitationRequest":{"properties":{"count":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Count","description":"Increment to apply. Defaults to 1 (one citation). Capped at 100 to bound a single mistake or replay.","default":1},"source":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Source","description":"Free-form attribution: 'chat', 'qa', 'manual', etc. Recorded in structured logs but not persisted in v1 — useful for grep'ing flusher behavior post-hoc."}},"additionalProperties":false,"type":"object","title":"FaqCitationRequest","description":"Body of ``POST /v1/.../faqs/{id}/citations`` — single FAQ."},"FaqCitationResponse":{"properties":{"external_id":{"type":"string","title":"External Id"},"citation_count":{"type":"integer","title":"Citation Count"},"priority":{"type":"integer","maximum":10.0,"minimum":1.0,"title":"Priority"},"priority_changed":{"type":"boolean","title":"Priority Changed","description":"True if the citation increment moved the priority band. When true, the FAQ was marked dirty for the flusher; when false, the row was updated but Turbopuffer was not touched."}},"additionalProperties":false,"type":"object","required":["external_id","citation_count","priority","priority_changed"],"title":"FaqCitationResponse","description":"Result of incrementing a single FAQ's citation_count."},"FaqDeleteResponse":{"properties":{"deleted":{"type":"boolean","title":"Deleted","description":"True if the row existed and was removed."},"deleted_vectors":{"type":"integer","title":"Deleted Vectors","description":"How many Turbopuffer vectors were removed. Single-chunk FAQs always return 0 or 1; future multi-chunk FAQs may return more."}},"additionalProperties":false,"type":"object","required":["deleted","deleted_vectors"],"title":"FaqDeleteResponse","description":"Result of ``DELETE /v1/.../faqs/{id}``."},"FaqListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/FaqResponse"},"type":"array","title":"Items"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Opaque cursor for the next page; null when no more pages. Pass back as the ``cursor`` query param to continue."}},"additionalProperties":false,"type":"object","required":["items"],"title":"FaqListResponse","description":"Paginated list of FAQs in a namespace + client_id."},"FaqResponse":{"properties":{"external_id":{"type":"string","title":"External Id"},"question":{"type":"string","title":"Question"},"answer":{"type":"string","title":"Answer"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"citation_count":{"type":"integer","title":"Citation Count","description":"Number of times the FAQ has been cited in chat responses. Drives the priority boost via the shared FAQ priority formula."},"view_count":{"type":"integer","title":"View Count","description":"Reserved — populated by future signal sources (e.g. public KB UI clicks). Always 0 for editorial FAQs in v1."},"vote_count":{"type":"integer","title":"Vote Count"},"rating":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rating"},"priority":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":1.0},{"type":"null"}],"title":"Priority","description":"Cached priority band (1..10) computed from the frequency signals. NULL until the first index pass populates it."},"vector_indexed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Vector Indexed At","description":"When this FAQ was last successfully upserted to Turbopuffer. NULL means the row exists in PG but has not yet been indexed."},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"additionalProperties":false,"type":"object","required":["external_id","question","answer","department","tags","citation_count","view_count","vote_count","rating","created_at","updated_at"],"title":"FaqResponse","description":"Canonical FAQ row shape returned by GET / PUT / list endpoints."},"FaqStatsResponse":{"properties":{"total":{"type":"integer","title":"Total"},"indexed":{"type":"integer","title":"Indexed","description":"Rows where vector_indexed_at IS NOT NULL."},"dirty":{"type":"integer","title":"Dirty","description":"Rows where vector_dirty=true — pending re-index by the FaqDirtyFlusher background task."},"top_cited":{"items":{"$ref":"#/components/schemas/FaqTopCitedItem"},"type":"array","title":"Top Cited","description":"Top 10 by citation_count, descending. Useful for spot-checks."}},"additionalProperties":false,"type":"object","required":["total","indexed","dirty"],"title":"FaqStatsResponse","description":"Lightweight admin telemetry — ``GET /v1/.../faqs/stats``."},"FaqTopCitedItem":{"properties":{"external_id":{"type":"string","title":"External Id"},"citation_count":{"type":"integer","title":"Citation Count"},"priority":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Priority"}},"additionalProperties":false,"type":"object","required":["external_id","citation_count","priority"],"title":"FaqTopCitedItem"},"FaqUpsertRequest":{"properties":{"question":{"type":"string","maxLength":2000,"minLength":1,"title":"Question","description":"The user-facing question text."},"answer":{"type":"string","maxLength":10000,"minLength":1,"title":"Answer","description":"The canonical answer text."},"department":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Department","description":"Optional department scoping. When set, retrieval can filter to FAQs from a single department (e.g. 'Sales')."},"tags":{"items":{"type":"string"},"type":"array","title":"Tags","description":"Free-form tags surfaced as Turbopuffer chunk metadata."}},"additionalProperties":false,"type":"object","required":["question","answer"],"title":"FaqUpsertRequest","description":"Body of ``PUT /v1/client/{cid}/ns/{ns}/faqs/{id}``.\n\nThe path's ``{id}`` is the cortex-hub UUID — caller-supplied so VS\ncan stay idempotent on the same external_id across retries."},"FeedbackBody":{"properties":{"vote":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vote","description":"'up' or 'down'"},"usage":{"type":"boolean","title":"Usage","default":false}},"type":"object","title":"FeedbackBody"},"FetchUrlRequest":{"properties":{"url":{"type":"string","maxLength":2048,"minLength":10,"title":"Url"},"doc_type":{"type":"string","title":"Doc Type","default":"webpage"},"category":{"type":"string","title":"Category","default":""},"priority":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":1.0},{"type":"null"}],"title":"Priority","description":"Document priority 1-10. When omitted, the tier default for this doc_type is used (see Admin Panel → Source Priority)."},"analyze_images":{"type":"boolean","title":"Analyze Images","description":"Extract and analyze images from the page via Vision LLM","default":false}},"type":"object","required":["url"],"title":"FetchUrlRequest"},"FetchUrlResponse":{"properties":{"doc_id":{"type":"string","title":"Doc Id"},"source_url":{"type":"string","title":"Source Url"},"status":{"type":"string","title":"Status"}},"type":"object","required":["doc_id","source_url","status"],"title":"FetchUrlResponse"},"FieldDistribution":{"properties":{"field":{"type":"string","title":"Field","description":"The metadata field name"},"groups":{"items":{"$ref":"#/components/schemas/GroupEntry"},"type":"array","title":"Groups","description":"Value → count pairs, sorted by count descending"},"total":{"type":"integer","title":"Total","description":"Total vectors that have this field set"}},"type":"object","required":["field","groups","total"],"title":"FieldDistribution"},"GenerateSopsBody":{"properties":{"cluster_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Cluster Ids","description":"Explicit cluster external_ids to generate for. When set, cluster_size_min is ignored."},"cluster_size_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cluster Size Min","description":"Minimum cluster_size; defaults to 3 (see SOP-GENERATOR.md)."},"limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit","description":"Upper bound on clusters touched by this job."},"min_dialogue_chars":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Min Dialogue Chars","description":"Skip clusters whose representative_id carries less than this many chars of customer-visible text. Prevents 'Generate all' from burning LLM tokens on clusters that would all be ``sops_skipped_empty``. Ignored when ``cluster_ids`` is set (explicit choice wins)."}},"type":"object","title":"GenerateSopsBody"},"GenerateSopsResponse":{"properties":{"job_id":{"type":"string","title":"Job Id"},"status":{"type":"string","title":"Status","default":"pending"},"phase":{"type":"string","title":"Phase","default":"sop-generating"}},"type":"object","required":["job_id"],"title":"GenerateSopsResponse"},"GroupEntry":{"properties":{"value":{"title":"Value","description":"The metadata field value"},"count":{"type":"integer","title":"Count","description":"Number of vectors with this value"}},"type":"object","required":["value","count"],"title":"GroupEntry"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","title":"Status"},"backend":{"type":"string","title":"Backend","default":"turbopuffer"},"uptime_seconds":{"type":"number","title":"Uptime Seconds","default":0}},"type":"object","required":["status"],"title":"HealthResponse"},"IngestDoneWebhook":{"properties":{"event":{"type":"string","const":"ingest.done","title":"Event","default":"ingest.done"},"ingest_id":{"type":"string","title":"Ingest Id"},"provider":{"type":"string","title":"Provider"},"namespace":{"type":"string","title":"Namespace"},"status":{"type":"string","enum":["pending","running","completed","failed","cancelled"],"title":"Status"},"stats":{"$ref":"#/components/schemas/IngestProgress","description":"Final counters (raw_fetched, documents_aggregated, documents_indexed, chunks_created)"},"timestamp":{"type":"string","title":"Timestamp"}},"type":"object","required":["ingest_id","provider","namespace","status","stats","timestamp"],"title":"IngestDoneWebhook","description":"Sent once when an ingest job completes successfully."},"IngestErrorWebhook":{"properties":{"event":{"type":"string","const":"ingest.error","title":"Event","default":"ingest.error"},"ingest_id":{"type":"string","title":"Ingest Id"},"provider":{"type":"string","title":"Provider"},"namespace":{"type":"string","title":"Namespace"},"status":{"type":"string","const":"failed","title":"Status"},"error":{"additionalProperties":true,"type":"object","title":"Error","description":"Error envelope: {code: snake_case, message: human readable}"},"timestamp":{"type":"string","title":"Timestamp"}},"type":"object","required":["ingest_id","provider","namespace","status","error","timestamp"],"title":"IngestErrorWebhook","description":"Sent if an ingest job fails."},"IngestHistoryResponse":{"properties":{"syncs":{"items":{"$ref":"#/components/schemas/IngestStatusResponse"},"type":"array","title":"Syncs","description":"Most recent ingest jobs for the namespace, newest first."}},"type":"object","required":["syncs"],"title":"IngestHistoryResponse"},"IngestProgress":{"properties":{"items_fetched":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Items Fetched","description":"Number of raw items fetched from the provider so far"},"items_total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Items Total","description":"Total raw items the provider expects to return, if known"},"raw_fetched":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Raw Fetched","description":"Total raw records fetched (final count after fetching phase)"},"documents_aggregated":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Documents Aggregated","description":"Number of aggregated documents produced from raw records"},"documents_indexed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Documents Indexed","description":"Number of documents successfully indexed into the vector store"},"chunks_created":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Chunks Created","description":"Total chunks created across all indexed documents"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Free-form transient status hint (e.g. 'starting')"}},"additionalProperties":true,"type":"object","title":"IngestProgress","description":"Progress counters reported by an ingest job.\n\nAll fields are optional and may appear depending on the current\nphase. The shape is stable: consumers can read any subset."},"IngestProgressWebhook":{"properties":{"event":{"type":"string","const":"ingest.progress","title":"Event","default":"ingest.progress"},"ingest_id":{"type":"string","title":"Ingest Id"},"provider":{"type":"string","title":"Provider"},"namespace":{"type":"string","title":"Namespace"},"phase":{"type":"string","enum":["fetching","aggregating","indexing","finalizing"],"title":"Phase"},"progress":{"$ref":"#/components/schemas/IngestProgress"},"timestamp":{"type":"string","title":"Timestamp","description":"ISO-8601 UTC timestamp"}},"type":"object","required":["ingest_id","provider","namespace","phase","progress","timestamp"],"title":"IngestProgressWebhook","description":"Sent at each phase boundary while an ingest job is running."},"IngestRequest":{"properties":{"provider":{"type":"string","maxLength":64,"minLength":1,"title":"Provider"},"credentials":{"additionalProperties":true,"type":"object","title":"Credentials","description":"Provider-specific credentials. Shape varies per provider — see GET /providers/{provider}/credential_fields. Optional override; if omitted, credentials configured via Admin Panel for this namespace are used."},"config":{"additionalProperties":true,"type":"object","title":"Config","description":"Provider-specific configuration (e.g. data_types, date ranges, concurrency). Shape varies per provider."},"pipeline":{"type":"string","maxLength":64,"title":"Pipeline","description":"Named pipeline config (enrichment/vector strategy)","default":""},"webhook_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Url","description":"Override webhook URL"},"phases":{"anyOf":[{"items":{"type":"string","enum":["fetch","aggregate","index"]},"type":"array"},{"type":"null"}],"title":"Phases","description":"Which pipeline phases to run. Default (omit or null) = all three: ['fetch','aggregate','index']. Use partial subsets for advanced scenarios:\n\n- `['fetch']` — refresh data.sqlite from the source API only, do not re-aggregate or re-index.\n- `['aggregate','index']` — re-process existing data.sqlite without dialing the source API. Useful when API is down or when clustering parameters changed.\n- `['index']` — re-index already-aggregated documents from data.sqlite. Useful after changing embedding model or pipeline config without re-running expensive fetch/aggregate phases.\n\nThe index phase is **resumable** — on crash mid-run, the next execution picks up the first row whose `indexed_at` is still NULL. Combine with `force_full=true` to re-index everything from scratch (clears all `indexed_at` flags before indexing)."},"force_full":{"type":"boolean","title":"Force Full","description":"If true, the aggregate phase clears all `indexed_at` flags in the persistent aggregated_documents store before writing, forcing the index phase to re-process every document instead of only the unindexed ones. Use after changing embedding model, pipeline config, or chunking strategy.","default":false},"schedule":{"anyOf":[{"items":{"$ref":"#/components/schemas/ScheduleConfig"},"type":"array"},{"type":"null"}],"title":"Schedule","description":"Optional. After running the initial ingest, also create recurring schedules for the listed data_types. Each entry validates against the provider's scheduling rules (supports_scheduling, min_interval_hours). Omit to run a one-shot ingest with no recurring follow-ups."}},"type":"object","required":["provider"],"title":"IngestRequest","examples":[{"config":{"data_types":["tickets","chats","kb","departments"]},"credentials":{"client_id":"246D36D5-...","client_secret":"73285AB8..."},"provider":"livehelpnow"}]},"IngestResponse":{"properties":{"ingest_id":{"type":"string","title":"Ingest Id","description":"Canonical ingest job identifier"},"provider":{"type":"string","title":"Provider"},"status":{"type":"string","enum":["pending","running","completed","failed","cancelled"],"title":"Status","description":"Initial job status, typically 'pending' immediately after creation"},"data_available":{"additionalProperties":{"anyOf":[{"type":"integer"},{"type":"string"}]},"type":"object","title":"Data Available","description":"Per-data-type availability returned by the provider's credential validation step. Keys are provider-specific data type names (e.g. 'tickets', 'kb_articles', 'departments'). Values are typically item counts (int); providers that cannot count exactly may return 'available' as a string sentinel."}},"type":"object","required":["ingest_id","provider","status"],"title":"IngestResponse"},"IngestStatusResponse":{"properties":{"ingest_id":{"type":"string","title":"Ingest Id","description":"Canonical ingest job identifier"},"provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider","description":"Data provider name (e.g. 'livehelpnow')"},"status":{"type":"string","enum":["pending","running","completed","failed","cancelled"],"title":"Status","description":"Job status. One of: pending, running, completed, failed, cancelled."},"phase":{"anyOf":[{"type":"string","enum":["fetching","aggregating","indexing","finalizing"]},{"type":"null"}],"title":"Phase","description":"Current pipeline phase. One of: fetching, aggregating, indexing, finalizing. Null when the job has not started or is finished."},"progress":{"$ref":"#/components/schemas/IngestProgress","description":"Progress counters (see IngestProgress)."},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if status is 'failed'"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At","description":"ISO-8601 UTC timestamp when the job entered 'running'"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finished At","description":"ISO-8601 UTC timestamp when the job reached a terminal status"}},"type":"object","required":["ingest_id","status"],"title":"IngestStatusResponse","description":"Current state of a single ingest job. Same shape is used by\n/status and (per-item) by /history."},"KBArticleDetail":{"properties":{"external_id":{"type":"string","title":"External Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"question":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Question"},"answer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer","description":"Full answer body with the original HTML preserved."},"categories":{"items":{"type":"string"},"type":"array","title":"Categories"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"ai_answer":{"type":"boolean","title":"Ai Answer","description":"Source marker. `true` on `/kb/{id}` (DynamicAnswer), `false` on `/faq/{id}` (human-authored FAQ). Always matches the endpoint that served the row — a mismatch produces HTTP 404."},"rating":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rating"},"view_count":{"type":"integer","title":"View Count","default":0},"vote_count":{"type":"integer","title":"Vote Count","default":0},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"fetched_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fetched At","description":"Timestamp of the last ingest that refreshed this row."}},"type":"object","required":["external_id","ai_answer"],"title":"KBArticleDetail"},"KBItem":{"properties":{"external_id":{"type":"string","title":"External Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"question":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Question"},"categories":{"items":{"type":"string"},"type":"array","title":"Categories"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"ai_answer":{"type":"boolean","title":"Ai Answer","description":"Source marker. Always `true` under `/kb` (DynamicAnswer) and always `false` under `/faq` (human-authored FAQ)."},"rating":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rating"},"view_count":{"type":"integer","title":"View Count","default":0},"vote_count":{"type":"integer","title":"Vote Count","default":0},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"}},"type":"object","required":["external_id","ai_answer"],"title":"KBItem"},"KBListResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"items":{"items":{"$ref":"#/components/schemas/KBItem"},"type":"array","title":"Items","description":"KB articles ordered by view_count DESC, rating DESC."}},"type":"object","required":["limit","offset","has_more","items"],"title":"KBListResponse"},"KBSearchItem":{"properties":{"external_id":{"type":"string","title":"External Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"question":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Question"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"rank":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rank","description":"PostgreSQL ts_rank relevance score."}},"type":"object","required":["external_id"],"title":"KBSearchItem"},"KBSearchResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"query":{"type":"string","title":"Query"},"items":{"items":{"$ref":"#/components/schemas/KBSearchItem"},"type":"array","title":"Items"}},"type":"object","required":["limit","offset","has_more","query","items"],"title":"KBSearchResponse"},"MCPCallRequest":{"properties":{"tool":{"type":"string","title":"Tool","description":"MCP tool name (see /tools)"},"params":{"additionalProperties":true,"type":"object","title":"Params","description":"Tool-specific parameters as defined by MCPToolDefinitionModel.parameters"}},"type":"object","required":["tool"],"title":"MCPCallRequest"},"MCPCallResponse":{"properties":{"tool":{"type":"string","title":"Tool"},"result":{"anyOf":[{"additionalProperties":true,"type":"object"},{"items":{},"type":"array"}],"title":"Result","description":"Tool result. Shape is tool-specific — see the tool definition's 'returns' field on the provider documentation."}},"type":"object","required":["tool","result"],"title":"MCPCallResponse"},"MCPToolDefinitionModel":{"properties":{"name":{"type":"string","title":"Name","description":"Tool name (passed as 'tool' in /call)"},"description":{"type":"string","title":"Description","description":"Human-readable tool description","default":""},"parameters":{"additionalProperties":true,"type":"object","title":"Parameters","description":"JSON Schema describing accepted parameters"}},"type":"object","required":["name"],"title":"MCPToolDefinitionModel","description":"OpenAPI-friendly view of an MCP tool definition."},"MCPToolsResponse":{"properties":{"provider":{"type":"string","title":"Provider"},"tools":{"items":{"$ref":"#/components/schemas/MCPToolDefinitionModel"},"type":"array","title":"Tools"}},"type":"object","required":["provider","tools"],"title":"MCPToolsResponse"},"MessageResponse":{"properties":{"message":{"type":"string","title":"Message"}},"type":"object","required":["message"],"title":"MessageResponse"},"OperatorStatsResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"items":{"items":{"$ref":"#/components/schemas/OperatorStatsRow"},"type":"array","title":"Items","description":"Operators ordered by tickets DESC."}},"type":"object","required":["limit","offset","has_more","items"],"title":"OperatorStatsResponse"},"OperatorStatsRow":{"properties":{"key":{"type":"string","title":"Key","description":"Operator display name (as stored on tickets)."},"tickets":{"type":"integer","title":"Tickets","description":"Total tickets assigned to this operator."},"closed":{"type":"integer","title":"Closed","description":"Closed tickets for this operator."},"open":{"type":"integer","title":"Open","description":"Open tickets for this operator."}},"type":"object","required":["key","tickets","closed","open"],"title":"OperatorStatsRow"},"OverviewResponse":{"properties":{"counts":{"$ref":"#/components/schemas/CountsByType","description":"Row counts per entity type."},"with_conversation":{"$ref":"#/components/schemas/WithConversationCounts","description":"Subset of `counts` restricted to rows that carry real dialogue. UI and LLMs should prefer these numbers over the raw totals when the goal is content retrieval."}},"type":"object","required":["counts","with_conversation"],"title":"OverviewResponse"},"PipelineConfigDeleteResponse":{"properties":{"deleted":{"type":"boolean","title":"Deleted"}},"type":"object","required":["deleted"],"title":"PipelineConfigDeleteResponse"},"PipelineConfigListResponse":{"properties":{"configs":{"items":{"$ref":"#/components/schemas/PipelineConfigSummary"},"type":"array","title":"Configs"}},"type":"object","required":["configs"],"title":"PipelineConfigListResponse"},"PipelineConfigRequest":{"properties":{"enrichment_enabled":{"type":"boolean","title":"Enrichment Enabled","default":true},"enrichment_prompt":{"type":"string","maxLength":10000,"title":"Enrichment Prompt","description":"Custom LLM prompt template for enrichment","default":""},"enrichment_output_schema":{"additionalProperties":true,"type":"object","title":"Enrichment Output Schema","description":"Opaque user-supplied JSON Schema that constrains the LLM enrichment output. Vector Storage does not interpret this — it is forwarded as-is to the enrichment provider for structured-output enforcement."},"enrichment_max_text":{"type":"integer","maximum":50000.0,"minimum":100.0,"title":"Enrichment Max Text","description":"Max chars of text passed to the LLM per chunk","default":3000},"vector_strategy":{"type":"string","pattern":"^(single|multi)$","title":"Vector Strategy","description":"'single' = one vector per chunk; 'multi' = one vector per template","default":"single"},"vector_templates":{"items":{"$ref":"#/components/schemas/VectorTemplateSchema"},"type":"array","title":"Vector Templates"},"chunk_size_tokens":{"type":"integer","maximum":4000.0,"minimum":50.0,"title":"Chunk Size Tokens","default":400},"chunk_overlap_tokens":{"type":"integer","maximum":500.0,"minimum":0.0,"title":"Chunk Overlap Tokens","default":50}},"type":"object","title":"PipelineConfigRequest","examples":[{"chunk_overlap_tokens":50,"chunk_size_tokens":400,"enrichment_enabled":true,"enrichment_output_schema":{"properties":{"title":{"type":"string"},"summary":{"type":"string"},"tags":{"items":{"type":"string"},"type":"array"}},"required":["title","summary"],"type":"object"},"enrichment_prompt":"Extract: title, summary, tags","vector_strategy":"single"}]},"PipelineConfigResponse":{"properties":{"client_id":{"type":"string","title":"Client Id"},"config_name":{"type":"string","title":"Config Name"},"enrichment_enabled":{"type":"boolean","title":"Enrichment Enabled"},"enrichment_prompt":{"type":"string","title":"Enrichment Prompt"},"enrichment_output_schema":{"additionalProperties":true,"type":"object","title":"Enrichment Output Schema","description":"Opaque user-supplied JSON Schema for LLM output validation"},"enrichment_max_text":{"type":"integer","title":"Enrichment Max Text"},"vector_strategy":{"type":"string","title":"Vector Strategy"},"vector_templates":{"items":{"$ref":"#/components/schemas/VectorTemplateSchema"},"type":"array","title":"Vector Templates"},"chunk_size_tokens":{"type":"integer","title":"Chunk Size Tokens"},"chunk_overlap_tokens":{"type":"integer","title":"Chunk Overlap Tokens"},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"}},"type":"object","required":["client_id","config_name","enrichment_enabled","enrichment_prompt","enrichment_output_schema","enrichment_max_text","vector_strategy","vector_templates","chunk_size_tokens","chunk_overlap_tokens","created_at","updated_at"],"title":"PipelineConfigResponse"},"PipelineConfigSummary":{"properties":{"client_id":{"type":"string","title":"Client Id"},"config_name":{"type":"string","title":"Config Name"},"enrichment_enabled":{"type":"boolean","title":"Enrichment Enabled"},"vector_strategy":{"type":"string","title":"Vector Strategy"},"vector_templates_count":{"type":"integer","title":"Vector Templates Count"},"updated_at":{"type":"string","title":"Updated At"}},"type":"object","required":["client_id","config_name","enrichment_enabled","vector_strategy","vector_templates_count","updated_at"],"title":"PipelineConfigSummary"},"ProviderInfo":{"properties":{"name":{"type":"string","title":"Name"},"has_mcp":{"type":"boolean","title":"Has Mcp","description":"Whether the provider exposes MCP tools"},"data_types":{"items":{"$ref":"#/components/schemas/DataTypeInfo"},"type":"array","title":"Data Types"}},"type":"object","required":["name","has_mcp"],"title":"ProviderInfo"},"ProvidersListResponse":{"properties":{"providers":{"items":{"$ref":"#/components/schemas/ProviderInfo"},"type":"array","title":"Providers"}},"type":"object","required":["providers"],"title":"ProvidersListResponse"},"PurgeResponse":{"properties":{"namespace":{"type":"string","title":"Namespace","description":"Resolved Turbopuffer namespace actually targeted (includes VS_TURBOPUFFER_PREFIX when set)."},"turbopuffer_deleted":{"type":"integer","title":"Turbopuffer Deleted","description":"Number of vectors deleted via attribute filter. -1 for the full-namespace path where no exact count is available; check `vs_kb_chunks_deleted` for verification."},"vs_kb_chunks_deleted":{"type":"integer","title":"Vs Kb Chunks Deleted","description":"Postgres `vs_kb_chunks` rows deleted for this purge."},"vs_faqs_deleted":{"type":"integer","title":"Vs Faqs Deleted","description":"Postgres `vs_faqs` rows deleted for this purge. Editorial FAQs owned by VS — wiped alongside vs_kb_chunks so a tenant offboarding doesn't leave stale Q+A behind. Returns 0 on legacy deployments where the FAQ store is not wired.","default":0},"status":{"type":"string","title":"Status","description":"`ok` when both sides succeeded, `partial` when one side errored (the other still ran), `failed` when both errored."},"turbopuffer_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Turbopuffer Error"},"postgres_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Postgres Error"}},"type":"object","required":["namespace","turbopuffer_deleted","vs_kb_chunks_deleted","status"],"title":"PurgeResponse"},"QueryRequest":{"properties":{"text":{"type":"string","maxLength":5000,"minLength":1,"title":"Text"},"top_k":{"type":"integer","maximum":1000.0,"minimum":1.0,"title":"Top K","description":"Maximum number of chunks to return. Default **5** is minimal (one-shot question answering); **10–20** is a better fit for typical RAG over long documents, where adjacent chunks often share context. Hard cap **1000** — larger values produce responses that exceed practical LLM context budgets and frontend payload limits.","default":5},"filters":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Filters","description":"Metadata filters applied **pre-ANN** by Turbopuffer (attribute-indexed, so adding a filter speeds up the query rather than slowing it down). Filters match against any metadata field stored alongside the vector.\n\n**Standard fields:** doc_id, client_id, doc_type, category, department, priority, heading, chunk_index, content_type, source, source_url, crawl_id, has_phone, has_price, has_datetime, has_address, has_mcp, has_sop_summary, ai_answer, external_id, representative_id, provider.\n\n**LLM-enriched fields** (when LLM enrichment is enabled): llm_category, llm_language, llm_entities, llm_topics, llm_content_kind.\n\n### Leaf operators (single field)\n| VS syntax | Meaning |\n|---|---|\n| `{\"doc_type\": \"faq\"}` | exact match |\n| `{\"doc_type\": [\"faq\",\"price_list\"]}` | IN (OR over values of one field) |\n| `{\"priority_gte\": 5}` | ≥ |\n| `{\"priority_lte\": 8}` | ≤ |\n| `{\"channel_ne\": \"\"}` | ≠ (per-field negation) |\n| `{\"llm_topics_contains\": \"pricing\"}` | array element **equals** `\"pricing\"` (not substring) |\n| `{\"has_price\": true}` | boolean (0/1 under the hood) |\n\nNote: `_contains` is element-equality on array attributes (exact match of one element), **not** substring search.\n\n### Boolean combinators (nestable)\n| Key | Meaning |\n|---|---|\n| `{\"_and\": [subfilter, …]}` | AND of arbitrary children |\n| `{\"_or\":  [subfilter, …]}` | OR of arbitrary children |\n| `{\"_not\": subfilter}` | negate a subtree |\n\nMultiple top-level keys without a combinator are treated as implicit AND (backwards-compatible). Combinator keys (`_and`/`_or`/`_not`) must appear **alone at their level** — wrap siblings in an explicit `_and` list.\n\n### Examples\n**OR between different fields** — «Billing-category OR Sales-department»:\n```json\n{\"_or\": [{\"category\": \"Billing\"},\n         {\"department\": \"Sales\"}]}\n```\n**Exclude one doc_type** — «anything except KB»:\n```json\n{\"_not\": {\"doc_type\": \"kb_article\"}}\n```\n**Combined tree** — «(KB OR resolved ticket) AND has phone»:\n```json\n{\"_and\": [\n  {\"_or\": [{\"doc_type\": \"kb_article\"},\n           {\"_and\": [{\"doc_type\": \"ticket_cluster\"},\n                     {\"has_sop_summary\": true}]}]},\n  {\"has_phone\": true}\n]}\n```\n\n### Knowledge-base / FAQ filtering\nFAQ / knowledge-base articles are stored with `doc_type=\"kb_article\"`. The `ai_answer` boolean separates human-authored KB entries (`false`) from DynamicAnswers generated by LiveHelpNow AI (`true`).\n\n```json\n// All KB content (human + DynamicAnswers)\n{\"doc_type\": \"kb_article\"}\n```\n```json\n// Only human-authored FAQ\n{\"_and\": [{\"doc_type\": \"kb_article\"}, {\"ai_answer\": false}]}\n```\n```json\n// Only DynamicAnswers (AI-generated)\n{\"_and\": [{\"doc_type\": \"kb_article\"}, {\"ai_answer\": true}]}\n```\n\n### Common mistake — combinator sibling to a leaf\n```json\n// ❌ INVALID — _or and doc_type at the same level:\n{\"_or\": [{\"category\": \"A\"}, {\"category\": \"B\"}],\n \"doc_type\": \"kb_article\"}\n```\n→ responds **HTTP 422** with `detail: \"filter combinator '_or' must appear alone at its level\"`. Fix by wrapping both sides in an explicit `_and`:\n```json\n// ✅ valid\n{\"_and\": [\n  {\"_or\": [{\"category\": \"A\"}, {\"category\": \"B\"}]},\n  {\"doc_type\": \"kb_article\"}\n]}\n```\n\nBehind the scenes each combinator maps to Turbopuffer's native `(\"And\"|\"Or\"|\"Not\", …)` filter tree."},"score_threshold":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"title":"Score Threshold","description":"Minimum similarity score (0.0-1.0). Results below this threshold are excluded."},"search_mode":{"anyOf":[{"type":"string","enum":["hybrid","dense","sparse"]},{"type":"null"}],"title":"Search Mode","description":"Search mode:\n- `hybrid` (default) — Turbopuffer ANN semantic + BM25 keyword search combined via RRF fusion\n- `dense` — ANN only (semantic similarity)\n- `sparse` — BM25 only (keyword matching)"},"dense_weight":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"title":"Dense Weight","description":"Weight of dense (ANN) results in hybrid fusion (0.0-1.0). BM25 weight = 1 - dense_weight. Default: 0.6."},"rerank":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Rerank","description":"Enable cross-encoder re-ranking on the results. When `true`, the initial retrieval results are re-scored by a cross-encoder model (Bedrock Amazon Rerank / Cohere Rerank, or Voyage rerank-2) for higher precision.\n\n- `null` (default) — uses server-wide `VS_RERANK_ENABLED` setting\n- `true` — force re-ranking for this query (requires active rerank provider)\n- `false` — skip re-ranking even if enabled globally\n\nRe-ranking adds ~50-200ms latency but significantly improves result relevance, especially for ambiguous or multi-topic queries."}},"type":"object","required":["text"],"title":"QueryRequest","examples":[{"dense_weight":0.6,"filters":{"has_price":true,"llm_category":"price_list","llm_topics_contains":"pricing"},"rerank":false,"score_threshold":0.5,"search_mode":"hybrid","text":"subscription pricing plans","top_k":10}]},"QueryResponse":{"properties":{"results":{"items":{"$ref":"#/components/schemas/QueryResultItem"},"type":"array","title":"Results","description":"Ranked list of matching chunks"},"search_time_ms":{"type":"number","title":"Search Time Ms","description":"Search time in milliseconds (excluding embedding)"},"embedding_time_ms":{"type":"number","title":"Embedding Time Ms","description":"Query embedding time in milliseconds"},"total_vectors":{"type":"integer","title":"Total Vectors","description":"Number of results returned by this query — equal to `len(results)`, after filter application and `top_k` truncation. **Not** the size of the namespace (use `GET /stats` for that)."}},"type":"object","required":["results","search_time_ms","embedding_time_ms","total_vectors"],"title":"QueryResponse"},"QueryResultItem":{"properties":{"chunk_id":{"type":"string","title":"Chunk Id","description":"Vector / chunk identifier. For documents indexed via the document pipeline this has the format `{doc_id}_chunk_{index}`. For chunks pushed via `POST /chunks/upsert` this is the caller-supplied chunk_id verbatim."},"score":{"type":"number","title":"Score","description":"Final relevance score. When re-ranking is applied, this is the **cross-encoder score** (see `original_score` for the pre-rerank value). Otherwise: in hybrid mode this is the RRF fusion score after Source Priority boost; in dense mode — cosine similarity; in sparse mode — BM25 score."},"original_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Original Score","description":"Pre-rerank score, only set when cross-encoder re-ranking was applied. This is the retrieval-stage score (RRF / cosine / BM25) before the cross-encoder re-scored the result. Null when re-ranking is disabled."},"rerank_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rerank Score","description":"Cross-encoder relevance score, only set when re-ranking was applied. Range depends on the rerank model (typically 0.0-1.0). Null when re-ranking is disabled."},"base_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Base Score","description":"Pre-boost RRF score, only set in hybrid mode when a non-trivial priority boost was applied to this result."},"priority_boost_applied":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Priority Boost Applied","description":"Multiplier applied to the RRF score by the Source Priority Engine. Values >1.0 mean the chunk was boosted; <1.0 means penalised. Null when no boost was applied."},"chunk_text":{"type":"string","title":"Chunk Text","description":"Original chunk text content. Empty string only for image-only chunks (where `metadata.content_type == \"image\"`) — a signed `metadata.image_url` is returned alongside in that case.","default":""},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata","description":"Chunk metadata. Standard fields: doc_id, client_id, doc_type, category, department, priority, heading, chunk_index, content_type, source, source_url, crawl_id, has_phone, has_price, has_datetime, has_address, has_mcp, has_sop_summary, ai_answer, external_id, representative_id, provider. LLM-enriched fields (when enrichment is enabled): llm_category, llm_language, llm_entities, llm_topics, llm_content_kind. The full filterable list is the same — see `QueryRequest.filters` for details."}},"type":"object","required":["chunk_id","score"],"title":"QueryResultItem"},"RebuildResponse":{"properties":{"namespace":{"type":"string","title":"Namespace"},"status":{"type":"string","title":"Status"}},"type":"object","required":["namespace","status"],"title":"RebuildResponse"},"ScheduleConfig":{"properties":{"data_type":{"type":"string","title":"Data Type","description":"Provider data_type to schedule (must have supports_scheduling=true)."},"interval_hours":{"type":"integer","maximum":8760.0,"minimum":1.0,"title":"Interval Hours","description":"Polling interval in hours. Must respect the data_type's min_interval_hours when set."},"enabled":{"type":"boolean","title":"Enabled","description":"Set to false to create the schedule paused.","default":true}},"type":"object","required":["data_type","interval_hours"],"title":"ScheduleConfig","description":"Single recurring update entry for one provider data_type.\n\nUsed both inside ``IngestRequest.schedule`` (when creating schedules\nalongside the initial ingest) and inside ``UpsertSchedulesRequest``\n(standalone schedule management API)."},"ScheduleListResponse":{"properties":{"schedules":{"items":{"$ref":"#/components/schemas/ScheduleResponse"},"type":"array","title":"Schedules"}},"type":"object","required":["schedules"],"title":"ScheduleListResponse"},"ScheduleResponse":{"properties":{"schedule_id":{"type":"string","title":"Schedule Id","description":"Internal identifier"},"namespace":{"type":"string","title":"Namespace"},"client_id":{"type":"string","title":"Client Id"},"provider":{"type":"string","title":"Provider"},"data_type":{"type":"string","title":"Data Type"},"interval_hours":{"type":"integer","title":"Interval Hours"},"enabled":{"type":"boolean","title":"Enabled"},"last_run_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Run At","description":"ISO-8601 UTC timestamp of the last run start, or null if the schedule has never fired."},"last_job_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Job Id","description":"ingest_id of the last (or current) ingest job triggered by this schedule."},"last_status":{"anyOf":[{"type":"string","enum":["pending","running","completed","failed","cancelled"]},{"type":"null"}],"title":"Last Status","description":"Status of the last run. One of: pending, running, completed, failed, cancelled."},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error","description":"Error message from the last failed run, if any."},"next_run_at":{"type":"string","title":"Next Run At","description":"ISO-8601 UTC timestamp of the next planned run."},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"}},"type":"object","required":["schedule_id","namespace","client_id","provider","data_type","interval_hours","enabled","next_run_at","created_at","updated_at"],"title":"ScheduleResponse","description":"Single schedule with its full state."},"SchemaColumn":{"properties":{"name":{"type":"string","title":"Name","description":"Column name."},"type":{"type":"string","title":"Type","description":"PostgreSQL data type (information_schema.columns.data_type)."}},"type":"object","required":["name","type"],"title":"SchemaColumn"},"SchemaResponse":{"properties":{"tables":{"additionalProperties":{"$ref":"#/components/schemas/SchemaTable"},"type":"object","title":"Tables","description":"Per-table schema reflection keyed by physical table name (e.g. `vs_lhn_tickets`)."}},"type":"object","required":["tables"],"title":"SchemaResponse"},"SchemaTable":{"properties":{"columns":{"items":{"$ref":"#/components/schemas/SchemaColumn"},"type":"array","title":"Columns","description":"Columns in ordinal position order."},"row_count":{"type":"integer","title":"Row Count","description":"Row count for this (namespace, client_id)."},"samples":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Samples","description":"Up to `sample_rows` sample rows for this tenant."},"jsonb_keys":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object","title":"Jsonb Keys","description":"For each jsonb column, the union of top-level keys observed in samples."}},"type":"object","required":["columns","row_count"],"title":"SchemaTable"},"SimilarResolvedItem":{"properties":{"external_id":{"type":"string","title":"External Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"rank":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rank","description":"PostgreSQL ts_rank relevance score."}},"type":"object","required":["external_id"],"title":"SimilarResolvedItem"},"SimilarResolvedRequest":{"properties":{"query":{"type":"string","minLength":1,"title":"Query","description":"Problem description or keywords to match."},"limit":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Limit","description":"Max results per page.","default":10},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset for pagination.","default":0}},"type":"object","required":["query"],"title":"SimilarResolvedRequest"},"SimilarResolvedResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"query":{"type":"string","title":"Query","description":"Echoes the submitted query."},"items":{"items":{"$ref":"#/components/schemas/SimilarResolvedItem"},"type":"array","title":"Items","description":"Matching closed tickets ordered by rank DESC."}},"type":"object","required":["limit","offset","has_more","query","items"],"title":"SimilarResolvedResponse"},"SopDetail":{"properties":{"external_id":{"type":"string","title":"External Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"confidence_score":{"type":"number","title":"Confidence Score","default":0.0},"status":{"type":"string","title":"Status"},"version":{"type":"integer","title":"Version","default":1},"generated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated At"},"cluster_external_id":{"type":"string","title":"Cluster External Id"},"cluster_size":{"type":"integer","title":"Cluster Size","default":0},"what_to_do":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"What To Do"},"what_to_say":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"What To Say"},"citation_map":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object","title":"Citation Map"},"source_ticket_ids":{"items":{"type":"string"},"type":"array","title":"Source Ticket Ids"},"source_chunk_ids":{"items":{"type":"string"},"type":"array","title":"Source Chunk Ids"},"usage_count":{"type":"integer","title":"Usage Count","default":0},"feedback_up":{"type":"integer","title":"Feedback Up","default":0},"feedback_down":{"type":"integer","title":"Feedback Down","default":0},"reviewed_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reviewed By"},"reviewed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reviewed At"},"debug_trail":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Debug Trail"}},"type":"object","required":["external_id","status","cluster_external_id"],"title":"SopDetail"},"SopListItem":{"properties":{"external_id":{"type":"string","title":"External Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Language"},"confidence_score":{"type":"number","title":"Confidence Score","default":0.0},"status":{"type":"string","title":"Status"},"version":{"type":"integer","title":"Version","default":1},"generated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Generated At"},"cluster_external_id":{"type":"string","title":"Cluster External Id"},"cluster_size":{"type":"integer","title":"Cluster Size","default":0}},"type":"object","required":["external_id","status","cluster_external_id"],"title":"SopListItem"},"SopListResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SopListItem"},"type":"array","title":"Items"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"has_more":{"type":"boolean","title":"Has More"}},"type":"object","required":["items","limit","offset","has_more"],"title":"SopListResponse"},"StatsResponse":{"properties":{"namespace":{"type":"string","title":"Namespace","description":"Base namespace the caller asked about. Stays unchanged even when `?client_id=` scopes the FAISS view to a composite internal key."},"backend":{"type":"string","title":"Backend","description":"Vector storage backend","default":"turbopuffer"},"vector_count":{"type":"integer","title":"Vector Count","description":"Number of vectors in Turbopuffer namespace","default":0},"vector_dimensions":{"type":"integer","title":"Vector Dimensions","description":"Embedding dimensionality","default":0},"index_type":{"type":"string","title":"Index Type","description":"Index type (Turbopuffer ANN)","default":"turbopuffer_ann"},"index_size_bytes":{"type":"integer","title":"Index Size Bytes","description":"Approximate logical bytes in Turbopuffer","default":0},"index_status":{"type":"string","title":"Index Status","description":"Turbopuffer index status (up-to-date/building)","default":"unknown"},"last_write_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Write At","description":"Last write timestamp in Turbopuffer"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At","description":"Namespace creation timestamp"},"attributes":{"items":{"type":"string"},"type":"array","title":"Attributes","description":"Vector attribute names in schema"},"aggregated":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Aggregated","description":"Aggregated docs: total, indexed, pending"},"source_data":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source Data","description":"Source data counts: tickets, chats, kb_articles, departments"},"wal_segments":{"type":"integer","title":"Wal Segments","description":"WAL segments (legacy, always 0)","default":0},"last_rebuild_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Rebuild At","description":"Last rebuild (legacy, always null)"},"cached":{"type":"boolean","title":"Cached","description":"Local cache (legacy, always false)","default":false},"documents":{"anyOf":[{"$ref":"#/components/schemas/DocumentStats"},{"type":"null"}],"description":"Aggregated document counts from StatsStore. Filtered by `client_id` when the query parameter is supplied, otherwise summed across every client of this namespace. Null when StatsStore is not enabled for this instance."},"pipelines":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Pipelines","description":"Per-data-provider pipeline statistics from Supabase. Includes raw doc counts, aggregated doc breakdown, top categories, date ranges, last sync info."}},"type":"object","required":["namespace"],"title":"StatsResponse","description":"Full `/v1/ns/{ns}/stats` payload.\n\nThe response combines three views of a namespace:\n\n  * **FAISS view** (top-level scalar fields) — live numbers from\n    the in-process index manager. When `?client_id=` is supplied\n    they reflect the composite `{ns}__{client_id}` internal key;\n    otherwise they reflect the naked namespace (which is zero\n    for any tenant using client_id isolation).\n  * **Document view** (`documents`) — aggregated counts from\n    `StatsStore`, filtered by `client_id` if provided.\n  * **Pipeline view** (`pipelines`) — per-data-provider snapshot\n    of the `data.sqlite` archive each provider persists to S3."},"StepPayload":{"properties":{"text":{"type":"string","maxLength":4000,"minLength":1,"title":"Text"},"cited_source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cited Source Id","description":"Source id the step cites. The service binds the step to this source's chunks for the LLM check. When null, the step is checked against all provided chunks concatenated."},"step_number":{"anyOf":[{"type":"integer","maximum":999.0,"minimum":0.0},{"type":"null"}],"title":"Step Number"}},"type":"object","required":["text"],"title":"StepPayload"},"StepVerdictOut":{"properties":{"step_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Step Number"},"supported":{"type":"boolean","title":"Supported"},"reason":{"type":"string","title":"Reason"},"cited_source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cited Source Id"}},"type":"object","required":["step_number","supported","reason"],"title":"StepVerdictOut"},"ThreadListResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"entity":{"type":"string","enum":["tickets","chats"],"title":"Entity"},"items":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Items","description":"**For `entity=chats`:** chat headers with `messages[]` inlined — the complete operator↔visitor transcript. **For `entity=tickets`:** ticket headers with `actions[]` inlined — but this is the **audit journal only** (state transitions, tags, assignments). The reply text (`comments[]` in `vs_lhn_ticket_comments`) is **NOT** included in this bulk response; call `GET /threads/tickets/{id}` per-ticket to read it. Ordering: request order when `ids` is used, else newest-first."},"total_all":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total All","description":"Total rows matching the non-content filters (date window, operator, category…) — **ignoring** the `with_conversation` gate. Lets the caller compare 'full corpus' vs 'with real dialogue only'. Not returned when `ids` is used."},"total_with_conversation":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total With Conversation","description":"Total rows that also have at least one customer↔agent message in the dialogue table (`vs_lhn_ticket_comments` or `vs_lhn_chat_messages`). Default listing (``with_conversation=true``) works inside this slice."},"with_conversation":{"type":"boolean","title":"With Conversation","description":"Echoes whether the returned `items` were filtered to rows with real dialogue.","default":true}},"type":"object","required":["limit","offset","has_more","entity","items"],"title":"ThreadListResponse"},"TicketDetailResponse":{"properties":{"external_id":{"type":"string","title":"External Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body"},"problem":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Problem"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"priority":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Priority"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"operator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operator"},"customer_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Name"},"customer_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Email"},"visitor_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visitor Id","description":"LHN visitor UUID — canonical cross-channel identity."},"source_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Type"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"has_attachments":{"type":"boolean","title":"Has Attachments","default":false},"actions_count":{"type":"integer","title":"Actions Count","default":0},"created_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created Time"},"updated_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated Time"},"actions":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Actions","description":"Audit journal rows from `vs_lhn_ticket_actions` (ticket_type, actor, content, action_at). These are state transitions (Created/Assigned/Status change/Tag), not the reply text — see `comments`."},"comments":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Comments","description":"Conversational back-and-forth from `vs_lhn_ticket_comments`. Each row carries `message` (the reply body, may be multi-KB on email-forwarded tickets), `created_by` (system | operator email), `type` (e.g. CommentFromUnknown, CommentFromAgent), `customer_notified`, `visible`, and timestamp. Populated by the ticket force_full after migration 006."},"related_chats":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Related Chats","description":"Only present when `include_related=true`. Compact chat headers for the same customer, joined via visitor_id OR customer_email — whichever identity the ticket carries."}},"type":"object","required":["external_id"],"title":"TicketDetailResponse"},"TicketHeader":{"properties":{"external_id":{"type":"string","title":"External Id"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"priority":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Priority"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"department":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department"},"operator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operator"},"customer_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Name"},"customer_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Email"},"visitor_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Visitor Id"},"source_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Type"},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"has_attachments":{"type":"boolean","title":"Has Attachments","default":false},"actions_count":{"type":"integer","title":"Actions Count","default":0},"created_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created Time"},"updated_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated Time"}},"type":"object","required":["external_id"],"title":"TicketHeader"},"TicketListResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"items":{"items":{"$ref":"#/components/schemas/TicketHeader"},"type":"array","title":"Items","description":"Ticket headers without actions; newest first."},"total_all":{"type":"integer","title":"Total All","description":"Total tickets matching this tenant (no `with_conversation` filter)."},"total_with_conversation":{"type":"integer","title":"Total With Conversation","description":"Tickets with ≥1 row in `vs_lhn_ticket_comments` (real customer↔agent dialogue). The default listing (``with_conversation=true``) works inside this slice."},"with_conversation":{"type":"boolean","title":"With Conversation","description":"Echoes whether items were filtered to rows with real dialogue.","default":true}},"type":"object","required":["limit","offset","has_more","items","total_all","total_with_conversation"],"title":"TicketListResponse"},"TimeseriesBucket":{"properties":{"bucket":{"type":"string","title":"Bucket","description":"Bucket boundary (ISO date-like, left-closed)."},"count":{"type":"integer","title":"Count","description":"Row count in this bucket."}},"type":"object","required":["bucket","count"],"title":"TimeseriesBucket"},"TimeseriesResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"entity":{"type":"string","enum":["tickets","chats"],"title":"Entity","description":"Which entity was bucketed."},"bucket":{"type":"string","enum":["day","week","month"],"title":"Bucket","description":"Bucket granularity used by date_trunc."},"data":{"items":{"$ref":"#/components/schemas/TimeseriesBucket"},"type":"array","title":"Data","description":"Bucket rows, newest first."}},"type":"object","required":["limit","offset","has_more","entity","bucket","data"],"title":"TimeseriesResponse"},"TopEntitiesResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"entity":{"type":"string","enum":["tickets","chats"],"title":"Entity"},"group_by":{"type":"string","enum":["category","department","operator","status","priority","source_type"],"title":"Group By"},"data":{"items":{"$ref":"#/components/schemas/TopEntityRow"},"type":"array","title":"Data","description":"Groups ordered by count DESC."}},"type":"object","required":["limit","offset","has_more","entity","group_by","data"],"title":"TopEntitiesResponse"},"TopEntityRow":{"properties":{"key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Key","description":"Group key value (null when the row has a NULL in the grouped column)."},"count":{"type":"integer","title":"Count","description":"Number of rows in this group."}},"type":"object","required":["key","count"],"title":"TopEntityRow"},"UpsertSchedulesRequest":{"properties":{"schedules":{"items":{"$ref":"#/components/schemas/ScheduleConfig"},"type":"array","maxItems":20,"minItems":1,"title":"Schedules","description":"Schedules to upsert. Existing entries with the same (provider, data_type) tuple are replaced atomically."}},"type":"object","required":["schedules"],"title":"UpsertSchedulesRequest"},"ValidateCredentialsRequest":{"properties":{"credentials":{"additionalProperties":true,"type":"object","title":"Credentials","description":"Provider-specific credentials to validate. Shape varies per provider."},"data_types":{"items":{"type":"string"},"type":"array","title":"Data Types","description":"Data types to check access for (empty = all data types the provider supports)"}},"type":"object","required":["credentials"],"title":"ValidateCredentialsRequest"},"ValidateCredentialsResponse":{"properties":{"ok":{"type":"boolean","title":"Ok","description":"True if credentials are valid for all requested data types"},"data_available":{"additionalProperties":{"anyOf":[{"type":"integer"},{"type":"string"}]},"type":"object","title":"Data Available","description":"Per-data-type availability. Keys are provider-specific data type names. Values are typically item counts (int); some providers return 'available' as a string sentinel when an exact count is unavailable."},"details":{"additionalProperties":{"type":"string"},"type":"object","title":"Details","description":"Per-check status messages. Each value either starts with 'ok' (success) or describes the failure (e.g. 'failed: invalid token')."}},"type":"object","required":["ok"],"title":"ValidateCredentialsResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VectorEntry":{"properties":{"chunk_id":{"type":"string","title":"Chunk Id","description":"Vector / chunk identifier"},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata","description":"Vector metadata"}},"type":"object","required":["chunk_id"],"title":"VectorEntry"},"VectorListResponse":{"properties":{"vectors":{"items":{"$ref":"#/components/schemas/VectorEntry"},"type":"array","title":"Vectors","description":"Vector entries with metadata"},"has_more":{"type":"boolean","title":"Has More","description":"Whether more results are available"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Cursor for the next page (pass as starting_after)"},"total":{"type":"integer","title":"Total","description":"Total active vectors in namespace (before pagination)"}},"type":"object","required":["vectors","has_more","total"],"title":"VectorListResponse"},"VectorTemplateSchema":{"properties":{"id_suffix":{"type":"string","maxLength":32,"minLength":1,"title":"Id Suffix","description":"Vector ID suffix appended to {doc_id}_chunk_{idx}_<suffix>"},"text_template":{"type":"string","minLength":1,"title":"Text Template","description":"Jinja2 template rendered with enrichment data to produce the vector text"},"metadata_include":{"items":{"type":"string"},"type":"array","title":"Metadata Include","description":"Whitelist of LLM-extracted fields to copy into chunk metadata. Empty = include all."}},"type":"object","required":["id_suffix","text_template"],"title":"VectorTemplateSchema"},"VisitorDetailResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"visitor_id":{"type":"string","title":"Visitor Id","description":"Echoes the requested visitor_id."},"tickets":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Tickets","description":"Ticket headers (compact) for this visitor, newest first."},"chats":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Chats","description":"Chat headers (compact) for this visitor, newest first."}},"type":"object","required":["limit","offset","has_more","visitor_id","tickets","chats"],"title":"VisitorDetailResponse"},"VisitorListResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"items":{"items":{"$ref":"#/components/schemas/VisitorRow"},"type":"array","title":"Items","description":"Visitors ordered by last_seen DESC."}},"type":"object","required":["limit","offset","has_more","items"],"title":"VisitorListResponse"},"VisitorRow":{"properties":{"visitor_id":{"type":"string","title":"Visitor Id","description":"Canonical LHN visitor UUID."},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email","description":"Last known email (customer_email on ticket side, visitor_email on chat side)."},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Last known display name."},"tickets_count":{"type":"integer","title":"Tickets Count","description":"Tickets that reference this visitor_id."},"chats_count":{"type":"integer","title":"Chats Count","description":"Chats that reference this visitor_id."},"last_seen":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Seen","description":"Most recent activity (max of created_time / start_time)."}},"type":"object","required":["visitor_id","tickets_count","chats_count"],"title":"VisitorRow"},"WarmupResponse":{"properties":{"namespace":{"type":"string","title":"Namespace"},"status":{"type":"string","title":"Status"},"load_time_ms":{"type":"number","title":"Load Time Ms"},"vector_count":{"type":"integer","title":"Vector Count"},"cache_size_mb":{"type":"number","title":"Cache Size Mb"}},"type":"object","required":["namespace","status","load_time_ms","vector_count","cache_size_mb"],"title":"WarmupResponse"},"WithConversationCounts":{"properties":{"tickets":{"type":"integer","title":"Tickets","description":"Tickets carrying at least one row in `vs_lhn_ticket_comments` (real customer↔agent dialogue). The default listing in `/tickets` and MCP `list_threads` operates inside this slice."},"chats":{"type":"integer","title":"Chats","description":"Chats carrying at least one row in `vs_lhn_chat_messages` (real message transcript). Excludes header-only chats from old ingests."}},"type":"object","required":["tickets","chats"],"title":"WithConversationCounts"},"app__api__lhn__CustomerHistoryResponse":{"properties":{"email":{"type":"string","title":"Email"},"items":{"items":{"$ref":"#/components/schemas/CustomerHistoryItem"},"type":"array","title":"Items"}},"type":"object","required":["email","items"],"title":"CustomerHistoryResponse"},"app__api__lhn_provider__CustomerHistoryResponse":{"properties":{"limit":{"type":"integer","minimum":1.0,"title":"Limit","description":"Page size used for this response."},"offset":{"type":"integer","minimum":0.0,"title":"Offset","description":"Starting offset used for this response."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one more row exists past the current page. Computed via a fetch-limit+1 probe — no COUNT(*) is performed."},"email":{"type":"string","title":"Email","description":"Echoed email query (supports ILIKE partial matches)."},"tickets":{"items":{"$ref":"#/components/schemas/CustomerHistoryRow"},"type":"array","title":"Tickets","description":"Tickets for `customer_email` matching this email pattern."},"chats":{"items":{"$ref":"#/components/schemas/CustomerHistoryRow"},"type":"array","title":"Chats","description":"Chats for `visitor_email` matching this email pattern."}},"type":"object","required":["limit","offset","has_more","email","tickets","chats"],"title":"CustomerHistoryResponse"}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API key","description":"Bearer token issued via the Admin Panel. Set once via the Authorize button and reused for all secured requests."}}},"tags":[{"name":"health","description":"Health and monitoring"},{"name":"management","description":"Namespace lifecycle (warmup, rebuild, stats, delete)"},{"name":"documents","description":"Document upload, fetch, and management"},{"name":"crawl","description":"Website crawling — multi-page document ingestion"},{"name":"chunks","description":"Pre-chunked text upsert (caller-supplied IDs + metadata → embedding + indexing)"},{"name":"pipeline","description":"Named pipeline configs (enrichment prompts, vector strategies)"},{"name":"ingest","description":"Data provider ingestion and MCP tools"},{"name":"lhn","description":"LiveHelpNow conversation list/detail (tickets + chats) for QA review"},{"name":"lhn-data","description":"LiveHelpNow data API — typed REST mirror of MCP tools with offset/limit pagination (tickets, chats, KB, visitors, analytics)"},{"name":"query","description":"Text-based semantic search (embed + hybrid search)"},{"name":"admin","description":"Admin panel API (requires admin role)"},{"name":"vectors","description":"Low-level vector CRUD and search"}],"security":[{"BearerAuth":[]}]}