From cb75ab3a43ef96064d62239bf052c04e08868cce Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Thu, 26 Feb 2026 17:38:12 -0800 Subject: [PATCH 01/15] Introduce MCP tools --- src/fuser/__init__.py | 6 + src/llm/function_schemas.py | 4 +- src/mcp_servers/__init__.py | 42 +++++++ src/mcp_servers/client.py | 212 ++++++++++++++++++++++++++++++++ src/mcp_servers/orchestrator.py | 167 +++++++++++++++++++++++++ src/runtime/config.py | 9 ++ src/runtime/converter.py | 1 + src/runtime/cortex.py | 16 +++ 8 files changed, 456 insertions(+), 1 deletion(-) create mode 100644 src/mcp_servers/__init__.py create mode 100644 src/mcp_servers/client.py create mode 100644 src/mcp_servers/orchestrator.py diff --git a/src/fuser/__init__.py b/src/fuser/__init__.py index 553900e82..2779452d0 100644 --- a/src/fuser/__init__.py +++ b/src/fuser/__init__.py @@ -133,6 +133,12 @@ async def fuse( if desc: actions_fused += desc + "\n\n" + # descriptions of MCP tools + if self.config.mcp_servers: + mcp_descriptions = self.config.mcp_servers.get_tool_descriptions() + if mcp_descriptions: + actions_fused += mcp_descriptions + "\n\n" + question_prompt = "What will you do? Actions:" # this is the final prompt: diff --git a/src/llm/function_schemas.py b/src/llm/function_schemas.py index 43b390502..28764b317 100644 --- a/src/llm/function_schemas.py +++ b/src/llm/function_schemas.py @@ -151,7 +151,9 @@ def convert_function_calls_to_actions(function_calls: list[dict]) -> list[Action else: args = function_args - if "action" in args and len(args) == 1: + if function_name.startswith("mcp_"): + action_value = json.dumps(args) if args else "" + elif "action" in args and len(args) == 1: action_value = args["action"] elif len(args) > 1: action_value = json.dumps(args) diff --git a/src/mcp_servers/__init__.py b/src/mcp_servers/__init__.py new file mode 100644 index 000000000..0cc5b0c5a --- /dev/null +++ b/src/mcp_servers/__init__.py @@ -0,0 +1,42 @@ +import asyncio +import logging +from typing import Dict, List + +import nest_asyncio + +from mcp_servers.client import MCPClientManager + +__all__ = ["MCPClientManager", "load_mcp"] + + +def load_mcp(server_configs: List[Dict]) -> MCPClientManager: + """Load and connect MCP servers. + + Parameters + ---------- + server_configs : list[dict] + MCP server configurations from config file. + + Returns + ------- + MCPClientManager + Connected MCP client. + """ + if not server_configs: + return MCPClientManager([]) + + try: + client = MCPClientManager(server_configs) + loop = asyncio.get_event_loop() + if loop.is_running(): + nest_asyncio.apply() + loop.run_until_complete(client.connect_all()) + else: + asyncio.run(client.connect_all()) + logging.info( + f"MCP client initialized with " f"{len(client.get_tool_schemas())} tools" + ) + return client + except Exception as e: + logging.error(f"Failed to initialize MCP client: {e}") + return MCPClientManager([]) diff --git a/src/mcp_servers/client.py b/src/mcp_servers/client.py new file mode 100644 index 000000000..54d311918 --- /dev/null +++ b/src/mcp_servers/client.py @@ -0,0 +1,212 @@ +import logging +from contextlib import AsyncExitStack +from dataclasses import dataclass +from typing import Any, Dict, List, Literal, Optional, Tuple, Union + +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client +from mcp.client.streamable_http import streamable_http_client +from pydantic import BaseModel, TypeAdapter + +logger = logging.getLogger(__name__) + + +class StdioServerConfig(BaseModel): + """Configuration for an MCP server using stdio transport.""" + + name: str + transport: Literal["stdio"] = "stdio" + command: str + args: List[str] = [] + env: Optional[Dict[str, str]] = None + + +class HttpServerConfig(BaseModel): + """Configuration for an MCP server using HTTP transport.""" + + name: str + transport: Literal["http"] + url: str + + +ServerConfig = Union[StdioServerConfig, HttpServerConfig] +_config_adapter = TypeAdapter(ServerConfig) + + +@dataclass +class MCPTool: + """Metadata for a single MCP tool.""" + + key: str + server_name: str + original_name: str + description: str + input_schema: dict + + def convert_to_schema(self) -> dict: + """Convert to OpenAI function-calling schema.""" + return { + "type": "function", + "function": { + "name": self.key, + "description": self.description, + "parameters": self.input_schema, + }, + } + + def generate_description(self) -> str: + """Generate description for LLM prompts.""" + params = self.input_schema.get("properties", {}) + param_str = ", ".join( + f"{param_name}: {param_info.get('type', 'any')}" + for param_name, param_info in params.items() + ) + return ( + f"MCP TOOL: {self.key}({param_str})\n" + f"Description: {self.description}\n" + f"Use this tool when you need to get external information. " + f"Call it first, then use the result to respond.\n" + ) + + +class StdioTransport: + """Create a stdio transport connection.""" + + @staticmethod + async def connect( + exit_stack: AsyncExitStack, config: StdioServerConfig + ) -> Tuple[Any, Any]: + """Open a stdio connection to an MCP server.""" + server_params = StdioServerParameters( + command=config.command, + args=config.args, + env=config.env, + ) + client_cm = stdio_client(server_params) + read, write = await exit_stack.enter_async_context(client_cm) + return read, write + + +class HttpTransport: + """Create an HTTP transport connection.""" + + @staticmethod + async def connect( + exit_stack: AsyncExitStack, config: HttpServerConfig + ) -> Tuple[Any, Any]: + """Open an HTTP connection to an MCP server.""" + client_cm = streamable_http_client(config.url) + read, write, _ = await exit_stack.enter_async_context(client_cm) + return read, write + + +_TRANSPORTS = { + "stdio": StdioTransport, + "http": HttpTransport, +} + + +class MCPClientManager: + """Manage connections to MCP servers and execute tool calls.""" + + def __init__(self, server_configs: List[Dict]): + self._configs = [_config_adapter.validate_python(c) for c in server_configs] + self._sessions: Dict[str, ClientSession] = {} + self._tools: Dict[str, MCPTool] = {} + self._exit_stack: Optional[AsyncExitStack] = None + + async def connect_all(self) -> None: + """Connect to all configured MCP servers and discover tools.""" + self._exit_stack = AsyncExitStack() + await self._exit_stack.__aenter__() + + for config in self._configs: + try: + await self._connect_server(config) + except Exception as e: + logger.error(f"Failed to connect to MCP server '{config.name}': {e}") + + async def _connect_server(self, config: ServerConfig) -> None: + """Connect to a single MCP server.""" + transport = _TRANSPORTS.get(config.transport) + if not transport: + raise ValueError(f"Unsupported MCP transport: {config.transport}") + + read, write = await transport.connect(self._exit_stack, config) + + session = ClientSession(read, write) + await self._exit_stack.enter_async_context(session) + await session.initialize() + + # Discover tools + tools_result = await session.list_tools() + self._sessions[config.name] = session + + for tool in tools_result.tools: + mcp_tool = MCPTool( + key=f"mcp_{config.name}_{tool.name}", + server_name=config.name, + original_name=tool.name, + description=tool.description or f"MCP tool: {tool.name}", + input_schema=tool.inputSchema or {"type": "object", "properties": {}}, + ) + self._tools[mcp_tool.key] = mcp_tool + + logger.info( + f"MCP server '{config.name}': {len(tools_result.tools)} tools " + f"({[t.name for t in tools_result.tools]})" + ) + + def get_tool_schemas(self) -> List[Dict]: + """Get OpenAI-format function schemas for all MCP tools.""" + return [tool.convert_to_schema() for tool in self._tools.values()] + + def get_tool_descriptions(self) -> str: + """Get text descriptions of MCP tools for the LLM prompt.""" + if not self._tools: + return "" + return "\n".join(tool.generate_description() for tool in self._tools.values()) + + def is_mcp_tool(self, tool_name: str) -> bool: + """Check if a tool name belongs to an MCP server.""" + return tool_name in self._tools + + async def call_tool(self, tool_key: str, arguments: Dict[str, Any]) -> str: + """Call an MCP tool and return the text result. + + Parameters + ---------- + tool_key : str + Tool key in format 'mcp_{server}_{tool_name}'. + arguments : dict + Arguments to pass to the MCP tool. + + Returns + ------- + str + Text result from the tool. + """ + tool = self._tools.get(tool_key) + if not tool: + raise ValueError(f"Unknown MCP tool: {tool_key}") + + session = self._sessions[tool.server_name] + result = await session.call_tool(tool.original_name, arguments=arguments) + + texts = [] + for content in result.content: + if hasattr(content, "text"): + texts.append(content.text) + + return "\n".join(texts) if texts else str(result.content) + + async def close_all(self) -> None: + """Close all MCP server connections.""" + if self._exit_stack: + try: + await self._exit_stack.aclose() + except Exception as e: + logger.error(f"Error closing MCP connections: {e}") + self._exit_stack = None + self._sessions.clear() + self._tools.clear() diff --git a/src/mcp_servers/orchestrator.py b/src/mcp_servers/orchestrator.py new file mode 100644 index 000000000..78161ebeb --- /dev/null +++ b/src/mcp_servers/orchestrator.py @@ -0,0 +1,167 @@ +import asyncio +import json +import logging +from dataclasses import dataclass +from typing import Any, Dict, List + +from llm.output_model import CortexOutputModel +from mcp_servers.client import MCPClientManager + +logger = logging.getLogger(__name__) + + +@dataclass +class ToolResult: + """Result from a single MCP tool execution.""" + + tool_key: str + success: bool + content: str + + +class MCPOrchestrator: + """Orchestrate MCP tool execution between LLM and action dispatch. + + Intercepts MCP tool calls, executes them, and re-calls the LLM with results. + + Parameters + ---------- + mcp_client : MCPClientManager + Connected MCP client with tool schemas. + llm : Any + The LLM instance (used to inject tool schemas). + max_concurrency : int + Maximum number of MCP tools to execute concurrently. + """ + + def __init__( + self, + mcp_client: MCPClientManager, + llm: Any, + max_concurrency: int = 5, + ): + self._mcp_client = mcp_client + self._max_concurrency = max_concurrency + + mcp_schemas = mcp_client.get_tool_schemas() + llm.function_schemas.extend(mcp_schemas) + logger.info(f"MCPOrchestrator initialized with {len(mcp_schemas)} tools") + + async def process(self, output: Any, prompt: str, llm: Any) -> Any: + """Process LLM output, execute MCP tools if needed. + + Parameters + ---------- + output : CortexOutputModel + The LLM's output containing actions. + prompt : str + The original prompt (used for re-calling LLM). + llm : Any + The LLM instance for follow-up inference. + + Returns + ------- + CortexOutputModel + Final output with merged actions. + """ + if output is None or not hasattr(output, "actions"): + return output + + mcp_actions = self._get_mcp_actions(output.actions) + + if not mcp_actions: + return output + + # Preserve OM1 actions to avoid actions loss + om1_actions = [ + a for a in output.actions if not self._mcp_client.is_mcp_tool(a.type) + ] + + logger.info( + f"MCP: executing {len(mcp_actions)} tool(s), preserving {len(om1_actions)} OM1 action(s)" + ) + + results = await self._execute_tools(mcp_actions) + second_output = await self._recall_llm(llm, prompt, results) + + if second_output is None or not hasattr(second_output, "actions"): + return CortexOutputModel(actions=om1_actions) if om1_actions else output + + merged = om1_actions + second_output.actions + return CortexOutputModel(actions=merged) + + def _get_mcp_actions(self, actions: list) -> list: + """Extract MCP tool calls from action list.""" + return [a for a in actions if self._mcp_client.is_mcp_tool(a.type)] + + def _parse_arguments(self, action: Any) -> Dict[str, Any]: + """Extract tool arguments from an action's value.""" + value = action.value + + if isinstance(value, dict): + return value + + if isinstance(value, str): + try: + parsed = json.loads(value) + if isinstance(parsed, dict): + return parsed + except (json.JSONDecodeError, TypeError): + pass + return {"action": value} + + return {"action": str(value)} + + async def _execute_single_tool(self, action: Any) -> ToolResult: + """Execute a single MCP tool call with error handling.""" + try: + args = self._parse_arguments(action) + content = await self._mcp_client.call_tool(action.type, args) + logger.info(f"MCP tool {action.type} returned: {content}") + return ToolResult(tool_key=action.type, success=True, content=content) + except Exception as e: + logger.error(f"Error calling {action.type}: {e}") + return ToolResult( + tool_key=action.type, + success=False, + content=f"Error: {e}", + ) + + async def _execute_tools(self, actions: list) -> List[ToolResult]: + """Execute multiple MCP tools concurrently.""" + semaphore = asyncio.Semaphore(self._max_concurrency) + + async def _guarded(action: Any) -> ToolResult: + async with semaphore: + return await self._execute_single_tool(action) + + return await asyncio.gather(*(_guarded(a) for a in actions)) + + def _build_result_prompt( + self, original_prompt: str, results: List[ToolResult] + ) -> str: + """Build a follow-up prompt that includes tool results.""" + lines = [] + for r in results: + status = "OK" if r.success else "FAILED" + lines.append(f"[{r.tool_key}] ({status}): {r.content}") + + result_block = "\n".join(lines) + return ( + f"{original_prompt}\n\n" + f"TOOL RESULTS:\n{result_block}\n\n" + f"Based on the tool results above, respond using the speak action " + f"to tell the user the information. Summarize concisely." + ) + + async def _recall_llm( + self, llm: Any, prompt: str, results: List[ToolResult] + ) -> Any: + """Re-call the LLM with tool results to generate the final response.""" + new_prompt = self._build_result_prompt(prompt, results) + logger.info("MCP execution complete, recall LLM") + return await llm.ask(new_prompt) + + async def close(self) -> None: + """Close underlying MCP server connections.""" + await self._mcp_client.close_all() diff --git a/src/runtime/config.py b/src/runtime/config.py index 6d9c8e0c6..c38ee58e9 100644 --- a/src/runtime/config.py +++ b/src/runtime/config.py @@ -16,6 +16,7 @@ from inputs import load_input from inputs.base import Sensor from llm import LLM, load_llm +from mcp_servers import load_mcp from runtime.converter import convert_to_multi_mode from runtime.env import load_env_vars from runtime.hook import ( @@ -156,6 +157,7 @@ class RuntimeConfig: action_execution_mode: Optional[str] = None action_dependencies: Optional[Dict[str, List[str]]] = None knowledge_base: Optional[Dict[str, Any]] = None + mcp_servers: Optional[Any] = None def add_meta( @@ -327,6 +329,7 @@ class ModeConfig: simulators: List[Simulator] = field(default_factory=list) agent_actions: List[AgentAction] = field(default_factory=list) backgrounds: List[Background] = field(default_factory=list) + mcp_servers: Optional[Any] = None action_execution_mode: Optional[str] = None action_dependencies: Optional[Dict[str, List[str]]] = None @@ -336,6 +339,7 @@ class ModeConfig: _raw_simulators: List[Dict] = field(default_factory=list) _raw_actions: List[Dict] = field(default_factory=list) _raw_backgrounds: List[Dict] = field(default_factory=list) + _raw_mcp_servers: List[Dict] = field(default_factory=list) def to_runtime_config(self, global_config: "ModeSystemConfig") -> RuntimeConfig: """ @@ -374,6 +378,7 @@ def to_runtime_config(self, global_config: "ModeSystemConfig") -> RuntimeConfig: action_execution_mode=self.action_execution_mode, action_dependencies=self.action_dependencies, knowledge_base=global_config.knowledge_base, + mcp_servers=self.mcp_servers, ) def load_components(self, system_config: "ModeSystemConfig"): @@ -616,6 +621,7 @@ def load_mode_config( _raw_actions=mode_data.get("agent_actions", []), _raw_backgrounds=mode_data.get("backgrounds", []), _raw_lifecycle_hooks=mode_data.get("lifecycle_hooks", []), + _raw_mcp_servers=mode_data.get("mcp_servers", []), ) mode_system_config.modes[mode_name] = mode_config @@ -745,6 +751,9 @@ def _load_mode_components(mode_config: ModeConfig, system_config: ModeSystemConf else: raise ValueError(f"No LLM configuration found for mode {mode_config.name}") + # Load MCP servers + mode_config.mcp_servers = load_mcp(mode_config._raw_mcp_servers) + def mode_config_to_dict(config: ModeSystemConfig) -> Dict[str, Any]: """ diff --git a/src/runtime/converter.py b/src/runtime/converter.py index 704f3eed1..3853e6954 100644 --- a/src/runtime/converter.py +++ b/src/runtime/converter.py @@ -114,6 +114,7 @@ def _build_mode_section(raw_config: dict) -> Dict: "action_execution_mode", "concurrent" ), "action_dependencies": raw_config.get("action_dependencies", {}), + "mcp_servers": raw_config.get("mcp_servers", []), } @staticmethod diff --git a/src/runtime/cortex.py b/src/runtime/cortex.py index 1296b1c48..f8e695c9d 100644 --- a/src/runtime/cortex.py +++ b/src/runtime/cortex.py @@ -8,6 +8,7 @@ from backgrounds.orchestrator import BackgroundOrchestrator from fuser import Fuser from inputs.orchestrator import InputOrchestrator +from mcp_servers.orchestrator import MCPOrchestrator from providers.config_provider import ConfigProvider from providers.io_provider import IOProvider from providers.sleep_ticker_provider import SleepTickerProvider @@ -90,6 +91,7 @@ def __init__( self.simulator_orchestrator: Optional[SimulatorOrchestrator] = None self.background_orchestrator: Optional[BackgroundOrchestrator] = None self.input_orchestrator: Optional[InputOrchestrator] = None + self.mcp_orchestrator: Optional[MCPOrchestrator] = None # Tasks for orchestrators self.input_listener_task: Optional[asyncio.Task] = None @@ -138,6 +140,9 @@ async def _initialize_mode(self, mode_name: str): self.action_orchestrator = ActionOrchestrator(self.current_config) self.simulator_orchestrator = SimulatorOrchestrator(self.current_config) self.background_orchestrator = BackgroundOrchestrator(self.current_config) + self.mcp_orchestrator = MCPOrchestrator( + self.current_config.mcp_servers, self.current_config.cortex_llm + ) logging.info(f"Mode '{mode_name}' initialized successfully") @@ -236,6 +241,10 @@ async def _stop_current_orchestrators(self) -> None: logging.debug("Stopping action orchestrator") self.action_orchestrator.stop() + if self.mcp_orchestrator: + logging.debug("Closing MCP connections") + await self.mcp_orchestrator.close() + tasks_to_cancel = {} if self.cortex_loop_task and not self.cortex_loop_task.done(): @@ -560,6 +569,13 @@ async def _tick(self) -> None: logging.debug("No output from LLM") return + output = await self.mcp_orchestrator.process( + output, prompt, self.current_config.cortex_llm + ) + if output is None: + logging.debug("No output from LLM after MCP processing") + return + if self._is_reloading: logging.debug("Skipping tick during config reload") return From f9ccdd805d1bc1d9074f63c9c1f008af1158af02 Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Fri, 27 Feb 2026 17:32:42 -0800 Subject: [PATCH 02/15] update orchestrator to support multi mcps --- src/mcp_servers/client.py | 4 +- src/mcp_servers/orchestrator.py | 226 +++++++++++++++++++++----------- src/runtime/cortex.py | 17 ++- 3 files changed, 161 insertions(+), 86 deletions(-) diff --git a/src/mcp_servers/client.py b/src/mcp_servers/client.py index 54d311918..95c670f23 100644 --- a/src/mcp_servers/client.py +++ b/src/mcp_servers/client.py @@ -6,6 +6,7 @@ from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from mcp.client.streamable_http import streamable_http_client +from mcp.types import TextContent from pydantic import BaseModel, TypeAdapter logger = logging.getLogger(__name__) @@ -135,6 +136,7 @@ async def _connect_server(self, config: ServerConfig) -> None: read, write = await transport.connect(self._exit_stack, config) session = ClientSession(read, write) + assert self._exit_stack is not None await self._exit_stack.enter_async_context(session) await session.initialize() @@ -195,7 +197,7 @@ async def call_tool(self, tool_key: str, arguments: Dict[str, Any]) -> str: texts = [] for content in result.content: - if hasattr(content, "text"): + if isinstance(content, TextContent): texts.append(content.text) return "\n".join(texts) if texts else str(result.content) diff --git a/src/mcp_servers/orchestrator.py b/src/mcp_servers/orchestrator.py index 78161ebeb..30c127880 100644 --- a/src/mcp_servers/orchestrator.py +++ b/src/mcp_servers/orchestrator.py @@ -2,7 +2,7 @@ import json import logging from dataclasses import dataclass -from typing import Any, Dict, List +from typing import Any, Dict, List, Set from llm.output_model import CortexOutputModel from mcp_servers.client import MCPClientManager @@ -19,20 +19,17 @@ class ToolResult: content: str -class MCPOrchestrator: - """Orchestrate MCP tool execution between LLM and action dispatch. +@dataclass +class RoundRecord: + """Record of a single orchestration round.""" - Intercepts MCP tool calls, executes them, and re-calls the LLM with results. + round_num: int + tools_called: List[str] + results: List[ToolResult] - Parameters - ---------- - mcp_client : MCPClientManager - Connected MCP client with tool schemas. - llm : Any - The LLM instance (used to inject tool schemas). - max_concurrency : int - Maximum number of MCP tools to execute concurrently. - """ + +class MCPOrchestrator: + """Orchestrate multi-round MCP tool execution.""" def __init__( self, @@ -44,63 +41,110 @@ def __init__( self._max_concurrency = max_concurrency mcp_schemas = mcp_client.get_tool_schemas() - llm.function_schemas.extend(mcp_schemas) - logger.info(f"MCPOrchestrator initialized with {len(mcp_schemas)} tools") - - async def process(self, output: Any, prompt: str, llm: Any) -> Any: - """Process LLM output, execute MCP tools if needed. - - Parameters - ---------- - output : CortexOutputModel - The LLM's output containing actions. - prompt : str - The original prompt (used for re-calling LLM). - llm : Any - The LLM instance for follow-up inference. - - Returns - ------- - CortexOutputModel - Final output with merged actions. - """ + base_schemas = [ + schema + for schema in llm.function_schemas + if not schema.get("function", {}).get("name", "").startswith("mcp_") + ] + llm.function_schemas = base_schemas + mcp_schemas + + logger.info( + f"MCP Orchestrator initialized with {len(mcp_schemas)} MCP tools, " + f"{len(base_schemas)} base tools" + ) + + async def process( + self, + output: Any, + prompt: str, + llm: Any, + dispatch_om1=None, + max_rounds: int = 5, + ) -> Any: + """Execute MCP tools in multi-round loop.""" if output is None or not hasattr(output, "actions"): return output - mcp_actions = self._get_mcp_actions(output.actions) + history: List[RoundRecord] = [] + succeeded_calls: Set[str] = set() + + for round_idx in range(max_rounds): + # Extract MCP actions from output + mcp_actions = self._extract_mcp_actions(output.actions) + if not mcp_actions: + break + + # Filter out duplicate actions in all rounds + new_actions = self._filter_new_actions(mcp_actions, succeeded_calls) + if not new_actions: + break + + # Extract OM1 actions from output + om1_actions = [ + action + for action in output.actions + if not self._mcp_client.is_mcp_tool(action.type) + ] + + # Start OM1 actions + if om1_actions and dispatch_om1: + await dispatch_om1(om1_actions) + + logger.info( + f"MCP round {round_idx + 1}/{max_rounds}: " + f"executing {len(new_actions)} tool(s)" + ) - if not mcp_actions: - return output + results = await self._execute_tools(new_actions) - # Preserve OM1 actions to avoid actions loss - om1_actions = [ - a for a in output.actions if not self._mcp_client.is_mcp_tool(a.type) - ] + for action, result in zip(new_actions, results): + if result.success: + succeeded_calls.add(self._build_call_signature(action)) - logger.info( - f"MCP: executing {len(mcp_actions)} tool(s), preserving {len(om1_actions)} OM1 action(s)" - ) + history.append( + RoundRecord( + round_num=round_idx + 1, + tools_called=[action.type for action in new_actions], + results=results, + ) + ) - results = await self._execute_tools(mcp_actions) - second_output = await self._recall_llm(llm, prompt, results) + output = await self._recall_llm(llm, prompt, history) - if second_output is None or not hasattr(second_output, "actions"): - return CortexOutputModel(actions=om1_actions) if om1_actions else output + if output is None or not hasattr(output, "actions"): + return None - merged = om1_actions + second_output.actions - return CortexOutputModel(actions=merged) + # If there are still mcp actions in the output after max_rounds, remove them + if output and hasattr(output, "actions"): + final_actions = [ + action + for action in output.actions + if not action.type.startswith("mcp_") + ] + return CortexOutputModel(actions=final_actions) + return output - def _get_mcp_actions(self, actions: list) -> list: - """Extract MCP tool calls from action list.""" - return [a for a in actions if self._mcp_client.is_mcp_tool(a.type)] + def _extract_mcp_actions(self, actions: list) -> list: + return [ + action for action in actions if self._mcp_client.is_mcp_tool(action.type) + ] + + def _filter_new_actions(self, actions: list, succeeded: Set[str]) -> list: + return [ + action + for action in actions + if self._build_call_signature(action) not in succeeded + ] + + def _build_call_signature(self, action: Any) -> str: + """Deterministic signature for dedup: tool_key + sorted args.""" + args = self._parse_arguments(action) + return f"{action.type}|{json.dumps(args, sort_keys=True, default=str)}" def _parse_arguments(self, action: Any) -> Dict[str, Any]: - """Extract tool arguments from an action's value.""" value = action.value - if isinstance(value, dict): return value - if isinstance(value, str): try: parsed = json.loads(value) @@ -109,59 +153,83 @@ def _parse_arguments(self, action: Any) -> Dict[str, Any]: except (json.JSONDecodeError, TypeError): pass return {"action": value} - return {"action": str(value)} - async def _execute_single_tool(self, action: Any) -> ToolResult: - """Execute a single MCP tool call with error handling.""" + async def _execute_single_tool( + self, action: Any, timeout: float = 10.0 + ) -> ToolResult: try: args = self._parse_arguments(action) - content = await self._mcp_client.call_tool(action.type, args) + content = await asyncio.wait_for( + self._mcp_client.call_tool(action.type, args), timeout=timeout + ) logger.info(f"MCP tool {action.type} returned: {content}") + + try: + parsed = json.loads(content) + if isinstance(parsed, dict) and "error" in parsed: + return ToolResult( + tool_key=action.type, success=False, content=content + ) + except (json.JSONDecodeError, TypeError): + pass + return ToolResult(tool_key=action.type, success=True, content=content) - except Exception as e: - logger.error(f"Error calling {action.type}: {e}") + except Exception as exc: + logger.error(f"Error calling {action.type}: {exc}") return ToolResult( tool_key=action.type, success=False, - content=f"Error: {e}", + content=f"Error: {exc}", ) async def _execute_tools(self, actions: list) -> List[ToolResult]: - """Execute multiple MCP tools concurrently.""" semaphore = asyncio.Semaphore(self._max_concurrency) async def _guarded(action: Any) -> ToolResult: async with semaphore: return await self._execute_single_tool(action) - return await asyncio.gather(*(_guarded(a) for a in actions)) + return await asyncio.gather(*(_guarded(action) for action in actions)) def _build_result_prompt( - self, original_prompt: str, results: List[ToolResult] + self, + original_prompt: str, + history: List[RoundRecord], ) -> str: - """Build a follow-up prompt that includes tool results.""" + """Build follow-up prompt.""" + # Tool results: concise, structured lines = [] - for r in results: - status = "OK" if r.success else "FAILED" - lines.append(f"[{r.tool_key}] ({status}): {r.content}") - + for record in history: + for result in record.results: + status = "OK" if result.success else "FAILED" + lines.append(f"[{result.tool_key}] {status}: {result.content}") result_block = "\n".join(lines) + return ( f"{original_prompt}\n\n" - f"TOOL RESULTS:\n{result_block}\n\n" - f"Based on the tool results above, respond using the speak action " - f"to tell the user the information. Summarize concisely." + f"[Tool Results]\n{result_block}\n\n" + f"[Next Step]\n" + f"Do NOT re-call any tool marked OK above. " + f"If all needed info is available, respond with speak. " + f"Otherwise call only the necessary tools in one batch.\n" ) async def _recall_llm( - self, llm: Any, prompt: str, results: List[ToolResult] + self, + llm: Any, + prompt: str, + history: List[RoundRecord], ) -> Any: - """Re-call the LLM with tool results to generate the final response.""" - new_prompt = self._build_result_prompt(prompt, results) - logger.info("MCP execution complete, recall LLM") - return await llm.ask(new_prompt) + """Recall LLM with tool results. Skips history to avoid pollution.""" + recall_prompt = self._build_result_prompt(prompt, history) + logger.info("MCP recall LLM with cumulative context") + llm._skip_state_management = True + try: + return await llm.ask(recall_prompt) + finally: + llm._skip_state_management = False async def close(self) -> None: - """Close underlying MCP server connections.""" + """Close all MCP client connections.""" await self._mcp_client.close_all() diff --git a/src/runtime/cortex.py b/src/runtime/cortex.py index f8e695c9d..46d2dd538 100644 --- a/src/runtime/cortex.py +++ b/src/runtime/cortex.py @@ -140,9 +140,10 @@ async def _initialize_mode(self, mode_name: str): self.action_orchestrator = ActionOrchestrator(self.current_config) self.simulator_orchestrator = SimulatorOrchestrator(self.current_config) self.background_orchestrator = BackgroundOrchestrator(self.current_config) - self.mcp_orchestrator = MCPOrchestrator( - self.current_config.mcp_servers, self.current_config.cortex_llm - ) + if self.current_config.mcp_servers: + self.mcp_orchestrator = MCPOrchestrator( + self.current_config.mcp_servers, self.current_config.cortex_llm + ) logging.info(f"Mode '{mode_name}' initialized successfully") @@ -569,9 +570,13 @@ async def _tick(self) -> None: logging.debug("No output from LLM") return - output = await self.mcp_orchestrator.process( - output, prompt, self.current_config.cortex_llm - ) + if self.mcp_orchestrator: + output = await self.mcp_orchestrator.process( + output, + prompt, + self.current_config.cortex_llm, + dispatch_om1=self.action_orchestrator.promise, + ) if output is None: logging.debug("No output from LLM after MCP processing") return From ff17ac6e7157d38b6269bbe6cdb0c5a7314c135c Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Fri, 27 Feb 2026 17:54:25 -0800 Subject: [PATCH 03/15] update unit test --- tests/fuser/test_init.py | 1 + tests/mcp_servers/__init__.py | 0 tests/mcp_servers/test_orchestrator.py | 449 +++++++++++++++++++ tests/runtime/test_cortex.py | 112 +++++ tests/runtime/test_cortex_mode_transition.py | 8 + 5 files changed, 570 insertions(+) create mode 100644 tests/mcp_servers/__init__.py create mode 100644 tests/mcp_servers/test_orchestrator.py diff --git a/tests/fuser/test_init.py b/tests/fuser/test_init.py index 45d968736..5edee50c2 100644 --- a/tests/fuser/test_init.py +++ b/tests/fuser/test_init.py @@ -48,6 +48,7 @@ def create_mock_config( mock_config.system_prompt_examples = "system prompt examples" mock_config.agent_actions = agent_actions mock_config.knowledge_base = knowledge_base + mock_config.mcp_servers = None return mock_config diff --git a/tests/mcp_servers/__init__.py b/tests/mcp_servers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/mcp_servers/test_orchestrator.py b/tests/mcp_servers/test_orchestrator.py new file mode 100644 index 000000000..16106ea86 --- /dev/null +++ b/tests/mcp_servers/test_orchestrator.py @@ -0,0 +1,449 @@ +import asyncio +from typing import Any, Dict, List, Optional, Union + +import pytest + +from llm.output_model import Action, CortexOutputModel +from mcp_servers.orchestrator import MCPOrchestrator, RoundRecord, ToolResult + +# -- Mocks ------------------------------------------------------------------ + + +class MockMCPClient: + """Mock MCP client that tracks tool calls.""" + + def __init__( + self, tool_responses: Optional[Dict[str, Union[str, Exception]]] = None + ): + self._tools = {"mcp_weather_get", "mcp_slack_post", "mcp_maps_geocode"} + self._responses = tool_responses or {} + self.calls: List[tuple] = [] + + def get_tool_schemas(self) -> list: + return [ + { + "type": "function", + "function": {"name": name, "parameters": {}}, + } + for name in self._tools + ] + + def is_mcp_tool(self, tool_type: str) -> bool: + return tool_type in self._tools + + async def call_tool(self, tool_key: str, args: dict) -> str: + self.calls.append((tool_key, args)) + if tool_key in self._responses: + resp = self._responses[tool_key] + if isinstance(resp, Exception): + raise resp + return resp + return f'{{"ok":true,"tool":"{tool_key}"}}' + + async def close_all(self): + pass + + +class MockLLM: + """Mock LLM that returns predefined outputs per call.""" + + def __init__(self, responses: list): + self._responses = list(responses) + self._call_count = 0 + self.function_schemas: list = [] + self._skip_state_management = False + + async def ask(self, prompt: str) -> Any: + if self._call_count < len(self._responses): + resp = self._responses[self._call_count] + self._call_count += 1 + return resp + return None + + +# -- Fixtures --------------------------------------------------------------- + + +@pytest.fixture +def mock_client(): + return MockMCPClient() + + +@pytest.fixture +def make_output(): + """Factory for CortexOutputModel.""" + + def _make(actions: List[tuple]) -> CortexOutputModel: + return CortexOutputModel(actions=[Action(type=t, value=v) for t, v in actions]) + + return _make + + +# -- Tests ------------------------------------------------------------------ + + +class TestInit: + """Test MCPOrchestrator initialization.""" + + def test_extends_function_schemas(self, mock_client): + llm = MockLLM([]) + llm.function_schemas = [{"type": "function", "function": {"name": "speak"}}] + + MCPOrchestrator(mock_client, llm) + + names = [s["function"]["name"] for s in llm.function_schemas] + assert "speak" in names + assert len(names) == 1 + len(mock_client._tools) + + +class TestProcessNoMCP: + """Test process() when there are no MCP actions.""" + + @pytest.mark.asyncio + async def test_no_actions_returns_output(self, mock_client, make_output): + llm = MockLLM([]) + orch = MCPOrchestrator(mock_client, llm) + + output = make_output([("speak", "hello"), ("emotion", "happy")]) + result = await orch.process(output, "test prompt", llm) + + assert result is not None + assert len(result.actions) == 2 + assert mock_client.calls == [] + + @pytest.mark.asyncio + async def test_none_input_returns_none(self, mock_client): + llm = MockLLM([]) + orch = MCPOrchestrator(mock_client, llm) + + result = await orch.process(None, "test prompt", llm) + assert result is None + + +class TestProcessWithMCP: + """Test process() with MCP tool calls.""" + + @pytest.mark.asyncio + async def test_single_round_mcp(self, mock_client, make_output): + initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) + final = make_output([("speak", "73°F"), ("emotion", "happy")]) + + llm = MockLLM([final]) + orch = MCPOrchestrator(mock_client, llm) + + result = await orch.process(initial, "weather?", llm) + + assert len(mock_client.calls) == 1 + assert mock_client.calls[0][0] == "mcp_weather_get" + assert len(result.actions) == 2 + assert all(not a.type.startswith("mcp_") for a in result.actions) + + @pytest.mark.asyncio + async def test_multi_round_mcp(self, mock_client, make_output): + initial = make_output([("mcp_maps_geocode", '{"address":"SF"}')]) + round1 = make_output([("mcp_weather_get", '{"lat":37}')]) + final = make_output([("speak", "sunny")]) + + llm = MockLLM([round1, final]) + orch = MCPOrchestrator(mock_client, llm) + + result = await orch.process(initial, "weather?", llm) + + assert len(mock_client.calls) == 2 + assert mock_client.calls[0][0] == "mcp_maps_geocode" + assert mock_client.calls[1][0] == "mcp_weather_get" + assert result.actions[0].type == "speak" + + @pytest.mark.asyncio + async def test_strips_mcp_from_final(self, mock_client, make_output): + initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) + # LLM returns mix of MCP + OM1 actions, then pure OM1 + round1 = make_output( + [ + ("speak", "sunny"), + ("mcp_slack_post", '{"text":"hi"}'), + ("emotion", "happy"), + ] + ) + final = make_output([("speak", "done"), ("emotion", "happy")]) + + llm = MockLLM([round1, final]) + orch = MCPOrchestrator(mock_client, llm) + + result = await orch.process(initial, "test", llm) + + types = [a.type for a in result.actions] + assert "speak" in types + assert "emotion" in types + assert not any(t.startswith("mcp_") for t in types) + + @pytest.mark.asyncio + async def test_max_rounds_limit(self, mock_client, make_output): + """Process stops after max_rounds even if LLM keeps requesting MCP.""" + mcp_output = make_output([("mcp_weather_get", '{"city":"SF"}')]) + llm = MockLLM([mcp_output, mcp_output, mcp_output, mcp_output, mcp_output]) + orch = MCPOrchestrator(mock_client, llm) + + await orch.process(mcp_output, "test", llm, max_rounds=3) + + assert len(mock_client.calls) <= 3 + + +class TestDeduplication: + """Test that identical tool+args calls are skipped.""" + + @pytest.mark.asyncio + async def test_skips_duplicate_calls(self, mock_client, make_output): + initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) + # LLM requests the exact same tool again + duplicate = make_output([("mcp_weather_get", '{"city":"SF"}')]) + final = make_output([("speak", "sunny")]) + + llm = MockLLM([duplicate, final]) + orch = MCPOrchestrator(mock_client, llm) + + await orch.process(initial, "test", llm) + + # Only called once, second was deduped + assert len(mock_client.calls) == 1 + + @pytest.mark.asyncio + async def test_different_args_not_deduped(self, mock_client, make_output): + initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) + different = make_output([("mcp_weather_get", '{"city":"LA"}')]) + final = make_output([("speak", "done")]) + + llm = MockLLM([different, final]) + orch = MCPOrchestrator(mock_client, llm) + + await orch.process(initial, "test", llm) + + assert len(mock_client.calls) == 2 + + +class TestErrorHandling: + """Test tool execution failure handling.""" + + @pytest.mark.asyncio + async def test_tool_exception_marked_failed(self, make_output): + client = MockMCPClient( + tool_responses={ + "mcp_weather_get": Exception("connection refused"), + } + ) + initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) + final = make_output([("speak", "sorry")]) + + llm = MockLLM([final]) + orch = MCPOrchestrator(client, llm) # type: ignore[arg-type] + + result = await orch.process(initial, "test", llm) + + assert result.actions[0].value == "sorry" + + @pytest.mark.asyncio + async def test_tool_timeout(self, make_output): + """Test that tool timeout is handled gracefully.""" + + async def slow_call(tool_key, args): + await asyncio.sleep(100) + return "never" + + client = MockMCPClient() + client.call_tool = slow_call + initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) + final = make_output([("speak", "timed out")]) + + llm = MockLLM([final]) + orch = MCPOrchestrator(client, llm) # type: ignore[arg-type] + + result = await orch.process(initial, "test", llm) + + assert result is not None + + @pytest.mark.asyncio + async def test_llm_returns_none(self, mock_client, make_output): + """LLM timeout returns None → process returns None.""" + initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) + + llm = MockLLM([None]) + orch = MCPOrchestrator(mock_client, llm) + + result = await orch.process(initial, "test", llm) + + assert result is None + + +class TestDispatchOM1: + """Test OM1 action dispatching during MCP rounds.""" + + @pytest.mark.asyncio + async def test_dispatches_om1_actions(self, mock_client, make_output): + initial = make_output( + [ + ("emotion", "think"), + ("mcp_weather_get", '{"city":"SF"}'), + ] + ) + final = make_output([("speak", "done")]) + + dispatched = [] + + async def mock_dispatch(actions): + dispatched.extend(actions) + + llm = MockLLM([final]) + orch = MCPOrchestrator(mock_client, llm) + + await orch.process(initial, "test", llm, dispatch_om1=mock_dispatch) + + assert len(dispatched) == 1 + assert dispatched[0].type == "emotion" + + +class TestSkipStateManagement: + """Test that recall_llm sets _skip_state_management flag.""" + + @pytest.mark.asyncio + async def test_flag_set_during_recall(self, mock_client, make_output): + initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) + final = make_output([("speak", "done")]) + + flags_during_ask = [] + + class TrackingLLM(MockLLM): + async def ask(self, prompt): + flags_during_ask.append(self._skip_state_management) + return await super().ask(prompt) + + llm = TrackingLLM([final]) + orch = MCPOrchestrator(mock_client, llm) + + await orch.process(initial, "test", llm) + + # The recall ask should have had _skip_state_management = True + assert flags_during_ask[-1] is True + + @pytest.mark.asyncio + async def test_flag_restored_after_recall(self, mock_client, make_output): + initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) + final = make_output([("speak", "done")]) + + llm = MockLLM([final]) + orch = MCPOrchestrator(mock_client, llm) + + await orch.process(initial, "test", llm) + + # Flag should be restored to False after process completes + assert llm._skip_state_management is False + + @pytest.mark.asyncio + async def test_flag_restored_on_error(self, mock_client, make_output): + initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) + + class FailingLLM(MockLLM): + async def ask(self, prompt): + if self._skip_state_management: + raise RuntimeError("LLM crashed") + return await super().ask(prompt) + + llm = FailingLLM([]) + orch = MCPOrchestrator(mock_client, llm) + + with pytest.raises(RuntimeError, match="LLM crashed"): + await orch.process(initial, "test", llm) + + # Flag must be restored even after exception + assert llm._skip_state_management is False + + +class TestBuildResultPrompt: + """Test _build_result_prompt output format.""" + + def test_includes_tool_results(self, mock_client): + llm = MockLLM([]) + orch = MCPOrchestrator(mock_client, llm) + + history = [ + RoundRecord( + round_num=1, + tools_called=["mcp_weather_get"], + results=[ToolResult("mcp_weather_get", True, '{"temp":73}')], + ) + ] + + prompt = orch._build_result_prompt("original", history) + + assert "original" in prompt + assert "mcp_weather_get" in prompt + assert '{"temp":73}' in prompt + assert "OK" in prompt + assert "3" in prompt + + def test_marks_failed_tools(self, mock_client): + llm = MockLLM([]) + orch = MCPOrchestrator(mock_client, llm) + + history = [ + RoundRecord( + round_num=1, + tools_called=["mcp_slack_post"], + results=[ToolResult("mcp_slack_post", False, "Error: timeout")], + ) + ] + + prompt = orch._build_result_prompt("original", history) + + assert "FAILED" in prompt + assert "Error: timeout" in prompt + + def test_succeeded_summary(self, mock_client): + llm = MockLLM([]) + orch = MCPOrchestrator(mock_client, llm) + + history = [ + RoundRecord( + round_num=1, + tools_called=["mcp_weather_get", "mcp_maps_geocode"], + results=[ + ToolResult("mcp_weather_get", True, "ok"), + ToolResult("mcp_maps_geocode", False, "error"), + ], + ) + ] + + prompt = orch._build_result_prompt("original", history) + + # OK tool should appear in results + assert "[mcp_weather_get] OK" in prompt + # FAILED tool should appear in results + assert "[mcp_maps_geocode] FAILED" in prompt + + +class TestParseArguments: + """Test _parse_arguments with various input formats.""" + + @pytest.fixture + def orch(self, mock_client): + llm = MockLLM([]) + return MCPOrchestrator(mock_client, llm) + + def test_json_string(self, orch): + action = Action(type="test", value='{"city": "SF", "units": "fahrenheit"}') + result = orch._parse_arguments(action) + assert result == {"city": "SF", "units": "fahrenheit"} + + def test_plain_string(self, orch): + action = Action(type="test", value="hello world") + result = orch._parse_arguments(action) + assert result == {"action": "hello world"} + + def test_json_array_fallback(self, orch): + action = Action(type="test", value='["a", "b"]') + result = orch._parse_arguments(action) + assert result == {"action": '["a", "b"]'} + + def test_empty_json(self, orch): + action = Action(type="test", value="{}") + result = orch._parse_arguments(action) + assert result == {} diff --git a/tests/runtime/test_cortex.py b/tests/runtime/test_cortex.py index a1af41c25..78bd493d1 100644 --- a/tests/runtime/test_cortex.py +++ b/tests/runtime/test_cortex.py @@ -145,16 +145,19 @@ async def test_initialize_mode(self, cortex_runtime, mock_mode_config): patch("runtime.cortex.ActionOrchestrator") as mock_action_class, patch("runtime.cortex.SimulatorOrchestrator") as mock_simulator_class, patch("runtime.cortex.BackgroundOrchestrator") as mock_background_class, + patch("runtime.cortex.MCPOrchestrator") as mock_mcp_class, ): mock_fuser = Mock() mock_action_orch = Mock() mock_simulator_orch = Mock() mock_background_orch = Mock() + mock_mcp_orch = Mock() mock_fuser_class.return_value = mock_fuser mock_action_class.return_value = mock_action_orch mock_simulator_class.return_value = mock_simulator_orch mock_background_class.return_value = mock_background_orch + mock_mcp_class.return_value = mock_mcp_orch runtime.mode_config.modes = {"test_mode": mock_mode_config} @@ -171,6 +174,115 @@ async def test_initialize_mode(self, cortex_runtime, mock_mode_config): assert runtime.action_orchestrator == mock_action_orch assert runtime.simulator_orchestrator == mock_simulator_orch assert runtime.background_orchestrator == mock_background_orch + assert runtime.mcp_orchestrator == mock_mcp_orch + + @pytest.mark.asyncio + async def test_initialize_mode_no_mcp_servers( + self, cortex_runtime, mock_mode_config + ): + """Test that mcp_orchestrator is None when mcp_servers is absent.""" + runtime, mocks = cortex_runtime + + with ( + patch("runtime.cortex.Fuser"), + patch("runtime.cortex.ActionOrchestrator"), + patch("runtime.cortex.SimulatorOrchestrator"), + patch("runtime.cortex.BackgroundOrchestrator"), + patch("runtime.cortex.MCPOrchestrator") as mock_mcp_class, + ): + mock_mode_config.to_runtime_config.return_value = Mock( + mcp_servers=None, + cortex_llm=Mock(), + ) + runtime.mode_config.modes = {"test_mode": mock_mode_config} + + await runtime._initialize_mode("test_mode") + + mock_mcp_class.assert_not_called() + assert runtime.mcp_orchestrator is None + + @pytest.mark.asyncio + async def test_tick_calls_mcp_process(self, cortex_runtime): + """Test that _tick calls mcp_orchestrator.process with correct args.""" + runtime, mocks = cortex_runtime + + mock_output = Mock() + mock_output.actions = [] + runtime.current_config = Mock() + runtime.current_config.hertz = 10.0 + runtime.current_config.cortex_llm = Mock() + runtime.current_config.cortex_llm.ask = AsyncMock(return_value=mock_output) + runtime.current_config.agent_inputs = [] + + runtime.fuser = Mock() + runtime.fuser.fuse = AsyncMock(return_value="test prompt") + runtime.action_orchestrator = Mock() + runtime.action_orchestrator.flush_promises = AsyncMock(return_value=([], None)) + runtime.action_orchestrator.promise = AsyncMock() + runtime.mcp_orchestrator = Mock() + runtime.mcp_orchestrator.process = AsyncMock(return_value=mock_output) + + # Mock io_provider with mode_transition_input context manager + ctx = Mock() + ctx.__enter__ = Mock(return_value=None) + ctx.__exit__ = Mock(return_value=False) + runtime.io_provider = Mock() + runtime.io_provider.mode_transition_input = Mock(return_value=ctx) + + runtime.mode_manager = Mock() + runtime.mode_manager.process_tick = AsyncMock(return_value=None) + + runtime._pending_mode_transition = None + runtime._mode_transition_event = Mock() + runtime._mode_transition_event.set = Mock() + + await runtime._tick() + + runtime.mcp_orchestrator.process.assert_called_once_with( + mock_output, + "test prompt", + runtime.current_config.cortex_llm, + dispatch_om1=runtime.action_orchestrator.promise, + ) + + @pytest.mark.asyncio + async def test_tick_skips_mcp_when_none(self, cortex_runtime): + """Test that _tick works normally when mcp_orchestrator is None.""" + runtime, mocks = cortex_runtime + + mock_output = Mock() + mock_output.actions = [] + runtime.current_config = Mock() + runtime.current_config.hertz = 10.0 + runtime.current_config.cortex_llm = Mock() + runtime.current_config.cortex_llm.ask = AsyncMock(return_value=mock_output) + runtime.current_config.agent_inputs = [] + + runtime.fuser = Mock() + runtime.fuser.fuse = AsyncMock(return_value="test prompt") + runtime.action_orchestrator = Mock() + runtime.action_orchestrator.flush_promises = AsyncMock(return_value=([], None)) + runtime.action_orchestrator.promise = AsyncMock() + runtime.mcp_orchestrator = None + + # Mock io_provider with mode_transition_input context manager + ctx = Mock() + ctx.__enter__ = Mock(return_value=None) + ctx.__exit__ = Mock(return_value=False) + runtime.io_provider = Mock() + runtime.io_provider.mode_transition_input = Mock(return_value=ctx) + + runtime.mode_manager = Mock() + runtime.mode_manager.process_tick = AsyncMock(return_value=None) + + runtime._pending_mode_transition = None + runtime._mode_transition_event = Mock() + runtime._mode_transition_event.set = Mock() + + await runtime._tick() + + # Should still reach action_orchestrator.promise + runtime.action_orchestrator.promise.assert_called_once() @pytest.mark.asyncio async def test_on_mode_transition(self, cortex_runtime): diff --git a/tests/runtime/test_cortex_mode_transition.py b/tests/runtime/test_cortex_mode_transition.py index adb370047..20d287b31 100644 --- a/tests/runtime/test_cortex_mode_transition.py +++ b/tests/runtime/test_cortex_mode_transition.py @@ -210,6 +210,8 @@ def cortex_runtime_with_mode_transition( runtime.action_orchestrator.promise = AsyncMock() runtime.simulator_orchestrator = Mock() runtime.simulator_orchestrator.promise = AsyncMock() + runtime.mcp_orchestrator = Mock() + runtime.mcp_orchestrator.process = AsyncMock() return runtime, { "mode_manager": mock_mode_manager, @@ -250,6 +252,8 @@ def cortex_runtime(mock_system_config, mock_io_provider, mock_mode_manager): runtime.action_orchestrator.flush_promises = AsyncMock(return_value=([], None)) runtime.action_orchestrator.promise = AsyncMock() runtime.simulator_orchestrator = None + runtime.mcp_orchestrator = Mock() + runtime.mcp_orchestrator.process = AsyncMock() runtime._pending_mode_transition = None runtime._mode_transition_event = Mock() @@ -324,6 +328,7 @@ async def test_tick_with_no_mode_transition_input_continues_normally( runtime._mode_transition_event.set.assert_not_called() runtime.action_orchestrator.promise.assert_called_once() + runtime.mcp_orchestrator.process.assert_called_once() @pytest.mark.asyncio @@ -347,6 +352,7 @@ async def test_tick_with_unrecognized_input_continues_normally( runtime._mode_transition_event.set.assert_not_called() runtime.action_orchestrator.promise.assert_called_once() + runtime.mcp_orchestrator.process.assert_called_once() @pytest.mark.asyncio @@ -715,6 +721,7 @@ async def test_no_mode_transition_input_continues_normal_processing(cortex_runti runtime.current_config.cortex_llm.ask.assert_called_once_with("test prompt") runtime.action_orchestrator.promise.assert_called_once() + runtime.mcp_orchestrator.process.assert_called_once() @pytest.mark.asyncio @@ -744,6 +751,7 @@ async def test_unrecognized_input_does_not_trigger_transition(cortex_runtime): runtime._mode_transition_event.set.assert_not_called() runtime.action_orchestrator.promise.assert_called_once() + runtime.mcp_orchestrator.process.assert_called_once() @pytest.mark.asyncio From 0b7d4f9b0a341ceb12f49ba504e6a0f8cc3a2268 Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Fri, 27 Feb 2026 18:02:34 -0800 Subject: [PATCH 04/15] add mcp dependencies --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 1fa1213b4..39f5a6910 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ "nest-asyncio==1.6.0", "tf-keras==2.18.0", "faiss-cpu>=1.7.4", + "mcp>=1.26.0", ] [project.optional-dependencies] From 9dd278a2bb15085f05051409294f73e3a4fa6d57 Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Mon, 2 Mar 2026 11:30:35 -0800 Subject: [PATCH 05/15] add doc string and use logging --- src/mcp_servers/client.py | 18 +- src/mcp_servers/orchestrator.py | 66 +++++-- tests/mcp_servers/test_client.py | 257 +++++++++++++++++++++++++ tests/mcp_servers/test_init.py | 82 ++++++++ tests/mcp_servers/test_orchestrator.py | 8 - 5 files changed, 405 insertions(+), 26 deletions(-) create mode 100644 tests/mcp_servers/test_client.py create mode 100644 tests/mcp_servers/test_init.py diff --git a/src/mcp_servers/client.py b/src/mcp_servers/client.py index 95c670f23..859df4234 100644 --- a/src/mcp_servers/client.py +++ b/src/mcp_servers/client.py @@ -9,8 +9,6 @@ from mcp.types import TextContent from pydantic import BaseModel, TypeAdapter -logger = logging.getLogger(__name__) - class StdioServerConfig(BaseModel): """Configuration for an MCP server using stdio transport.""" @@ -108,9 +106,15 @@ async def connect( class MCPClientManager: - """Manage connections to MCP servers and execute tool calls.""" + """Manage connections to multiple MCP servers and execute tool calls. + + Parameters + ---------- + server_configs : list of dict + Raw configuration dicts. + """ - def __init__(self, server_configs: List[Dict]): + def __init__(self, server_configs: List[Dict]) -> None: self._configs = [_config_adapter.validate_python(c) for c in server_configs] self._sessions: Dict[str, ClientSession] = {} self._tools: Dict[str, MCPTool] = {} @@ -125,7 +129,7 @@ async def connect_all(self) -> None: try: await self._connect_server(config) except Exception as e: - logger.error(f"Failed to connect to MCP server '{config.name}': {e}") + logging.error(f"Failed to connect to MCP server '{config.name}': {e}") async def _connect_server(self, config: ServerConfig) -> None: """Connect to a single MCP server.""" @@ -154,7 +158,7 @@ async def _connect_server(self, config: ServerConfig) -> None: ) self._tools[mcp_tool.key] = mcp_tool - logger.info( + logging.info( f"MCP server '{config.name}': {len(tools_result.tools)} tools " f"({[t.name for t in tools_result.tools]})" ) @@ -208,7 +212,7 @@ async def close_all(self) -> None: try: await self._exit_stack.aclose() except Exception as e: - logger.error(f"Error closing MCP connections: {e}") + logging.error(f"Error closing MCP connections: {e}") self._exit_stack = None self._sessions.clear() self._tools.clear() diff --git a/src/mcp_servers/orchestrator.py b/src/mcp_servers/orchestrator.py index 30c127880..441a11b6d 100644 --- a/src/mcp_servers/orchestrator.py +++ b/src/mcp_servers/orchestrator.py @@ -7,8 +7,6 @@ from llm.output_model import CortexOutputModel from mcp_servers.client import MCPClientManager -logger = logging.getLogger(__name__) - @dataclass class ToolResult: @@ -29,14 +27,28 @@ class RoundRecord: class MCPOrchestrator: - """Orchestrate multi-round MCP tool execution.""" + """Orchestrate multi-round MCP tool execution. + + Manages the lifecycle of MCP tool calls within a single LLM tick, + executing tools in batches and recalling the LLM with results until + no more MCP actions are requested or the round limit is reached. + + Parameters + ---------- + mcp_client : MCPClientManager + The client manager for MCP server connections. + llm : Any + The LLM instance whose function_schemas will be extended. + max_concurrency : int + Maximum number of concurrent tool executions per round. + """ def __init__( self, mcp_client: MCPClientManager, llm: Any, max_concurrency: int = 5, - ): + ) -> None: self._mcp_client = mcp_client self._max_concurrency = max_concurrency @@ -48,7 +60,7 @@ def __init__( ] llm.function_schemas = base_schemas + mcp_schemas - logger.info( + logging.info( f"MCP Orchestrator initialized with {len(mcp_schemas)} MCP tools, " f"{len(base_schemas)} base tools" ) @@ -61,7 +73,34 @@ async def process( dispatch_om1=None, max_rounds: int = 5, ) -> Any: - """Execute MCP tools in multi-round loop.""" + """Execute MCP tools in a multi-round loop. + + Extracts MCP actions from the LLM output, executes them + concurrently, and recalls the LLM with results. Repeats until + no MCP actions remain or ``max_rounds`` is reached. + + Parameters + ---------- + output : Any + The initial LLM output containing actions. + prompt : str + The original user prompt for LLM recall. + llm : Any + The LLM instance to recall with tool results. + dispatch_om1 : callable, optional + Dispatch non-MCP (OM1) actions immediately. + Because MCP rounds are sometimes serially dependent, + OM1 actions from earlier rounds cannot be carried + over to the final output. This callback ensures + they are dispatched. + max_rounds : int + Maximum number of tool-execution rounds. + + Returns + ------- + Any + Final LLM output with MCP actions removed. + """ if output is None or not hasattr(output, "actions"): return output @@ -90,7 +129,7 @@ async def process( if om1_actions and dispatch_om1: await dispatch_om1(om1_actions) - logger.info( + logging.info( f"MCP round {round_idx + 1}/{max_rounds}: " f"executing {len(new_actions)} tool(s)" ) @@ -125,11 +164,13 @@ async def process( return output def _extract_mcp_actions(self, actions: list) -> list: + """Return only the actions that target an MCP tool.""" return [ action for action in actions if self._mcp_client.is_mcp_tool(action.type) ] def _filter_new_actions(self, actions: list, succeeded: Set[str]) -> list: + """Filter out actions whose call signature already succeeded.""" return [ action for action in actions @@ -142,6 +183,7 @@ def _build_call_signature(self, action: Any) -> str: return f"{action.type}|{json.dumps(args, sort_keys=True, default=str)}" def _parse_arguments(self, action: Any) -> Dict[str, Any]: + """Parse the action value into a dict suitable for MCP tool args.""" value = action.value if isinstance(value, dict): return value @@ -158,12 +200,13 @@ def _parse_arguments(self, action: Any) -> Dict[str, Any]: async def _execute_single_tool( self, action: Any, timeout: float = 10.0 ) -> ToolResult: + """Execute one MCP tool call with a timeout.""" try: args = self._parse_arguments(action) content = await asyncio.wait_for( self._mcp_client.call_tool(action.type, args), timeout=timeout ) - logger.info(f"MCP tool {action.type} returned: {content}") + logging.info(f"MCP tool {action.type} returned: {content}") try: parsed = json.loads(content) @@ -176,7 +219,7 @@ async def _execute_single_tool( return ToolResult(tool_key=action.type, success=True, content=content) except Exception as exc: - logger.error(f"Error calling {action.type}: {exc}") + logging.error(f"Error calling {action.type}: {exc}") return ToolResult( tool_key=action.type, success=False, @@ -184,6 +227,7 @@ async def _execute_single_tool( ) async def _execute_tools(self, actions: list) -> List[ToolResult]: + """Execute multiple MCP tools concurrently with a semaphore.""" semaphore = asyncio.Semaphore(self._max_concurrency) async def _guarded(action: Any) -> ToolResult: @@ -197,7 +241,7 @@ def _build_result_prompt( original_prompt: str, history: List[RoundRecord], ) -> str: - """Build follow-up prompt.""" + """Build the follow-up prompt containing tool results.""" # Tool results: concise, structured lines = [] for record in history: @@ -223,7 +267,7 @@ async def _recall_llm( ) -> Any: """Recall LLM with tool results. Skips history to avoid pollution.""" recall_prompt = self._build_result_prompt(prompt, history) - logger.info("MCP recall LLM with cumulative context") + logging.info("MCP recall LLM with cumulative context") llm._skip_state_management = True try: return await llm.ask(recall_prompt) diff --git a/tests/mcp_servers/test_client.py b/tests/mcp_servers/test_client.py new file mode 100644 index 000000000..9b083082b --- /dev/null +++ b/tests/mcp_servers/test_client.py @@ -0,0 +1,257 @@ +from unittest.mock import AsyncMock, Mock, patch + +import pytest + +from mcp_servers.client import ( + HttpServerConfig, + MCPClientManager, + MCPTool, + StdioServerConfig, +) + + +class TestMCPToolSchema: + """Test MCPTool schema generation.""" + + def test_convert_to_schema(self): + tool = MCPTool( + key="mcp_weather_get", + server_name="weather", + original_name="get", + description="Get weather", + input_schema={"type": "object", "properties": {"city": {"type": "string"}}}, + ) + schema = tool.convert_to_schema() + + assert schema["type"] == "function" + assert schema["function"]["name"] == "mcp_weather_get" + assert schema["function"]["description"] == "Get weather" + assert "city" in schema["function"]["parameters"]["properties"] + + def test_generate_description(self): + tool = MCPTool( + key="mcp_weather_get", + server_name="weather", + original_name="get", + description="Get weather", + input_schema={"type": "object", "properties": {"city": {"type": "string"}}}, + ) + desc = tool.generate_description() + + assert "mcp_weather_get" in desc + assert "city: string" in desc + assert "Get weather" in desc + + +class TestConfigParsing: + """Test server config validation.""" + + def test_stdio_config(self): + config = StdioServerConfig(name="test", command="python", args=["-m", "server"]) + assert config.transport == "stdio" + assert config.name == "test" + + def test_http_config(self): + config = HttpServerConfig( + name="test", transport="http", url="http://localhost:8080" + ) + assert config.transport == "http" + assert config.url == "http://localhost:8080" + + def test_client_manager_parses_configs(self): + configs = [ + {"name": "s1", "transport": "stdio", "command": "python", "args": []}, + {"name": "s2", "transport": "http", "url": "http://localhost:8080"}, + ] + manager = MCPClientManager(configs) + + assert len(manager._configs) == 2 + assert isinstance(manager._configs[0], StdioServerConfig) + assert isinstance(manager._configs[1], HttpServerConfig) + + def test_invalid_transport_raises(self): + with pytest.raises(Exception): + MCPClientManager([{"name": "bad", "transport": "grpc", "url": "x"}]) + + +class TestMCPClientManager: + """Test MCPClientManager methods.""" + + def _make_manager_with_tools(self): + """Create a manager with pre-populated tools (no real connection).""" + manager = MCPClientManager([]) + manager._tools = { + "mcp_weather_get": MCPTool( + key="mcp_weather_get", + server_name="weather", + original_name="get", + description="Get weather", + input_schema={ + "type": "object", + "properties": {"city": {"type": "string"}}, + }, + ), + "mcp_slack_post": MCPTool( + key="mcp_slack_post", + server_name="slack", + original_name="post", + description="Post message", + input_schema={ + "type": "object", + "properties": {"text": {"type": "string"}}, + }, + ), + } + return manager + + def test_get_tool_schemas(self): + manager = self._make_manager_with_tools() + schemas = manager.get_tool_schemas() + + assert len(schemas) == 2 + names = {s["function"]["name"] for s in schemas} + assert names == {"mcp_weather_get", "mcp_slack_post"} + + def test_get_tool_descriptions_empty(self): + manager = MCPClientManager([]) + assert manager.get_tool_descriptions() == "" + + def test_get_tool_descriptions_non_empty(self): + manager = self._make_manager_with_tools() + desc = manager.get_tool_descriptions() + + assert "mcp_weather_get" in desc + assert "mcp_slack_post" in desc + + def test_is_mcp_tool(self): + manager = self._make_manager_with_tools() + + assert manager.is_mcp_tool("mcp_weather_get") is True + assert manager.is_mcp_tool("mcp_slack_post") is True + assert manager.is_mcp_tool("speak") is False + assert manager.is_mcp_tool("unknown") is False + + @pytest.mark.asyncio + async def test_call_tool_returns_text(self): + from mcp.types import TextContent + + manager = self._make_manager_with_tools() + + mock_result = Mock() + mock_result.content = [ + TextContent(type="text", text="sunny 72°F"), + ] + + mock_session = AsyncMock() + mock_session.call_tool = AsyncMock(return_value=mock_result) + manager._sessions["weather"] = mock_session + + result = await manager.call_tool("mcp_weather_get", {"city": "SF"}) + + assert result == "sunny 72°F" + mock_session.call_tool.assert_called_once_with("get", arguments={"city": "SF"}) + + @pytest.mark.asyncio + async def test_call_tool_unknown_raises(self): + manager = MCPClientManager([]) + + with pytest.raises(ValueError, match="Unknown MCP tool"): + await manager.call_tool("mcp_nonexistent", {}) + + @pytest.mark.asyncio + async def test_close_all_clears_state(self): + manager = self._make_manager_with_tools() + manager._sessions = {"weather": Mock()} + manager._exit_stack = AsyncMock() + + await manager.close_all() + + assert manager._exit_stack is None + assert len(manager._sessions) == 0 + assert len(manager._tools) == 0 + + @pytest.mark.asyncio + async def test_close_all_handles_error(self): + manager = self._make_manager_with_tools() + manager._sessions = {"weather": Mock()} + mock_stack = AsyncMock() + mock_stack.aclose = AsyncMock(side_effect=Exception("close error")) + manager._exit_stack = mock_stack + + await manager.close_all() + + assert manager._exit_stack is None + assert len(manager._sessions) == 0 + + @pytest.mark.asyncio + async def test_close_all_noop_when_no_stack(self): + manager = MCPClientManager([]) + manager._exit_stack = None + + await manager.close_all() + + assert manager._exit_stack is None + + +class TestConnectAll: + """Test connect_all with mocked transports.""" + + @pytest.mark.asyncio + async def test_connect_discovers_tools(self): + mock_tool = Mock() + mock_tool.name = "get_weather" + mock_tool.description = "Get weather info" + mock_tool.inputSchema = { + "type": "object", + "properties": {"city": {"type": "string"}}, + } + + mock_session = AsyncMock() + mock_session.initialize = AsyncMock() + mock_session.list_tools = AsyncMock(return_value=Mock(tools=[mock_tool])) + + configs = [ + {"name": "weather", "transport": "stdio", "command": "python", "args": []}, + ] + manager = MCPClientManager(configs) + + with ( + patch("mcp_servers.client._TRANSPORTS") as mock_transports, + patch("mcp_servers.client.ClientSession", return_value=mock_session), + ): + mock_transport = AsyncMock() + mock_transport.connect = AsyncMock(return_value=("read", "write")) + mock_transports.get = Mock(return_value=mock_transport) + + await manager.connect_all() + + assert "mcp_weather_get_weather" in manager._tools + assert ( + manager._tools["mcp_weather_get_weather"].description == "Get weather info" + ) + + @pytest.mark.asyncio + async def test_connect_handles_server_failure(self): + configs = [ + {"name": "bad_server", "transport": "stdio", "command": "fail", "args": []}, + ] + manager = MCPClientManager(configs) + + with patch("mcp_servers.client._TRANSPORTS") as mock_transports: + mock_transport = AsyncMock() + mock_transport.connect = AsyncMock(side_effect=ConnectionError("refused")) + mock_transports.get = Mock(return_value=mock_transport) + + await manager.connect_all() + + assert len(manager._tools) == 0 + assert len(manager._sessions) == 0 + + @pytest.mark.asyncio + async def test_unsupported_transport_raises(self): + manager = MCPClientManager([]) + config = Mock() + config.transport = "grpc" + + with pytest.raises(ValueError, match="Unsupported MCP transport"): + await manager._connect_server(config) diff --git a/tests/mcp_servers/test_init.py b/tests/mcp_servers/test_init.py new file mode 100644 index 000000000..9b5282dc8 --- /dev/null +++ b/tests/mcp_servers/test_init.py @@ -0,0 +1,82 @@ +from unittest.mock import AsyncMock, MagicMock, patch + +from mcp_servers import load_mcp + + +class TestLoadMcp: + """Test the load_mcp factory function.""" + + def test_empty_configs_returns_empty_manager(self): + client = load_mcp([]) + + assert client.get_tool_schemas() == [] + assert client.get_tool_descriptions() == "" + + def test_connect_success(self): + configs = [ + {"name": "test", "transport": "stdio", "command": "echo", "args": []}, + ] + + mock_client = MagicMock() + mock_client.get_tool_schemas.return_value = [{"type": "function"}] + mock_client.connect_all = AsyncMock() + + with ( + patch("mcp_servers.MCPClientManager", return_value=mock_client), + patch("asyncio.get_event_loop") as mock_loop, + ): + mock_loop.return_value.is_running.return_value = False + + with patch("asyncio.run", new_callable=MagicMock) as mock_run: + result = load_mcp(configs) + + assert result == mock_client + + def test_connect_failure_returns_empty_manager(self): + configs = [ + {"name": "bad", "transport": "stdio", "command": "fail", "args": []}, + ] + + from mcp_servers.client import MCPClientManager as RealManager + + mock_client = MagicMock() + fallback_client = RealManager([]) + + with ( + patch( + "mcp_servers.MCPClientManager", + side_effect=[mock_client, fallback_client], + ), + patch("asyncio.get_event_loop") as mock_loop, + ): + mock_loop.return_value.is_running.return_value = False + + with patch("asyncio.run", side_effect=Exception("connection failed")): + result = load_mcp(configs) + + assert result.get_tool_schemas() == [] + assert result.is_mcp_tool("anything") is False + + def test_connect_with_running_loop_uses_nest_asyncio(self): + configs = [ + {"name": "test", "transport": "stdio", "command": "echo", "args": []}, + ] + + mock_client = MagicMock() + mock_client.get_tool_schemas.return_value = [] + mock_client.connect_all = AsyncMock() + + with ( + patch("mcp_servers.MCPClientManager", return_value=mock_client), + patch("mcp_servers.nest_asyncio") as mock_nest, + patch("asyncio.get_event_loop") as mock_loop, + ): + loop = MagicMock() + loop.is_running.return_value = True + mock_loop.return_value = loop + + result = load_mcp(configs) + + mock_nest.apply.assert_called_once() + loop.run_until_complete.assert_called_once() + assert result == mock_client diff --git a/tests/mcp_servers/test_orchestrator.py b/tests/mcp_servers/test_orchestrator.py index 16106ea86..0dc65aae0 100644 --- a/tests/mcp_servers/test_orchestrator.py +++ b/tests/mcp_servers/test_orchestrator.py @@ -6,8 +6,6 @@ from llm.output_model import Action, CortexOutputModel from mcp_servers.orchestrator import MCPOrchestrator, RoundRecord, ToolResult -# -- Mocks ------------------------------------------------------------------ - class MockMCPClient: """Mock MCP client that tracks tool calls.""" @@ -61,9 +59,6 @@ async def ask(self, prompt: str) -> Any: return None -# -- Fixtures --------------------------------------------------------------- - - @pytest.fixture def mock_client(): return MockMCPClient() @@ -79,9 +74,6 @@ def _make(actions: List[tuple]) -> CortexOutputModel: return _make -# -- Tests ------------------------------------------------------------------ - - class TestInit: """Test MCPOrchestrator initialization.""" From 34c8c9907229830390e89274dc004a9d057dde39 Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Mon, 2 Mar 2026 11:31:10 -0800 Subject: [PATCH 06/15] update uv lock --- uv.lock | 556 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 424 insertions(+), 132 deletions(-) diff --git a/uv.lock b/uv.lock index f57137a0e..c88911819 100644 --- a/uv.lock +++ b/uv.lock @@ -168,8 +168,8 @@ version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version == '3.12.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ @@ -193,8 +193,8 @@ dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version == '3.12.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126, upload-time = "2025-01-05T13:13:11.095Z" } wheels = [ @@ -529,7 +529,8 @@ dependencies = [ { name = "bip-utils" }, { name = "coincurve" }, { name = "cryptography" }, - { name = "pydantic" }, + { name = "pydantic", version = "2.11.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, { name = "pyjwt" }, { name = "python-dateutil" }, { name = "urllib3" }, @@ -1371,7 +1372,8 @@ dependencies = [ { name = "eth-rlp" }, { name = "eth-utils" }, { name = "hexbytes" }, - { name = "pydantic" }, + { name = "pydantic", version = "2.11.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, { name = "rlp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fa/c9/38a55928f6eef30c8c7144f00400988bc1950e10b89de09991b315ff9d98/eth_account-0.13.4.tar.gz", hash = "sha256:2e1f2de240bef3d9f3d8013656135d2a79b6be6d4e7885bce9cace4334a4a376", size = 931759, upload-time = "2024-09-25T20:27:03.202Z" } @@ -1440,8 +1442,8 @@ name = "eth-typing" version = "5.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/6f/ecd98de0b67eefc68e17f6979433534a63e11aac88adaae7dede0b694567/eth_typing-5.1.0.tar.gz", hash = "sha256:8581f212ee6252aaa285377a77620f6e5f6e16ac3f144c61f098fafd47967b1a", size = 21727, upload-time = "2025-01-08T19:01:01.752Z" } wheels = [ @@ -1509,10 +1511,11 @@ name = "fastapi" version = "0.115.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, + { name = "pydantic", version = "2.11.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, { name = "starlette" }, - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a2/b2/5a5dc4affdb6661dea100324e19a7721d5dc524b464fe8e366c093fd7d87/fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9", size = 295403, upload-time = "2025-01-30T14:06:41.138Z" } wheels = [ @@ -1901,6 +1904,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + [[package]] name = "identify" version = "2.6.6" @@ -2348,6 +2360,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/7d/6a8b31dd07ed856b3eae001c9129670ef75c4698fa1c2a6ac9f00a4a7054/matplotlib-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3809916157ba871bcdd33d3493acd7fe3037db5daa917ca6e77975a94cef779", size = 8590087, upload-time = "2025-02-27T19:19:46.709Z" }, ] +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic", version = "2.11.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -2693,6 +2732,7 @@ dependencies = [ { name = "json5" }, { name = "jsonschema" }, { name = "matplotlib" }, + { name = "mcp" }, { name = "nest-asyncio" }, { name = "numpy" }, { name = "om1-modules" }, @@ -2755,6 +2795,7 @@ requires-dist = [ { name = "json5", specifier = "==0.10.0" }, { name = "jsonschema", specifier = "==4.23.0" }, { name = "matplotlib", specifier = "==3.10.1" }, + { name = "mcp", specifier = ">=1.26.0" }, { name = "nest-asyncio", specifier = "==1.6.0" }, { name = "numpy", specifier = "==2.0.2" }, { name = "om1-modules", git = "https://github.com/OpenMind/om1-modules.git?rev=8502244d94e9592816f3cad9f3df61a903315b35" }, @@ -2823,11 +2864,12 @@ dependencies = [ { name = "distro" }, { name = "httpx" }, { name = "jiter" }, - { name = "pydantic" }, + { name = "pydantic", version = "2.11.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, { name = "sniffio" }, { name = "tqdm" }, - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4c/c4/a220c957aa4097f25498770c6eff8f3abd35934a8859e7a78928a8a70846/openai-1.60.1.tar.gz", hash = "sha256:beb1541dfc38b002bd629ab68b0d6fe35b870c5f4311d9bc4404d85af3214d5e", size = 348070, upload-time = "2025-01-24T19:06:02.687Z" } wheels = [ @@ -2865,8 +2907,8 @@ name = "optree" version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/3a/313dae3303d526c333259544e9196207d33a43f0768cdca45f8e69cdd8ba/optree-0.14.0.tar.gz", hash = "sha256:d2b4b8784f5c7651a899997c9d6d4cd814c4222cd450c76d1fa386b8f5728d61", size = 158834, upload-time = "2025-01-16T22:24:34.985Z" } wheels = [ @@ -3398,93 +3440,300 @@ wheels = [ [[package]] name = "pydantic" -version = "2.10.3" +version = "2.11.10" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "annotated-types", marker = "python_full_version == '3.11.*'" }, + { name = "pydantic-core", version = "2.33.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-inspection", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/54/ecab642b3bed45f7d5f59b38443dcb36ef50f85af192e6ece103dbfe9587/pydantic-2.11.10.tar.gz", hash = "sha256:dc280f0982fbda6c38fada4e476dc0a4f3aeaf9c6ad4c28df68a666ec3c61423", size = 788494, upload-time = "2025-10-04T10:40:41.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/1f/73c53fcbfb0b5a78f91176df41945ca466e71e9d9d836e5c522abda39ee7/pydantic-2.11.10-py3-none-any.whl", hash = "sha256:802a655709d49bd004c31e865ef37da30b540786a46bfce02333e0e24b5fe29a", size = 444823, upload-time = "2025-10-04T10:40:39.055Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'", + "python_full_version >= '3.12' and platform_python_implementation == 'PyPy' and sys_platform == 'win32'", + "(python_full_version >= '3.14' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", ] -sdist = { url = "https://files.pythonhosted.org/packages/45/0f/27908242621b14e649a84e62b133de45f84c255eecb350ab02979844a788/pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9", size = 786486, upload-time = "2024-12-03T15:59:02.347Z" } +dependencies = [ + { name = "annotated-types", marker = "python_full_version != '3.11.*'" }, + { name = "pydantic-core", version = "2.41.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, + { name = "typing-inspection", marker = "python_full_version != '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/51/72c18c55cf2f46ff4f91ebcc8f75aa30f7305f3d726be3f4ebffb4ae972b/pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d", size = 456997, upload-time = "2024-12-03T15:58:59.867Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] name = "pydantic-core" -version = "2.27.1" +version = "2.33.2" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] dependencies = [ - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'", + "python_full_version >= '3.12' and platform_python_implementation == 'PyPy' and sys_platform == 'win32'", + "(python_full_version >= '3.14' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", version = "2.11.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785, upload-time = "2024-11-22T00:24:49.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/ce/60fd96895c09738648c83f3f00f595c807cb6735c70d3306b548cc96dd49/pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", size = 1897984, upload-time = "2024-11-22T00:21:25.431Z" }, - { url = "https://files.pythonhosted.org/packages/fd/b9/84623d6b6be98cc209b06687d9bca5a7b966ffed008d15225dd0d20cce2e/pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b", size = 1807491, upload-time = "2024-11-22T00:21:27.318Z" }, - { url = "https://files.pythonhosted.org/packages/01/72/59a70165eabbc93b1111d42df9ca016a4aa109409db04304829377947028/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", size = 1831953, upload-time = "2024-11-22T00:21:28.606Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0c/24841136476adafd26f94b45bb718a78cb0500bd7b4f8d667b67c29d7b0d/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", size = 1856071, upload-time = "2024-11-22T00:21:29.931Z" }, - { url = "https://files.pythonhosted.org/packages/53/5e/c32957a09cceb2af10d7642df45d1e3dbd8596061f700eac93b801de53c0/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", size = 2038439, upload-time = "2024-11-22T00:21:32.245Z" }, - { url = "https://files.pythonhosted.org/packages/e4/8f/979ab3eccd118b638cd6d8f980fea8794f45018255a36044dea40fe579d4/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", size = 2787416, upload-time = "2024-11-22T00:21:33.708Z" }, - { url = "https://files.pythonhosted.org/packages/02/1d/00f2e4626565b3b6d3690dab4d4fe1a26edd6a20e53749eb21ca892ef2df/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", size = 2134548, upload-time = "2024-11-22T00:21:35.823Z" }, - { url = "https://files.pythonhosted.org/packages/9d/46/3112621204128b90898adc2e721a3cd6cf5626504178d6f32c33b5a43b79/pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", size = 1989882, upload-time = "2024-11-22T00:21:37.872Z" }, - { url = "https://files.pythonhosted.org/packages/49/ec/557dd4ff5287ffffdf16a31d08d723de6762bb1b691879dc4423392309bc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", size = 1995829, upload-time = "2024-11-22T00:21:39.966Z" }, - { url = "https://files.pythonhosted.org/packages/6e/b2/610dbeb74d8d43921a7234555e4c091cb050a2bdb8cfea86d07791ce01c5/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", size = 2091257, upload-time = "2024-11-22T00:21:41.99Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7f/4bf8e9d26a9118521c80b229291fa9558a07cdd9a968ec2d5c1026f14fbc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", size = 2143894, upload-time = "2024-11-22T00:21:44.193Z" }, - { url = "https://files.pythonhosted.org/packages/1f/1c/875ac7139c958f4390f23656fe696d1acc8edf45fb81e4831960f12cd6e4/pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", size = 1816081, upload-time = "2024-11-22T00:21:45.468Z" }, - { url = "https://files.pythonhosted.org/packages/d7/41/55a117acaeda25ceae51030b518032934f251b1dac3704a53781383e3491/pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", size = 1981109, upload-time = "2024-11-22T00:21:47.452Z" }, - { url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553, upload-time = "2024-11-22T00:21:48.859Z" }, - { url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220, upload-time = "2024-11-22T00:21:50.354Z" }, - { url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727, upload-time = "2024-11-22T00:21:51.722Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282, upload-time = "2024-11-22T00:21:53.098Z" }, - { url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437, upload-time = "2024-11-22T00:21:55.185Z" }, - { url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899, upload-time = "2024-11-22T00:21:56.633Z" }, - { url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022, upload-time = "2024-11-22T00:21:59.154Z" }, - { url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969, upload-time = "2024-11-22T00:22:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625, upload-time = "2024-11-22T00:22:03.447Z" }, - { url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089, upload-time = "2024-11-22T00:22:04.941Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496, upload-time = "2024-11-22T00:22:06.57Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758, upload-time = "2024-11-22T00:22:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864, upload-time = "2024-11-22T00:22:10Z" }, - { url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327, upload-time = "2024-11-22T00:22:11.478Z" }, - { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239, upload-time = "2024-11-22T00:22:13.775Z" }, - { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070, upload-time = "2024-11-22T00:22:15.438Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096, upload-time = "2024-11-22T00:22:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708, upload-time = "2024-11-22T00:22:19.412Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751, upload-time = "2024-11-22T00:22:20.979Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863, upload-time = "2024-11-22T00:22:22.951Z" }, - { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161, upload-time = "2024-11-22T00:22:24.785Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294, upload-time = "2024-11-22T00:22:27.076Z" }, - { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468, upload-time = "2024-11-22T00:22:29.346Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413, upload-time = "2024-11-22T00:22:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735, upload-time = "2024-11-22T00:22:32.616Z" }, - { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633, upload-time = "2024-11-22T00:22:35.027Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973, upload-time = "2024-11-22T00:22:37.502Z" }, - { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215, upload-time = "2024-11-22T00:22:39.186Z" }, - { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033, upload-time = "2024-11-22T00:22:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542, upload-time = "2024-11-22T00:22:43.341Z" }, - { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854, upload-time = "2024-11-22T00:22:44.96Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389, upload-time = "2024-11-22T00:22:47.305Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934, upload-time = "2024-11-22T00:22:49.093Z" }, - { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176, upload-time = "2024-11-22T00:22:50.822Z" }, - { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720, upload-time = "2024-11-22T00:22:52.638Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972, upload-time = "2024-11-22T00:22:54.31Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477, upload-time = "2024-11-22T00:22:56.451Z" }, - { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186, upload-time = "2024-11-22T00:22:58.226Z" }, - { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429, upload-time = "2024-11-22T00:22:59.985Z" }, - { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713, upload-time = "2024-11-22T00:23:01.715Z" }, - { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897, upload-time = "2024-11-22T00:23:03.497Z" }, - { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983, upload-time = "2024-11-22T00:23:05.983Z" }, - { url = "https://files.pythonhosted.org/packages/7c/60/e5eb2d462595ba1f622edbe7b1d19531e510c05c405f0b87c80c1e89d5b1/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", size = 1894016, upload-time = "2024-11-22T00:24:03.815Z" }, - { url = "https://files.pythonhosted.org/packages/61/20/da7059855225038c1c4326a840908cc7ca72c7198cb6addb8b92ec81c1d6/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", size = 1771648, upload-time = "2024-11-22T00:24:05.981Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fc/5485cf0b0bb38da31d1d292160a4d123b5977841ddc1122c671a30b76cfd/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", size = 1826929, upload-time = "2024-11-22T00:24:08.163Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ff/fb1284a210e13a5f34c639efc54d51da136074ffbe25ec0c279cf9fbb1c4/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", size = 1980591, upload-time = "2024-11-22T00:24:10.291Z" }, - { url = "https://files.pythonhosted.org/packages/f1/14/77c1887a182d05af74f6aeac7b740da3a74155d3093ccc7ee10b900cc6b5/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", size = 1981326, upload-time = "2024-11-22T00:24:13.169Z" }, - { url = "https://files.pythonhosted.org/packages/06/aa/6f1b2747f811a9c66b5ef39d7f02fbb200479784c75e98290d70004b1253/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", size = 1989205, upload-time = "2024-11-22T00:24:16.049Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d2/8ce2b074d6835f3c88d85f6d8a399790043e9fdb3d0e43455e72d19df8cc/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", size = 2079616, upload-time = "2024-11-22T00:24:19.099Z" }, - { url = "https://files.pythonhosted.org/packages/65/71/af01033d4e58484c3db1e5d13e751ba5e3d6b87cc3368533df4c50932c8b/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", size = 2133265, upload-time = "2024-11-22T00:24:21.397Z" }, - { url = "https://files.pythonhosted.org/packages/33/72/f881b5e18fbb67cf2fb4ab253660de3c6899dbb2dba409d0b757e3559e3d/pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", size = 2001864, upload-time = "2024-11-22T00:24:24.354Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, ] [[package]] @@ -3505,6 +3754,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pynacl" version = "1.6.2" @@ -3697,8 +3951,8 @@ version = "1.1.408" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } wheels = [ @@ -3786,6 +4040,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + [[package]] name = "python-xlib" version = "0.33" @@ -3818,21 +4081,24 @@ wheels = [ [[package]] name = "pywin32" -version = "308" +version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/a6/3e9f2c474895c1bb61b11fa9640be00067b5c5b363c501ee9c3fa53aec01/pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e", size = 5927028, upload-time = "2024-10-12T20:41:58.898Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b4/84e2463422f869b4b718f79eb7530a4c1693e96b8a4e5e968de38be4d2ba/pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e", size = 6558484, upload-time = "2024-10-12T20:42:01.271Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8f/fb84ab789713f7c6feacaa08dad3ec8105b88ade8d1c4f0f0dfcaaa017d6/pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c", size = 7971454, upload-time = "2024-10-12T20:42:03.544Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e2/02652007469263fe1466e98439831d65d4ca80ea1a2df29abecedf7e47b7/pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a", size = 5928156, upload-time = "2024-10-12T20:42:05.78Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/f4fb45e2196bc7ffe09cad0542d9aff66b0e33f6c0954b43e49c33cad7bd/pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b", size = 6559559, upload-time = "2024-10-12T20:42:07.644Z" }, - { url = "https://files.pythonhosted.org/packages/79/ef/68bb6aa865c5c9b11a35771329e95917b5559845bd75b65549407f9fc6b4/pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6", size = 7972495, upload-time = "2024-10-12T20:42:09.803Z" }, - { url = "https://files.pythonhosted.org/packages/00/7c/d00d6bdd96de4344e06c4afbf218bc86b54436a94c01c71a8701f613aa56/pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", size = 5939729, upload-time = "2024-10-12T20:42:12.001Z" }, - { url = "https://files.pythonhosted.org/packages/21/27/0c8811fbc3ca188f93b5354e7c286eb91f80a53afa4e11007ef661afa746/pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", size = 6543015, upload-time = "2024-10-12T20:42:14.044Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0f/d40f8373608caed2255781a3ad9a51d03a594a1248cd632d6a298daca693/pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", size = 7976033, upload-time = "2024-10-12T20:42:16.215Z" }, - { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579, upload-time = "2024-10-12T20:42:18.623Z" }, - { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056, upload-time = "2024-10-12T20:42:20.864Z" }, - { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986, upload-time = "2024-10-12T20:42:22.799Z" }, + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] [[package]] @@ -3886,8 +4152,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.13'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version == '3.12.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } wheels = [ @@ -4020,8 +4286,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/31/103501e85e885e3e202c087fa612cfe450693210372766552ce1ab5b57b9/rich_click-1.8.5.tar.gz", hash = "sha256:a3eebe81da1c9da3c32f3810017c79bd687ff1b3fa35bfc9d8a3338797f1d1a1", size = 38229, upload-time = "2024-12-01T19:49:22.083Z" } wheels = [ @@ -4307,6 +4573,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186, upload-time = "2024-08-13T13:39:10.986Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" }, +] + [[package]] name = "starlette" version = "0.45.3" @@ -4386,8 +4664,8 @@ dependencies = [ { name = "tensorboard" }, { name = "tensorflow-io-gcs-filesystem", marker = "python_full_version < '3.12'" }, { name = "termcolor" }, - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, { name = "wrapt" }, ] wheels = [ @@ -4518,8 +4796,8 @@ dependencies = [ { name = "setuptools", marker = "python_full_version >= '3.12'" }, { name = "sympy" }, { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/37/81/aa9ab58ec10264c1abe62c8b73f5086c3c558885d6beecebf699f0dbeaeb/torch-2.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:6860df13d9911ac158f4c44031609700e1eba07916fff62e21e6ffa0a9e01961", size = 766685561, upload-time = "2025-01-29T16:19:12.12Z" }, @@ -4603,8 +4881,8 @@ dependencies = [ { name = "click" }, { name = "rich" }, { name = "shellingham" }, - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789, upload-time = "2024-12-04T17:44:58.956Z" } wheels = [ @@ -4628,18 +4906,6 @@ name = "typing-extensions" version = "4.12.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", - "python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'", - "python_full_version >= '3.12' and platform_python_implementation == 'PyPy' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version == '3.11.*' and sys_platform == 'win32'", @@ -4655,6 +4921,18 @@ name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ + "python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "python_full_version >= '3.12' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'", + "python_full_version >= '3.12' and platform_python_implementation == 'PyPy' and sys_platform == 'win32'", + "(python_full_version >= '3.14' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", + "(python_full_version >= '3.12' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version < '3.11' and sys_platform == 'darwin'", "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version < '3.11' and sys_platform == 'win32'", @@ -4665,6 +4943,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + [[package]] name = "tzdata" version = "2025.1" @@ -4774,13 +5065,14 @@ dependencies = [ { name = "eth-typing" }, { name = "eth-utils" }, { name = "hexbytes" }, - { name = "pydantic" }, + { name = "pydantic", version = "2.11.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, { name = "pyunormalize" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "requests" }, { name = "types-requests" }, - { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, { name = "websockets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/09/4a/7679004558ff677248e90952cf05789ddba19b52389fccf5387d2b602de4/web3-7.6.0.tar.gz", hash = "sha256:25df8acdcb78eb872c3299408b79e8b4fd091602de5e3d29cbd8459e8f75ff23", size = 2172471, upload-time = "2024-11-22T17:54:10.9Z" } From 2776fd7245a7d49ce1f24a5e31bc56aa8de3839f Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Mon, 2 Mar 2026 11:35:26 -0800 Subject: [PATCH 07/15] fix lint --- tests/mcp_servers/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mcp_servers/test_init.py b/tests/mcp_servers/test_init.py index 9b5282dc8..edaa68f68 100644 --- a/tests/mcp_servers/test_init.py +++ b/tests/mcp_servers/test_init.py @@ -27,7 +27,7 @@ def test_connect_success(self): ): mock_loop.return_value.is_running.return_value = False - with patch("asyncio.run", new_callable=MagicMock) as mock_run: + with patch("asyncio.run", new_callable=MagicMock): result = load_mcp(configs) assert result == mock_client From f3f9405b1b2ccc3baf5ddc0287d6f15cf5697aaf Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Mon, 2 Mar 2026 11:38:53 -0800 Subject: [PATCH 08/15] fix unit test --- tests/mcp_servers/test_client.py | 2 +- tests/mcp_servers/test_init.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/mcp_servers/test_client.py b/tests/mcp_servers/test_client.py index 9b083082b..2ec0612b9 100644 --- a/tests/mcp_servers/test_client.py +++ b/tests/mcp_servers/test_client.py @@ -1,6 +1,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest +from mcp.types import TextContent from mcp_servers.client import ( HttpServerConfig, @@ -133,7 +134,6 @@ def test_is_mcp_tool(self): @pytest.mark.asyncio async def test_call_tool_returns_text(self): - from mcp.types import TextContent manager = self._make_manager_with_tools() diff --git a/tests/mcp_servers/test_init.py b/tests/mcp_servers/test_init.py index edaa68f68..afaa7f96c 100644 --- a/tests/mcp_servers/test_init.py +++ b/tests/mcp_servers/test_init.py @@ -1,6 +1,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from mcp_servers import load_mcp +from mcp_servers.client import MCPClientManager class TestLoadMcp: @@ -37,10 +38,8 @@ def test_connect_failure_returns_empty_manager(self): {"name": "bad", "transport": "stdio", "command": "fail", "args": []}, ] - from mcp_servers.client import MCPClientManager as RealManager - mock_client = MagicMock() - fallback_client = RealManager([]) + fallback_client = MCPClientManager([]) with ( patch( From a7362bb292f58b23da95428eb63760620c332ccf Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Mon, 2 Mar 2026 12:02:09 -0800 Subject: [PATCH 09/15] improve structure --- src/runtime/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/config.py b/src/runtime/config.py index c38ee58e9..9489f55ff 100644 --- a/src/runtime/config.py +++ b/src/runtime/config.py @@ -329,10 +329,10 @@ class ModeConfig: simulators: List[Simulator] = field(default_factory=list) agent_actions: List[AgentAction] = field(default_factory=list) backgrounds: List[Background] = field(default_factory=list) - mcp_servers: Optional[Any] = None action_execution_mode: Optional[str] = None action_dependencies: Optional[Dict[str, List[str]]] = None + mcp_servers: Optional[Any] = None _raw_inputs: List[Dict] = field(default_factory=list) _raw_llm: Optional[Dict] = None From 8bad1bc9a2228b386b16800c2f34b7a052058e54 Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Mon, 2 Mar 2026 14:59:11 -0800 Subject: [PATCH 10/15] remove http connection --- src/mcp_servers/client.py | 49 ++++--------------------------- tests/mcp_servers/test_client.py | 50 ++++++++++---------------------- 2 files changed, 21 insertions(+), 78 deletions(-) diff --git a/src/mcp_servers/client.py b/src/mcp_servers/client.py index 859df4234..2a08ffb49 100644 --- a/src/mcp_servers/client.py +++ b/src/mcp_servers/client.py @@ -1,37 +1,23 @@ import logging from contextlib import AsyncExitStack from dataclasses import dataclass -from typing import Any, Dict, List, Literal, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client -from mcp.client.streamable_http import streamable_http_client from mcp.types import TextContent -from pydantic import BaseModel, TypeAdapter +from pydantic import BaseModel class StdioServerConfig(BaseModel): """Configuration for an MCP server using stdio transport.""" name: str - transport: Literal["stdio"] = "stdio" command: str args: List[str] = [] env: Optional[Dict[str, str]] = None -class HttpServerConfig(BaseModel): - """Configuration for an MCP server using HTTP transport.""" - - name: str - transport: Literal["http"] - url: str - - -ServerConfig = Union[StdioServerConfig, HttpServerConfig] -_config_adapter = TypeAdapter(ServerConfig) - - @dataclass class MCPTool: """Metadata for a single MCP tool.""" @@ -69,7 +55,7 @@ def generate_description(self) -> str: class StdioTransport: - """Create a stdio transport connection.""" + """Handles stdio transport connections to MCP servers.""" @staticmethod async def connect( @@ -86,25 +72,6 @@ async def connect( return read, write -class HttpTransport: - """Create an HTTP transport connection.""" - - @staticmethod - async def connect( - exit_stack: AsyncExitStack, config: HttpServerConfig - ) -> Tuple[Any, Any]: - """Open an HTTP connection to an MCP server.""" - client_cm = streamable_http_client(config.url) - read, write, _ = await exit_stack.enter_async_context(client_cm) - return read, write - - -_TRANSPORTS = { - "stdio": StdioTransport, - "http": HttpTransport, -} - - class MCPClientManager: """Manage connections to multiple MCP servers and execute tool calls. @@ -115,7 +82,7 @@ class MCPClientManager: """ def __init__(self, server_configs: List[Dict]) -> None: - self._configs = [_config_adapter.validate_python(c) for c in server_configs] + self._configs = [StdioServerConfig(**c) for c in server_configs] self._sessions: Dict[str, ClientSession] = {} self._tools: Dict[str, MCPTool] = {} self._exit_stack: Optional[AsyncExitStack] = None @@ -131,13 +98,9 @@ async def connect_all(self) -> None: except Exception as e: logging.error(f"Failed to connect to MCP server '{config.name}': {e}") - async def _connect_server(self, config: ServerConfig) -> None: + async def _connect_server(self, config: StdioServerConfig) -> None: """Connect to a single MCP server.""" - transport = _TRANSPORTS.get(config.transport) - if not transport: - raise ValueError(f"Unsupported MCP transport: {config.transport}") - - read, write = await transport.connect(self._exit_stack, config) + read, write = await StdioTransport.connect(self._exit_stack, config) session = ClientSession(read, write) assert self._exit_stack is not None diff --git a/tests/mcp_servers/test_client.py b/tests/mcp_servers/test_client.py index 2ec0612b9..7afa9a9b8 100644 --- a/tests/mcp_servers/test_client.py +++ b/tests/mcp_servers/test_client.py @@ -4,7 +4,6 @@ from mcp.types import TextContent from mcp_servers.client import ( - HttpServerConfig, MCPClientManager, MCPTool, StdioServerConfig, @@ -49,30 +48,22 @@ class TestConfigParsing: def test_stdio_config(self): config = StdioServerConfig(name="test", command="python", args=["-m", "server"]) - assert config.transport == "stdio" assert config.name == "test" - def test_http_config(self): - config = HttpServerConfig( - name="test", transport="http", url="http://localhost:8080" - ) - assert config.transport == "http" - assert config.url == "http://localhost:8080" - def test_client_manager_parses_configs(self): configs = [ - {"name": "s1", "transport": "stdio", "command": "python", "args": []}, - {"name": "s2", "transport": "http", "url": "http://localhost:8080"}, + {"name": "s1", "command": "python", "args": []}, + {"name": "s2", "command": "node", "args": ["-y", "server"]}, ] manager = MCPClientManager(configs) assert len(manager._configs) == 2 assert isinstance(manager._configs[0], StdioServerConfig) - assert isinstance(manager._configs[1], HttpServerConfig) + assert isinstance(manager._configs[1], StdioServerConfig) - def test_invalid_transport_raises(self): + def test_missing_command_raises(self): with pytest.raises(Exception): - MCPClientManager([{"name": "bad", "transport": "grpc", "url": "x"}]) + MCPClientManager([{"name": "bad"}]) class TestMCPClientManager: @@ -211,18 +202,17 @@ async def test_connect_discovers_tools(self): mock_session.list_tools = AsyncMock(return_value=Mock(tools=[mock_tool])) configs = [ - {"name": "weather", "transport": "stdio", "command": "python", "args": []}, + {"name": "weather", "command": "python", "args": []}, ] manager = MCPClientManager(configs) with ( - patch("mcp_servers.client._TRANSPORTS") as mock_transports, + patch( + "mcp_servers.client.StdioTransport.connect", + return_value=("read", "write"), + ), patch("mcp_servers.client.ClientSession", return_value=mock_session), ): - mock_transport = AsyncMock() - mock_transport.connect = AsyncMock(return_value=("read", "write")) - mock_transports.get = Mock(return_value=mock_transport) - await manager.connect_all() assert "mcp_weather_get_weather" in manager._tools @@ -233,25 +223,15 @@ async def test_connect_discovers_tools(self): @pytest.mark.asyncio async def test_connect_handles_server_failure(self): configs = [ - {"name": "bad_server", "transport": "stdio", "command": "fail", "args": []}, + {"name": "bad_server", "command": "fail", "args": []}, ] manager = MCPClientManager(configs) - with patch("mcp_servers.client._TRANSPORTS") as mock_transports: - mock_transport = AsyncMock() - mock_transport.connect = AsyncMock(side_effect=ConnectionError("refused")) - mock_transports.get = Mock(return_value=mock_transport) - + with patch( + "mcp_servers.client.StdioTransport.connect", + side_effect=ConnectionError("refused"), + ): await manager.connect_all() assert len(manager._tools) == 0 assert len(manager._sessions) == 0 - - @pytest.mark.asyncio - async def test_unsupported_transport_raises(self): - manager = MCPClientManager([]) - config = Mock() - config.transport = "grpc" - - with pytest.raises(ValueError, match="Unsupported MCP transport"): - await manager._connect_server(config) From 6118283d18492a125d649f8bb2bb570de1f03944 Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Mon, 2 Mar 2026 15:04:19 -0800 Subject: [PATCH 11/15] fix lint --- src/mcp_servers/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp_servers/client.py b/src/mcp_servers/client.py index 2a08ffb49..56487a1e5 100644 --- a/src/mcp_servers/client.py +++ b/src/mcp_servers/client.py @@ -100,10 +100,10 @@ async def connect_all(self) -> None: async def _connect_server(self, config: StdioServerConfig) -> None: """Connect to a single MCP server.""" + assert self._exit_stack is not None read, write = await StdioTransport.connect(self._exit_stack, config) session = ClientSession(read, write) - assert self._exit_stack is not None await self._exit_stack.enter_async_context(session) await session.initialize() From 4c164235431c649a10f5160cede2c0e286978cca Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Mon, 2 Mar 2026 17:25:57 -0800 Subject: [PATCH 12/15] refactor prompt structure --- src/mcp_servers/orchestrator.py | 50 ++++----------- tests/mcp_servers/test_orchestrator.py | 86 +++++--------------------- 2 files changed, 28 insertions(+), 108 deletions(-) diff --git a/src/mcp_servers/orchestrator.py b/src/mcp_servers/orchestrator.py index 441a11b6d..2905be5a4 100644 --- a/src/mcp_servers/orchestrator.py +++ b/src/mcp_servers/orchestrator.py @@ -17,15 +17,6 @@ class ToolResult: content: str -@dataclass -class RoundRecord: - """Record of a single orchestration round.""" - - round_num: int - tools_called: List[str] - results: List[ToolResult] - - class MCPOrchestrator: """Orchestrate multi-round MCP tool execution. @@ -104,7 +95,6 @@ async def process( if output is None or not hasattr(output, "actions"): return output - history: List[RoundRecord] = [] succeeded_calls: Set[str] = set() for round_idx in range(max_rounds): @@ -140,15 +130,7 @@ async def process( if result.success: succeeded_calls.add(self._build_call_signature(action)) - history.append( - RoundRecord( - round_num=round_idx + 1, - tools_called=[action.type for action in new_actions], - results=results, - ) - ) - - output = await self._recall_llm(llm, prompt, history) + output = await self._recall_llm(llm, prompt, results) if output is None or not hasattr(output, "actions"): return None @@ -239,23 +221,21 @@ async def _guarded(action: Any) -> ToolResult: def _build_result_prompt( self, original_prompt: str, - history: List[RoundRecord], + latest_results: List[ToolResult], ) -> str: - """Build the follow-up prompt containing tool results.""" - # Tool results: concise, structured + """Build the follow-up prompt with the latest tool results.""" lines = [] - for record in history: - for result in record.results: - status = "OK" if result.success else "FAILED" - lines.append(f"[{result.tool_key}] {status}: {result.content}") + for result in latest_results: + status = "OK" if result.success else "FAILED" + lines.append(f"[{result.tool_key}] {status}: {result.content}") result_block = "\n".join(lines) return ( f"{original_prompt}\n\n" f"[Tool Results]\n{result_block}\n\n" f"[Next Step]\n" - f"Do NOT re-call any tool marked OK above. " - f"If all needed info is available, respond with speak. " + f"Do NOT re-call tools you have already called successfully. " + f"If all needed info is available, respond with your final actions. " f"Otherwise call only the necessary tools in one batch.\n" ) @@ -263,16 +243,12 @@ async def _recall_llm( self, llm: Any, prompt: str, - history: List[RoundRecord], + latest_results: List[ToolResult], ) -> Any: - """Recall LLM with tool results. Skips history to avoid pollution.""" - recall_prompt = self._build_result_prompt(prompt, history) - logging.info("MCP recall LLM with cumulative context") - llm._skip_state_management = True - try: - return await llm.ask(recall_prompt) - finally: - llm._skip_state_management = False + """Recall LLM with the latest tool results.""" + recall_prompt = self._build_result_prompt(prompt, latest_results) + logging.info("MCP recall LLM with latest results") + return await llm.ask(recall_prompt) async def close(self) -> None: """Close all MCP client connections.""" diff --git a/tests/mcp_servers/test_orchestrator.py b/tests/mcp_servers/test_orchestrator.py index 0dc65aae0..0df48d9e8 100644 --- a/tests/mcp_servers/test_orchestrator.py +++ b/tests/mcp_servers/test_orchestrator.py @@ -4,7 +4,7 @@ import pytest from llm.output_model import Action, CortexOutputModel -from mcp_servers.orchestrator import MCPOrchestrator, RoundRecord, ToolResult +from mcp_servers.orchestrator import MCPOrchestrator, ToolResult class MockMCPClient: @@ -293,11 +293,12 @@ async def mock_dispatch(actions): assert dispatched[0].type == "emotion" -class TestSkipStateManagement: - """Test that recall_llm sets _skip_state_management flag.""" +class TestHistoryManagement: + """Test that recall_llm allows normal history management.""" @pytest.mark.asyncio - async def test_flag_set_during_recall(self, mock_client, make_output): + async def test_flag_not_set_during_recall(self, mock_client, make_output): + """MCP recall should NOT skip state management.""" initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) final = make_output([("speak", "done")]) @@ -313,40 +314,7 @@ async def ask(self, prompt): await orch.process(initial, "test", llm) - # The recall ask should have had _skip_state_management = True - assert flags_during_ask[-1] is True - - @pytest.mark.asyncio - async def test_flag_restored_after_recall(self, mock_client, make_output): - initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) - final = make_output([("speak", "done")]) - - llm = MockLLM([final]) - orch = MCPOrchestrator(mock_client, llm) - - await orch.process(initial, "test", llm) - - # Flag should be restored to False after process completes - assert llm._skip_state_management is False - - @pytest.mark.asyncio - async def test_flag_restored_on_error(self, mock_client, make_output): - initial = make_output([("mcp_weather_get", '{"city":"SF"}')]) - - class FailingLLM(MockLLM): - async def ask(self, prompt): - if self._skip_state_management: - raise RuntimeError("LLM crashed") - return await super().ask(prompt) - - llm = FailingLLM([]) - orch = MCPOrchestrator(mock_client, llm) - - with pytest.raises(RuntimeError, match="LLM crashed"): - await orch.process(initial, "test", llm) - - # Flag must be restored even after exception - assert llm._skip_state_management is False + assert flags_during_ask[-1] is False class TestBuildResultPrompt: @@ -356,59 +324,35 @@ def test_includes_tool_results(self, mock_client): llm = MockLLM([]) orch = MCPOrchestrator(mock_client, llm) - history = [ - RoundRecord( - round_num=1, - tools_called=["mcp_weather_get"], - results=[ToolResult("mcp_weather_get", True, '{"temp":73}')], - ) - ] - - prompt = orch._build_result_prompt("original", history) + results = [ToolResult("mcp_weather_get", True, '{"temp":73}')] + prompt = orch._build_result_prompt("original", results) assert "original" in prompt assert "mcp_weather_get" in prompt assert '{"temp":73}' in prompt assert "OK" in prompt - assert "3" in prompt def test_marks_failed_tools(self, mock_client): llm = MockLLM([]) orch = MCPOrchestrator(mock_client, llm) - history = [ - RoundRecord( - round_num=1, - tools_called=["mcp_slack_post"], - results=[ToolResult("mcp_slack_post", False, "Error: timeout")], - ) - ] - - prompt = orch._build_result_prompt("original", history) + results = [ToolResult("mcp_slack_post", False, "Error: timeout")] + prompt = orch._build_result_prompt("original", results) assert "FAILED" in prompt assert "Error: timeout" in prompt - def test_succeeded_summary(self, mock_client): + def test_mixed_results(self, mock_client): llm = MockLLM([]) orch = MCPOrchestrator(mock_client, llm) - history = [ - RoundRecord( - round_num=1, - tools_called=["mcp_weather_get", "mcp_maps_geocode"], - results=[ - ToolResult("mcp_weather_get", True, "ok"), - ToolResult("mcp_maps_geocode", False, "error"), - ], - ) + results = [ + ToolResult("mcp_weather_get", True, "ok"), + ToolResult("mcp_maps_geocode", False, "error"), ] + prompt = orch._build_result_prompt("original", results) - prompt = orch._build_result_prompt("original", history) - - # OK tool should appear in results assert "[mcp_weather_get] OK" in prompt - # FAILED tool should appear in results assert "[mcp_maps_geocode] FAILED" in prompt From 1bb74e42a3b5fbc40d58697b0f775fa0e245be2f Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Tue, 3 Mar 2026 15:19:08 -0800 Subject: [PATCH 13/15] Use owner task to bypaas the MCP anyio issue --- src/mcp_servers/__init__.py | 28 +----- src/mcp_servers/client.py | 73 +++++++++++--- src/mcp_servers/orchestrator.py | 6 +- src/runtime/cortex.py | 4 +- tests/mcp_servers/test_client.py | 10 +- tests/mcp_servers/test_init.py | 76 +++------------ tests/runtime/test_cortex.py | 161 +++++++++++++++++++++++++++++++ 7 files changed, 249 insertions(+), 109 deletions(-) diff --git a/src/mcp_servers/__init__.py b/src/mcp_servers/__init__.py index 0cc5b0c5a..96883012f 100644 --- a/src/mcp_servers/__init__.py +++ b/src/mcp_servers/__init__.py @@ -1,16 +1,13 @@ -import asyncio -import logging from typing import Dict, List -import nest_asyncio - from mcp_servers.client import MCPClientManager __all__ = ["MCPClientManager", "load_mcp"] def load_mcp(server_configs: List[Dict]) -> MCPClientManager: - """Load and connect MCP servers. + """Create an MCP client manager. + Parameters ---------- @@ -20,23 +17,6 @@ def load_mcp(server_configs: List[Dict]) -> MCPClientManager: Returns ------- MCPClientManager - Connected MCP client. + MCP client manager. """ - if not server_configs: - return MCPClientManager([]) - - try: - client = MCPClientManager(server_configs) - loop = asyncio.get_event_loop() - if loop.is_running(): - nest_asyncio.apply() - loop.run_until_complete(client.connect_all()) - else: - asyncio.run(client.connect_all()) - logging.info( - f"MCP client initialized with " f"{len(client.get_tool_schemas())} tools" - ) - return client - except Exception as e: - logging.error(f"Failed to initialize MCP client: {e}") - return MCPClientManager([]) + return MCPClientManager(server_configs or []) diff --git a/src/mcp_servers/client.py b/src/mcp_servers/client.py index 56487a1e5..98eb3b4b2 100644 --- a/src/mcp_servers/client.py +++ b/src/mcp_servers/client.py @@ -1,3 +1,4 @@ +import asyncio import logging from contextlib import AsyncExitStack from dataclasses import dataclass @@ -73,11 +74,11 @@ async def connect( class MCPClientManager: - """Manage connections to multiple MCP servers and execute tool calls. + """Manage connections to multiple MCP servers. Parameters ---------- - server_configs : list of dict + server_configs : list[dict] Raw configuration dicts. """ @@ -87,7 +88,48 @@ def __init__(self, server_configs: List[Dict]) -> None: self._tools: Dict[str, MCPTool] = {} self._exit_stack: Optional[AsyncExitStack] = None - async def connect_all(self) -> None: + self._connect_event = asyncio.Event() + self._close_event = asyncio.Event() + self._ready = asyncio.Event() + self._closed = asyncio.Event() + self.task: Optional[asyncio.Task] = None + + async def start(self) -> None: + """Connect to all configured MCP servers.""" + if self.task is None or self.task.done(): + self.task = asyncio.create_task(self._run_event_loop()) + self._connect_event.set() + await self._ready.wait() + + async def stop(self) -> None: + """Disconnect all MCP servers.""" + if not self._ready.is_set(): + return + self._close_event.set() + await self._closed.wait() + + async def _run_event_loop(self) -> None: + """Internal loop that owns all MCP connections in a single task.""" + try: + while True: + await self._connect_event.wait() + self._connect_event.clear() + self._closed.clear() + + await self._connect_all() + self._ready.set() + + await self._close_event.wait() + self._close_event.clear() + self._ready.clear() + + await self._close_all() + self._closed.set() + except asyncio.CancelledError: + await self._close_all() + raise + + async def _connect_all(self) -> None: """Connect to all configured MCP servers and discover tools.""" self._exit_stack = AsyncExitStack() await self._exit_stack.__aenter__() @@ -98,6 +140,19 @@ async def connect_all(self) -> None: except Exception as e: logging.error(f"Failed to connect to MCP server '{config.name}': {e}") + logging.info(f"MCP client connected with {len(self._tools)} tools") + + async def _close_all(self) -> None: + """Close all MCP server connections.""" + if self._exit_stack: + try: + await self._exit_stack.aclose() + except Exception as e: + logging.error(f"Error closing MCP connections: {e}") + self._exit_stack = None + self._sessions.clear() + self._tools.clear() + async def _connect_server(self, config: StdioServerConfig) -> None: """Connect to a single MCP server.""" assert self._exit_stack is not None @@ -107,7 +162,6 @@ async def _connect_server(self, config: StdioServerConfig) -> None: await self._exit_stack.enter_async_context(session) await session.initialize() - # Discover tools tools_result = await session.list_tools() self._sessions[config.name] = session @@ -168,14 +222,3 @@ async def call_tool(self, tool_key: str, arguments: Dict[str, Any]) -> str: texts.append(content.text) return "\n".join(texts) if texts else str(result.content) - - async def close_all(self) -> None: - """Close all MCP server connections.""" - if self._exit_stack: - try: - await self._exit_stack.aclose() - except Exception as e: - logging.error(f"Error closing MCP connections: {e}") - self._exit_stack = None - self._sessions.clear() - self._tools.clear() diff --git a/src/mcp_servers/orchestrator.py b/src/mcp_servers/orchestrator.py index 2905be5a4..165f66cab 100644 --- a/src/mcp_servers/orchestrator.py +++ b/src/mcp_servers/orchestrator.py @@ -250,6 +250,6 @@ async def _recall_llm( logging.info("MCP recall LLM with latest results") return await llm.ask(recall_prompt) - async def close(self) -> None: - """Close all MCP client connections.""" - await self._mcp_client.close_all() + async def stop(self) -> None: + """Stop all MCP server connections.""" + await self._mcp_client.stop() diff --git a/src/runtime/cortex.py b/src/runtime/cortex.py index 46d2dd538..ad7a53ff4 100644 --- a/src/runtime/cortex.py +++ b/src/runtime/cortex.py @@ -141,6 +141,7 @@ async def _initialize_mode(self, mode_name: str): self.simulator_orchestrator = SimulatorOrchestrator(self.current_config) self.background_orchestrator = BackgroundOrchestrator(self.current_config) if self.current_config.mcp_servers: + await self.current_config.mcp_servers.start() self.mcp_orchestrator = MCPOrchestrator( self.current_config.mcp_servers, self.current_config.cortex_llm ) @@ -244,7 +245,8 @@ async def _stop_current_orchestrators(self) -> None: if self.mcp_orchestrator: logging.debug("Closing MCP connections") - await self.mcp_orchestrator.close() + await self.mcp_orchestrator.stop() + self.mcp_orchestrator = None tasks_to_cancel = {} diff --git a/tests/mcp_servers/test_client.py b/tests/mcp_servers/test_client.py index 7afa9a9b8..f4e00a31a 100644 --- a/tests/mcp_servers/test_client.py +++ b/tests/mcp_servers/test_client.py @@ -155,7 +155,7 @@ async def test_close_all_clears_state(self): manager._sessions = {"weather": Mock()} manager._exit_stack = AsyncMock() - await manager.close_all() + await manager._close_all() assert manager._exit_stack is None assert len(manager._sessions) == 0 @@ -169,7 +169,7 @@ async def test_close_all_handles_error(self): mock_stack.aclose = AsyncMock(side_effect=Exception("close error")) manager._exit_stack = mock_stack - await manager.close_all() + await manager._close_all() assert manager._exit_stack is None assert len(manager._sessions) == 0 @@ -179,7 +179,7 @@ async def test_close_all_noop_when_no_stack(self): manager = MCPClientManager([]) manager._exit_stack = None - await manager.close_all() + await manager._close_all() assert manager._exit_stack is None @@ -213,7 +213,7 @@ async def test_connect_discovers_tools(self): ), patch("mcp_servers.client.ClientSession", return_value=mock_session), ): - await manager.connect_all() + await manager._connect_all() assert "mcp_weather_get_weather" in manager._tools assert ( @@ -231,7 +231,7 @@ async def test_connect_handles_server_failure(self): "mcp_servers.client.StdioTransport.connect", side_effect=ConnectionError("refused"), ): - await manager.connect_all() + await manager._connect_all() assert len(manager._tools) == 0 assert len(manager._sessions) == 0 diff --git a/tests/mcp_servers/test_init.py b/tests/mcp_servers/test_init.py index afaa7f96c..c597f27b3 100644 --- a/tests/mcp_servers/test_init.py +++ b/tests/mcp_servers/test_init.py @@ -1,5 +1,3 @@ -from unittest.mock import AsyncMock, MagicMock, patch - from mcp_servers import load_mcp from mcp_servers.client import MCPClientManager @@ -10,72 +8,28 @@ class TestLoadMcp: def test_empty_configs_returns_empty_manager(self): client = load_mcp([]) + assert isinstance(client, MCPClientManager) assert client.get_tool_schemas() == [] assert client.get_tool_descriptions() == "" - def test_connect_success(self): - configs = [ - {"name": "test", "transport": "stdio", "command": "echo", "args": []}, - ] - - mock_client = MagicMock() - mock_client.get_tool_schemas.return_value = [{"type": "function"}] - mock_client.connect_all = AsyncMock() - - with ( - patch("mcp_servers.MCPClientManager", return_value=mock_client), - patch("asyncio.get_event_loop") as mock_loop, - ): - mock_loop.return_value.is_running.return_value = False - - with patch("asyncio.run", new_callable=MagicMock): - result = load_mcp(configs) - - assert result == mock_client - - def test_connect_failure_returns_empty_manager(self): + def test_returns_manager_with_parsed_configs(self): configs = [ - {"name": "bad", "transport": "stdio", "command": "fail", "args": []}, + {"name": "test", "command": "echo", "args": []}, ] + client = load_mcp(configs) - mock_client = MagicMock() - fallback_client = MCPClientManager([]) - - with ( - patch( - "mcp_servers.MCPClientManager", - side_effect=[mock_client, fallback_client], - ), - patch("asyncio.get_event_loop") as mock_loop, - ): - mock_loop.return_value.is_running.return_value = False - - with patch("asyncio.run", side_effect=Exception("connection failed")): - result = load_mcp(configs) - - assert result.get_tool_schemas() == [] - assert result.is_mcp_tool("anything") is False + assert isinstance(client, MCPClientManager) + assert len(client._configs) == 1 + assert client._configs[0].name == "test" - def test_connect_with_running_loop_uses_nest_asyncio(self): + def test_no_connection_on_creation(self): + """load_mcp should be lazy — no connection until start() is called.""" configs = [ - {"name": "test", "transport": "stdio", "command": "echo", "args": []}, + {"name": "test", "command": "echo", "args": []}, ] + client = load_mcp(configs) - mock_client = MagicMock() - mock_client.get_tool_schemas.return_value = [] - mock_client.connect_all = AsyncMock() - - with ( - patch("mcp_servers.MCPClientManager", return_value=mock_client), - patch("mcp_servers.nest_asyncio") as mock_nest, - patch("asyncio.get_event_loop") as mock_loop, - ): - loop = MagicMock() - loop.is_running.return_value = True - mock_loop.return_value = loop - - result = load_mcp(configs) - - mock_nest.apply.assert_called_once() - loop.run_until_complete.assert_called_once() - assert result == mock_client + assert client._exit_stack is None + assert len(client._sessions) == 0 + assert len(client._tools) == 0 + assert client.task is None diff --git a/tests/runtime/test_cortex.py b/tests/runtime/test_cortex.py index 78bd493d1..72d9468ce 100644 --- a/tests/runtime/test_cortex.py +++ b/tests/runtime/test_cortex.py @@ -159,6 +159,12 @@ async def test_initialize_mode(self, cortex_runtime, mock_mode_config): mock_background_class.return_value = mock_background_orch mock_mcp_class.return_value = mock_mcp_orch + mock_mcp_servers = Mock() + mock_mcp_servers.start = AsyncMock() + mock_mode_config.to_runtime_config.return_value = Mock( + mcp_servers=mock_mcp_servers, + cortex_llm=Mock(), + ) runtime.mode_config.modes = {"test_mode": mock_mode_config} await runtime._initialize_mode("test_mode") @@ -169,6 +175,7 @@ async def test_initialize_mode(self, cortex_runtime, mock_mode_config): mock_mode_config.to_runtime_config.assert_called_once_with( runtime.mode_config ) + mock_mcp_servers.start.assert_awaited_once() assert runtime.fuser == mock_fuser assert runtime.action_orchestrator == mock_action_orch @@ -441,6 +448,160 @@ async def test_cleanup_tasks(self, cortex_runtime): mock_gather.assert_called_once() +class TestMCPModeTransition: + """Test MCP lifecycle during cortex mode transitions.""" + + @pytest.mark.asyncio + async def test_initialize_mode_calls_mcp_start( + self, cortex_runtime, mock_mode_config + ): + """_initialize_mode should call mcp_servers.start() when MCP is configured.""" + runtime, mocks = cortex_runtime + + mock_mcp_servers = Mock() + mock_mcp_servers.start = AsyncMock() + mock_mode_config.to_runtime_config.return_value = Mock( + mcp_servers=mock_mcp_servers, + cortex_llm=Mock(), + ) + runtime.mode_config.modes = {"test_mode": mock_mode_config} + + with ( + patch("runtime.cortex.Fuser"), + patch("runtime.cortex.ActionOrchestrator"), + patch("runtime.cortex.SimulatorOrchestrator"), + patch("runtime.cortex.BackgroundOrchestrator"), + patch("runtime.cortex.MCPOrchestrator"), + ): + await runtime._initialize_mode("test_mode") + + mock_mcp_servers.start.assert_awaited_once() + + @pytest.mark.asyncio + async def test_stop_orchestrators_calls_mcp_stop(self, cortex_runtime): + """_stop_current_orchestrators should call mcp_orchestrator.stop().""" + runtime, mocks = cortex_runtime + + mock_mcp_orch = Mock() + mock_mcp_orch.stop = AsyncMock() + runtime.current_config = Mock() + runtime.mcp_orchestrator = mock_mcp_orch + + with patch("asyncio.wait", new_callable=AsyncMock) as mock_wait: + mock_wait.return_value = (set(), set()) + await runtime._stop_current_orchestrators() + + mock_mcp_orch.stop.assert_awaited_once() + assert runtime.mcp_orchestrator is None + + @pytest.mark.asyncio + async def test_transition_mcp_to_no_mcp(self, cortex_runtime, mock_mode_config): + """Mode transition from MCP mode to non-MCP mode.""" + runtime, mocks = cortex_runtime + + mock_mcp_orch = Mock() + mock_mcp_orch.stop = AsyncMock() + runtime.current_config = Mock() + runtime.mcp_orchestrator = mock_mcp_orch + + with patch("asyncio.wait", new_callable=AsyncMock) as mock_wait: + mock_wait.return_value = (set(), set()) + await runtime._stop_current_orchestrators() + + mock_mcp_orch.stop.assert_awaited_once() + assert runtime.mcp_orchestrator is None + + mock_mode_config.to_runtime_config.return_value = Mock( + mcp_servers=None, + cortex_llm=Mock(), + ) + runtime.mode_config.modes = {"no_mcp_mode": mock_mode_config} + + with ( + patch("runtime.cortex.Fuser"), + patch("runtime.cortex.ActionOrchestrator"), + patch("runtime.cortex.SimulatorOrchestrator"), + patch("runtime.cortex.BackgroundOrchestrator"), + patch("runtime.cortex.MCPOrchestrator") as mock_mcp_class, + ): + await runtime._initialize_mode("no_mcp_mode") + + mock_mcp_class.assert_not_called() + assert runtime.mcp_orchestrator is None + + @pytest.mark.asyncio + async def test_transition_no_mcp_to_mcp(self, cortex_runtime, mock_mode_config): + """Mode transition from non-MCP mode to MCP mode.""" + runtime, mocks = cortex_runtime + + runtime.current_config = Mock(mcp_servers=None) + runtime.mcp_orchestrator = None + + with patch("asyncio.wait", new_callable=AsyncMock) as mock_wait: + mock_wait.return_value = (set(), set()) + await runtime._stop_current_orchestrators() + + mock_mcp_servers = Mock() + mock_mcp_servers.start = AsyncMock() + mock_mode_config.to_runtime_config.return_value = Mock( + mcp_servers=mock_mcp_servers, + cortex_llm=Mock(), + ) + runtime.mode_config.modes = {"mcp_mode": mock_mode_config} + + with ( + patch("runtime.cortex.Fuser"), + patch("runtime.cortex.ActionOrchestrator"), + patch("runtime.cortex.SimulatorOrchestrator"), + patch("runtime.cortex.BackgroundOrchestrator"), + patch("runtime.cortex.MCPOrchestrator") as mock_mcp_class, + ): + await runtime._initialize_mode("mcp_mode") + + mock_mcp_servers.start.assert_awaited_once() + mock_mcp_class.assert_called_once() + + @pytest.mark.asyncio + async def test_transition_mcp_to_mcp(self, cortex_runtime, mock_mode_config): + """Full e2e mode transition from MCP mode to a different MCP mode.""" + runtime, mocks = cortex_runtime + + old_mcp_orch = Mock() + old_mcp_orch.stop = AsyncMock() + runtime.current_config = Mock() + runtime.mcp_orchestrator = old_mcp_orch + + with patch("asyncio.wait", new_callable=AsyncMock) as mock_wait: + mock_wait.return_value = (set(), set()) + await runtime._stop_current_orchestrators() + + old_mcp_orch.stop.assert_awaited_once() + assert runtime.mcp_orchestrator is None + + new_mcp_servers = Mock() + new_mcp_servers.start = AsyncMock() + mock_mode_config.to_runtime_config.return_value = Mock( + mcp_servers=new_mcp_servers, + cortex_llm=Mock(), + ) + runtime.mode_config.modes = {"new_mcp_mode": mock_mode_config} + + with ( + patch("runtime.cortex.Fuser"), + patch("runtime.cortex.ActionOrchestrator"), + patch("runtime.cortex.SimulatorOrchestrator"), + patch("runtime.cortex.BackgroundOrchestrator"), + patch("runtime.cortex.MCPOrchestrator") as mock_mcp_class, + ): + mock_new_orch = Mock() + mock_mcp_class.return_value = mock_new_orch + await runtime._initialize_mode("new_mcp_mode") + + new_mcp_servers.start.assert_awaited_once() + mock_mcp_class.assert_called_once() + assert runtime.mcp_orchestrator == mock_new_orch + + class TestModeCortexRuntimeHotReload: """Test cases for hot reload functionality in ModeCortexRuntime.""" From 50930c3558b97f3bd53ada7330a70bac96056bff Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Tue, 3 Mar 2026 17:10:49 -0800 Subject: [PATCH 14/15] Add task cancellation --- src/mcp_servers/client.py | 8 ++++++++ src/runtime/config.py | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/mcp_servers/client.py b/src/mcp_servers/client.py index 98eb3b4b2..77ab0c2ab 100644 --- a/src/mcp_servers/client.py +++ b/src/mcp_servers/client.py @@ -108,6 +108,14 @@ async def stop(self) -> None: self._close_event.set() await self._closed.wait() + if self.task is not None and not self.task.done(): + self.task.cancel() + try: + await self.task + except asyncio.CancelledError: + pass + self.task = None + async def _run_event_loop(self) -> None: """Internal loop that owns all MCP connections in a single task.""" try: diff --git a/src/runtime/config.py b/src/runtime/config.py index 9489f55ff..1067ab7be 100644 --- a/src/runtime/config.py +++ b/src/runtime/config.py @@ -752,7 +752,9 @@ def _load_mode_components(mode_config: ModeConfig, system_config: ModeSystemConf raise ValueError(f"No LLM configuration found for mode {mode_config.name}") # Load MCP servers - mode_config.mcp_servers = load_mcp(mode_config._raw_mcp_servers) + mode_config.mcp_servers = ( + load_mcp(mode_config._raw_mcp_servers) if mode_config._raw_mcp_servers else None + ) def mode_config_to_dict(config: ModeSystemConfig) -> Dict[str, Any]: From 7d70a55e0b388cd1e2bce52bd6ce94d5a84ae348 Mon Sep 17 00:00:00 2001 From: YuchengZhou821 Date: Thu, 5 Mar 2026 14:18:28 -0800 Subject: [PATCH 15/15] Add node installation in dockerfile --- Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dockerfile b/Dockerfile index a0804c080..c18ed9360 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,11 @@ RUN python3 -m pip install --upgrade pip COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/ +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + RUN mkdir -p /etc/alsa && \ ln -snf /usr/share/alsa/alsa.conf.d /etc/alsa/conf.d