diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 61e0d5c5..c35895ad 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -11,7 +11,7 @@ runs: steps: - name: "Run tests" shell: bash - run: source .venv/bin/activate && uv run --dev pytest --junitxml=test-results.xml --cov=rogue --cov-report=xml + run: source .venv/bin/activate && pytest --junitxml=test-results.xml --cov=rogue --cov-report=xml - name: "Upload Test Results" uses: actions/upload-artifact@v4 diff --git a/.github/workflows/rogue.yml b/.github/workflows/rogue.yml index 708a6b78..a967443c 100644 --- a/.github/workflows/rogue.yml +++ b/.github/workflows/rogue.yml @@ -25,6 +25,8 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 + with: + prune-cache: false - name: Setup Python uses: actions/setup-python@v5 @@ -49,7 +51,7 @@ jobs: echo "🚀 Starting AI agent..." # Not using uv because it will reinstall the sdk from pypi - source .venv/bin/activate && uv run python -m examples.tshirt_store_agent --host 0.0.0.0 --port 10001 & + source .venv/bin/activate && python -m examples.tshirt_store_agent --host 0.0.0.0 --port 10001 & AGENT_PID=$! echo "Agent started with PID: $AGENT_PID" trap 'echo "🛑 Stopping agent..."; kill $AGENT_PID' EXIT diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f6bf7b49..4729290e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,11 +33,13 @@ jobs: - name: Create venv run: uv venv - - name: Install rogue sdk - run: source .venv/bin/activate && uv pip install -e sdks/python --force-reinstall - - - name: Install rogue server - run: source .venv/bin/activate && uv sync --dev + - name: Install rogue server with local sdk + run: | + source .venv/bin/activate + uv sync --dev + uv pip install -e . + uv pip uninstall rogue-ai-sdk + uv pip install -e sdks/python --force-reinstall - name: Run tests uses: ./.github/actions/run-tests diff --git a/.vscode/launch.json b/.vscode/launch.json index 47bb5332..a24fcc73 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -58,7 +58,7 @@ "type": "debugpy", "request": "launch", "python": "./.venv/bin/python", - "program": "./examples/tshirt_store_agent", + "module": "examples.tshirt_store_agent", "envFile": "${workspaceFolder}/examples/tshirt_store_agent/.env" }, { @@ -66,8 +66,8 @@ "type": "debugpy", "request": "launch", "python": "./.venv/bin/python", - "program": "./examples/mcp/tshirt_store_mcp", - "envFile": "${workspaceFolder}/examples/mcp/tshirt_store_mcp/.env" + "module": "examples.mcp.tshirt_store_langgraph_mcp", + "envFile": "${workspaceFolder}/examples/mcp/tshirt_store_langgraph_mcp/.env" } ] } diff --git a/examples/mcp/tshirt_store_langgraph_mcp/__main__.py b/examples/mcp/tshirt_store_langgraph_mcp/__main__.py index 5180222b..f0cb66a1 100644 --- a/examples/mcp/tshirt_store_langgraph_mcp/__main__.py +++ b/examples/mcp/tshirt_store_langgraph_mcp/__main__.py @@ -1,18 +1,31 @@ +from typing import Literal + +import click from dotenv import load_dotenv -from mcp_agent_wrapper import mcp + +from .mcp_agent_wrapper import get_mcp_server load_dotenv() -def main() -> None: +@click.command() +@click.option("--host", "host", default="127.0.0.1", help="Host to run the server on") +@click.option("--port", "port", default=10001, help="Port to run the server on") +@click.option( + "--transport", + "transport", + default="streamable-http", + choices=["streamable-http", "sse"], + help="Transport to use for the mcp server", +) +def main(host: str, port: int, transport: Literal["streamable-http", "sse"]) -> None: print("Starting MCP server...") + mcp = get_mcp_server(host=host, port=port) - # Can also be "sse". # When using "sse", the url will be http://localhost:10001/sse # When using "streamable-http", the url will be http://localhost:10001/mcp # stdio isn't supported in this example, since rogue won't be able to connect to it. - mcp.run(transport="streamable-http") - # mcp.run(transport="sse") + mcp.run(transport=transport) if __name__ == "__main__": diff --git a/examples/mcp/tshirt_store_langgraph_mcp/mcp_agent_wrapper.py b/examples/mcp/tshirt_store_langgraph_mcp/mcp_agent_wrapper.py index 70ec3042..3a626d18 100644 --- a/examples/mcp/tshirt_store_langgraph_mcp/mcp_agent_wrapper.py +++ b/examples/mcp/tshirt_store_langgraph_mcp/mcp_agent_wrapper.py @@ -6,38 +6,45 @@ you only need to implement the send_message tool. """ +from functools import lru_cache + from loguru import logger from mcp.server.fastmcp import Context, FastMCP -from shirtify_agent import ShirtifyAgent from starlette.requests import Request -agent = ShirtifyAgent() -mcp = FastMCP( - "shirtify_agent_mcp", - port=10001, - host="127.0.0.1", -) +from .shirtify_agent import ShirtifyAgent + + +@lru_cache(maxsize=1) +def get_mcp_server(host: str = "127.0.0.1", port: int = 10001) -> FastMCP: + agent = ShirtifyAgent() + mcp = FastMCP( + "shirtify_agent_mcp", + host=host, + port=port, + ) + @mcp.tool() + def send_message(message: str, context: Context) -> str: + session_id: str | None = None + try: + request: Request = context.request_context.request # type: ignore -@mcp.tool() -def send_message(message: str, context: Context) -> str: - session_id: str | None = None - try: - request: Request = context.request_context.request # type: ignore + # The session id should be in the headers for streamable-http transport + session_id = request.headers.get("mcp-session-id") - # The session id should be in the headers for streamable-http transport - session_id = request.headers.get("mcp-session-id") + # The session id might also be in query param when using sse transport + if session_id is None: + session_id = request.query_params.get("session_id") + except Exception: + session_id = None + logger.exception("Error while extracting session id") - # The session id might also be in query param when using sse transport if session_id is None: - session_id = request.query_params.get("session_id") - except Exception: - session_id = None - logger.exception("Error while extracting session id") + logger.error("Couldn't extract session id") - if session_id is None: - logger.error("Couldn't extract session id") + # Invoking our agent + response = agent.invoke(message, session_id) + return response.get("content", "") - # Invoking our agent - response = agent.invoke(message, session_id) - return response.get("content", "") + return mcp diff --git a/lefthook.yaml b/lefthook.yaml index b30af213..ce0da4cd 100644 --- a/lefthook.yaml +++ b/lefthook.yaml @@ -30,7 +30,7 @@ pre-commit: - name: isort glob: "*.py" - run: isort {staged_files} + run: isort --profile black {staged_files} - name: black glob: "*.py" diff --git a/pyproject.toml b/pyproject.toml index 4cde7e4a..25a3be97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ dependencies = [ "click>=8.0.0", "datasets==3.6.0", "fastapi>=0.115.0", + "fastmcp>=2.12.5", "google-adk==1.5.0", "gradio==5.35.0", "langchain-openai>=0.3.35", diff --git a/rogue/__main__.py b/rogue/__main__.py index 7d1fdc5e..490b42d0 100644 --- a/rogue/__main__.py +++ b/rogue/__main__.py @@ -44,8 +44,9 @@ def common_parser() -> ArgumentParser: parent_parser.add_argument( "--example", type=str, - choices=["tshirt_store"], - help="Run with an example agent (e.g., tshirt_store)", + choices=["tshirt_store", "tshirt_store_langgraph_mcp"], + help="Run with an example agent " + "(e.g., tshirt_store, tshirt_store_langgraph_mcp)", ) parent_parser.add_argument( "--example-host", @@ -126,6 +127,19 @@ def start_example_agent( "--port", str(port), ] + elif example_name == "tshirt_store_langgraph_mcp": + # Use subprocess to run the example agent + cmd = [ + sys.executable, + "-m", + "examples.mcp.tshirt_store_langgraph_mcp", + "--host", + host, + "--port", + str(port), + "--transport", + "streamable-http", + ] else: logger.error(f"Unknown example: {example_name}") return None diff --git a/rogue/evaluator_agent/__init__.py b/rogue/evaluator_agent/__init__.py index 3a238c7e..c8764806 100644 --- a/rogue/evaluator_agent/__init__.py +++ b/rogue/evaluator_agent/__init__.py @@ -1 +1,21 @@ -from . import evaluator_agent, policy_evaluation, run_evaluator_agent +from . import ( + a2a, + base_evaluator_agent, + evaluator_agent_factory, + mcp, + policy_evaluation, + run_evaluator_agent, +) +from .a2a import A2AEvaluatorAgent +from .mcp import MCPEvaluatorAgent + +__all__ = [ + "base_evaluator_agent", + "evaluator_agent_factory", + "policy_evaluation", + "run_evaluator_agent", + "a2a", + "mcp", + "A2AEvaluatorAgent", + "MCPEvaluatorAgent", +] diff --git a/rogue/evaluator_agent/a2a/__init__.py b/rogue/evaluator_agent/a2a/__init__.py new file mode 100644 index 00000000..ff95f834 --- /dev/null +++ b/rogue/evaluator_agent/a2a/__init__.py @@ -0,0 +1,6 @@ +from . import a2a_evaluator_agent +from .a2a_evaluator_agent import A2AEvaluatorAgent + +__all__ = [ + "A2AEvaluatorAgent", +] diff --git a/rogue/evaluator_agent/a2a/a2a_evaluator_agent.py b/rogue/evaluator_agent/a2a/a2a_evaluator_agent.py new file mode 100644 index 00000000..d7effda5 --- /dev/null +++ b/rogue/evaluator_agent/a2a/a2a_evaluator_agent.py @@ -0,0 +1,213 @@ +import json +from types import TracebackType +from typing import Callable, Optional, Self, Type +from uuid import uuid4 + +from a2a.client import A2ACardResolver +from a2a.types import Message, MessageSendParams, Part, Role, Task, TextPart +from httpx import AsyncClient +from loguru import logger +from rogue_sdk.types import Protocol, Scenarios, Transport + +from ...common.remote_agent_connection import ( + JSON_RPC_ERROR_TYPES, + RemoteAgentConnections, +) +from ..base_evaluator_agent import BaseEvaluatorAgent + + +class A2AEvaluatorAgent(BaseEvaluatorAgent): + def __init__( + self, + evaluated_agent_address: str, + transport: Optional[Transport], + judge_llm: str, + scenarios: Scenarios, + business_context: Optional[str], + headers: Optional[dict[str, str]] = None, + judge_llm_auth: Optional[str] = None, + debug: bool = False, + deep_test_mode: bool = False, + chat_update_callback: Optional[Callable[[dict], None]] = None, + http_client: Optional[AsyncClient] = None, + **kwargs, + ) -> None: + super().__init__( + evaluated_agent_address=evaluated_agent_address, + protocol=Protocol.A2A, + transport=transport, + headers=headers, + judge_llm=judge_llm, + scenarios=scenarios, + business_context=business_context, + judge_llm_auth=judge_llm_auth, + debug=debug, + deep_test_mode=deep_test_mode, + chat_update_callback=chat_update_callback, + **kwargs, + ) + self._http_client = http_client or AsyncClient( + headers=headers or {}, + timeout=30, + ) + self.__evaluated_agent_client: RemoteAgentConnections | None = None + + async def _get_evaluated_agent_client(self) -> RemoteAgentConnections: + logger.debug("_get_evaluated_agent - enter") + if self.__evaluated_agent_client is None: + card_resolver = A2ACardResolver( + self._http_client, + self._evaluated_agent_address, + ) + card = await card_resolver.get_agent_card() + self.__evaluated_agent_client = RemoteAgentConnections( + self._http_client, + card, + ) + + return self.__evaluated_agent_client + + @staticmethod + def _get_text_from_response( + response: Task | Message | JSON_RPC_ERROR_TYPES, + ) -> str | None: + # TODO: add support for multi-model responses (audio, images, etc.) + logger.debug(f"_get_text_from_response {response}") + + def get_parts_text(parts: list[Part]) -> str: + text = "" + for p in parts: + if p.root.kind == "text": + text += p.root.text + elif p.root.kind == "data": + text += json.dumps(p.root.data) + elif p.root.kind == "file": + text += p.root.file.model_dump_json() + + return text + + if isinstance(response, Message): + return get_parts_text(response.parts) + elif isinstance(response, Task): + if response.artifacts is None: + if ( + response.status is not None + and response.status.message is not None + and response.status.message.parts is not None + ): + logger.debug("Returning text from task status message") + return get_parts_text(response.status.message.parts) + return None + + artifacts_text = "" + + for artifact in response.artifacts: + if artifact.name: + artifacts_text += f"Artifact: {artifact.name}:\n" + artifacts_text += get_parts_text(artifact.parts) + artifacts_text += "\n" + + return artifacts_text + + return None + + async def _send_message_to_evaluated_agent( + self, + context_id: str, + message: str, + ) -> dict[str, str]: + """ + Sends a message to the evaluated agent. + :param message: the text to send to the other agent. + :param context_id: The context ID of the conversation. + Each conversation has a unique context_id. All messages in the conversation + have the same context_id. + :return: A dictionary containing the response from the evaluated agent. + - "response": the response string. If there is no response + from the other agent, the string is empty. + """ + try: + logger.info( + "🔗 Making A2A call to evaluated agent", + extra={ + "message": message[:100] + "..." if len(message) > 100 else message, + "context_id": context_id, + "agent_url": self._evaluated_agent_address, + }, + ) + + self._add_message_to_chat_history(context_id, "user", message) + + agent_client = await self._get_evaluated_agent_client() + response = await agent_client.send_message( + MessageSendParams( + message=Message( + contextId=context_id, + messageId=uuid4().hex, + role=Role.user, + parts=[ + Part( + root=TextPart( + text=message, + ), + ), + ], + ), + ), + ) + + if not response: + logger.debug( + "_send_message_to_evaluated_agent - no response", + extra={"protocol": "a2a"}, + ) + return {"response": ""} + + agent_response_text = ( + self._get_text_from_response(response) or "Not a text response" + ) + + self._add_message_to_chat_history( + context_id, + "assistant", + agent_response_text, + ) + + logger.info( + "✅ A2A call successful - received response from evaluated agent", + extra={ + "response_length": len(agent_response_text), + "response_preview": ( + agent_response_text[:100] + "..." + if len(agent_response_text) > 100 + else agent_response_text + ), + "context_id": context_id, + }, + ) + return {"response": response.model_dump_json()} + except Exception as e: + logger.exception( + "❌ A2A call failed - error sending message to evaluated agent", + extra={ + "message": message[:100] + "..." if len(message) > 100 else message, + "context_id": context_id, + "agent_url": self._evaluated_agent_address, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + return {"response": "", "error": str(e)} + + async def __aenter__(self) -> Self: + await self._http_client.__aenter__() + return await super().__aenter__() + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + await super().__aexit__(exc_type, exc_value, traceback) + await self._http_client.__aexit__(exc_type, exc_value, traceback) diff --git a/rogue/evaluator_agent/evaluator_agent.py b/rogue/evaluator_agent/base_evaluator_agent.py similarity index 76% rename from rogue/evaluator_agent/evaluator_agent.py rename to rogue/evaluator_agent/base_evaluator_agent.py index 3738b88d..86f1d09e 100644 --- a/rogue/evaluator_agent/evaluator_agent.py +++ b/rogue/evaluator_agent/base_evaluator_agent.py @@ -1,11 +1,8 @@ -import json -from typing import TYPE_CHECKING, Any, Callable, Optional +from abc import ABC, abstractmethod +from types import TracebackType +from typing import TYPE_CHECKING, Any, Callable, Optional, Self, Type from uuid import uuid4 -from a2a.client import A2ACardResolver -from a2a.types import Message, MessageSendParams, Part, Role, Task, TextPart -from google.genai import types -from httpx import AsyncClient from loguru import logger from pydantic import ValidationError from pydantic_yaml import to_yaml_str @@ -15,17 +12,15 @@ ConversationEvaluation, EvaluationResult, EvaluationResults, + Protocol, Scenario, Scenarios, ScenarioType, + Transport, ) from ..common.agent_model_wrapper import get_llm_from_model -from ..common.remote_agent_connection import ( - JSON_RPC_ERROR_TYPES, - RemoteAgentConnections, -) -from ..evaluator_agent.policy_evaluation import evaluate_policy +from .policy_evaluation import evaluate_policy if TYPE_CHECKING: from google.adk.agents import LlmAgent @@ -158,51 +153,45 @@ """ # noqa: E501 -class EvaluatorAgent: +class BaseEvaluatorAgent(ABC): def __init__( self, - http_client: AsyncClient, evaluated_agent_address: str, + protocol: Protocol, + transport: Optional[Transport], judge_llm: str, scenarios: Scenarios, business_context: Optional[str], + headers: Optional[dict[str, str]] = None, judge_llm_auth: Optional[str] = None, debug: bool = False, - chat_update_callback: Optional[Callable[[dict], None]] = None, deep_test_mode: bool = False, + chat_update_callback: Optional[Callable[[dict], None]] = None, + *args, + **kwargs, ) -> None: - self._http_client = http_client self._evaluated_agent_address = evaluated_agent_address + self._protocol = protocol + self._transport = transport or self._protocol.get_default_transport() + if not self._transport.is_valid_for_protocol(protocol): + raise ValueError(f"Unsupported transport for {protocol}: {self._transport}") + + self._headers = headers or {} self._judge_llm = judge_llm self._judge_llm_auth = judge_llm_auth self._scenarios = scenarios self._evaluation_results: EvaluationResults = EvaluationResults() - self.__evaluated_agent_client: RemoteAgentConnections | None = None self._context_id_to_chat_history: dict[str, ChatHistory] = {} self._debug = debug self._business_context = business_context or "" - self._chat_update_callback = chat_update_callback self._deep_test_mode = deep_test_mode - - async def _get_evaluated_agent_client(self) -> RemoteAgentConnections: - logger.debug("_get_evaluated_agent - enter") - if self.__evaluated_agent_client is None: - card_resolver = A2ACardResolver( - self._http_client, - self._evaluated_agent_address, - ) - card = await card_resolver.get_agent_card() - self.__evaluated_agent_client = RemoteAgentConnections( - self._http_client, - card, - ) - - return self.__evaluated_agent_client + self._chat_update_callback = chat_update_callback def get_underlying_agent(self) -> "LlmAgent": # adk imports take a while, importing them here to reduce rogue startup time. from google.adk.agents import LlmAgent from google.adk.tools import FunctionTool + from google.genai.types import GenerateContentConfig instructions_template = ( AGENT_INSTRUCTIONS if self._deep_test_mode else FAST_MODE_AGENT_INSTRUCTIONS @@ -251,7 +240,7 @@ def get_underlying_agent(self) -> "LlmAgent": after_tool_callback=self._after_tool_callback, before_model_callback=self._before_model_callback, after_model_callback=self._after_model_callback, - generate_content_config=types.GenerateContentConfig( + generate_content_config=GenerateContentConfig( temperature=0.0, ), ) @@ -498,49 +487,7 @@ def _log_evaluation( def get_evaluation_results(self) -> EvaluationResults: return self._evaluation_results - @staticmethod - def _get_text_from_response( - response: Task | Message | JSON_RPC_ERROR_TYPES, - ) -> str | None: - logger.debug(f"_get_text_from_response {response}") - - def get_parts_text(parts: list[Part]) -> str: - text = "" - for p in parts: - if p.root.kind == "text": - text += p.root.text - elif p.root.kind == "data": - text += json.dumps(p.root.data) - elif p.root.kind == "file": - text += p.root.file.model_dump_json() - - return text - - if isinstance(response, Message): - return get_parts_text(response.parts) - elif isinstance(response, Task): - if response.artifacts is None: - if ( - response.status is not None - and response.status.message is not None - and response.status.message.parts is not None - ): - logger.debug("Returning text from task status message") - return get_parts_text(response.status.message.parts) - return None - - artifacts_text = "" - - for artifact in response.artifacts: - if artifact.name: - artifacts_text += f"Artifact: {artifact.name}:\n" - artifacts_text += get_parts_text(artifact.parts) - artifacts_text += "\n" - - return artifacts_text - - return None - + @abstractmethod async def _send_message_to_evaluated_agent( self, context_id: str, @@ -548,6 +495,8 @@ async def _send_message_to_evaluated_agent( ) -> dict[str, str]: """ Sends a message to the evaluated agent. + This method must be implemented by the subclass based + on the communication protocol. :param message: the text to send to the other agent. :param context_id: The context ID of the conversation. Each conversation has a unique context_id. All messages in the conversation @@ -556,93 +505,7 @@ async def _send_message_to_evaluated_agent( - "response": the response string. If there is no response from the other agent, the string is empty. """ - try: - logger.info( - "🔗 Making A2A call to evaluated agent", - extra={ - "message": message[:100] + "..." if len(message) > 100 else message, - "context_id": context_id, - "agent_url": self._evaluated_agent_address, - }, - ) - - if self._chat_update_callback: - self._chat_update_callback( - {"role": "Rogue", "content": message}, - ) - - if context_id not in self._context_id_to_chat_history: - self._context_id_to_chat_history[context_id] = ChatHistory() - - self._context_id_to_chat_history[context_id].add_message( - ChatMessage( - role="user", - content=message, - ), - ) - - agent_client = await self._get_evaluated_agent_client() - response = await agent_client.send_message( - MessageSendParams( - message=Message( - contextId=context_id, - messageId=uuid4().hex, - role=Role.user, - parts=[ - Part( - root=TextPart( - text=message, - ), - ), - ], - ), - ), - ) - - if not response: - logger.debug("_send_message_to_evaluated_agent - no response") - return {"response": ""} - - agent_response_text = ( - self._get_text_from_response(response) or "Not a text response" - ) - self._context_id_to_chat_history[context_id].add_message( - ChatMessage( - role="assistant", - content=agent_response_text, - ), - ) - - if self._chat_update_callback: - self._chat_update_callback( - {"role": "Agent Under Test", "content": agent_response_text}, - ) - - logger.info( - "✅ A2A call successful - received response from evaluated agent", - extra={ - "response_length": len(agent_response_text), - "response_preview": ( - agent_response_text[:100] + "..." - if len(agent_response_text) > 100 - else agent_response_text - ), - "context_id": context_id, - }, - ) - return {"response": response.model_dump_json()} - except Exception as e: - logger.exception( - "❌ A2A call failed - error sending message to evaluated agent", - extra={ - "message": message[:100] + "..." if len(message) > 100 else message, - "context_id": context_id, - "agent_url": self._evaluated_agent_address, - "error": str(e), - "error_type": type(e).__name__, - }, - ) - return {"response": "", "error": str(e)} + raise NotImplementedError("Subclasses must implement this method") @staticmethod def _get_conversation_context_id() -> str: @@ -652,3 +515,42 @@ def _get_conversation_context_id() -> str: """ logger.debug("_get_conversation_context_id - enter") return uuid4().hex + + def _add_message_to_chat_history( + self, + context_id: str, + role: str, + message: str, + ) -> None: + """ + Adds a message to the chat history. + If a callback is provided, it will also call the callback with the message. + :param context_id: The context ID of the conversation. + :param role: The role of the message. + :param message: The message to add to the chat history. + """ + if context_id not in self._context_id_to_chat_history: + self._context_id_to_chat_history[context_id] = ChatHistory() + self._context_id_to_chat_history[context_id].add_message( + ChatMessage( + role=role, + content=message, + ), + ) + + callback_role = "Rogue" if role == "user" else "Agent Under Test" + if self._chat_update_callback: + self._chat_update_callback( + {"role": callback_role, "content": message}, + ) + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + pass diff --git a/rogue/evaluator_agent/evaluator_agent_factory.py b/rogue/evaluator_agent/evaluator_agent_factory.py new file mode 100644 index 00000000..05b42b52 --- /dev/null +++ b/rogue/evaluator_agent/evaluator_agent_factory.py @@ -0,0 +1,45 @@ +from typing import Callable, Optional + +from rogue_sdk.types import Protocol, Scenarios, Transport + +from .a2a.a2a_evaluator_agent import A2AEvaluatorAgent +from .base_evaluator_agent import BaseEvaluatorAgent +from .mcp.mcp_evaluator_agent import MCPEvaluatorAgent + +_PROTOCOL_TO_AGENT_CLASS = { + Protocol.A2A: A2AEvaluatorAgent, + Protocol.MCP: MCPEvaluatorAgent, +} + + +def get_evaluator_agent( + protocol: Protocol, + transport: Transport | None, + evaluated_agent_address: str, + judge_llm: str, + scenarios: Scenarios, + business_context: Optional[str], + headers: Optional[dict[str, str]] = None, + judge_llm_auth: Optional[str] = None, + debug: bool = False, + deep_test_mode: bool = False, + chat_update_callback: Optional[Callable[[dict], None]] = None, + **kwargs, +) -> BaseEvaluatorAgent: + agent_class = _PROTOCOL_TO_AGENT_CLASS.get(protocol, None) + if not agent_class: + raise ValueError(f"Invalid protocol: {protocol}") + + return agent_class( + transport=transport, + evaluated_agent_address=evaluated_agent_address, + judge_llm=judge_llm, + scenarios=scenarios, + business_context=business_context, + headers=headers, + judge_llm_auth=judge_llm_auth, + debug=debug, + deep_test_mode=deep_test_mode, + chat_update_callback=chat_update_callback, + **kwargs, + ) diff --git a/rogue/evaluator_agent/mcp/__init__.py b/rogue/evaluator_agent/mcp/__init__.py new file mode 100644 index 00000000..b00c5500 --- /dev/null +++ b/rogue/evaluator_agent/mcp/__init__.py @@ -0,0 +1,6 @@ +from . import mcp_evaluator_agent +from .mcp_evaluator_agent import MCPEvaluatorAgent + +__all__ = [ + "MCPEvaluatorAgent", +] diff --git a/rogue/evaluator_agent/mcp/mcp_evaluator_agent.py b/rogue/evaluator_agent/mcp/mcp_evaluator_agent.py new file mode 100644 index 00000000..d101d7a0 --- /dev/null +++ b/rogue/evaluator_agent/mcp/mcp_evaluator_agent.py @@ -0,0 +1,145 @@ +from types import TracebackType +from typing import Callable, Optional, Self, Type + +from loguru import logger +from rogue_sdk.types import Protocol, Scenarios, Transport + +from ..base_evaluator_agent import BaseEvaluatorAgent + + +class MCPEvaluatorAgent(BaseEvaluatorAgent): + def __init__( + self, + transport: Optional[Transport], + evaluated_agent_address: str, + judge_llm: str, + scenarios: Scenarios, + business_context: Optional[str], + headers: Optional[dict[str, str]] = None, + judge_llm_auth: Optional[str] = None, + debug: bool = False, + deep_test_mode: bool = False, + chat_update_callback: Optional[Callable[[dict], None]] = None, + *args, + **kwargs, + ): + from fastmcp import Client + from fastmcp.client import SSETransport, StreamableHttpTransport + + super().__init__( + evaluated_agent_address=evaluated_agent_address, + protocol=Protocol.MCP, + transport=transport, + judge_llm=judge_llm, + scenarios=scenarios, + business_context=business_context, + headers=headers, + judge_llm_auth=judge_llm_auth, + debug=debug, + deep_test_mode=deep_test_mode, + chat_update_callback=chat_update_callback, + ) + + self._client: Client[SSETransport | StreamableHttpTransport] + + if self._transport == Transport.SSE: + self._client = Client[SSETransport]( + transport=SSETransport( + url=evaluated_agent_address, + headers=headers, + ), + ) + elif self._transport == Transport.STREAMABLE_HTTP: + self._client = Client[StreamableHttpTransport]( + transport=StreamableHttpTransport( + url=evaluated_agent_address, + headers=headers, + ), + ) + else: + raise ValueError(f"Unsupported transport for MCP: {self._transport}") + + async def __aenter__(self) -> Self: + await self._client.__aenter__() + return await super().__aenter__() + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + await super().__aexit__(exc_type, exc_value, traceback) + await self._client.__aexit__(exc_type, exc_value, traceback) + + async def _send_message_to_evaluated_agent( + self, + context_id: str, + message: str, + ) -> dict[str, str]: + logger.info( + "🔗 Making MCP call to evaluated agent", + extra={ + "message": message[:100] + "..." if len(message) > 100 else message, + "context_id": context_id, + "agent_url": self._evaluated_agent_address, + "transport": self._transport.value, + }, + ) + + self._add_message_to_chat_history(context_id, "user", message) + response = await self._invoke_mcp_agent(message) + # TODO: add support for multi-model responses (audio, images, etc.) + if not response or not response.get("response"): + logger.debug( + "_send_message_to_evaluated_agent - no response", + extra={"protocol": "mcp"}, + ) + return {"response": ""} + + self._add_message_to_chat_history( + context_id, + "assistant", + response.get( + "response", + "Not a text response", + ), + ) + + return response + + async def _invoke_mcp_agent(self, message: str) -> dict[str, str]: + try: + tool_result = await self._client.call_tool( + name="send_message", + arguments={ + "message": message, + }, + ) + + text_response = "" + for part in tool_result.content: + if part.type != "text": + logger.warning( + "Received non-text part in tool result", + extra={"type": part.type}, + ) + continue + + text_response += part.text + + return {"response": text_response} + except Exception as e: + logger.exception( + "Error while sending message to evaluated agent using mcp", + extra={ + "protocol": "mcp", + "agent_url": self._evaluated_agent_address, + "transport": self._transport.value, + "message": message[:100] + "..." if len(message) > 100 else message, + }, + ) + return { + "response": "", + "error": str(e), + } diff --git a/rogue/evaluator_agent/run_evaluator_agent.py b/rogue/evaluator_agent/run_evaluator_agent.py index 257864ee..7a7ce8e4 100644 --- a/rogue/evaluator_agent/run_evaluator_agent.py +++ b/rogue/evaluator_agent/run_evaluator_agent.py @@ -2,13 +2,11 @@ from asyncio import Queue from typing import TYPE_CHECKING, Any, AsyncGenerator -from google.genai import types -from httpx import AsyncClient from loguru import logger -from rogue_sdk.types import AuthType, EvaluationResults, Scenarios +from rogue_sdk.types import AuthType, EvaluationResults, Protocol, Scenarios, Transport from ..common.agent_sessions import create_session -from .evaluator_agent import EvaluatorAgent +from .evaluator_agent_factory import get_evaluator_agent if TYPE_CHECKING: from google.adk.runners import Runner @@ -20,15 +18,17 @@ async def _run_agent( input_text: str, session: "Session", ) -> str: + from google.genai.types import Content, Part + input_text_preview = ( input_text[:100] + "..." if len(input_text) > 100 else input_text ) logger.info(f"🎯 running agent with input: '{input_text_preview}'") # Create content from user input - content = types.Content( + content = Content( role="user", - parts=[types.Part(text=input_text)], + parts=[Part(text=input_text)], ) agent_output = "" @@ -73,6 +73,8 @@ async def _run_agent( async def arun_evaluator_agent( + protocol: Protocol, + transport: Transport | None, evaluated_agent_url: str, auth_type: AuthType, auth_credentials: str | None, @@ -89,6 +91,8 @@ async def arun_evaluator_agent( logger.info( "🤖 arun_evaluator_agent starting", extra={ + "protocol": protocol.value, + "transport": transport.value if transport else None, "evaluated_agent_url": evaluated_agent_url, "auth_type": auth_type.value, "judge_llm": judge_llm, @@ -104,20 +108,22 @@ async def arun_evaluator_agent( update_queue: Queue = Queue() results_queue: Queue = Queue() - logger.info("🌐 Creating HTTP client and evaluator agent") - async with AsyncClient(headers=headers, timeout=30) as httpx_client: - evaluator_agent = EvaluatorAgent( - http_client=httpx_client, - evaluated_agent_address=evaluated_agent_url, - scenarios=scenarios, - business_context=business_context, - chat_update_callback=update_queue.put_nowait, - deep_test_mode=deep_test_mode, - judge_llm=judge_llm, - judge_llm_auth=judge_llm_api_key, - ) - logger.info("✅ EvaluatorAgent created successfully") + logger.info("🌐 Creating evaluator agent") + evaluator_agent = get_evaluator_agent( + protocol=protocol, + transport=transport, + evaluated_agent_address=evaluated_agent_url, + judge_llm=judge_llm, + scenarios=scenarios, + business_context=business_context, + headers=headers, + judge_llm_auth=judge_llm_api_key, + debug=False, + deep_test_mode=deep_test_mode, + chat_update_callback=update_queue.put_nowait, + ) + async with evaluator_agent as evaluator_agent: session_service = InMemorySessionService() app_name = "evaluator_agent" @@ -212,6 +218,8 @@ async def agent_runner_task(): def run_evaluator_agent( + protocol: Protocol, + transport: Transport | None, evaluated_agent_url: str, auth_type: AuthType, auth_credentials: str | None, @@ -223,6 +231,8 @@ def run_evaluator_agent( ) -> EvaluationResults: async def run_evaluator_agent_task(): async for update_type, data in arun_evaluator_agent( + protocol=protocol, + transport=transport, evaluated_agent_url=evaluated_agent_url, auth_type=auth_type, auth_credentials=auth_credentials, diff --git a/rogue/server/core/evaluation_orchestrator.py b/rogue/server/core/evaluation_orchestrator.py index 2d86e286..bc7244bb 100644 --- a/rogue/server/core/evaluation_orchestrator.py +++ b/rogue/server/core/evaluation_orchestrator.py @@ -4,7 +4,7 @@ from typing import Any, AsyncGenerator, Tuple -from rogue_sdk.types import AuthType, EvaluationResults, Scenarios +from rogue_sdk.types import AuthType, EvaluationResults, Protocol, Scenarios, Transport from ...common.logging import get_logger from ...evaluator_agent.run_evaluator_agent import arun_evaluator_agent @@ -21,6 +21,8 @@ class EvaluationOrchestrator: def __init__( self, + protocol: Protocol, + transport: Transport | None, evaluated_agent_url: str, evaluated_agent_auth_type: AuthType, evaluated_agent_auth_credentials: str | None, @@ -30,6 +32,8 @@ def __init__( business_context: str, deep_test_mode: bool, ): + self.protocol = protocol + self.transport = transport self.evaluated_agent_url = evaluated_agent_url self.evaluated_agent_auth_type = evaluated_agent_auth_type self.evaluated_agent_auth_credentials = evaluated_agent_auth_credentials @@ -79,6 +83,8 @@ async def run_evaluation(self) -> AsyncGenerator[Tuple[str, Any], None]: # Call the evaluator agent directly async for update_type, data in arun_evaluator_agent( + protocol=self.protocol, + transport=self.transport, evaluated_agent_url=self.evaluated_agent_url, auth_type=self.evaluated_agent_auth_type, auth_credentials=self.evaluated_agent_auth_credentials, diff --git a/rogue/server/services/evaluation_library.py b/rogue/server/services/evaluation_library.py index e5b3fea7..002afa84 100644 --- a/rogue/server/services/evaluation_library.py +++ b/rogue/server/services/evaluation_library.py @@ -55,6 +55,8 @@ async def evaluate_agent( try: service = ScenarioEvaluationService( + protocol=agent_config.protocol, + transport=agent_config.transport, evaluated_agent_url=str(agent_config.evaluated_agent_url), evaluated_agent_auth_type=agent_config.evaluated_agent_auth_type, evaluated_agent_auth_credentials=( @@ -146,6 +148,8 @@ async def evaluate_agent_streaming( Tuple of (update_type, data) for real-time updates """ service = ScenarioEvaluationService( + protocol=agent_config.protocol, + transport=agent_config.transport, evaluated_agent_url=str(agent_config.evaluated_agent_url), evaluated_agent_auth_type=agent_config.evaluated_agent_auth_type, evaluated_agent_auth_credentials=agent_config.evaluated_agent_credentials, diff --git a/rogue/server/services/evaluation_service.py b/rogue/server/services/evaluation_service.py index e4d78aa5..148f054d 100644 --- a/rogue/server/services/evaluation_service.py +++ b/rogue/server/services/evaluation_service.py @@ -96,6 +96,8 @@ async def run_job(self, job_id: str): # Create evaluation orchestrator (server-native) agent_config = job.request.agent_config orchestrator = EvaluationOrchestrator( + protocol=agent_config.protocol, + transport=agent_config.transport, evaluated_agent_url=str(agent_config.evaluated_agent_url), evaluated_agent_auth_type=agent_config.evaluated_agent_auth_type, evaluated_agent_auth_credentials=( diff --git a/rogue/server/services/scenario_evaluation_service.py b/rogue/server/services/scenario_evaluation_service.py index 8de94c76..4c5252f9 100644 --- a/rogue/server/services/scenario_evaluation_service.py +++ b/rogue/server/services/scenario_evaluation_service.py @@ -1,7 +1,7 @@ from typing import Any, AsyncGenerator from loguru import logger -from rogue_sdk.types import AuthType, EvaluationResults, Scenarios +from rogue_sdk.types import AuthType, EvaluationResults, Protocol, Scenarios, Transport from ...evaluator_agent.run_evaluator_agent import arun_evaluator_agent @@ -9,6 +9,8 @@ class ScenarioEvaluationService: def __init__( self, + protocol: Protocol, + transport: Transport | None, evaluated_agent_url: str, evaluated_agent_auth_type: AuthType, evaluated_agent_auth_credentials: str | None, @@ -18,6 +20,8 @@ def __init__( business_context: str, deep_test_mode: bool, ): + self._protocol = protocol + self._transport = transport self._evaluated_agent_url = evaluated_agent_url self._evaluated_agent_auth_type = evaluated_agent_auth_type self._evaluated_agent_auth_credentials = evaluated_agent_auth_credentials @@ -55,6 +59,8 @@ async def evaluate_scenarios(self) -> AsyncGenerator[tuple[str, Any], None]: update_count = 0 async for update_type, data in arun_evaluator_agent( + protocol=self._protocol, + transport=self._transport, evaluated_agent_url=str(self._evaluated_agent_url), auth_type=self._evaluated_agent_auth_type, auth_credentials=self._evaluated_agent_auth_credentials, diff --git a/sdks/python/rogue_sdk/__init__.py b/sdks/python/rogue_sdk/__init__.py index 9308c7f8..6d838e57 100644 --- a/sdks/python/rogue_sdk/__init__.py +++ b/sdks/python/rogue_sdk/__init__.py @@ -73,6 +73,7 @@ async def main(): "EvaluationJob", "EvaluationResult", "AgentConfig", + "Protocol", "Scenario", "AuthType", "ScenarioType", diff --git a/sdks/python/rogue_sdk/sdk.py b/sdks/python/rogue_sdk/sdk.py index d1ec55cf..c6f5297b 100644 --- a/sdks/python/rogue_sdk/sdk.py +++ b/sdks/python/rogue_sdk/sdk.py @@ -22,10 +22,12 @@ HealthResponse, InterviewSession, JobListResponse, + Protocol, RogueClientConfig, Scenarios, SendMessageResponse, StructuredSummary, + Transport, WebSocketEventType, ) from .websocket import RogueWebSocketClient @@ -240,17 +242,24 @@ async def run_evaluation( scenarios: Scenarios, business_context: str, judge_model: str = "openai/gpt-4o-mini", + judge_llm_api_key: Optional[str] = None, + protocol: Protocol = Protocol.A2A, + transport: Transport | None = None, auth_type: AuthType = AuthType.NO_AUTH, auth_credentials: Optional[str] = None, deep_test: bool = False, timeout: float = 600.0, ) -> EvaluationJob: """Quick evaluation helper.""" + agent_config = AgentConfig( + protocol=protocol, + transport=transport, evaluated_agent_url=HttpUrl(agent_url), evaluated_agent_auth_type=auth_type, evaluated_agent_credentials=auth_credentials, judge_llm=judge_model, + judge_llm_api_key=judge_llm_api_key, deep_test_mode=deep_test, interview_mode=True, parallel_runs=1, diff --git a/sdks/python/rogue_sdk/types.py b/sdks/python/rogue_sdk/types.py index 70cd15e8..2addede0 100644 --- a/sdks/python/rogue_sdk/types.py +++ b/sdks/python/rogue_sdk/types.py @@ -14,6 +14,7 @@ ConfigDict, Field, HttpUrl, + SecretStr, field_validator, model_validator, ) @@ -29,8 +30,13 @@ class AuthType(str, Enum): def get_auth_header( self, - auth_credentials: Optional[str], + auth_credentials: Optional[str | SecretStr], ) -> dict[str, str]: + auth_credentials = ( + auth_credentials.get_secret_value() + if isinstance(auth_credentials, SecretStr) + else auth_credentials + ) if self == AuthType.NO_AUTH or not auth_credentials: return {} elif self == AuthType.API_KEY: @@ -59,12 +65,47 @@ class EvaluationStatus(str, Enum): CANCELLED = "cancelled" +class Protocol(str, Enum): + """Protocol types for communicating with the evaluator agent.""" + + A2A = "a2a" + MCP = "mcp" + + def get_default_transport(self) -> "Transport": + if self == Protocol.A2A: + return Transport.HTTP + elif self == Protocol.MCP: + return Transport.STREAMABLE_HTTP + raise ValueError(f"No default transport for protocol {self}") + + +class Transport(str, Enum): + """Transport types for communicating with the evaluator agent.""" + + # A2A transports + HTTP = "http" + + # MCP transports + STREAMABLE_HTTP = "streamable_http" + SSE = "sse" + + def is_valid_for_protocol(self, protocol: Protocol) -> bool: + return self in PROTOCOL_TO_TRANSPORTS[protocol] + + +PROTOCOL_TO_TRANSPORTS: dict[Protocol, list[Transport]] = { + Protocol.A2A: [Transport.HTTP], + Protocol.MCP: [Transport.STREAMABLE_HTTP, Transport.SSE], +} + # Core Models class AgentConfig(BaseModel): """Configuration for the agent being evaluated.""" + protocol: Protocol = Protocol.A2A + transport: Transport | None = None evaluated_agent_url: HttpUrl evaluated_agent_auth_type: AuthType = Field( default=AuthType.NO_AUTH, @@ -90,6 +131,10 @@ def check_auth_credentials(self) -> "AgentConfig": ) return self + def model_post_init(self, __context: Any) -> None: + if self.transport is None: + self.transport = self.protocol.get_default_transport() + class Scenario(BaseModel): """Evaluation scenario definition.""" diff --git a/uv.lock b/uv.lock index 3b4bbbe0..5db1dd28 100644 --- a/uv.lock +++ b/uv.lock @@ -746,6 +746,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/b5/c5e179772ec38adb1c072b3aa13937d2860509ba32b2462bf1dda153833b/cryptography-46.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c4b93af7920cdf80f71650769464ccf1fb49a4b56ae0024173c24c48eb6b1612", size = 3438518, upload-time = "2025-10-01T00:29:06.139Z" }, ] +[[package]] +name = "cyclopts" +version = "3.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser", marker = "python_full_version < '4'" }, + { name = "rich" }, + { name = "rich-rst" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ca/7782da3b03242d5f0a16c20371dff99d4bd1fedafe26bc48ff82e42be8c9/cyclopts-3.24.0.tar.gz", hash = "sha256:de6964a041dfb3c57bf043b41e68c43548227a17de1bad246e3a0bfc5c4b7417", size = 76131, upload-time = "2025-09-08T15:40:57.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/8b/2c95f0645c6f40211896375e6fa51f504b8ccb29c21f6ae661fe87ab044e/cyclopts-3.24.0-py3-none-any.whl", hash = "sha256:809d04cde9108617106091140c3964ee6fceb33cecdd537f7ffa360bde13ed71", size = 86154, upload-time = "2025-09-08T15:40:56.41Z" }, +] + [[package]] name = "datasets" version = "3.6.0" @@ -798,6 +814,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + [[package]] name = "docstring-parser" version = "0.17.0" @@ -807,12 +832,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] +[[package]] +name = "docutils" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/c0/89fe6215b443b919cb98a5002e107cb5026854ed1ccb6b5833e0768419d1/docutils-0.22.2.tar.gz", hash = "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", size = 2289092, upload-time = "2025-09-20T17:55:47.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/dd/f95350e853a4468ec37478414fc04ae2d61dad7a947b3015c3dcc51a09b9/docutils-0.22.2-py3-none-any.whl", hash = "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8", size = 632667, upload-time = "2025-09-20T17:55:43.052Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -833,6 +880,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/70/584c4d7cad80f5e833715c0a29962d7c93b4d18eed522a02981a6d1b6ee5/fastapi-0.119.0-py3-none-any.whl", hash = "sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2", size = 107095, upload-time = "2025-10-11T17:13:39.048Z" }, ] +[[package]] +name = "fastmcp" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "mcp" }, + { name = "openapi-core" }, + { name = "openapi-pydantic" }, + { name = "pydantic", extra = ["email"] }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/a6/e3b46cd3e228635e0064c2648788b6f66a53bf0d0ddbf5fb44cca951f908/fastmcp-2.12.5.tar.gz", hash = "sha256:2dfd02e255705a4afe43d26caddbc864563036e233dbc6870f389ee523b39a6a", size = 7190263, upload-time = "2025-10-17T13:24:58.896Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/c1/9fb98c9649e15ea8cc691b4b09558b61dafb3dc0345f7322f8c4a8991ade/fastmcp-2.12.5-py3-none-any.whl", hash = "sha256:b1e542f9b83dbae7cecfdc9c73b062f77074785abda9f2306799116121344133", size = 329099, upload-time = "2025-10-17T13:24:57.518Z" }, +] + [[package]] name = "fastuuid" version = "0.13.5" @@ -1886,6 +1955,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -2007,6 +2085,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, ] +[[package]] +name = "jsonschema-path" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, +] + [[package]] name = "jsonschema-specifications" version = "2025.9.1" @@ -2139,6 +2232,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/a4/db5903757d710c4c401e7a87f6ba53a8242c580e8c1df5869b7acb949b2d/langsmith-0.4.34-py3-none-any.whl", hash = "sha256:3b83b2544f99bb8f6fca2681ee80fe6a44b0578c29e809e5a4e72fdee4db9146", size = 386981, upload-time = "2025-10-09T23:34:24.386Z" }, ] +[[package]] +name = "lazy-object-proxy" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" }, + { url = "https://files.pythonhosted.org/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" }, + { url = "https://files.pythonhosted.org/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" }, + { url = "https://files.pythonhosted.org/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, + { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, + { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, + { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, + { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, + { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, + { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, + { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, + { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, + { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" }, +] + [[package]] name = "litellm" version = "1.76.1" @@ -2283,7 +2421,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.17.0" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2298,9 +2436,9 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/79/5724a540df19e192e8606c543cdcf162de8eb435077520cca150f7365ec0/mcp-1.17.0.tar.gz", hash = "sha256:1b57fabf3203240ccc48e39859faf3ae1ccb0b571ff798bbedae800c73c6df90", size = 477951, upload-time = "2025-10-10T12:16:44.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/a1/b1f328da3b153683d2ec34f849b4b6eac2790fb240e3aef06ff2fab3df9d/mcp-1.16.0.tar.gz", hash = "sha256:39b8ca25460c578ee2cdad33feeea122694cfdf73eef58bee76c42f6ef0589df", size = 472918, upload-time = "2025-10-02T16:58:20.631Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/72/3751feae343a5ad07959df713907b5c3fbaed269d697a14b0c449080cf2e/mcp-1.17.0-py3-none-any.whl", hash = "sha256:0660ef275cada7a545af154db3082f176cf1d2681d5e35ae63e014faf0a35d40", size = 167737, upload-time = "2025-10-10T12:16:42.863Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0e/7cebc88e17daf94ebe28c95633af595ccb2864dc2ee7abd75542d98495cc/mcp-1.16.0-py3-none-any.whl", hash = "sha256:ec917be9a5d31b09ba331e1768aa576e0af45470d657a0319996a20a57d7d633", size = 167266, upload-time = "2025-10-02T16:58:19.039Z" }, ] [package.optional-dependencies] @@ -2318,6 +2456,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + [[package]] name = "multidict" version = "6.7.0" @@ -2702,6 +2849,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/5b/4be258ff072ed8ee15f6bfd8d5a1a4618aa4704b127c0c5959212ad177d6/openai-2.3.0-py3-none-any.whl", hash = "sha256:a7aa83be6f7b0ab2e4d4d7bcaf36e3d790874c0167380c5d0afd0ed99a86bd7b", size = 999768, upload-time = "2025-10-10T01:12:48.647Z" }, ] +[[package]] +name = "openapi-core" +version = "0.19.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "isodate" }, + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "more-itertools" }, + { name = "openapi-schema-validator" }, + { name = "openapi-spec-validator" }, + { name = "parse" }, + { name = "typing-extensions" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264, upload-time = "2025-03-20T20:17:28.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595, upload-time = "2025-03-20T20:17:26.77Z" }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + +[[package]] +name = "openapi-schema-validator" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-specifications" }, + { name = "rfc3339-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" }, +] + +[[package]] +name = "openapi-spec-validator" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "jsonschema-path" }, + { name = "lazy-object-proxy" }, + { name = "openapi-schema-validator" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.37.0" @@ -2975,6 +3183,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] +[[package]] +name = "parse" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" }, +] + +[[package]] +name = "pathable" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -3373,6 +3599,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.33.2" @@ -3524,6 +3755,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, ] +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -3855,6 +4095,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + [[package]] name = "rich" version = "14.2.0" @@ -3868,6 +4120,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, ] +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + [[package]] name = "rogue-ai" source = { editable = "." } @@ -3877,6 +4142,7 @@ dependencies = [ { name = "click" }, { name = "datasets" }, { name = "fastapi" }, + { name = "fastmcp" }, { name = "google-adk" }, { name = "gradio" }, { name = "langchain-openai" }, @@ -3922,6 +4188,7 @@ requires-dist = [ { name = "click", specifier = ">=8.0.0" }, { name = "datasets", specifier = "==3.6.0" }, { name = "fastapi", specifier = ">=0.115.0" }, + { name = "fastmcp", specifier = ">=2.12.5" }, { name = "google-adk", specifier = "==1.5.0" }, { name = "gradio", specifier = "==5.35.0" }, { name = "langchain-openai", specifier = ">=0.3.35" }, @@ -4779,6 +5046,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] +[[package]] +name = "werkzeug" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453, upload-time = "2024-11-01T16:40:45.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" }, +] + [[package]] name = "win32-setctime" version = "1.2.0"