-
Notifications
You must be signed in to change notification settings - Fork 34
Add LLM provider routing for speaking bots #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
be9efd3
4b17daf
b7e1147
fc50ccc
31a8899
6ebdae1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Use built-in collection types and Ruff flags Proposed cleanup-from typing import Any, Dict, List, Literal, Optional
+from typing import Any, Literal
- text: Optional[str] = Field(
+ text: str | None = Field(
...
- headers: Optional[Dict[str, str]] = Field(
+ headers: dict[str, str] | None = Field(
...
- tools: Optional[List[str]] = Field(
+ tools: list[str] | None = Field(
...
- prompt_data_sources: Optional[List[PromptDataSource]] = Field(
+ prompt_data_sources: list[PromptDataSource] | None = Field(Also applies to: 67-79, 123-151, 187-192, 248-290 🧰 Tools🪛 Ruff (0.15.20)[warning] 4-4: (UP035) [warning] 4-4: (UP035) 🤖 Prompt for AI AgentsSources: Coding guidelines, Linters/SAST tools |
||
|
|
||
| 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,153 @@ 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.", | ||
| ) | ||
|
Comment on lines
+67
to
+70
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: git ls-files app/models.py && printf '\n---\n' && sed -n '1,140p' app/models.py && printf '\n---\n' && rg -n "prompt_data_token_limit|max_length|request-size|body size|content-length|token_limit|inline context|type is text" -S .Repository: Meeting-BaaS/speaking-meeting-bot Length of output: 9434 🏁 Script executed: sed -n '240,320p' app/models.py && printf '\n---\n' && sed -n '1,260p' app/services/prompt_context.py && printf '\n---\n' && sed -n '200,260p' app/routes.py && printf '\n---\n' && rg -n "max_request|body_size|max_body|limit.*bytes|Content-Length|request size|request_size|client_max_body_size|limit_bytes" -S app README.md .Repository: Meeting-BaaS/speaking-meeting-bot Length of output: 14926 🏁 Script executed: rg -n "FastAPI\\(|uvicorn|nginx|client_max_body_size|max_body|request size|body size|Content-Length|limit.*bytes|prompt_data_token_limit|PROMPT_DATA_SOURCE_MAX_BYTES" -S . && printf '\n---\n' && git ls-files | rg '(^|/)(main|app|server|run|docker|nginx|compose|proxy|deploy|README|docs).*'Repository: Meeting-BaaS/speaking-meeting-bot Length of output: 7548 Cap inline prompt text 🤖 Prompt for AI Agents |
||
| 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://") | ||
|
Comment on lines
+92
to
+93
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Do not allow cleartext HTTP for user-configured prompt and MCP URLs. These URLs can be fetched with caller-supplied headers or MCP credentials, so accepting Proposed hardening- if not normalized.startswith(("http://", "https://")):
- raise ValueError("prompt data source url must start with http:// or https://")
+ if not normalized.startswith("https://"):
+ raise ValueError("prompt data source url must start with https://")
...
- if not normalized.startswith(("http://", "https://")):
- raise ValueError("mcp server url must start with http:// or https://")
+ if not normalized.startswith("https://"):
+ raise ValueError("mcp server url must start with https://")Also applies to: 163-164 🧰 Tools🪛 Ruff (0.15.20)[warning] 93-93: Avoid specifying long messages outside the exception class (TRY003) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| 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"] | ||
| LLMProvider = Literal["openai", "anthropic", "zai"] | ||
|
|
||
|
|
||
| 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 +211,28 @@ 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, | ||
| "llm_provider": "anthropic", | ||
| "llm_model": "claude-opus-4-8", | ||
| "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 +262,37 @@ 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", | ||
| ) | ||
| llm_provider: Optional[LLMProvider] = Field( | ||
| None, | ||
| description="LLM provider for this bot. Defaults to LLM_PROVIDER, then openai.", | ||
| ) | ||
| llm_model: Optional[str] = Field( | ||
| None, | ||
| min_length=1, | ||
| max_length=120, | ||
| description="Provider model for this bot. Defaults to provider-specific env vars.", | ||
| ) | ||
| 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 | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Snapshot field list omits
llm_provider/llm_model.The "current
BotRequestfields" list mentionsprompt_data_sources,prompt_data_token_limit,mcp, andspeech_speed, but notllm_provider/llm_model, even though LLM provider routing is this cohort's headline feature and both fields are present in the generated schema.📝 Proposed fix
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`. +`BotRequest` fields for `prompt_data_sources`, `prompt_data_token_limit`, `mcp`, +`llm_provider`, `llm_model`, and `speech_speed`.📝 Committable suggestion
🤖 Prompt for AI Agents