{"openapi":"3.1.0","info":{"title":"Jamb AI Server API","description":"FastAPI-based server with real-time speech transcription capabilities","version":"1.0.0"},"paths":{"/api/v1/rest/translate":{"post":{"tags":["Utilities"],"summary":"Translate Text","description":"Translate text to multiple target languages using Google Cloud Translation API\n\nArgs:\n    request: TranslationRequest with text and target_languages\n\nReturns:\n    Dictionary with translations mapped by language code, detected source language,\n    and latency in milliseconds","operationId":"translate_text_api_v1_rest_translate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslationRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/update-transcript":{"post":{"tags":["Utilities"],"summary":"Correct transcript errors using LLM (audio optional)","description":"Uses Google Gemini (default: Gemini 2.5 Flash) to correct real-time transcription errors.\n    Supports multilingual transcripts (English, Spanish, Portuguese).\n\n    Audio is **optional** - the LLM can correct transcripts using text-only context for spelling,\n    grammar, and common misheard words. When audio is provided, the model uses it to verify\n    corrections against the original audio.\n\n    Optionally generates short summaries in all three languages (English, Portuguese, Spanish).\n    Use `generate_summary=true` to enable. Summary generation runs in parallel with correction.\n\n    ## Supported Input Methods\n\n    ### Method 1: Multipart Form-Data (File Upload)\n    Best for direct file uploads under 100MB.\n    - `transcript` (form field): JSON string with transcript data\n    - `audio` (file field): Audio file (WAV, MP3, AIFF, AAC, OGG, FLAC) - **required for multipart**\n    - Maximum file size: 100MB\n\n    ### Method 2: JSON with Base64-Encoded Audio\n    - Content-Type: application/json\n    - Body: `{\"transcript\": [...], \"audio\": {\"data\": \"base64...\", \"mime_type\": \"audio/wav\"}}`\n    - Maximum base64 length: 140,000,000 characters (~100MB decoded)\n\n    ### Method 3: JSON with Audio URL\n    - Content-Type: application/json\n    - Body: `{\"transcript\": [...], \"audio\": {\"url\": \"gs://bucket/file.wav\", \"mime_type\": \"audio/wav\"}}`\n    - Supported: Google Cloud Storage (gs://) and HTTPS URLs\n    - Maximum download size: 100MB\n\n    ### Method 4: JSON without Audio (Text-Only Correction)\n    Best for simple spelling/grammar corrections without audio context.\n    - Content-Type: application/json\n    - Body: `{\"transcript\": [...]}`\n    - Audio field can be omitted entirely\n\n    ## Security Features\n    - SSRF protection for URL downloads\n    - Size limits enforced (100MB max)\n    - Base64 validation before decoding\n    - Configurable HTTP timeout (default: 30s)\n\n    ## Query Parameters\n    - `generate_summary` (optional, boolean, default `true`): Set to `false` to skip\n      multilingual summary generation for lower cost.\n    - `voicemail` (optional, boolean, default `false`): Enables voicemail-aware semantics.\n      The server filters the transcript to caller-only utterances (`user_id != agent_id`\n      from `users_metadata` where `type == \"agent\"`), runs a fast binary classifier on\n      the caller text, and either short-circuits with an empty result\n      (`is_voicemail=true, is_informative=false`) or falls through to the normal\n      correction (`is_voicemail=true, is_informative=true`). Requires `users_metadata`\n      with an `agent` entry; missing agent → suppression is skipped and the request\n      is treated as informative. The classifier fails open: model errors are logged\n      and treated as informative, so a transient failure never silently drops a\n      legitimate voicemail.\n    - `classify_only` (optional, boolean, default `false`): Only honored together\n      with `voicemail=true`. Returns immediately after the classifier with an empty\n      `corrected_transcript` and empty `summary` even on the informative path,\n      skipping the per-line correction LLM (~15s on many-utterance calls). Useful\n      when a client wants a fast informativeness verdict and runs a second full\n      request in parallel for the corrected transcript.\n\n    ## Voicemail Response Fields\n    `is_voicemail` and `is_informative` are always present in the response (defaults\n    `false` and `true` — fail-open). When `?voicemail=true` is set the endpoint\n    overrides them with the actual classifier verdict.","operationId":"update_transcript_api_v1_rest_update_transcript_post","parameters":[{"name":"generate_summary","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Generate Summary"}},{"name":"voicemail","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Voicemail"}},{"name":"classify_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Classify Only"}}],"responses":{"200":{"description":"Transcript correction response with partial success indicators","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LLMCorrectionOutput"},"examples":{"success":{"summary":"Full success","description":"Both correction and summary succeeded","value":{"corrected_transcript":{"transcript":[{"user_id":"user1","text":"This is the corrected text.","original_text":"This is a test transcript."}]},"summary":{"en":"Brief English summary.","pt":"Resumo breve em português.","es":"Resumen breve en español."},"correction_failed":false,"summary_failed":false}},"correction_failed":{"summary":"Correction failed, summary succeeded","description":"LLM correction failed but summary generated. Original text preserved.","value":{"corrected_transcript":{"transcript":[{"user_id":"user1","text":"Original uncorrected text.","original_text":"Original uncorrected text."}]},"summary":{"en":"Brief English summary.","pt":"Resumo breve em português.","es":"Resumen breve en español."},"correction_failed":true,"summary_failed":false}},"both_failed":{"summary":"Both correction and summary failed","description":"Both operations failed. Original text and empty summary returned.","value":{"corrected_transcript":{"transcript":[{"user_id":"user1","text":"Original uncorrected text.","original_text":"Original uncorrected text."}]},"summary":{"en":"","pt":"","es":""},"correction_failed":true,"summary_failed":true}},"voicemail_informative":{"summary":"Voicemail mode — informative","description":"`?voicemail=true` and the caller said something the recipient should see. Runs the normal correction flow; stamps voicemail fields.","value":{"corrected_transcript":{"transcript":[{"user_id":"caller-aci","text":"Hi, this is Sarah, please call me back at 555-1234.","original_text":"hi this is sarah please call me back at 555 1234"}]},"summary":{"en":"Sarah left a callback request at 555-1234.","pt":"Sarah pediu retorno no 555-1234.","es":"Sarah pidió una devolución de llamada al 555-1234."},"correction_failed":false,"summary_failed":false,"is_voicemail":true,"is_informative":true}},"voicemail_uninformative":{"summary":"Voicemail mode — uninformative","description":"`?voicemail=true` and the caller said nothing the recipient would want to read (filler/noise only). Endpoint short-circuits with empty transcript + summary; the correction LLM is not called.","value":{"corrected_transcript":{"transcript":[]},"summary":{"en":"","pt":"","es":""},"correction_failed":false,"summary_failed":false,"is_voicemail":true,"is_informative":false}},"voicemail_classify_only":{"summary":"Voicemail mode — classify_only","description":"`?voicemail=true&classify_only=true`. Returns the verdict in ~classifier-latency (~500ms) without paying the per-line correction cost, even on the informative path. Client typically runs a second full request in parallel for the corrected transcript.","value":{"corrected_transcript":{"transcript":[]},"summary":{"en":"","pt":"","es":""},"correction_failed":false,"summary_failed":false,"is_voicemail":true,"is_informative":true}}}}}},"400":{"description":"Invalid input (malformed JSON, invalid base64, SSRF blocked URL)"},"413":{"description":"Audio file exceeds 100MB limit"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/agent-identity":{"get":{"tags":["Utilities"],"summary":"Get the agent's persona and wake configuration","description":"Returns the agent's identity as this deployment is configured to present it.\n\n    Clients (`jamb-signal-cli`, the mobile apps) render the agent's name in\n    several different shapes — a two-field Signal profile, a single-line\n    speaker label, a short name, avatar initials. Serving the pair rather than\n    one pre-joined string lets each of them project it themselves instead of\n    guessing where the boundary between the two halves is.\n\n    Cached in-process: the answer is fixed for the life of a deployment and the\n    consumers poll it.\n\n    Gated by the same API auth as the rest of `/api/v1/rest/*`.","operationId":"get_agent_identity_api_v1_rest_agent_identity_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentIdentityResponse"}}}}}}},"/api/v1/rest/agent-avatar":{"get":{"tags":["Utilities"],"summary":"Get the agent's avatar image","description":"Serves the PNG this deployment presents as the agent's avatar — the image\n    half of the persona that `agent-identity` serves the name half of.\n\n    `jamb-signal-cli` fetches this and publishes the bytes on the agent's\n    Signal profile, so every client renders it the way it renders any other\n    contact's picture, with no bundled image of its own to go stale.\n\n    Reached via the `avatar_url` that `agent-identity` publishes rather than by\n    hard-coding this path: a deployment that has no avatar answers `null`\n    there, and asking for it anyway is what gets the 404 below.\n\n    Gated by the same API auth as the rest of `/api/v1/rest/*`.","operationId":"get_agent_avatar_api_v1_rest_agent_avatar_get","responses":{"200":{"description":"The agent's avatar.","content":{"image/png":{}}},"404":{"description":"This deployment has no readable avatar file."}}}},"/api/v1/rest/conversation/{group_id}":{"get":{"tags":["Conversation"],"summary":"Get conversation session details","description":"Retrieves the current state of a conversation session.\n\n    **Returns:**\n    - Session ID and user roster\n    - User metadata (names, types)\n    - Agent ACI identifier\n    - Last event ID for resumption","operationId":"get_session_api_v1_rest_conversation__group_id__get","parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"string","title":"Group Id"}}],"responses":{"200":{"description":"Session details retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSessionResponse"}}}},"404":{"description":"Conversation session not found"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Conversation"],"summary":"Create or resume conversation session","description":"Establishes or resumes a mediation session for a Signal group conversation.\n\n    **Behavior:**\n    - **Idempotent**: Repeated calls with same group-id return existing session\n    - **Session ID**: Returns ADK session identifier (format: agents/{agent}/sessions/{session})\n    - **User List**: Echoes current participant roster for confirmation\n    - **Resume Point**: Returns last_event ID if session has message history, null for new sessions\n\n    **Use Cases:**\n    - Bot initialization: Create session when joining new group\n    - Reconnection: Resume session after bot restart with existing history\n    - Health check: Verify session still active","operationId":"create_or_resume_session_api_v1_rest_conversation__group_id__put","parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"string","title":"Group Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSessionRequest"}}}},"responses":{"200":{"description":"Session created or resumed successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSessionResponse"}}}},"400":{"description":"Invalid request format or ACI IDs"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Conversation"],"summary":"Delete conversation and all related assets","description":"Permanently deletes a conversation and all associated data.\n\n    **This endpoint deletes:**\n    - **ADK Session**: The conversation session and all message events\n    - **Tasks**: All tasks associated with the conversation\n    - **Topics**: All detected topics for the conversation\n    - **Memories**: All long-term memories from Mem0 for the conversation\n    - **Background Progress**: All background task progress records\n\n    **Warning:** This operation is irreversible. All data for the specified\n    conversation will be permanently deleted.\n\n    **Use Cases:**\n    - Clean up test conversations\n    - Remove conversations for privacy/GDPR compliance\n    - Reset a conversation completely\n\n    **Example:**\n    ```bash\n    curl -X DELETE \"http://localhost:8000/api/v1/rest/conversation/group-abc-123\"\n    ```","operationId":"delete_conversation_api_v1_rest_conversation__group_id__delete","parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"string","title":"Group Id"}}],"responses":{"200":{"description":"Conversation and all assets deleted successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteConversationResponse"},"example":{"message":"Conversation deleted successfully","group_id":"group-abc-123","deleted":{"session":true,"tasks":5,"topics":3,"memories":{"message":"Memories deleted successfully","deleted_count":10},"background_progress":2,"pending_jobs":1}}}}},"404":{"description":"Conversation session not found"},"500":{"description":"Internal server error during deletion"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/conversation/{group_id}/update":{"put":{"tags":["Conversation"],"summary":"Update conversation with new messages","description":"Sends new messages to the agent and optionally updates group membership.\n\n    **Behavior:**\n    - **Membership Updates**: users field is optional; only include when roster changes\n    - **Message Batching**: Multiple messages can be sent in a single request\n    - **Processing**: Agent processes messages asynchronously; responses available via GET endpoint\n    - **Deduplication**: Server tracks message IDs in metadata to prevent duplicate processing\n    - **invoke_agent**: Agent processes messages in background (tools, sub-agents) by default.\n      Set to false to only store messages without agent processing.\n\n    **Use Cases:**\n    - Forward user messages from Signal to agent\n    - Batch multiple messages for efficiency\n    - Update group membership when users join/leave\n    - Send attachments (images, files, audio) as base64-encoded data\n    - Use invoke_agent=false to store messages without agent processing","operationId":"update_conversation_api_v1_rest_conversation__group_id__update_put","parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"string","title":"Group Id"}},{"name":"invoke_agent","in":"query","required":false,"schema":{"type":"boolean","description":"If true, invoke agent asynchronously (tools, sub-agents) without generating a response","default":true,"title":"Invoke Agent"},"description":"If true, invoke agent asynchronously (tools, sub-agents) without generating a response"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateConversationRequest"}}}},"responses":{"200":{"description":"Messages accepted successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateConversationResponse"}}}},"400":{"description":"Invalid request format or message data"},"404":{"description":"Conversation session not found"},"413":{"description":"Message payload too large"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/conversation/{group_id}/response":{"post":{"tags":["Conversation"],"summary":"Get agent responses","description":"Retrieves AI-generated responses ready to be sent back to the Signal group.\n\n    **Behavior:**\n    - **Optional Input Messages**: Can include new messages in request body or send empty array\n    - **Response Format**: All agent messages have user: \"AI\"\n    - **Multiple Responses**: Agent may return multiple message parts\n    - **Polling Pattern**: Bot can poll this endpoint to retrieve async responses\n    - **Empty Response**: Returns empty message array if no responses pending\n\n    **Use Cases:**\n    - Retrieve agent responses after sending updates\n    - Poll for async responses (e.g., after tool execution)\n    - Send quick follow-up and get immediate response in one call","operationId":"get_agent_response_api_v1_rest_conversation__group_id__response_post","parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"string","title":"Group Id"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetResponseRequest","default":{"messages":[]}}}}},"responses":{"200":{"description":"Agent responses retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetResponseResponse"}}}},"404":{"description":"Conversation session not found"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/conversation/{group_id}/tasks":{"get":{"tags":["Conversation"],"summary":"Get Conversation Tasks","description":"Get all tasks for a conversation group, organized hierarchically.","operationId":"get_conversation_tasks_api_v1_rest_conversation__group_id__tasks_get","parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"string","title":"Group Id"}},{"name":"language","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO-639 code to translate title/description into","title":"Language"},"description":"ISO-639 code to translate title/description into"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TaskRead"},"title":"Response Get Conversation Tasks Api V1 Rest Conversation  Group Id  Tasks Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/conversation/{group_id}/topics":{"get":{"tags":["Conversation"],"summary":"Get all topics for a conversation","description":"Retrieves all detected topics for a conversation group, ordered by start_timestamp.\n\n    Topics represent discussion themes detected in the conversation. Each topic includes:\n    - category: Topic dimension ('dwelling', 'room', or 'trade')\n    - name: Topic value (e.g., 'kitchen', 'plumbing', 'main-house')\n    - start_timestamp: When the topic first appeared (timestamp of first message mentioning it)\n    - end_timestamp: When the topic was last mentioned (timestamp of last message mentioning it, always set)\n\n    Topics define time spans within the conversation. Multiple topics can overlap in time,\n    and different topics can have the same timestamps if they appear in the same message.\n\n    **Use Cases:**\n    - Track conversation themes over time\n    - Analyze topic patterns and transitions\n    - Filter messages by topic span (messages with timestamp between start_timestamp and end_timestamp)","operationId":"get_conversation_topics_api_v1_rest_conversation__group_id__topics_get","parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"string","title":"Group Id"}}],"responses":{"200":{"description":"Successfully retrieved topics","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TopicRead"},"title":"Response Get Conversation Topics Api V1 Rest Conversation  Group Id  Topics Get"},"example":[{"id":1,"group_id":"group-abc-123","category":"trade","name":"electrical","start_timestamp":"2025-11-17T10:00:00Z","end_timestamp":"2025-11-17T10:30:00Z","created_at":"2025-11-17T10:00:00Z","updated_at":"2025-11-17T10:30:00Z"},{"id":2,"group_id":"group-abc-123","category":"room","name":"kitchen","start_timestamp":"2025-11-17T10:15:00Z","end_timestamp":"2025-11-17T10:30:00Z","created_at":"2025-11-17T10:15:00Z","updated_at":"2025-11-17T10:30:00Z"}]}}},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/conversation/{group_id}/artifacts":{"get":{"tags":["Conversation"],"summary":"Get all artifacts for a conversation","description":"Retrieves all artifacts (files, images, etc.) shared in a conversation group.\n\n    Artifacts are files uploaded via the REST API that have been stored in GCS.\n    This endpoint returns metadata about artifacts; use the `/rest/artifacts/{filename}`\n    endpoint to retrieve the actual file content.\n\n    **Query Parameters:**\n    - `page`: Page number (1-indexed, default: 1)\n    - `page_size`: Items per page (default: 20, max: 100)\n    - `call_id`: Optional filter for artifacts from a specific call\n\n    **Use Cases:**\n    - List all files shared in a conversation\n    - Track images uploaded during a chat session\n    - Get artifact metadata before downloading","operationId":"get_conversation_artifacts_api_v1_rest_conversation__group_id__artifacts_get","parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"string","title":"Group Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number (1-indexed)","default":1,"title":"Page"},"description":"Page number (1-indexed)"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Items per page (max 100)","default":20,"title":"Page Size"},"description":"Items per page (max 100)"},{"name":"call_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by call ID (optional)","title":"Call Id"},"description":"Filter by call ID (optional)"}],"responses":{"200":{"description":"Successfully retrieved artifacts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactListResponse"},"example":{"artifacts":[{"id":1,"group_id":"group-abc-123","filename":"photo.png","mime_type":"image/png","size_bytes":102400,"version":1,"created_at":"2025-11-17T10:00:00Z","updated_at":"2025-11-17T10:00:00Z"}],"total":1,"page":1,"page_size":20,"has_more":false}}}},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/memories":{"get":{"tags":["Memories"],"summary":"Get memories by conversation or user","description":"Retrieve long-term memories from Mem0 filtered by conversation_id and/or user_id.\n\n    **Filtering:**\n    - **conversation_id**: Returns memories associated with a specific conversation/group\n    - **user_id**: Returns memories associated with a specific user\n    - **Both**: Returns memories matching both filters (conversation AND user)\n\n    **Use Cases:**\n    - Retrieve conversation context for a specific group\n    - Get user-specific memories across all conversations\n    - Combine filters for precise memory retrieval\n\n    **Examples:**\n    - Get all memories for a conversation: `?conversation_id=group-abc-123`\n    - Get memories for a user: `?user_id=aci-user1`\n    - Get memories for a user in a specific conversation: `?conversation_id=group-abc-123&user_id=aci-user1`","operationId":"get_memories_api_v1_rest_memories_get","parameters":[{"name":"conversation_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by conversation/group ID","title":"Conversation Id"},"description":"Filter by conversation/group ID"},{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by user ID","title":"User Id"},"description":"Filter by user ID"}],"responses":{"200":{"description":"Successfully retrieved memories","content":{"application/json":{"schema":{},"example":[{"id":"mem-123","memory":"User prefers morning meetings","user_id":"aci-user1","run_id":"group-abc-123","created_at":"2025-01-01T12:00:00Z"}]}}},"400":{"description":"Invalid request - missing required parameters"},"500":{"description":"Internal server error - Mem0 service unavailable"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Memories"],"summary":"Delete memories by conversation or user","description":"Delete long-term memories from Mem0 filtered by conversation_id and/or user_id.\n\n    **Filtering:**\n    - **conversation_id**: Deletes all memories associated with a specific conversation/group\n    - **user_id**: Deletes all memories associated with a specific user\n    - **Both**: Deletes memories matching both filters (conversation AND user)\n\n    **Use Cases:**\n    - Reset conversation context for a specific group\n    - Clear all user-specific memories\n    - Remove memories matching both filters for precise deletion\n\n    **Examples:**\n    - Delete all memories for a conversation: `?conversation_id=group-abc-123`\n    - Delete memories for a user: `?user_id=aci-user1`\n    - Delete memories for a user in a specific conversation: `?conversation_id=group-abc-123&user_id=aci-user1`\n\n    **Warning:** This operation is irreversible. All matching memories will be permanently deleted.","operationId":"delete_memories_api_v1_rest_memories_delete","parameters":[{"name":"conversation_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by conversation/group ID","title":"Conversation Id"},"description":"Filter by conversation/group ID"},{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by user ID","title":"User Id"},"description":"Filter by user ID"}],"responses":{"200":{"description":"Successfully deleted memories","content":{"application/json":{"schema":{},"example":{"message":"Memories deleted successfully","deleted_count":5}}}},"400":{"description":"Invalid request - missing required parameters"},"500":{"description":"Internal server error - Mem0 service unavailable"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/task":{"post":{"tags":["Tasks"],"summary":"Create a new task","description":"Create a new task with the specified details.\n\n    **Required Fields:**\n    - **group_id**: Conversation group ID to associate the task with\n    - **title**: Short task title (max 256 characters)\n\n    **Optional Fields:**\n    - **description**: Detailed task description\n    - **assigned_to_user_id**: User ID to assign the task to\n    - **parent_task_id**: ID of parent task (for subtasks)\n    - **status**: Task status (TODO, INPROGRESS, DONE) - defaults to TODO\n\n    **Use Cases:**\n    - Create standalone tasks for a conversation\n    - Create subtasks by specifying parent_task_id\n    - Assign tasks to specific users","operationId":"create_task_api_v1_rest_task_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskCreate"}}}},"responses":{"200":{"description":"Task created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskRead"}}}},"400":{"description":"Invalid request data"},"404":{"description":"Parent task not found"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Tasks"],"summary":"List tasks with optional filters (paginated)","description":"Retrieve tasks filtered by conversation (group_id), assigned user, and/or call ID.\n\n    **Query Parameters:**\n    - **conversation**: Filter by conversation/group ID\n    - **user**: Filter by assigned user ACI\n    - **call_id**: Filter by call ID (tasks created during a specific call)\n    - **page**: Page number (default: 1)\n    - **size**: Items per page (default: 50, max: 100)\n\n    At least one filter (conversation, user, or call_id) must be provided.\n\n    **Behavior:**\n    - Returns top-level tasks (parent_task_id is NULL) with their subtasks nested\n    - Tasks are ordered by creation date (newest first)\n    - Subtasks are included in the response via the 'subtasks' field\n    - Response includes pagination metadata (total, page, size, pages)\n\n    **Use Cases:**\n    - Get all tasks for a conversation: `?conversation=group-abc-123`\n    - Get all tasks assigned to a user: `?user=aci-user1`\n    - Get all tasks from a specific call: `?call_id=call-uuid`\n    - Combine filters: `?conversation=group-abc-123&user=aci-user1`\n    - Paginate results: `?conversation=group-abc-123&page=2&size=20`","operationId":"list_tasks_api_v1_rest_task_get","parameters":[{"name":"conversation","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by conversation/group ID","title":"Conversation"},"description":"Filter by conversation/group ID"},{"name":"user","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by assigned user ACI","title":"User"},"description":"Filter by assigned user ACI"},{"name":"call_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by call ID (tasks created during a specific call)","title":"Call Id"},"description":"Filter by call ID (tasks created during a specific call)"},{"name":"language","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO-639 code to translate title/description into","title":"Language"},"description":"ISO-639 code to translate title/description into"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Page size","default":50,"title":"Size"},"description":"Page size"}],"responses":{"200":{"description":"Tasks retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_TaskRead_"}}}},"400":{"description":"Missing required query parameters"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/task/{task_id}":{"get":{"tags":["Tasks"],"summary":"Get a specific task by ID","description":"Retrieve a specific task by its ID.\n\n    **Response:**\n    - Returns the task with all its subtasks nested in the 'subtasks' field\n    - Includes all task metadata (title, description, status, timestamps, etc.)\n\n    **Use Cases:**\n    - Fetch details of a specific task\n    - Get task with all its subtasks","operationId":"get_task_api_v1_rest_task__task_id__get","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"integer","title":"Task Id"}},{"name":"language","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO-639 code to translate title/description into","title":"Language"},"description":"ISO-639 code to translate title/description into"}],"responses":{"200":{"description":"Task retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskRead"}}}},"404":{"description":"Task not found"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Tasks"],"summary":"Update a task","description":"Update an existing task. Only provided fields will be updated (partial update).\n\n    **Updatable Fields:**\n    - **title**: Update task title\n    - **description**: Update task description\n    - **assigned_to_user_id**: Reassign task to a different user\n    - **parent_task_id**: Move task to a different parent (or make it top-level with null)\n    - **status**: Update task status (TODO, INPROGRESS, DONE)\n\n    **Validation:**\n    - parent_task_id must reference an existing task in the same conversation\n    - Cannot set parent_task_id to the task's own ID or any of its subtasks (circular reference)\n\n    **Language handling:**\n    - A task is always stored in the language it was created in (`detected_language`).\n    - Content fields (title, description, summary, notes, updates[].message) submitted\n      in another language — e.g. an edit made against a `GET ...?language=es` view — are\n      translated back into `detected_language` before persisting.\n    - Prose edits are reconciled by an **LLM** shown the original source-language task\n      and the user's edit: it applies the change in the source language, preserves\n      unchanged wording (so \"charge the car\" doesn't round-trip to \"load the car\"), and\n      regenerates the `summary`. It falls back to machine translation if the LLM is\n      unavailable; `updates[].message` lines use machine translation directly.\n    - **`?language=<iso>`** (optional): the language the submitted content is in — pass\n      the same code the client used on the prior GET. When provided, translate-back is\n      deterministic (detection is skipped), so even short / one-word edits are translated\n      correctly; a no-op when it equals `detected_language`.\n    - When `?language=` is omitted, the submitted language is auto-detected; same-language\n      and very short / language-neutral edits (which can't be confidently identified) are\n      stored verbatim.\n    - **Stable contract:** payloads that contain no content fields (e.g. status-only or\n      assignment-only PUTs) perform zero translation work and never rewrite content.\n      `detected_language` is likewise never altered by a PUT.\n\n    **Use Cases:**\n    - Mark task as complete: `{\"status\": \"DONE\"}`\n    - Reassign task: `{\"assigned_to_user_id\": \"new-user-id\"}`\n    - Update multiple fields at once","operationId":"update_task_api_v1_rest_task__task_id__put","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"integer","title":"Task Id"}},{"name":"language","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO-639 code: the language the submitted content is in (e.g. the ?language= the client used on the prior GET). When provided, the edit is translated back into the task's detected_language deterministically (detection is skipped). When omitted, the language is auto-detected.","title":"Language"},"description":"ISO-639 code: the language the submitted content is in (e.g. the ?language= the client used on the prior GET). When provided, the edit is translated back into the task's detected_language deterministically (detection is skipped). When omitted, the language is auto-detected."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskUpdate"}}}},"responses":{"200":{"description":"Task updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskRead"}}}},"400":{"description":"Invalid update data"},"404":{"description":"Task or parent task not found"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Tasks"],"summary":"Delete a task and its subtasks","description":"Delete a task and all its subtasks (cascade delete).\n    Requires Signal Basic Auth. Only the assigned user may delete a task.\n\n    **Behavior:**\n    - Authenticates the caller via Signal whoami\n    - Checks the caller owns the task (or task is unassigned)\n    - Deletes the specified task and all subtasks\n    - Deletion is permanent and irreversible\n\n    **Use Cases:**\n    - Remove completed tasks\n    - Delete task hierarchies\n    - Clean up test data","operationId":"delete_task_api_v1_rest_task__task_id__delete","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"integer","title":"Task Id"}},{"name":"accept-language","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Accept-Language"}}],"responses":{"200":{"description":"Task deleted successfully","content":{"application/json":{"schema":{},"example":{"message":"Task deleted successfully","task_id":123,"deleted_subtasks":5}}}},"401":{"description":"Invalid Signal credentials"},"403":{"description":"Not authorized to delete this task"},"404":{"description":"Task not found"},"500":{"description":"Internal server error"},"502":{"description":"Signal server unreachable"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/task/{task_id}/artifacts/{artifact_id}":{"post":{"tags":["Tasks"],"summary":"Link an artifact to a task","description":"Associate an existing artifact with a task by setting its task_id (#699). Artifact and task must share a conversation group. Idempotent; re-points an artifact already linked to a different task. message_timestamp is preserved.","operationId":"link_task_artifact_api_v1_rest_task__task_id__artifacts__artifact_id__post","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"integer","title":"Task Id"}},{"name":"artifact_id","in":"path","required":true,"schema":{"type":"string","title":"Artifact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Tasks"],"summary":"Unlink an artifact from a task","description":"Clear an artifact's task association (#699). Only succeeds when the artifact currently points at this task (409 otherwise); already-unlinked is a no-op. The artifact itself is retained.","operationId":"unlink_task_artifact_api_v1_rest_task__task_id__artifacts__artifact_id__delete","parameters":[{"name":"task_id","in":"path","required":true,"schema":{"type":"integer","title":"Task Id"}},{"name":"artifact_id","in":"path","required":true,"schema":{"type":"string","title":"Artifact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/instruction":{"post":{"tags":["AnsweringService"],"summary":"Create an Instruction","operationId":"create_instruction_api_v1_rest_instruction_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstructionCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstructionRead"}}}},"401":{"description":"Missing or invalid demo API key"},"409":{"description":"diversion_number already exists"},"422":{"description":"Invalid request body"}}},"get":{"tags":["AnsweringService"],"summary":"Lookup Instruction by diversion_number, or list paginated","operationId":"list_or_get_instructions_api_v1_rest_instruction_get","parameters":[{"name":"diversion_number","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"description":"If provided, return the single Instruction matching this E.164 diversion number. Must be non-empty when present (use `null` / omit the parameter entirely to list). If omitted, return a paginated list.","title":"Diversion Number"},"description":"If provided, return the single Instruction matching this E.164 diversion number. Must be non-empty when present (use `null` / omit the parameter entirely to list). If omitted, return a paginated list."},{"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":"Found / listed","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InstructionRead"},{"$ref":"#/components/schemas/InstructionList"}],"title":"Response List Or Get Instructions Api V1 Rest Instruction Get"}}}},"401":{"description":"Missing or invalid demo API key"},"404":{"description":"Not found (when looking up by diversion_number)"},"422":{"description":"Invalid query parameters"}}}},"/api/v1/rest/instruction/preview":{"post":{"tags":["AnsweringService"],"summary":"Render the effective system prompt for an Instruction draft","description":"Return the prompt the agent actually runs with, not just the operator's text.\n\nThe admin UI approximated this in JavaScript, which could only show the\nfields the operator typed — the preamble, the HOW TO BEHAVE block, the\nrecipient-resolution rules and the tool contract wrapped around them were\ninvisible, so the pane could not answer \"what is the agent actually being\ntold\". Rendering server-side through the same\n``render_answering_service_instruction`` the realtime session calls at call\ntime (``app/google_search_agent/agent.py``) keeps one source of truth: what\nthis endpoint returns is byte-for-byte the agent's system instruction.\n\nStateless by design — it takes a draft rather than an id, so an operator can\nsee the effect of an edit before saving it. Nothing is read from or written\nto the DB.","operationId":"preview_instruction_prompt_api_v1_rest_instruction_preview_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstructionPreviewRequest"}}},"required":true},"responses":{"200":{"description":"Rendered prompt","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstructionPromptPreview"}}}},"401":{"description":"Missing or invalid demo API key"},"422":{"description":"Invalid request body"}}}},"/api/v1/rest/instruction/{instruction_id}":{"get":{"tags":["AnsweringService"],"summary":"Get Instruction by id","operationId":"get_instruction_api_v1_rest_instruction__instruction_id__get","parameters":[{"name":"instruction_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Instruction Id"}}],"responses":{"200":{"description":"Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstructionRead"}}}},"401":{"description":"Missing or invalid demo API key"},"404":{"description":"Not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["AnsweringService"],"summary":"Top-level partial update (each included field replaces wholesale)","operationId":"update_instruction_api_v1_rest_instruction__instruction_id__put","parameters":[{"name":"instruction_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Instruction Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstructionUpdate"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstructionRead"}}}},"401":{"description":"Missing or invalid demo API key"},"404":{"description":"Not found"},"409":{"description":"diversion_number collides with another row"},"422":{"description":"Invalid request body"}}},"delete":{"tags":["AnsweringService"],"summary":"Hard delete an Instruction","operationId":"delete_instruction_api_v1_rest_instruction__instruction_id__delete","parameters":[{"name":"instruction_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Instruction Id"}}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Missing or invalid demo API key"},"404":{"description":"Not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/realtime/call":{"post":{"tags":["Calls"],"summary":"Create a new call","description":"Creates a new call session linked to an existing chat session.\n\n    `chat_session_id` is the long-lived conversation/group identifier. The\n    returned API `call_id` is the persisted `CallSession.call_session_id`, linked\n    to it through `CallSession.chat_session_id`. One chat session can therefore\n    have many calls. Pass both values to the conversation/group WebSocket; when\n    tracing is enabled and configured, Arize records the call ID as `session.id`.\n\n    The call copies user_metadata from the chat session's ADK state.\n    Call context is tracked via the CallSession database table (not session state).\n    Returns the call details including status=STARTED.","operationId":"create_call_api_v1_realtime_call_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallCreateRequest"}}}},"responses":{"200":{"description":"Call created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallCreateResponse"}}}},"404":{"description":"Chat session not found"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Calls"],"summary":"List calls","description":"Lists call sessions with optional filters.\n\n    At least one of `chat_session_id` or `user_id` must be provided.","operationId":"list_calls_api_v1_realtime_call_get","parameters":[{"name":"chat_session_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by chat session ID","title":"Chat Session Id"},"description":"Filter by chat session ID"},{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by participant user ID","title":"User Id"},"description":"Filter by participant user ID"}],"responses":{"200":{"description":"Calls retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallListResponse"}}}},"400":{"description":"Missing required query parameters"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/realtime/call/{call_id}":{"get":{"tags":["Calls"],"summary":"Get call details","description":"Retrieves details of a specific call session.","operationId":"get_call_api_v1_realtime_call__call_id__get","parameters":[{"name":"call_id","in":"path","required":true,"schema":{"type":"string","title":"Call Id"}}],"responses":{"200":{"description":"Call details retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallReadResponse"}}}},"404":{"description":"Call not found"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Calls"],"summary":"Update call","description":"Updates a call session.\n\n    Set `status` to \"ENDED\" to terminate the call.\n    When a call is terminated, a background task is triggered to process the call.","operationId":"update_call_api_v1_realtime_call__call_id__put","parameters":[{"name":"call_id","in":"path","required":true,"schema":{"type":"string","title":"Call Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallUpdateRequest"}}}},"responses":{"200":{"description":"Call updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallReadResponse"}}}},"404":{"description":"Call not found"},"500":{"description":"Internal server error"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/demos/traces":{"get":{"tags":["demos","demos","Observability"],"summary":"Fetch Arize trace spans for a call or conversation","description":"Returns the OpenInference spans Arize recorded for a single call (`session_id` = the call_id) or an entire conversation (`chat_session_id`), normalized into per-turn trace trees for the demo Call/Chat Trace tabs. Spans are fetched on demand from Arize's REST API and are gated by `ENABLE_LLM_LOGGING` (returns empty when off). Provide exactly one of `session_id` or `chat_session_id`. Each span carries `metadata.purpose` (call, chat, task_detection, topic_detection, transcript_correction, voicemail_classifier, artifact_analysis) so a trace can be traced back to what produced it.","operationId":"demo_traces_api_v1_demos_traces_get","parameters":[{"name":"session_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Call session id (call_id) for the Call Trace tab","title":"Session Id"},"description":"Call session id (call_id) for the Call Trace tab"},{"name":"chat_session_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Conversation id for the Chat Trace tab","title":"Chat Session Id"},"description":"Conversation id for the Chat Trace tab"}],"responses":{"200":{"description":"Spans grouped into per-turn trace trees.","content":{"application/json":{"schema":{},"examples":{"call_trace":{"summary":"Call Trace (?session_id=<call_id>)","value":{"session_id":"3eb756b2-1001-4bd3-a50a-dda981d8244c","span_count":15,"truncated":false,"traces":[{"trace_id":"da815a9b25994a759357f1592ce80eb9","roots":[{"name":"invocation [JAMB]","kind":"CHAIN","output_value":"Argentina won the 2022 FIFA World Cup.","children":[{"name":"agent_run [Jamb_AI]","kind":"AGENT"}]}]}],"llm_logging_enabled":true,"arize_project":"jamb-test","arize_url":"https://app.arize.com/organizations/.../spaces/.../projects/...?selectedSessionId=..."}},"chat_trace":{"summary":"Chat Trace (?chat_session_id=<conversation_id>)","value":{"chat_session_id":"all-e2e-c8664152","session_count":1,"sessions":[{"session_id":"all-e2e-c8664152","span_count":8,"traces":[]}],"truncated":false,"llm_logging_enabled":true,"arize_project":"jamb-test"}}}}}},"400":{"description":"Neither session_id nor chat_session_id provided."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/demos/ai-message":{"post":{"tags":["demos","demos"],"summary":"Send Ai Message Demo","description":"Send an AI-initiated message through a connected agent's Signal identity.","operationId":"send_ai_message_demo_api_v1_demos_ai_message_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiMessageRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/demos/daily-reminder":{"post":{"tags":["demos","demos"],"summary":"Trigger Daily Reminder","description":"Trigger a daily task reminder for a specific user on demand.","operationId":"trigger_daily_reminder_api_v1_demos_daily_reminder_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DailyReminderRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/artifacts/file":{"post":{"tags":["Artifacts"],"summary":"Upload Artifact File","description":"Upload a file to GCS via multipart form data.\n\nFor JSON body upload, use POST /json.\nFor direct client upload, use POST /upload-url to get a signed URL.\n\nThe returned file_uri can be used in messages:\n- In `metadata.file_uri` for any message type\n- The server uses Part.from_uri(file_uri) for efficient LLM processing\n\nArgs:\n    file: File to upload (multipart form)\n    session_id: Session ID to associate the artifact with\n    user_id: Optional user ID who uploaded the artifact\n\nReturns:\n    UploadArtifactResponse with artifact_id and file_uri (GCS URI)","operationId":"upload_artifact_file_api_v1_rest_artifacts_file_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_artifact_file_api_v1_rest_artifacts_file_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadArtifactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/artifacts/json":{"post":{"tags":["Artifacts"],"summary":"Upload Artifact Json","description":"Upload a file to GCS via JSON body with base64 data.\n\nThe returned file_uri can be used in messages:\n- In `metadata.file_uri` for any message type\n- The server uses Part.from_uri(file_uri) for efficient LLM processing\n\nArgs:\n    request: JSON body with session_id, filename, mime_type, and base64 data\n\nReturns:\n    UploadArtifactResponse with artifact_id and file_uri (GCS URI)","operationId":"upload_artifact_json_api_v1_rest_artifacts_json_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadArtifactRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadArtifactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/artifacts/upload-url":{"post":{"tags":["Artifacts"],"summary":"Get Upload Url","description":"Get an upload URL for direct client upload to GCS.\n\nSupports two upload modes:\n- **Signed URL** (default): Single PUT request, 15-minute expiration\n- **Resumable** (?resumable=true): Chunked uploads with resume capability\n\nFlow:\n1. Client calls this endpoint with file metadata\n2. Server returns upload URL and creates the DB record\n3. Client uploads directly to GCS\n4. Client uses artifact_id in subsequent messages\n\nFor resumable uploads, client follows GCS resumable upload protocol:\n- Upload chunks with Content-Range header\n- 308 response means continue, 200/201 means complete\n- Query status with Content-Range: bytes */{total} to resume\n\nArgs:\n    request: File metadata (session_id, filename, mime_type, size_bytes)\n    resumable: Use resumable upload protocol (default: False)\n\nReturns:\n    SignedUrlResponse with upload_url, artifact_id, and upload metadata","operationId":"get_upload_url_api_v1_rest_artifacts_upload_url_post","parameters":[{"name":"resumable","in":"query","required":false,"schema":{"type":"boolean","description":"Use resumable upload protocol for chunked uploads (recommended for files > 10MB)","default":false,"title":"Resumable"},"description":"Use resumable upload protocol for chunked uploads (recommended for files > 10MB)"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignedUrlRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignedUrlResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/artifacts/{artifact_id}":{"get":{"tags":["Artifacts"],"summary":"Get Artifact","description":"Retrieve an artifact from GCS storage by ID.\n\nThe artifact_id uniquely identifies the artifact. The server looks up\nthe artifact record and returns the file content directly from GCS.","operationId":"get_artifact_api_v1_rest_artifacts__artifact_id__get","parameters":[{"name":"artifact_id","in":"path","required":true,"schema":{"type":"string","description":"Artifact UUID from upload response","title":"Artifact Id"},"description":"Artifact UUID from upload response"},{"name":"version","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Artifact version (latest if omitted)","title":"Version"},"description":"Artifact version (latest if omitted)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"head":{"tags":["Artifacts"],"summary":"Head Artifact","description":"Check if an artifact exists and return metadata without downloading.\n\nThe artifact_id uniquely identifies the artifact. The server looks up\nthe artifact record and verifies the file exists in GCS before returning\nmetadata (mime_type, size, etc.).","operationId":"head_artifact_api_v1_rest_artifacts__artifact_id__head","parameters":[{"name":"artifact_id","in":"path","required":true,"schema":{"type":"string","description":"Artifact UUID from upload response","title":"Artifact Id"},"description":"Artifact UUID from upload response"},{"name":"version","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Artifact version (latest if omitted)","title":"Version"},"description":"Artifact version (latest if omitted)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/artifacts/{artifact_id}/upload-complete":{"post":{"tags":["Artifacts"],"summary":"Upload Complete","description":"Signal that a signed URL / resumable upload has completed.\n\nTriggers background analysis for the uploaded artifact.\nCall this after the client finishes uploading to the signed URL.","operationId":"upload_complete_api_v1_rest_artifacts__artifact_id__upload_complete_post","parameters":[{"name":"artifact_id","in":"path","required":true,"schema":{"type":"string","description":"Artifact UUID","title":"Artifact Id"},"description":"Artifact UUID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/artifacts/{artifact_id}/metadata":{"get":{"tags":["Artifacts"],"summary":"Get Artifact Metadata","description":"Get artifact metadata including analysis status and description.\n\nThe main GET /{artifact_id} returns raw file bytes. This endpoint\nreturns JSON metadata with the new analysis fields.","operationId":"get_artifact_metadata_api_v1_rest_artifacts__artifact_id__metadata_get","parameters":[{"name":"artifact_id","in":"path","required":true,"schema":{"type":"string","description":"Artifact UUID","title":"Artifact Id"},"description":"Artifact UUID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/images/{image_id}":{"get":{"tags":["Images"],"summary":"Get Image","description":"Get image analysis by image_analyses.id.","operationId":"get_image_api_v1_rest_images__image_id__get","parameters":[{"name":"image_id","in":"path","required":true,"schema":{"type":"string","description":"Image analysis UUID","title":"Image Id"},"description":"Image analysis UUID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageAnalysisRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/images/by-artifact/{artifact_id}":{"get":{"tags":["Images"],"summary":"Get Image By Artifact","description":"Get image analysis by artifact_id.\n\nReturns 404 with analysis_status if no analysis exists.","operationId":"get_image_by_artifact_api_v1_rest_images_by_artifact__artifact_id__get","parameters":[{"name":"artifact_id","in":"path","required":true,"schema":{"type":"string","description":"Artifact UUID","title":"Artifact Id"},"description":"Artifact UUID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageAnalysisRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/videos/{video_id}":{"get":{"tags":["Videos"],"summary":"Get Video","description":"Get video analysis by video_analyses.id.","operationId":"get_video_api_v1_rest_videos__video_id__get","parameters":[{"name":"video_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"Video analysis UUID","title":"Video Id"},"description":"Video analysis UUID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoAnalysisRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/videos/by-artifact/{artifact_id}":{"get":{"tags":["Videos"],"summary":"Get Video By Artifact","description":"Get video analysis by artifact_id.\n\nReturns 404 with analysis_status if no analysis exists.","operationId":"get_video_by_artifact_api_v1_rest_videos_by_artifact__artifact_id__get","parameters":[{"name":"artifact_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"Artifact UUID","title":"Artifact Id"},"description":"Artifact UUID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoAnalysisRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/documents/{document_id}":{"get":{"tags":["Documents"],"summary":"Get Document","description":"Get document analysis by document_analyses.id.","operationId":"get_document_api_v1_rest_documents__document_id__get","parameters":[{"name":"document_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"Document analysis UUID","title":"Document Id"},"description":"Document analysis UUID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentAnalysisRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/documents/by-artifact/{artifact_id}":{"get":{"tags":["Documents"],"summary":"Get Document By Artifact","description":"Get document analysis by artifact_id.\n\nReturns 404 with analysis_status if no analysis exists.","operationId":"get_document_by_artifact_api_v1_rest_documents_by_artifact__artifact_id__get","parameters":[{"name":"artifact_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"Artifact UUID","title":"Artifact Id"},"description":"Artifact UUID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentAnalysisRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/audios/{audio_id}":{"get":{"tags":["Audios"],"summary":"Get Audio","description":"Get audio analysis by audio_analyses.id.","operationId":"get_audio_api_v1_rest_audios__audio_id__get","parameters":[{"name":"audio_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"Audio analysis UUID","title":"Audio Id"},"description":"Audio analysis UUID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioAnalysisRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rest/audios/by-artifact/{artifact_id}":{"get":{"tags":["Audios"],"summary":"Get Audio By Artifact","description":"Get audio analysis by artifact_id.\n\nReturns 404 with analysis_status if no analysis exists.","operationId":"get_audio_by_artifact_api_v1_rest_audios_by_artifact__artifact_id__get","parameters":[{"name":"artifact_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"Artifact UUID","title":"Artifact Id"},"description":"Artifact UUID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioAnalysisRead"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/healthz":{"get":{"summary":"Health Check","description":"Health check endpoint to verify server is properly configured","operationId":"health_check_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health Check","description":"Health check endpoint to verify server is properly configured","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/realtime/transcribe/ws":{"summary":"Real-time Speech Transcription WebSocket","description":"Real-time speech transcription using Deepgram Nova-3, Deepgram Flux, or OpenAI realtime models","operationId":"transcription_websocket","tags":["realtime"],"get":{"summary":"Real-time Speech Transcription WebSocket","description":"Real-time speech transcription using Deepgram with optimized binary protocol support.\n\n## Audio Format\n- Sample rate: 8000-48000 Hz (configurable, default 24000)\n- Format: Mono, 16-bit linear PCM\n- Chunk size: 20-50ms for Nova-3; Flux is reframed upstream to 80ms\n\n## Query Parameters\n- `protocol`: 'json' (default) or 'binary' for raw PCM (34% bandwidth savings)\n- `transcription_model`: `nova-3` (default), `flux-general-en`, `flux-general-multi`,\n  `gpt-realtime-whisper`, or `gpt-4o-transcribe`\n- `lang`: 'multi' (default, auto-detect) or specific ISO code (en, es, fr, etc.)\n- `sample_rate`: 8000, 16000, 24000 (default), 32000, 44100, or 48000 Hz\n- `vad_events`: Truthy values keep VAD events enabled and force interim results.\n  Query-level false does not disable the default; use the JSON config message to disable VAD events.\n- `denoise`: `off` (default) or `rnnoise`\n- `return_audio`: Stream post-denoise PCM back to the client (default false)\n- `turn_detector`: `builtin` (the model's own signal -- Deepgram endpointing,\n  Flux `EndOfTurn`, `gpt-4o-transcribe` server VAD, `chirp-3` voice-activity\n  events; also accepted under its legacy spelling `deepgram`; the default),\n  `smart_turn_v3`, `livekit_v1`, or `livekit_v1_mini`. Which values a given\n  `transcription_model` accepts is declared per-model (see\n  `app/services/transcription/models.py`); an explicit request for an\n  unsupported value closes with policy code 1008, an unsupported *default*\n  falls back silently.\n- `language_detector`: `fasttext` (default, content-based override) or\n  `built_in` (trust the model's own native tag as-is). Same per-model\n  compatibility rule as `turn_detector`.\n- `keyterms`: Semicolon-separated Deepgram recognition keyterms\n- `acoustic_wake_word`: Override acoustic Hey Jamb detection for this connection\n\n`interim_results` defaults to true and can be changed in the JSON config message;\nit is not a URL query parameter.\n\n## JSON Protocol (default)\nClient messages:\n```json\n// Config (optional, send before audio):\n{\"type\": \"config\", \"language\": \"multi\", \"interim_results\": true, \"vad_events\": true, \"sample_rate\": 24000}\n\n// Audio chunk:\n{\"type\": \"audio\", \"data\": \"<base64 PCM>\", \"timestamp\": \"ISO-8601\"}\n\n// Keepalive:\n{\"type\": \"ping\", \"timestamp\": \"ISO-8601\"}\n```\n\n## Binary Protocol (protocol=binary)\nAudio: Send raw PCM bytes directly as binary WebSocket frames (no wrapper).\nControl: Config, ping, and all other messages remain JSON.\n\n```javascript\n// Audio: Direct binary transmission\nws.send(pcmAudioBuffer);  // ArrayBuffer or Buffer\n\n// Config: Still JSON\nws.send(JSON.stringify({type: \"config\", language: \"en\"}));\n```\n\n## Server Messages (always JSON)\n```json\n// Connected:\n{\"type\": \"connected\", \"session_id\": \"uuid\", \"timestamp\": \"ISO-8601\"}\n\n// Config acknowledged:\n{\"type\": \"config_ack\", \"language\": \"multi\", \"interim_results\": true, \"vad_events\": true}\n\n// Speech detected (vad_events=true):\n{\"type\": \"speech_started\", \"timestamp\": \"ISO-8601\", \"session_id\": \"uuid\", \"utterance_id\": \"uuid\"}\n\n// Transcript (with optional latencies):\n// - latency_ms on first transcript (SpeechStarted→first transcript)\n// - tail_latency_ms on final only (UtteranceEnd→final)\n{\"type\": \"transcript\", \"text\": \"Hello\", \"confidence\": 0.98, \"is_final\": true, \"language\": \"en\",\n \"latency_ms\": 187.5, \"tail_latency_ms\": 92.0, \"utterance_id\": \"uuid\"}\n\n// Utterance end:\n{\"type\": \"utterance_end\", \"timestamp\": \"ISO-8601\", \"utterance_id\": \"uuid\"}\n\n// Error:\n{\"type\": \"error\", \"error\": \"Description\", \"timestamp\": \"ISO-8601\"}\n```\n\nNotes:\n- Default sample rate is 24000 Hz for optimal quality/bandwidth balance\n- Config must be sent before audio (optional if using defaults)\n- Server always responds with JSON regardless of protocol\n- Latency is measured once per utterance from speech_started event to the first transcript with content\n- VAD events are enabled by default for latency tracking\n- UtteranceEnd is enabled by default at 1000ms (server-side) to mark end-of-speech; clients cannot configure this\n\n## Session identifier\n\nThe `session_id` returned by this standalone transcription endpoint is an\nephemeral connection UUID. It is not a `CallSession.call_session_id`, has no\n`CallSession.chat_session_id` association, and is not the Arize call\n`session.id` used by the conversation WebSockets.","operationId":"transcription_websocket","tags":["realtime"],"responses":{"101":{"description":"Switching Protocols - WebSocket connection established","content":{"application/json":{"examples":{"connected":{"summary":"Connection established","value":{"type":"connected","session_id":"4a25f11e-aff0-46ae-8859-75f704e68adb","timestamp":"2024-01-01T12:00:00Z"}},"config_ack":{"summary":"Config acknowledged","value":{"type":"config_ack","language":"en","interim_results":false,"timestamp":"2024-01-01T12:00:00Z"}},"transcript":{"summary":"Transcript result","value":{"type":"transcript","text":"Hello world","confidence":0.98,"is_final":true,"language":"en","latency_ms":234.5,"utterance_id":"9f3c2a26-7e5a-4df2-8e4d-1f0b7b6a9f33","tail_latency_ms":87.3,"timestamp":"2024-01-01T12:00:00Z"}},"speech_started":{"summary":"Speech detection event","value":{"type":"speech_started","session_id":"4a25f11e-aff0-46ae-8859-75f704e68adb","utterance_id":"9f3c2a26-7e5a-4df2-8e4d-1f0b7b6a9f33","timestamp":"2024-01-01T12:00:00Z"}},"utterance_end":{"summary":"Utterance end (requires utterance_end_ms)","value":{"type":"utterance_end","utterance_id":"9f3c2a26-7e5a-4df2-8e4d-1f0b7b6a9f33","timestamp":"2024-01-01T12:00:00Z"}},"error":{"summary":"Error message","value":{"type":"error","error":"Audio data too large (max 1MB)","timestamp":"2024-01-01T12:00:00Z"}}}}}}},"parameters":[{"name":"transcription_model","in":"query","required":false,"schema":{"type":"string","enum":["nova-3","flux-general-en","flux-general-multi"],"default":"nova-3"},"description":"Transcription model. Flux uses /v2/listen and built-in turn detection."},{"name":"protocol","in":"query","required":false,"schema":{"type":"string","enum":["json","binary"],"default":"json"},"description":"Frame protocol: json (default) or binary raw PCM."},{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"multi"},"description":"Deepgram language code or multi for automatic detection."},{"name":"sample_rate","in":"query","required":false,"schema":{"type":"integer","enum":[8000,16000,24000,32000,44100,48000],"default":24000},"description":"Input PCM sample rate in Hz."},{"name":"vad_events","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Truthy values keep the default enabled and force interim results. Query-level false does not disable VAD; use the JSON config message."},{"name":"denoise","in":"query","required":false,"schema":{"type":"string","enum":["off","rnnoise"],"default":"off"},"description":"Streaming noise cancellation mode."},{"name":"return_audio","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Return post-denoise PCM audio to the client when denoise is active."},{"name":"turn_detector","in":"query","required":false,"schema":{"type":"string","enum":["builtin","deepgram","smart_turn_v3","livekit_v1","livekit_v1_mini"],"default":"deepgram"},"description":"Turn boundary detector. Which values a transcription_model accepts is declared per model (see MODEL_CAPABILITIES in app/services/transcription/models.py); Flux always uses its built-in detector, gpt-realtime-whisper has no built-in option and requires an external detector."},{"name":"keyterms","in":"query","required":false,"schema":{"type":"string"},"description":"Semicolon-separated Deepgram recognition keyterms."},{"name":"acoustic_wake_word","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Override acoustic Hey Jamb detection for this connection."},{"name":"eot_threshold","in":"query","required":false,"schema":{"type":"number","minimum":0.5,"maximum":0.9},"description":"Deepgram Flux only. Confidence required to commit an EndOfTurn. Higher waits for more certainty, trading latency for fewer premature turn ends. Ignored by non-Flux models. Defaults to FLUX_EOT_THRESHOLD, else 0.7."},{"name":"eager_eot_threshold","in":"query","required":false,"schema":{"type":"number","minimum":0.3,"maximum":0.9},"description":"Deepgram Flux only. Enables speculative EagerEndOfTurn transcripts at this lower confidence; must not exceed the effective eot_threshold. Omitting it is what disables the feature, so there is no 'off' value, and it cannot be disabled mid-stream. Ignored by non-Flux models. Defaults to FLUX_EAGER_EOT_THRESHOLD, else unset."},{"name":"eot_timeout_ms","in":"query","required":false,"schema":{"type":"integer","minimum":500,"maximum":60000},"description":"Deepgram Flux only. Maximum silence before a turn is forced closed. Ignored by non-Flux models. Defaults to FLUX_EOT_TIMEOUT_MS, else 5000."}]}},"/api/v1/realtime/conversation/ws":{"summary":"Real-time Conversation WebSocket","description":"Bidirectional streaming conversation agent with Google ADK integration","operationId":"conversation_websocket","get":{"tags":["realtime"],"summary":"Real-time Conversation WebSocket","description":"WebSocket endpoint for bidirectional conversation streaming using Google ADK.\n\n## Overview\nThis endpoint provides real-time bidirectional conversation capabilities with Google ADK integration,\nsupporting both text and audio input/output. It mirrors the functionality of the REST API endpoints\n`/api/conversation/update` and `/api/conversation/response` but with WebSocket streaming.\n\n## Connection\nWebSocket URL: `wss://host/api/v1/realtime/conversation/ws?session_id={chat_id}&call_id={call_id}`\n\n## Query Parameters\n- `session_id`: Recommended chat session ID. At least one of `session_id` or `session` is required.\n- `session`: Deprecated alias for `session_id`.\n- `call_id`: Optional REST-created call ID. If omitted, the server creates a call\n  for backwards compatibility, but does not return that generated ID over this WebSocket.\n- `is_audio`: Choose client input mode: audio when true, client-forwarded text when false. Agent output remains audio in both modes.\n- `skip_intro`: Retained for backwards compatibility (default: false)\n- `sample_rate`: Input audio sample rate in Hz (default: 16000)\n- `activity_signal_provider`: `deepgram` (default), `smart_turn_v3`, `livekit_v1`, or `livekit_v1_mini`; transcription-first mode only\n- `transcription_first`: Override the server input-mode default for this call\n- `proactive_audio`: Override proactive Gemini Live output (deployment default from `AGENT_PROACTIVE_AUDIO`)\n\n## Voice Configuration\n- **Environment Variable**: `REALTIME_VOICE` (default: Zephyr)\n- **Available voices**: Puck, Charon, Kore, Fenrir, Aoede, Zephyr\n\n## Message Format\n\n### Client → Server Messages\n\n#### Conversation Request\n```json\n{\n  \"users\": [\"aci-user1\", \"aci-user2\"],\n  \"messages\": [\n    {\n      \"user\": \"aci-user1\",\n      \"mimetype\": \"text/plain\",\n      \"data\": \"Hello, how can I help you?\",\n      \"metadata\": {\n        \"aci\": \"aci-user1\",\n        \"timestamp\": \"2024-01-01T12:00:00Z\"\n      }\n    },\n    {\n      \"user\": \"aci-user1\",\n      \"mimetype\": \"audio/pcm\",\n      \"data\": \"base64_encoded_pcm_audio\",\n      \"metadata\": {\n        \"sample_rate_hz\": 16000,\n        \"num_channels\": 1,\n        \"encoding\": \"pcm_s16le\"\n      }\n    }\n  ]\n}\n```\n\n### Server → Client Messages\n\n#### Conversation Response\n```json\n{\n  \"messages\": [\n    {\n      \"user\": \"AI\",\n      \"mimetype\": \"text/plain\",\n      \"data\": \"I'd be happy to help you with that task!\",\n      \"metadata\": {\n        \"model\": \"gemini-2.0-flash-exp\",\n        \"timestamp\": \"2024-01-01T12:00:05Z\",\n        \"partial\": false,\n        \"final\": true\n      }\n    },\n    {\n      \"user\": \"AI\",\n      \"mimetype\": \"audio/pcm\",\n      \"data\": \"base64_encoded_pcm_audio_response\",\n      \"metadata\": {\n        \"sample_rate_hz\": 24000,\n        \"num_channels\": 1,\n        \"encoding\": \"pcm_s16le\",\n        \"final\": true\n      }\n    }\n  ]\n}\n```\n\n## Audio Format\n\n### Input Audio (Client → Server)\n- **Encoding**: 16-bit PCM, mono, little-endian\n- **Sample Rate**: 16 kHz\n- **Format**: Base64 encoded in JSON message\n- **MIME Type**: `audio/pcm`\n\n### Output Audio (Server → Client)\n- **Encoding**: 16-bit PCM, mono, little-endian\n- **Sample Rate**: 24 kHz (ADK default)\n- **Format**: Base64 encoded in JSON message\n- **MIME Type**: `audio/pcm`\n\n## Session Management\n\n- Uses Google ADK's DatabaseSessionService for PostgreSQL-backed persistence\n- Sessions are shared with REST API endpoints (`/api/v1/rest/conversation/*`)\n- Session ID (group_id) must be provided via `session` query parameter\n\n## Error Handling\n\n### Error Response Format\n```json\n{\n  \"error\": \"Internal server error: {details}\",\n  \"session_id\": \"group_id\"\n}\n```\n\n## Best Practices\n\n### Audio Streaming\n- Send audio in chunks of 20-50ms for optimal latency\n- Use 16kHz sample rate for input audio\n- Handle 24kHz output audio appropriately\n\n### Session Management\n- Reuse session IDs for conversation continuity\n- Sessions persist across server restarts via PostgreSQL\n\n### Message Flow\n- Send conversation requests with both `users` and `messages` arrays\n- Handle both text and audio responses from the AI\n- Monitor metadata for partial/final response states\n\n## Rate Limits & Resource Constraints\n\n- **Message Rate**: 50 messages per request maximum\n- **Audio Size**: 100MB per audio message (base64 decoded)\n- **Concurrent Connections**: Limited by server resources\n- **Session Persistence**: PostgreSQL-backed via ADK\n\n## Notes\n\n- Full Google ADK integration with Google Search tools\n- Bidirectional streaming with LiveRequestQueue and LiveEvents\n- Session persistence via DatabaseSessionService\n- Supports both text and audio response modalities\n- Compatible with existing REST API conversation endpoints\n\n## Chat session and call IDs\n\n- `chat_session_id` is the long-lived conversation/group identifier stored as\n  `CallSession.chat_session_id`.\n- API `call_id` is the per-call identifier stored as\n  `CallSession.call_session_id`. `POST /api/v1/realtime/call` creates the row\n  that associates one call with its parent chat; one chat can have many calls.\n- The recommended flow is: create the chat, create a call with\n  `chat_session_id`, then connect the appropriate WebSocket with the returned\n  `call_id` and any route-specific parameters shown above.\n- `GET /api/v1/realtime/call/{call_id}` returns the persisted association.\n\n## Arize correlation\n\nWhen Arize tracing is enabled and configured, this handler records the resolved\ncall ID as `session.id`. For a REST-created call, search\n`session.id == call_id`; do not search by the parent chat ID. Each normal\nrealtime turn emits the manual `CHAIN -> AGENT` pair and no LLM\nchild/model/token attributes. Tool-using turns can additionally contain `TOOL`\nspans.","parameters":[{"name":"session_id","in":"query","required":false,"schema":{"type":"string"},"description":"At least one of session_id or the deprecated session alias is required. This is the persistent CallSession.chat_session_id value."},{"name":"session","in":"query","required":false,"schema":{"type":"string"},"description":"Deprecated alias for session_id. At least one of the two is required.","deprecated":true},{"name":"call_id","in":"query","required":false,"schema":{"type":"string"},"description":"Optional CallSession.call_session_id returned by POST /api/v1/realtime/call. If omitted, the server creates a call for backwards compatibility."},{"name":"is_audio","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Choose client input mode: audio when true, client-forwarded text when false. Agent output remains audio in both modes."},{"name":"skip_intro","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Retained for backwards compatibility; no longer changes greeting behavior."},{"name":"sample_rate","in":"query","required":false,"schema":{"type":"integer","default":16000},"description":"Input audio sample rate in Hz."},{"name":"activity_signal_provider","in":"query","required":false,"schema":{"type":"string","enum":["builtin","deepgram","smart_turn_v3","livekit_v1","livekit_v1_mini"],"default":"deepgram"},"description":"Turn boundary provider in transcription-first mode."},{"name":"transcription_first","in":"query","required":false,"schema":{"type":"boolean"},"description":"Override ENABLE_TRANSCRIPTION_FIRST for this call."},{"name":"proactive_audio","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Enable Gemini Live proactive audio for this call."}],"responses":{"101":{"description":"Switching Protocols - WebSocket connection established","content":{"application/json":{"examples":{"conversation_response":{"summary":"AI conversation response","value":{"messages":[{"user":"AI","mimetype":"text/plain","data":"I'd be happy to help you with that task!","metadata":{"model":"gemini-2.0-flash-exp","timestamp":"2024-01-01T12:00:05Z","partial":false,"final":true}}]}},"audio_response":{"summary":"AI audio response","value":{"messages":[{"user":"AI","mimetype":"audio/pcm","data":"base64_encoded_pcm_audio_response","metadata":{"sample_rate_hz":24000,"num_channels":1,"encoding":"pcm_s16le","final":true}}]}},"error":{"summary":"Error message","value":{"error":"Internal server error: connection failed","session_id":"group-abc-123"}}}}}}}}},"/api/v1/realtime/group-conversation/ws":{"summary":"Group Conversation WebSocket (Wake-Word Activated)","description":"Wake-word triggered group voice conversation with ADK integration","operationId":"group_conversation_websocket","get":{"tags":["realtime"],"summary":"Group Conversation WebSocket (Wake-Word Activated)","description":"WebSocket endpoint for wake-word activated group voice conversations.\n\n## Overview\nThis endpoint enables hands-free group voice conversations where the AI agent only responds\nwhen addressed by wake word (\"Hey Jamb\"). Uses transcription-first architecture: audio is\ntranscribed by Deepgram before being sent to ADK, enabling reliable wake word detection.\n\n## Voice Configuration\n- **Environment Variable**: `GROUP_CALL_VOICE` (default: Zephyr)\n- **Available voices**: Puck, Charon, Kore, Fenrir, Aoede, Zephyr\n\n## Connection\nWebSocket URL: `wss://host/api/v1/realtime/group-conversation/ws?session_id={chat_id}&call_id={call_id}`\n\n## Query Parameters\n- `session_id`: Recommended chat session ID. At least one of `session_id` or `session` is required.\n- `session`: Deprecated alias for `session_id`.\n- `call_id`: Optional REST-created call ID. If omitted, the server creates a call\n  for backwards compatibility, but does not return that generated ID over this WebSocket.\n- `is_audio`: Audio input via Deepgram when true; client-forwarded text when false (default: true)\n- `proactive_audio`: Override proactive responses (deployment default from `GROUP_PROACTIVE_AUDIO`)\n- `affective_dialog`: Enable affective dialog (default: false)\n- `protocol`: Optional. `json` (default) or `binary` for raw PCM frames (~25-30% bandwidth savings)\n- `skip_intro`: Retained for backwards compatibility (default: false)\n- `sample_rate`: Input audio sample rate in Hz (default: 16000)\n- `acoustic_wake_word`: Override acoustic Hey Jamb detection for this call\n\n## Message Format\n\n### Client → Server Messages\n\n#### Audio/Text Request\n```json\n{\n  \"messages\": [\n    {\n      \"user\": \"aci-user1\",\n      \"mimetype\": \"audio/pcm\",\n      \"data\": \"base64_encoded_16kHz_pcm\",\n      \"metadata\": {\"sample_rate_hz\": \"16000\"}\n    }\n  ]\n}\n```\n\n#### Keepalive Ping\n```json\n{\"type\": \"ping\"}\n```\n\n### Binary Protocol (protocol=binary)\nSend raw 16kHz 16-bit PCM audio as binary WebSocket frames for ~25-30% bandwidth savings.\n```javascript\n// Audio: Direct binary transmission\nws.send(pcmAudioBuffer);  // ArrayBuffer with raw PCM bytes\n\n// Control messages: Still JSON\nws.send(JSON.stringify({type: \"ping\"}));\n```\n\n**Validation:**\n- Maximum frame size: 1MB\n- PCM format: 16-bit (even byte count required)\n- Invalid frames return JSON error response\n\n### Server → Client Messages\n\n#### Keepalive Pong\n```json\n{\"type\": \"pong\"}\n```\n\n#### Error Response\n```json\n{\n  \"error\": \"Invalid JSON: ...\",\n  \"session_id\": \"group_id\"\n}\n```\n\n## Audio Format\n\n### Input Audio (Client → Server)\n- **Encoding**: 16-bit PCM, mono, little-endian\n- **Sample Rate**: 16 kHz (required for Deepgram transcription)\n- **Format**: Base64 encoded in JSON message\n- **MIME Type**: `audio/pcm`\n\n### Output Audio (Server → Client)\n- **Encoding**: 16-bit PCM, mono, little-endian\n- **Sample Rate**: 24 kHz (ADK default)\n- **Format**: Base64 encoded in JSON message\n- **MIME Type**: `audio/pcm`\n\n## Session Validation\n\n- Session ID cannot be empty\n- Maximum length: 255 characters\n- Allowed characters: alphanumeric, hyphens, underscores, dots, colons\n\n## Architecture\nAudio → Deepgram (keyterm boost) → Text → ADK (Gemini Live) → Audio\n\nSee `specs/realtime-group-call.md` for full specification.\n\n## Chat session and call IDs\n\n- `chat_session_id` is the long-lived conversation/group identifier stored as\n  `CallSession.chat_session_id`.\n- API `call_id` is the per-call identifier stored as\n  `CallSession.call_session_id`. `POST /api/v1/realtime/call` creates the row\n  that associates one call with its parent chat; one chat can have many calls.\n- The recommended flow is: create the chat, create a call with\n  `chat_session_id`, then connect the appropriate WebSocket with the returned\n  `call_id` and any route-specific parameters shown above.\n- `GET /api/v1/realtime/call/{call_id}` returns the persisted association.\n\n## Arize correlation\n\nWhen Arize tracing is enabled and configured, this handler records the resolved\ncall ID as `session.id`. For a REST-created call, search\n`session.id == call_id`; do not search by the parent chat ID. Each normal\nrealtime turn emits the manual `CHAIN -> AGENT` pair and no LLM\nchild/model/token attributes. Tool-using turns can additionally contain `TOOL`\nspans.","parameters":[{"name":"session_id","in":"query","required":false,"schema":{"type":"string"},"description":"At least one of session_id or the deprecated session alias is required. This is the persistent CallSession.chat_session_id value."},{"name":"session","in":"query","required":false,"schema":{"type":"string"},"description":"Deprecated alias for session_id. At least one of the two is required.","deprecated":true},{"name":"call_id","in":"query","required":false,"schema":{"type":"string"},"description":"Optional CallSession.call_session_id returned by POST /api/v1/realtime/call. If omitted, the server creates a call for backwards compatibility."},{"name":"is_audio","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Use Deepgram audio transcription when true; accept client text when false."},{"name":"proactive_audio","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Enable proactive audio responses."},{"name":"affective_dialog","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Enable affective dialog."},{"name":"protocol","in":"query","required":false,"schema":{"type":"string","enum":["json","binary"],"default":"json"},"description":"Frame protocol: json or binary raw PCM."},{"name":"skip_intro","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Skip the initial greeting."},{"name":"sample_rate","in":"query","required":false,"schema":{"type":"integer","default":16000},"description":"Input audio sample rate in Hz."},{"name":"acoustic_wake_word","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Override acoustic Hey Jamb detection for this call."},{"name":"turn_protocol","in":"query","required":false,"schema":{"type":"string","enum":["eot-v1"]},"description":"Declare that this client forwards per-fragment turn signals (end_of_turn, source_span_id, utterance_id). Omitted or unrecognized means undeclared."}],"responses":{"101":{"description":"Switching Protocols - WebSocket connection established","content":{"application/json":{"examples":{"pong":{"summary":"Keepalive pong response","value":{"type":"pong"}},"binary_error":{"summary":"Binary frame validation error","value":{"error":"Binary frame too large (max 1MB)","session_id":"group-abc-123"}},"error":{"summary":"JSON parsing error","value":{"error":"Invalid JSON: Expecting value","session_id":"group-abc-123"}}}}}}}}},"/api/v1/realtime/auto-conversation/ws":{"get":{"tags":["realtime"],"summary":"Auto-Conversation WebSocket (AI-Initiated Outbound Call)","description":"WebSocket endpoint for AI-initiated outbound calls where the agent drives the conversation toward a goal.\n\n## Overview\nThe agent calls a recipient on behalf of a caller, collects required information, and terminates the call.\nAlways create a call first via `POST /api/v1/realtime/call` with `auto_call_config`.\n\n## Connection\n```\nws://host/api/v1/realtime/auto-conversation/ws?session={session_id}&call_id={call_id}&is_audio=true&proactive_audio=true\n```\n\n## Query Parameters\n- `session_id`: Recommended chat session ID. At least one of `session_id` or `session` is required.\n- `session`: Deprecated alias for `session_id`.\n- `call_id`: REST-created call ID. Required to load `auto_call_config`; omission is supported only as a legacy unconfigured-call fallback.\n- `is_audio`: Enable Deepgram transcription (default: true)\n- `proactive_audio`: Agent speaks first without waiting for user input (deployment default from `AUTOCALL_PROACTIVE_AUDIO`)\n- `affective_dialog`: Enable affective dialog (default: false)\n- `protocol`: `json` (default) or `binary`\n- `sample_rate`: Input audio sample rate in Hz (default: 16000)\n\n## Voice Configuration\n- **Environment Variable**: `AUTO_CALL_VOICE` (default: Schedar)\n- **Available voices**: Puck, Charon, Kore, Fenrir, Aoede, Leda, Orus, Schedar, Zephyr\n\n## Setup Flow\n1. `PUT /api/v1/rest/conversation/{session_id}` with caller + recipient + agent participants\n2. `POST /api/v1/realtime/call` with `auto_call_config: {callers, target_recipient, call_reason, required_info}`\n3. Connect to this WebSocket\n\n## Client → Server Messages\n```json\n{\"messages\": [{\"user\": \"caller-id\", \"mimetype\": \"audio/pcm\", \"data\": \"<base64 16kHz PCM>\",\n  \"metadata\": {\"sample_rate_hz\": \"16000\", \"encoding\": \"pcm_s16le\"}}]}\n{\"type\": \"ping\"}\n```\n\n## Server → Client Messages\n```json\n{\"messages\": [{\"user\": \"AI\", \"mimetype\": \"audio/pcm\", \"data\": \"<base64 24kHz PCM>\", \"metadata\": {...}}]}\n{\"type\": \"control\", \"value\": \"terminate\", \"collected_info\": [{\"question\": \"...\", \"answer\": \"...\"}]}\n{\"type\": \"goal_update\", \"question\": \"...\", \"answer\": \"...\", \"all_collected\": false}\n{\"type\": \"pong\"}\n```\n\n## Chat session and call IDs\n\n- `chat_session_id` is the long-lived conversation/group identifier stored as\n  `CallSession.chat_session_id`.\n- API `call_id` is the per-call identifier stored as\n  `CallSession.call_session_id`. `POST /api/v1/realtime/call` creates the row\n  that associates one call with its parent chat; one chat can have many calls.\n- The recommended flow is: create the chat, create a call with\n  `chat_session_id`, then connect the appropriate WebSocket with the returned\n  `call_id` and any route-specific parameters shown above.\n- `GET /api/v1/realtime/call/{call_id}` returns the persisted association.\n\n## Arize correlation\n\nWhen Arize tracing is enabled and configured, this handler establishes the call\nID as `session.id` for instrumented child spans, but it does not emit the manual\nrealtime `CHAIN -> AGENT` turn pair used by the conversation and\ngroup-conversation handlers.","operationId":"auto_conversation_websocket","parameters":[{"name":"session_id","in":"query","required":false,"schema":{"type":"string"},"description":"At least one of session_id or the deprecated session alias is required. This is the persistent CallSession.chat_session_id value."},{"name":"session","in":"query","required":false,"schema":{"type":"string"},"description":"Deprecated alias for session_id. At least one of the two is required.","deprecated":true},{"name":"call_id","in":"query","required":false,"schema":{"type":"string"},"description":"CallSession.call_session_id returned by POST /api/v1/realtime/call. Required to load auto_call_config; omission is a legacy fallback."},{"name":"is_audio","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Enable Deepgram transcription of caller audio."},{"name":"proactive_audio","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Let the agent speak first without waiting for user input."},{"name":"affective_dialog","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Enable affective dialog."},{"name":"protocol","in":"query","required":false,"schema":{"type":"string","enum":["json","binary"],"default":"json"},"description":"Frame protocol: json or binary raw PCM."},{"name":"sample_rate","in":"query","required":false,"schema":{"type":"integer","default":16000},"description":"Input audio sample rate in Hz."}],"responses":{"101":{"description":"Switching Protocols - WebSocket connection established","content":{"application/json":{"examples":{"terminate":{"summary":"Agent finished — terminate signal with collected info","value":{"type":"control","value":"terminate","collected_info":[{"question":"Store hours","answer":"9am-6pm Mon-Fri"}]}},"goal_update":{"summary":"Real-time goal update","value":{"type":"goal_update","question":"Store hours","answer":"9am-6pm Mon-Fri","all_collected":false}}}}}}}}},"/api/v1/realtime/voicemail/ws":{"get":{"tags":["realtime"],"summary":"Voicemail WebSocket (AI-Answered Voicemail Call)","description":"WebSocket endpoint for AI-answered voicemail calls.\n\n## Overview\nWhen a Jamb user doesn't answer a call, their AI agent joins in voicemail mode: greets the caller,\nasks them to leave a message, listens, and thanks them.\nAlways create a call first via `POST /api/v1/realtime/call` with `voicemail_config`.\n\n## Connection\n```\nws://host/api/v1/realtime/voicemail/ws?call_id={call_id}&session_id={session_id}\n```\n\n## Query Parameters\n- `call_id`: **Required**. Call ID from `POST /api/v1/realtime/call`\n- `session_id`: Chat session ID (optional — inferred from call if omitted)\n- `is_audio`: Enable Deepgram transcription of caller audio (default: true)\n- `sample_rate`: Input audio sample rate in Hz (default: 16000)\n\n## Voice Configuration\n- **Environment Variable**: `VOICEMAIL_VOICE` (default: Zephyr, same as group call)\n- **Available voices**: Puck, Charon, Kore, Fenrir, Aoede, Zephyr\n\n## Setup Flow\n1. `PUT /api/v1/rest/conversation/{session_id}` with receiver + caller + agent participants\n2. `POST /api/v1/realtime/call` with `voicemail_config: {receiver_name, caller_name, pstn, ...}`\n3. Connect to this WebSocket\n\n## voicemail_config Fields\n- `receiver_id` (string, optional): UUID of the person whose voicemail is being answered\n- `receiver_name` (string): Person whose voicemail is being answered\n- `receiver_group` (array, optional): Absent members for multi-person groups. Each: `{id, name}`.\n- `caller_id` (string, optional): UUID of the caller\n- `caller_name` (string): Person leaving the message\n- `group_id` (string, optional): Signal group identifier\n- `pstn` (boolean, optional): Caller is on a phone line\n\n## Client → Server Messages\n```json\n{\"messages\": [{\"user\": \"caller-id\", \"mimetype\": \"audio/pcm\", \"data\": \"<base64 16kHz PCM>\",\n  \"metadata\": {\"sample_rate_hz\": \"16000\", \"encoding\": \"pcm_s16le\"}}]}\n{\"type\": \"ping\"}\n```\n\n## Server → Client Messages\n```json\n{\"messages\": [{\"user\": \"AI\", \"mimetype\": \"audio/pcm\", \"data\": \"<base64 24kHz PCM>\", \"metadata\": {...}}]}\n{\"type\": \"pong\"}\n```\n\n**Note**: The voicemail agent does not send a terminate signal. The server closes the WebSocket\nafter the agent's farewell, and the caller hangs up naturally.\n\n## Chat session and call IDs\n\n- `chat_session_id` is the long-lived conversation/group identifier stored as\n  `CallSession.chat_session_id`.\n- API `call_id` is the per-call identifier stored as\n  `CallSession.call_session_id`. `POST /api/v1/realtime/call` creates the row\n  that associates one call with its parent chat; one chat can have many calls.\n- The recommended flow is: create the chat, create a call with\n  `chat_session_id`, then connect the appropriate WebSocket with the returned\n  `call_id` and any route-specific parameters shown above.\n- `GET /api/v1/realtime/call/{call_id}` returns the persisted association.\n\n## Arize correlation\n\nWhen Arize tracing is enabled and configured, this handler establishes the call\nID as `session.id` for instrumented child spans, but it does not emit the manual\nrealtime `CHAIN -> AGENT` turn pair used by the conversation and\ngroup-conversation handlers.","operationId":"voicemail_websocket","parameters":[{"name":"call_id","in":"query","required":true,"schema":{"type":"string"},"description":"Required CallSession.call_session_id returned by POST /api/v1/realtime/call; the call must contain voicemail_config."},{"name":"session_id","in":"query","required":false,"schema":{"type":"string"},"description":"Optional CallSession.chat_session_id; inferred from the call row when omitted."},{"name":"is_audio","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Enable audio input."},{"name":"sample_rate","in":"query","required":false,"schema":{"type":"integer","default":16000},"description":"Input audio sample rate in Hz."}],"responses":{"101":{"description":"Switching Protocols - WebSocket connection established","content":{"application/json":{"examples":{"audio":{"summary":"Agent audio response","value":{"messages":[{"user":"AI","mimetype":"audio/pcm","data":"<base64>","metadata":{"sample_rate_hz":"24000","num_channels":"1","encoding":"pcm_s16le"}}]}}}}}}}}},"/api/v1/realtime/transcriber/ws":{"get":{"tags":["realtime"],"summary":"Transcriber WebSocket (Meeting Note-Taker)","description":"WebSocket endpoint for a meeting transcriber.\n\n## Overview\nThe agent transcribes the room and stays out of the way, answering only when it judges it\nwas addressed. It is the V2 group transport and the V2 group agent -- the same turn model\nand the same `should_ai_respond` gate -- reached by a route that says which mode the caller\npicked (jamb-project#487).\n\n**V2 is a group by transport, not by headcount.** An in-person meeting is one device in a\nroom with several people talking into it: one registered participant, and unmistakably a\ngroup. So this route does not require two humans, and memory is session-scoped\naccordingly -- what is said in a meeting belongs to the meeting.\n\n**Speakers are not distinguished.** With room audio and no attributed transcripts the\nserver can tell the human side from the agent side and nothing finer. Transcripts show\nWHAT was said, not WHO said it.\n\n## The opening announcement\nThe agent says one short sentence when the call starts -- that it is transcribing, and that\nit answers when addressed by name -- then returns to its normal gate and is silent unless\nit judges it was addressed.\n\nIt announces **once per `call_id`, not once per connection.** ADK session resumption means\na dropped socket resumes the same call, so the delivered announcement is recorded on the\ncall session and a reconnect does not repeat it. Without a `call_id` there is no identity by\nwhich a reconnect could be recognized, so the announcement falls back to per-connection.\n\n`skip_intro` is deliberately NOT accepted here. A caller who wants the announcement\nsuppressed wants `/api/v2/realtime/group-conversation/ws` instead.\n\n`proactive_audio` is not accepted either, and is forced on. V2's turn gate depends on it:\n`should_ai_respond` approves a turn by returning a function response, and proactivity is what\nlets the model continue from that response into speech. A connection that turned it off would\nget an agent that decides to answer and then says nothing -- indistinguishable, from the\ncaller's side, from the wake word not working at all.\n\n## Connection\n```\nws://host/api/v1/realtime/transcriber/ws?session_id={session_id}&call_id={call_id}&is_audio=true\n```\n\n## Query Parameters\n- `session_id`: Chat session ID (group_id). Required unless the deprecated `session` is given\n- `session`: **Deprecated** alias for `session_id`\n- `call_id`: Call ID from `POST /api/v1/realtime/call`. Strongly recommended -- it is what\n  makes the announcement once-per-call rather than once-per-connection\n- `is_audio`: Native Gemini room audio (default: true)\n- `affective_dialog`: Affective dialog (default: false)\n- `protocol`: `json` or `binary` (default: json). **Inbound only.** Under `binary` the client\n  may send raw PCM frames instead of JSON envelopes; it does not change the SERVER's framing,\n  which is JSON in both cases. There is no outbound binary path on this transport --\n  `group_agent_to_client_messaging` calls `send_json` unconditionally\n- `sample_rate`: Input audio sample rate in Hz (default: 16000)\n- `acoustic_wake_word`: Override the acoustic wake-word detector (default: server setting).\n  The detector matters here because ASR mangles the wake phrase often enough (\"Hey Jamm\",\n  \"Hey Jim\") that the model's own report of what it heard loses turns\n- `turn_protocol`: Declare per-fragment turn signals (`eot-v1`)\n- `debug_tool_calls`: Surface `should_ai_respond` calls and decisions to the client as\n  System transcript entries, for debugging turn gating (default: false)\n\n## Refusals\nClosed at connect with `1008` for an empty, over-long or malformed `session_id`, a\nmalformed `call_id`, or `Call session not found: {call_id}`. These are **WebSocket close\ncodes, not HTTP statuses**, so they are documented here rather than under `responses` -- an\nOpenAPI Responses Object may only be keyed by HTTP status.\n\n## Client -> Server Messages\n```json\n{\"messages\": [{\"user\": \"caller-id\", \"mimetype\": \"audio/pcm\", \"data\": \"<base64 16kHz PCM>\",\n  \"metadata\": {\"sample_rate_hz\": \"16000\", \"encoding\": \"pcm_s16le\"}}]}\n{\"type\": \"ping\"}\n```\n\n## Server -> Client Messages\n```json\n{\"messages\": [{\"user\": \"AI\", \"mimetype\": \"audio/pcm\", \"data\": \"<base64 24kHz PCM>\", \"metadata\": {...}}]}\n{\"type\": \"pong\"}\n```","operationId":"transcriber_websocket","parameters":[{"name":"session_id","in":"query","required":false,"schema":{"type":"string"},"description":"Chat session ID (group_id). Required unless the deprecated `session` is given"},{"name":"session","in":"query","required":false,"schema":{"type":"string"},"description":"[Deprecated] Use session_id instead"},{"name":"call_id","in":"query","required":false,"schema":{"type":"string"},"description":"Call ID from POST /api/v1/realtime/call. Makes the announcement once-per-call rather than once-per-connection"},{"name":"is_audio","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Native Gemini room audio"},{"name":"affective_dialog","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Enable affective dialog"},{"name":"protocol","in":"query","required":false,"schema":{"type":"string","enum":["json","binary"],"default":"json"},"description":"Under `binary`, agent PCM arrives as raw frames rather than JSON envelopes"},{"name":"sample_rate","in":"query","required":false,"schema":{"type":"integer","default":16000},"description":"Input audio sample rate in Hz"},{"name":"acoustic_wake_word","in":"query","required":false,"schema":{"type":"boolean"},"description":"Override the acoustic wake-word detector"},{"name":"turn_protocol","in":"query","required":false,"schema":{"type":"string","enum":["eot-v1"]},"description":"Declare per-fragment turn signals"},{"name":"debug_tool_calls","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Surface should_ai_respond calls and decisions to the client for debugging turn gating"}],"responses":{"101":{"description":"Switching Protocols - WebSocket connection established","content":{"application/json":{"examples":{"audio":{"summary":"Agent audio (the opening announcement, or an answer when addressed)","value":{"messages":[{"user":"AI","mimetype":"audio/pcm","data":"<base64>","metadata":{"sample_rate_hz":"24000","num_channels":"1","encoding":"pcm_s16le"}}]}}}}}}}}},"/api/v1/realtime/translation/ws":{"get":{"tags":["realtime"],"summary":"Translation WebSocket (Live Interpreter)","description":"WebSocket endpoint for a live turn-taking interpreter.\n\n## Overview\nThe call opens **directly in interpreter behavior**. The caller never asks for translation:\nthe endpoint IS the mode, so there is no handoff, no acknowledgement beep, and no wake-word\nstyle state machine. The interpreter's first spoken turn asks which two languages to\ntranslate between; from then on it renders each completed turn in the other language,\nafter the speaker finishes rather than over them.\n\nIt answers nothing, obeys nothing said in the room, and adds nothing of its own.\n\nChosen from the call-mode menu on the mobile call button (jamb-project#487). The older\nverbal route — saying \"help us translate\" mid-call on `/conversation/ws` — still works and\nreaches the same interpreter; this endpoint does not replace it.\n\n## Connection\n```\nws://host/api/v1/realtime/translation/ws?session_id={session_id}&is_audio=true&transcription_first=false\n```\n\n## Query Parameters\n- `session_id`: Chat session ID (group_id). Required unless the deprecated `session` is given\n- `session`: **Deprecated** alias for `session_id`\n- `call_id`: Call ID from `POST /api/v1/realtime/call`, when the call was created server-side\n- `is_audio`: Native-audio transport (default: true). **Must be true**\n- `sample_rate`: Input audio sample rate in Hz (default: 16000)\n- `transcription_first`: Override `ENABLE_TRANSCRIPTION_FIRST`. **Must resolve false**\n\n## Transport requirement\nThe interpreter runs only on the native-audio transport, so a connection that would be\ntranscription-first, or that sends no audio, is **refused at connect** rather than accepted\nand left silent:\n\n```\n1008 \"translation requires native audio\"\n```\n\nOther refusals, all `1008`: empty / over-long / malformed `session_id`, malformed `call_id`,\nand `Call session not found: {call_id}` when a supplied `call_id` has no call record.\n\nThese are **WebSocket close codes, not HTTP statuses**, so they are documented here rather\nthan under `responses` — an OpenAPI Responses Object may only be keyed by HTTP status, and a\n`1008` key there is rejected by strict validators and client generators.\n\n## Language pair\nAgreed **by voice**, not on the wire. There is no language query parameter and no picker:\nthe opening turn asks, and the agent reports the agreed pair back through its\n`set_language_pair` tool so a pair-specific reinforcement can be sent. Once agreed the pair\nis fixed for the call.\n\n## One-way\nTranslation mode does not transfer back. Leaving it means ending the call.\n\n## Client → Server Messages\n```json\n{\"messages\": [{\"user\": \"caller-id\", \"mimetype\": \"audio/pcm\", \"data\": \"<base64 16kHz PCM>\",\n  \"metadata\": {\"sample_rate_hz\": \"16000\", \"encoding\": \"pcm_s16le\"}}]}\n{\"type\": \"ping\"}\n```\n\n## Server → Client Messages\n```json\n{\"messages\": [{\"user\": \"AI\", \"mimetype\": \"audio/pcm\", \"data\": \"<base64 24kHz PCM>\", \"metadata\": {...}}]}\n{\"type\": \"pong\"}\n```\n\n**Note**: silence on a turn that carried speech is never correct here, so the server arms a\nbounded re-cue when a completed caller turn goes unrendered.","operationId":"translation_websocket","parameters":[{"name":"session_id","in":"query","required":false,"schema":{"type":"string"},"description":"Chat session ID (group_id). Required unless the deprecated `session` is given"},{"name":"session","in":"query","required":false,"schema":{"type":"string"},"description":"[Deprecated] Use session_id instead"},{"name":"call_id","in":"query","required":false,"schema":{"type":"string"},"description":"Call ID from POST /api/v1/realtime/call"},{"name":"is_audio","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Native-audio transport. Must be true — the interpreter is refused 1008 otherwise"},{"name":"sample_rate","in":"query","required":false,"schema":{"type":"integer","default":16000},"description":"Input audio sample rate in Hz"},{"name":"transcription_first","in":"query","required":false,"schema":{"type":"boolean"},"description":"Override ENABLE_TRANSCRIPTION_FIRST. Must resolve false — a true is refused, not honored"}],"responses":{"101":{"description":"Switching Protocols - WebSocket connection established","content":{"application/json":{"examples":{"audio":{"summary":"Interpreter audio rendering of a completed turn","value":{"messages":[{"user":"AI","mimetype":"audio/pcm","data":"<base64>","metadata":{"sample_rate_hz":"24000","num_channels":"1","encoding":"pcm_s16le"}}]}}}}}}}}},"/api/v1/realtime/answering-service/ws":{"get":{"tags":["realtime"],"summary":"Answering Service WebSocket (Inbound PSTN AI Receptionist)","description":"WebSocket endpoint for the Answering Service inbound-PSTN AI receptionist.\n\n## Overview\nAn inbound PSTN call forwarded to a Jamb business number is answered by an AI receptionist\nconfigured per-org via the `Instruction` resource (see `POST /api/v1/rest/instruction`).\nThe agent greets the caller, handles FAQs from the instruction, and — once Stage 4 lands —\nemits terminal `function` events (`redirectCall`, `notifyVoicemail`, `endCall`) for the\nclient (jamb-signal-cli) to act on.\n\n## Connection\n```\nws://host/api/v1/realtime/answering-service/ws?call_id={call_id}&instruction_id={instruction_id}&caller_phone_number={e164}&diversion_number={e164}\n```\n\nAll four query parameters are **required** and validated at connect time. A `call_session`\nrow keyed by `call_id` must already exist; see the call-session service.\n\n## Setup Flow\n1. `POST /api/v1/rest/instruction` (or pre-existing) — Instruction record for the diversion number.\n2. Create the `call_session` row keyed by `call_id`.\n3. Connect to this WebSocket.\n\n## Client → Server Messages\n```json\n{\"type\": \"audio\", \"data\": \"<base64 16kHz mono PCM>\"}\n```\nOther inbound types (`control`, `function_response`) are silently ignored at Stage 3 and\ngain real handling in Stage 4 (#502).\n\n## Server → Client Messages\n```json\n{\"type\": \"audio\", \"data\": \"<base64 16kHz mono PCM>\"}\n```\nStage 4 adds `function` events with the payload defined in\n`specs/answering-service-realtime-agent.md`.\n\n## Close Codes\n- `1000` — normal close (graceful shutdown).\n- `4400 missing_param:<name>` — a required query param was empty.\n- `4400 invalid_call_id` — `call_id` is not a UUID.\n- `4400 invalid_instruction_id` — `instruction_id` is not a UUID.\n- `4404 instruction_not_found` — no Instruction row matches `instruction_id`.\n- `4404 diversion_mismatch` — Instruction's `diversion_number` does not match the query param.\n- `4404 call_session_not_found` — no `call_session` row keyed by `call_id`.\n- `4408` — (Stage 4) `function_response` ack timeout (60 s).\n\n## Chat session and call IDs\n\n- `chat_session_id` is the long-lived conversation/group identifier stored as\n  `CallSession.chat_session_id`.\n- API `call_id` is the per-call identifier stored as\n  `CallSession.call_session_id`. `POST /api/v1/realtime/call` creates the row\n  that associates one call with its parent chat; one chat can have many calls.\n- The recommended flow is: create the chat, create a call with\n  `chat_session_id`, then connect the appropriate WebSocket with the returned\n  `call_id` and any route-specific parameters shown above.\n- `GET /api/v1/realtime/call/{call_id}` returns the persisted association.\n\n## Arize correlation\n\nThe call/chat association is available through the `CallSession` row, but this\nhandler does not currently establish an Arize `session.id` context or emit the\nmanual realtime `CHAIN -> AGENT` turn pair.","operationId":"answering_service_websocket","parameters":[{"name":"call_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Call session id from POST /api/v1/realtime/call"},{"name":"instruction_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Instruction record UUID"},{"name":"caller_phone_number","in":"query","required":true,"schema":{"type":"string"},"description":"Inbound caller's E.164 number"},{"name":"diversion_number","in":"query","required":true,"schema":{"type":"string"},"description":"The Jamb business number the call was forwarded to (E.164); must match the Instruction's diversion_number"}],"responses":{"101":{"description":"Switching Protocols - WebSocket connection established","content":{"application/json":{"examples":{"agent_audio":{"summary":"Agent audio frame","value":{"type":"audio","data":"<base64 16kHz PCM>"}}}}}}}}},"/api/v2/realtime/group-conversation/ws":{"summary":"V2 Group Conversation WebSocket","description":"Turn-gated group conversation using a dedicated V2 agent and API.","operationId":"group_conversation_v2_websocket","get":{"tags":["realtime"],"summary":"V2 Group Conversation WebSocket","description":"WebSocket endpoint for wake-word activated group voice conversations.\n\n## Overview\nThis endpoint enables hands-free group voice conversations where the AI agent only responds\nwhen addressed by wake word (\"Hey Jamb\"). Uses transcription-first architecture: audio is\ntranscribed by Deepgram before being sent to ADK, enabling reliable wake word detection.\n\n## Voice Configuration\n- **Environment Variable**: `GROUP_CALL_VOICE` (default: Zephyr)\n- **Available voices**: Puck, Charon, Kore, Fenrir, Aoede, Zephyr\n\n## Connection\nWebSocket URL: `wss://host/api/v2/realtime/group-conversation/ws?session_id={chat_id}&call_id={call_id}`\n\n## Query Parameters\n- `session_id`: Recommended chat session ID. At least one of `session_id` or `session` is required.\n- `session`: Deprecated alias for `session_id`.\n- `call_id`: Optional REST-created call ID. If omitted, the server creates a call\n  for backwards compatibility, but does not return that generated ID over this WebSocket.\n- `is_audio`: Audio input via Deepgram when true; client-forwarded text when false (default: true)\n- `proactive_audio`: Override proactive responses (deployment default from `GROUP_PROACTIVE_AUDIO`)\n- `affective_dialog`: Enable affective dialog (default: false)\n- `protocol`: Optional. `json` (default) or `binary` for raw PCM frames (~25-30% bandwidth savings)\n- `skip_intro`: Retained for backwards compatibility (default: false)\n- `sample_rate`: Input audio sample rate in Hz (default: 16000)\n- `acoustic_wake_word`: Override acoustic Hey Jamb detection for this call\n\n## Message Format\n\n### Client → Server Messages\n\n#### Audio/Text Request\n```json\n{\n  \"messages\": [\n    {\n      \"user\": \"aci-user1\",\n      \"mimetype\": \"audio/pcm\",\n      \"data\": \"base64_encoded_16kHz_pcm\",\n      \"metadata\": {\"sample_rate_hz\": \"16000\"}\n    }\n  ]\n}\n```\n\n#### Keepalive Ping\n```json\n{\"type\": \"ping\"}\n```\n\n### Binary Protocol (protocol=binary)\nSend raw 16kHz 16-bit PCM audio as binary WebSocket frames for ~25-30% bandwidth savings.\n```javascript\n// Audio: Direct binary transmission\nws.send(pcmAudioBuffer);  // ArrayBuffer with raw PCM bytes\n\n// Control messages: Still JSON\nws.send(JSON.stringify({type: \"ping\"}));\n```\n\n**Validation:**\n- Maximum frame size: 1MB\n- PCM format: 16-bit (even byte count required)\n- Invalid frames return JSON error response\n\n### Server → Client Messages\n\n#### Keepalive Pong\n```json\n{\"type\": \"pong\"}\n```\n\n#### Error Response\n```json\n{\n  \"error\": \"Invalid JSON: ...\",\n  \"session_id\": \"group_id\"\n}\n```\n\n## Audio Format\n\n### Input Audio (Client → Server)\n- **Encoding**: 16-bit PCM, mono, little-endian\n- **Sample Rate**: 16 kHz (required for Deepgram transcription)\n- **Format**: Base64 encoded in JSON message\n- **MIME Type**: `audio/pcm`\n\n### Output Audio (Server → Client)\n- **Encoding**: 16-bit PCM, mono, little-endian\n- **Sample Rate**: 24 kHz (ADK default)\n- **Format**: Base64 encoded in JSON message\n- **MIME Type**: `audio/pcm`\n\n## Session Validation\n\n- Session ID cannot be empty\n- Maximum length: 255 characters\n- Allowed characters: alphanumeric, hyphens, underscores, dots, colons\n\n## Architecture\nAudio → Deepgram (keyterm boost) → Text → ADK (Gemini Live) → Audio\n\nSee `specs/realtime-group-call.md` for full specification.\n\n## Chat session and call IDs\n\n- `chat_session_id` is the long-lived conversation/group identifier stored as\n  `CallSession.chat_session_id`.\n- API `call_id` is the per-call identifier stored as\n  `CallSession.call_session_id`. `POST /api/v1/realtime/call` creates the row\n  that associates one call with its parent chat; one chat can have many calls.\n- The recommended flow is: create the chat, create a call with\n  `chat_session_id`, then connect the appropriate WebSocket with the returned\n  `call_id` and any route-specific parameters shown above.\n- `GET /api/v1/realtime/call/{call_id}` returns the persisted association.\n\n## Arize correlation\n\nWhen Arize tracing is enabled and configured, this handler records the resolved\ncall ID as `session.id`. For a REST-created call, search\n`session.id == call_id`; do not search by the parent chat ID. Each normal\nrealtime turn emits the manual `CHAIN -> AGENT` pair and no LLM\nchild/model/token attributes. Tool-using turns can additionally contain `TOOL`\nspans.","parameters":[{"name":"session_id","in":"query","required":false,"schema":{"type":"string"},"description":"At least one of session_id or the deprecated session alias is required. This is the persistent CallSession.chat_session_id value."},{"name":"session","in":"query","required":false,"schema":{"type":"string"},"description":"Deprecated alias for session_id. At least one of the two is required.","deprecated":true},{"name":"call_id","in":"query","required":false,"schema":{"type":"string"},"description":"Optional CallSession.call_session_id returned by POST /api/v1/realtime/call. If omitted, the server creates a call for backwards compatibility."},{"name":"is_audio","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Use Deepgram audio transcription when true; accept client text when false."},{"name":"proactive_audio","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Accepted for compatibility; the V2 agent always enables proactivity."},{"name":"affective_dialog","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Enable affective dialog."},{"name":"protocol","in":"query","required":false,"schema":{"type":"string","enum":["json","binary"],"default":"json"},"description":"Frame protocol: json or binary raw PCM."},{"name":"skip_intro","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Skip the initial greeting."},{"name":"sample_rate","in":"query","required":false,"schema":{"type":"integer","default":16000},"description":"Input audio sample rate in Hz."},{"name":"acoustic_wake_word","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Override acoustic Hey Jamb detection for this call."},{"name":"turn_protocol","in":"query","required":false,"schema":{"type":"string","enum":["eot-v1"]},"description":"Declare that this client forwards per-fragment turn signals (end_of_turn, source_span_id, utterance_id). Omitted or unrecognized means undeclared."},{"name":"debug_tool_calls","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Surface should_ai_respond calls/decisions to the client as System transcript entries, for debugging turn gating."}],"responses":{"101":{"description":"Switching Protocols - WebSocket connection established","content":{"application/json":{"examples":{"pong":{"summary":"Keepalive pong response","value":{"type":"pong"}},"binary_error":{"summary":"Binary frame validation error","value":{"error":"Binary frame too large (max 1MB)","session_id":"group-abc-123"}},"error":{"summary":"JSON parsing error","value":{"error":"Invalid JSON: Expecting value","session_id":"group-abc-123"}}}}}}},"operationId":"group_conversation_v2_websocket"}}},"components":{"schemas":{"AgentIdentityResponse":{"properties":{"given":{"type":"string","title":"Given","description":"The agent's given name, e.g. 'Jamb'.","examples":["Jamb"]},"family":{"type":"string","title":"Family","description":"The agent's family name, e.g. 'Agent'.","examples":["Agent"]},"wakeword_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Wakeword Model","description":"Registry key of the acoustic wake-word model this deployment runs, or null when it runs none and detection is transcript-only. Null does NOT mean the agent has no wake phrase — read `wake_phrase` for that, which is served either way.","examples":["hey_jamb"]},"wake_phrase":{"type":"string","title":"Wake Phrase","description":"The phrase that wakes the agent, whether or not an acoustic model backs it. Not derivable from the name — a deployment can run a wake-word model whose phrase is unrelated to it, so read this rather than composing one.","examples":["Hey Jamb"]},"agent_profile_prefixes":{"items":{"type":"string"},"type":"array","title":"Agent Profile Prefixes","description":"Every display name a client should still recognise as this agent, current first. Signal profiles do not backfill, so an agent that has not re-asserted its profile since a rename still publishes an earlier name — a client matching only the current pair would stop recognising it. This list GROWS across a rename and is never replaced. Reading it is what lets a client hold no persona of its own (jamb-ai/jamb-project#441).","examples":[["Jamb Agent","Donna AI","JambX Agent"]]},"avatar_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Avatar Url","description":"Where to fetch the agent's avatar, as a ROOT-RELATIVE path to be resolved against this API's base URL — never an absolute URL to another host, so a client can send it the same credentials it sends every other call here. Null when this deployment has no readable avatar file, which reads as 'publish no avatar', never as 'clear the one that is published'. Always present as a key: a missing key means a server that predates the field.","examples":["/api/v1/rest/agent-avatar"]}},"type":"object","required":["given","family","wakeword_model","wake_phrase","agent_profile_prefixes","avatar_url"],"title":"AgentIdentityResponse","description":"The agent's identity as its consumers need to render it.\n\nA ``{given, family}`` pair rather than one display string because each\nconsumer projects it differently: ``jamb-signal-cli`` writes a Signal\nprofile with two separate fields, the Telnyx speaker label and the mobile\ndisplay name render ``given + family``, mobile's short name and this\nserver's own prompts render ``given`` alone, and mobile's avatar renders\nthe initials. Flattening to ``\"Jamb Agent\"`` loses the split\nirreversibly — nothing downstream can recover where the boundary was.\n\nThe wake fields ride along because they are the same question asked of the\nsame deployment (\"who is this agent, and what do I call it?\"), and because\nthe wake phrase is deliberately NOT derivable from the name — the\n``jamb_agent`` registry entry's phrase is the agent's full name, not\n``\"Hey \" + given``.\n\nOf the two wake fields only the *model* is optional. The phrase is what the\ntranscript gate matches on, so it is load-bearing whether or not an acoustic\nclassifier backs it; on the transcript-driven paths the classifier is a\nrecall backstop for finals where the ASR dropped the phrase. A deployment\nserving those can therefore answer to a wake phrase it has no trained model\nfor, and this schema has to be able to say so — hence\n``wakeword_model: str | None`` with ``wake_phrase`` required. (Native-audio\ngroup calls are the exception and reject a model-less entry outright: with\nno Deepgram transcript there is no gate to fall back on.)\n\nEvery description and example below is interpolated from the live config\nrather than written out, so this schema tracks ``WAKEWORD_MODEL`` and the\npersona env pair the way #1000 requires."},"AiMessageRequest":{"properties":{"service_id":{"type":"string","title":"Service Id"},"group_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Group Id"},"text":{"type":"string","title":"Text"}},"type":"object","required":["service_id","text"],"title":"AiMessageRequest"},"ArtifactListResponse":{"properties":{"artifacts":{"items":{"$ref":"#/components/schemas/ArtifactRead"},"type":"array","title":"Artifacts"},"total":{"type":"integer","title":"Total","description":"Total number of artifacts matching the query"},"page":{"type":"integer","title":"Page","description":"Current page number (1-indexed)","default":1},"page_size":{"type":"integer","title":"Page Size","description":"Number of items per page","default":20},"has_more":{"type":"boolean","title":"Has More","description":"Whether there are more pages available","default":false}},"type":"object","required":["total"],"title":"ArtifactListResponse","description":"Paginated response for artifact list endpoints."},"ArtifactRead":{"properties":{"id":{"type":"string","title":"Id"},"group_id":{"type":"string","maxLength":128,"minLength":1,"title":"Group Id"},"user_id":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"User Id"},"call_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Call Id"},"filename":{"type":"string","maxLength":512,"minLength":1,"title":"Filename"},"mime_type":{"type":"string","maxLength":128,"minLength":1,"title":"Mime Type"},"size_bytes":{"type":"integer","minimum":0.0,"title":"Size Bytes"},"version":{"type":"integer","minimum":0.0,"title":"Version"},"file_uri":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"title":"File Uri"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"caption":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caption"},"analysis_status":{"type":"string","title":"Analysis Status","default":"pending"},"video_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Video Id"},"document_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Document Id"},"audio_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Audio Id"},"task_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Task Id"},"message_timestamp":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Message Timestamp"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","group_id","filename","mime_type","size_bytes","version","created_at","updated_at"],"title":"ArtifactRead","description":"Schema for reading artifact records."},"AudioAnalysisRead":{"properties":{"audio_id":{"type":"string","title":"Audio Id"},"artifact_id":{"type":"string","title":"Artifact Id"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"detailed_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detailed Description"},"transcription":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Transcription"},"chapters":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Chapters"},"tasks":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Tasks"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["audio_id","artifact_id","created_at"],"title":"AudioAnalysisRead","description":"Response schema for audio analysis endpoints."},"Body_upload_artifact_file_api_v1_rest_artifacts_file_post":{"properties":{"file":{"anyOf":[{"type":"string","contentMediaType":"application/octet-stream"},{"type":"null"}],"title":"File","description":"File to upload"},"session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session Id","description":"Session ID"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id","description":"User ID"},"caption":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caption","description":"User-provided caption/note (#703)"}},"type":"object","title":"Body_upload_artifact_file_api_v1_rest_artifacts_file_post"},"CallCreateRequest":{"properties":{"chat_session_id":{"type":"string","title":"Chat Session Id","description":"Long-lived parent conversation/group ID stored as CallSession.chat_session_id"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id","description":"Optional client-provided CallSession.call_session_id; the server generates one when omitted"},"is_group":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Group","description":"Whether this is a group call"},"auto_call_config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Auto Call Config","description":"Auto-call configuration (caller/recipient info, goals)"},"voicemail_config":{"anyOf":[{"$ref":"#/components/schemas/VoicemailConfig"},{"type":"null"}],"description":"Voicemail configuration (receiver/caller info)"}},"type":"object","required":["chat_session_id"],"title":"CallCreateRequest","description":"Request model for POST /api/v1/realtime/call."},"CallCreateResponse":{"properties":{"call_id":{"type":"string","title":"Call Id","description":"Per-call identifier stored as CallSession.call_session_id"},"chat_session_id":{"type":"string","title":"Chat Session Id","description":"Long-lived parent ID stored as CallSession.chat_session_id"},"user_metadata":{"items":{"$ref":"#/components/schemas/UserMetadata"},"type":"array","title":"User Metadata","description":"Participants from chat session"},"status":{"type":"string","title":"Status","description":"Call status","default":"STARTED"},"start_time":{"type":"string","format":"date-time","title":"Start Time","description":"When the call was created"}},"type":"object","required":["call_id","chat_session_id","start_time"],"title":"CallCreateResponse","description":"Response model for POST /api/v1/realtime/call."},"CallListResponse":{"properties":{"calls":{"items":{"$ref":"#/components/schemas/CallReadResponse"},"type":"array","title":"Calls","description":"List of calls matching filters"}},"type":"object","title":"CallListResponse","description":"Response model for GET /api/v1/realtime/call."},"CallReadResponse":{"properties":{"call_id":{"type":"string","title":"Call Id","description":"Per-call identifier stored as CallSession.call_session_id"},"chat_session_id":{"type":"string","title":"Chat Session Id","description":"Long-lived parent ID stored as CallSession.chat_session_id"},"user_metadata":{"items":{"$ref":"#/components/schemas/UserMetadata"},"type":"array","title":"User Metadata","description":"Participants from chat session"},"status":{"type":"string","title":"Status","description":"Call status: STARTED or ENDED"},"start_time":{"type":"string","format":"date-time","title":"Start Time","description":"When the call was created"},"end_time":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End Time","description":"When the call ended (only present when status=ENDED)"},"summary":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Summary","description":"Call summary (generated after call ends). Contains: participants, start_time, duration_seconds, summary (multilingual), tasks_updated"},"summary_status":{"anyOf":[{"type":"string","enum":["generating","ready","failed"]},{"type":"null"}],"title":"Summary Status","description":"Summary generation status: 'generating' (in progress), 'ready' (summary available), 'failed' (generation failed). Absent/null means not yet started."}},"type":"object","required":["call_id","chat_session_id","status","start_time"],"title":"CallReadResponse","description":"Response model for GET /api/v1/realtime/call/{id}."},"CallUpdateRequest":{"properties":{"status":{"anyOf":[{"type":"string","enum":["STARTED","ENDED"]},{"type":"null"}],"title":"Status","description":"Update status to 'ENDED' to terminate call"},"call_metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Call Metadata","description":"Additional metadata to merge"}},"type":"object","title":"CallUpdateRequest","description":"Request model for PUT /api/v1/realtime/call/{id}."},"CompanyInfo":{"properties":{"name":{"type":"string","title":"Name"},"location":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location"},"hours":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hours"}},"additionalProperties":true,"type":"object","required":["name"],"title":"CompanyInfo"},"CorrectedTranscript":{"properties":{"transcript":{"items":{"$ref":"#/components/schemas/UpdatedTranscriptEntry"},"type":"array","title":"Transcript"}},"type":"object","required":["transcript"],"title":"CorrectedTranscript","description":"Corrected transcript response from LLM."},"CreateSessionRequest":{"properties":{"users":{"items":{"type":"string"},"type":"array","maxItems":50,"minItems":1,"title":"Users","description":"List of participant ACI IDs"},"users_metadata":{"anyOf":[{"items":{"$ref":"#/components/schemas/UserMetadata"},"type":"array"},{"type":"null"}],"title":"Users Metadata","description":"Optional metadata (names) for each participant"},"agent_aci":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Aci","description":"Agent's ACI ID for memory scoping"}},"type":"object","required":["users"],"title":"CreateSessionRequest","description":"Request model for creating or resuming a conversation session."},"CreateSessionResponse":{"properties":{"session_id":{"type":"string","title":"Session Id","description":"ADK session identifier"},"users":{"items":{"type":"string"},"type":"array","title":"Users","description":"Current participant roster"},"last_event":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Event","description":"Last event ID or null for new sessions"}},"type":"object","required":["session_id","users"],"title":"CreateSessionResponse","description":"Response model for session creation/resumption."},"DailyReminderRequest":{"properties":{"service_id":{"type":"string","title":"Service Id"}},"type":"object","required":["service_id"],"title":"DailyReminderRequest"},"DeleteConversationDetails":{"properties":{"session":{"type":"boolean","title":"Session","description":"Whether the ADK session was deleted"},"tasks":{"type":"integer","title":"Tasks","description":"Number of tasks deleted"},"topics":{"type":"integer","title":"Topics","description":"Number of topics deleted"},"memories":{"type":"integer","title":"Memories","description":"Number of memories deleted"},"artifacts":{"type":"integer","title":"Artifacts","description":"Number of artifact records deleted","default":0},"background_progress":{"type":"integer","title":"Background Progress","description":"Number of background task progress records deleted"},"pending_jobs":{"type":"integer","title":"Pending Jobs","description":"Number of pending Procrastinate jobs cancelled","default":0}},"type":"object","required":["session","tasks","topics","memories","background_progress"],"title":"DeleteConversationDetails","description":"Details of what was deleted for a conversation."},"DeleteConversationResponse":{"properties":{"message":{"type":"string","title":"Message","description":"Success message","default":"Conversation deleted successfully"},"group_id":{"type":"string","title":"Group Id","description":"The group ID that was deleted"},"deleted":{"$ref":"#/components/schemas/DeleteConversationDetails","description":"Details of deleted resources"}},"type":"object","required":["group_id","deleted"],"title":"DeleteConversationResponse","description":"Response model for conversation deletion."},"DocumentAnalysisRead":{"properties":{"document_id":{"type":"string","title":"Document Id"},"artifact_id":{"type":"string","title":"Artifact Id"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"detailed_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detailed Description"},"markdown_content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Markdown Content"},"doc_metadata":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Doc Metadata"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["document_id","artifact_id","created_at"],"title":"DocumentAnalysisRead","description":"Response schema for document analysis endpoints."},"GetResponseRequest":{"properties":{"messages":{"items":{"$ref":"#/components/schemas/Message"},"type":"array","maxItems":10,"title":"Messages","description":"Optional new messages to process before returning response"}},"type":"object","title":"GetResponseRequest","description":"Request model for retrieving agent responses (optional messages)."},"GetResponseResponse":{"properties":{"messages":{"items":{"$ref":"#/components/schemas/Message"},"type":"array","title":"Messages","description":"AI response messages"}},"type":"object","title":"GetResponseResponse","description":"Response model containing AI-generated messages."},"GetSessionResponse":{"properties":{"session_id":{"type":"string","title":"Session Id","description":"ADK session identifier"},"users":{"items":{"type":"string"},"type":"array","title":"Users","description":"Current participant roster"},"users_metadata":{"items":{"$ref":"#/components/schemas/UserMetadata"},"type":"array","title":"Users Metadata","description":"Metadata for each participant"},"agent_aci":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Agent Aci","description":"Agent ACI identifier"},"last_event":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Event","description":"Last event ID or null for new sessions"}},"type":"object","required":["session_id","users"],"title":"GetSessionResponse","description":"Response model for GET session endpoint."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ImageAnalysisRead":{"properties":{"image_id":{"type":"string","title":"Image Id"},"artifact_id":{"type":"string","title":"Artifact Id"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"detailed_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detailed Description"},"objects":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Objects"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["image_id","artifact_id","created_at"],"title":"ImageAnalysisRead","description":"Response schema for image analysis endpoints."},"InstructionCreate":{"properties":{"diversion_number":{"type":"string","title":"Diversion Number","description":"E.164 diversion number"},"company_info":{"$ref":"#/components/schemas/CompanyInfo"},"team_info":{"items":{"$ref":"#/components/schemas/TeamMember"},"type":"array","title":"Team Info"},"instructions":{"$ref":"#/components/schemas/InstructionsBlock"}},"type":"object","required":["diversion_number","company_info","team_info","instructions"],"title":"InstructionCreate"},"InstructionList":{"properties":{"items":{"items":{"$ref":"#/components/schemas/InstructionRead"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"}},"type":"object","required":["items","total","limit","offset"],"title":"InstructionList","description":"Paginated list of Instructions."},"InstructionPreviewRequest":{"properties":{"company_info":{"additionalProperties":true,"type":"object","title":"Company Info"},"team_info":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Team Info"},"instructions":{"additionalProperties":true,"type":"object","title":"Instructions"}},"type":"object","title":"InstructionPreviewRequest","description":"A (possibly unsaved) Instruction draft to render the system prompt from.\n\nDeliberately looser than ``InstructionCreate``: the admin UI re-renders the\nprompt while the operator is still typing, so a half-filled draft — no\ncompany name yet, a phone number mid-entry — must render rather than 422.\n``render_answering_service_instruction`` already tolerates missing values\n(a member with no usable number renders as untransferable), so the fields\nare raw dicts here and every one is optional.\n\nExtra top-level keys (``diversion_number``, ``id``) are ignored, so the UI\ncan post exactly the payload it would save."},"InstructionPromptPreview":{"properties":{"prompt":{"type":"string","title":"Prompt","description":"The complete system instruction the answering-service agent runs with for this Instruction — preamble, behavioral rules, company block, team directory, routing rules, and the operator's own guidance, not just the latter."}},"type":"object","required":["prompt"],"title":"InstructionPromptPreview","description":"The effective system prompt an Instruction produces."},"InstructionRead":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"diversion_number":{"type":"string","title":"Diversion Number"},"company_info":{"additionalProperties":true,"type":"object","title":"Company Info"},"team_info":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Team Info"},"instructions":{"additionalProperties":true,"type":"object","title":"Instructions"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["id","diversion_number","company_info","team_info","instructions","created_at","updated_at"],"title":"InstructionRead"},"InstructionUpdate":{"properties":{"diversion_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Diversion Number"},"company_info":{"anyOf":[{"$ref":"#/components/schemas/CompanyInfo"},{"type":"null"}]},"team_info":{"anyOf":[{"items":{"$ref":"#/components/schemas/TeamMember"},"type":"array"},{"type":"null"}],"title":"Team Info"},"instructions":{"anyOf":[{"$ref":"#/components/schemas/InstructionsBlock"},{"type":"null"}]}},"type":"object","title":"InstructionUpdate","description":"Top-level partial update. Each included field replaces the existing one wholesale.\n\nFields are optional (omit to leave unchanged), but explicit ``null`` is\nrejected so callers can't bypass DB NOT NULL constraints by sending\n``{\"company_info\": null}``."},"InstructionsBlock":{"properties":{"recording_disclosure":{"type":"string","title":"Recording Disclosure"},"general":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"General"}},"additionalProperties":true,"type":"object","required":["recording_disclosure"],"title":"InstructionsBlock"},"LLMCorrectionOutput":{"properties":{"corrected_transcript":{"$ref":"#/components/schemas/CorrectedTranscript"},"summary":{"$ref":"#/components/schemas/TranscriptSummary"},"correction_failed":{"type":"boolean","title":"Correction Failed","default":false},"summary_failed":{"type":"boolean","title":"Summary Failed","default":false},"tasks_updated":{"anyOf":[{"items":{"$ref":"#/components/schemas/TaskSummaryItem"},"type":"array"},{"type":"null"}],"title":"Tasks Updated"},"diarization_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Diarization Status"},"diarization_ops":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Diarization Ops"},"is_voicemail":{"type":"boolean","title":"Is Voicemail","default":false},"is_informative":{"type":"boolean","title":"Is Informative","default":true}},"type":"object","required":["corrected_transcript","summary"],"title":"LLMCorrectionOutput","description":"Response from transcript correction endpoint."},"Message":{"description":"Core message envelope used for all conversation content.","properties":{"user":{"description":"Sender identifier (ACI ID or 'AI' for agent)","title":"User","type":"string"},"mimetype":{"default":"text/plain","description":"Content type: 'text/plain', 'application/json', etc.","title":"Mimetype","type":"string"},"data":{"description":"Raw content (text or base64-encoded binary)","title":"Data","type":"string"},"metadata":{"additionalProperties":true,"description":"Flexible key-value pairs (supports nested structures)","title":"Metadata","type":"object"}},"required":["user","data"],"title":"Message","type":"object"},"Page_TaskRead_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/TaskRead"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[TaskRead]"},"ReceiverInfo":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"}},"type":"object","required":["id","name"],"title":"ReceiverInfo","description":"A single absent group member in a multi-person voicemail call."},"SignedUrlRequest":{"properties":{"session_id":{"type":"string","minLength":1,"title":"Session Id","description":"Session ID to associate the artifact with"},"filename":{"type":"string","title":"Filename","description":"Display name for the file"},"mime_type":{"type":"string","title":"Mime Type","description":"MIME type of the file"},"size_bytes":{"type":"integer","minimum":0.0,"title":"Size Bytes","description":"Expected file size in bytes"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id","description":"User ID who will upload the artifact"},"caption":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caption","description":"User-provided caption/note (#703)"}},"type":"object","required":["session_id","filename","mime_type","size_bytes"],"title":"SignedUrlRequest","description":"Request model for getting a signed upload URL."},"SignedUrlResponse":{"properties":{"artifact_id":{"type":"string","title":"Artifact Id","description":"Artifact ID (UUID) for referencing in messages via metadata.files"},"upload_url":{"type":"string","title":"Upload Url","description":"URL for direct upload to GCS"},"file_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"File Uri","description":"Internal: GCS URI (clients should use artifact_id, not file_uri)"},"expires_in_seconds":{"type":"integer","title":"Expires In Seconds","description":"URL expiration time in seconds"},"http_method":{"type":"string","title":"Http Method","description":"HTTP method for upload","default":"PUT"},"upload_type":{"type":"string","title":"Upload Type","description":"Upload type: 'signed' for single PUT, 'resumable' for chunked","default":"signed"},"recommended_chunk_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Recommended Chunk Size","description":"Recommended chunk size in bytes for resumable uploads"}},"type":"object","required":["artifact_id","upload_url","expires_in_seconds"],"title":"SignedUrlResponse","description":"Response model for upload URL (signed or resumable)."},"TaskCreate":{"properties":{"group_id":{"type":"string","title":"Group Id"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"title":{"type":"string","title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"updates":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Updates"},"assigned_to_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assigned To User Id"},"parent_task_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Task Id"},"status":{"type":"string","enum":["TODO","INPROGRESS","DONE"],"title":"Status","default":"TODO"},"due_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Due Date"},"start_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start Date"}},"type":"object","required":["group_id","title"],"title":"TaskCreate"},"TaskRead":{"properties":{"id":{"type":"integer","title":"Id"},"group_id":{"type":"string","title":"Group Id"},"call_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Call Id"},"parent_task_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Task Id"},"title":{"type":"string","title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"updates":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Updates"},"attributed_updates":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Attributed Updates"},"message_timestamp":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Message Timestamp"},"artifact_ids":{"items":{"type":"string"},"type":"array","title":"Artifact Ids","default":[]},"assigned_to_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assigned To User Id"},"status":{"type":"string","title":"Status"},"previous_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Status"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"due_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Due Date"},"start_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start Date"},"detected_language":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detected Language"},"subtasks":{"items":{"$ref":"#/components/schemas/TaskRead"},"type":"array","title":"Subtasks"}},"type":"object","required":["id","group_id","title","status","created_at","updated_at"],"title":"TaskRead"},"TaskSummaryItem":{"properties":{"id":{"type":"integer","title":"Id"},"title":{"type":"string","title":"Title"},"status":{"type":"string","title":"Status"}},"type":"object","required":["id","title","status"],"title":"TaskSummaryItem","description":"Task item for summary output."},"TaskUpdate":{"properties":{"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Summary"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"updates":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}],"title":"Updates"},"assigned_to_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Assigned To User Id"},"parent_task_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Task Id"},"status":{"anyOf":[{"type":"string","enum":["SUGGESTED_TODO","SUGGESTED_INPROGRESS","SUGGESTED_DONE","SUGGESTED_DELETE","TODO","INPROGRESS","DONE"]},{"type":"null"}],"title":"Status"},"due_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Due Date"},"start_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start Date"}},"type":"object","title":"TaskUpdate","description":"Model for partial task updates - all fields are optional."},"TeamMember":{"properties":{"member_name":{"type":"string","title":"Member Name"},"member_phone_number":{"type":"string","title":"Member Phone Number"},"member_aci":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Member Aci"},"member_dept":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Member Dept"},"is_private":{"type":"boolean","title":"Is Private","default":false}},"additionalProperties":true,"type":"object","required":["member_name","member_phone_number"],"title":"TeamMember","description":"A routable person in the org directory.\n\nThe answering service routes on ``member_phone_number`` (#945), so it is\nrequired: a member without one cannot be transferred to, and accepting the\nrecord would just defer the failure to call time.\n\n``member_aci`` is optional — it is a Signal-internal identifier, the person\nmay not have a Jamb account at all, and it is never rendered into the\nanswering-service prompt nor emitted on the answering-service wire. It\nstays on the model for the Instruction record's other consumers.\n\nNote this validates writes only. ``InstructionRead`` returns ``team_info``\nas raw dicts, so rows stored before this field existed still read back\nfine; they simply can't be updated without adding a number."},"TopicRead":{"properties":{"id":{"type":"integer","title":"Id"},"group_id":{"type":"string","title":"Group Id"},"category":{"type":"string","title":"Category"},"name":{"type":"string","title":"Name"},"start_timestamp":{"type":"string","title":"Start Timestamp"},"end_timestamp":{"type":"string","title":"End Timestamp"},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"}},"type":"object","required":["id","group_id","category","name","start_timestamp","end_timestamp","created_at","updated_at"],"title":"TopicRead","description":"API response schema per specs/topics.md Section 2.3."},"TranscriptSummary":{"properties":{"en":{"type":"string","title":"En","description":"English summary"},"pt":{"type":"string","title":"Pt","description":"Portuguese summary"},"es":{"type":"string","title":"Es","description":"Spanish summary"}},"type":"object","required":["en","pt","es"],"title":"TranscriptSummary","description":"Summary of the transcript in multiple languages."},"TranslationRequest":{"properties":{"text":{"type":"string","maxLength":1000000,"title":"Text"},"target_languages":{"items":{"type":"string"},"type":"array","maxItems":10,"minItems":1,"title":"Target Languages"},"api_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Version"}},"type":"object","required":["text","target_languages"],"title":"TranslationRequest"},"TranslationResponse":{"properties":{"detected_language":{"type":"string","title":"Detected Language"},"language":{"type":"string","title":"Language"},"translations":{"additionalProperties":{"type":"string"},"type":"object","title":"Translations"},"latency_ms":{"type":"number","title":"Latency Ms"}},"type":"object","required":["detected_language","language","translations","latency_ms"],"title":"TranslationResponse"},"UpdateConversationRequest":{"properties":{"users":{"anyOf":[{"items":{"type":"string"},"type":"array","maxItems":50,"minItems":1},{"type":"null"}],"title":"Users","description":"Optional updated participant list"},"messages":{"items":{"$ref":"#/components/schemas/Message"},"type":"array","maxItems":50,"title":"Messages","description":"Messages to send to agent (max 50 per request)"}},"type":"object","title":"UpdateConversationRequest","description":"Request model for updating conversation with new messages."},"UpdateConversationResponse":{"properties":{"status":{"type":"string","title":"Status","description":"Update status","default":"ok"},"accepted_count":{"type":"integer","title":"Accepted Count","description":"Number of messages accepted"},"responses":{"anyOf":[{"items":{"$ref":"#/components/schemas/Message"},"type":"array"},{"type":"null"}],"title":"Responses","description":"AI responses generated from the messages"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if status is 'error'"}},"type":"object","required":["accepted_count"],"title":"UpdateConversationResponse","description":"Response model for conversation update acknowledgment."},"UpdatedTranscriptEntry":{"properties":{"user_id":{"type":"string","title":"User Id"},"text":{"type":"string","title":"Text"},"original_text":{"type":"string","title":"Original Text"}},"type":"object","required":["user_id","text","original_text"],"title":"UpdatedTranscriptEntry"},"UploadArtifactRequest":{"properties":{"session_id":{"type":"string","minLength":1,"title":"Session Id","description":"Session ID to associate the artifact with"},"filename":{"type":"string","title":"Filename","description":"Display name for the file"},"mime_type":{"type":"string","title":"Mime Type","description":"MIME type of the file"},"data":{"type":"string","title":"Data","description":"Base64-encoded file content"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id","description":"User ID who uploaded the artifact"}},"type":"object","required":["session_id","filename","mime_type","data"],"title":"UploadArtifactRequest","description":"Request model for uploading artifact via JSON body."},"UploadArtifactResponse":{"properties":{"artifact_id":{"type":"string","title":"Artifact Id","description":"Artifact ID (UUID) for referencing in messages via metadata.files"},"filename":{"type":"string","title":"Filename","description":"Stored filename"},"file_uri":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"File Uri","description":"Internal: GCS URI (clients should use artifact_id, not file_uri)"},"mime_type":{"type":"string","title":"Mime Type","description":"MIME type of the file"},"size_bytes":{"type":"integer","title":"Size Bytes","description":"File size in bytes"}},"type":"object","required":["artifact_id","filename","mime_type","size_bytes"],"title":"UploadArtifactResponse","description":"Response model for artifact upload."},"UserMetadata":{"properties":{"id":{"type":"string","title":"Id","description":"Participant ACI ID"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","description":"Display name"},"type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type","description":"Participant type: 'agent' identifies AI, 'voip' identifies a telephone proxy, and 'user' identifies a direct human participant"}},"type":"object","required":["id"],"title":"UserMetadata","description":"Metadata for a participant in the conversation."},"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"},"VideoAnalysisRead":{"properties":{"video_id":{"type":"string","title":"Video Id"},"artifact_id":{"type":"string","title":"Artifact Id"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"detailed_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detailed Description"},"transcription":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Transcription"},"chapters":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Chapters"},"tasks":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Tasks"},"key_segments":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Key Segments"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["video_id","artifact_id","created_at"],"title":"VideoAnalysisRead","description":"Response schema for video analysis endpoints."},"VoicemailConfig":{"properties":{"receiver_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Receiver Id"},"receiver_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Receiver Name"},"receiver_group":{"anyOf":[{"items":{"$ref":"#/components/schemas/ReceiverInfo"},"type":"array"},{"type":"null"}],"title":"Receiver Group"},"caller_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caller Id"},"caller_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Caller Name"},"group_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Group Id"},"pstn":{"type":"boolean","title":"Pstn","default":false}},"type":"object","title":"VoicemailConfig","description":"Voicemail configuration sent by signal-cli when a call goes unanswered."},"RealTimeConversationRequest":{"description":"Request model for updating conversation with new messages via WebSocket.","properties":{"users":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"minItems":1,"type":"array"},{"type":"null"}],"default":null,"description":"Optional updated participant list","title":"Users"},"messages":{"description":"Messages to send to agent (max 50 per request)","items":{"$ref":"#/components/schemas/Message"},"maxItems":50,"title":"Messages","type":"array"}},"title":"RealTimeConversationRequest","type":"object"},"RealTimeConversationResponse":{"description":"Response model containing AI-generated messages via WebSocket.","properties":{"messages":{"description":"AI response messages","items":{"$ref":"#/components/schemas/Message"},"title":"Messages","type":"array"}},"title":"RealTimeConversationResponse","type":"object"}}}}