diff --git a/README.md b/README.md index 581bbc2..071d725 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,99 @@ curl -X POST http://localhost:${PORT}/bots \ }' ``` +You can attach external prompt context and MCP servers per bot. External +context is loaded before the Pipecat process starts and capped by +`prompt_data_token_limit` using an approximate token budget. URL sources block +localhost and private-network targets by default; set +`PROMPT_DATA_ALLOW_PRIVATE_URLS=true` only in trusted deployments. + +MCP servers are live-query capable only when their config is connectable. +`http`, `streamable_http`, and `sse` servers require `transport`, `url`, and +optional `headers`. Local process `stdio` MCP is intentionally not accepted by +the public API because it would execute caller-supplied commands. Remote MCP +URLs also block localhost and private-network targets by default; production +deployments should use `MCP_ALLOWED_PRIVATE_URLS` with exact `http://host:port/path` +entries for trusted loopback MCPs instead of the broad +`MCP_ALLOW_PRIVATE_URLS=true` development bypass. If `transport` is omitted, the +server is treated as metadata-only and MCP tools are not executed. Secrets are +not required in the request, but `headers` are available for deployments that +need them. Use `tool_allowlist` to constrain which server tools the bot may +call, and set `enabled: false` to document a server without connecting to it. + +```bash +curl -X POST http://localhost:${PORT}/bots \ + -H "Content-Type: application/json" \ + -H "x-meeting-baas-api-key: your-api-key" \ + -d '{ + "meeting_url": "https://meet.google.com/xxx-yyyy-zzz", + "personas": ["account_executive"], + "prompt_data_token_limit": 4000, + "prompt_data_sources": [ + { + "name": "CRM account notes", + "type": "url", + "url": "https://example.com/account-notes.md" + }, + { + "name": "Call objective", + "type": "text", + "text": "Confirm timeline, budget, and integration constraints." + } + ], + "mcp": { + "instructions": "Use Google Drive and CRM context only when relevant.", + "servers": [ + { + "name": "drive-docs-work", + "enabled": true, + "transport": "streamable_http", + "url": "http://127.0.0.1:8123/mcp", + "tool_allowlist": ["search", "read"], + "timeout_seconds": 20, + "instructions": "Use only meeting-relevant Drive docs." + }, + { + "name": "remote-crm", + "enabled": true, + "transport": "streamable_http", + "url": "https://mcp.example.com/mcp", + "headers": { + "Authorization": "Bearer optional-token" + }, + "tools": ["get_account", "list_recent_calls"], + "tool_allowlist": ["get_account", "list_recent_calls"], + "timeout_seconds": 15 + } + ] + }, + "speech_speed": 1.25 + }' +``` + +`speech_speed` overrides `CARTESIA_TTS_SPEED`, `TTS_SPEED`, or +`SPEECH_SPEED`. The Cartesia runner clamps speed to `0.6..1.5`. + +### OpenAPI Snapshots + +This repo contains three OpenAPI files with different roles: + +- `openapi.json` is this FastAPI service snapshot for generic tooling. +- `speaking-bot-openapi.json` is the same service snapshot, named explicitly + for the speaking-bots MCP sync. +- `meeting-baas-openapi-v1.json` is the upstream MeetingBaaS v1 API snapshot. +- `openapi-v2.json` is the upstream MeetingBaaS v2 API snapshot. + +The service snapshots include `/bots`, `/bots/{bot_id}`, +`/personas/generate-image`, `/health`, `/ready`, `/webhook`, and the current +`BotRequest` fields for `prompt_data_sources`, `prompt_data_token_limit`, `mcp`, +and `speech_speed`. + +Regenerate the service snapshot after API model changes: + +```bash +poetry run python scripts/export_openapi.py +``` + You can still manually specify a WebSocket URL if needed: ```bash @@ -440,6 +533,7 @@ Once the server is running, you can access: - Interactive API docs: `http://localhost:${PORT}/docs` - OpenAPI specification: `http://localhost:${PORT}/openapi.json` +- Committed service OpenAPI snapshots: `openapi.json`, `speaking-bot-openapi.json` - Health endpoint: `http://localhost:${PORT}/health` - Readiness endpoint: `http://localhost:${PORT}/ready` diff --git a/app/models.py b/app/models.py index eaca2df..4bc151b 100644 --- a/app/models.py +++ b/app/models.py @@ -1,9 +1,9 @@ """Data models for the Speaking Meeting Bot API.""" from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Literal, Optional -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator def _validate_meeting_url(value: str) -> str: @@ -49,6 +49,152 @@ class TurnConfig(BaseModel): ) +class PromptDataSource(BaseModel): + """External context to append to the bot prompt under a token budget.""" + + model_config = ConfigDict(extra="forbid") + + name: str = Field( + "external_context", + min_length=1, + max_length=120, + description="Human-readable source name shown inside the prompt context block", + ) + type: Literal["text", "url"] = Field( + ..., + description="Whether to load inline text or fetch an external HTTP(S) URL", + ) + text: Optional[str] = Field( + None, + description="Inline context. Required when type is text.", + ) + url: Optional[str] = Field( + None, + description="HTTP(S) URL to fetch. Required when type is url.", + ) + headers: Optional[Dict[str, str]] = Field( + None, + description="Optional HTTP headers for URL sources. Avoid request-specific secrets unless needed.", + ) + token_limit: Optional[int] = Field( + None, + ge=1, + le=50_000, + description="Optional per-source token cap before the request-level cap is applied", + ) + + @field_validator("url") + @classmethod + def validate_url(cls, value: Optional[str]) -> Optional[str]: + if value is None: + return value + normalized = value.strip() + if not normalized.startswith(("http://", "https://")): + raise ValueError("prompt data source url must start with http:// or https://") + return normalized + + @model_validator(mode="after") + def validate_source_payload(self): + if self.type == "text" and not self.text: + raise ValueError("text is required when prompt data source type is text") + if self.type == "url" and not self.url: + raise ValueError("url is required when prompt data source type is url") + if self.type == "text" and self.url: + raise ValueError("url is not allowed when prompt data source type is text") + if self.type == "url" and self.text: + raise ValueError("text is not allowed when prompt data source type is url") + return self + + +MCPTransport = Literal["http", "streamable_http", "sse"] + + +class MCPServerConfig(BaseModel): + """MCP server metadata and optional live connection details.""" + + model_config = ConfigDict(extra="forbid") + + name: str = Field(..., min_length=1, max_length=120) + enabled: bool = Field( + True, + description="Whether this server may be used. Disabled servers are documented but not connected.", + ) + url: Optional[str] = Field( + None, + description="Remote MCP server URL. Required for http, streamable_http, and sse transports.", + ) + headers: Optional[Dict[str, str]] = Field( + None, + description="Optional HTTP headers for remote MCP servers. Use only when a server requires them.", + ) + transport: Optional[MCPTransport] = Field( + None, + description="Remote MCP transport. Omit for metadata-only servers that cannot execute tools.", + ) + tools: Optional[List[str]] = Field( + None, + max_length=50, + description="Known tool names exposed by this MCP server", + ) + tool_allowlist: Optional[List[str]] = Field( + None, + max_length=50, + description="Optional allowlist of MCP tool names this bot may call from this server.", + ) + timeout_seconds: Optional[float] = Field( + None, + ge=0.1, + le=300.0, + description="Optional per-server connection/tool timeout in seconds.", + ) + instructions: Optional[str] = Field( + None, + max_length=4_000, + description="Operator instructions or constraints for this MCP server", + ) + + @field_validator("url") + @classmethod + def validate_mcp_url(cls, value: Optional[str]) -> Optional[str]: + if value is None: + return value + normalized = value.strip() + if not normalized.startswith(("http://", "https://")): + raise ValueError("mcp server url must start with http:// or https://") + return normalized + + @model_validator(mode="after") + def validate_connection_details(self): + if self.transport in {"http", "streamable_http", "sse"}: + if not self.url: + raise ValueError( + f"url is required when MCP transport is {self.transport}" + ) + else: + if self.url or self.headers: + raise ValueError( + "transport is required when MCP connection details are supplied" + ) + return self + + +class MCPConfig(BaseModel): + """MCP server metadata and optional live connection details.""" + + model_config = ConfigDict(extra="forbid") + + servers: List[MCPServerConfig] = Field( + default_factory=list, + max_length=10, + description="MCP servers to document and optionally connect for tool calls", + ) + instructions: Optional[str] = Field( + None, + max_length=4_000, + description="Global MCP usage instructions for the bot", + ) + + class BotRequest(BaseModel): """Request model for creating a speaking bot in a meeting.""" @@ -64,6 +210,26 @@ class BotRequest(BaseModel): "enable_tools": True, "extra": {"company": "ACME Corp", "meeting_purpose": "Weekly sync"}, "websocket_url": "wss://bots.example.com", + "prompt_data_token_limit": 3000, + "prompt_data_sources": [ + { + "name": "CRM account notes", + "type": "url", + "url": "https://example.com/account-notes.md", + } + ], + "speech_speed": 1.15, + "mcp": { + "servers": [ + { + "name": "crm", + "url": "https://mcp.example.com", + "transport": "streamable_http", + "tools": ["get_account", "list_recent_calls"], + "tool_allowlist": ["get_account", "list_recent_calls"], + } + ] + }, "prompt": "You are Meeting Assistant, a concise and professional \ AI bot that helps summarize key points and keep the meeting on track. Speak clearly and stay on topic.", } @@ -93,6 +259,27 @@ class BotRequest(BaseModel): None, description="Per-bot turn-taking tuning (VAD confidence/start_secs/stop_secs/min_volume)", ) + prompt_data_sources: Optional[List[PromptDataSource]] = Field( + None, + max_length=10, + description="External text or URL data sources to append to the bot prompt", + ) + prompt_data_token_limit: int = Field( + 4_000, + ge=0, + le=50_000, + description="Approximate total token cap for loaded prompt_data_sources. 0 disables loading.", + ) + mcp: Optional[MCPConfig] = Field( + None, + description="MCP server/tool metadata and optional live connection details", + ) + speech_speed: Optional[float] = Field( + None, + ge=0.5, + le=2.0, + description="TTS speaking speed multiplier. Defaults to CARTESIA_TTS_SPEED, TTS_SPEED, SPEECH_SPEED, or the runner default.", + ) # NOTE: streaming_audio_frequency is intentionally excluded and handled internally diff --git a/app/routes.py b/app/routes.py index 663bc86..51f6e08 100644 --- a/app/routes.py +++ b/app/routes.py @@ -11,7 +11,7 @@ import openai from fastapi import APIRouter, HTTPException, Request, status -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.responses import JSONResponse from app.models import ( BotRequest, @@ -21,6 +21,11 @@ PersonaImageResponse, ) from app.services.image_service import image_service +from app.services.prompt_context import ( + PromptContextError, + load_prompt_context, + merge_context_blocks, +) from config.persona_utils import persona_manager from core.connection import MEETING_DETAILS, PIPECAT_PROCESSES, registry from core.process import start_pipecat_process, terminate_process_gracefully @@ -44,7 +49,6 @@ update_ngrok_client_id, ) from utils.runtime import build_public_base_url, get_internal_pipecat_ws_url, get_state_dir -from config.prompts import PERSONA_INTERACTION_INSTRUCTIONS # Import the new persona detail extraction service from app.services.persona_detail_extraction import extract_persona_details_from_prompt @@ -120,7 +124,6 @@ async def join_meeting(request: BotRequest, client_request: Request): log_ngrok_status() # --- Streamlined Persona and Prompt Resolution Logic --- - final_prompt: str = "" resolved_persona_data: Dict[str, Any] = {} persona_name_for_logging: str = "Unknown" @@ -143,7 +146,6 @@ async def join_meeting(request: BotRequest, client_request: Request): "is_temporary": True # Mark as temporary persona } persona_name_for_logging = resolved_persona_data["name"] - final_prompt = request.prompt + PERSONA_INTERACTION_INSTRUCTIONS logger.info(f"Dynamically created persona '{persona_name_for_logging}' from prompt.") else: # Fallback if prompt details extraction fails or returns unexpected type @@ -151,7 +153,6 @@ async def join_meeting(request: BotRequest, client_request: Request): resolved_persona_data = persona_manager.get_persona("baas_onboarder") resolved_persona_data["is_temporary"] = False # Ensure fallback is not marked temporary persona_name_for_logging = resolved_persona_data.get("name", "baas_onboarder") - final_prompt = resolved_persona_data["prompt"] + PERSONA_INTERACTION_INSTRUCTIONS else: # Case 2: No custom prompt, use pre-defined persona resolved_persona_name: str @@ -176,14 +177,12 @@ async def join_meeting(request: BotRequest, client_request: Request): resolved_persona_data = persona_manager.get_persona(resolved_persona_name) resolved_persona_data["is_temporary"] = False # Mark as not temporary persona_name_for_logging = resolved_persona_data.get("name", resolved_persona_name) - final_prompt = resolved_persona_data["prompt"] + PERSONA_INTERACTION_INSTRUCTIONS logger.info(f"Using pre-defined persona '{persona_name_for_logging}'.") except KeyError as e: logger.error(f"Resolved persona '{resolved_persona_name}' not found: {e}. Falling back to baas_onboarder.") resolved_persona_data = persona_manager.get_persona("baas_onboarder") resolved_persona_data["is_temporary"] = False # Ensure fallback is not marked temporary persona_name_for_logging = resolved_persona_data.get("name", "baas_onboarder") - final_prompt = resolved_persona_data["prompt"] + PERSONA_INTERACTION_INSTRUCTIONS logger.info(f"Using fallback persona '{persona_name_for_logging}'.") # Populate image if not present @@ -203,7 +202,7 @@ async def join_meeting(request: BotRequest, client_request: Request): resolved_persona_data["image"] = generated_image logger.info(f"Generated image URL for '{persona_name_for_logging}': {generated_image}") else: - logger.warning(f"Image generation returned no URL.") + logger.warning("Image generation returned no URL.") resolved_persona_data["image"] = None # Ensure no invalid image data is stored except Exception as e: logger.error(f"Failed to generate image for '{persona_name_for_logging}': {e}") @@ -216,12 +215,44 @@ async def join_meeting(request: BotRequest, client_request: Request): resolved_persona_data["cartesia_voice_id"] = cartesia_voice_id logger.info(f"Resolved Cartesia voice ID for '{persona_name_for_logging}': {cartesia_voice_id}") - logger.info(f"Final resolved persona data for Pipecat process:") + logger.info("Final resolved persona data for Pipecat process:") logger.info(f" Name: {resolved_persona_data.get('name')}") logger.info(f" Image: {resolved_persona_data.get('image')}") logger.info(f" Voice ID: {resolved_persona_data.get('cartesia_voice_id')}") logger.info(f" Is Temporary: {resolved_persona_data.get('is_temporary')}") + try: + prompt_context = await load_prompt_context( + request.prompt_data_sources, + request.prompt_data_token_limit, + ) + except PromptContextError as e: + return JSONResponse( + content={"message": e.message, "status": "error"}, + status_code=e.status_code, + ) + + if prompt_context.block: + resolved_persona_data["additional_content"] = merge_context_blocks( + [resolved_persona_data.get("additional_content") or "", prompt_context.block] + ) + + if prompt_context.sources: + resolved_persona_data["prompt_data_sources"] = prompt_context.sources + resolved_persona_data["prompt_data_estimated_tokens"] = ( + prompt_context.estimated_tokens + ) + logger.info( + f"Loaded {len(prompt_context.sources)} prompt data source(s), " + f"estimated {prompt_context.estimated_tokens} tokens" + ) + + if request.mcp: + resolved_persona_data["mcp"] = request.mcp.model_dump(exclude_none=True) + + if request.speech_speed is not None: + resolved_persona_data["speech_speed"] = request.speech_speed + # Store all relevant details in MEETING_DETAILS dictionary. # Index 5 carries the FULL resolved persona dict (prompt, voice, image…): # the Pipecat child is spawned later from app/websockets.py when MeetingBaas @@ -282,7 +313,7 @@ async def join_meeting(request: BotRequest, client_request: Request): bot_image = generated_image_url logger.info(f"Generated image: {bot_image}") else: - logger.error(f"Failed to generate image from prompt derived details: No URL returned.") + logger.error("Failed to generate image from prompt derived details: No URL returned.") bot_image = None except Exception as e: logger.error(f"Failed to generate image from prompt derived details: {e}") diff --git a/app/services/prompt_context.py b/app/services/prompt_context.py new file mode 100644 index 0000000..5af9614 --- /dev/null +++ b/app/services/prompt_context.py @@ -0,0 +1,280 @@ +"""Load and format external prompt context under a token budget.""" + +import math +import os +import socket +from dataclasses import dataclass +from ipaddress import ip_address +from typing import Any, Iterable, Mapping, Sequence +from urllib.parse import urlparse + + +CHARS_PER_TOKEN = 4 +DEFAULT_SOURCE_MAX_BYTES = 1_000_000 + + +class PromptContextError(Exception): + """Raised when external prompt context cannot be loaded.""" + + def __init__(self, message: str, status_code: int = 400): + super().__init__(message) + self.message = message + self.status_code = status_code + + +@dataclass +class PromptContextResult: + block: str + sources: list[dict[str, Any]] + estimated_tokens: int + + +def estimate_tokens(text: str) -> int: + """Cheap token estimate good enough for bounding prompt context.""" + if not text: + return 0 + return math.ceil(len(text) / CHARS_PER_TOKEN) + + +def truncate_to_token_limit(text: str, token_limit: int) -> tuple[str, bool]: + """Truncate text to an approximate token limit.""" + if token_limit <= 0: + return "", bool(text) + + max_chars = token_limit * CHARS_PER_TOKEN + if len(text) <= max_chars: + return text, False + + suffix = "\n\n[truncated to prompt_data_token_limit]" + allowed = max(0, max_chars - len(suffix)) + return f"{text[:allowed].rstrip()}{suffix}", True + + +def _section_header(index: int, name: str, source_type: str, url: str | None) -> str: + heading = f"Source {index}: {name} ({source_type})" + if url: + heading += f"\nURL: {url}" + return heading + + +def _get(source: Any, key: str, default: Any = None) -> Any: + if isinstance(source, Mapping): + return source.get(key, default) + return getattr(source, key, default) + + +def _dump_source(source: Any) -> dict[str, Any]: + if hasattr(source, "model_dump"): + return source.model_dump(exclude_none=True) + if isinstance(source, Mapping): + return {k: v for k, v in source.items() if v is not None} + return { + key: getattr(source, key) + for key in ("name", "type", "text", "url", "headers", "token_limit") + if getattr(source, key, None) is not None + } + + +async def _fetch_url_source(source: Any) -> str: + import aiohttp + + url = _get(source, "url") + _validate_fetch_url(url) + headers = _get(source, "headers") or {} + max_bytes = int(os.getenv("PROMPT_DATA_SOURCE_MAX_BYTES", DEFAULT_SOURCE_MAX_BYTES)) + + timeout = aiohttp.ClientTimeout(total=12) + try: + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.get(url, headers=headers, allow_redirects=False) as resp: + if 300 <= resp.status < 400: + raise PromptContextError( + f"Prompt data source '{url}' returned a redirect; redirects are not followed", + status_code=400, + ) + if resp.status >= 400: + raise PromptContextError( + f"Prompt data source '{url}' returned HTTP {resp.status}", + status_code=502, + ) + body = await resp.content.read(max_bytes + 1) + except PromptContextError: + raise + except Exception as e: + raise PromptContextError( + f"Could not fetch prompt data source '{url}': {e}", + status_code=502, + ) from e + + if len(body) > max_bytes: + body = body[:max_bytes] + + return body.decode("utf-8", errors="replace") + + +def _private_urls_allowed() -> bool: + return os.getenv("PROMPT_DATA_ALLOW_PRIVATE_URLS", "").lower() in { + "1", + "true", + "yes", + } + + +def _is_private_ip(value: str) -> bool: + parsed = ip_address(value) + return ( + parsed.is_private + or parsed.is_loopback + or parsed.is_link_local + or parsed.is_multicast + or parsed.is_reserved + or parsed.is_unspecified + ) + + +def _validate_fetch_url(url: str) -> None: + """Block obvious SSRF targets unless explicitly allowed.""" + parsed = urlparse(url) + if parsed.scheme not in {"http", "https"} or not parsed.hostname: + raise PromptContextError( + f"Invalid prompt data source URL: {url}", + status_code=400, + ) + + if _private_urls_allowed(): + return + + host = parsed.hostname + try: + if _is_private_ip(host): + raise PromptContextError( + f"Prompt data source URL host is private or local: {host}", + status_code=400, + ) + return + except ValueError: + pass + + try: + addresses = socket.getaddrinfo(host, None) + except socket.gaierror as e: + raise PromptContextError( + f"Could not resolve prompt data source host '{host}': {e}", + status_code=400, + ) from e + + for address in addresses: + resolved_ip = address[4][0] + if _is_private_ip(resolved_ip): + raise PromptContextError( + f"Prompt data source URL resolves to private or local address: {host}", + status_code=400, + ) + + +async def _load_source_text(source: Any) -> str: + source_type = _get(source, "type") + if source_type == "text": + return _get(source, "text") or "" + if source_type == "url": + return await _fetch_url_source(source) + raise PromptContextError(f"Unsupported prompt data source type: {source_type}") + + +async def load_prompt_context( + sources: Sequence[Any] | None, + total_token_limit: int, +) -> PromptContextResult: + """Load sources, truncate to budget, and return prompt-ready context.""" + if not sources or total_token_limit <= 0: + return PromptContextResult(block="", sources=[], estimated_tokens=0) + + remaining_tokens = total_token_limit + loaded_sources: list[dict[str, Any]] = [] + sections: list[str] = [] + + for index, source in enumerate(sources, start=1): + if remaining_tokens <= 0: + break + + name = _get(source, "name") or f"source_{index}" + source_type = _get(source, "type") + raw_text = (await _load_source_text(source)).strip() + source_limit = _get(source, "token_limit") + effective_limit = min(remaining_tokens, int(source_limit or remaining_tokens)) + header = _section_header(index, name, source_type, _get(source, "url")) + header_tokens = estimate_tokens(f"{header}\n") + content_limit = max(0, effective_limit - header_tokens) + text, truncated = truncate_to_token_limit(raw_text, content_limit) + tokens = header_tokens + estimate_tokens(text) + remaining_tokens = max(0, remaining_tokens - tokens) + + source_record = _dump_source(source) + source_record.pop("headers", None) + source_record.pop("text", None) + source_record.update( + { + "loaded": True, + "estimated_tokens": tokens, + "truncated": truncated, + } + ) + loaded_sources.append(source_record) + + if text: + sections.append(f"{header}\n{text}") + + if not sections: + return PromptContextResult(block="", sources=loaded_sources, estimated_tokens=0) + + block = ( + "External prompt context supplied by the API. Use it as background " + "knowledge for this meeting. Do not mention source mechanics unless asked.\n\n" + + "\n\n---\n\n".join(sections) + ) + return PromptContextResult( + block=block, + sources=loaded_sources, + estimated_tokens=estimate_tokens(block), + ) + + +def format_mcp_context(mcp: Any | None) -> str: + """Format MCP metadata as prompt context. Does not execute MCP tools.""" + if not mcp: + return "" + + data = mcp.model_dump(exclude_none=True) if hasattr(mcp, "model_dump") else mcp + if not isinstance(data, Mapping): + return "" + + lines = [ + "MCP context supplied by the API.", + "Use this as integration metadata. Do not claim to call MCP tools unless a tool result appears in the conversation context.", + ] + + instructions = data.get("instructions") + if instructions: + lines.append(f"Global instructions: {instructions}") + + servers = data.get("servers") or [] + for server in servers: + if not isinstance(server, Mapping): + continue + lines.append(f"- Server: {server.get('name', 'unnamed')}") + if server.get("url"): + lines.append(f" URL: {server['url']}") + if server.get("transport"): + lines.append(f" Transport: {server['transport']}") + tools = server.get("tools") or [] + if tools: + lines.append(f" Tools: {', '.join(tools)}") + if server.get("instructions"): + lines.append(f" Instructions: {server['instructions']}") + + return "\n".join(lines) + + +def merge_context_blocks(blocks: Iterable[str]) -> str: + """Merge non-empty context blocks.""" + return "\n\n".join(block.strip() for block in blocks if block and block.strip()) diff --git a/core/process.py b/core/process.py index b650616..6f20f92 100644 --- a/core/process.py +++ b/core/process.py @@ -9,6 +9,7 @@ import threading from meetingbaas_pipecat.utils.logger import logger +from utils.runtime import get_state_dir PIPECAT_PROCESSES: Dict[str, subprocess.Popen] = {} @@ -44,8 +45,18 @@ def start_pipecat_process( """ logger.info(f"Starting Pipecat process for client {client_id}") - # Convert persona_data to JSON string - persona_data_json = json.dumps(persona_data) + payload_dir = os.path.join(get_state_dir(), "persona_payloads") + os.makedirs(payload_dir, exist_ok=True) + persona_data_path = os.path.join(payload_dir, f"{client_id}.json") + payload_fd = os.open( + persona_data_path, + os.O_WRONLY | os.O_CREAT | os.O_TRUNC, + 0o600, + ) + with os.fdopen(payload_fd, "w") as f: + json.dump(persona_data, f) + if (persona_data or {}).get("mcp"): + logger.info(f"Passing MCP metadata to Pipecat process for client {client_id}") # Construct the command to run the meetingbaas.py script script_path = os.path.join( @@ -76,8 +87,8 @@ def start_pipecat_process( meeting_url, "--persona-name", persona_folder_name, - "--persona-data-json", - persona_data_json, + "--persona-data-file", + persona_data_path, "--streaming-audio-frequency", streaming_audio_frequency, ] @@ -146,6 +157,6 @@ def terminate_process_gracefully( # Try one last time with kill try: process.kill() - except: + except Exception: pass return False diff --git a/env.example b/env.example index d8eaaae..3e48ac3 100644 --- a/env.example +++ b/env.example @@ -23,6 +23,10 @@ CARTESIA_API_KEY=your_cartesia_api_key_here # Cartesia Voice Id - Default voice ID for personas CARTESIA_VOICE_ID="79a125e8-cd45-4c13-8a67-188112f4dd22" +# Default TTS speed multiplier when a request does not set speech_speed. +# Cartesia-backed runners clamp this to 0.6..1.5. +CARTESIA_TTS_SPEED=1.2 + ### ### MULTIPLE BOTS FUNCTIONALITY - can be ignored if only running one bot ### @@ -50,4 +54,17 @@ APP_ID=your_app_id_here BASE_URL=your_base_url_here # The port the API server will listen on. -PORT=7014 \ No newline at end of file +PORT=7014 + +# Maximum bytes fetched per external prompt_data_sources URL. +PROMPT_DATA_SOURCE_MAX_BYTES=1000000 + +# Set true only in trusted networks if URL prompt sources must fetch localhost/private IPs. +PROMPT_DATA_ALLOW_PRIVATE_URLS=false + +# MCP server connection details are supplied per /bots request. No global MCP +# secret is required here; use per-server mcp.servers[].env for stdio servers or +# mcp.servers[].headers for remote http/streamable_http/sse servers when needed. + +# Set true only in trusted networks if remote MCP URLs must reach localhost/private IPs. +MCP_ALLOW_PRIVATE_URLS=false diff --git a/meeting-baas-openapi-v1.json b/meeting-baas-openapi-v1.json new file mode 100644 index 0000000..6af5fc1 --- /dev/null +++ b/meeting-baas-openapi-v1.json @@ -0,0 +1,4703 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Meeting BaaS API", + "summary": "API for recording and transcribing video meetings across Zoom, Google Meet, and Microsoft Teams. Features include bot management, calendar integration, and transcription services.", + "description": "Meeting BaaS API", + "termsOfService": "https://meetingbaas.com/terms-and-conditions", + "version": "1.1" + }, + "servers": [ + { + "url": "https://api.meetingbaas.com", + "description": "Production server" + } + ], + "paths": { + "/bots/": { + "post": { + "summary": "Join", + "description": "Have a bot join a meeting, now or in the future. You can provide a `webhook_url` parameter to receive webhook events specific to this bot, overriding your account's default webhook URL. Events include recording completion, failures, and transcription updates.", + "operationId": "join", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JoinRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JoinResponse" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/bots/{uuid}": { + "delete": { + "summary": "Leave", + "description": "Leave", + "operationId": "leave", + "parameters": [ + { + "in": "path", + "name": "uuid", + "description": "The UUID identifier", + "required": true, + "schema": { + "description": "The UUID identifier", + "type": "string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveResponse" + } + } + } + } + } + } + }, + "/bots/meeting_data": { + "get": { + "summary": "Get Meeting Data", + "description": "Get meeting recording and metadata", + "operationId": "get_meeting_data", + "parameters": [ + { + "in": "query", + "name": "bot_id", + "required": true, + "schema": { + "type": "string" + }, + "style": "form" + }, + { + "in": "query", + "name": "include_transcripts", + "description": "Whether to include transcription data in the response. Defaults to true if not specified.", + "schema": { + "description": "Whether to include transcription data in the response. Defaults to true if not specified.", + "default": true, + "type": "boolean" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Metadata" + } + } + } + } + } + } + }, + "/bots/{uuid}/delete_data": { + "post": { + "summary": "Delete Data", + "description": "Deletes a bot's data including recording, transcription, and logs. Only metadata is retained. Rate limited to 5 requests per minute per API key.", + "operationId": "delete_data", + "parameters": [ + { + "in": "path", + "name": "uuid", + "description": "The UUID identifier", + "required": true, + "schema": { + "description": "The UUID identifier", + "type": "string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteResponse" + } + } + } + }, + "401": { + "description": "no content" + }, + "403": { + "description": "no content" + }, + "404": { + "description": "no content" + }, + "429": { + "description": "no content" + } + } + } + }, + "/bots/bots_with_metadata": { + "get": { + "summary": "List Bots with Metadata", + "description": "Retrieves a paginated list of the user's bots with essential metadata, including IDs, names, and meeting details. Supports filtering, sorting, and advanced querying options.", + "operationId": "bots_with_metadata", + "parameters": [ + { + "in": "query", + "name": "bot_name", + "description": "Filter bots by name containing this string.\n\nPerforms a case-insensitive partial match on the bot's name. Useful for finding bots with specific naming conventions or to locate a particular bot when you don't have its ID.\n\nExample: \"Sales\" would match \"Sales Meeting\", \"Quarterly Sales\", etc.", + "schema": { + "description": "Filter bots by name containing this string.\n\nPerforms a case-insensitive partial match on the bot's name. Useful for finding bots with specific naming conventions or to locate a particular bot when you don't have its ID.\n\nExample: \"Sales\" would match \"Sales Meeting\", \"Quarterly Sales\", etc.", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "created_after", + "description": "Filter bots created after this date (ISO format).\n\nLimits results to bots created at or after the specified timestamp. Used for time-based filtering to find recent additions.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-01T00:00:00\"", + "schema": { + "description": "Filter bots created after this date (ISO format).\n\nLimits results to bots created at or after the specified timestamp. Used for time-based filtering to find recent additions.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-01T00:00:00\"", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "created_before", + "description": "Filter bots created before this date (ISO format).\n\nLimits results to bots created at or before the specified timestamp. Used for time-based filtering to exclude recent additions.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-31T23:59:59\"", + "schema": { + "description": "Filter bots created before this date (ISO format).\n\nLimits results to bots created at or before the specified timestamp. Used for time-based filtering to exclude recent additions.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-31T23:59:59\"", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "cursor", + "description": "Cursor for pagination, obtained from previous response.\n\nUsed for retrieving the next set of results after a previous call. The cursor value is returned in the `nextCursor` field of responses that have additional results available.\n\nFormat: Base64-encoded string containing pagination metadata", + "schema": { + "description": "Cursor for pagination, obtained from previous response.\n\nUsed for retrieving the next set of results after a previous call. The cursor value is returned in the `nextCursor` field of responses that have additional results available.\n\nFormat: Base64-encoded string containing pagination metadata", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "ended_after", + "description": "Filter bots ended after this date (ISO format).\n\nLimits results to bots that ended at or after the specified timestamp. Useful for finding completed meetings within a specific time period.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-01T00:00:00\"", + "schema": { + "description": "Filter bots ended after this date (ISO format).\n\nLimits results to bots that ended at or after the specified timestamp. Useful for finding completed meetings within a specific time period.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-01T00:00:00\"", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "filter_by_extra", + "description": "Filter bots by matching values in the extra JSON payload.\n\nThis parameter performs in-memory filtering on the `extra` JSON field, similar to a SQL WHERE clause. It reduces the result set to only include bots that match all specified conditions.\n\nFormat specifications: - Single condition: \"field:value\" - Multiple conditions: \"field1:value1,field2:value2\"\n\nExamples: - \"customer_id:12345\" - Only bots with this customer ID - \"status:active,project:sales\" - Only active bots from sales projects\n\nNotes: - All conditions must match for a bot to be included - Values are matched exactly (case-sensitive) - Bots without the specified field are excluded", + "schema": { + "description": "Filter bots by matching values in the extra JSON payload.\n\nThis parameter performs in-memory filtering on the `extra` JSON field, similar to a SQL WHERE clause. It reduces the result set to only include bots that match all specified conditions.\n\nFormat specifications: - Single condition: \"field:value\" - Multiple conditions: \"field1:value1,field2:value2\"\n\nExamples: - \"customer_id:12345\" - Only bots with this customer ID - \"status:active,project:sales\" - Only active bots from sales projects\n\nNotes: - All conditions must match for a bot to be included - Values are matched exactly (case-sensitive) - Bots without the specified field are excluded", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of bots to return in a single request.\n\nLimits the number of results returned in a single API call. This parameter helps control response size and page length.\n\nDefault: 10 Minimum: 1 Maximum: 50", + "schema": { + "description": "Maximum number of bots to return in a single request.\n\nLimits the number of results returned in a single API call. This parameter helps control response size and page length.\n\nDefault: 10 Minimum: 1 Maximum: 50", + "default": 10, + "type": "integer", + "format": "int32" + }, + "style": "form" + }, + { + "in": "query", + "name": "meeting_url", + "description": "Filter bots by meeting URL containing this string.\n\nPerforms a case-insensitive partial match on the bot's meeting URL. Use this to find bots associated with specific meeting platforms or particular meeting IDs.\n\nExample: \"zoom.us\" would match all Zoom meetings", + "schema": { + "description": "Filter bots by meeting URL containing this string.\n\nPerforms a case-insensitive partial match on the bot's meeting URL. Use this to find bots associated with specific meeting platforms or particular meeting IDs.\n\nExample: \"zoom.us\" would match all Zoom meetings", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "sort_by_extra", + "description": "Sort the results by a field in the extra JSON payload.\n\nThis parameter performs in-memory sorting on the `extra` JSON field, similar to a SQL ORDER BY clause. It changes the order of results but not which results are included.\n\nFormat specifications: - Default (ascending): \"field\" - Explicit direction: \"field:asc\" or \"field:desc\"\n\nExamples: - \"customer_id\" - Sort by customer_id (ascending) - \"priority:desc\" - Sort by priority (descending)\n\nNotes: - Applied after all filtering - String comparison is used for sorting - Bots with the field come before bots without it - Can be combined with filter_by_extra", + "schema": { + "description": "Sort the results by a field in the extra JSON payload.\n\nThis parameter performs in-memory sorting on the `extra` JSON field, similar to a SQL ORDER BY clause. It changes the order of results but not which results are included.\n\nFormat specifications: - Default (ascending): \"field\" - Explicit direction: \"field:asc\" or \"field:desc\"\n\nExamples: - \"customer_id\" - Sort by customer_id (ascending) - \"priority:desc\" - Sort by priority (descending)\n\nNotes: - Applied after all filtering - String comparison is used for sorting - Bots with the field come before bots without it - Can be combined with filter_by_extra", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "speaker_name", + "description": "NOTE: this is a preview feature and not yet available\n\nFilter bots by speaker name containing this string.\n\nPerforms a case-insensitive partial match on the speakers in the meeting. Useful for finding meetings that included a specific person.\n\nExample: \"John\" would match meetings with speakers like \"John Smith\" or \"John Doe\"", + "schema": { + "description": "NOTE: this is a preview feature and not yet available\n\nFilter bots by speaker name containing this string.\n\nPerforms a case-insensitive partial match on the speakers in the meeting. Useful for finding meetings that included a specific person.\n\nExample: \"John\" would match meetings with speakers like \"John Smith\" or \"John Doe\"", + "type": [ + "string", + "null" + ] + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListRecentBotsResponse" + } + } + } + } + } + } + }, + "/bots/retranscribe": { + "post": { + "summary": "Retranscribe Bot", + "description": "Transcribe or retranscribe a bot's audio using the Default or your provided Speech to Text Provider", + "operationId": "retranscribe_bot", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RetranscribeBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "description" + }, + "202": { + "description": "no content" + } + } + } + }, + "/bots/{uuid}/screenshots": { + "get": { + "summary": "Get Screenshots", + "description": "Retrieves screenshots captured during the bot's session", + "operationId": "get_screenshots", + "parameters": [ + { + "in": "path", + "name": "uuid", + "description": "The UUID identifier", + "required": true, + "schema": { + "description": "The UUID identifier", + "type": "string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScreenshotsList" + } + } + } + } + } + } + }, + "/bots/webhooks": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "Webhook Events Documentation", + "description": "Meeting BaaS sends webhook events to your configured webhook URL when specific events occur.\n\n## Webhook Event Types\n\n### 1. `complete`\nSent when a bot successfully completes recording a meeting. Contains full transcription data and a link to the recording.\n```json\n{\n \\\"event\\\": \\\"complete\\\",\n \\\"data\\\": {\n \\\"bot_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\",\n \\\"event_uuid\\\": \\\"123e4567-e89b-12d3-a456-426614174001\\\",\n \\\"transcript\\\": [\n {\n \\\"speaker\\\": \\\"John Doe\\\",\n \\\"offset\\\": 1.5,\n \\\"start_time\\\": 1.5,\n \\\"end_time\\\": 2.4,\n \\\"words\\\": [\n {\n \\\"start\\\": 1.5,\n \\\"end\\\": 1.9,\n \\\"word\\\": \\\"Hello\\\"\n },\n {\n \\\"start\\\": 2.0,\n \\\"end\\\": 2.4,\n \\\"word\\\": \\\"everyone\\\"\n }\n ]\n }\n ],\n \\\"speakers\\\": [\n \\\"Jane Smith\\\",\n \\\"John Doe\\\"\n ],\n \\\"mp4\\\": \\\"https://storage.example.com/recordings/video123.mp4?token=abc\\\",\n \\\"audio\\\": \\\"https://storage.example.com/recordings/audio123.wav?token=abc\\\",\n \\\"event\\\": \\\"complete\\\",\n \\\"extra\\\": {\n \\\"foo\\\": \\\"bar\\\"\n }\n }\n}\n```\n\nThe `complete` event includes:\n- **bot_id**: Unique identifier for the bot that completed recording\n- **event_uuid**: UUID of the calendar event (if this bot was created from an event)\n- **speakers**: A set of speaker names identified in the meeting\n- **transcript**: Full transcript data with speaker identification and word timing\n- **mp4**: URL to the recording file (valid for 24 hours by default)\n- **event**: Event type identifier (\"complete\")\n\n### 2. `failed`\nSent when a bot fails to join or record a meeting. Contains error details.\n```json\n{\n \\\"event\\\": \\\"failed\\\",\n \\\"data\\\": {\n \\\"bot_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\",\n \\\"event_uuid\\\": \\\"123e4567-e89b-12d3-a456-426614174001\\\",\n \\\"error\\\": \\\"meeting_not_found\\\",\n \\\"message\\\": \\\"Could not join meeting: The meeting ID was not found or has expired\\\",\n \\\"extra\\\": {\n \\\"foo\\\": \\\"bar\\\"\n }\n }\n}\n```\n\nThe `failed` event includes:\n- **bot_id**: Unique identifier for the bot that failed\n- **event_uuid**: UUID of the calendar event (if this bot was created from an event)\n- **error**: Error code identifying the type of failure\n- **message**: Detailed human-readable error message\n\nCommon error types include:\n- `meeting_not_found`: The meeting ID or link was invalid or expired\n- `access_denied`: The bot was denied access to the meeting\n- `authentication_error`: Failed to authenticate with the meeting platform\n- `network_error`: Network connectivity issues during recording\n- `internal_error`: Internal server error\n\n### 3. `calendar.sync_events`\nSent when calendar events are synced. Contains information about which events were updated.\n```json\n{\n \\\"event\\\": \\\"calendar.sync_events\\\",\n \\\"data\\\": {\n \\\"calendar_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\",\n \\\"last_updated_ts\\\": \\\"2023-05-01T12:00:00Z\\\",\n \\\"affected_event_uuids\\\": [\n \\\"123e4567-e89b-12d3-a456-426614174001\\\",\n \\\"123e4567-e89b-12d3-a456-426614174002\\\"\n ]\n }\n}\n```\n\nThe `calendar.sync_events` event includes:\n- **calendar_id**: UUID of the calendar that was synced\n- **last_updated_ts**: ISO-8601 timestamp of when the sync occurred\n- **affected_event_uuids**: Array of UUIDs for calendar events that were added, updated, or deleted\n\nThis event is triggered when:\n- Calendar data is synced with the external provider (Google, Microsoft)\n- Multiple events may be created, updated, or deleted in a single sync operation\n- Use this event to update your local cache of calendar events\n\n### 4. `transcription_complete`\nSent when transcription is completed separately from recording (e.g., after retranscribing).\n```json\n{\n \\\"event\\\": \\\"transcription_complete\\\",\n \\\"data\\\": {\n \\\"bot_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\"\n }\n}\n```\n\nThe `transcription_complete` event includes:\n- **bot_id**: Unique identifier for the bot with the completed transcription\n\nThis event is sent when:\n- You request a retranscription via the `/bots/retranscribe` endpoint\n- An asynchronous transcription process completes after the recording has ended\n\n## Setting Up Webhooks\n\nYou can configure webhooks in two ways:\n1. **Account-level webhook URL**: Set a default webhook URL for all bots in your account using the `/accounts/webhook_url` endpoint\n2. **Bot-specific webhook URL**: Provide a `webhook_url` parameter when creating a bot with the `/bots` endpoint\n\nYour webhook endpoint must:\n- Accept POST requests with JSON payload\n- Return a 2xx status code to acknowledge receipt\n- Process requests within 10 seconds to avoid timeouts\n- Handle each event type appropriately based on the event type\n\nAll webhook requests include:\n- `x-meeting-baas-api-key` header with your API key for verification\n- `content-type: application/json` header\n- JSON body containing the event details\n\n## Webhook Reliability\n\nIf your endpoint fails to respond or returns an error, the system will attempt to retry the webhook delivery. For critical events, we recommend implementing:\n\n- Idempotency handling to prevent duplicate processing of the same event\n- Proper logging of webhook receipts for audit purposes\n- Asynchronous processing to quickly acknowledge receipt before handling the event data\n\nFor security, always validate the API key in the `x-meeting-baas-api-key` header matches your API key.", + "operationId": "webhook_documentation", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/bots/webhooks/bot": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "Bot Webhook Events Documentation", + "description": "Meeting BaaS sends the following webhook events related to bot recordings.\n\n## Bot Webhook Event Types\n\n### 1. `complete`\nSent when a bot successfully completes recording a meeting.\n\n**Payload Structure:**\n```json\n{\n \\\"event\\\": \\\"complete\\\",\n \\\"data\\\": {\n \\\"bot_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\",\n \\\"event_uuid\\\": \\\"123e4567-e89b-12d3-a456-426614174001\\\",\n \\\"transcript\\\": [\n {\n \\\"speaker\\\": \\\"John Doe\\\",\n \\\"offset\\\": 1.5,\n \\\"start_time\\\": 1.5,\n \\\"end_time\\\": 2.4,\n \\\"words\\\": [\n {\n \\\"start\\\": 1.5,\n \\\"end\\\": 1.9,\n \\\"word\\\": \\\"Hello\\\"\n },\n {\n \\\"start\\\": 2.0,\n \\\"end\\\": 2.4,\n \\\"word\\\": \\\"everyone\\\"\n }\n ]\n }\n ],\n \\\"speakers\\\": [\n \\\"Jane Smith\\\",\n \\\"John Doe\\\"\n ],\n \\\"mp4\\\": \\\"https://storage.example.com/recordings/video123.mp4?token=abc\\\",\n \\\"audio\\\": \\\"https://storage.example.com/recordings/audio123.wav?token=abc\\\",\n \\\"event\\\": \\\"complete\\\",\n \\\"extra\\\": {\n \\\"foo\\\": \\\"bar\\\"\n }\n }\n}\n```\n\n**When it's triggered:**\n- After a bot successfully records and processes a meeting\n- After the recording is uploaded and made available\n- When all processing of the meeting recording is complete\n\n**What to do with it:**\n- Download the MP4 recording for storage in your system\n- Store the transcript data in your database\n- Update meeting status in your application\n- Notify users that the recording is available\n- Use `event_uuid` to correlate with calendar events (if applicable)\n\n### 2. `failed`\nSent when a bot fails to join or record a meeting.\n\n**Payload Structure:**\n```json\n{\n \\\"event\\\": \\\"failed\\\",\n \\\"data\\\": {\n \\\"bot_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\",\n \\\"event_uuid\\\": \\\"123e4567-e89b-12d3-a456-426614174001\\\",\n \\\"error\\\": \\\"meeting_not_found\\\",\n \\\"message\\\": \\\"Could not join meeting: The meeting ID was not found or has expired\\\",\n \\\"extra\\\": {\n \\\"foo\\\": \\\"bar\\\"\n }\n }\n}\n```\n\n**Common error types:**\n- `meeting_not_found`: The meeting ID or link was invalid or expired\n- `access_denied`: The bot was denied access to the meeting\n- `authentication_error`: Failed to authenticate with the meeting platform\n- `network_error`: Network connectivity issues during recording\n- `internal_error`: Internal server error\n\n**What to do with it:**\n- Log the failure for troubleshooting\n- Notify administrators or users about the failed recording\n- Attempt to reschedule if appropriate\n- Update meeting status in your system\n- Use `event_uuid` to correlate with calendar events (if applicable)\n\n### 3. `transcription_complete`\nSent when transcription is completed separately from recording.\n\n**Payload Structure:**\n```json\n{\n \\\"event\\\": \\\"transcription_complete\\\",\n \\\"data\\\": {\n \\\"bot_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\"\n }\n}\n```\n\n**When it's triggered:**\n- After requesting retranscription via the API\n- When an asynchronous transcription job completes\n- When a higher quality or different language transcription becomes available\n\n**What to do with it:**\n- Update the transcript data in your system\n- Notify users that improved transcription is available\n- Run any post-processing on the new transcript data\n\n## Webhook Usage Tips\n\n- Each event includes the `bot_id` so you can correlate with your internal data\n- The `event_uuid` field is included when the bot was created from a calendar event (null for direct bots or scheduled bots)\n- The complete event includes speaker identification and full transcript data\n- For downloading recordings, the mp4 URL is valid for 24 hours\n- Handle the webhook asynchronously and return 200 OK quickly to prevent timeouts\n\nFor security, always validate the API key in the `x-meeting-baas-api-key` header matches your API key.", + "operationId": "bot_webhook_documentation", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/bots/webhooks/calendar": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "Calendar Webhook Events Documentation", + "description": "Meeting BaaS sends the following webhook events related to calendar integrations.\n\n## Calendar Webhook Event Types\n\n### 1. `calendar.sync_events`\nSent when calendar events are synced with external providers.\n\n**Payload Structure:**\n```json\n{\n \\\"event\\\": \\\"calendar.sync_events\\\",\n \\\"data\\\": {\n \\\"calendar_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\",\n \\\"last_updated_ts\\\": \\\"2023-05-01T12:00:00Z\\\",\n \\\"affected_event_uuids\\\": [\n \\\"123e4567-e89b-12d3-a456-426614174001\\\",\n \\\"123e4567-e89b-12d3-a456-426614174002\\\"\n ]\n }\n}\n```\n\n**When it's triggered:**\n- After initial calendar connection is established\n- When external calendar providers (Google, Microsoft) send change notifications\n- After manual calendar resync operations\n- During scheduled periodic syncs\n- When events are created, updated, or deleted in the source calendar\n\n**What to do with it:**\n- Update your local copy of calendar events\n- Process any new events that match your criteria\n- Remove any deleted events from your system\n- Update schedules for any modified events\n- Refresh your UI to show the latest calendar data\n\n**Field details:**\n- `calendar_id`: The UUID of the synchronized calendar\n- `last_updated_ts`: ISO-8601 timestamp when the sync occurred\n- `affected_event_uuids**: Array of UUIDs for events that were changed\n\n## Integration with Meeting BaaS Calendar API\n\nAfter receiving a calendar webhook event, you can:\n1. Use the `/calendar_events` endpoint to retrieve detailed information about specific events\n2. Use the `/calendars/:uuid` endpoint to get calendar metadata\n3. Schedule recording bots for any new meetings with the `/calendar_events/:uuid/bot` endpoint\n\n## Webhook Usage Tips\n\n- Each event includes affected event UUIDs for efficient processing\n- You don't need to retrieve all calendar events - just process the changed ones\n- The timestamp helps determine the sequence of updates\n- For high-frequency calendars, consider batch processing of multiple events\n\nFor security, always validate the API key in the `x-meeting-baas-api-key` header matches your API key.", + "operationId": "calendar_webhook_documentation", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/calendars/raw": { + "post": { + "tags": [ + "Calendars" + ], + "summary": "List Raw Calendars", + "description": "Retrieves unprocessed calendar data directly from the provider (Google, Microsoft) using provided OAuth credentials. This endpoint is typically used during the initial setup process to allow users to select which calendars to integrate. Returns a list of available calendars with their unique IDs, email addresses, and primary status. This data is not persisted until a calendar is formally created using the create_calendar endpoint.", + "operationId": "list_raw_calendars", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListRawCalendarsParams" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListRawCalendarsResponse" + } + } + } + } + } + } + }, + "/calendars/": { + "get": { + "tags": [ + "Calendars" + ], + "summary": "List Calendars", + "description": "Retrieves all calendars that have been integrated with the system for the authenticated user. Returns a list of calendars with their names, email addresses, provider information, and sync status. This endpoint shows only calendars that have been formally connected through the create_calendar endpoint, not all available calendars from the provider.", + "operationId": "list_calendars", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Calendar" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Calendars" + ], + "summary": "Create Calendar", + "description": "Integrates a new calendar with the system using OAuth credentials. This endpoint establishes a connection with the calendar provider (Google, Microsoft), sets up webhook notifications for real-time updates, and performs an initial sync of all calendar events. It requires OAuth credentials (client ID, client secret, and refresh token) and the platform type. Once created, the calendar is assigned a unique UUID that should be used for all subsequent operations. Returns the newly created calendar object with all integration details.", + "operationId": "create_calendar", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCalendarParams" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCalendarResponse" + } + } + } + } + } + } + }, + "/calendars/resync_all": { + "post": { + "tags": [ + "Calendars" + ], + "summary": "Resync All Calendars", + "description": "Forces a sync of all your connected calendars with their providers (Google, Microsoft).\n\nProcesses each calendar individually and returns:\n- `synced_calendars`: UUIDs of successfully synced calendars\n- `errors`: Details of any failures\n\nSends webhook notifications for calendars with updates.", + "operationId": "resync_all_calendars", + "parameters": [ + { + "in": "query", + "name": "days", + "description": "Number of days to sync forward (default: 30 for rolling window)", + "schema": { + "description": "Number of days to sync forward (default: 30 for rolling window)", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResyncAllCalendarsResponse" + } + } + } + } + } + } + }, + "/calendars/{uuid}": { + "get": { + "tags": [ + "Calendars" + ], + "summary": "Get Calendar", + "description": "Retrieves detailed information about a specific calendar integration by its UUID. Returns comprehensive calendar data including the calendar name, email address, provider details (Google, Microsoft), sync status, and other metadata. This endpoint is useful for displaying calendar information to users or verifying the status of a calendar integration before performing operations on its events.", + "operationId": "get_calendar", + "parameters": [ + { + "in": "path", + "name": "uuid", + "description": "The UUID identifier", + "required": true, + "schema": { + "description": "The UUID identifier", + "type": "string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Calendar" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Calendars" + ], + "summary": "Delete Calendar", + "description": "Permanently removes a calendar integration by its UUID, including all associated events and bot configurations. This operation cancels any active subscriptions with the calendar provider, stops all webhook notifications, and unschedules any pending recordings. All related resources are cleaned up in the database. This action cannot be undone, and subsequent requests to this calendar's UUID will return 404 Not Found errors.", + "operationId": "delete_calendar", + "parameters": [ + { + "in": "path", + "name": "uuid", + "description": "The UUID identifier", + "required": true, + "schema": { + "description": "The UUID identifier", + "type": "string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "no content" + } + } + }, + "patch": { + "tags": [ + "Calendars" + ], + "summary": "Update Calendar", + "description": "Updates a calendar integration with new credentials or platform while maintaining the same UUID. This operation is performed as an atomic transaction to ensure data integrity. The system automatically unschedules existing bots to prevent duplicates, updates the calendar credentials, and triggers a full resync of all events. Useful when OAuth tokens need to be refreshed or when migrating a calendar between providers. Returns the updated calendar object with its new configuration.", + "operationId": "update_calendar", + "parameters": [ + { + "in": "path", + "name": "uuid", + "description": "The UUID identifier", + "required": true, + "schema": { + "description": "The UUID identifier", + "type": "string" + }, + "style": "simple" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCalendarParams" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCalendarResponse" + } + } + } + } + } + } + }, + "/calendar_events/{uuid}": { + "get": { + "tags": [ + "Calendars" + ], + "summary": "Get Event", + "description": "Retrieves comprehensive details about a specific calendar event by its UUID. Returns complete event information including title, meeting link, start and end times, organizer status, recurrence information, and the full list of attendees with their names and email addresses. Also includes any associated bot parameters if recording is scheduled for this event. The raw calendar data from the provider is also included for advanced use cases.", + "operationId": "get_event", + "parameters": [ + { + "in": "path", + "name": "uuid", + "description": "The UUID identifier", + "required": true, + "schema": { + "description": "The UUID identifier", + "type": "string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + } + } + } + } + }, + "/calendar_events/{uuid}/bot": { + "post": { + "tags": [ + "Calendars" + ], + "summary": "Schedule Record Event", + "description": "Configures a bot to automatically join and record a specific calendar event at its scheduled time. The UUID in the request path is the event UUID. The request body contains detailed bot configuration, including recording options, streaming settings, and webhook notification URLs. For recurring events, the 'all_occurrences' parameter can be set to true to schedule recording for all instances of the recurring series, or false (default) to schedule only the specific instance. Returns the updated event(s) with the bot parameters attached.", + "operationId": "schedule_record_event", + "parameters": [ + { + "in": "path", + "name": "uuid", + "description": "The UUID identifier", + "required": true, + "schema": { + "description": "The UUID identifier", + "type": "string" + }, + "style": "simple" + }, + { + "in": "query", + "name": "all_occurrences", + "description": "schedule a bot to all occurences of a recurring event", + "schema": { + "description": "schedule a bot to all occurences of a recurring event", + "type": [ + "boolean", + "null" + ] + }, + "style": "form" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BotParam2" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Event" + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "Calendars" + ], + "summary": "Unschedule Record Event", + "description": "Cancels a previously scheduled recording for a calendar event and releases associated bot resources. For recurring events, the 'all_occurrences' parameter controls whether to unschedule from all instances of the recurring series or just the specific occurrence. This operation is idempotent and will not error if no bot was scheduled. Returns the updated event(s) with the bot parameters removed.", + "operationId": "unschedule_record_event", + "parameters": [ + { + "in": "path", + "name": "uuid", + "description": "The UUID identifier", + "required": true, + "schema": { + "description": "The UUID identifier", + "type": "string" + }, + "style": "simple" + }, + { + "in": "query", + "name": "all_occurrences", + "description": "unschedule a bot from all occurences of a recurring event", + "schema": { + "description": "unschedule a bot from all occurences of a recurring event", + "type": [ + "boolean", + "null" + ] + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Event" + } + } + } + } + } + } + }, + "patch": { + "tags": [ + "Calendars" + ], + "summary": "Patch Bot", + "description": "Updates the configuration of a bot already scheduled to record an event. Allows modification of recording settings, webhook URLs, and other bot parameters without canceling and recreating the scheduled recording. For recurring events, the 'all_occurrences' parameter determines whether changes apply to all instances or just the specific occurrence. Returns the updated event(s) with the modified bot parameters.", + "operationId": "patch_bot", + "parameters": [ + { + "in": "path", + "name": "uuid", + "description": "The UUID identifier", + "required": true, + "schema": { + "description": "The UUID identifier", + "type": "string" + }, + "style": "simple" + }, + { + "in": "query", + "name": "all_occurrences", + "description": "schedule a bot to all occurences of a recurring event", + "schema": { + "description": "schedule a bot to all occurences of a recurring event", + "type": [ + "boolean", + "null" + ] + }, + "style": "form" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BotParam3" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Event" + } + } + } + } + } + } + } + }, + "/calendar_events/": { + "get": { + "tags": [ + "Calendars" + ], + "summary": "List Events", + "description": "Retrieves a paginated list of calendar events with comprehensive filtering options. Supports filtering by organizer email, attendee email, date ranges (start_date_gte, start_date_lte), and event status. Results can be limited to upcoming events (default), past events, or all events. Each event includes full details such as meeting links, participants, and recording status. The response includes a 'next' pagination cursor for retrieving additional results.", + "operationId": "list_events", + "parameters": [ + { + "in": "query", + "name": "attendee_email", + "description": "If provided, filters events to include only those with this attendee's email address Example: \"jane.smith@example.com\"", + "schema": { + "description": "If provided, filters events to include only those with this attendee's email address Example: \"jane.smith@example.com\"", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "calendar_id", + "description": "Calendar ID to filter events by This is required to specify which calendar's events to retrieve", + "required": true, + "schema": { + "description": "Calendar ID to filter events by This is required to specify which calendar's events to retrieve", + "type": "string" + }, + "style": "form" + }, + { + "in": "query", + "name": "cursor", + "description": "Optional cursor for pagination This value is included in the `next` field of the previous response", + "schema": { + "description": "Optional cursor for pagination This value is included in the `next` field of the previous response", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "organizer_email", + "description": "If provided, filters events to include only those with this organizer's email address Example: \"john.doe@example.com\"", + "schema": { + "description": "If provided, filters events to include only those with this organizer's email address Example: \"john.doe@example.com\"", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "start_date_gte", + "description": "If provided, filters events to include only those with a start date greater than or equal to this timestamp Format: ISO-8601 string, e.g., \"2023-01-01T00:00:00Z\"", + "schema": { + "description": "If provided, filters events to include only those with a start date greater than or equal to this timestamp Format: ISO-8601 string, e.g., \"2023-01-01T00:00:00Z\"", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "start_date_lte", + "description": "If provided, filters events to include only those with a start date less than or equal to this timestamp Format: ISO-8601 string, e.g., \"2023-12-31T23:59:59Z\"", + "schema": { + "description": "If provided, filters events to include only those with a start date less than or equal to this timestamp Format: ISO-8601 string, e.g., \"2023-12-31T23:59:59Z\"", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "status", + "description": "Filter events by meeting status Valid values: \"upcoming\" (default) returns events after current time, \"past\" returns previous events, \"all\" returns both", + "schema": { + "description": "Filter events by meeting status Valid values: \"upcoming\" (default) returns events after current time, \"past\" returns previous events, \"all\" returns both", + "type": [ + "string", + "null" + ] + }, + "style": "form" + }, + { + "in": "query", + "name": "updated_at_gte", + "description": "If provided, fetches only events updated at or after this timestamp Format: ISO-8601 string, e.g., \"2023-01-01T00:00:00Z\"", + "schema": { + "description": "If provided, fetches only events updated at or after this timestamp Format: ISO-8601 string, e.g., \"2023-01-01T00:00:00Z\"", + "type": [ + "string", + "null" + ] + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListEventResponse" + } + } + } + } + } + } + }, + "/zoom_oauth_connections/": { + "get": { + "tags": [ + "Zoom OAuth" + ], + "summary": "List Zoom OAuth Connections", + "description": "Retrieves all Zoom OAuth connections associated with the authenticated account. Each connection represents a Zoom user who has authorized your app via OAuth. Use this to display connected users or to find the `zoom_user_id` needed for the `zoom_obf_token_user_id` bot parameter. Sensitive token data is never included in the response.", + "operationId": "list_zoom_oauth_connections", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ZoomOAuthConnectionResponse" + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "post": { + "tags": [ + "Zoom OAuth" + ], + "summary": "Create Zoom OAuth Connection", + "description": "Exchanges a Zoom OAuth authorization code for access and refresh tokens, retrieves the Zoom user's profile, and stores the connection for managed OBF token generation. The authorization code is obtained by directing a Zoom user through the OAuth consent flow for your Zoom OAuth app. Once stored, you can reference this connection's `zoom_user_id` as the `zoom_obf_token_user_id` parameter when creating a bot, and the system will automatically fetch a fresh OBF token at join time. Note: the authorization code is single-use and expires in approximately 10 minutes.", + "operationId": "create_zoom_oauth_connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateConnectionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Error response" + }, + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ZoomOAuthConnectionResponse" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/zoom_oauth_connections/{uuid}": { + "get": { + "tags": [ + "Zoom OAuth" + ], + "summary": "Get Zoom OAuth Connection", + "description": "Retrieves a specific Zoom OAuth connection by its UUID. Returns the connection details including the Zoom user ID, account ID, connection state, and granted scopes. Sensitive token data is never included in the response.", + "operationId": "get_zoom_oauth_connection", + "parameters": [ + { + "in": "path", + "name": "uuid", + "description": "The UUID identifier", + "required": true, + "schema": { + "description": "The UUID identifier", + "type": "string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ZoomOAuthConnectionResponse" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Zoom OAuth" + ], + "summary": "Delete Zoom OAuth Connection", + "description": "Permanently deletes a Zoom OAuth connection by its UUID, removing all stored tokens. After deletion, bots using this connection's `zoom_user_id` as `zoom_obf_token_user_id` will no longer be able to automatically fetch OBF tokens. The Zoom user would need to re-authorize to create a new connection.", + "operationId": "delete_zoom_oauth_connection", + "parameters": [ + { + "in": "path", + "name": "uuid", + "description": "The UUID identifier", + "required": true, + "schema": { + "description": "The UUID identifier", + "type": "string" + }, + "style": "simple" + } + ], + "responses": { + "200": { + "description": "Error response" + }, + "204": { + "description": "no content" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + } + }, + "components": { + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "x-meeting-baas-api-key", + "description": "API key for authentication" + } + }, + "schemas": { + "Account": { + "description": "This structure represents the user's account information.", + "type": "object", + "required": [ + "created_at", + "email", + "id", + "secret", + "status" + ], + "properties": { + "company_name": { + "type": [ + "string", + "null" + ] + }, + "created_at": { + "$ref": "#/components/schemas/SystemTime" + }, + "email": { + "type": "string" + }, + "firstname": { + "type": [ + "string", + "null" + ] + }, + "id": { + "type": "integer", + "format": "int32" + }, + "lastname": { + "type": [ + "string", + "null" + ] + }, + "phone": { + "type": [ + "string", + "null" + ] + }, + "secret": { + "type": "string" + }, + "status": { + "type": "integer", + "format": "int32" + } + } + }, + "AccountInfos": { + "description": "Structure consisting account information.", + "type": "object", + "required": [ + "account" + ], + "properties": { + "account": { + "$ref": "#/components/schemas/Account" + } + } + }, + "ApiKeyResponse": { + "type": "object", + "required": [ + "api_key" + ], + "properties": { + "api_key": { + "type": "string" + } + } + }, + "Attendee": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "description": "The email address of the meeting attendee", + "type": "string" + }, + "name": { + "description": "The display name of the attendee if available from the calendar provider (Google, Microsoft)", + "type": [ + "string", + "null" + ] + } + } + }, + "AudioFile": { + "type": "object", + "required": [ + "file_extension", + "file_type", + "filename" + ], + "properties": { + "file_extension": { + "type": "string" + }, + "file_type": { + "type": "string" + }, + "filename": { + "type": "string" + } + } + }, + "AudioFrequency": { + "type": "string", + "enum": [ + "16khz", + "24khz" + ] + }, + "AutomaticLeaveRequest": { + "type": "object", + "properties": { + "noone_joined_timeout": { + "description": "The timeout in seconds for the bot to wait for participants to join before leaving the meeting, defaults to 600 seconds (10 minutes). Minimum: 120 seconds (2 minutes). Maximum: 1800 seconds (30 minutes). When a bot first joins a meeting, it uses this timeout to determine if any participants have joined. If no participants are detected within this period, the bot will leave the meeting. Once participants are detected, the silence_timeout takes over. Applies to Google Meet and Microsoft Teams only.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, + "silence_timeout": { + "description": "The timeout in seconds for the bot to leave the meeting if no speaker activity is detected, defaults to 600 seconds (10 minutes). Minimum: 300 seconds (5 minutes). Maximum: 1800 seconds (30 minutes). This timeout becomes active after participants are detected. The bot monitors for audio activity, and if no sound is detected for the duration of this timeout, it will automatically leave the meeting. Important: Configure these timeouts carefully to ensure the bot doesn't leave too early - the noone_joined_timeout should be long enough to wait for late joiners, and the silence_timeout should account for intentional pauses. Applies to Google Meet and Microsoft Teams only.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, + "waiting_room_timeout": { + "description": "The timeout in seconds for the bot to wait in the waiting room before leaving the meeting, defaults to 600 seconds (10 minutes). Minimum: 120 seconds (2 minutes). Maximum: 1800 seconds (30 minutes). Note: Google Meet also has it's own waiting room timeout (about ~10 minutes). Setting a higher value for such meetings would have no effect because Google Meet will deny entry to the bot after its own timeout.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + } + } + }, + "Bot": { + "type": "object", + "required": [ + "bot", + "duration", + "params" + ], + "properties": { + "bot": { + "$ref": "#/components/schemas/Bot2" + }, + "duration": { + "type": "integer", + "format": "int64" + }, + "params": { + "$ref": "#/components/schemas/BotParam" + } + } + }, + "Bot2": { + "type": "object", + "required": [ + "account_id", + "bot_exited_at", + "bot_joined_at", + "bot_param_id", + "created_at", + "diarization_v2", + "ended_at", + "id", + "meeting_url", + "mp4_s3_path", + "reserved", + "uuid" + ], + "properties": { + "account_id": { + "type": "integer", + "format": "int32" + }, + "bot_exited_at": { + "$ref": "#/components/schemas/OptionalDateTime" + }, + "bot_joined_at": { + "$ref": "#/components/schemas/OptionalDateTime" + }, + "bot_param_id": { + "type": "integer", + "format": "int32" + }, + "created_at": { + "$ref": "#/components/schemas/DateTime" + }, + "diarization_fails": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "diarization_v2": { + "type": "boolean" + }, + "ended_at": { + "$ref": "#/components/schemas/OptionalDateTime" + }, + "errors": { + "type": [ + "string", + "null" + ] + }, + "event_id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "meeting_url": { + "type": "string" + }, + "mp4_s3_path": { + "type": "string" + }, + "reserved": { + "type": "boolean" + }, + "scheduled_bot_id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "session_id": { + "type": [ + "string", + "null" + ] + }, + "transcription_fails": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "transcription_payloads": true, + "user_reported_error": true, + "uuid": { + "type": "string", + "format": "uuid" + } + } + }, + "BotCrashedQuery": { + "type": "object", + "required": [ + "bot_uuid" + ], + "properties": { + "bot_uuid": { + "type": "string" + } + } + }, + "BotCrashedRequest": { + "type": "object", + "required": [ + "crashReason" + ], + "properties": { + "crashReason": { + "type": "string" + }, + "efsManifest": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/EfsManifestEntry" + } + }, + "exitCode": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "signal": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "BotData": { + "type": "object", + "required": [ + "bot", + "transcripts" + ], + "properties": { + "bot": { + "$ref": "#/components/schemas/BotWithParams" + }, + "event_uuid": { + "description": "UUID of the calendar event (if this bot was created from an event)", + "type": [ + "string", + "null" + ], + "format": "uuid" + }, + "transcripts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Transcript" + } + } + } + }, + "BotPagined": { + "description": "Paginated bot results with status information\n\nThis response includes bots with their status information and pagination metadata to support loading additional results.\n\nThe bots are automatically sorted by status priority (critical issues first).", + "type": "object", + "required": [ + "bots", + "has_more" + ], + "properties": { + "bots": { + "description": "List of bots with status information, sorted by priority", + "type": "array", + "items": { + "$ref": "#/components/schemas/BotWithStatus" + } + }, + "has_more": { + "description": "Whether there are more results available at the next offset", + "type": "boolean" + } + } + }, + "BotParam": { + "type": "object", + "required": [ + "bot_name", + "extra", + "transcription_custom_parameters", + "webhook_url" + ], + "properties": { + "bot_image": { + "type": [ + "string", + "null" + ] + }, + "bot_name": { + "type": "string" + }, + "deduplication_key": { + "type": [ + "string", + "null" + ] + }, + "enter_message": { + "type": [ + "string", + "null" + ] + }, + "extra": { + "$ref": "#/components/schemas/Extra" + }, + "noone_joined_timeout": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "recording_mode": { + "anyOf": [ + { + "$ref": "#/components/schemas/RecordingMode" + }, + { + "type": "null" + } + ] + }, + "speech_to_text_api_key": { + "type": [ + "string", + "null" + ] + }, + "speech_to_text_provider": { + "anyOf": [ + { + "$ref": "#/components/schemas/SpeechToTextProvider" + }, + { + "type": "null" + } + ] + }, + "streaming_audio_frequency": { + "anyOf": [ + { + "$ref": "#/components/schemas/AudioFrequency" + }, + { + "type": "null" + } + ] + }, + "streaming_input": { + "type": [ + "string", + "null" + ] + }, + "streaming_output": { + "type": [ + "string", + "null" + ] + }, + "transcription_custom_parameters": { + "$ref": "#/components/schemas/Extra" + }, + "waiting_room_timeout": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "webhook_url": { + "type": "string" + }, + "zoom_access_token_url": { + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token": { + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token_url": { + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token_user_id": { + "type": [ + "string", + "null" + ] + }, + "zoom_sdk_id": { + "type": [ + "string", + "null" + ] + }, + "zoom_sdk_pwd": { + "type": [ + "string", + "null" + ] + } + } + }, + "BotParam2": { + "type": "object", + "required": [ + "bot_name" + ], + "properties": { + "bot_image": { + "type": [ + "string", + "null" + ] + }, + "bot_name": { + "type": "string" + }, + "deduplication_key": { + "type": [ + "string", + "null" + ] + }, + "enter_message": { + "type": [ + "string", + "null" + ] + }, + "extra": { + "default": null, + "$ref": "#/components/schemas/Extra" + }, + "noone_joined_timeout": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "recording_mode": { + "anyOf": [ + { + "$ref": "#/components/schemas/RecordingMode" + }, + { + "type": "null" + } + ] + }, + "silence_timeout": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "speech_to_text": { + "anyOf": [ + { + "$ref": "#/components/schemas/SpeechToText" + }, + { + "type": "null" + } + ] + }, + "streaming_audio_frequency": { + "anyOf": [ + { + "$ref": "#/components/schemas/AudioFrequency" + }, + { + "type": "null" + } + ] + }, + "streaming_input": { + "type": [ + "string", + "null" + ] + }, + "streaming_output": { + "type": [ + "string", + "null" + ] + }, + "transcription_custom_parameters": { + "default": null, + "$ref": "#/components/schemas/Extra" + }, + "waiting_room_timeout": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "webhook_url": { + "type": [ + "string", + "null" + ] + }, + "zoom_access_token_url": { + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token": { + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token_url": { + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token_user_id": { + "type": [ + "string", + "null" + ] + }, + "zoom_sdk_id": { + "type": [ + "string", + "null" + ] + }, + "zoom_sdk_pwd": { + "type": [ + "string", + "null" + ] + } + } + }, + "BotParam3": { + "type": "object", + "properties": { + "bot_image": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "bot_name": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "deduplication_key": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "enter_message": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "extra": { + "default": null + }, + "noone_joined_timeout": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "recording_mode": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/RecordingMode" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, + "speech_to_text": { + "default": null, + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/SpeechToText" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, + "streaming_audio_frequency": { + "default": null, + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/AudioFrequency" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, + "streaming_input": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "streaming_output": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "transcription_custom_parameters": { + "default": null + }, + "waiting_room_timeout": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "webhook_url": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "zoom_access_token_url": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token_url": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token_user_id": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "zoom_sdk_id": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "zoom_sdk_pwd": { + "default": null, + "type": [ + "string", + "null" + ] + } + } + }, + "BotStatusResponse": { + "description": "API response type for frontend developers to display and sort bot statuses\n\n# Fields * `value` - String representation of the status suitable for displaying and filtering * `type` - Category of status (success, error, warning, pending) * `details` - Optional detailed explanation of the status * `sort_priority` - Numeric value for sorting (lower = higher priority) * `category` - Logical grouping of similar status types", + "type": "object", + "required": [ + "category", + "sort_priority", + "type", + "value" + ], + "properties": { + "category": { + "description": "Logical grouping of status", + "type": "string" + }, + "details": { + "description": "Detailed explanation when available", + "type": [ + "string", + "null" + ] + }, + "sort_priority": { + "description": "Numeric priority for sorting (0 = highest priority)", + "type": "integer", + "format": "int32" + }, + "type": { + "description": "Status type (success, error, warning, pending)", + "type": "string" + }, + "value": { + "description": "Display text for the status", + "type": "string" + } + } + }, + "BotWithParams": { + "type": "object", + "required": [ + "account_id", + "bot_exited_at", + "bot_joined_at", + "bot_name", + "bot_param_id", + "created_at", + "diarization_v2", + "ended_at", + "extra", + "id", + "meeting_url", + "mp4_s3_path", + "reserved", + "transcription_custom_parameters", + "uuid", + "webhook_url" + ], + "properties": { + "account_id": { + "type": "integer", + "format": "int32" + }, + "bot_exited_at": { + "$ref": "#/components/schemas/OptionalDateTime" + }, + "bot_image": { + "type": [ + "string", + "null" + ] + }, + "bot_joined_at": { + "$ref": "#/components/schemas/OptionalDateTime" + }, + "bot_name": { + "type": "string" + }, + "bot_param_id": { + "type": "integer", + "format": "int32" + }, + "created_at": { + "$ref": "#/components/schemas/DateTime" + }, + "deduplication_key": { + "type": [ + "string", + "null" + ] + }, + "diarization_fails": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "diarization_v2": { + "type": "boolean" + }, + "ended_at": { + "$ref": "#/components/schemas/OptionalDateTime" + }, + "enter_message": { + "type": [ + "string", + "null" + ] + }, + "errors": { + "type": [ + "string", + "null" + ] + }, + "event_id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "extra": { + "$ref": "#/components/schemas/Extra" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "meeting_url": { + "type": "string" + }, + "mp4_s3_path": { + "type": "string" + }, + "noone_joined_timeout": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "recording_mode": { + "anyOf": [ + { + "$ref": "#/components/schemas/RecordingMode" + }, + { + "type": "null" + } + ] + }, + "reserved": { + "type": "boolean" + }, + "scheduled_bot_id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "session_id": { + "type": [ + "string", + "null" + ] + }, + "silence_timeout": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "speech_to_text_api_key": { + "type": [ + "string", + "null" + ] + }, + "speech_to_text_provider": { + "anyOf": [ + { + "$ref": "#/components/schemas/SpeechToTextProvider" + }, + { + "type": "null" + } + ] + }, + "streaming_audio_frequency": { + "anyOf": [ + { + "$ref": "#/components/schemas/AudioFrequency" + }, + { + "type": "null" + } + ] + }, + "streaming_input": { + "type": [ + "string", + "null" + ] + }, + "streaming_output": { + "type": [ + "string", + "null" + ] + }, + "transcription_custom_parameters": { + "$ref": "#/components/schemas/Extra" + }, + "transcription_fails": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "transcription_payloads": true, + "user_reported_error": true, + "uuid": { + "type": "string", + "format": "uuid" + }, + "waiting_room_timeout": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "webhook_url": { + "type": "string" + }, + "zoom_access_token_url": { + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token": { + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token_url": { + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token_user_id": { + "type": [ + "string", + "null" + ] + }, + "zoom_sdk_id": { + "type": [ + "string", + "null" + ] + }, + "zoom_sdk_pwd": { + "type": [ + "string", + "null" + ] + } + } + }, + "BotWithStatus": { + "description": "Bot information with status metadata\n\nThis struct combines the bot data with status information optimized for UI display.", + "type": "object", + "required": [ + "account_id", + "bot_exited_at", + "bot_joined_at", + "bot_param_id", + "created_at", + "diarization_v2", + "duration", + "ended_at", + "id", + "meeting_url", + "mp4_s3_path", + "params", + "reserved", + "status", + "uuid" + ], + "properties": { + "account_email": { + "description": "The email of the account owner (only included for special domain users)", + "type": [ + "string", + "null" + ] + }, + "account_id": { + "description": "The account that owns this bot", + "type": "integer", + "format": "int32" + }, + "bot_exited_at": { + "description": "When the bot actually exited the meeting (precise timing)", + "$ref": "#/components/schemas/OptionalDateTime" + }, + "bot_joined_at": { + "description": "When the bot actually joined the meeting (precise timing)", + "$ref": "#/components/schemas/OptionalDateTime" + }, + "bot_param_id": { + "description": "ID of the bot parameters used", + "type": "integer", + "format": "int32" + }, + "created_at": { + "description": "When the bot was created", + "$ref": "#/components/schemas/DateTime" + }, + "diarization_fails": { + "description": "Number of diarization failures", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "diarization_v2": { + "description": "Whether diarization v2 is enabled", + "type": "boolean" + }, + "duration": { + "description": "Duration of the recording in seconds", + "type": "integer", + "format": "int64" + }, + "ended_at": { + "description": "When the bot ended recording", + "$ref": "#/components/schemas/OptionalDateTime" + }, + "errors": { + "description": "Any error messages", + "type": [ + "string", + "null" + ] + }, + "event_id": { + "description": "ID of the calendar event if scheduled", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "id": { + "description": "The bot's unique identifier", + "type": "integer", + "format": "int32" + }, + "meeting_url": { + "description": "The meeting URL this bot is recording", + "type": "string" + }, + "mp4_s3_path": { + "description": "Path to the MP4 file in S3", + "type": "string" + }, + "params": { + "description": "Bot parameters", + "$ref": "#/components/schemas/BotParam" + }, + "reserved": { + "description": "Whether this bot is reserved", + "type": "boolean" + }, + "scheduled_bot_id": { + "description": "ID of the scheduled bot if scheduled", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "session_id": { + "description": "The session ID for this bot instance", + "type": [ + "string", + "null" + ] + }, + "status": { + "description": "Frontend-friendly status information for display and sorting", + "$ref": "#/components/schemas/BotStatusResponse" + }, + "transcription_fails": { + "description": "Number of transcription failures", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "user_reported_error": { + "description": "User reported error information" + }, + "uuid": { + "description": "Unique identifier for this bot", + "type": "string", + "format": "uuid" + } + } + }, + "Calendar": { + "type": "object", + "required": [ + "email", + "google_id", + "name", + "uuid" + ], + "properties": { + "email": { + "type": "string" + }, + "google_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "resource_id": { + "type": [ + "string", + "null" + ] + }, + "uuid": { + "type": "string", + "format": "uuid" + } + } + }, + "CalendarListEntry": { + "type": "object", + "required": [ + "email", + "id", + "is_primary" + ], + "properties": { + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_primary": { + "type": "boolean" + } + } + }, + "CalendarUuidParam": { + "description": "Calendar UUID path parameter for API endpoints", + "type": "object", + "required": [ + "calendar_uuid" + ], + "properties": { + "calendar_uuid": { + "description": "The calendar UUID", + "type": "string" + } + } + }, + "CreateCalendarParams": { + "type": "object", + "required": [ + "oauth_client_id", + "oauth_client_secret", + "oauth_refresh_token", + "platform" + ], + "properties": { + "oauth_client_id": { + "type": "string" + }, + "oauth_client_secret": { + "type": "string" + }, + "oauth_refresh_token": { + "type": "string" + }, + "platform": { + "$ref": "#/components/schemas/Provider" + }, + "raw_calendar_id": { + "type": [ + "string", + "null" + ] + } + } + }, + "CreateCalendarResponse": { + "type": "object", + "required": [ + "calendar" + ], + "properties": { + "calendar": { + "$ref": "#/components/schemas/Calendar" + } + } + }, + "CreateConnectionRequest": { + "type": "object", + "required": [ + "authorization_code", + "redirect_uri", + "zoom_client_id", + "zoom_client_secret" + ], + "properties": { + "authorization_code": { + "description": "The OAuth authorization code received from Zoom after user consent. This is a single-use code that expires in approximately 10 minutes.", + "type": "string" + }, + "redirect_uri": { + "description": "The redirect URI that was used in the OAuth authorization request. Must match exactly what was configured in your Zoom OAuth app.", + "type": "string" + }, + "zoom_client_id": { + "description": "Your Zoom OAuth app's Client ID, found in the Zoom App Marketplace under your app's credentials.", + "type": "string" + }, + "zoom_client_secret": { + "description": "Your Zoom OAuth app's Client Secret, found in the Zoom App Marketplace under your app's credentials.", + "type": "string" + } + } + }, + "DailyTokenConsumption": { + "type": "object", + "required": [ + "consumption_by_service", + "date" + ], + "properties": { + "consumption_by_service": { + "$ref": "#/components/schemas/TokenConsumptionByService" + }, + "date": { + "type": "string" + } + } + }, + "DateTime": { + "type": "string", + "format": "date-time" + }, + "DeleteResponse": { + "type": "object", + "required": [ + "ok", + "status" + ], + "properties": { + "ok": { + "description": "Whether the request was processed successfully", + "type": "boolean" + }, + "status": { + "description": "The detailed status of the deletion operation", + "$ref": "#/components/schemas/DeleteStatus" + } + } + }, + "DeleteStatus": { + "oneOf": [ + { + "description": "All data was successfully deleted", + "type": "string", + "enum": [ + "deleted" + ] + }, + { + "description": "Some data was deleted, but other parts couldn't be removed", + "type": "string", + "enum": [ + "partiallyDeleted" + ] + }, + { + "description": "No data needed to be deleted as it was already removed", + "type": "string", + "enum": [ + "alreadyDeleted" + ] + }, + { + "description": "No data was found for the specified bot", + "type": "string", + "enum": [ + "noDataFound" + ] + } + ] + }, + "EfsManifestEntry": { + "type": "object", + "required": [ + "path", + "size" + ], + "properties": { + "path": { + "type": "string" + }, + "size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + } + }, + "EndMeetingQuery": { + "type": "object", + "required": [ + "bot_uuid" + ], + "properties": { + "bot_uuid": { + "type": "string" + } + } + }, + "EndMeetingTrampolineQuery": { + "type": "object", + "required": [ + "bot_uuid" + ], + "properties": { + "bot_uuid": { + "type": "string" + } + } + }, + "EndMeetingTrampolineRequest": { + "type": "object", + "required": [ + "bot_exited_at", + "bot_joined_at", + "diarization_v2" + ], + "properties": { + "bot_exited_at": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "bot_joined_at": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "diarization_fail_count": { + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0 + }, + "diarization_v2": { + "type": "boolean" + }, + "ended_at": { + "description": "Optional Unix timestamp (seconds, no ms) for when the meeting actually ended. When provided (e.g. for manual replay), used instead of server time for bot.ended_at.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "files_generated": { + "anyOf": [ + { + "$ref": "#/components/schemas/FilesGenerated" + }, + { + "type": "null" + } + ] + }, + "transcription_fail_count": { + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0 + } + } + }, + "Event": { + "type": "object", + "required": [ + "attendees", + "calendar_uuid", + "deleted", + "end_time", + "google_id", + "is_organizer", + "is_recurring", + "last_updated_at", + "meeting_url", + "name", + "raw", + "start_time", + "uuid" + ], + "properties": { + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attendee" + } + }, + "bot_param": { + "description": "Associated bot parameters if a bot is scheduled for this event", + "anyOf": [ + { + "$ref": "#/components/schemas/BotParam" + }, + { + "type": "null" + } + ] + }, + "calendar_uuid": { + "type": "string", + "format": "uuid" + }, + "deleted": { + "description": "Indicates whether this event has been deleted", + "type": "boolean" + }, + "end_time": { + "description": "The end time of the event in UTC timezone", + "type": "string", + "format": "date-time" + }, + "google_id": { + "description": "The unique identifier of the event from the calendar provider (Google, Microsoft)", + "type": "string" + }, + "is_organizer": { + "description": "Indicates whether the current user is the organizer of this event", + "type": "boolean" + }, + "is_recurring": { + "description": "Indicates whether this event is part of a recurring series", + "type": "boolean" + }, + "last_updated_at": { + "description": "The timestamp when this event was last updated", + "type": "string", + "format": "date-time" + }, + "meeting_url": { + "description": "The URL that can be used to join the meeting (if available)", + "type": "string" + }, + "name": { + "description": "The title/name of the calendar event", + "type": "string" + }, + "raw": { + "description": "The raw calendar data from the provider in JSON format", + "$ref": "#/components/schemas/Extra" + }, + "recurring_event_id": { + "description": "For recurring events, the ID of the parent recurring event series (if applicable)", + "type": [ + "string", + "null" + ] + }, + "start_time": { + "description": "The start time of the event in UTC timezone", + "type": "string", + "format": "date-time" + }, + "uuid": { + "type": "string", + "format": "uuid" + } + } + }, + "Extra": { + "description": "Custom data object", + "additionalProperties": true + }, + "FailedRecordRequest": { + "type": "object", + "required": [ + "meeting_url", + "message" + ], + "properties": { + "ended_at": { + "description": "Optional Unix timestamp (seconds, no ms) for when the bot actually ended. When provided (e.g. for manual replay), used instead of server time to avoid incorrect billing.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "error_code": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "meeting_url": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "FilesGenerated": { + "type": "object", + "required": [ + "file_extension", + "file_type", + "filename" + ], + "properties": { + "audio_file": { + "anyOf": [ + { + "$ref": "#/components/schemas/AudioFile" + }, + { + "type": "null" + } + ] + }, + "file_extension": { + "type": "string" + }, + "file_type": { + "type": "string" + }, + "filename": { + "type": "string" + } + } + }, + "GetAllBotsQuery": { + "description": "Query parameters for getting all bots", + "type": "object", + "required": [ + "limit", + "offset" + ], + "properties": { + "account_id": { + "description": "Filter by account ID (comma-separated for multiple values)\n\nExample: \"1,2,3\" will match bots from accounts 1, 2, or 3", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "bot_uuid": { + "description": "Filter by bot UUID (comma-separated for multiple values)\n\nExample: \"123e4567-e89b-12d3-a456-426614174000,987fcdeb-51d3-a456-426614174000\"", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "creator_email_contains": { + "description": "Filter by creator email containing any of these texts (comma-separated)\n\nExample: \"john,doe\" will match emails containing either \"john\" or \"doe\"", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "diarization_v2": { + "description": "Filter by diarization v2 status (comma-separated for multiple values)\n\nExample: \"true,false\" will match both diarization v2 and non-diarization v2 bots", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "end_date": { + "description": "Filter by end date", + "default": null, + "type": [ + "string", + "null" + ], + "format": "partial-date-time" + }, + "extra_contains": { + "description": "Filter by extra JSON containing any of these texts (comma-separated)\n\nExample: \"customer_id,project\" will match extra JSON containing either \"customer_id\" or \"project\"", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "limit": { + "description": "Limit for pagination", + "type": "integer", + "format": "int64" + }, + "meeting_url": { + "description": "Filter by exact meeting URL (comma-separated for multiple values)\n\nExample: \"https://meet.google.com/abc-def,https://zoom.us/j/123456\"", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "meeting_url_contains": { + "description": "Filter by meeting URL containing any of these texts (comma-separated)\n\nExample: \"meet.google.com,zoom.us\" will match URLs containing either \"meet.google.com\" or \"zoom.us\"", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "offset": { + "description": "Offset for pagination", + "type": "integer", + "format": "int64" + }, + "reserved": { + "description": "Filter by reserved status (comma-separated for multiple values)\n\nExample: \"true,false\" will match both reserved and non-reserved bots", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "start_date": { + "description": "Filter by start date", + "default": null, + "type": [ + "string", + "null" + ], + "format": "partial-date-time" + }, + "status": { + "description": "Filter by user-reported status (comma-separated for multiple values)\n\nExample: \"open,in_progress,closed\" will match bots with any of these user-reported statuses", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "status_category": { + "description": "Filter by status category (comma-separated for multiple values)\n\nExample: \"system_error,auth_error,connection_error\" will match bots with any of these categories\n\nCommon categories are: - system_error: Internal system issues - auth_error: Authentication and authorization issues - connection_error: Network and meeting connection issues - permission_error: Access and permission issues - input_error: Invalid input parameters - webhook_error: Webhook delivery issues - duplicate_error: Duplicate meeting or bot issues - unknown_error: Unclassified errors", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "status_priority": { + "description": "Filter by status priority (comma-separated for multiple values)\n\nExample: \"critical,high,medium,low\" will match bots with any of these priorities\n\nPriorities are: - critical: System errors requiring immediate attention - high: Serious issues that prevent meeting functionality - medium: Issues that affect functionality but not critically - low: Minor issues that don't greatly impact functionality - none: For success states", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "status_type": { + "description": "Filter by status type (comma-separated for multiple values)\n\nExample: \"success,error,warning,pending\" will match bots with any of these status types\n\nStatus types are: - success: Bot completed successfully or is in progress - error: Bot encountered a system error - warning: Bot has a non-critical issue - pending: Bot is waiting to start", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "user_email": { + "description": "Filter by user email (comma-separated for multiple values)\n\nExample: \"user1@example.com,user2@example.com\" will match bots from accounts with these emails", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "user_reported_error_contains": { + "description": "Filter by user reported error containing any of these texts (comma-separated)\n\nExample: \"failed,error\" will match errors containing either \"failed\" or \"error\"", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "user_reported_error_json": { + "description": "Filter by user reported error JSON (comma-separated for multiple conditions)\n\nExample: '{\"status\":\"open\"},{\"priority\":\"high\"}' will match bots with either status open or priority high", + "default": null, + "type": [ + "array", + "null" + ], + "items": true + } + } + }, + "GetMeetingDataQuery": { + "type": "object", + "required": [ + "bot_id" + ], + "properties": { + "bot_id": { + "type": "string" + }, + "include_transcripts": { + "description": "Whether to include transcription data in the response. Defaults to true if not specified.", + "default": true, + "type": "boolean" + } + } + }, + "GetStartedAccount": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string" + }, + "firstname": { + "type": [ + "string", + "null" + ] + }, + "google_token_id": { + "type": [ + "string", + "null" + ] + }, + "lastname": { + "type": [ + "string", + "null" + ] + }, + "microsoft_token_id": { + "type": [ + "string", + "null" + ] + } + } + }, + "GetWebhookUrlResponse": { + "type": "object", + "properties": { + "webhook_url": { + "type": [ + "string", + "null" + ] + } + } + }, + "GetstartedQuery": { + "type": "object", + "properties": { + "redirect_url": { + "type": [ + "string", + "null" + ] + } + } + }, + "JoinRequest": { + "type": "object", + "required": [ + "bot_name", + "meeting_url" + ], + "properties": { + "automatic_leave": { + "description": "Configuration for automatic meeting exit behavior. The bot uses waiting_room_timeout to wait in the waiting room, then noone_joined_timeout to wait for participants when first joining the meeting, and finally switches to silence_timeout monitoring once participants are detected. Applies to Google Meet and Microsoft Teams only.", + "anyOf": [ + { + "$ref": "#/components/schemas/AutomaticLeaveRequest" + }, + { + "type": "null" + } + ] + }, + "bot_image": { + "description": "The image to use for the bot, must be a URL. Recommended ratio is 16:9.", + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "bot_name": { + "type": "string" + }, + "deduplication_key": { + "description": "We prevent multiple bots with same API key joining a meeting within 5 mins, unless overridden by deduplication_key.", + "type": [ + "string", + "null" + ] + }, + "entry_message": { + "description": "There are no entry messages on Microsoft Teams as guests outside of an organization do not have access to the chat.", + "type": [ + "string", + "null" + ] + }, + "extra": { + "description": "A JSON object that allows you to add custom data to a bot for your convenience, e.g. your end user's ID.", + "default": null, + "$ref": "#/components/schemas/Extra" + }, + "meeting_url": { + "type": "string" + }, + "recording_mode": { + "description": "The recording mode for the bot, defaults to 'speaker_view'. Supported values are 'speaker_view' and 'audio_only'. 'gallery_view' is currently under development.", + "anyOf": [ + { + "$ref": "#/components/schemas/RecordingMode" + }, + { + "type": "null" + } + ] + }, + "reserved": { + "description": "Deprecated, do not use.", + "default": false, + "type": "boolean" + }, + "speech_to_text": { + "description": "The default speech to text provider is Gladia.", + "anyOf": [ + { + "$ref": "#/components/schemas/SpeechToText" + }, + { + "type": "null" + } + ] + }, + "start_time": { + "description": "Reserved has been deprecated in favour of start_time. Unix timestamp (in seconds) for when the bot should join the meeting. The bot joins eaxctly at the start time.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "streaming": { + "description": "WebSocket streams for 16 kHz audio. Input stream receives audio sent to the bot. Output stream receives audio from the bot.", + "anyOf": [ + { + "$ref": "#/components/schemas/StreamingApiParameter" + }, + { + "type": "null" + } + ] + }, + "transcription_custom_parameters": { + "description": "For your own transcription parameters", + "default": null + }, + "webhook_url": { + "description": "A webhook URL to send events to, overrides the webhook URL set in your account settings.", + "type": [ + "string", + "null" + ] + }, + "zoom_access_token_url": { + "description": "URL that returns a Zoom ZAK token (short-lived access token) for joining authenticated meetings.", + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token": { + "description": "A raw Zoom On Behalf Of (OBF) token for joining external Zoom meetings. Required for meetings that enforce authenticated join after March 2, 2026.", + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token_url": { + "description": "URL that returns a Zoom OBF token. The bot will fetch the token from this URL at join time.", + "type": [ + "string", + "null" + ] + }, + "zoom_obf_token_user_id": { + "description": "The Zoom user ID associated with a stored OAuth connection. When set, the system will automatically fetch an OBF token using the managed OAuth credentials.", + "type": [ + "string", + "null" + ] + }, + "zoom_sdk_id": { + "description": "For the Own Zoom Credentials feature, we need your zoom sdk id.", + "type": [ + "string", + "null" + ] + }, + "zoom_sdk_pwd": { + "description": "For the Own Zoom Credentials feature, we need your zoom sdk pwd.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "JoinRequestScheduled": { + "type": "object", + "required": [ + "bot_param_id", + "meeting_url", + "schedule_origin" + ], + "properties": { + "bot_param_id": { + "type": "integer", + "format": "int32" + }, + "meeting_url": { + "type": "string" + }, + "schedule_origin": { + "$ref": "#/components/schemas/ScheduleOrigin" + } + }, + "additionalProperties": false + }, + "JoinResponse": { + "type": "object", + "required": [ + "bot_id" + ], + "properties": { + "bot_id": { + "type": "string", + "format": "uuid" + } + } + }, + "JoinResponse2": { + "type": "object", + "required": [ + "bot_id" + ], + "properties": { + "bot_id": { + "type": "string", + "format": "uuid" + } + } + }, + "LeaveResponse": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + }, + "ListEventResponse": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "description": "Vector of calendar events matching the list criteria", + "type": "array", + "items": { + "$ref": "#/components/schemas/Event" + } + }, + "next": { + "description": "Optional url for fetching the next page of results if there are more results to fetch. The limit of events returned is 100. When None, there are no more results to fetch.", + "type": [ + "string", + "null" + ] + } + } + }, + "ListRawCalendarsParams": { + "type": "object", + "required": [ + "oauth_client_id", + "oauth_client_secret", + "oauth_refresh_token", + "platform" + ], + "properties": { + "oauth_client_id": { + "type": "string" + }, + "oauth_client_secret": { + "type": "string" + }, + "oauth_refresh_token": { + "type": "string" + }, + "platform": { + "$ref": "#/components/schemas/Provider" + } + } + }, + "ListRawCalendarsResponse": { + "type": "object", + "required": [ + "calendars" + ], + "properties": { + "calendars": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CalendarListEntry" + } + } + } + }, + "ListRecentBotsQuery": { + "description": "Query parameters for listing recent bots", + "type": "object", + "properties": { + "bot_name": { + "description": "Filter bots by name containing this string.\n\nPerforms a case-insensitive partial match on the bot's name. Useful for finding bots with specific naming conventions or to locate a particular bot when you don't have its ID.\n\nExample: \"Sales\" would match \"Sales Meeting\", \"Quarterly Sales\", etc.", + "type": [ + "string", + "null" + ] + }, + "created_after": { + "description": "Filter bots created after this date (ISO format).\n\nLimits results to bots created at or after the specified timestamp. Used for time-based filtering to find recent additions.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-01T00:00:00\"", + "type": [ + "string", + "null" + ] + }, + "created_before": { + "description": "Filter bots created before this date (ISO format).\n\nLimits results to bots created at or before the specified timestamp. Used for time-based filtering to exclude recent additions.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-31T23:59:59\"", + "type": [ + "string", + "null" + ] + }, + "cursor": { + "description": "Cursor for pagination, obtained from previous response.\n\nUsed for retrieving the next set of results after a previous call. The cursor value is returned in the `nextCursor` field of responses that have additional results available.\n\nFormat: Base64-encoded string containing pagination metadata", + "type": [ + "string", + "null" + ] + }, + "ended_after": { + "description": "Filter bots ended after this date (ISO format).\n\nLimits results to bots that ended at or after the specified timestamp. Useful for finding completed meetings within a specific time period.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-01T00:00:00\"", + "type": [ + "string", + "null" + ] + }, + "filter_by_extra": { + "description": "Filter bots by matching values in the extra JSON payload.\n\nThis parameter performs in-memory filtering on the `extra` JSON field, similar to a SQL WHERE clause. It reduces the result set to only include bots that match all specified conditions.\n\nFormat specifications: - Single condition: \"field:value\" - Multiple conditions: \"field1:value1,field2:value2\"\n\nExamples: - \"customer_id:12345\" - Only bots with this customer ID - \"status:active,project:sales\" - Only active bots from sales projects\n\nNotes: - All conditions must match for a bot to be included - Values are matched exactly (case-sensitive) - Bots without the specified field are excluded", + "type": [ + "string", + "null" + ] + }, + "limit": { + "description": "Maximum number of bots to return in a single request.\n\nLimits the number of results returned in a single API call. This parameter helps control response size and page length.\n\nDefault: 10 Minimum: 1 Maximum: 50", + "default": 10, + "type": "integer", + "format": "int32" + }, + "meeting_url": { + "description": "Filter bots by meeting URL containing this string.\n\nPerforms a case-insensitive partial match on the bot's meeting URL. Use this to find bots associated with specific meeting platforms or particular meeting IDs.\n\nExample: \"zoom.us\" would match all Zoom meetings", + "type": [ + "string", + "null" + ] + }, + "sort_by_extra": { + "description": "Sort the results by a field in the extra JSON payload.\n\nThis parameter performs in-memory sorting on the `extra` JSON field, similar to a SQL ORDER BY clause. It changes the order of results but not which results are included.\n\nFormat specifications: - Default (ascending): \"field\" - Explicit direction: \"field:asc\" or \"field:desc\"\n\nExamples: - \"customer_id\" - Sort by customer_id (ascending) - \"priority:desc\" - Sort by priority (descending)\n\nNotes: - Applied after all filtering - String comparison is used for sorting - Bots with the field come before bots without it - Can be combined with filter_by_extra", + "type": [ + "string", + "null" + ] + }, + "speaker_name": { + "description": "NOTE: this is a preview feature and not yet available\n\nFilter bots by speaker name containing this string.\n\nPerforms a case-insensitive partial match on the speakers in the meeting. Useful for finding meetings that included a specific person.\n\nExample: \"John\" would match meetings with speakers like \"John Smith\" or \"John Doe\"", + "type": [ + "string", + "null" + ] + } + } + }, + "ListRecentBotsResponse": { + "description": "Response for listing recent bots", + "type": "object", + "required": [ + "bots" + ], + "properties": { + "bots": { + "description": "List of recent bots with their metadata\n\nThis field is serialized as both \"bots\" and \"recent_bots\" for backwards compatibility. New clients should use the \"bots\" field name.", + "type": "array", + "items": { + "$ref": "#/components/schemas/RecentBotEntry" + } + }, + "last_updated": { + "description": "Timestamp of when this data was generated (in ISO-8601 format)\n\nThis field is maintained for backwards compatibility. It is automatically set to the current time when the response is created.", + "default": "2026-02-17T23:00:13.081843563+00:00", + "type": "string", + "format": "date-time" + }, + "next_cursor": { + "description": "Optional cursor for pagination", + "type": [ + "string", + "null" + ] + } + } + }, + "LoginAccount": { + "type": "object", + "required": [ + "password", + "pseudo" + ], + "properties": { + "app_signin_token": { + "type": [ + "string", + "null" + ] + }, + "google_chrome_token_id": { + "type": [ + "string", + "null" + ] + }, + "google_token_id": { + "type": [ + "string", + "null" + ] + }, + "microsoft_token_id": { + "type": [ + "string", + "null" + ] + }, + "password": { + "type": "string" + }, + "pseudo": { + "type": "string" + } + } + }, + "LoginQuery": { + "type": "object", + "properties": { + "redirect_url": { + "type": [ + "string", + "null" + ] + } + } + }, + "Metadata": { + "type": "object", + "required": [ + "audio", + "bot_data", + "duration", + "meeting_participants_file", + "mp4", + "speaker_diarization_file", + "speaker_diarization_file_network" + ], + "properties": { + "audio": { + "description": "URL to access the recording WAV audio file. Will be an empty string if the file doesn't exist in S3.", + "type": "string" + }, + "bot_data": { + "$ref": "#/components/schemas/BotData" + }, + "duration": { + "description": "Duration of the recording in seconds", + "type": "integer", + "format": "int64" + }, + "meeting_participants_file": { + "description": "URL to access the meeting participants log file. Contains information about meeting participants. Will be an empty string if the file doesn't exist in S3.", + "type": "string" + }, + "mp4": { + "description": "URL to access the recording MP4 file. Will be an empty string if the file doesn't exist in S3.", + "type": "string" + }, + "speaker_diarization_file": { + "description": "URL to access the speaker diarization metadata file. Contains real-time speaker activity data with timestamps indicating when each speaker is talking. The file contains JSON arrays with speaker information including name, ID, timestamp, and speaking status. Will be an empty string if the file doesn't exist in S3.", + "type": "string" + }, + "speaker_diarization_file_network": { + "description": "URL to access the network speaker detection log file. Contains speaker observation observed through network (not UI changes). Will be an empty string if the file doesn't exist in S3.", + "type": "string" + } + } + }, + "ObfTokenQuery": { + "type": "object", + "required": [ + "bot_uuid" + ], + "properties": { + "bot_uuid": { + "type": "string" + }, + "zoom_user_id": { + "type": [ + "string", + "null" + ] + } + } + }, + "OptionalDateTime": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "PostWebhookUrlRequest": { + "type": "object", + "required": [ + "webhook_url" + ], + "properties": { + "webhook_url": { + "type": "string" + } + } + }, + "Provider": { + "description": "Fields with value `\"simple\"` parse as `Kind::Simple`. Fields with value `\"fancy\"` parse as `Kind::SoFancy`.", + "type": "string", + "enum": [ + "Google", + "Microsoft" + ] + }, + "PublicBotAnalytics": { + "description": "Watered-down bot info for public analytics", + "type": "object", + "required": [ + "created_at", + "duration", + "meeting_platform", + "status" + ], + "properties": { + "created_at": { + "$ref": "#/components/schemas/DateTime" + }, + "duration": { + "type": "integer", + "format": "int64" + }, + "meeting_platform": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/BotStatusResponse" + }, + "user_reported_error_message": { + "type": [ + "string", + "null" + ] + }, + "user_reported_error_status": { + "type": [ + "string", + "null" + ] + } + } + }, + "QueryListEvent": { + "type": "object", + "required": [ + "calendar_id" + ], + "properties": { + "attendee_email": { + "description": "If provided, filters events to include only those with this attendee's email address Example: \"jane.smith@example.com\"", + "type": [ + "string", + "null" + ] + }, + "calendar_id": { + "description": "Calendar ID to filter events by This is required to specify which calendar's events to retrieve", + "type": "string" + }, + "cursor": { + "description": "Optional cursor for pagination This value is included in the `next` field of the previous response", + "type": [ + "string", + "null" + ] + }, + "organizer_email": { + "description": "If provided, filters events to include only those with this organizer's email address Example: \"john.doe@example.com\"", + "type": [ + "string", + "null" + ] + }, + "start_date_gte": { + "description": "If provided, filters events to include only those with a start date greater than or equal to this timestamp Format: ISO-8601 string, e.g., \"2023-01-01T00:00:00Z\"", + "type": [ + "string", + "null" + ] + }, + "start_date_lte": { + "description": "If provided, filters events to include only those with a start date less than or equal to this timestamp Format: ISO-8601 string, e.g., \"2023-12-31T23:59:59Z\"", + "type": [ + "string", + "null" + ] + }, + "status": { + "description": "Filter events by meeting status Valid values: \"upcoming\" (default) returns events after current time, \"past\" returns previous events, \"all\" returns both", + "type": [ + "string", + "null" + ] + }, + "updated_at_gte": { + "description": "If provided, fetches only events updated at or after this timestamp Format: ISO-8601 string, e.g., \"2023-01-01T00:00:00Z\"", + "type": [ + "string", + "null" + ] + } + } + }, + "QueryPatchRecordEvent": { + "type": "object", + "properties": { + "all_occurrences": { + "description": "schedule a bot to all occurences of a recurring event", + "type": [ + "boolean", + "null" + ] + } + } + }, + "QueryScheduleRecordEvent": { + "type": "object", + "properties": { + "all_occurrences": { + "description": "schedule a bot to all occurences of a recurring event", + "type": [ + "boolean", + "null" + ] + } + } + }, + "QueryUnScheduleRecordEvent": { + "type": "object", + "properties": { + "all_occurrences": { + "description": "unschedule a bot from all occurences of a recurring event", + "type": [ + "boolean", + "null" + ] + } + } + }, + "ReceivedMessageQuery": { + "type": "object", + "required": [ + "session_id" + ], + "properties": { + "session_id": { + "type": "string" + } + } + }, + "RecentBotEntry": { + "description": "Entry for a recent bot in the list response", + "type": "object", + "required": [ + "bot_name", + "created_at", + "extra", + "id", + "meeting_url", + "speakers", + "uuid" + ], + "properties": { + "access_count": { + "description": "Number of times this bot data has been accessed (if tracked)", + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "bot_name": { + "description": "Name of the bot", + "type": "string" + }, + "created_at": { + "description": "Creation timestamp of the bot in ISO-8601 format", + "type": "string" + }, + "duration": { + "description": "Duration of the bot session in seconds (if completed)", + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "ended_at": { + "description": "End time of the bot session (if completed) in ISO-8601 format", + "type": [ + "string", + "null" + ] + }, + "extra": { + "description": "Extra custom data provided during bot creation", + "$ref": "#/components/schemas/Extra" + }, + "id": { + "description": "Unique identifier of the bot (legacy field)\n\nThis field is maintained for backwards compatibility. It is serialized as a UUID string to match the old API format. New clients should use the uuid field instead.", + "type": "integer", + "format": "int32" + }, + "last_accessed_at": { + "description": "Last time this bot data was accessed (if available)", + "type": [ + "string", + "null" + ] + }, + "meeting_url": { + "description": "URL of the meeting the bot joined", + "type": "string" + }, + "session_id": { + "description": "Session ID if the bot is active", + "type": [ + "string", + "null" + ] + }, + "speakers": { + "description": "List of unique speaker names from the bot's transcripts", + "type": "array", + "items": { + "type": "string" + } + }, + "uuid": { + "description": "Unique identifier of the bot (new field)\n\nThis is the preferred field to use for bot identification. The id field is maintained for backwards compatibility.", + "type": "string", + "format": "uuid" + } + } + }, + "RecognizerTranscript": { + "type": "object", + "required": [ + "speaker", + "start_time" + ], + "properties": { + "end_time": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "lang": { + "type": [ + "string", + "null" + ] + }, + "speaker": { + "type": "string" + }, + "start_time": { + "type": "number", + "format": "double" + }, + "user_id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "RecognizerWord": { + "type": "object", + "required": [ + "end_time", + "start_time", + "text" + ], + "properties": { + "end_time": { + "type": "number", + "format": "double" + }, + "start_time": { + "type": "number", + "format": "double" + }, + "text": { + "type": "string" + }, + "user_id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "RecordingMode": { + "description": "Recording mode for the bot", + "oneOf": [ + { + "description": "Records the active speaker view", + "type": "string", + "enum": [ + "speaker_view" + ] + }, + { + "description": "Records the gallery view showing multiple participants", + "type": "string", + "enum": [ + "gallery_view" + ] + }, + { + "description": "Records only the audio from the meeting", + "type": "string", + "enum": [ + "audio_only" + ] + } + ] + }, + "ResyncAllCalendarsQuery": { + "description": "Resync all calendars for the user\n\nThis will fetch all events from the calendar again, regardless of whether they have been synced before. It is useful when the calendar data becomes out of sync.", + "type": "object", + "properties": { + "days": { + "description": "Number of days to sync forward (default: 30 for rolling window)", + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "ResyncAllCalendarsResponse": { + "type": "object", + "required": [ + "errors", + "synced_calendars" + ], + "properties": { + "errors": { + "description": "List of calendar UUIDs that failed to resync, with detailed error messages explaining the failure reason", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "synced_calendars": { + "description": "List of calendar UUIDs that were successfully resynced with their calendar provider (Google, Microsoft)", + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + }, + "ResyncAllQuery": { + "description": "Resync all calendars for the user\n\nThis will fetch all events from the calendar again, regardless of whether they have been fetched before. It is useful when the calendar data becomes out of sync.", + "type": "object", + "properties": { + "days": { + "description": "Number of days to sync forward (default: 30 for rolling window)", + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "ResyncAllResponse": { + "type": "object", + "required": [ + "errors", + "synced_calendars" + ], + "properties": { + "errors": { + "description": "List of calendar UUIDs that failed to resync, with error messages", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "synced_calendars": { + "description": "List of calendar UUIDs that were successfully resynced", + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + }, + "RetranscribeBody": { + "type": "object", + "required": [ + "bot_uuid" + ], + "properties": { + "bot_uuid": { + "type": "string" + }, + "speech_to_text": { + "anyOf": [ + { + "$ref": "#/components/schemas/SpeechToText" + }, + { + "type": "null" + } + ] + }, + "webhook_url": { + "type": [ + "string", + "null" + ] + } + } + }, + "RetryWebhookQuery": { + "type": "object", + "required": [ + "bot_uuid" + ], + "properties": { + "bot_uuid": { + "type": "string" + }, + "webhook_url": { + "type": [ + "string", + "null" + ] + } + } + }, + "ScheduleOrigin": { + "oneOf": [ + { + "type": "object", + "required": [ + "Event" + ], + "properties": { + "Event": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "int32" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ScheduledBot" + ], + "properties": { + "ScheduledBot": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "int32" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "ScreenshotWrapper": { + "description": "Schema-compatible wrapper for the Screenshot struct", + "type": "object", + "required": [ + "date", + "url" + ], + "properties": { + "date": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "ScreenshotsList": { + "description": "Wrapper struct for Screenshots list that implements JsonSchema", + "type": "array", + "items": { + "$ref": "#/components/schemas/ScreenshotWrapper" + } + }, + "SpeechToText": { + "anyOf": [ + { + "$ref": "#/components/schemas/SpeechToTextApiParameter" + }, + { + "$ref": "#/components/schemas/SpeechToTextProvider" + } + ] + }, + "SpeechToTextApiParameter": { + "type": "object", + "required": [ + "provider" + ], + "properties": { + "api_key": { + "type": [ + "string", + "null" + ] + }, + "provider": { + "$ref": "#/components/schemas/SpeechToTextProvider" + } + } + }, + "SpeechToTextProvider": { + "type": "string", + "enum": [ + "Gladia", + "Runpod", + "Default" + ] + }, + "StartRecordFailedQuery": { + "type": "object", + "properties": { + "bot_uuid": { + "type": [ + "string", + "null" + ] + } + } + }, + "StreamingApiParameter": { + "type": "object", + "properties": { + "audio_frequency": { + "anyOf": [ + { + "$ref": "#/components/schemas/AudioFrequency" + }, + { + "type": "null" + } + ] + }, + "input": { + "type": [ + "string", + "null" + ] + }, + "output": { + "type": [ + "string", + "null" + ] + } + } + }, + "SyncResponse": { + "type": "object", + "properties": { + "affected_event_uuids": { + "description": "UUIDs of affected events", + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "format": "uuid" + } + }, + "has_updates": { + "description": "timestamp of last updated event if some events has been updated.", + "type": [ + "string", + "null" + ], + "format": "date-time" + } + } + }, + "SystemTime": { + "type": "object", + "required": [ + "nanos_since_epoch", + "secs_since_epoch" + ], + "properties": { + "nanos_since_epoch": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "secs_since_epoch": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + } + }, + "TokenConsumptionByService": { + "type": "object", + "required": [ + "duration", + "recording_tokens", + "streaming_input_hour", + "streaming_input_tokens", + "streaming_output_hour", + "streaming_output_tokens", + "transcription_byok_hour", + "transcription_byok_tokens", + "transcription_hour", + "transcription_tokens" + ], + "properties": { + "duration": { + "type": "string" + }, + "recording_tokens": { + "type": "string" + }, + "streaming_input_hour": { + "type": "string" + }, + "streaming_input_tokens": { + "type": "string" + }, + "streaming_output_hour": { + "type": "string" + }, + "streaming_output_tokens": { + "type": "string" + }, + "transcription_byok_hour": { + "type": "string" + }, + "transcription_byok_tokens": { + "type": "string" + }, + "transcription_hour": { + "type": "string" + }, + "transcription_tokens": { + "type": "string" + } + } + }, + "TokenConsumptionQuery": { + "type": "object", + "required": [ + "end_date", + "start_date" + ], + "properties": { + "end_date": { + "type": "string", + "format": "partial-date-time" + }, + "start_date": { + "type": "string", + "format": "partial-date-time" + } + } + }, + "Transcript": { + "type": "object", + "required": [ + "bot_id", + "id", + "speaker", + "start_time", + "words" + ], + "properties": { + "bot_id": { + "type": "integer", + "format": "int32" + }, + "end_time": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "lang": { + "type": [ + "string", + "null" + ] + }, + "speaker": { + "type": "string" + }, + "start_time": { + "type": "number", + "format": "double" + }, + "user_id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "words": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Word" + } + } + } + }, + "Transcript2": { + "type": "object", + "required": [ + "bot_id", + "id", + "speaker", + "start_time" + ], + "properties": { + "bot_id": { + "type": "integer", + "format": "int32" + }, + "end_time": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "lang": { + "type": [ + "string", + "null" + ] + }, + "speaker": { + "type": "string" + }, + "start_time": { + "type": "number", + "format": "double" + }, + "user_id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "Transcript3": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "bot_id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "end_time": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "lang": { + "type": [ + "string", + "null" + ] + }, + "speaker": { + "type": [ + "string", + "null" + ] + }, + "start_time": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "user_id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "UpdateCalendarParams": { + "type": "object", + "required": [ + "oauth_client_id", + "oauth_client_secret", + "oauth_refresh_token", + "platform" + ], + "properties": { + "oauth_client_id": { + "type": "string" + }, + "oauth_client_secret": { + "type": "string" + }, + "oauth_refresh_token": { + "type": "string" + }, + "platform": { + "$ref": "#/components/schemas/Provider" + } + } + }, + "UserReportedErrorPayload": { + "type": "object", + "required": [ + "note" + ], + "properties": { + "chat_id": { + "type": [ + "string", + "null" + ] + }, + "note": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/UserReportedErrorStatus" + }, + { + "type": "null" + } + ] + } + } + }, + "UserReportedErrorStatus": { + "description": "Example usage for frontend developers\n\n```ignore // How to use in your API handler: let status = calculate_bot_status(bot.errors, bot.ended_at, bot.duration, bot.created_at); let status_response = status_to_response(status);\n\n// How to sort by priority: bots.sort_by_key(|bot| bot.status.sort_priority);\n\n// How to filter by type: let errors = bots.filter(|bot| bot.status.r#type == \"error\");\n\n// How to filter by category: let connection_errors = bots.filter(|bot| bot.status.category == \"connection_error\"); ```", + "type": "string", + "enum": [ + "open", + "in_progress", + "closed" + ] + }, + "UserTokensResponse": { + "type": "object", + "required": [ + "available_tokens", + "total_tokens_purchased" + ], + "properties": { + "available_tokens": { + "type": "string" + }, + "last_purchase_date": { + "type": [ + "string", + "null" + ], + "format": "partial-date-time" + }, + "total_tokens_purchased": { + "type": "string" + } + } + }, + "UuidParam": { + "description": "UUID path parameter for API endpoints", + "type": "object", + "required": [ + "uuid" + ], + "properties": { + "uuid": { + "description": "The UUID identifier", + "type": "string" + } + } + }, + "Version": { + "type": "object", + "required": [ + "build_date", + "build_timestamp", + "location" + ], + "properties": { + "build_date": { + "type": "string" + }, + "build_timestamp": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "Word": { + "type": "object", + "required": [ + "bot_id", + "end_time", + "id", + "start_time", + "text" + ], + "properties": { + "bot_id": { + "type": "integer", + "format": "int32" + }, + "end_time": { + "type": "number", + "format": "double" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "start_time": { + "type": "number", + "format": "double" + }, + "text": { + "type": "string" + }, + "user_id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "ZoomOAuthConnectionResponse": { + "description": "A stored Zoom OAuth connection. Tokens are managed server-side and never exposed.", + "type": "object", + "required": [ + "created_at", + "state", + "updated_at", + "uuid", + "zoom_user_id" + ], + "properties": { + "created_at": { + "description": "Timestamp when the connection was first created.", + "type": "string", + "format": "partial-date-time" + }, + "scopes": { + "description": "OAuth scopes granted by the user during authorization (e.g. `user:read:user`).", + "type": [ + "string", + "null" + ] + }, + "state": { + "description": "Connection state. `connected` means tokens are valid; `disconnected` means the user needs to re-authorize.", + "type": "string" + }, + "updated_at": { + "description": "Timestamp when the connection was last updated (e.g. token refresh).", + "type": "string", + "format": "partial-date-time" + }, + "uuid": { + "description": "Unique identifier for this connection.", + "type": "string", + "format": "uuid" + }, + "zoom_account_id": { + "description": "The Zoom account ID that the connected user belongs to.", + "type": [ + "string", + "null" + ] + }, + "zoom_user_id": { + "description": "The Zoom user ID of the connected user. Use this value as `zoom_obf_token_user_id` when creating a bot to have the system automatically fetch an OBF token for this user.", + "type": "string" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "tags": [ + { + "name": "Webhooks", + "description": "Webhooks allow you to receive real-time notifications when specific events occur in the Meeting BaaS system. To use webhooks, set a webhook URL in your account settings or provide it when creating a bot.", + "externalDocs": { + "description": "Detailed webhook documentation", + "url": "https://docs.meetingbaas.com/webhooks" + } + } + ] +} diff --git a/openapi.json b/openapi.json index 6af5fc1..cef530c 100644 --- a/openapi.json +++ b/openapi.json @@ -1,4703 +1,889 @@ { - "openapi": "3.1.0", - "info": { - "title": "Meeting BaaS API", - "summary": "API for recording and transcribing video meetings across Zoom, Google Meet, and Microsoft Teams. Features include bot management, calendar integration, and transcription services.", - "description": "Meeting BaaS API", - "termsOfService": "https://meetingbaas.com/terms-and-conditions", - "version": "1.1" - }, - "servers": [ - { - "url": "https://api.meetingbaas.com", - "description": "Production server" - } - ], - "paths": { - "/bots/": { - "post": { - "summary": "Join", - "description": "Have a bot join a meeting, now or in the future. You can provide a `webhook_url` parameter to receive webhook events specific to this bot, overriding your account's default webhook URL. Events include recording completion, failures, and transcription updates.", - "operationId": "join", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/JoinRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/JoinResponse" - } - } - } - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] + "openapi": "3.1.0", + "info": { + "title": "Speaking Meeting Bot API", + "description": "API for deploying AI-powered speaking agents in video meetings. Combines MeetingBaas for meeting connectivity with Pipecat for voice AI processing.", + "version": "0.1.0" + }, + "paths": { + "/bots": { + "post": { + "tags": [ + "bots" + ], + "summary": "Join Meeting", + "description": "Create and deploy a speaking bot in a meeting.\n\nLaunches an AI-powered bot that joins a video meeting through MeetingBaas\nand processes audio using Pipecat's voice AI framework.", + "operationId": "join_meeting_bots_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BotRequest" + } } + }, + "required": true }, - "/bots/{uuid}": { - "delete": { - "summary": "Leave", - "description": "Leave", - "operationId": "leave", - "parameters": [ - { - "in": "path", - "name": "uuid", - "description": "The UUID identifier", - "required": true, - "schema": { - "description": "The UUID identifier", - "type": "string" - }, - "style": "simple" - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LeaveResponse" - } - } - } - } - } + "responses": { + "201": { + "description": "Bot successfully created and joined the meeting", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JoinResponse" + } + } } - }, - "/bots/meeting_data": { - "get": { - "summary": "Get Meeting Data", - "description": "Get meeting recording and metadata", - "operationId": "get_meeting_data", - "parameters": [ - { - "in": "query", - "name": "bot_id", - "required": true, - "schema": { - "type": "string" - }, - "style": "form" - }, - { - "in": "query", - "name": "include_transcripts", - "description": "Whether to include transcription data in the response. Defaults to true if not specified.", - "schema": { - "description": "Whether to include transcription data in the response. Defaults to true if not specified.", - "default": true, - "type": "boolean" - }, - "style": "form" - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Metadata" - } - } - } - } - } + }, + "400": { + "description": "Bad request - Missing required fields or invalid data" + }, + "500": { + "description": "Server error - Failed to create bot through MeetingBaas API" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } } - }, - "/bots/{uuid}/delete_data": { - "post": { - "summary": "Delete Data", - "description": "Deletes a bot's data including recording, transcription, and logs. Only metadata is retained. Rate limited to 5 requests per minute per API key.", - "operationId": "delete_data", - "parameters": [ - { - "in": "path", - "name": "uuid", - "description": "The UUID identifier", - "required": true, - "schema": { - "description": "The UUID identifier", - "type": "string" - }, - "style": "simple" - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeleteResponse" - } - } - } - }, - "401": { - "description": "no content" - }, - "403": { - "description": "no content" - }, - "404": { - "description": "no content" - }, - "429": { - "description": "no content" - } - } + } + } + } + }, + "/bots/{bot_id}": { + "delete": { + "tags": [ + "bots" + ], + "summary": "Leave Bot", + "description": "Remove a bot from a meeting by its ID.\n\nThis will:\n1. Call the MeetingBaas API to make the bot leave\n2. Close WebSocket connections if they exist\n3. Terminate the associated Pipecat process", + "operationId": "leave_bot_bots__bot_id__delete", + "parameters": [ + { + "name": "bot_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Bot Id" } - }, - "/bots/bots_with_metadata": { - "get": { - "summary": "List Bots with Metadata", - "description": "Retrieves a paginated list of the user's bots with essential metadata, including IDs, names, and meeting details. Supports filtering, sorting, and advanced querying options.", - "operationId": "bots_with_metadata", - "parameters": [ - { - "in": "query", - "name": "bot_name", - "description": "Filter bots by name containing this string.\n\nPerforms a case-insensitive partial match on the bot's name. Useful for finding bots with specific naming conventions or to locate a particular bot when you don't have its ID.\n\nExample: \"Sales\" would match \"Sales Meeting\", \"Quarterly Sales\", etc.", - "schema": { - "description": "Filter bots by name containing this string.\n\nPerforms a case-insensitive partial match on the bot's name. Useful for finding bots with specific naming conventions or to locate a particular bot when you don't have its ID.\n\nExample: \"Sales\" would match \"Sales Meeting\", \"Quarterly Sales\", etc.", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "created_after", - "description": "Filter bots created after this date (ISO format).\n\nLimits results to bots created at or after the specified timestamp. Used for time-based filtering to find recent additions.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-01T00:00:00\"", - "schema": { - "description": "Filter bots created after this date (ISO format).\n\nLimits results to bots created at or after the specified timestamp. Used for time-based filtering to find recent additions.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-01T00:00:00\"", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "created_before", - "description": "Filter bots created before this date (ISO format).\n\nLimits results to bots created at or before the specified timestamp. Used for time-based filtering to exclude recent additions.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-31T23:59:59\"", - "schema": { - "description": "Filter bots created before this date (ISO format).\n\nLimits results to bots created at or before the specified timestamp. Used for time-based filtering to exclude recent additions.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-31T23:59:59\"", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "cursor", - "description": "Cursor for pagination, obtained from previous response.\n\nUsed for retrieving the next set of results after a previous call. The cursor value is returned in the `nextCursor` field of responses that have additional results available.\n\nFormat: Base64-encoded string containing pagination metadata", - "schema": { - "description": "Cursor for pagination, obtained from previous response.\n\nUsed for retrieving the next set of results after a previous call. The cursor value is returned in the `nextCursor` field of responses that have additional results available.\n\nFormat: Base64-encoded string containing pagination metadata", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "ended_after", - "description": "Filter bots ended after this date (ISO format).\n\nLimits results to bots that ended at or after the specified timestamp. Useful for finding completed meetings within a specific time period.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-01T00:00:00\"", - "schema": { - "description": "Filter bots ended after this date (ISO format).\n\nLimits results to bots that ended at or after the specified timestamp. Useful for finding completed meetings within a specific time period.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-01T00:00:00\"", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "filter_by_extra", - "description": "Filter bots by matching values in the extra JSON payload.\n\nThis parameter performs in-memory filtering on the `extra` JSON field, similar to a SQL WHERE clause. It reduces the result set to only include bots that match all specified conditions.\n\nFormat specifications: - Single condition: \"field:value\" - Multiple conditions: \"field1:value1,field2:value2\"\n\nExamples: - \"customer_id:12345\" - Only bots with this customer ID - \"status:active,project:sales\" - Only active bots from sales projects\n\nNotes: - All conditions must match for a bot to be included - Values are matched exactly (case-sensitive) - Bots without the specified field are excluded", - "schema": { - "description": "Filter bots by matching values in the extra JSON payload.\n\nThis parameter performs in-memory filtering on the `extra` JSON field, similar to a SQL WHERE clause. It reduces the result set to only include bots that match all specified conditions.\n\nFormat specifications: - Single condition: \"field:value\" - Multiple conditions: \"field1:value1,field2:value2\"\n\nExamples: - \"customer_id:12345\" - Only bots with this customer ID - \"status:active,project:sales\" - Only active bots from sales projects\n\nNotes: - All conditions must match for a bot to be included - Values are matched exactly (case-sensitive) - Bots without the specified field are excluded", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "limit", - "description": "Maximum number of bots to return in a single request.\n\nLimits the number of results returned in a single API call. This parameter helps control response size and page length.\n\nDefault: 10 Minimum: 1 Maximum: 50", - "schema": { - "description": "Maximum number of bots to return in a single request.\n\nLimits the number of results returned in a single API call. This parameter helps control response size and page length.\n\nDefault: 10 Minimum: 1 Maximum: 50", - "default": 10, - "type": "integer", - "format": "int32" - }, - "style": "form" - }, - { - "in": "query", - "name": "meeting_url", - "description": "Filter bots by meeting URL containing this string.\n\nPerforms a case-insensitive partial match on the bot's meeting URL. Use this to find bots associated with specific meeting platforms or particular meeting IDs.\n\nExample: \"zoom.us\" would match all Zoom meetings", - "schema": { - "description": "Filter bots by meeting URL containing this string.\n\nPerforms a case-insensitive partial match on the bot's meeting URL. Use this to find bots associated with specific meeting platforms or particular meeting IDs.\n\nExample: \"zoom.us\" would match all Zoom meetings", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "sort_by_extra", - "description": "Sort the results by a field in the extra JSON payload.\n\nThis parameter performs in-memory sorting on the `extra` JSON field, similar to a SQL ORDER BY clause. It changes the order of results but not which results are included.\n\nFormat specifications: - Default (ascending): \"field\" - Explicit direction: \"field:asc\" or \"field:desc\"\n\nExamples: - \"customer_id\" - Sort by customer_id (ascending) - \"priority:desc\" - Sort by priority (descending)\n\nNotes: - Applied after all filtering - String comparison is used for sorting - Bots with the field come before bots without it - Can be combined with filter_by_extra", - "schema": { - "description": "Sort the results by a field in the extra JSON payload.\n\nThis parameter performs in-memory sorting on the `extra` JSON field, similar to a SQL ORDER BY clause. It changes the order of results but not which results are included.\n\nFormat specifications: - Default (ascending): \"field\" - Explicit direction: \"field:asc\" or \"field:desc\"\n\nExamples: - \"customer_id\" - Sort by customer_id (ascending) - \"priority:desc\" - Sort by priority (descending)\n\nNotes: - Applied after all filtering - String comparison is used for sorting - Bots with the field come before bots without it - Can be combined with filter_by_extra", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "speaker_name", - "description": "NOTE: this is a preview feature and not yet available\n\nFilter bots by speaker name containing this string.\n\nPerforms a case-insensitive partial match on the speakers in the meeting. Useful for finding meetings that included a specific person.\n\nExample: \"John\" would match meetings with speakers like \"John Smith\" or \"John Doe\"", - "schema": { - "description": "NOTE: this is a preview feature and not yet available\n\nFilter bots by speaker name containing this string.\n\nPerforms a case-insensitive partial match on the speakers in the meeting. Useful for finding meetings that included a specific person.\n\nExample: \"John\" would match meetings with speakers like \"John Smith\" or \"John Doe\"", - "type": [ - "string", - "null" - ] - }, - "style": "form" - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListRecentBotsResponse" - } - } - } - } - } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveBotRequest" + } } + } }, - "/bots/retranscribe": { - "post": { - "summary": "Retranscribe Bot", - "description": "Transcribe or retranscribe a bot's audio using the Default or your provided Speech to Text Provider", - "operationId": "retranscribe_bot", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RetranscribeBody" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "description" - }, - "202": { - "description": "no content" - } - } + "responses": { + "200": { + "description": "Bot successfully removed from meeting", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Leave Bot Bots Bot Id Delete" + } + } } - }, - "/bots/{uuid}/screenshots": { - "get": { - "summary": "Get Screenshots", - "description": "Retrieves screenshots captured during the bot's session", - "operationId": "get_screenshots", - "parameters": [ - { - "in": "path", - "name": "uuid", - "description": "The UUID identifier", - "required": true, - "schema": { - "description": "The UUID identifier", - "type": "string" - }, - "style": "simple" - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ScreenshotsList" - } - } - } - } - } + }, + "400": { + "description": "Bad request - Missing required fields or identifiers" + }, + "404": { + "description": "Bot not found - No bot with the specified ID" + }, + "500": { + "description": "Server error - Failed to remove bot from MeetingBaas API" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } } - }, - "/bots/webhooks": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "Webhook Events Documentation", - "description": "Meeting BaaS sends webhook events to your configured webhook URL when specific events occur.\n\n## Webhook Event Types\n\n### 1. `complete`\nSent when a bot successfully completes recording a meeting. Contains full transcription data and a link to the recording.\n```json\n{\n \\\"event\\\": \\\"complete\\\",\n \\\"data\\\": {\n \\\"bot_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\",\n \\\"event_uuid\\\": \\\"123e4567-e89b-12d3-a456-426614174001\\\",\n \\\"transcript\\\": [\n {\n \\\"speaker\\\": \\\"John Doe\\\",\n \\\"offset\\\": 1.5,\n \\\"start_time\\\": 1.5,\n \\\"end_time\\\": 2.4,\n \\\"words\\\": [\n {\n \\\"start\\\": 1.5,\n \\\"end\\\": 1.9,\n \\\"word\\\": \\\"Hello\\\"\n },\n {\n \\\"start\\\": 2.0,\n \\\"end\\\": 2.4,\n \\\"word\\\": \\\"everyone\\\"\n }\n ]\n }\n ],\n \\\"speakers\\\": [\n \\\"Jane Smith\\\",\n \\\"John Doe\\\"\n ],\n \\\"mp4\\\": \\\"https://storage.example.com/recordings/video123.mp4?token=abc\\\",\n \\\"audio\\\": \\\"https://storage.example.com/recordings/audio123.wav?token=abc\\\",\n \\\"event\\\": \\\"complete\\\",\n \\\"extra\\\": {\n \\\"foo\\\": \\\"bar\\\"\n }\n }\n}\n```\n\nThe `complete` event includes:\n- **bot_id**: Unique identifier for the bot that completed recording\n- **event_uuid**: UUID of the calendar event (if this bot was created from an event)\n- **speakers**: A set of speaker names identified in the meeting\n- **transcript**: Full transcript data with speaker identification and word timing\n- **mp4**: URL to the recording file (valid for 24 hours by default)\n- **event**: Event type identifier (\"complete\")\n\n### 2. `failed`\nSent when a bot fails to join or record a meeting. Contains error details.\n```json\n{\n \\\"event\\\": \\\"failed\\\",\n \\\"data\\\": {\n \\\"bot_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\",\n \\\"event_uuid\\\": \\\"123e4567-e89b-12d3-a456-426614174001\\\",\n \\\"error\\\": \\\"meeting_not_found\\\",\n \\\"message\\\": \\\"Could not join meeting: The meeting ID was not found or has expired\\\",\n \\\"extra\\\": {\n \\\"foo\\\": \\\"bar\\\"\n }\n }\n}\n```\n\nThe `failed` event includes:\n- **bot_id**: Unique identifier for the bot that failed\n- **event_uuid**: UUID of the calendar event (if this bot was created from an event)\n- **error**: Error code identifying the type of failure\n- **message**: Detailed human-readable error message\n\nCommon error types include:\n- `meeting_not_found`: The meeting ID or link was invalid or expired\n- `access_denied`: The bot was denied access to the meeting\n- `authentication_error`: Failed to authenticate with the meeting platform\n- `network_error`: Network connectivity issues during recording\n- `internal_error`: Internal server error\n\n### 3. `calendar.sync_events`\nSent when calendar events are synced. Contains information about which events were updated.\n```json\n{\n \\\"event\\\": \\\"calendar.sync_events\\\",\n \\\"data\\\": {\n \\\"calendar_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\",\n \\\"last_updated_ts\\\": \\\"2023-05-01T12:00:00Z\\\",\n \\\"affected_event_uuids\\\": [\n \\\"123e4567-e89b-12d3-a456-426614174001\\\",\n \\\"123e4567-e89b-12d3-a456-426614174002\\\"\n ]\n }\n}\n```\n\nThe `calendar.sync_events` event includes:\n- **calendar_id**: UUID of the calendar that was synced\n- **last_updated_ts**: ISO-8601 timestamp of when the sync occurred\n- **affected_event_uuids**: Array of UUIDs for calendar events that were added, updated, or deleted\n\nThis event is triggered when:\n- Calendar data is synced with the external provider (Google, Microsoft)\n- Multiple events may be created, updated, or deleted in a single sync operation\n- Use this event to update your local cache of calendar events\n\n### 4. `transcription_complete`\nSent when transcription is completed separately from recording (e.g., after retranscribing).\n```json\n{\n \\\"event\\\": \\\"transcription_complete\\\",\n \\\"data\\\": {\n \\\"bot_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\"\n }\n}\n```\n\nThe `transcription_complete` event includes:\n- **bot_id**: Unique identifier for the bot with the completed transcription\n\nThis event is sent when:\n- You request a retranscription via the `/bots/retranscribe` endpoint\n- An asynchronous transcription process completes after the recording has ended\n\n## Setting Up Webhooks\n\nYou can configure webhooks in two ways:\n1. **Account-level webhook URL**: Set a default webhook URL for all bots in your account using the `/accounts/webhook_url` endpoint\n2. **Bot-specific webhook URL**: Provide a `webhook_url` parameter when creating a bot with the `/bots` endpoint\n\nYour webhook endpoint must:\n- Accept POST requests with JSON payload\n- Return a 2xx status code to acknowledge receipt\n- Process requests within 10 seconds to avoid timeouts\n- Handle each event type appropriately based on the event type\n\nAll webhook requests include:\n- `x-meeting-baas-api-key` header with your API key for verification\n- `content-type: application/json` header\n- JSON body containing the event details\n\n## Webhook Reliability\n\nIf your endpoint fails to respond or returns an error, the system will attempt to retry the webhook delivery. For critical events, we recommend implementing:\n\n- Idempotency handling to prevent duplicate processing of the same event\n- Proper logging of webhook receipts for audit purposes\n- Asynchronous processing to quickly acknowledge receipt before handling the event data\n\nFor security, always validate the API key in the `x-meeting-baas-api-key` header matches your API key.", - "operationId": "webhook_documentation", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": {} - } - } - } - } + } + } + } + }, + "/personas/generate-image": { + "post": { + "tags": [ + "personas" + ], + "summary": "Generate Persona Image", + "description": "Generate an image for a persona using Replicate.", + "operationId": "generate_persona_image_personas_generate_image_post", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonaImageRequest" + } } + } }, - "/bots/webhooks/bot": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "Bot Webhook Events Documentation", - "description": "Meeting BaaS sends the following webhook events related to bot recordings.\n\n## Bot Webhook Event Types\n\n### 1. `complete`\nSent when a bot successfully completes recording a meeting.\n\n**Payload Structure:**\n```json\n{\n \\\"event\\\": \\\"complete\\\",\n \\\"data\\\": {\n \\\"bot_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\",\n \\\"event_uuid\\\": \\\"123e4567-e89b-12d3-a456-426614174001\\\",\n \\\"transcript\\\": [\n {\n \\\"speaker\\\": \\\"John Doe\\\",\n \\\"offset\\\": 1.5,\n \\\"start_time\\\": 1.5,\n \\\"end_time\\\": 2.4,\n \\\"words\\\": [\n {\n \\\"start\\\": 1.5,\n \\\"end\\\": 1.9,\n \\\"word\\\": \\\"Hello\\\"\n },\n {\n \\\"start\\\": 2.0,\n \\\"end\\\": 2.4,\n \\\"word\\\": \\\"everyone\\\"\n }\n ]\n }\n ],\n \\\"speakers\\\": [\n \\\"Jane Smith\\\",\n \\\"John Doe\\\"\n ],\n \\\"mp4\\\": \\\"https://storage.example.com/recordings/video123.mp4?token=abc\\\",\n \\\"audio\\\": \\\"https://storage.example.com/recordings/audio123.wav?token=abc\\\",\n \\\"event\\\": \\\"complete\\\",\n \\\"extra\\\": {\n \\\"foo\\\": \\\"bar\\\"\n }\n }\n}\n```\n\n**When it's triggered:**\n- After a bot successfully records and processes a meeting\n- After the recording is uploaded and made available\n- When all processing of the meeting recording is complete\n\n**What to do with it:**\n- Download the MP4 recording for storage in your system\n- Store the transcript data in your database\n- Update meeting status in your application\n- Notify users that the recording is available\n- Use `event_uuid` to correlate with calendar events (if applicable)\n\n### 2. `failed`\nSent when a bot fails to join or record a meeting.\n\n**Payload Structure:**\n```json\n{\n \\\"event\\\": \\\"failed\\\",\n \\\"data\\\": {\n \\\"bot_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\",\n \\\"event_uuid\\\": \\\"123e4567-e89b-12d3-a456-426614174001\\\",\n \\\"error\\\": \\\"meeting_not_found\\\",\n \\\"message\\\": \\\"Could not join meeting: The meeting ID was not found or has expired\\\",\n \\\"extra\\\": {\n \\\"foo\\\": \\\"bar\\\"\n }\n }\n}\n```\n\n**Common error types:**\n- `meeting_not_found`: The meeting ID or link was invalid or expired\n- `access_denied`: The bot was denied access to the meeting\n- `authentication_error`: Failed to authenticate with the meeting platform\n- `network_error`: Network connectivity issues during recording\n- `internal_error`: Internal server error\n\n**What to do with it:**\n- Log the failure for troubleshooting\n- Notify administrators or users about the failed recording\n- Attempt to reschedule if appropriate\n- Update meeting status in your system\n- Use `event_uuid` to correlate with calendar events (if applicable)\n\n### 3. `transcription_complete`\nSent when transcription is completed separately from recording.\n\n**Payload Structure:**\n```json\n{\n \\\"event\\\": \\\"transcription_complete\\\",\n \\\"data\\\": {\n \\\"bot_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\"\n }\n}\n```\n\n**When it's triggered:**\n- After requesting retranscription via the API\n- When an asynchronous transcription job completes\n- When a higher quality or different language transcription becomes available\n\n**What to do with it:**\n- Update the transcript data in your system\n- Notify users that improved transcription is available\n- Run any post-processing on the new transcript data\n\n## Webhook Usage Tips\n\n- Each event includes the `bot_id` so you can correlate with your internal data\n- The `event_uuid` field is included when the bot was created from a calendar event (null for direct bots or scheduled bots)\n- The complete event includes speaker identification and full transcript data\n- For downloading recordings, the mp4 URL is valid for 24 hours\n- Handle the webhook asynchronously and return 200 OK quickly to prevent timeouts\n\nFor security, always validate the API key in the `x-meeting-baas-api-key` header matches your API key.", - "operationId": "bot_webhook_documentation", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": {} - } - } - } - } + "responses": { + "201": { + "description": "Image successfully generated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonaImageResponse" + } + } } - }, - "/bots/webhooks/calendar": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "Calendar Webhook Events Documentation", - "description": "Meeting BaaS sends the following webhook events related to calendar integrations.\n\n## Calendar Webhook Event Types\n\n### 1. `calendar.sync_events`\nSent when calendar events are synced with external providers.\n\n**Payload Structure:**\n```json\n{\n \\\"event\\\": \\\"calendar.sync_events\\\",\n \\\"data\\\": {\n \\\"calendar_id\\\": \\\"123e4567-e89b-12d3-a456-426614174000\\\",\n \\\"last_updated_ts\\\": \\\"2023-05-01T12:00:00Z\\\",\n \\\"affected_event_uuids\\\": [\n \\\"123e4567-e89b-12d3-a456-426614174001\\\",\n \\\"123e4567-e89b-12d3-a456-426614174002\\\"\n ]\n }\n}\n```\n\n**When it's triggered:**\n- After initial calendar connection is established\n- When external calendar providers (Google, Microsoft) send change notifications\n- After manual calendar resync operations\n- During scheduled periodic syncs\n- When events are created, updated, or deleted in the source calendar\n\n**What to do with it:**\n- Update your local copy of calendar events\n- Process any new events that match your criteria\n- Remove any deleted events from your system\n- Update schedules for any modified events\n- Refresh your UI to show the latest calendar data\n\n**Field details:**\n- `calendar_id`: The UUID of the synchronized calendar\n- `last_updated_ts`: ISO-8601 timestamp when the sync occurred\n- `affected_event_uuids**: Array of UUIDs for events that were changed\n\n## Integration with Meeting BaaS Calendar API\n\nAfter receiving a calendar webhook event, you can:\n1. Use the `/calendar_events` endpoint to retrieve detailed information about specific events\n2. Use the `/calendars/:uuid` endpoint to get calendar metadata\n3. Schedule recording bots for any new meetings with the `/calendar_events/:uuid/bot` endpoint\n\n## Webhook Usage Tips\n\n- Each event includes affected event UUIDs for efficient processing\n- You don't need to retrieve all calendar events - just process the changed ones\n- The timestamp helps determine the sequence of updates\n- For high-frequency calendars, consider batch processing of multiple events\n\nFor security, always validate the API key in the `x-meeting-baas-api-key` header matches your API key.", - "operationId": "calendar_webhook_documentation", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": {} - } - } - } - } + }, + "400": { + "description": "Invalid request data" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } } - }, - "/calendars/raw": { - "post": { - "tags": [ - "Calendars" - ], - "summary": "List Raw Calendars", - "description": "Retrieves unprocessed calendar data directly from the provider (Google, Microsoft) using provided OAuth credentials. This endpoint is typically used during the initial setup process to allow users to select which calendars to integrate. Returns a list of available calendars with their unique IDs, email addresses, and primary status. This data is not persisted until a calendar is formally created using the create_calendar endpoint.", - "operationId": "list_raw_calendars", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListRawCalendarsParams" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListRawCalendarsResponse" - } - } - } - } - } - } - }, - "/calendars/": { - "get": { - "tags": [ - "Calendars" - ], - "summary": "List Calendars", - "description": "Retrieves all calendars that have been integrated with the system for the authenticated user. Returns a list of calendars with their names, email addresses, provider information, and sync status. This endpoint shows only calendars that have been formally connected through the create_calendar endpoint, not all available calendars from the provider.", - "operationId": "list_calendars", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Calendar" - } - } - } - } - } - } - }, - "post": { - "tags": [ - "Calendars" - ], - "summary": "Create Calendar", - "description": "Integrates a new calendar with the system using OAuth credentials. This endpoint establishes a connection with the calendar provider (Google, Microsoft), sets up webhook notifications for real-time updates, and performs an initial sync of all calendar events. It requires OAuth credentials (client ID, client secret, and refresh token) and the platform type. Once created, the calendar is assigned a unique UUID that should be used for all subsequent operations. Returns the newly created calendar object with all integration details.", - "operationId": "create_calendar", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateCalendarParams" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateCalendarResponse" - } - } - } - } - } - } - }, - "/calendars/resync_all": { - "post": { - "tags": [ - "Calendars" - ], - "summary": "Resync All Calendars", - "description": "Forces a sync of all your connected calendars with their providers (Google, Microsoft).\n\nProcesses each calendar individually and returns:\n- `synced_calendars`: UUIDs of successfully synced calendars\n- `errors`: Details of any failures\n\nSends webhook notifications for calendars with updates.", - "operationId": "resync_all_calendars", - "parameters": [ - { - "in": "query", - "name": "days", - "description": "Number of days to sync forward (default: 30 for rolling window)", - "schema": { - "description": "Number of days to sync forward (default: 30 for rolling window)", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "style": "form" - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResyncAllCalendarsResponse" - } - } - } - } - } + } + } + } + }, + "/webhook": { + "post": { + "tags": [ + "webhook" + ], + "summary": "Meetingbaas Webhook", + "description": "Webhook endpoint for MeetingBaas callbacks.\n\nReceives events like bot_joined, bot_left, call_ended, transcription, etc.\n- On 'in_call_recording': signals Pipecat to start speaking\n- On call end: generates a summary from the transcript", + "operationId": "meetingbaas_webhook_webhook_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } } - }, - "/calendars/{uuid}": { - "get": { - "tags": [ - "Calendars" - ], - "summary": "Get Calendar", - "description": "Retrieves detailed information about a specific calendar integration by its UUID. Returns comprehensive calendar data including the calendar name, email address, provider details (Google, Microsoft), sync status, and other metadata. This endpoint is useful for displaying calendar information to users or verifying the status of a calendar integration before performing operations on its events.", - "operationId": "get_calendar", - "parameters": [ - { - "in": "path", - "name": "uuid", - "description": "The UUID identifier", - "required": true, - "schema": { - "description": "The UUID identifier", - "type": "string" - }, - "style": "simple" - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Calendar" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Calendars" - ], - "summary": "Delete Calendar", - "description": "Permanently removes a calendar integration by its UUID, including all associated events and bot configurations. This operation cancels any active subscriptions with the calendar provider, stops all webhook notifications, and unschedules any pending recordings. All related resources are cleaned up in the database. This action cannot be undone, and subsequent requests to this calendar's UUID will return 404 Not Found errors.", - "operationId": "delete_calendar", - "parameters": [ - { - "in": "path", - "name": "uuid", - "description": "The UUID identifier", - "required": true, - "schema": { - "description": "The UUID identifier", - "type": "string" - }, - "style": "simple" - } - ], - "responses": { - "200": { - "description": "no content" - } - } - }, - "patch": { - "tags": [ - "Calendars" - ], - "summary": "Update Calendar", - "description": "Updates a calendar integration with new credentials or platform while maintaining the same UUID. This operation is performed as an atomic transaction to ensure data integrity. The system automatically unschedules existing bots to prevent duplicates, updates the calendar credentials, and triggers a full resync of all events. Useful when OAuth tokens need to be refreshed or when migrating a calendar between providers. Returns the updated calendar object with its new configuration.", - "operationId": "update_calendar", - "parameters": [ - { - "in": "path", - "name": "uuid", - "description": "The UUID identifier", - "required": true, - "schema": { - "description": "The UUID identifier", - "type": "string" - }, - "style": "simple" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateCalendarParams" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateCalendarResponse" - } - } - } - } - } + } + } + } + }, + "/health": { + "get": { + "tags": [ + "system" + ], + "summary": "Health", + "description": "Health check endpoint", + "operationId": "health_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } } - }, - "/calendar_events/{uuid}": { - "get": { - "tags": [ - "Calendars" - ], - "summary": "Get Event", - "description": "Retrieves comprehensive details about a specific calendar event by its UUID. Returns complete event information including title, meeting link, start and end times, organizer status, recurrence information, and the full list of attendees with their names and email addresses. Also includes any associated bot parameters if recording is scheduled for this event. The raw calendar data from the provider is also included for advanced use cases.", - "operationId": "get_event", - "parameters": [ - { - "in": "path", - "name": "uuid", - "description": "The UUID identifier", - "required": true, - "schema": { - "description": "The UUID identifier", - "type": "string" - }, - "style": "simple" - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Event" - } - } - } - } - } + } + } + } + }, + "/ready": { + "get": { + "tags": [ + "system" + ], + "summary": "Ready", + "description": "Readiness endpoint with externally visible base URL resolution.", + "operationId": "ready_ready_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } } - }, - "/calendar_events/{uuid}/bot": { - "post": { - "tags": [ - "Calendars" - ], - "summary": "Schedule Record Event", - "description": "Configures a bot to automatically join and record a specific calendar event at its scheduled time. The UUID in the request path is the event UUID. The request body contains detailed bot configuration, including recording options, streaming settings, and webhook notification URLs. For recurring events, the 'all_occurrences' parameter can be set to true to schedule recording for all instances of the recurring series, or false (default) to schedule only the specific instance. Returns the updated event(s) with the bot parameters attached.", - "operationId": "schedule_record_event", - "parameters": [ - { - "in": "path", - "name": "uuid", - "description": "The UUID identifier", - "required": true, - "schema": { - "description": "The UUID identifier", - "type": "string" - }, - "style": "simple" - }, - { - "in": "query", - "name": "all_occurrences", - "description": "schedule a bot to all occurences of a recurring event", - "schema": { - "description": "schedule a bot to all occurences of a recurring event", - "type": [ - "boolean", - "null" - ] - }, - "style": "form" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BotParam2" - } - } - }, - "required": true + } + } + } + } + }, + "components": { + "schemas": { + "BotRequest": { + "properties": { + "meeting_url": { + "type": "string", + "title": "Meeting Url", + "description": "URL of the Google Meet, Zoom or Microsoft Teams meeting to join" + }, + "bot_name": { + "type": "string", + "title": "Bot Name", + "description": "Name to display for the bot in the meeting", + "default": "" + }, + "personas": { + "anyOf": [ + { + "items": { + "type": "string" }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Event" - } - } - } - } - } - } - }, - "delete": { - "tags": [ - "Calendars" - ], - "summary": "Unschedule Record Event", - "description": "Cancels a previously scheduled recording for a calendar event and releases associated bot resources. For recurring events, the 'all_occurrences' parameter controls whether to unschedule from all instances of the recurring series or just the specific occurrence. This operation is idempotent and will not error if no bot was scheduled. Returns the updated event(s) with the bot parameters removed.", - "operationId": "unschedule_record_event", - "parameters": [ - { - "in": "path", - "name": "uuid", - "description": "The UUID identifier", - "required": true, - "schema": { - "description": "The UUID identifier", - "type": "string" - }, - "style": "simple" - }, - { - "in": "query", - "name": "all_occurrences", - "description": "unschedule a bot from all occurences of a recurring event", - "schema": { - "description": "unschedule a bot from all occurences of a recurring event", - "type": [ - "boolean", - "null" - ] - }, - "style": "form" - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Event" - } - } - } - } - } - } - }, - "patch": { - "tags": [ - "Calendars" - ], - "summary": "Patch Bot", - "description": "Updates the configuration of a bot already scheduled to record an event. Allows modification of recording settings, webhook URLs, and other bot parameters without canceling and recreating the scheduled recording. For recurring events, the 'all_occurrences' parameter determines whether changes apply to all instances or just the specific occurrence. Returns the updated event(s) with the modified bot parameters.", - "operationId": "patch_bot", - "parameters": [ - { - "in": "path", - "name": "uuid", - "description": "The UUID identifier", - "required": true, - "schema": { - "description": "The UUID identifier", - "type": "string" - }, - "style": "simple" - }, - { - "in": "query", - "name": "all_occurrences", - "description": "schedule a bot to all occurences of a recurring event", - "schema": { - "description": "schedule a bot to all occurences of a recurring event", - "type": [ - "boolean", - "null" - ] - }, - "style": "form" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BotParam3" - } - } - }, - "required": true + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Personas", + "description": "List of persona names to use. The first available will be selected." + }, + "bot_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Bot Image" + }, + "entry_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Entry Message" + }, + "extra": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Extra" + }, + "enable_tools": { + "type": "boolean", + "title": "Enable Tools", + "default": true + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt" + }, + "websocket_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Websocket Url", + "description": "Optional public WebSocket base URL override, e.g. wss://bot.example.com" + }, + "turn_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/TurnConfig" + }, + { + "type": "null" + } + ], + "description": "Per-bot turn-taking tuning (VAD confidence/start_secs/stop_secs/min_volume)" + }, + "prompt_data_sources": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/PromptDataSource" }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Event" - } - } - } - } - } - } - } + "type": "array", + "maxItems": 10 + }, + { + "type": "null" + } + ], + "title": "Prompt Data Sources", + "description": "External text or URL data sources to append to the bot prompt" + }, + "prompt_data_token_limit": { + "type": "integer", + "maximum": 50000.0, + "minimum": 0.0, + "title": "Prompt Data Token Limit", + "description": "Approximate total token cap for loaded prompt_data_sources. 0 disables loading.", + "default": 4000 + }, + "mcp": { + "anyOf": [ + { + "$ref": "#/components/schemas/MCPConfig" + }, + { + "type": "null" + } + ], + "description": "MCP server/tool metadata and optional live connection details" + }, + "speech_speed": { + "anyOf": [ + { + "type": "number", + "maximum": 2.0, + "minimum": 0.5 + }, + { + "type": "null" + } + ], + "title": "Speech Speed", + "description": "TTS speaking speed multiplier. Defaults to CARTESIA_TTS_SPEED, TTS_SPEED, SPEECH_SPEED, or the runner default." + } }, - "/calendar_events/": { - "get": { - "tags": [ - "Calendars" - ], - "summary": "List Events", - "description": "Retrieves a paginated list of calendar events with comprehensive filtering options. Supports filtering by organizer email, attendee email, date ranges (start_date_gte, start_date_lte), and event status. Results can be limited to upcoming events (default), past events, or all events. Each event includes full details such as meeting links, participants, and recording status. The response includes a 'next' pagination cursor for retrieving additional results.", - "operationId": "list_events", - "parameters": [ - { - "in": "query", - "name": "attendee_email", - "description": "If provided, filters events to include only those with this attendee's email address Example: \"jane.smith@example.com\"", - "schema": { - "description": "If provided, filters events to include only those with this attendee's email address Example: \"jane.smith@example.com\"", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "calendar_id", - "description": "Calendar ID to filter events by This is required to specify which calendar's events to retrieve", - "required": true, - "schema": { - "description": "Calendar ID to filter events by This is required to specify which calendar's events to retrieve", - "type": "string" - }, - "style": "form" - }, - { - "in": "query", - "name": "cursor", - "description": "Optional cursor for pagination This value is included in the `next` field of the previous response", - "schema": { - "description": "Optional cursor for pagination This value is included in the `next` field of the previous response", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "organizer_email", - "description": "If provided, filters events to include only those with this organizer's email address Example: \"john.doe@example.com\"", - "schema": { - "description": "If provided, filters events to include only those with this organizer's email address Example: \"john.doe@example.com\"", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "start_date_gte", - "description": "If provided, filters events to include only those with a start date greater than or equal to this timestamp Format: ISO-8601 string, e.g., \"2023-01-01T00:00:00Z\"", - "schema": { - "description": "If provided, filters events to include only those with a start date greater than or equal to this timestamp Format: ISO-8601 string, e.g., \"2023-01-01T00:00:00Z\"", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "start_date_lte", - "description": "If provided, filters events to include only those with a start date less than or equal to this timestamp Format: ISO-8601 string, e.g., \"2023-12-31T23:59:59Z\"", - "schema": { - "description": "If provided, filters events to include only those with a start date less than or equal to this timestamp Format: ISO-8601 string, e.g., \"2023-12-31T23:59:59Z\"", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "status", - "description": "Filter events by meeting status Valid values: \"upcoming\" (default) returns events after current time, \"past\" returns previous events, \"all\" returns both", - "schema": { - "description": "Filter events by meeting status Valid values: \"upcoming\" (default) returns events after current time, \"past\" returns previous events, \"all\" returns both", - "type": [ - "string", - "null" - ] - }, - "style": "form" - }, - { - "in": "query", - "name": "updated_at_gte", - "description": "If provided, fetches only events updated at or after this timestamp Format: ISO-8601 string, e.g., \"2023-01-01T00:00:00Z\"", - "schema": { - "description": "If provided, fetches only events updated at or after this timestamp Format: ISO-8601 string, e.g., \"2023-01-01T00:00:00Z\"", - "type": [ - "string", - "null" - ] - }, - "style": "form" - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ListEventResponse" - } - } - } - } - } + "additionalProperties": false, + "type": "object", + "required": [ + "meeting_url" + ], + "title": "BotRequest", + "description": "Request model for creating a speaking bot in a meeting.", + "example": { + "bot_image": "https://example.com/bot-avatar.png", + "bot_name": "Meeting Assistant", + "enable_tools": true, + "entry_message": "Hello! I'm here to assist with the meeting.", + "extra": { + "company": "ACME Corp", + "meeting_purpose": "Weekly sync" + }, + "mcp": { + "servers": [ + { + "name": "crm", + "tool_allowlist": [ + "get_account", + "list_recent_calls" + ], + "tools": [ + "get_account", + "list_recent_calls" + ], + "transport": "streamable_http", + "url": "https://mcp.example.com" + } + ] + }, + "meeting_url": "https://meet.google.com/abc-defg-hij", + "personas": [ + "helpful_assistant", + "meeting_facilitator" + ], + "prompt": "You are Meeting Assistant, a concise and professional AI bot that helps summarize key points and keep the meeting on track. Speak clearly and stay on topic.", + "prompt_data_sources": [ + { + "name": "CRM account notes", + "type": "url", + "url": "https://example.com/account-notes.md" } + ], + "prompt_data_token_limit": 3000, + "speech_speed": 1.15, + "websocket_url": "wss://bots.example.com" + } + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } }, - "/zoom_oauth_connections/": { - "get": { - "tags": [ - "Zoom OAuth" - ], - "summary": "List Zoom OAuth Connections", - "description": "Retrieves all Zoom OAuth connections associated with the authenticated account. Each connection represents a Zoom user who has authorized your app via OAuth. Use this to display connected users or to find the `zoom_user_id` needed for the `zoom_obf_token_user_id` bot parameter. Sensitive token data is never included in the response.", - "operationId": "list_zoom_oauth_connections", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ZoomOAuthConnectionResponse" - } - } - } - } - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - }, - "post": { - "tags": [ - "Zoom OAuth" - ], - "summary": "Create Zoom OAuth Connection", - "description": "Exchanges a Zoom OAuth authorization code for access and refresh tokens, retrieves the Zoom user's profile, and stores the connection for managed OBF token generation. The authorization code is obtained by directing a Zoom user through the OAuth consent flow for your Zoom OAuth app. Once stored, you can reference this connection's `zoom_user_id` as the `zoom_obf_token_user_id` parameter when creating a bot, and the system will automatically fetch a fresh OBF token at join time. Note: the authorization code is single-use and expires in approximately 10 minutes.", - "operationId": "create_zoom_oauth_connection", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateConnectionRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Error response" - }, - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ZoomOAuthConnectionResponse" - } - } - } - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } + "type": "object", + "title": "HTTPValidationError" + }, + "JoinResponse": { + "properties": { + "bot_id": { + "type": "string", + "title": "Bot Id", + "description": "The MeetingBaas bot ID used for API operations with MeetingBaas" + } }, - "/zoom_oauth_connections/{uuid}": { - "get": { - "tags": [ - "Zoom OAuth" - ], - "summary": "Get Zoom OAuth Connection", - "description": "Retrieves a specific Zoom OAuth connection by its UUID. Returns the connection details including the Zoom user ID, account ID, connection state, and granted scopes. Sensitive token data is never included in the response.", - "operationId": "get_zoom_oauth_connection", - "parameters": [ - { - "in": "path", - "name": "uuid", - "description": "The UUID identifier", - "required": true, - "schema": { - "description": "The UUID identifier", - "type": "string" - }, - "style": "simple" - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ZoomOAuthConnectionResponse" - } - } - } - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - }, - "delete": { - "tags": [ - "Zoom OAuth" - ], - "summary": "Delete Zoom OAuth Connection", - "description": "Permanently deletes a Zoom OAuth connection by its UUID, removing all stored tokens. After deletion, bots using this connection's `zoom_user_id` as `zoom_obf_token_user_id` will no longer be able to automatically fetch OBF tokens. The Zoom user would need to re-authorize to create a new connection.", - "operationId": "delete_zoom_oauth_connection", - "parameters": [ - { - "in": "path", - "name": "uuid", - "description": "The UUID identifier", - "required": true, - "schema": { - "description": "The UUID identifier", - "type": "string" - }, - "style": "simple" - } - ], - "responses": { - "200": { - "description": "Error response" - }, - "204": { - "description": "no content" - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } - } - }, - "components": { - "securitySchemes": { - "ApiKeyAuth": { - "type": "apiKey", - "in": "header", - "name": "x-meeting-baas-api-key", - "description": "API key for authentication" - } + "type": "object", + "required": [ + "bot_id" + ], + "title": "JoinResponse", + "description": "Response model for a bot joining a meeting" + }, + "LeaveBotRequest": { + "properties": { + "bot_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Bot Id", + "description": "The MeetingBaas bot ID to remove from the meeting. This will also close the WebSocket connection made through Pipecat by this bot." + } }, - "schemas": { - "Account": { - "description": "This structure represents the user's account information.", - "type": "object", - "required": [ - "created_at", - "email", - "id", - "secret", - "status" - ], - "properties": { - "company_name": { - "type": [ - "string", - "null" - ] - }, - "created_at": { - "$ref": "#/components/schemas/SystemTime" - }, - "email": { - "type": "string" - }, - "firstname": { - "type": [ - "string", - "null" - ] - }, - "id": { - "type": "integer", - "format": "int32" - }, - "lastname": { - "type": [ - "string", - "null" - ] - }, - "phone": { - "type": [ - "string", - "null" - ] - }, - "secret": { - "type": "string" - }, - "status": { - "type": "integer", - "format": "int32" - } - } - }, - "AccountInfos": { - "description": "Structure consisting account information.", - "type": "object", - "required": [ - "account" - ], - "properties": { - "account": { - "$ref": "#/components/schemas/Account" - } - } - }, - "ApiKeyResponse": { - "type": "object", - "required": [ - "api_key" - ], - "properties": { - "api_key": { - "type": "string" - } - } - }, - "Attendee": { - "type": "object", - "required": [ - "email" - ], - "properties": { - "email": { - "description": "The email address of the meeting attendee", - "type": "string" - }, - "name": { - "description": "The display name of the attendee if available from the calendar provider (Google, Microsoft)", - "type": [ - "string", - "null" - ] - } - } - }, - "AudioFile": { - "type": "object", - "required": [ - "file_extension", - "file_type", - "filename" - ], - "properties": { - "file_extension": { - "type": "string" - }, - "file_type": { - "type": "string" - }, - "filename": { - "type": "string" - } - } - }, - "AudioFrequency": { - "type": "string", - "enum": [ - "16khz", - "24khz" - ] - }, - "AutomaticLeaveRequest": { - "type": "object", - "properties": { - "noone_joined_timeout": { - "description": "The timeout in seconds for the bot to wait for participants to join before leaving the meeting, defaults to 600 seconds (10 minutes). Minimum: 120 seconds (2 minutes). Maximum: 1800 seconds (30 minutes). When a bot first joins a meeting, it uses this timeout to determine if any participants have joined. If no participants are detected within this period, the bot will leave the meeting. Once participants are detected, the silence_timeout takes over. Applies to Google Meet and Microsoft Teams only.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0 - }, - "silence_timeout": { - "description": "The timeout in seconds for the bot to leave the meeting if no speaker activity is detected, defaults to 600 seconds (10 minutes). Minimum: 300 seconds (5 minutes). Maximum: 1800 seconds (30 minutes). This timeout becomes active after participants are detected. The bot monitors for audio activity, and if no sound is detected for the duration of this timeout, it will automatically leave the meeting. Important: Configure these timeouts carefully to ensure the bot doesn't leave too early - the noone_joined_timeout should be long enough to wait for late joiners, and the silence_timeout should account for intentional pauses. Applies to Google Meet and Microsoft Teams only.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0 - }, - "waiting_room_timeout": { - "description": "The timeout in seconds for the bot to wait in the waiting room before leaving the meeting, defaults to 600 seconds (10 minutes). Minimum: 120 seconds (2 minutes). Maximum: 1800 seconds (30 minutes). Note: Google Meet also has it's own waiting room timeout (about ~10 minutes). Setting a higher value for such meetings would have no effect because Google Meet will deny entry to the bot after its own timeout.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0 - } - } - }, - "Bot": { - "type": "object", - "required": [ - "bot", - "duration", - "params" - ], - "properties": { - "bot": { - "$ref": "#/components/schemas/Bot2" - }, - "duration": { - "type": "integer", - "format": "int64" - }, - "params": { - "$ref": "#/components/schemas/BotParam" - } - } - }, - "Bot2": { - "type": "object", - "required": [ - "account_id", - "bot_exited_at", - "bot_joined_at", - "bot_param_id", - "created_at", - "diarization_v2", - "ended_at", - "id", - "meeting_url", - "mp4_s3_path", - "reserved", - "uuid" - ], - "properties": { - "account_id": { - "type": "integer", - "format": "int32" - }, - "bot_exited_at": { - "$ref": "#/components/schemas/OptionalDateTime" - }, - "bot_joined_at": { - "$ref": "#/components/schemas/OptionalDateTime" - }, - "bot_param_id": { - "type": "integer", - "format": "int32" - }, - "created_at": { - "$ref": "#/components/schemas/DateTime" - }, - "diarization_fails": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "diarization_v2": { - "type": "boolean" - }, - "ended_at": { - "$ref": "#/components/schemas/OptionalDateTime" - }, - "errors": { - "type": [ - "string", - "null" - ] - }, - "event_id": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "id": { - "type": "integer", - "format": "int32" - }, - "meeting_url": { - "type": "string" - }, - "mp4_s3_path": { - "type": "string" - }, - "reserved": { - "type": "boolean" - }, - "scheduled_bot_id": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "session_id": { - "type": [ - "string", - "null" - ] - }, - "transcription_fails": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "transcription_payloads": true, - "user_reported_error": true, - "uuid": { - "type": "string", - "format": "uuid" - } - } - }, - "BotCrashedQuery": { - "type": "object", - "required": [ - "bot_uuid" - ], - "properties": { - "bot_uuid": { - "type": "string" - } - } - }, - "BotCrashedRequest": { - "type": "object", - "required": [ - "crashReason" - ], - "properties": { - "crashReason": { - "type": "string" - }, - "efsManifest": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/components/schemas/EfsManifestEntry" - } - }, - "exitCode": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "signal": { - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "BotData": { - "type": "object", - "required": [ - "bot", - "transcripts" - ], - "properties": { - "bot": { - "$ref": "#/components/schemas/BotWithParams" - }, - "event_uuid": { - "description": "UUID of the calendar event (if this bot was created from an event)", - "type": [ - "string", - "null" - ], - "format": "uuid" - }, - "transcripts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Transcript" - } - } - } - }, - "BotPagined": { - "description": "Paginated bot results with status information\n\nThis response includes bots with their status information and pagination metadata to support loading additional results.\n\nThe bots are automatically sorted by status priority (critical issues first).", - "type": "object", - "required": [ - "bots", - "has_more" - ], - "properties": { - "bots": { - "description": "List of bots with status information, sorted by priority", - "type": "array", - "items": { - "$ref": "#/components/schemas/BotWithStatus" - } - }, - "has_more": { - "description": "Whether there are more results available at the next offset", - "type": "boolean" - } - } - }, - "BotParam": { - "type": "object", - "required": [ - "bot_name", - "extra", - "transcription_custom_parameters", - "webhook_url" - ], - "properties": { - "bot_image": { - "type": [ - "string", - "null" - ] - }, - "bot_name": { - "type": "string" - }, - "deduplication_key": { - "type": [ - "string", - "null" - ] - }, - "enter_message": { - "type": [ - "string", - "null" - ] - }, - "extra": { - "$ref": "#/components/schemas/Extra" - }, - "noone_joined_timeout": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "recording_mode": { - "anyOf": [ - { - "$ref": "#/components/schemas/RecordingMode" - }, - { - "type": "null" - } - ] - }, - "speech_to_text_api_key": { - "type": [ - "string", - "null" - ] - }, - "speech_to_text_provider": { - "anyOf": [ - { - "$ref": "#/components/schemas/SpeechToTextProvider" - }, - { - "type": "null" - } - ] - }, - "streaming_audio_frequency": { - "anyOf": [ - { - "$ref": "#/components/schemas/AudioFrequency" - }, - { - "type": "null" - } - ] - }, - "streaming_input": { - "type": [ - "string", - "null" - ] - }, - "streaming_output": { - "type": [ - "string", - "null" - ] - }, - "transcription_custom_parameters": { - "$ref": "#/components/schemas/Extra" - }, - "waiting_room_timeout": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "webhook_url": { - "type": "string" - }, - "zoom_access_token_url": { - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token": { - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token_url": { - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token_user_id": { - "type": [ - "string", - "null" - ] - }, - "zoom_sdk_id": { - "type": [ - "string", - "null" - ] - }, - "zoom_sdk_pwd": { - "type": [ - "string", - "null" - ] - } - } - }, - "BotParam2": { - "type": "object", - "required": [ - "bot_name" - ], - "properties": { - "bot_image": { - "type": [ - "string", - "null" - ] - }, - "bot_name": { - "type": "string" - }, - "deduplication_key": { - "type": [ - "string", - "null" - ] - }, - "enter_message": { - "type": [ - "string", - "null" - ] - }, - "extra": { - "default": null, - "$ref": "#/components/schemas/Extra" - }, - "noone_joined_timeout": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "recording_mode": { - "anyOf": [ - { - "$ref": "#/components/schemas/RecordingMode" - }, - { - "type": "null" - } - ] - }, - "silence_timeout": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "speech_to_text": { - "anyOf": [ - { - "$ref": "#/components/schemas/SpeechToText" - }, - { - "type": "null" - } - ] - }, - "streaming_audio_frequency": { - "anyOf": [ - { - "$ref": "#/components/schemas/AudioFrequency" - }, - { - "type": "null" - } - ] - }, - "streaming_input": { - "type": [ - "string", - "null" - ] - }, - "streaming_output": { - "type": [ - "string", - "null" - ] - }, - "transcription_custom_parameters": { - "default": null, - "$ref": "#/components/schemas/Extra" - }, - "waiting_room_timeout": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "webhook_url": { - "type": [ - "string", - "null" - ] - }, - "zoom_access_token_url": { - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token": { - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token_url": { - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token_user_id": { - "type": [ - "string", - "null" - ] - }, - "zoom_sdk_id": { - "type": [ - "string", - "null" - ] - }, - "zoom_sdk_pwd": { - "type": [ - "string", - "null" - ] - } - } - }, - "BotParam3": { - "type": "object", - "properties": { - "bot_image": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "bot_name": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "deduplication_key": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "enter_message": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "extra": { - "default": null - }, - "noone_joined_timeout": { - "default": null, - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "recording_mode": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/components/schemas/RecordingMode" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } - ] - }, - "speech_to_text": { - "default": null, - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/components/schemas/SpeechToText" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } - ] - }, - "streaming_audio_frequency": { - "default": null, - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/components/schemas/AudioFrequency" - }, - { - "type": "null" - } - ] - }, - { - "type": "null" - } - ] - }, - "streaming_input": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "streaming_output": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "transcription_custom_parameters": { - "default": null - }, - "waiting_room_timeout": { - "default": null, - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "webhook_url": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "zoom_access_token_url": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token_url": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token_user_id": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "zoom_sdk_id": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "zoom_sdk_pwd": { - "default": null, - "type": [ - "string", - "null" - ] - } - } - }, - "BotStatusResponse": { - "description": "API response type for frontend developers to display and sort bot statuses\n\n# Fields * `value` - String representation of the status suitable for displaying and filtering * `type` - Category of status (success, error, warning, pending) * `details` - Optional detailed explanation of the status * `sort_priority` - Numeric value for sorting (lower = higher priority) * `category` - Logical grouping of similar status types", - "type": "object", - "required": [ - "category", - "sort_priority", - "type", - "value" - ], - "properties": { - "category": { - "description": "Logical grouping of status", - "type": "string" - }, - "details": { - "description": "Detailed explanation when available", - "type": [ - "string", - "null" - ] - }, - "sort_priority": { - "description": "Numeric priority for sorting (0 = highest priority)", - "type": "integer", - "format": "int32" - }, - "type": { - "description": "Status type (success, error, warning, pending)", - "type": "string" - }, - "value": { - "description": "Display text for the status", - "type": "string" - } - } - }, - "BotWithParams": { - "type": "object", - "required": [ - "account_id", - "bot_exited_at", - "bot_joined_at", - "bot_name", - "bot_param_id", - "created_at", - "diarization_v2", - "ended_at", - "extra", - "id", - "meeting_url", - "mp4_s3_path", - "reserved", - "transcription_custom_parameters", - "uuid", - "webhook_url" - ], - "properties": { - "account_id": { - "type": "integer", - "format": "int32" - }, - "bot_exited_at": { - "$ref": "#/components/schemas/OptionalDateTime" - }, - "bot_image": { - "type": [ - "string", - "null" - ] - }, - "bot_joined_at": { - "$ref": "#/components/schemas/OptionalDateTime" - }, - "bot_name": { - "type": "string" - }, - "bot_param_id": { - "type": "integer", - "format": "int32" - }, - "created_at": { - "$ref": "#/components/schemas/DateTime" - }, - "deduplication_key": { - "type": [ - "string", - "null" - ] - }, - "diarization_fails": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "diarization_v2": { - "type": "boolean" - }, - "ended_at": { - "$ref": "#/components/schemas/OptionalDateTime" - }, - "enter_message": { - "type": [ - "string", - "null" - ] - }, - "errors": { - "type": [ - "string", - "null" - ] - }, - "event_id": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "extra": { - "$ref": "#/components/schemas/Extra" - }, - "id": { - "type": "integer", - "format": "int32" - }, - "meeting_url": { - "type": "string" - }, - "mp4_s3_path": { - "type": "string" - }, - "noone_joined_timeout": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "recording_mode": { - "anyOf": [ - { - "$ref": "#/components/schemas/RecordingMode" - }, - { - "type": "null" - } - ] - }, - "reserved": { - "type": "boolean" - }, - "scheduled_bot_id": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "session_id": { - "type": [ - "string", - "null" - ] - }, - "silence_timeout": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "speech_to_text_api_key": { - "type": [ - "string", - "null" - ] - }, - "speech_to_text_provider": { - "anyOf": [ - { - "$ref": "#/components/schemas/SpeechToTextProvider" - }, - { - "type": "null" - } - ] - }, - "streaming_audio_frequency": { - "anyOf": [ - { - "$ref": "#/components/schemas/AudioFrequency" - }, - { - "type": "null" - } - ] - }, - "streaming_input": { - "type": [ - "string", - "null" - ] - }, - "streaming_output": { - "type": [ - "string", - "null" - ] - }, - "transcription_custom_parameters": { - "$ref": "#/components/schemas/Extra" - }, - "transcription_fails": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "transcription_payloads": true, - "user_reported_error": true, - "uuid": { - "type": "string", - "format": "uuid" - }, - "waiting_room_timeout": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "webhook_url": { - "type": "string" - }, - "zoom_access_token_url": { - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token": { - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token_url": { - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token_user_id": { - "type": [ - "string", - "null" - ] - }, - "zoom_sdk_id": { - "type": [ - "string", - "null" - ] - }, - "zoom_sdk_pwd": { - "type": [ - "string", - "null" - ] - } - } - }, - "BotWithStatus": { - "description": "Bot information with status metadata\n\nThis struct combines the bot data with status information optimized for UI display.", - "type": "object", - "required": [ - "account_id", - "bot_exited_at", - "bot_joined_at", - "bot_param_id", - "created_at", - "diarization_v2", - "duration", - "ended_at", - "id", - "meeting_url", - "mp4_s3_path", - "params", - "reserved", - "status", - "uuid" - ], - "properties": { - "account_email": { - "description": "The email of the account owner (only included for special domain users)", - "type": [ - "string", - "null" - ] - }, - "account_id": { - "description": "The account that owns this bot", - "type": "integer", - "format": "int32" - }, - "bot_exited_at": { - "description": "When the bot actually exited the meeting (precise timing)", - "$ref": "#/components/schemas/OptionalDateTime" - }, - "bot_joined_at": { - "description": "When the bot actually joined the meeting (precise timing)", - "$ref": "#/components/schemas/OptionalDateTime" - }, - "bot_param_id": { - "description": "ID of the bot parameters used", - "type": "integer", - "format": "int32" - }, - "created_at": { - "description": "When the bot was created", - "$ref": "#/components/schemas/DateTime" - }, - "diarization_fails": { - "description": "Number of diarization failures", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "diarization_v2": { - "description": "Whether diarization v2 is enabled", - "type": "boolean" - }, - "duration": { - "description": "Duration of the recording in seconds", - "type": "integer", - "format": "int64" - }, - "ended_at": { - "description": "When the bot ended recording", - "$ref": "#/components/schemas/OptionalDateTime" - }, - "errors": { - "description": "Any error messages", - "type": [ - "string", - "null" - ] - }, - "event_id": { - "description": "ID of the calendar event if scheduled", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "id": { - "description": "The bot's unique identifier", - "type": "integer", - "format": "int32" - }, - "meeting_url": { - "description": "The meeting URL this bot is recording", - "type": "string" - }, - "mp4_s3_path": { - "description": "Path to the MP4 file in S3", - "type": "string" - }, - "params": { - "description": "Bot parameters", - "$ref": "#/components/schemas/BotParam" - }, - "reserved": { - "description": "Whether this bot is reserved", - "type": "boolean" - }, - "scheduled_bot_id": { - "description": "ID of the scheduled bot if scheduled", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "session_id": { - "description": "The session ID for this bot instance", - "type": [ - "string", - "null" - ] - }, - "status": { - "description": "Frontend-friendly status information for display and sorting", - "$ref": "#/components/schemas/BotStatusResponse" - }, - "transcription_fails": { - "description": "Number of transcription failures", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "user_reported_error": { - "description": "User reported error information" - }, - "uuid": { - "description": "Unique identifier for this bot", - "type": "string", - "format": "uuid" - } - } - }, - "Calendar": { - "type": "object", - "required": [ - "email", - "google_id", - "name", - "uuid" - ], - "properties": { - "email": { - "type": "string" - }, - "google_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "resource_id": { - "type": [ - "string", - "null" - ] - }, - "uuid": { - "type": "string", - "format": "uuid" - } - } - }, - "CalendarListEntry": { - "type": "object", - "required": [ - "email", - "id", - "is_primary" - ], - "properties": { - "email": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_primary": { - "type": "boolean" - } - } - }, - "CalendarUuidParam": { - "description": "Calendar UUID path parameter for API endpoints", - "type": "object", - "required": [ - "calendar_uuid" - ], - "properties": { - "calendar_uuid": { - "description": "The calendar UUID", - "type": "string" - } - } - }, - "CreateCalendarParams": { - "type": "object", - "required": [ - "oauth_client_id", - "oauth_client_secret", - "oauth_refresh_token", - "platform" - ], - "properties": { - "oauth_client_id": { - "type": "string" - }, - "oauth_client_secret": { - "type": "string" - }, - "oauth_refresh_token": { - "type": "string" - }, - "platform": { - "$ref": "#/components/schemas/Provider" - }, - "raw_calendar_id": { - "type": [ - "string", - "null" - ] - } - } - }, - "CreateCalendarResponse": { - "type": "object", - "required": [ - "calendar" - ], - "properties": { - "calendar": { - "$ref": "#/components/schemas/Calendar" - } - } - }, - "CreateConnectionRequest": { - "type": "object", - "required": [ - "authorization_code", - "redirect_uri", - "zoom_client_id", - "zoom_client_secret" - ], - "properties": { - "authorization_code": { - "description": "The OAuth authorization code received from Zoom after user consent. This is a single-use code that expires in approximately 10 minutes.", - "type": "string" - }, - "redirect_uri": { - "description": "The redirect URI that was used in the OAuth authorization request. Must match exactly what was configured in your Zoom OAuth app.", - "type": "string" - }, - "zoom_client_id": { - "description": "Your Zoom OAuth app's Client ID, found in the Zoom App Marketplace under your app's credentials.", - "type": "string" - }, - "zoom_client_secret": { - "description": "Your Zoom OAuth app's Client Secret, found in the Zoom App Marketplace under your app's credentials.", - "type": "string" - } - } - }, - "DailyTokenConsumption": { - "type": "object", - "required": [ - "consumption_by_service", - "date" - ], - "properties": { - "consumption_by_service": { - "$ref": "#/components/schemas/TokenConsumptionByService" - }, - "date": { - "type": "string" - } - } - }, - "DateTime": { + "additionalProperties": false, + "type": "object", + "title": "LeaveBotRequest", + "description": "Request model for making a bot leave a meeting" + }, + "MCPConfig": { + "properties": { + "servers": { + "items": { + "$ref": "#/components/schemas/MCPServerConfig" + }, + "type": "array", + "maxItems": 10, + "title": "Servers", + "description": "MCP servers to document and optionally connect for tool calls" + }, + "instructions": { + "anyOf": [ + { "type": "string", - "format": "date-time" - }, - "DeleteResponse": { - "type": "object", - "required": [ - "ok", - "status" - ], - "properties": { - "ok": { - "description": "Whether the request was processed successfully", - "type": "boolean" - }, - "status": { - "description": "The detailed status of the deletion operation", - "$ref": "#/components/schemas/DeleteStatus" - } - } - }, - "DeleteStatus": { - "oneOf": [ - { - "description": "All data was successfully deleted", - "type": "string", - "enum": [ - "deleted" - ] - }, - { - "description": "Some data was deleted, but other parts couldn't be removed", - "type": "string", - "enum": [ - "partiallyDeleted" - ] - }, - { - "description": "No data needed to be deleted as it was already removed", - "type": "string", - "enum": [ - "alreadyDeleted" - ] - }, - { - "description": "No data was found for the specified bot", - "type": "string", - "enum": [ - "noDataFound" - ] - } - ] - }, - "EfsManifestEntry": { - "type": "object", - "required": [ - "path", - "size" - ], - "properties": { - "path": { - "type": "string" - }, - "size": { - "type": "integer", - "format": "uint64", - "minimum": 0 - } - } - }, - "EndMeetingQuery": { - "type": "object", - "required": [ - "bot_uuid" - ], - "properties": { - "bot_uuid": { - "type": "string" - } - } - }, - "EndMeetingTrampolineQuery": { - "type": "object", - "required": [ - "bot_uuid" - ], - "properties": { - "bot_uuid": { - "type": "string" - } - } - }, - "EndMeetingTrampolineRequest": { - "type": "object", - "required": [ - "bot_exited_at", - "bot_joined_at", - "diarization_v2" - ], - "properties": { - "bot_exited_at": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "bot_joined_at": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "diarization_fail_count": { - "type": [ - "integer", - "null" - ], - "format": "uint", - "minimum": 0 - }, - "diarization_v2": { - "type": "boolean" - }, - "ended_at": { - "description": "Optional Unix timestamp (seconds, no ms) for when the meeting actually ended. When provided (e.g. for manual replay), used instead of server time for bot.ended_at.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0 - }, - "files_generated": { - "anyOf": [ - { - "$ref": "#/components/schemas/FilesGenerated" - }, - { - "type": "null" - } - ] - }, - "transcription_fail_count": { - "type": [ - "integer", - "null" - ], - "format": "uint", - "minimum": 0 - } - } - }, - "Event": { - "type": "object", - "required": [ - "attendees", - "calendar_uuid", - "deleted", - "end_time", - "google_id", - "is_organizer", - "is_recurring", - "last_updated_at", - "meeting_url", - "name", - "raw", - "start_time", - "uuid" - ], - "properties": { - "attendees": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Attendee" - } - }, - "bot_param": { - "description": "Associated bot parameters if a bot is scheduled for this event", - "anyOf": [ - { - "$ref": "#/components/schemas/BotParam" - }, - { - "type": "null" - } - ] - }, - "calendar_uuid": { - "type": "string", - "format": "uuid" - }, - "deleted": { - "description": "Indicates whether this event has been deleted", - "type": "boolean" - }, - "end_time": { - "description": "The end time of the event in UTC timezone", - "type": "string", - "format": "date-time" - }, - "google_id": { - "description": "The unique identifier of the event from the calendar provider (Google, Microsoft)", - "type": "string" - }, - "is_organizer": { - "description": "Indicates whether the current user is the organizer of this event", - "type": "boolean" - }, - "is_recurring": { - "description": "Indicates whether this event is part of a recurring series", - "type": "boolean" - }, - "last_updated_at": { - "description": "The timestamp when this event was last updated", - "type": "string", - "format": "date-time" - }, - "meeting_url": { - "description": "The URL that can be used to join the meeting (if available)", - "type": "string" - }, - "name": { - "description": "The title/name of the calendar event", - "type": "string" - }, - "raw": { - "description": "The raw calendar data from the provider in JSON format", - "$ref": "#/components/schemas/Extra" - }, - "recurring_event_id": { - "description": "For recurring events, the ID of the parent recurring event series (if applicable)", - "type": [ - "string", - "null" - ] - }, - "start_time": { - "description": "The start time of the event in UTC timezone", - "type": "string", - "format": "date-time" - }, - "uuid": { - "type": "string", - "format": "uuid" - } - } - }, - "Extra": { - "description": "Custom data object", - "additionalProperties": true - }, - "FailedRecordRequest": { - "type": "object", - "required": [ - "meeting_url", - "message" - ], - "properties": { - "ended_at": { - "description": "Optional Unix timestamp (seconds, no ms) for when the bot actually ended. When provided (e.g. for manual replay), used instead of server time to avoid incorrect billing.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0 - }, - "error_code": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "meeting_url": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "FilesGenerated": { - "type": "object", - "required": [ - "file_extension", - "file_type", - "filename" - ], - "properties": { - "audio_file": { - "anyOf": [ - { - "$ref": "#/components/schemas/AudioFile" - }, - { - "type": "null" - } - ] - }, - "file_extension": { - "type": "string" - }, - "file_type": { - "type": "string" - }, - "filename": { - "type": "string" - } - } - }, - "GetAllBotsQuery": { - "description": "Query parameters for getting all bots", - "type": "object", - "required": [ - "limit", - "offset" - ], - "properties": { - "account_id": { - "description": "Filter by account ID (comma-separated for multiple values)\n\nExample: \"1,2,3\" will match bots from accounts 1, 2, or 3", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "bot_uuid": { - "description": "Filter by bot UUID (comma-separated for multiple values)\n\nExample: \"123e4567-e89b-12d3-a456-426614174000,987fcdeb-51d3-a456-426614174000\"", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "creator_email_contains": { - "description": "Filter by creator email containing any of these texts (comma-separated)\n\nExample: \"john,doe\" will match emails containing either \"john\" or \"doe\"", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "diarization_v2": { - "description": "Filter by diarization v2 status (comma-separated for multiple values)\n\nExample: \"true,false\" will match both diarization v2 and non-diarization v2 bots", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "end_date": { - "description": "Filter by end date", - "default": null, - "type": [ - "string", - "null" - ], - "format": "partial-date-time" - }, - "extra_contains": { - "description": "Filter by extra JSON containing any of these texts (comma-separated)\n\nExample: \"customer_id,project\" will match extra JSON containing either \"customer_id\" or \"project\"", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "limit": { - "description": "Limit for pagination", - "type": "integer", - "format": "int64" - }, - "meeting_url": { - "description": "Filter by exact meeting URL (comma-separated for multiple values)\n\nExample: \"https://meet.google.com/abc-def,https://zoom.us/j/123456\"", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "meeting_url_contains": { - "description": "Filter by meeting URL containing any of these texts (comma-separated)\n\nExample: \"meet.google.com,zoom.us\" will match URLs containing either \"meet.google.com\" or \"zoom.us\"", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "offset": { - "description": "Offset for pagination", - "type": "integer", - "format": "int64" - }, - "reserved": { - "description": "Filter by reserved status (comma-separated for multiple values)\n\nExample: \"true,false\" will match both reserved and non-reserved bots", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "start_date": { - "description": "Filter by start date", - "default": null, - "type": [ - "string", - "null" - ], - "format": "partial-date-time" - }, - "status": { - "description": "Filter by user-reported status (comma-separated for multiple values)\n\nExample: \"open,in_progress,closed\" will match bots with any of these user-reported statuses", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "status_category": { - "description": "Filter by status category (comma-separated for multiple values)\n\nExample: \"system_error,auth_error,connection_error\" will match bots with any of these categories\n\nCommon categories are: - system_error: Internal system issues - auth_error: Authentication and authorization issues - connection_error: Network and meeting connection issues - permission_error: Access and permission issues - input_error: Invalid input parameters - webhook_error: Webhook delivery issues - duplicate_error: Duplicate meeting or bot issues - unknown_error: Unclassified errors", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "status_priority": { - "description": "Filter by status priority (comma-separated for multiple values)\n\nExample: \"critical,high,medium,low\" will match bots with any of these priorities\n\nPriorities are: - critical: System errors requiring immediate attention - high: Serious issues that prevent meeting functionality - medium: Issues that affect functionality but not critically - low: Minor issues that don't greatly impact functionality - none: For success states", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "status_type": { - "description": "Filter by status type (comma-separated for multiple values)\n\nExample: \"success,error,warning,pending\" will match bots with any of these status types\n\nStatus types are: - success: Bot completed successfully or is in progress - error: Bot encountered a system error - warning: Bot has a non-critical issue - pending: Bot is waiting to start", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "user_email": { - "description": "Filter by user email (comma-separated for multiple values)\n\nExample: \"user1@example.com,user2@example.com\" will match bots from accounts with these emails", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "user_reported_error_contains": { - "description": "Filter by user reported error containing any of these texts (comma-separated)\n\nExample: \"failed,error\" will match errors containing either \"failed\" or \"error\"", - "default": null, - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } - }, - "user_reported_error_json": { - "description": "Filter by user reported error JSON (comma-separated for multiple conditions)\n\nExample: '{\"status\":\"open\"},{\"priority\":\"high\"}' will match bots with either status open or priority high", - "default": null, - "type": [ - "array", - "null" - ], - "items": true - } - } - }, - "GetMeetingDataQuery": { - "type": "object", - "required": [ - "bot_id" - ], - "properties": { - "bot_id": { - "type": "string" - }, - "include_transcripts": { - "description": "Whether to include transcription data in the response. Defaults to true if not specified.", - "default": true, - "type": "boolean" - } - } - }, - "GetStartedAccount": { - "type": "object", - "required": [ - "email" - ], - "properties": { - "email": { - "type": "string" - }, - "firstname": { - "type": [ - "string", - "null" - ] - }, - "google_token_id": { - "type": [ - "string", - "null" - ] - }, - "lastname": { - "type": [ - "string", - "null" - ] - }, - "microsoft_token_id": { - "type": [ - "string", - "null" - ] - } - } - }, - "GetWebhookUrlResponse": { - "type": "object", - "properties": { - "webhook_url": { - "type": [ - "string", - "null" - ] - } - } - }, - "GetstartedQuery": { - "type": "object", - "properties": { - "redirect_url": { - "type": [ - "string", - "null" - ] - } - } - }, - "JoinRequest": { - "type": "object", - "required": [ - "bot_name", - "meeting_url" - ], - "properties": { - "automatic_leave": { - "description": "Configuration for automatic meeting exit behavior. The bot uses waiting_room_timeout to wait in the waiting room, then noone_joined_timeout to wait for participants when first joining the meeting, and finally switches to silence_timeout monitoring once participants are detected. Applies to Google Meet and Microsoft Teams only.", - "anyOf": [ - { - "$ref": "#/components/schemas/AutomaticLeaveRequest" - }, - { - "type": "null" - } - ] - }, - "bot_image": { - "description": "The image to use for the bot, must be a URL. Recommended ratio is 16:9.", - "type": [ - "string", - "null" - ], - "format": "uri" - }, - "bot_name": { - "type": "string" - }, - "deduplication_key": { - "description": "We prevent multiple bots with same API key joining a meeting within 5 mins, unless overridden by deduplication_key.", - "type": [ - "string", - "null" - ] - }, - "entry_message": { - "description": "There are no entry messages on Microsoft Teams as guests outside of an organization do not have access to the chat.", - "type": [ - "string", - "null" - ] - }, - "extra": { - "description": "A JSON object that allows you to add custom data to a bot for your convenience, e.g. your end user's ID.", - "default": null, - "$ref": "#/components/schemas/Extra" - }, - "meeting_url": { - "type": "string" - }, - "recording_mode": { - "description": "The recording mode for the bot, defaults to 'speaker_view'. Supported values are 'speaker_view' and 'audio_only'. 'gallery_view' is currently under development.", - "anyOf": [ - { - "$ref": "#/components/schemas/RecordingMode" - }, - { - "type": "null" - } - ] - }, - "reserved": { - "description": "Deprecated, do not use.", - "default": false, - "type": "boolean" - }, - "speech_to_text": { - "description": "The default speech to text provider is Gladia.", - "anyOf": [ - { - "$ref": "#/components/schemas/SpeechToText" - }, - { - "type": "null" - } - ] - }, - "start_time": { - "description": "Reserved has been deprecated in favour of start_time. Unix timestamp (in seconds) for when the bot should join the meeting. The bot joins eaxctly at the start time.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0 - }, - "streaming": { - "description": "WebSocket streams for 16 kHz audio. Input stream receives audio sent to the bot. Output stream receives audio from the bot.", - "anyOf": [ - { - "$ref": "#/components/schemas/StreamingApiParameter" - }, - { - "type": "null" - } - ] - }, - "transcription_custom_parameters": { - "description": "For your own transcription parameters", - "default": null - }, - "webhook_url": { - "description": "A webhook URL to send events to, overrides the webhook URL set in your account settings.", - "type": [ - "string", - "null" - ] - }, - "zoom_access_token_url": { - "description": "URL that returns a Zoom ZAK token (short-lived access token) for joining authenticated meetings.", - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token": { - "description": "A raw Zoom On Behalf Of (OBF) token for joining external Zoom meetings. Required for meetings that enforce authenticated join after March 2, 2026.", - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token_url": { - "description": "URL that returns a Zoom OBF token. The bot will fetch the token from this URL at join time.", - "type": [ - "string", - "null" - ] - }, - "zoom_obf_token_user_id": { - "description": "The Zoom user ID associated with a stored OAuth connection. When set, the system will automatically fetch an OBF token using the managed OAuth credentials.", - "type": [ - "string", - "null" - ] - }, - "zoom_sdk_id": { - "description": "For the Own Zoom Credentials feature, we need your zoom sdk id.", - "type": [ - "string", - "null" - ] - }, - "zoom_sdk_pwd": { - "description": "For the Own Zoom Credentials feature, we need your zoom sdk pwd.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "JoinRequestScheduled": { - "type": "object", - "required": [ - "bot_param_id", - "meeting_url", - "schedule_origin" - ], - "properties": { - "bot_param_id": { - "type": "integer", - "format": "int32" - }, - "meeting_url": { - "type": "string" - }, - "schedule_origin": { - "$ref": "#/components/schemas/ScheduleOrigin" - } + "maxLength": 4000 + }, + { + "type": "null" + } + ], + "title": "Instructions", + "description": "Global MCP usage instructions for the bot" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MCPConfig", + "description": "MCP server metadata and optional live connection details." + }, + "MCPServerConfig": { + "properties": { + "name": { + "type": "string", + "maxLength": 120, + "minLength": 1, + "title": "Name" + }, + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Whether this server may be used. Disabled servers are documented but not connected.", + "default": true + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url", + "description": "Remote MCP server URL. Required for http, streamable_http, and sse transports." + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" }, - "additionalProperties": false - }, - "JoinResponse": { - "type": "object", - "required": [ - "bot_id" - ], - "properties": { - "bot_id": { - "type": "string", - "format": "uuid" - } - } - }, - "JoinResponse2": { - "type": "object", - "required": [ - "bot_id" - ], - "properties": { - "bot_id": { - "type": "string", - "format": "uuid" - } - } - }, - "LeaveResponse": { - "type": "object", - "required": [ - "ok" - ], - "properties": { - "ok": { - "type": "boolean" - } - } - }, - "ListEventResponse": { - "type": "object", - "required": [ - "data" - ], - "properties": { - "data": { - "description": "Vector of calendar events matching the list criteria", - "type": "array", - "items": { - "$ref": "#/components/schemas/Event" - } - }, - "next": { - "description": "Optional url for fetching the next page of results if there are more results to fetch. The limit of events returned is 100. When None, there are no more results to fetch.", - "type": [ - "string", - "null" - ] - } - } - }, - "ListRawCalendarsParams": { - "type": "object", - "required": [ - "oauth_client_id", - "oauth_client_secret", - "oauth_refresh_token", - "platform" - ], - "properties": { - "oauth_client_id": { - "type": "string" - }, - "oauth_client_secret": { - "type": "string" - }, - "oauth_refresh_token": { - "type": "string" - }, - "platform": { - "$ref": "#/components/schemas/Provider" - } - } - }, - "ListRawCalendarsResponse": { - "type": "object", - "required": [ - "calendars" - ], - "properties": { - "calendars": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CalendarListEntry" - } - } - } - }, - "ListRecentBotsQuery": { - "description": "Query parameters for listing recent bots", - "type": "object", - "properties": { - "bot_name": { - "description": "Filter bots by name containing this string.\n\nPerforms a case-insensitive partial match on the bot's name. Useful for finding bots with specific naming conventions or to locate a particular bot when you don't have its ID.\n\nExample: \"Sales\" would match \"Sales Meeting\", \"Quarterly Sales\", etc.", - "type": [ - "string", - "null" - ] - }, - "created_after": { - "description": "Filter bots created after this date (ISO format).\n\nLimits results to bots created at or after the specified timestamp. Used for time-based filtering to find recent additions.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-01T00:00:00\"", - "type": [ - "string", - "null" - ] - }, - "created_before": { - "description": "Filter bots created before this date (ISO format).\n\nLimits results to bots created at or before the specified timestamp. Used for time-based filtering to exclude recent additions.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-31T23:59:59\"", - "type": [ - "string", - "null" - ] - }, - "cursor": { - "description": "Cursor for pagination, obtained from previous response.\n\nUsed for retrieving the next set of results after a previous call. The cursor value is returned in the `nextCursor` field of responses that have additional results available.\n\nFormat: Base64-encoded string containing pagination metadata", - "type": [ - "string", - "null" - ] - }, - "ended_after": { - "description": "Filter bots ended after this date (ISO format).\n\nLimits results to bots that ended at or after the specified timestamp. Useful for finding completed meetings within a specific time period.\n\nFormat: ISO-8601 date-time string (YYYY-MM-DDThh:mm:ss) Example: \"2023-05-01T00:00:00\"", - "type": [ - "string", - "null" - ] - }, - "filter_by_extra": { - "description": "Filter bots by matching values in the extra JSON payload.\n\nThis parameter performs in-memory filtering on the `extra` JSON field, similar to a SQL WHERE clause. It reduces the result set to only include bots that match all specified conditions.\n\nFormat specifications: - Single condition: \"field:value\" - Multiple conditions: \"field1:value1,field2:value2\"\n\nExamples: - \"customer_id:12345\" - Only bots with this customer ID - \"status:active,project:sales\" - Only active bots from sales projects\n\nNotes: - All conditions must match for a bot to be included - Values are matched exactly (case-sensitive) - Bots without the specified field are excluded", - "type": [ - "string", - "null" - ] - }, - "limit": { - "description": "Maximum number of bots to return in a single request.\n\nLimits the number of results returned in a single API call. This parameter helps control response size and page length.\n\nDefault: 10 Minimum: 1 Maximum: 50", - "default": 10, - "type": "integer", - "format": "int32" - }, - "meeting_url": { - "description": "Filter bots by meeting URL containing this string.\n\nPerforms a case-insensitive partial match on the bot's meeting URL. Use this to find bots associated with specific meeting platforms or particular meeting IDs.\n\nExample: \"zoom.us\" would match all Zoom meetings", - "type": [ - "string", - "null" - ] - }, - "sort_by_extra": { - "description": "Sort the results by a field in the extra JSON payload.\n\nThis parameter performs in-memory sorting on the `extra` JSON field, similar to a SQL ORDER BY clause. It changes the order of results but not which results are included.\n\nFormat specifications: - Default (ascending): \"field\" - Explicit direction: \"field:asc\" or \"field:desc\"\n\nExamples: - \"customer_id\" - Sort by customer_id (ascending) - \"priority:desc\" - Sort by priority (descending)\n\nNotes: - Applied after all filtering - String comparison is used for sorting - Bots with the field come before bots without it - Can be combined with filter_by_extra", - "type": [ - "string", - "null" - ] - }, - "speaker_name": { - "description": "NOTE: this is a preview feature and not yet available\n\nFilter bots by speaker name containing this string.\n\nPerforms a case-insensitive partial match on the speakers in the meeting. Useful for finding meetings that included a specific person.\n\nExample: \"John\" would match meetings with speakers like \"John Smith\" or \"John Doe\"", - "type": [ - "string", - "null" - ] - } - } - }, - "ListRecentBotsResponse": { - "description": "Response for listing recent bots", - "type": "object", - "required": [ - "bots" - ], - "properties": { - "bots": { - "description": "List of recent bots with their metadata\n\nThis field is serialized as both \"bots\" and \"recent_bots\" for backwards compatibility. New clients should use the \"bots\" field name.", - "type": "array", - "items": { - "$ref": "#/components/schemas/RecentBotEntry" - } - }, - "last_updated": { - "description": "Timestamp of when this data was generated (in ISO-8601 format)\n\nThis field is maintained for backwards compatibility. It is automatically set to the current time when the response is created.", - "default": "2026-02-17T23:00:13.081843563+00:00", - "type": "string", - "format": "date-time" - }, - "next_cursor": { - "description": "Optional cursor for pagination", - "type": [ - "string", - "null" - ] - } - } - }, - "LoginAccount": { - "type": "object", - "required": [ - "password", - "pseudo" - ], - "properties": { - "app_signin_token": { - "type": [ - "string", - "null" - ] - }, - "google_chrome_token_id": { - "type": [ - "string", - "null" - ] - }, - "google_token_id": { - "type": [ - "string", - "null" - ] - }, - "microsoft_token_id": { - "type": [ - "string", - "null" - ] - }, - "password": { - "type": "string" - }, - "pseudo": { - "type": "string" - } - } - }, - "LoginQuery": { - "type": "object", - "properties": { - "redirect_url": { - "type": [ - "string", - "null" - ] - } - } - }, - "Metadata": { - "type": "object", - "required": [ - "audio", - "bot_data", - "duration", - "meeting_participants_file", - "mp4", - "speaker_diarization_file", - "speaker_diarization_file_network" - ], - "properties": { - "audio": { - "description": "URL to access the recording WAV audio file. Will be an empty string if the file doesn't exist in S3.", - "type": "string" - }, - "bot_data": { - "$ref": "#/components/schemas/BotData" - }, - "duration": { - "description": "Duration of the recording in seconds", - "type": "integer", - "format": "int64" - }, - "meeting_participants_file": { - "description": "URL to access the meeting participants log file. Contains information about meeting participants. Will be an empty string if the file doesn't exist in S3.", - "type": "string" - }, - "mp4": { - "description": "URL to access the recording MP4 file. Will be an empty string if the file doesn't exist in S3.", - "type": "string" - }, - "speaker_diarization_file": { - "description": "URL to access the speaker diarization metadata file. Contains real-time speaker activity data with timestamps indicating when each speaker is talking. The file contains JSON arrays with speaker information including name, ID, timestamp, and speaking status. Will be an empty string if the file doesn't exist in S3.", - "type": "string" - }, - "speaker_diarization_file_network": { - "description": "URL to access the network speaker detection log file. Contains speaker observation observed through network (not UI changes). Will be an empty string if the file doesn't exist in S3.", - "type": "string" - } - } - }, - "ObfTokenQuery": { - "type": "object", - "required": [ - "bot_uuid" - ], - "properties": { - "bot_uuid": { - "type": "string" - }, - "zoom_user_id": { - "type": [ - "string", - "null" - ] - } - } - }, - "OptionalDateTime": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "PostWebhookUrlRequest": { - "type": "object", - "required": [ - "webhook_url" - ], - "properties": { - "webhook_url": { - "type": "string" - } - } - }, - "Provider": { - "description": "Fields with value `\"simple\"` parse as `Kind::Simple`. Fields with value `\"fancy\"` parse as `Kind::SoFancy`.", + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Headers", + "description": "Optional HTTP headers for remote MCP servers. Use only when a server requires them." + }, + "transport": { + "anyOf": [ + { "type": "string", "enum": [ - "Google", - "Microsoft" - ] - }, - "PublicBotAnalytics": { - "description": "Watered-down bot info for public analytics", - "type": "object", - "required": [ - "created_at", - "duration", - "meeting_platform", - "status" - ], - "properties": { - "created_at": { - "$ref": "#/components/schemas/DateTime" - }, - "duration": { - "type": "integer", - "format": "int64" - }, - "meeting_platform": { - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/BotStatusResponse" - }, - "user_reported_error_message": { - "type": [ - "string", - "null" - ] - }, - "user_reported_error_status": { - "type": [ - "string", - "null" - ] - } - } - }, - "QueryListEvent": { - "type": "object", - "required": [ - "calendar_id" - ], - "properties": { - "attendee_email": { - "description": "If provided, filters events to include only those with this attendee's email address Example: \"jane.smith@example.com\"", - "type": [ - "string", - "null" - ] - }, - "calendar_id": { - "description": "Calendar ID to filter events by This is required to specify which calendar's events to retrieve", - "type": "string" - }, - "cursor": { - "description": "Optional cursor for pagination This value is included in the `next` field of the previous response", - "type": [ - "string", - "null" - ] - }, - "organizer_email": { - "description": "If provided, filters events to include only those with this organizer's email address Example: \"john.doe@example.com\"", - "type": [ - "string", - "null" - ] - }, - "start_date_gte": { - "description": "If provided, filters events to include only those with a start date greater than or equal to this timestamp Format: ISO-8601 string, e.g., \"2023-01-01T00:00:00Z\"", - "type": [ - "string", - "null" - ] - }, - "start_date_lte": { - "description": "If provided, filters events to include only those with a start date less than or equal to this timestamp Format: ISO-8601 string, e.g., \"2023-12-31T23:59:59Z\"", - "type": [ - "string", - "null" - ] - }, - "status": { - "description": "Filter events by meeting status Valid values: \"upcoming\" (default) returns events after current time, \"past\" returns previous events, \"all\" returns both", - "type": [ - "string", - "null" - ] - }, - "updated_at_gte": { - "description": "If provided, fetches only events updated at or after this timestamp Format: ISO-8601 string, e.g., \"2023-01-01T00:00:00Z\"", - "type": [ - "string", - "null" - ] - } - } - }, - "QueryPatchRecordEvent": { - "type": "object", - "properties": { - "all_occurrences": { - "description": "schedule a bot to all occurences of a recurring event", - "type": [ - "boolean", - "null" - ] - } - } - }, - "QueryScheduleRecordEvent": { - "type": "object", - "properties": { - "all_occurrences": { - "description": "schedule a bot to all occurences of a recurring event", - "type": [ - "boolean", - "null" - ] - } - } - }, - "QueryUnScheduleRecordEvent": { - "type": "object", - "properties": { - "all_occurrences": { - "description": "unschedule a bot from all occurences of a recurring event", - "type": [ - "boolean", - "null" - ] - } - } - }, - "ReceivedMessageQuery": { - "type": "object", - "required": [ - "session_id" - ], - "properties": { - "session_id": { - "type": "string" - } - } - }, - "RecentBotEntry": { - "description": "Entry for a recent bot in the list response", - "type": "object", - "required": [ - "bot_name", - "created_at", - "extra", - "id", - "meeting_url", - "speakers", - "uuid" - ], - "properties": { - "access_count": { - "description": "Number of times this bot data has been accessed (if tracked)", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "bot_name": { - "description": "Name of the bot", - "type": "string" - }, - "created_at": { - "description": "Creation timestamp of the bot in ISO-8601 format", - "type": "string" - }, - "duration": { - "description": "Duration of the bot session in seconds (if completed)", - "type": [ - "integer", - "null" - ], - "format": "int64" - }, - "ended_at": { - "description": "End time of the bot session (if completed) in ISO-8601 format", - "type": [ - "string", - "null" - ] - }, - "extra": { - "description": "Extra custom data provided during bot creation", - "$ref": "#/components/schemas/Extra" - }, - "id": { - "description": "Unique identifier of the bot (legacy field)\n\nThis field is maintained for backwards compatibility. It is serialized as a UUID string to match the old API format. New clients should use the uuid field instead.", - "type": "integer", - "format": "int32" - }, - "last_accessed_at": { - "description": "Last time this bot data was accessed (if available)", - "type": [ - "string", - "null" - ] - }, - "meeting_url": { - "description": "URL of the meeting the bot joined", - "type": "string" - }, - "session_id": { - "description": "Session ID if the bot is active", - "type": [ - "string", - "null" - ] - }, - "speakers": { - "description": "List of unique speaker names from the bot's transcripts", - "type": "array", - "items": { - "type": "string" - } - }, - "uuid": { - "description": "Unique identifier of the bot (new field)\n\nThis is the preferred field to use for bot identification. The id field is maintained for backwards compatibility.", - "type": "string", - "format": "uuid" - } - } - }, - "RecognizerTranscript": { - "type": "object", - "required": [ - "speaker", - "start_time" - ], - "properties": { - "end_time": { - "type": [ - "number", - "null" - ], - "format": "double" - }, - "lang": { - "type": [ - "string", - "null" - ] - }, - "speaker": { - "type": "string" - }, - "start_time": { - "type": "number", - "format": "double" - }, - "user_id": { - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "RecognizerWord": { - "type": "object", - "required": [ - "end_time", - "start_time", - "text" - ], - "properties": { - "end_time": { - "type": "number", - "format": "double" - }, - "start_time": { - "type": "number", - "format": "double" - }, - "text": { - "type": "string" - }, - "user_id": { - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "RecordingMode": { - "description": "Recording mode for the bot", - "oneOf": [ - { - "description": "Records the active speaker view", - "type": "string", - "enum": [ - "speaker_view" - ] - }, - { - "description": "Records the gallery view showing multiple participants", - "type": "string", - "enum": [ - "gallery_view" - ] - }, - { - "description": "Records only the audio from the meeting", - "type": "string", - "enum": [ - "audio_only" - ] - } + "http", + "streamable_http", + "sse" ] - }, - "ResyncAllCalendarsQuery": { - "description": "Resync all calendars for the user\n\nThis will fetch all events from the calendar again, regardless of whether they have been synced before. It is useful when the calendar data becomes out of sync.", - "type": "object", - "properties": { - "days": { - "description": "Number of days to sync forward (default: 30 for rolling window)", - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "ResyncAllCalendarsResponse": { - "type": "object", - "required": [ - "errors", - "synced_calendars" - ], - "properties": { - "errors": { - "description": "List of calendar UUIDs that failed to resync, with detailed error messages explaining the failure reason", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "string" - } - ], - "maxItems": 2, - "minItems": 2 - } - }, - "synced_calendars": { - "description": "List of calendar UUIDs that were successfully resynced with their calendar provider (Google, Microsoft)", - "type": "array", - "items": { - "type": "string", - "format": "uuid" - } - } - } - }, - "ResyncAllQuery": { - "description": "Resync all calendars for the user\n\nThis will fetch all events from the calendar again, regardless of whether they have been fetched before. It is useful when the calendar data becomes out of sync.", - "type": "object", - "properties": { - "days": { - "description": "Number of days to sync forward (default: 30 for rolling window)", - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "ResyncAllResponse": { - "type": "object", - "required": [ - "errors", - "synced_calendars" - ], - "properties": { - "errors": { - "description": "List of calendar UUIDs that failed to resync, with error messages", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "string" - } - ], - "maxItems": 2, - "minItems": 2 - } - }, - "synced_calendars": { - "description": "List of calendar UUIDs that were successfully resynced", - "type": "array", - "items": { - "type": "string", - "format": "uuid" - } - } - } - }, - "RetranscribeBody": { - "type": "object", - "required": [ - "bot_uuid" - ], - "properties": { - "bot_uuid": { - "type": "string" - }, - "speech_to_text": { - "anyOf": [ - { - "$ref": "#/components/schemas/SpeechToText" - }, - { - "type": "null" - } - ] - }, - "webhook_url": { - "type": [ - "string", - "null" - ] - } - } - }, - "RetryWebhookQuery": { - "type": "object", - "required": [ - "bot_uuid" - ], - "properties": { - "bot_uuid": { - "type": "string" - }, - "webhook_url": { - "type": [ - "string", - "null" - ] - } - } - }, - "ScheduleOrigin": { - "oneOf": [ - { - "type": "object", - "required": [ - "Event" - ], - "properties": { - "Event": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer", - "format": "int32" - } - } - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "ScheduledBot" - ], - "properties": { - "ScheduledBot": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer", - "format": "int32" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "ScreenshotWrapper": { - "description": "Schema-compatible wrapper for the Screenshot struct", - "type": "object", - "required": [ - "date", - "url" - ], - "properties": { - "date": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "ScreenshotsList": { - "description": "Wrapper struct for Screenshots list that implements JsonSchema", + }, + { + "type": "null" + } + ], + "title": "Transport", + "description": "Remote MCP transport. Omit for metadata-only servers that cannot execute tools." + }, + "tools": { + "anyOf": [ + { + "items": { + "type": "string" + }, "type": "array", + "maxItems": 50 + }, + { + "type": "null" + } + ], + "title": "Tools", + "description": "Known tool names exposed by this MCP server" + }, + "tool_allowlist": { + "anyOf": [ + { "items": { - "$ref": "#/components/schemas/ScreenshotWrapper" - } - }, - "SpeechToText": { - "anyOf": [ - { - "$ref": "#/components/schemas/SpeechToTextApiParameter" - }, - { - "$ref": "#/components/schemas/SpeechToTextProvider" - } - ] - }, - "SpeechToTextApiParameter": { - "type": "object", - "required": [ - "provider" - ], - "properties": { - "api_key": { - "type": [ - "string", - "null" - ] - }, - "provider": { - "$ref": "#/components/schemas/SpeechToTextProvider" - } - } - }, - "SpeechToTextProvider": { - "type": "string", - "enum": [ - "Gladia", - "Runpod", - "Default" - ] - }, - "StartRecordFailedQuery": { - "type": "object", - "properties": { - "bot_uuid": { - "type": [ - "string", - "null" - ] - } - } - }, - "StreamingApiParameter": { - "type": "object", - "properties": { - "audio_frequency": { - "anyOf": [ - { - "$ref": "#/components/schemas/AudioFrequency" - }, - { - "type": "null" - } - ] - }, - "input": { - "type": [ - "string", - "null" - ] - }, - "output": { - "type": [ - "string", - "null" - ] - } - } - }, - "SyncResponse": { - "type": "object", - "properties": { - "affected_event_uuids": { - "description": "UUIDs of affected events", - "type": [ - "array", - "null" - ], - "items": { - "type": "string", - "format": "uuid" - } - }, - "has_updates": { - "description": "timestamp of last updated event if some events has been updated.", - "type": [ - "string", - "null" - ], - "format": "date-time" - } - } - }, - "SystemTime": { - "type": "object", - "required": [ - "nanos_since_epoch", - "secs_since_epoch" - ], - "properties": { - "nanos_since_epoch": { - "type": "integer", - "format": "uint32", - "minimum": 0 - }, - "secs_since_epoch": { - "type": "integer", - "format": "uint64", - "minimum": 0 - } - } - }, - "TokenConsumptionByService": { - "type": "object", - "required": [ - "duration", - "recording_tokens", - "streaming_input_hour", - "streaming_input_tokens", - "streaming_output_hour", - "streaming_output_tokens", - "transcription_byok_hour", - "transcription_byok_tokens", - "transcription_hour", - "transcription_tokens" - ], - "properties": { - "duration": { - "type": "string" - }, - "recording_tokens": { - "type": "string" - }, - "streaming_input_hour": { - "type": "string" - }, - "streaming_input_tokens": { - "type": "string" - }, - "streaming_output_hour": { - "type": "string" - }, - "streaming_output_tokens": { - "type": "string" - }, - "transcription_byok_hour": { - "type": "string" - }, - "transcription_byok_tokens": { - "type": "string" - }, - "transcription_hour": { - "type": "string" - }, - "transcription_tokens": { - "type": "string" - } - } - }, - "TokenConsumptionQuery": { - "type": "object", - "required": [ - "end_date", - "start_date" - ], - "properties": { - "end_date": { - "type": "string", - "format": "partial-date-time" - }, - "start_date": { - "type": "string", - "format": "partial-date-time" - } - } - }, - "Transcript": { - "type": "object", - "required": [ - "bot_id", - "id", - "speaker", - "start_time", - "words" - ], - "properties": { - "bot_id": { - "type": "integer", - "format": "int32" - }, - "end_time": { - "type": [ - "number", - "null" - ], - "format": "double" - }, - "id": { - "type": "integer", - "format": "int32" - }, - "lang": { - "type": [ - "string", - "null" - ] - }, - "speaker": { - "type": "string" - }, - "start_time": { - "type": "number", - "format": "double" - }, - "user_id": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "words": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Word" - } - } - } - }, - "Transcript2": { - "type": "object", - "required": [ - "bot_id", - "id", - "speaker", - "start_time" - ], - "properties": { - "bot_id": { - "type": "integer", - "format": "int32" - }, - "end_time": { - "type": [ - "number", - "null" - ], - "format": "double" - }, - "id": { - "type": "integer", - "format": "int32" - }, - "lang": { - "type": [ - "string", - "null" - ] - }, - "speaker": { - "type": "string" - }, - "start_time": { - "type": "number", - "format": "double" - }, - "user_id": { - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "Transcript3": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "bot_id": { - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "end_time": { - "type": [ - "number", - "null" - ], - "format": "double" - }, - "id": { - "type": "integer", - "format": "int32" - }, - "lang": { - "type": [ - "string", - "null" - ] - }, - "speaker": { - "type": [ - "string", - "null" - ] - }, - "start_time": { - "type": [ - "number", - "null" - ], - "format": "double" - }, - "user_id": { - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "UpdateCalendarParams": { - "type": "object", - "required": [ - "oauth_client_id", - "oauth_client_secret", - "oauth_refresh_token", - "platform" - ], - "properties": { - "oauth_client_id": { - "type": "string" - }, - "oauth_client_secret": { - "type": "string" - }, - "oauth_refresh_token": { - "type": "string" - }, - "platform": { - "$ref": "#/components/schemas/Provider" - } - } - }, - "UserReportedErrorPayload": { - "type": "object", - "required": [ - "note" - ], - "properties": { - "chat_id": { - "type": [ - "string", - "null" - ] - }, - "note": { - "type": "string" - }, - "status": { - "anyOf": [ - { - "$ref": "#/components/schemas/UserReportedErrorStatus" - }, - { - "type": "null" - } - ] - } - } - }, - "UserReportedErrorStatus": { - "description": "Example usage for frontend developers\n\n```ignore // How to use in your API handler: let status = calculate_bot_status(bot.errors, bot.ended_at, bot.duration, bot.created_at); let status_response = status_to_response(status);\n\n// How to sort by priority: bots.sort_by_key(|bot| bot.status.sort_priority);\n\n// How to filter by type: let errors = bots.filter(|bot| bot.status.r#type == \"error\");\n\n// How to filter by category: let connection_errors = bots.filter(|bot| bot.status.category == \"connection_error\"); ```", + "type": "string" + }, + "type": "array", + "maxItems": 50 + }, + { + "type": "null" + } + ], + "title": "Tool Allowlist", + "description": "Optional allowlist of MCP tool names this bot may call from this server." + }, + "timeout_seconds": { + "anyOf": [ + { + "type": "number", + "maximum": 300.0, + "minimum": 0.1 + }, + { + "type": "null" + } + ], + "title": "Timeout Seconds", + "description": "Optional per-server connection/tool timeout in seconds." + }, + "instructions": { + "anyOf": [ + { "type": "string", - "enum": [ - "open", - "in_progress", - "closed" - ] - }, - "UserTokensResponse": { - "type": "object", - "required": [ - "available_tokens", - "total_tokens_purchased" - ], - "properties": { - "available_tokens": { - "type": "string" - }, - "last_purchase_date": { - "type": [ - "string", - "null" - ], - "format": "partial-date-time" - }, - "total_tokens_purchased": { - "type": "string" - } - } - }, - "UuidParam": { - "description": "UUID path parameter for API endpoints", - "type": "object", - "required": [ - "uuid" - ], - "properties": { - "uuid": { - "description": "The UUID identifier", - "type": "string" - } - } - }, - "Version": { - "type": "object", - "required": [ - "build_date", - "build_timestamp", - "location" - ], - "properties": { - "build_date": { - "type": "string" - }, - "build_timestamp": { - "type": "string" - }, - "location": { - "type": "string" - } - } - }, - "Word": { - "type": "object", - "required": [ - "bot_id", - "end_time", - "id", - "start_time", - "text" - ], - "properties": { - "bot_id": { - "type": "integer", - "format": "int32" - }, - "end_time": { - "type": "number", - "format": "double" - }, - "id": { - "type": "integer", - "format": "int32" - }, - "start_time": { - "type": "number", - "format": "double" - }, - "text": { - "type": "string" - }, - "user_id": { - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "ZoomOAuthConnectionResponse": { - "description": "A stored Zoom OAuth connection. Tokens are managed server-side and never exposed.", - "type": "object", - "required": [ - "created_at", - "state", - "updated_at", - "uuid", - "zoom_user_id" - ], - "properties": { - "created_at": { - "description": "Timestamp when the connection was first created.", - "type": "string", - "format": "partial-date-time" - }, - "scopes": { - "description": "OAuth scopes granted by the user during authorization (e.g. `user:read:user`).", - "type": [ - "string", - "null" - ] - }, - "state": { - "description": "Connection state. `connected` means tokens are valid; `disconnected` means the user needs to re-authorize.", - "type": "string" - }, - "updated_at": { - "description": "Timestamp when the connection was last updated (e.g. token refresh).", - "type": "string", - "format": "partial-date-time" - }, - "uuid": { - "description": "Unique identifier for this connection.", - "type": "string", - "format": "uuid" - }, - "zoom_account_id": { - "description": "The Zoom account ID that the connected user belongs to.", - "type": [ - "string", - "null" - ] - }, - "zoom_user_id": { - "description": "The Zoom user ID of the connected user. Use this value as `zoom_obf_token_user_id` when creating a bot to have the system automatically fetch an OBF token for this user.", - "type": "string" - } - } - } - } - }, - "security": [ - { - "ApiKeyAuth": [] + "maxLength": 4000 + }, + { + "type": "null" + } + ], + "title": "Instructions", + "description": "Operator instructions or constraints for this MCP server" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ], + "title": "MCPServerConfig", + "description": "MCP server metadata and optional live connection details." + }, + "PersonaImageRequest": { + "type": "object", + "required": [ + "name", + "description" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the persona to generate an image for" + }, + "description": { + "type": "string", + "description": "Detailed description of the persona's appearance and characteristics" + }, + "gender": { + "type": "string", + "description": "Gender of the persona (optional)", + "enum": [ + "male", + "female", + "non-binary" + ] + }, + "characteristics": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of specific characteristics or features of the persona" + } } - ], - "tags": [ - { - "name": "Webhooks", - "description": "Webhooks allow you to receive real-time notifications when specific events occur in the Meeting BaaS system. To use webhooks, set a webhook URL in your account settings or provide it when creating a bot.", - "externalDocs": { - "description": "Detailed webhook documentation", - "url": "https://docs.meetingbaas.com/webhooks" - } + }, + "PersonaImageResponse": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the persona" + }, + "image_url": { + "type": "string", + "description": "URL of the generated image" + }, + "generated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the image was generated" + } } - ] + }, + "PromptDataSource": { + "properties": { + "name": { + "type": "string", + "maxLength": 120, + "minLength": 1, + "title": "Name", + "description": "Human-readable source name shown inside the prompt context block", + "default": "external_context" + }, + "type": { + "type": "string", + "enum": [ + "text", + "url" + ], + "title": "Type", + "description": "Whether to load inline text or fetch an external HTTP(S) URL" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Text", + "description": "Inline context. Required when type is text." + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url", + "description": "HTTP(S) URL to fetch. Required when type is url." + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Headers", + "description": "Optional HTTP headers for URL sources. Avoid request-specific secrets unless needed." + }, + "token_limit": { + "anyOf": [ + { + "type": "integer", + "maximum": 50000.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Token Limit", + "description": "Optional per-source token cap before the request-level cap is applied" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "PromptDataSource", + "description": "External context to append to the bot prompt under a token budget." + }, + "TurnConfig": { + "properties": { + "confidence": { + "anyOf": [ + { + "type": "number", + "maximum": 1.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Confidence", + "description": "VAD speech confidence threshold" + }, + "start_secs": { + "anyOf": [ + { + "type": "number", + "maximum": 5.0, + "minimum": 0.05 + }, + { + "type": "null" + } + ], + "title": "Start Secs", + "description": "Sustained speech (seconds) before a turn registers" + }, + "stop_secs": { + "anyOf": [ + { + "type": "number", + "maximum": 10.0, + "minimum": 0.1 + }, + { + "type": "null" + } + ], + "title": "Stop Secs", + "description": "Silence (seconds) before the bot considers the speaker done and replies" + }, + "min_volume": { + "anyOf": [ + { + "type": "number", + "maximum": 1.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Min Volume", + "description": "Minimum input volume for VAD" + } + }, + "additionalProperties": false, + "type": "object", + "title": "TurnConfig", + "description": "Per-bot voice-activity / turn-taking tuning.\n\nAll fields optional; unset fields fall back to the VAD_* env vars, then\npipecat defaults. Human-facing bots want snappy turn-taking (low\nstop_secs); bot-vs-bot meetings want patience (higher stop_secs and\nstart_secs) so the bots stop barging in on each other." + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + }, + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "x-meeting-baas-api-key", + "description": "MeetingBaas API key for authentication" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "servers": [ + { + "url": "https://speaking.meetingbaas.com", + "description": "Production server" + }, + { + "url": "/", + "description": "Local development server" + } + ] } diff --git a/scripts/export_openapi.py b/scripts/export_openapi.py new file mode 100644 index 0000000..2d8ef0e --- /dev/null +++ b/scripts/export_openapi.py @@ -0,0 +1,26 @@ +"""Export the current FastAPI OpenAPI schema to the repo snapshot.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +OUTPUTS = (ROOT / "openapi.json", ROOT / "speaking-bot-openapi.json") + +sys.path.insert(0, str(ROOT)) + +from app.main import create_app # noqa: E402 + + +def main() -> None: + schema = create_app().openapi() + serialized = json.dumps(schema, indent=2) + "\n" + for output in OUTPUTS: + output.write_text(serialized, encoding="utf-8") + print(f"Wrote {output.relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/meetingbaas.py b/scripts/meetingbaas.py index 91e2b7f..85049ee 100644 --- a/scripts/meetingbaas.py +++ b/scripts/meetingbaas.py @@ -1,6 +1,9 @@ import asyncio import os import argparse +import inspect +import json as jsonlib +from dataclasses import dataclass from datetime import datetime import aiohttp @@ -9,7 +12,7 @@ from pipecat.adapters.schemas.function_schema import FunctionSchema from pipecat.adapters.schemas.tools_schema import ToolsSchema from pipecat.audio.vad.silero import SileroVADAnalyzer, VADParams -from pipecat.frames.frames import LLMMessagesAppendFrame, TextFrame, TTSSpeakFrame +from pipecat.frames.frames import LLMMessagesAppendFrame, TTSSpeakFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask @@ -21,6 +24,7 @@ from pipecat.serializers.protobuf import ProtobufFrameSerializer from pipecat.services.cartesia.tts import CartesiaTTSService from pipecat.services.deepgram.stt import DeepgramSTTService +from pipecat.services.llm_service import FunctionCallParams # from pipecat.services.gladia.stt import GladiaSTTService from pipecat.services.openai.llm import OpenAILLMService @@ -34,6 +38,12 @@ from config.persona_utils import PersonaManager from utils.floor import floor_blocked_by_sibling +from utils.mcp_client import ( + HttpMcpClient, + McpClientError, + StdioMcpClient, + build_mcp_tool_name, +) from utils.runtime import get_state_dir from config.prompts import DEFAULT_SYSTEM_PROMPT from meetingbaas_pipecat.utils.logger import configure_logger @@ -46,9 +56,6 @@ # Directory for ready signals from webhook READY_SIGNALS_DIR = os.path.join(get_state_dir(), "ready_signals") - -from pipecat.services.llm_service import FunctionCallParams - load_dotenv(override=True) logger = configure_logger() @@ -68,6 +75,317 @@ def log_and_flush(level, msg): h.flush() +def _coerce_float(value, default=None): + if value in (None, ""): + return default + try: + return float(value) + except (TypeError, ValueError): + return default + + +def resolve_tts_speed(persona: dict | None) -> float: + """Resolve TTS speed with request data taking precedence over env defaults.""" + persona = persona or {} + speech_config = persona.get("speech") or persona.get("tts") or {} + persona_speed = None + if isinstance(speech_config, dict): + persona_speed = ( + speech_config.get("speed") + or speech_config.get("tts_speed") + or speech_config.get("speech_speed") + ) + persona_speed = ( + persona_speed + or persona.get("tts_speed") + or persona.get("speech_speed") + or persona.get("speed") + ) + speed = _coerce_float(persona_speed) + if speed is None: + speed = _coerce_float( + os.getenv("CARTESIA_TTS_SPEED") + or os.getenv("TTS_SPEED") + or os.getenv("SPEECH_SPEED"), + 1.2, + ) + return min(max(speed, 0.6), 1.5) + + +def build_cartesia_tts_kwargs( + *, + api_key: str | None, + voice_id: str | None, + sample_rate: int, + speed: float, +) -> dict: + """Build Cartesia kwargs across Pipecat versions without breaking startup.""" + kwargs = { + "api_key": api_key, + "voice_id": voice_id, + "sample_rate": sample_rate, + } + try: + signature = inspect.signature(CartesiaTTSService.__init__) + except (TypeError, ValueError): + signature = None + + params = signature.parameters if signature else {} + if "speed" in params: + kwargs["speed"] = speed + return kwargs + + if "params" not in params or not hasattr(CartesiaTTSService, "InputParams"): + return kwargs + + try: + kwargs["params"] = CartesiaTTSService.InputParams( + generation_config={"speed": speed} + ) + except Exception as exc: + log_and_flush( + logging.WARNING, + f"[TTS] Cartesia speed config not supported by this Pipecat version: {exc}", + ) + return kwargs + + +def build_mcp_context_prompt(mcp_config) -> str: + """Summarize MCP metadata for the LLM without implying tool execution works.""" + if not mcp_config: + return "" + + if not isinstance(mcp_config, dict): + return ( + "\n\nMCP context was provided for this session, but its metadata was " + "not in a structured format. Do not claim you called MCP tools unless " + "tool execution is explicitly implemented in this runner." + ) + + lines = [ + "\n\nExternal MCP context has been provided for this session.", + "Live MCP tools may be available as callable functions when the server config is connectable. Only say you used a tool after receiving a tool result.", + ] + if mcp_config.get("instructions"): + lines.append(f"MCP instructions: {str(mcp_config['instructions'])[:500]}") + + servers = mcp_config.get("servers") or mcp_config.get("server") or [] + if isinstance(servers, dict): + servers = [ + {"name": name, **value} if isinstance(value, dict) else {"name": name} + for name, value in servers.items() + ] + if isinstance(servers, list) and servers: + lines.append("MCP servers:") + for server in servers[:8]: + if isinstance(server, dict): + name = server.get("name") or server.get("id") or server.get("url") + if name: + server_line = f"- {name}" + if server.get("transport"): + server_line += f" ({server['transport']})" + if server.get("url"): + server_line += f": {server['url']}" + lines.append(server_line) + tools = server.get("tools") or [] + if tools: + lines.append(f" Tools: {', '.join(str(tool) for tool in tools[:20])}") + if server.get("instructions"): + lines.append(f" Instructions: {str(server['instructions'])[:300]}") + elif server: + lines.append(f"- {str(server)[:180]}") + + tools = mcp_config.get("tools") or mcp_config.get("tool") or [] + if isinstance(tools, dict): + tools = [ + {"name": name, **value} if isinstance(value, dict) else {"name": name} + for name, value in tools.items() + ] + if isinstance(tools, list) and tools: + lines.append("MCP tools/data advertised:") + for tool in tools[:12]: + if isinstance(tool, dict): + name = tool.get("name") or tool.get("id") or tool.get("title") + description = tool.get("description") or tool.get("summary") + if name and description: + lines.append(f"- {name}: {str(description)[:180]}") + elif name: + lines.append(f"- {name}") + elif tool: + lines.append(f"- {str(tool)[:180]}") + + return "\n".join(lines) + + +def _schema_from_mcp_tool(tool: dict) -> tuple[dict, list[str]]: + schema = tool.get("inputSchema") or tool.get("input_schema") or {} + if not isinstance(schema, dict): + return {}, [] + properties = schema.get("properties") if isinstance(schema.get("properties"), dict) else {} + required = schema.get("required") if isinstance(schema.get("required"), list) else [] + return properties, required + + +def _tool_result_to_text(result) -> str: + if result is None: + return "MCP tool returned no result." + if isinstance(result, str): + return result + if isinstance(result, dict): + content = result.get("content") + if isinstance(content, list): + parts = [] + for item in content: + if not isinstance(item, dict): + parts.append(str(item)) + continue + if item.get("type") == "text" and item.get("text") is not None: + parts.append(str(item["text"])) + elif item.get("type") == "json" and item.get("json") is not None: + parts.append(jsonlib.dumps(item["json"], ensure_ascii=False)) + elif item.get("type") == "data" and item.get("data") is not None: + parts.append(jsonlib.dumps(item["data"], ensure_ascii=False)) + elif item.get("type") == "resource" and item.get("resource"): + parts.append(jsonlib.dumps(item["resource"], ensure_ascii=False)) + else: + parts.append(jsonlib.dumps(item, ensure_ascii=False)) + if parts: + return "\n".join(parts) + if "structuredContent" in result: + return jsonlib.dumps(result["structuredContent"], ensure_ascii=False) + return jsonlib.dumps(result, ensure_ascii=False, default=str) + + +@dataclass +class LiveMCPTool: + function_name: str + server_name: str + tool_name: str + client: object + schema: dict + + +class LiveMCPManager: + """Owns live MCP clients and maps Pipecat function names to MCP tools.""" + + def __init__(self, mcp_config: dict | None): + self._mcp_config = mcp_config or {} + self._clients = [] + self._tools: dict[str, LiveMCPTool] = {} + + async def connect(self) -> list[dict]: + discovered = [] + for server in self._mcp_config.get("servers") or []: + if not isinstance(server, dict) or server.get("enabled") is False: + continue + if not server.get("transport"): + continue + + client = self._build_client(server) + server_name = str(server.get("name") or "mcp") + try: + await client.initialize() + self._clients.append(client) + tool_allowlist = set(server.get("tool_allowlist") or []) + for tool in await client.list_tools(): + tool_name = str(tool.get("name") or "") + if not tool_name: + continue + if tool_allowlist and tool_name not in tool_allowlist: + continue + function_name = build_mcp_tool_name(server_name, tool_name) + tool_ref = LiveMCPTool( + function_name=function_name, + server_name=server_name, + tool_name=tool_name, + client=client, + schema=tool, + ) + self._tools[function_name] = tool_ref + discovered.append( + { + **tool, + "server_name": server_name, + "function_name": function_name, + } + ) + except Exception as exc: + await client.close() + if server.get("required"): + raise + log_and_flush( + logging.WARNING, + f"[MCP] Could not connect server {server_name}: {exc}", + ) + return discovered + + def _build_client(self, server: dict): + transport = str(server.get("transport") or "").lower() + timeout = float(server.get("timeout_seconds") or 12) + if transport == "stdio": + if os.getenv("MCP_ALLOW_STDIO", "").lower() not in {"1", "true", "yes"}: + raise McpClientError("stdio MCP transport is disabled") + command = [server["command"], *(server.get("args") or [])] + return StdioMcpClient(command=command, env=server.get("env")) + if transport in {"http", "streamable_http", "streamable-http", "sse"}: + return HttpMcpClient( + url=server["url"], + headers=server.get("headers"), + timeout_seconds=timeout, + ) + raise McpClientError(f"Unsupported MCP transport: {transport}") + + async def call_tool_by_function_name(self, function_name: str, arguments: dict): + tool_ref = self._tools.get(function_name) + if not tool_ref: + raise McpClientError(f"Unknown MCP function: {function_name}") + return await tool_ref.client.call_tool(tool_ref.tool_name, arguments or {}) + + async def close(self): + await asyncio.gather( + *(client.close() for client in self._clients), + return_exceptions=True, + ) + + +async def setup_mcp_tools(llm, tool_schemas: list, mcp_config: dict | None): + """Connect configured MCP servers and register their tools with Pipecat.""" + if not mcp_config: + return None + + manager = LiveMCPManager(mcp_config) + discovered = await manager.connect() + if not discovered: + log_and_flush(logging.INFO, "[MCP] No live MCP tools discovered") + return manager + + for tool_ref in discovered: + function_name = tool_ref["function_name"] + properties, required = _schema_from_mcp_tool(tool_ref) + + async def call_mcp_tool(params, name=function_name): + try: + result = await manager.call_tool_by_function_name(name, params.arguments) + await params.result_callback(_tool_result_to_text(result)) + except Exception as exc: + log_and_flush(logging.ERROR, f"[MCP] Tool {name} failed: {exc}") + await params.result_callback(f"MCP tool {name} failed: {exc}") + + llm.register_function(function_name, call_mcp_tool) + tool_schemas.append( + FunctionSchema( + name=function_name, + description=tool_ref.get("description") + or f"Call MCP tool {tool_ref['name']} on {tool_ref['server_name']}", + properties=properties, + required=required, + ) + ) + + log_and_flush(logging.INFO, f"[MCP] Registered {len(discovered)} live MCP tool(s)") + return manager + + def save_transcript(bot_id: str, persona_name: str, messages: list): """Save the conversation transcript to a JSON file for webhook processing.""" os.makedirs(TRANSCRIPT_DIR, exist_ok=True) @@ -358,12 +676,19 @@ async def on_connection_error(transport, error): voice_id = persona.get("cartesia_voice_id") or os.getenv("CARTESIA_VOICE_ID") log_and_flush(logging.INFO, f"[PERSONA] Using voice ID: {voice_id}") - tts = CartesiaTTSService( + tts_speed = resolve_tts_speed(persona) + tts_kwargs = build_cartesia_tts_kwargs( api_key=os.getenv("CARTESIA_API_KEY"), voice_id=voice_id, sample_rate=output_sample_rate, + speed=tts_speed, + ) + tts = CartesiaTTSService(**tts_kwargs) + speed_applied = "speed" in tts_kwargs or "params" in tts_kwargs + log_and_flush( + logging.INFO, + f"[TTS] Cartesia TTS initialized with sample_rate={output_sample_rate}, voice_id={voice_id}, speed={tts_speed}, speed_applied={speed_applied}", ) - log_and_flush(logging.INFO, f"[TTS] Cartesia TTS initialized with sample_rate={output_sample_rate}, voice_id={voice_id}") llm = OpenAILLMService( api_key=os.getenv("OPENAI_API_KEY"), @@ -372,6 +697,7 @@ async def on_connection_error(transport, error): ) log_and_flush(logging.INFO, "[LLM] OpenAI LLM initialized with model=gpt-4.1") + mcp_manager = None if enable_tools: log_and_flush(logging.INFO, "[TOOLS] Registering function tools") llm.register_function("get_weather", get_weather) @@ -437,8 +763,11 @@ async def on_connection_error(transport, error): required=["prospect_name", "company_name", "summary", "next_steps", "qualified"], ) + tool_schemas = [weather_function, time_function, save_call_summary_function] + mcp_manager = await setup_mcp_tools(llm, tool_schemas, persona.get("mcp")) + # Create tools schema - tools = ToolsSchema(standard_tools=[weather_function, time_function, save_call_summary_function]) + tools = ToolsSchema(standard_tools=tool_schemas) else: log_and_flush(logging.INFO, "[TOOLS] Function tools are disabled") tools = None @@ -478,6 +807,10 @@ async def on_connection_error(transport, error): system_content += "You are a meeting bot. You are in a meeting with a group of people. You are here to help the group. You are not the host of the meeting. You are not the organizer of the meeting. You are not the participant in the meeting. You are the meeting bot." system_content += "YOU ARE HELP TO HELP. KEEP IT SHORT. EVERYTHING YOU SAY WILL BE REPEATED BACK TO THE GROUP OUT LOUD so DO NOT add PUNCTUATION OR CAPS. JUST SAY WHAT YOU NEED TO SAY IN A CONCISE MANNER." + mcp_context = build_mcp_context_prompt(persona.get("mcp")) + if mcp_context: + system_content += mcp_context + log_and_flush(logging.INFO, "[MCP] Added MCP metadata to system context") # Set up messages messages = [ @@ -586,12 +919,12 @@ async def start_conversation(): waited = 0 ready = False - log_and_flush(logging.INFO, f"[BOT] Waiting for ready signal (admission roster)...") + log_and_flush(logging.INFO, "[BOT] Waiting for ready signal (admission roster)...") log_and_flush(logging.INFO, f"[BOT] Looking for ready file: {ready_file}") while waited < max_wait_seconds: if os.path.exists(ready_file): - log_and_flush(logging.INFO, f"[BOT] Ready signal received! Bot is in the call.") + log_and_flush(logging.INFO, "[BOT] Ready signal received! Bot is in the call.") ready = True try: os.remove(ready_file) @@ -636,7 +969,7 @@ async def start_conversation(): ) else: # No entry message - prompt LLM to introduce itself - log_and_flush(logging.INFO, f"[BOT] No entry message, prompting LLM to introduce") + log_and_flush(logging.INFO, "[BOT] No entry message, prompting LLM to introduce") initial_prompt = {"role": "user", "content": "Please introduce yourself and start the conversation."} await task.queue_frames([LLMMessagesAppendFrame(messages=[initial_prompt], run_llm=True)]) log_and_flush(logging.INFO, "[BOT] LLM prompted to introduce itself") @@ -665,6 +998,12 @@ async def start_conversation(): log_and_flush(logging.INFO, "[TRANSCRIPT] Final transcript saved on shutdown") except Exception as e: log_and_flush(logging.ERROR, f"[TRANSCRIPT] Error saving final transcript: {e}") + if mcp_manager: + try: + await mcp_manager.close() + log_and_flush(logging.INFO, "[MCP] Closed MCP connections") + except Exception as e: + log_and_flush(logging.WARNING, f"[MCP] Error closing MCP connections: {e}") def cli() -> None: @@ -697,6 +1036,10 @@ def cli() -> None: ) parser.add_argument("--client-id", help="Internal client ID for the bot") parser.add_argument("--persona-data-json", help="Persona data as JSON string") + parser.add_argument( + "--persona-data-file", + help="Path to persona data JSON payload. Preferred for MCP secrets.", + ) parser.add_argument("--api-key", help="API key for authentication") parser.add_argument("--meetingbaas-bot-id", help="MeetingBaas bot ID") @@ -709,13 +1052,25 @@ def cli() -> None: # Parse the full persona payload from the parent process; main() uses it # directly when it carries a prompt (dynamic personas are never on disk). persona_data = None - if args.persona_data_json: + if args.persona_data_file: + try: + with open(args.persona_data_file, "r") as f: + persona_data = json.load(f) + try: + os.remove(args.persona_data_file) + except OSError: + pass + if persona_name == "Meeting Bot" and persona_data.get("path"): + persona_name = os.path.basename(persona_data["path"]) + print(f"[STARTUP] Extracted persona name from path: {persona_name}") + except Exception as e: + print(f"Error parsing persona data file: {e}") + persona_data = None + elif args.persona_data_json: try: - import json persona_data = json.loads(args.persona_data_json) # If persona_name is still the default, try to get folder name from path if persona_name == "Meeting Bot" and persona_data.get("path"): - import os persona_name = os.path.basename(persona_data["path"]) print(f"[STARTUP] Extracted persona name from path: {persona_name}") except Exception as e: diff --git a/speaking-bot-openapi.json b/speaking-bot-openapi.json new file mode 100644 index 0000000..cef530c --- /dev/null +++ b/speaking-bot-openapi.json @@ -0,0 +1,889 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Speaking Meeting Bot API", + "description": "API for deploying AI-powered speaking agents in video meetings. Combines MeetingBaas for meeting connectivity with Pipecat for voice AI processing.", + "version": "0.1.0" + }, + "paths": { + "/bots": { + "post": { + "tags": [ + "bots" + ], + "summary": "Join Meeting", + "description": "Create and deploy a speaking bot in a meeting.\n\nLaunches an AI-powered bot that joins a video meeting through MeetingBaas\nand processes audio using Pipecat's voice AI framework.", + "operationId": "join_meeting_bots_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BotRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Bot successfully created and joined the meeting", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JoinResponse" + } + } + } + }, + "400": { + "description": "Bad request - Missing required fields or invalid data" + }, + "500": { + "description": "Server error - Failed to create bot through MeetingBaas API" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/bots/{bot_id}": { + "delete": { + "tags": [ + "bots" + ], + "summary": "Leave Bot", + "description": "Remove a bot from a meeting by its ID.\n\nThis will:\n1. Call the MeetingBaas API to make the bot leave\n2. Close WebSocket connections if they exist\n3. Terminate the associated Pipecat process", + "operationId": "leave_bot_bots__bot_id__delete", + "parameters": [ + { + "name": "bot_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Bot Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveBotRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Bot successfully removed from meeting", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Leave Bot Bots Bot Id Delete" + } + } + } + }, + "400": { + "description": "Bad request - Missing required fields or identifiers" + }, + "404": { + "description": "Bot not found - No bot with the specified ID" + }, + "500": { + "description": "Server error - Failed to remove bot from MeetingBaas API" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/personas/generate-image": { + "post": { + "tags": [ + "personas" + ], + "summary": "Generate Persona Image", + "description": "Generate an image for a persona using Replicate.", + "operationId": "generate_persona_image_personas_generate_image_post", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonaImageRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Image successfully generated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonaImageResponse" + } + } + } + }, + "400": { + "description": "Invalid request data" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/webhook": { + "post": { + "tags": [ + "webhook" + ], + "summary": "Meetingbaas Webhook", + "description": "Webhook endpoint for MeetingBaas callbacks.\n\nReceives events like bot_joined, bot_left, call_ended, transcription, etc.\n- On 'in_call_recording': signals Pipecat to start speaking\n- On call end: generates a summary from the transcript", + "operationId": "meetingbaas_webhook_webhook_post", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/health": { + "get": { + "tags": [ + "system" + ], + "summary": "Health", + "description": "Health check endpoint", + "operationId": "health_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/ready": { + "get": { + "tags": [ + "system" + ], + "summary": "Ready", + "description": "Readiness endpoint with externally visible base URL resolution.", + "operationId": "ready_ready_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + } + }, + "components": { + "schemas": { + "BotRequest": { + "properties": { + "meeting_url": { + "type": "string", + "title": "Meeting Url", + "description": "URL of the Google Meet, Zoom or Microsoft Teams meeting to join" + }, + "bot_name": { + "type": "string", + "title": "Bot Name", + "description": "Name to display for the bot in the meeting", + "default": "" + }, + "personas": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Personas", + "description": "List of persona names to use. The first available will be selected." + }, + "bot_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Bot Image" + }, + "entry_message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Entry Message" + }, + "extra": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Extra" + }, + "enable_tools": { + "type": "boolean", + "title": "Enable Tools", + "default": true + }, + "prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Prompt" + }, + "websocket_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Websocket Url", + "description": "Optional public WebSocket base URL override, e.g. wss://bot.example.com" + }, + "turn_config": { + "anyOf": [ + { + "$ref": "#/components/schemas/TurnConfig" + }, + { + "type": "null" + } + ], + "description": "Per-bot turn-taking tuning (VAD confidence/start_secs/stop_secs/min_volume)" + }, + "prompt_data_sources": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/PromptDataSource" + }, + "type": "array", + "maxItems": 10 + }, + { + "type": "null" + } + ], + "title": "Prompt Data Sources", + "description": "External text or URL data sources to append to the bot prompt" + }, + "prompt_data_token_limit": { + "type": "integer", + "maximum": 50000.0, + "minimum": 0.0, + "title": "Prompt Data Token Limit", + "description": "Approximate total token cap for loaded prompt_data_sources. 0 disables loading.", + "default": 4000 + }, + "mcp": { + "anyOf": [ + { + "$ref": "#/components/schemas/MCPConfig" + }, + { + "type": "null" + } + ], + "description": "MCP server/tool metadata and optional live connection details" + }, + "speech_speed": { + "anyOf": [ + { + "type": "number", + "maximum": 2.0, + "minimum": 0.5 + }, + { + "type": "null" + } + ], + "title": "Speech Speed", + "description": "TTS speaking speed multiplier. Defaults to CARTESIA_TTS_SPEED, TTS_SPEED, SPEECH_SPEED, or the runner default." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "meeting_url" + ], + "title": "BotRequest", + "description": "Request model for creating a speaking bot in a meeting.", + "example": { + "bot_image": "https://example.com/bot-avatar.png", + "bot_name": "Meeting Assistant", + "enable_tools": true, + "entry_message": "Hello! I'm here to assist with the meeting.", + "extra": { + "company": "ACME Corp", + "meeting_purpose": "Weekly sync" + }, + "mcp": { + "servers": [ + { + "name": "crm", + "tool_allowlist": [ + "get_account", + "list_recent_calls" + ], + "tools": [ + "get_account", + "list_recent_calls" + ], + "transport": "streamable_http", + "url": "https://mcp.example.com" + } + ] + }, + "meeting_url": "https://meet.google.com/abc-defg-hij", + "personas": [ + "helpful_assistant", + "meeting_facilitator" + ], + "prompt": "You are Meeting Assistant, a concise and professional AI bot that helps summarize key points and keep the meeting on track. Speak clearly and stay on topic.", + "prompt_data_sources": [ + { + "name": "CRM account notes", + "type": "url", + "url": "https://example.com/account-notes.md" + } + ], + "prompt_data_token_limit": 3000, + "speech_speed": 1.15, + "websocket_url": "wss://bots.example.com" + } + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "JoinResponse": { + "properties": { + "bot_id": { + "type": "string", + "title": "Bot Id", + "description": "The MeetingBaas bot ID used for API operations with MeetingBaas" + } + }, + "type": "object", + "required": [ + "bot_id" + ], + "title": "JoinResponse", + "description": "Response model for a bot joining a meeting" + }, + "LeaveBotRequest": { + "properties": { + "bot_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Bot Id", + "description": "The MeetingBaas bot ID to remove from the meeting. This will also close the WebSocket connection made through Pipecat by this bot." + } + }, + "additionalProperties": false, + "type": "object", + "title": "LeaveBotRequest", + "description": "Request model for making a bot leave a meeting" + }, + "MCPConfig": { + "properties": { + "servers": { + "items": { + "$ref": "#/components/schemas/MCPServerConfig" + }, + "type": "array", + "maxItems": 10, + "title": "Servers", + "description": "MCP servers to document and optionally connect for tool calls" + }, + "instructions": { + "anyOf": [ + { + "type": "string", + "maxLength": 4000 + }, + { + "type": "null" + } + ], + "title": "Instructions", + "description": "Global MCP usage instructions for the bot" + } + }, + "additionalProperties": false, + "type": "object", + "title": "MCPConfig", + "description": "MCP server metadata and optional live connection details." + }, + "MCPServerConfig": { + "properties": { + "name": { + "type": "string", + "maxLength": 120, + "minLength": 1, + "title": "Name" + }, + "enabled": { + "type": "boolean", + "title": "Enabled", + "description": "Whether this server may be used. Disabled servers are documented but not connected.", + "default": true + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url", + "description": "Remote MCP server URL. Required for http, streamable_http, and sse transports." + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Headers", + "description": "Optional HTTP headers for remote MCP servers. Use only when a server requires them." + }, + "transport": { + "anyOf": [ + { + "type": "string", + "enum": [ + "http", + "streamable_http", + "sse" + ] + }, + { + "type": "null" + } + ], + "title": "Transport", + "description": "Remote MCP transport. Omit for metadata-only servers that cannot execute tools." + }, + "tools": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "maxItems": 50 + }, + { + "type": "null" + } + ], + "title": "Tools", + "description": "Known tool names exposed by this MCP server" + }, + "tool_allowlist": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array", + "maxItems": 50 + }, + { + "type": "null" + } + ], + "title": "Tool Allowlist", + "description": "Optional allowlist of MCP tool names this bot may call from this server." + }, + "timeout_seconds": { + "anyOf": [ + { + "type": "number", + "maximum": 300.0, + "minimum": 0.1 + }, + { + "type": "null" + } + ], + "title": "Timeout Seconds", + "description": "Optional per-server connection/tool timeout in seconds." + }, + "instructions": { + "anyOf": [ + { + "type": "string", + "maxLength": 4000 + }, + { + "type": "null" + } + ], + "title": "Instructions", + "description": "Operator instructions or constraints for this MCP server" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ], + "title": "MCPServerConfig", + "description": "MCP server metadata and optional live connection details." + }, + "PersonaImageRequest": { + "type": "object", + "required": [ + "name", + "description" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the persona to generate an image for" + }, + "description": { + "type": "string", + "description": "Detailed description of the persona's appearance and characteristics" + }, + "gender": { + "type": "string", + "description": "Gender of the persona (optional)", + "enum": [ + "male", + "female", + "non-binary" + ] + }, + "characteristics": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of specific characteristics or features of the persona" + } + } + }, + "PersonaImageResponse": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the persona" + }, + "image_url": { + "type": "string", + "description": "URL of the generated image" + }, + "generated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the image was generated" + } + } + }, + "PromptDataSource": { + "properties": { + "name": { + "type": "string", + "maxLength": 120, + "minLength": 1, + "title": "Name", + "description": "Human-readable source name shown inside the prompt context block", + "default": "external_context" + }, + "type": { + "type": "string", + "enum": [ + "text", + "url" + ], + "title": "Type", + "description": "Whether to load inline text or fetch an external HTTP(S) URL" + }, + "text": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Text", + "description": "Inline context. Required when type is text." + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url", + "description": "HTTP(S) URL to fetch. Required when type is url." + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Headers", + "description": "Optional HTTP headers for URL sources. Avoid request-specific secrets unless needed." + }, + "token_limit": { + "anyOf": [ + { + "type": "integer", + "maximum": 50000.0, + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Token Limit", + "description": "Optional per-source token cap before the request-level cap is applied" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ], + "title": "PromptDataSource", + "description": "External context to append to the bot prompt under a token budget." + }, + "TurnConfig": { + "properties": { + "confidence": { + "anyOf": [ + { + "type": "number", + "maximum": 1.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Confidence", + "description": "VAD speech confidence threshold" + }, + "start_secs": { + "anyOf": [ + { + "type": "number", + "maximum": 5.0, + "minimum": 0.05 + }, + { + "type": "null" + } + ], + "title": "Start Secs", + "description": "Sustained speech (seconds) before a turn registers" + }, + "stop_secs": { + "anyOf": [ + { + "type": "number", + "maximum": 10.0, + "minimum": 0.1 + }, + { + "type": "null" + } + ], + "title": "Stop Secs", + "description": "Silence (seconds) before the bot considers the speaker done and replies" + }, + "min_volume": { + "anyOf": [ + { + "type": "number", + "maximum": 1.0, + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Min Volume", + "description": "Minimum input volume for VAD" + } + }, + "additionalProperties": false, + "type": "object", + "title": "TurnConfig", + "description": "Per-bot voice-activity / turn-taking tuning.\n\nAll fields optional; unset fields fall back to the VAD_* env vars, then\npipecat defaults. Human-facing bots want snappy turn-taking (low\nstop_secs); bot-vs-bot meetings want patience (higher stop_secs and\nstart_secs) so the bots stop barging in on each other." + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + }, + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "x-meeting-baas-api-key", + "description": "MeetingBaas API key for authentication" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "servers": [ + { + "url": "https://speaking.meetingbaas.com", + "description": "Production server" + }, + { + "url": "/", + "description": "Local development server" + } + ] +} diff --git a/tests/test_mcp_client.py b/tests/test_mcp_client.py new file mode 100644 index 0000000..8bcabaf --- /dev/null +++ b/tests/test_mcp_client.py @@ -0,0 +1,249 @@ +import asyncio +import importlib.util +import json +import os +import sys +import unittest +from pathlib import Path +from types import SimpleNamespace + +MCP_CLIENT_PATH = ( + Path(__file__).resolve().parents[1] / "utils" / "mcp_client.py" +) +spec = importlib.util.spec_from_file_location("mcp_client", MCP_CLIENT_PATH) +mcp_client = importlib.util.module_from_spec(spec) +assert spec and spec.loader +sys.modules["mcp_client"] = mcp_client +spec.loader.exec_module(mcp_client) + +HttpMcpClient = mcp_client.HttpMcpClient +StdioMcpClient = mcp_client.StdioMcpClient +encode_stdio_message = mcp_client.encode_stdio_message +build_mcp_tool_name = mcp_client.build_mcp_tool_name +normalize_tool_result = mcp_client.normalize_tool_result +normalize_tools = mcp_client.normalize_tools +parse_sse_json = mcp_client.parse_sse_json +sanitize_mapping = mcp_client.sanitize_mapping +validate_mcp_http_url = mcp_client.validate_mcp_http_url +McpClientError = mcp_client.McpClientError + + +class McpClientHelpersTest(unittest.TestCase): + def test_encode_stdio_message_uses_content_length_frame(self) -> None: + frame = encode_stdio_message({"jsonrpc": "2.0", "id": 1, "method": "ping"}) + header, body = frame.split(b"\r\n\r\n", 1) + + self.assertEqual(header, f"Content-Length: {len(body)}".encode("ascii")) + self.assertEqual(json.loads(body.decode("utf-8"))["method"], "ping") + + def test_build_mcp_tool_name_sanitizes_names(self) -> None: + self.assertEqual( + build_mcp_tool_name("Google Drive", "read-file"), + "mcp_google_drive_read_file", + ) + + def test_parse_sse_json_reads_data_lines(self) -> None: + parsed = parse_sse_json( + ': keepalive\n' + 'event: message\n' + 'data: {"jsonrpc":"2.0","result":{"ok":true}}\n\n' + "data: [DONE]\n\n" + ) + + self.assertEqual(parsed["result"], {"ok": True}) + + def test_normalize_tools_converts_schema_key(self) -> None: + tools = normalize_tools( + { + "tools": [ + { + "name": "search", + "description": "Search CRM", + "inputSchema": {"type": "object"}, + } + ] + } + ) + + self.assertEqual( + tools, + [ + { + "name": "search", + "description": "Search CRM", + "input_schema": {"type": "object"}, + } + ], + ) + + def test_normalize_tool_result_extracts_text_and_jsonish_content(self) -> None: + result = normalize_tool_result( + { + "content": [ + {"type": "text", "text": '{"account":"acme"}'}, + {"type": "json", "json": {"score": 42}}, + "plain text", + ] + } + ) + + self.assertFalse(result["is_error"]) + self.assertEqual(result["content"][0]["json"], {"account": "acme"}) + self.assertEqual(result["content"][1]["json"], {"score": 42}) + self.assertEqual(result["content"][2]["text"], "plain text") + + def test_sanitize_mapping_redacts_secret_like_keys(self) -> None: + sanitized = sanitize_mapping( + { + "Authorization": "Bearer secret", + "Mcp-Session-Id": "session-secret", + "X-Trace-Id": "trace-123", + } + ) + + self.assertEqual(sanitized["Authorization"], "[redacted]") + self.assertEqual(sanitized["Mcp-Session-Id"], "[redacted]") + self.assertEqual(sanitized["X-Trace-Id"], "trace-123") + + def test_validate_mcp_http_url_blocks_localhost(self) -> None: + with self.assertRaises(McpClientError): + validate_mcp_http_url("http://127.0.0.1:3000/mcp") + + def test_validate_mcp_http_url_allows_exact_private_url(self) -> None: + original_allowed = os.environ.get("MCP_ALLOWED_PRIVATE_URLS") + os.environ["MCP_ALLOWED_PRIVATE_URLS"] = "http://127.0.0.1:8123/mcp" + try: + validate_mcp_http_url("http://127.0.0.1:8123/mcp") + with self.assertRaises(McpClientError): + validate_mcp_http_url("http://127.0.0.1:8124/mcp") + finally: + if original_allowed is None: + os.environ.pop("MCP_ALLOWED_PRIVATE_URLS", None) + else: + os.environ["MCP_ALLOWED_PRIVATE_URLS"] = original_allowed + + +class McpClientTransportTest(unittest.TestCase): + def test_http_client_preserves_session_id_without_network(self) -> None: + calls: list[dict[str, str]] = [] + + class FakeResponse: + def __init__(self, headers: dict[str, str]) -> None: + self.status = 200 + self.headers = headers + + async def __aenter__(self) -> "FakeResponse": + return self + + async def __aexit__(self, *args: object) -> None: + return None + + async def json(self, content_type: object = None) -> dict[str, object]: + return {"jsonrpc": "2.0", "result": {"ok": True}} + + class FakeSession: + def __init__(self, timeout: object) -> None: + self.timeout = timeout + + async def __aenter__(self) -> "FakeSession": + return self + + async def __aexit__(self, *args: object) -> None: + return None + + def post( + self, + url: str, + json: dict[str, object], + headers: dict[str, str], + allow_redirects: bool, + ) -> FakeResponse: + self.allow_redirects = allow_redirects + calls.append(dict(headers)) + response_headers = {"Mcp-Session-Id": "session-123"} if len(calls) == 1 else {} + return FakeResponse(response_headers) + + original_aiohttp = mcp_client.aiohttp + original_allowed = os.environ.get("MCP_ALLOWED_PRIVATE_URLS") + os.environ["MCP_ALLOWED_PRIVATE_URLS"] = "http://127.0.0.1:8123/mcp" + mcp_client.aiohttp = SimpleNamespace( + ClientSession=FakeSession, + ClientTimeout=lambda total: {"total": total}, + ) + try: + client = HttpMcpClient( + "http://127.0.0.1:8123/mcp", + headers={"Authorization": "Bearer not-logged"}, + ) + asyncio.run(client._post({"jsonrpc": "2.0", "id": 1, "method": "one"})) + asyncio.run(client._post({"jsonrpc": "2.0", "id": 2, "method": "two"})) + finally: + mcp_client.aiohttp = original_aiohttp + if original_allowed is None: + os.environ.pop("MCP_ALLOWED_PRIVATE_URLS", None) + else: + os.environ["MCP_ALLOWED_PRIVATE_URLS"] = original_allowed + + self.assertNotIn("Mcp-Session-Id", calls[0]) + self.assertEqual(calls[1]["Mcp-Session-Id"], "session-123") + + def test_stdio_client_with_tiny_python_server(self) -> None: + server_code = r""" +import json +import sys + +def read_message(): + headers = {} + while True: + line = sys.stdin.buffer.readline() + if not line: + return None + if line in (b"\r\n", b"\n"): + break + name, value = line.decode("ascii").split(":", 1) + headers[name.lower()] = value.strip() + body = sys.stdin.buffer.read(int(headers["content-length"])) + return json.loads(body.decode("utf-8")) + +def send(payload): + body = json.dumps(payload, separators=(",", ":")).encode("utf-8") + sys.stdout.buffer.write(b"Content-Length: " + str(len(body)).encode("ascii") + b"\r\n\r\n" + body) + sys.stdout.buffer.flush() + +while True: + message = read_message() + if message is None: + break + if "id" not in message: + continue + method = message["method"] + if method == "initialize": + result = {"protocolVersion": "2024-11-05", "capabilities": {}} + elif method == "tools/list": + result = {"tools": [{"name": "echo", "inputSchema": {"type": "object"}}]} + elif method == "tools/call": + result = {"content": [{"type": "text", "text": "{\"echo\": true}"}]} + else: + result = {} + send({"jsonrpc": "2.0", "id": message["id"], "result": result}) +""" + + async def run_client() -> tuple[dict[str, object], list[dict[str, object]], dict[str, object]]: + client = StdioMcpClient([sys.executable, "-c", server_code]) + try: + initialized = await client.initialize() + tools = await client.list_tools() + result = await client.call_tool("echo", {"value": "hi"}) + return initialized, tools, result + finally: + await client.close() + + initialized, tools, result = asyncio.run(run_client()) + + self.assertEqual(initialized["protocolVersion"], "2024-11-05") + self.assertEqual(tools[0]["name"], "echo") + self.assertEqual(result["content"][0]["json"], {"echo": True}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..699d29b --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,78 @@ +import unittest +import importlib.util +import sys +from pathlib import Path + +try: + from pydantic import ValidationError +except ModuleNotFoundError: + ValidationError = None + +if ValidationError is not None: + MODELS_PATH = Path(__file__).resolve().parents[1] / "app" / "models.py" + spec = importlib.util.spec_from_file_location("models", MODELS_PATH) + models = importlib.util.module_from_spec(spec) + assert spec and spec.loader + sys.modules["models"] = models + spec.loader.exec_module(models) + MCPServerConfig = models.MCPServerConfig +else: + MCPServerConfig = None + + +class MCPServerConfigTest(unittest.TestCase): + def setUp(self) -> None: + if MCPServerConfig is None or ValidationError is None: + self.skipTest("pydantic is not installed") + + def test_stdio_transport_is_not_public_api(self) -> None: + with self.assertRaises(ValidationError): + MCPServerConfig( + name="google-drive", + enabled=True, + transport="stdio", + tool_allowlist=["search", "read_file"], + timeout_seconds=20, + ) + + def test_remote_server_accepts_url_headers_and_allowlist(self) -> None: + config = MCPServerConfig( + name="crm", + transport="streamable_http", + url="https://mcp.example.com", + headers={"Authorization": "Bearer token"}, + tool_allowlist=["get_account"], + ) + + self.assertEqual(config.url, "https://mcp.example.com") + self.assertEqual(config.headers["Authorization"], "Bearer token") + + def test_metadata_only_server_is_allowed_but_not_connectable(self) -> None: + config = MCPServerConfig( + name="crm", + tools=["get_account"], + instructions="Available when configured with a live transport.", + ) + + self.assertIsNone(config.transport) + self.assertTrue(config.enabled) + + def test_command_fields_are_rejected(self) -> None: + with self.assertRaises(ValidationError): + MCPServerConfig( + name="google-drive", + transport="streamable_http", + url="https://mcp.example.com", + command="npx", + ) + + def test_remote_transport_requires_url(self) -> None: + with self.assertRaises(ValidationError): + MCPServerConfig(name="crm", transport="streamable_http") + + def test_connection_details_require_transport(self) -> None: + with self.assertRaises(ValidationError): + MCPServerConfig(name="crm", url="https://mcp.example.com") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_openapi_snapshot.py b/tests/test_openapi_snapshot.py new file mode 100644 index 0000000..a8e88d0 --- /dev/null +++ b/tests/test_openapi_snapshot.py @@ -0,0 +1,26 @@ +import json +from pathlib import Path + +from app.main import create_app + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_committed_openapi_snapshot_matches_app_schema(): + snapshot = json.loads((ROOT / "speaking-bot-openapi.json").read_text()) + root_snapshot = json.loads((ROOT / "openapi.json").read_text()) + generated = create_app().openapi() + + assert snapshot == generated + assert root_snapshot == generated + + +def test_openapi_snapshot_covers_prompt_context_mcp_and_speech_controls(): + snapshot = json.loads((ROOT / "speaking-bot-openapi.json").read_text()) + bot_props = snapshot["components"]["schemas"]["BotRequest"]["properties"] + + assert "prompt_data_sources" in bot_props + assert "prompt_data_token_limit" in bot_props + assert "mcp" in bot_props + assert "speech_speed" in bot_props diff --git a/tests/test_prompt_context.py b/tests/test_prompt_context.py new file mode 100644 index 0000000..abf2ed2 --- /dev/null +++ b/tests/test_prompt_context.py @@ -0,0 +1,137 @@ +import asyncio +import importlib.util +import sys +import unittest +from pathlib import Path +from types import SimpleNamespace + +PROMPT_CONTEXT_PATH = ( + Path(__file__).resolve().parents[1] / "app" / "services" / "prompt_context.py" +) +spec = importlib.util.spec_from_file_location("prompt_context", PROMPT_CONTEXT_PATH) +prompt_context = importlib.util.module_from_spec(spec) +assert spec and spec.loader +spec.loader.exec_module(prompt_context) + +estimate_tokens = prompt_context.estimate_tokens +format_mcp_context = prompt_context.format_mcp_context +load_prompt_context = prompt_context.load_prompt_context +truncate_to_token_limit = prompt_context.truncate_to_token_limit +PromptContextError = prompt_context.PromptContextError +_validate_fetch_url = prompt_context._validate_fetch_url +_fetch_url_source = prompt_context._fetch_url_source + + +class PromptContextTest(unittest.TestCase): + def test_estimate_tokens_uses_four_chars_per_token(self) -> None: + self.assertEqual(estimate_tokens("abcd"), 1) + self.assertEqual(estimate_tokens("abcde"), 2) + + def test_truncate_to_token_limit(self) -> None: + text, truncated = truncate_to_token_limit("abcdefghij", 2) + + self.assertTrue(truncated) + self.assertLessEqual(estimate_tokens(text), 12) + self.assertIn("truncated", text) + + def test_load_prompt_context_from_inline_source(self) -> None: + source = SimpleNamespace( + name="CRM notes", + type="text", + text="Prospect uses MeetingBaas and wants MCP support.", + url=None, + headers=None, + token_limit=None, + ) + + result = asyncio.run(load_prompt_context([source], total_token_limit=100)) + + self.assertIn("CRM notes", result.block) + self.assertIn("MCP support", result.block) + self.assertEqual(result.sources[0]["name"], "CRM notes") + self.assertNotIn("text", result.sources[0]) + + def test_format_mcp_context(self) -> None: + mcp = { + "instructions": "Use CRM data when relevant.", + "servers": [ + { + "name": "crm", + "url": "https://mcp.example.com", + "transport": "streamable_http", + "tools": ["get_account", "list_calls"], + } + ], + } + + block = format_mcp_context(mcp) + + self.assertIn("Server: crm", block) + self.assertIn("get_account", block) + self.assertIn("Use CRM data", block) + + def test_private_prompt_urls_blocked_by_default(self) -> None: + with self.assertRaises(PromptContextError): + _validate_fetch_url("http://127.0.0.1:8000/notes.md") + + def test_prompt_url_redirects_are_not_followed(self) -> None: + calls = [] + + class FakeResponse: + status = 302 + content = SimpleNamespace(read=lambda *_args: b"") + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + return None + + class FakeSession: + def __init__(self, timeout): + self.timeout = timeout + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + return None + + def get(self, url, headers, allow_redirects): + calls.append( + { + "url": url, + "headers": headers, + "allow_redirects": allow_redirects, + } + ) + return FakeResponse() + + fake_aiohttp = SimpleNamespace( + ClientSession=FakeSession, + ClientTimeout=lambda total: {"total": total}, + ) + original_aiohttp = sys.modules.get("aiohttp") + sys.modules["aiohttp"] = fake_aiohttp + try: + with self.assertRaises(PromptContextError): + asyncio.run( + _fetch_url_source( + SimpleNamespace( + type="url", + url="https://example.com/context.txt", + headers=None, + ) + ) + ) + finally: + if original_aiohttp is None: + sys.modules.pop("aiohttp", None) + else: + sys.modules["aiohttp"] = original_aiohttp + + self.assertEqual(calls[0]["allow_redirects"], False) + + +if __name__ == "__main__": + unittest.main() diff --git a/utils/mcp_client.py b/utils/mcp_client.py new file mode 100644 index 0000000..64f8480 --- /dev/null +++ b/utils/mcp_client.py @@ -0,0 +1,531 @@ +"""Minimal MCP client primitives for stdio and streamable HTTP servers.""" + +from __future__ import annotations + +import asyncio +import json +import logging +import os +import re +import socket +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field +from ipaddress import ip_address +from typing import Any +from urllib.parse import urlparse + + +LOGGER = logging.getLogger(__name__) +JSONRPC_VERSION = "2.0" +MCP_PROTOCOL_VERSION = "2024-11-05" +SESSION_HEADER = "Mcp-Session-Id" +SECRET_KEY_PARTS = ( + "api_key", + "apikey", + "authorization", + "cookie", + "password", + "secret", + "session", + "token", +) +aiohttp: Any | None = None + + +class McpClientError(Exception): + """Raised when an MCP request cannot be completed.""" + + +def build_mcp_tool_name(server_name: str, tool_name: str) -> str: + """Return an OpenAI/Pipecat-safe function name for an MCP tool.""" + raw = f"mcp_{server_name}_{tool_name}".lower() + safe = re.sub(r"[^a-z0-9_]", "_", raw) + safe = re.sub(r"_+", "_", safe).strip("_") + return safe[:64] or "mcp_tool" + + +def sanitize_mapping(values: Mapping[str, Any] | None) -> dict[str, Any]: + """Return a copy with likely secret values redacted for safe logs/errors.""" + if not values: + return {} + + sanitized: dict[str, Any] = {} + for key, value in values.items(): + normalized = key.lower().replace("-", "_") + if any(part in normalized for part in SECRET_KEY_PARTS): + sanitized[key] = "[redacted]" + else: + sanitized[key] = value + return sanitized + + +def _private_mcp_urls_allowed() -> bool: + return os.getenv("MCP_ALLOW_PRIVATE_URLS", "").lower() in {"1", "true", "yes"} + + +def _allowed_private_mcp_urls() -> set[str]: + raw = os.getenv("MCP_ALLOWED_PRIVATE_URLS", "") + return {item.strip().rstrip("/") for item in raw.split(",") if item.strip()} + + +def _is_allowed_private_mcp_url(url: str) -> bool: + normalized = url.rstrip("/") + return normalized in _allowed_private_mcp_urls() + + +def _is_private_ip(value: str) -> bool: + parsed = ip_address(value) + return ( + parsed.is_private + or parsed.is_loopback + or parsed.is_link_local + or parsed.is_multicast + or parsed.is_reserved + or parsed.is_unspecified + ) + + +def validate_mcp_http_url(url: str) -> None: + """Block obvious SSRF targets unless explicitly allowed.""" + parsed = urlparse(url) + if parsed.scheme not in {"http", "https"} or not parsed.hostname: + raise McpClientError(f"Invalid MCP HTTP URL: {url}") + + if _private_mcp_urls_allowed(): + return + + if _is_allowed_private_mcp_url(url): + return + + host = parsed.hostname + try: + if _is_private_ip(host): + raise McpClientError(f"MCP HTTP URL host is private or local: {host}") + return + except ValueError: + pass + + try: + addresses = socket.getaddrinfo(host, None) + except socket.gaierror as e: + raise McpClientError(f"Could not resolve MCP HTTP host '{host}': {e}") from e + + for address in addresses: + if _is_private_ip(address[4][0]): + raise McpClientError( + f"MCP HTTP URL resolves to private or local address: {host}" + ) + + +def encode_stdio_message(message: Mapping[str, Any]) -> bytes: + """Encode one JSON-RPC message using MCP Content-Length framing.""" + body = json.dumps(message, separators=(",", ":")).encode("utf-8") + return b"Content-Length: " + str(len(body)).encode("ascii") + b"\r\n\r\n" + body + + +async def read_stdio_message(reader: asyncio.StreamReader) -> dict[str, Any]: + """Read one Content-Length framed JSON message from a subprocess stream.""" + headers: dict[str, str] = {} + + while True: + line = await reader.readline() + if line == b"": + raise McpClientError("MCP stdio server closed stdout") + if line in {b"\r\n", b"\n"}: + break + try: + name, value = line.decode("ascii").split(":", 1) + except ValueError as e: + raise McpClientError("Invalid MCP stdio header") from e + headers[name.strip().lower()] = value.strip() + + try: + content_length = int(headers["content-length"]) + except (KeyError, ValueError) as e: + raise McpClientError("Missing or invalid MCP stdio Content-Length") from e + + body = await reader.readexactly(content_length) + try: + parsed = json.loads(body.decode("utf-8")) + except json.JSONDecodeError as e: + raise McpClientError("Invalid MCP stdio JSON response") from e + + if not isinstance(parsed, dict): + raise McpClientError("MCP stdio response must be a JSON object") + return parsed + + +def parse_sse_json(text: str) -> dict[str, Any]: + """Parse a simple text/event-stream response and return the first JSON data.""" + events: list[str] = [] + current: list[str] = [] + + for raw_line in text.splitlines(): + line = raw_line.rstrip("\r") + if not line: + if current: + events.append("\n".join(current)) + current = [] + continue + if line.startswith(":"): + continue + if line.startswith("data:"): + current.append(line[5:].lstrip()) + + if current: + events.append("\n".join(current)) + + for event in events: + if event == "[DONE]": + continue + try: + parsed = json.loads(event) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict): + return parsed + + raise McpClientError("No JSON data found in MCP event stream") + + +def normalize_tools(tools_payload: Mapping[str, Any]) -> list[dict[str, Any]]: + """Convert an MCP tools/list result into runner-friendly dictionaries.""" + tools = tools_payload.get("tools", []) + if not isinstance(tools, list): + return [] + + normalized: list[dict[str, Any]] = [] + for tool in tools: + if not isinstance(tool, Mapping): + continue + normalized.append( + { + "name": tool.get("name"), + "description": tool.get("description", ""), + "input_schema": tool.get("inputSchema") or tool.get("input_schema") or {}, + } + ) + return normalized + + +def normalize_tool_result(result_payload: Mapping[str, Any]) -> dict[str, Any]: + """Convert an MCP tools/call result into simple content dictionaries.""" + content = result_payload.get("content", []) + normalized_content: list[dict[str, Any]] = [] + + if isinstance(content, list): + for item in content: + normalized_content.append(_normalize_content_item(item)) + elif content is not None: + normalized_content.append(_normalize_content_item(content)) + + return { + "content": normalized_content, + "is_error": bool(result_payload.get("isError", result_payload.get("is_error", False))), + } + + +def _normalize_content_item(item: Any) -> dict[str, Any]: + if isinstance(item, Mapping): + item_type = item.get("type") + if item_type == "text": + text = str(item.get("text", "")) + parsed_json = _parse_jsonish(text) + normalized: dict[str, Any] = {"type": "text", "text": text} + if parsed_json is not None: + normalized["json"] = parsed_json + return normalized + if item_type == "json": + return {"type": "json", "json": item.get("json", item.get("data"))} + if "json" in item: + return {"type": item_type or "json", "json": item["json"]} + if "data" in item: + return {"type": item_type or "data", "data": item["data"]} + return dict(item) + + if isinstance(item, str): + parsed_json = _parse_jsonish(item) + normalized = {"type": "text", "text": item} + if parsed_json is not None: + normalized["json"] = parsed_json + return normalized + + return {"type": "json", "json": item} + + +def _parse_jsonish(value: str) -> Any | None: + stripped = value.strip() + if not stripped or stripped[0] not in "[{": + return None + try: + return json.loads(stripped) + except json.JSONDecodeError: + return None + + +@dataclass +class _JsonRpcState: + next_id: int = 1 + + def request(self, method: str, params: Mapping[str, Any] | None = None) -> dict[str, Any]: + request_id = self.next_id + self.next_id += 1 + message: dict[str, Any] = { + "jsonrpc": JSONRPC_VERSION, + "id": request_id, + "method": method, + } + if params is not None: + message["params"] = dict(params) + return message + + def notification( + self, + method: str, + params: Mapping[str, Any] | None = None, + ) -> dict[str, Any]: + message: dict[str, Any] = {"jsonrpc": JSONRPC_VERSION, "method": method} + if params is not None: + message["params"] = dict(params) + return message + + +def _extract_result(response: Mapping[str, Any]) -> dict[str, Any]: + if "error" in response: + error = response["error"] + if isinstance(error, Mapping): + message = error.get("message") or error + else: + message = error + raise McpClientError(f"MCP server returned error: {message}") + + result = response.get("result", {}) + if not isinstance(result, dict): + raise McpClientError("MCP response result must be a JSON object") + return result + + +@dataclass +class StdioMcpClient: + """MCP client for subprocess servers using Content-Length stdio framing.""" + + command: Sequence[str] + env: Mapping[str, str] | None = None + cwd: str | None = None + client_name: str = "speaking-meeting-bot" + client_version: str = "0.1.0" + _state: _JsonRpcState = field(default_factory=_JsonRpcState, init=False) + _process: asyncio.subprocess.Process | None = field(default=None, init=False) + + async def start(self) -> None: + if self._process is not None: + return + if not self.command: + raise McpClientError("Stdio MCP command cannot be empty") + env = os.environ.copy() + if self.env is not None: + env.update({str(key): str(value) for key, value in self.env.items()}) + + self._process = await asyncio.create_subprocess_exec( + *self.command, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.DEVNULL, + env=env, + cwd=self.cwd, + ) + + async def initialize(self) -> dict[str, Any]: + result = await self._request( + "initialize", + { + "protocolVersion": MCP_PROTOCOL_VERSION, + "capabilities": {}, + "clientInfo": { + "name": self.client_name, + "version": self.client_version, + }, + }, + ) + await self._notification("notifications/initialized") + return result + + async def list_tools(self) -> list[dict[str, Any]]: + return normalize_tools(await self._request("tools/list")) + + async def call_tool( + self, + name: str, + arguments: Mapping[str, Any] | None = None, + ) -> dict[str, Any]: + result = await self._request( + "tools/call", + {"name": name, "arguments": dict(arguments or {})}, + ) + return normalize_tool_result(result) + + async def close(self) -> None: + process = self._process + self._process = None + if process is None: + return + if process.stdin and not process.stdin.is_closing(): + process.stdin.close() + try: + await process.stdin.wait_closed() + except BrokenPipeError: + pass + try: + await asyncio.wait_for(process.wait(), timeout=2) + except asyncio.TimeoutError: + process.terminate() + await process.wait() + + async def _request( + self, + method: str, + params: Mapping[str, Any] | None = None, + ) -> dict[str, Any]: + await self.start() + assert self._process and self._process.stdin and self._process.stdout + message = self._state.request(method, params) + self._process.stdin.write(encode_stdio_message(message)) + await self._process.stdin.drain() + response = await read_stdio_message(self._process.stdout) + return _extract_result(response) + + async def _notification( + self, + method: str, + params: Mapping[str, Any] | None = None, + ) -> None: + await self.start() + assert self._process and self._process.stdin + self._process.stdin.write(encode_stdio_message(self._state.notification(method, params))) + await self._process.stdin.drain() + + +@dataclass +class HttpMcpClient: + """MCP client for streamable-http/http servers using JSON-RPC POST.""" + + url: str + headers: Mapping[str, str] | None = None + client_name: str = "speaking-meeting-bot" + client_version: str = "0.1.0" + timeout_seconds: float = 12 + _state: _JsonRpcState = field(default_factory=_JsonRpcState, init=False) + _session_id: str | None = field(default=None, init=False) + + async def initialize(self) -> dict[str, Any]: + result = await self._request( + "initialize", + { + "protocolVersion": MCP_PROTOCOL_VERSION, + "capabilities": {}, + "clientInfo": { + "name": self.client_name, + "version": self.client_version, + }, + }, + ) + await self._notification("notifications/initialized") + return result + + async def list_tools(self) -> list[dict[str, Any]]: + return normalize_tools(await self._request("tools/list")) + + async def call_tool( + self, + name: str, + arguments: Mapping[str, Any] | None = None, + ) -> dict[str, Any]: + result = await self._request( + "tools/call", + {"name": name, "arguments": dict(arguments or {})}, + ) + return normalize_tool_result(result) + + async def close(self) -> None: + return None + + async def _request( + self, + method: str, + params: Mapping[str, Any] | None = None, + ) -> dict[str, Any]: + response = await self._post(self._state.request(method, params)) + return _extract_result(response) + + async def _notification( + self, + method: str, + params: Mapping[str, Any] | None = None, + ) -> None: + await self._post(self._state.notification(method, params)) + + async def _post(self, message: Mapping[str, Any]) -> dict[str, Any]: + aiohttp_module = _get_aiohttp() + validate_mcp_http_url(self.url) + request_headers = { + "Accept": "application/json, text/event-stream", + "Content-Type": "application/json", + **dict(self.headers or {}), + } + if self._session_id: + request_headers[SESSION_HEADER] = self._session_id + + timeout = aiohttp_module.ClientTimeout(total=self.timeout_seconds) + try: + async with aiohttp_module.ClientSession(timeout=timeout) as session: + async with session.post( + self.url, + json=message, + headers=request_headers, + allow_redirects=False, + ) as response: + if SESSION_HEADER in response.headers: + self._session_id = response.headers[SESSION_HEADER] + if 300 <= response.status < 400: + raise McpClientError("MCP HTTP redirects are not followed") + if response.status >= 400: + safe_headers = sanitize_mapping(request_headers) + LOGGER.warning( + "MCP HTTP request failed: status=%s headers=%s", + response.status, + safe_headers, + ) + raise McpClientError( + f"MCP HTTP server returned HTTP {response.status}" + ) + if response.status in {202, 204}: + return {} + return await _parse_http_response(response) + except McpClientError: + raise + except Exception as e: + raise McpClientError(f"MCP HTTP request failed: {e}") from e + + +def _get_aiohttp() -> Any: + global aiohttp + if aiohttp is None: + import aiohttp as aiohttp_module + + aiohttp = aiohttp_module + return aiohttp + + +async def _parse_http_response(response: Any) -> dict[str, Any]: + content_type = response.headers.get("Content-Type", "") + if "text/event-stream" in content_type: + return parse_sse_json(await response.text()) + + try: + parsed = await response.json(content_type=None) + except Exception as e: + raise McpClientError("Invalid MCP HTTP JSON response") from e + + if not isinstance(parsed, dict): + raise McpClientError("MCP HTTP response must be a JSON object") + return parsed