From 68091dd90b709fec2dc67a3253bdce18b8005e73 Mon Sep 17 00:00:00 2001 From: Jon Saad-Falcon Date: Tue, 24 Feb 2026 03:59:24 +0000 Subject: [PATCH] Refactor agent hierarchy: extract BaseAgent/ToolUsingAgent helpers, add native ReAct/OpenHands - Pull shared boilerplate (event emission, message building, generation, think-tag stripping) into BaseAgent concrete helpers and ToolUsingAgent intermediate base class with tool-call loop - Add NativeReActAgent and NativeOpenHandsAgent as clean implementations built on the new base classes - Simplify SimpleAgent, OrchestratorAgent, ReActAgent, OpenHandsAgent, and RLMAgent to use inherited helpers instead of duplicated logic - Remove CustomAgent (superseded by BaseAgent subclassing) - Add backward-compat tests, base agent tests, native agent tests Co-Authored-By: Claude Opus 4.6 --- pyproject.toml | 1 + src/openjarvis/agents/__init__.py | 27 +- src/openjarvis/agents/_stubs.py | 148 +++- src/openjarvis/agents/custom.py | 34 - src/openjarvis/agents/native_openhands.py | 312 +++++++ src/openjarvis/agents/native_react.py | 148 ++++ src/openjarvis/agents/openhands.py | 369 +------- src/openjarvis/agents/orchestrator.py | 127 +-- src/openjarvis/agents/react.py | 186 +--- src/openjarvis/agents/rlm.py | 87 +- src/openjarvis/agents/simple.py | 52 +- src/openjarvis/cli/ask.py | 2 +- src/openjarvis/sdk.py | 2 +- src/openjarvis/system.py | 5 +- tests/agents/test_agent_routing_matrix.py | 26 +- tests/agents/test_backward_compat.py | 43 + tests/agents/test_base_agent.py | 275 ++++++ tests/agents/test_custom.py | 28 - tests/agents/test_native_openhands.py | 527 ++++++++++++ tests/agents/test_native_react.py | 477 +++++++++++ tests/agents/test_openhands.py | 523 +---------- tests/agents/test_react.py | 470 +--------- tests/cli/test_ask_agent.py | 2 - tests/test_integration_extended.py | 56 +- uv.lock | 999 +++++++++++++++++++++- 25 files changed, 3132 insertions(+), 1794 deletions(-) delete mode 100644 src/openjarvis/agents/custom.py create mode 100644 src/openjarvis/agents/native_openhands.py create mode 100644 src/openjarvis/agents/native_react.py create mode 100644 tests/agents/test_backward_compat.py create mode 100644 tests/agents/test_base_agent.py delete mode 100644 tests/agents/test_custom.py create mode 100644 tests/agents/test_native_openhands.py create mode 100644 tests/agents/test_native_react.py diff --git a/pyproject.toml b/pyproject.toml index d39cc4b3..eab8c4df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ server = [ "pydantic>=2.0", ] agents = [] +openhands = ["openhands-sdk>=1.0; python_version >= '3.12'"] learning = [] orchestrator-training = ["torch>=2.0", "transformers>=4.40"] channel-telegram = ["python-telegram-bot>=21.0"] diff --git a/src/openjarvis/agents/__init__.py b/src/openjarvis/agents/__init__.py index 04e977b3..f16294af 100644 --- a/src/openjarvis/agents/__init__.py +++ b/src/openjarvis/agents/__init__.py @@ -2,7 +2,12 @@ from __future__ import annotations -from openjarvis.agents._stubs import AgentContext, AgentResult, BaseAgent +from openjarvis.agents._stubs import ( + AgentContext, + AgentResult, + BaseAgent, + ToolUsingAgent, +) # Import agent modules to trigger @AgentRegistry.register() decorators try: @@ -16,12 +21,17 @@ except ImportError: pass try: - import openjarvis.agents.custom # noqa: F401 + import openjarvis.agents.native_react # noqa: F401 except ImportError: pass try: - import openjarvis.agents.react # noqa: F401 + import openjarvis.agents.native_openhands # noqa: F401 +except ImportError: + pass + +try: + import openjarvis.agents.react # noqa: F401 -- backward-compat shim except ImportError: pass @@ -35,4 +45,13 @@ try: except ImportError: pass -__all__ = ["AgentContext", "AgentResult", "BaseAgent"] +# Registry alias: "react" -> NativeReActAgent (for backward compat) +try: + from openjarvis.core.registry import AgentRegistry + + if AgentRegistry.contains("native_react") and not AgentRegistry.contains("react"): + AgentRegistry.register_value("react", AgentRegistry.get("native_react")) +except Exception: + pass + +__all__ = ["AgentContext", "AgentResult", "BaseAgent", "ToolUsingAgent"] diff --git a/src/openjarvis/agents/_stubs.py b/src/openjarvis/agents/_stubs.py index bf51fa37..5e6259ca 100644 --- a/src/openjarvis/agents/_stubs.py +++ b/src/openjarvis/agents/_stubs.py @@ -1,16 +1,21 @@ """ABC for agent implementations. Adapted from IPW's ``BaseAgent`` at ``src/agents/base.py``. -Phase 3 will provide concrete implementations (SimpleAgent, OrchestratorAgent, etc.). +Provides ``BaseAgent`` with concrete helper methods for event emission, +message building, and generation, plus ``ToolUsingAgent`` intermediate +base for agents that accept tools. """ from __future__ import annotations +import re from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any, Dict, List, Optional -from openjarvis.core.types import Conversation, ToolResult +from openjarvis.core.events import EventBus, EventType +from openjarvis.core.types import Conversation, Message, Role, ToolResult +from openjarvis.engine._stubs import InferenceEngine @dataclass(slots=True) @@ -38,9 +43,115 @@ class BaseAgent(ABC): Subclasses must be registered via ``@AgentRegistry.register("name")`` to become discoverable. + + Provides concrete helper methods that eliminate boilerplate in + subclasses: + + - :meth:`_emit_turn_start` / :meth:`_emit_turn_end` -- event bus + - :meth:`_build_messages` -- conversation + system prompt assembly + - :meth:`_generate` -- delegates to engine with stored defaults + - :meth:`_max_turns_result` -- standard max-turns-exceeded result + - :meth:`_strip_think_tags` -- remove ```` blocks """ agent_id: str + accepts_tools: bool = False + + def __init__( + self, + engine: InferenceEngine, + model: str, + *, + bus: Optional[EventBus] = None, + temperature: float = 0.7, + max_tokens: int = 1024, + ) -> None: + self._engine = engine + self._model = model + self._bus = bus + self._temperature = temperature + self._max_tokens = max_tokens + + # ------------------------------------------------------------------ + # Concrete helpers + # ------------------------------------------------------------------ + + def _emit_turn_start(self, input: str) -> None: + """Publish ``AGENT_TURN_START`` if an event bus is available.""" + if self._bus: + self._bus.publish( + EventType.AGENT_TURN_START, + {"agent": self.agent_id, "input": input}, + ) + + def _emit_turn_end(self, **data: Any) -> None: + """Publish ``AGENT_TURN_END`` if an event bus is available.""" + if self._bus: + payload: Dict[str, Any] = {"agent": self.agent_id} + payload.update(data) + self._bus.publish(EventType.AGENT_TURN_END, payload) + + def _build_messages( + self, + input: str, + context: Optional[AgentContext] = None, + *, + system_prompt: Optional[str] = None, + ) -> list[Message]: + """Assemble the message list for a generate call. + + Optionally prepends a system prompt, then appends any context + conversation messages, and finally the user input. + """ + messages: list[Message] = [] + if system_prompt: + messages.append(Message(role=Role.SYSTEM, content=system_prompt)) + if context and context.conversation.messages: + messages.extend(context.conversation.messages) + messages.append(Message(role=Role.USER, content=input)) + return messages + + def _generate(self, messages: list[Message], **extra_kwargs: Any) -> dict: + """Call ``engine.generate()`` with stored defaults. + + Extra kwargs (e.g. ``tools``) are forwarded to the engine. + """ + return self._engine.generate( + messages, + model=self._model, + temperature=self._temperature, + max_tokens=self._max_tokens, + **extra_kwargs, + ) + + def _max_turns_result( + self, + tool_results: list[ToolResult], + turns: int, + content: str = "", + ) -> AgentResult: + """Build the standard result for when ``max_turns`` is exceeded.""" + self._emit_turn_end(turns=turns, max_turns_exceeded=True) + return AgentResult( + content=content or "Maximum turns reached without a final answer.", + tool_results=tool_results, + turns=turns, + metadata={"max_turns_exceeded": True}, + ) + + @staticmethod + def _strip_think_tags(text: str) -> str: + """Remove ``...`` blocks from model output. + + Handles both ``...`` and the common distilled-model + pattern where the opening ```` is absent and the response + begins directly with reasoning text followed by ````. + """ + # Full ... blocks + text = re.sub(r".*?\s*", "", text, flags=re.DOTALL) + # Leading content before a bare (no opening tag) + text = re.sub(r"^.*?\s*", "", text, flags=re.DOTALL) + return text.strip() @abstractmethod def run( @@ -52,4 +163,35 @@ class BaseAgent(ABC): """Execute the agent on *input* and return an ``AgentResult``.""" -__all__ = ["AgentContext", "AgentResult", "BaseAgent"] +class ToolUsingAgent(BaseAgent): + """Intermediate base for agents that accept and use tools. + + Sets ``accepts_tools = True`` for CLI/SDK introspection, and + initialises a :class:`ToolExecutor` from the provided tools. + """ + + accepts_tools: bool = True + + def __init__( + self, + engine: InferenceEngine, + model: str, + *, + tools: Optional[List["BaseTool"]] = None, # noqa: F821 + bus: Optional[EventBus] = None, + max_turns: int = 10, + temperature: float = 0.7, + max_tokens: int = 1024, + ) -> None: + super().__init__( + engine, model, bus=bus, + temperature=temperature, max_tokens=max_tokens, + ) + from openjarvis.tools._stubs import ToolExecutor + + self._tools = tools or [] + self._executor = ToolExecutor(self._tools, bus=bus) + self._max_turns = max_turns + + +__all__ = ["AgentContext", "AgentResult", "BaseAgent", "ToolUsingAgent"] diff --git a/src/openjarvis/agents/custom.py b/src/openjarvis/agents/custom.py deleted file mode 100644 index 8eea724e..00000000 --- a/src/openjarvis/agents/custom.py +++ /dev/null @@ -1,34 +0,0 @@ -"""CustomAgent — template for user-defined agents.""" - -from __future__ import annotations - -from typing import Any, Optional - -from openjarvis.agents._stubs import AgentContext, AgentResult, BaseAgent -from openjarvis.core.registry import AgentRegistry - - -@AgentRegistry.register("custom") -class CustomAgent(BaseAgent): - """Template for user-defined agents. - - Subclass this agent and override ``run()`` to implement custom behavior. - Register your subclass with ``@AgentRegistry.register("my-agent")``. - """ - - agent_id = "custom" - - def run( - self, - input: str, - context: Optional[AgentContext] = None, - **kwargs: Any, - ) -> AgentResult: - raise NotImplementedError( - "CustomAgent is a template. Subclass it and override run() " - "to implement your custom agent logic. Register with " - "@AgentRegistry.register('your-agent-name')." - ) - - -__all__ = ["CustomAgent"] diff --git a/src/openjarvis/agents/native_openhands.py b/src/openjarvis/agents/native_openhands.py new file mode 100644 index 00000000..f09c3c8a --- /dev/null +++ b/src/openjarvis/agents/native_openhands.py @@ -0,0 +1,312 @@ +"""NativeOpenHandsAgent -- code-execution-centric agent. + +Renamed from ``OpenHandsAgent`` to clarify this is OpenJarvis's native +CodeAct-style implementation. The ``OpenHandsAgent`` name is now used +for the real openhands-sdk integration in ``openhands.py``. +""" + +from __future__ import annotations + +import json as _json +import re +from typing import Any, List, Optional + +from openjarvis.agents._stubs import AgentContext, AgentResult, ToolUsingAgent +from openjarvis.core.events import EventBus +from openjarvis.core.registry import AgentRegistry +from openjarvis.core.types import Message, Role, ToolCall, ToolResult +from openjarvis.engine._stubs import InferenceEngine +from openjarvis.tools._stubs import BaseTool + +OPENHANDS_SYSTEM_PROMPT = """\ +You are an AI assistant with access to tools. You MUST use tools when they would help answer the user's question. + +## How to use tools + +To call a tool, write on its own lines: + +Action: +Action Input: + +You will receive the result, then continue your response. + +## Available tools + +{tool_descriptions} + +## Important rules + +- When the user asks you to look up, search, fetch, or summarize a URL or topic, you MUST use web_search. Do NOT say you cannot browse the web. +- When the user provides a URL, pass the FULL URL (including https://) as the query to web_search. Do NOT rewrite URLs into search keywords. +- When the user asks a math question, use calculator. +- When the user asks to read a file, use file_read. +- You CAN write Python code in ```python blocks and it will be executed. Use this for computation, data processing, or when no specific tool fits. +- If no tool or code is needed, respond directly with your answer. +- Do NOT include tags or internal reasoning in your response. Respond directly.\ +""" + + +def _build_tool_descriptions(tools: list) -> str: + """Build detailed tool descriptions from ToolSpec objects.""" + if not tools: + return "No tools available." + lines = [] + for t in tools: + s = t.spec + params = s.parameters.get("properties", {}) + required = s.parameters.get("required", []) + param_parts = [] + for pname, pinfo in params.items(): + req_mark = " (required)" if pname in required else "" + param_parts.append( + f" - {pname}{req_mark}: {pinfo.get('description', pinfo.get('type', ''))}" + ) + param_str = "\n".join(param_parts) if param_parts else " (no parameters)" + lines.append(f"### {s.name}\n{s.description}\nParameters:\n{param_str}") + return "\n\n".join(lines) + + +@AgentRegistry.register("native_openhands") +class NativeOpenHandsAgent(ToolUsingAgent): + """Native CodeAct agent -- generates and executes Python code.""" + + agent_id = "native_openhands" + + def __init__( + self, + engine: InferenceEngine, + model: str, + *, + tools: Optional[List[BaseTool]] = None, + bus: Optional[EventBus] = None, + max_turns: int = 3, + temperature: float = 0.7, + max_tokens: int = 2048, + ) -> None: + super().__init__( + engine, model, tools=tools, bus=bus, + max_turns=max_turns, temperature=temperature, + max_tokens=max_tokens, + ) + + @staticmethod + def _expand_urls(text: str) -> tuple[str, bool]: + """If the user message contains a URL, fetch it and inline the content. + + Returns (possibly_expanded_text, was_expanded). + """ + import re as _re + + url_match = _re.search(r"https?://[^\s,;\"'<>]+", text) + if not url_match: + return text, False + url = url_match.group(0).rstrip(".,;)") + try: + from openjarvis.tools.web_search import WebSearchTool + + content = WebSearchTool._fetch_url(url, max_chars=4000) + expanded = text.replace(url, f"\n\n--- Content from {url} ---\n{content}\n--- End of content ---\n") + return expanded, True + except Exception: + return text, False + + def _truncate_if_needed(self, messages: list[Message], max_prompt_tokens: int = 3000) -> list[Message]: + """Truncate messages if estimated token count exceeds limit.""" + total_chars = sum(len(m.content) for m in messages) + estimated_tokens = total_chars // 4 + if estimated_tokens <= max_prompt_tokens: + return messages + # Find the last user message and truncate its content + for i in range(len(messages) - 1, -1, -1): + if messages[i].role == Role.USER: + excess_tokens = estimated_tokens - max_prompt_tokens + excess_chars = excess_tokens * 4 + original = messages[i].content + if len(original) > excess_chars + 200: + truncated = original[: len(original) - excess_chars] + messages[i] = Message( + role=Role.USER, + content=truncated + "\n\n[Input truncated to fit context window]", + ) + break + return messages + + def _extract_code(self, text: str) -> str | None: + """Extract Python code from markdown code blocks.""" + match = re.search(r"```python\n(.*?)```", text, re.DOTALL) + if match: + return match.group(1).strip() + return None + + def _extract_tool_call(self, text: str) -> tuple[str, str] | None: + """Extract tool call from structured output. + + Supports two formats: + 1. Action: tool_name / Action Input: {"key": "value"} + 2. tool_name\\n$key=value (XML-style) + """ + # Format 1: Action / Action Input + action_match = re.search(r"Action:\s*(.+)", text) + input_match = re.search( + r"Action Input:\s*(.+?)(?=\n\n|\Z)", text, re.DOTALL + ) + if action_match: + return ( + action_match.group(1).strip(), + input_match.group(1).strip() if input_match else "{}", + ) + + # Format 2: tool_name ... or + xml_match = re.search( + r"\s*(\w+)\s*(.*?)", + text, + re.DOTALL, + ) + if xml_match: + tool_name = xml_match.group(1).strip() + raw_params = xml_match.group(2).strip() + # Parse $key=value or value params into JSON + params: dict[str, Any] = {} + # $key=value format + for m in re.finditer(r"\$(\w+)=(.+?)(?=\$|\n<|\n") + # value format + for m in re.finditer(r"<(\w+)>(.*?)", raw_params, re.DOTALL): + key, val = m.group(1), m.group(2).strip() + # Try to parse as int + try: + params[key] = int(val) + except ValueError: + params[key] = val + if params: + return (tool_name, _json.dumps(params)) + return (tool_name, "{}") + + return None + + def run( + self, + input: str, + context: Optional[AgentContext] = None, + **kwargs: Any, + ) -> AgentResult: + self._emit_turn_start(input) + + tool_descriptions = _build_tool_descriptions(self._tools) + system_prompt = OPENHANDS_SYSTEM_PROMPT.format(tool_descriptions=tool_descriptions) + + # Pre-fetch any URLs in the input so the LLM gets the content directly + input, url_expanded = self._expand_urls(input) + + # If URL content was inlined, skip the tool loop -- just summarize directly + if url_expanded: + direct_messages: list[Message] = [ + Message(role=Role.SYSTEM, content="You are a helpful assistant. Respond directly to the user's request using the provided content. Do NOT include tags."), + Message(role=Role.USER, content=input), + ] + direct_messages = self._truncate_if_needed(direct_messages) + try: + result = self._generate(direct_messages) + content = self._strip_think_tags(result.get("content", "")) + self._emit_turn_end(turns=1) + return AgentResult(content=content, tool_results=[], turns=1) + except Exception as exc: + error_str = str(exc) + error_msg = ( + "The input is too long for the model's context window. Please try a shorter message." + if "400" in error_str + else f"The model returned an error: {error_str}" + ) + self._emit_turn_end(turns=1, error=True) + return AgentResult(content=error_msg, tool_results=[], turns=1, metadata={"error": True}) + + messages = self._build_messages(input, context, system_prompt=system_prompt) + messages = self._truncate_if_needed(messages) + + all_tool_results: list[ToolResult] = [] + turns = 0 + last_content = "" + + for _turn in range(self._max_turns): + turns += 1 + # Truncate before every generate call -- tool results may have + # expanded the context beyond what the model supports. + messages = self._truncate_if_needed(messages) + + try: + result = self._generate(messages) + except Exception as exc: + error_str = str(exc) + if "400" in error_str: + error_msg = ( + "The input is too long for the model's context window. " + "Please try a shorter message." + ) + else: + error_msg = f"The model returned an error: {error_str}" + self._emit_turn_end(turns=turns, error=True) + return AgentResult( + content=error_msg, + tool_results=all_tool_results, + turns=turns, + metadata={"error": True}, + ) + + content = result.get("content", "") + # Strip think tags so they don't interfere with parsing + content = self._strip_think_tags(content) + last_content = content + + # Try to extract code + code = self._extract_code(content) + if code: + messages.append(Message(role=Role.ASSISTANT, content=content)) + + # Execute via code_interpreter tool if available + tool_call = ToolCall( + id=f"code_{turns}", + name="code_interpreter", + arguments=_json.dumps({"code": code}), + ) + tool_result = self._executor.execute(tool_call) + all_tool_results.append(tool_result) + + obs_text = tool_result.content + if len(obs_text) > 4000: + obs_text = obs_text[:4000] + "\n\n[Output truncated]" + observation = f"Output:\n{obs_text}" + messages.append(Message(role=Role.USER, content=observation)) + continue + + # Try tool call + tool_info = self._extract_tool_call(content) + if tool_info: + action, action_input = tool_info + messages.append(Message(role=Role.ASSISTANT, content=content)) + + tool_call = ToolCall( + id=f"tool_{turns}", name=action, arguments=action_input + ) + tool_result = self._executor.execute(tool_call) + all_tool_results.append(tool_result) + + obs_text = tool_result.content + if len(obs_text) > 4000: + obs_text = obs_text[:4000] + "\n\n[Output truncated]" + observation = f"Result: {obs_text}" + messages.append(Message(role=Role.USER, content=observation)) + continue + + # No code or tool call -- this is the final answer + content = self._strip_think_tags(content) + self._emit_turn_end(turns=turns) + return AgentResult( + content=content, tool_results=all_tool_results, turns=turns + ) + + # Max turns + final = self._strip_think_tags(last_content) or "Maximum turns reached." + return self._max_turns_result(all_tool_results, turns, content=final) + + +__all__ = ["NativeOpenHandsAgent"] diff --git a/src/openjarvis/agents/native_react.py b/src/openjarvis/agents/native_react.py new file mode 100644 index 00000000..ebab4fd2 --- /dev/null +++ b/src/openjarvis/agents/native_react.py @@ -0,0 +1,148 @@ +"""NativeReActAgent -- Thought-Action-Observation loop agent. + +Renamed from ``ReActAgent`` to clarify this is OpenJarvis's native +implementation, not an integration with an external project. +""" + +from __future__ import annotations + +import re +from typing import Any, List, Optional + +from openjarvis.agents._stubs import AgentContext, AgentResult, ToolUsingAgent +from openjarvis.core.events import EventBus +from openjarvis.core.registry import AgentRegistry +from openjarvis.core.types import Message, Role, ToolCall, ToolResult +from openjarvis.engine._stubs import InferenceEngine +from openjarvis.tools._stubs import BaseTool + +REACT_SYSTEM_PROMPT = """\ +You are a ReAct agent. For each step, respond with exactly one of: + +1. To think and act: +Thought: +Action: +Action Input: + +2. To give a final answer: +Thought: +Final Answer: + +Available tools: {tool_names}""" + + +@AgentRegistry.register("native_react") +class NativeReActAgent(ToolUsingAgent): + """ReAct agent: Thought -> Action -> Observation loop.""" + + agent_id = "native_react" + + def __init__( + self, + engine: InferenceEngine, + model: str, + *, + tools: Optional[List[BaseTool]] = None, + bus: Optional[EventBus] = None, + max_turns: int = 10, + temperature: float = 0.7, + max_tokens: int = 1024, + ) -> None: + super().__init__( + engine, model, tools=tools, bus=bus, + max_turns=max_turns, temperature=temperature, + max_tokens=max_tokens, + ) + + def _parse_response(self, text: str) -> dict: + """Parse ReAct structured output.""" + result = {"thought": "", "action": "", "action_input": "", "final_answer": ""} + + # Extract Thought + thought_match = re.search( + r"Thought:\s*(.+?)(?=\nAction:|\nFinal Answer:|\Z)", text, re.DOTALL + ) + if thought_match: + result["thought"] = thought_match.group(1).strip() + + # Check for Final Answer + final_match = re.search(r"Final Answer:\s*(.+)", text, re.DOTALL) + if final_match: + result["final_answer"] = final_match.group(1).strip() + return result + + # Extract Action and Action Input + action_match = re.search(r"Action:\s*(.+)", text) + if action_match: + result["action"] = action_match.group(1).strip() + + input_match = re.search( + r"Action Input:\s*(.+?)(?=\n\n|\nThought:|\Z)", text, re.DOTALL + ) + if input_match: + result["action_input"] = input_match.group(1).strip() + + return result + + def run( + self, + input: str, + context: Optional[AgentContext] = None, + **kwargs: Any, + ) -> AgentResult: + self._emit_turn_start(input) + + # Build system prompt with available tools + tool_names = ( + ", ".join(t.spec.name for t in self._tools) if self._tools else "none" + ) + system_prompt = REACT_SYSTEM_PROMPT.format(tool_names=tool_names) + + messages = self._build_messages(input, context, system_prompt=system_prompt) + + all_tool_results: list[ToolResult] = [] + turns = 0 + + for _turn in range(self._max_turns): + turns += 1 + + result = self._generate(messages) + + content = result.get("content", "") + parsed = self._parse_response(content) + + # Final answer? + if parsed["final_answer"]: + self._emit_turn_end(turns=turns) + return AgentResult( + content=parsed["final_answer"], + tool_results=all_tool_results, + turns=turns, + ) + + # No action? Treat content as final answer + if not parsed["action"]: + self._emit_turn_end(turns=turns) + return AgentResult( + content=content, tool_results=all_tool_results, turns=turns + ) + + # Execute action + messages.append(Message(role=Role.ASSISTANT, content=content)) + + tool_call = ToolCall( + id=f"react_{turns}", + name=parsed["action"], + arguments=parsed["action_input"] or "{}", + ) + tool_result = self._executor.execute(tool_call) + all_tool_results.append(tool_result) + + observation = f"Observation: {tool_result.content}" + messages.append(Message(role=Role.USER, content=observation)) + + # Max turns exceeded + return self._max_turns_result(all_tool_results, turns) + + +__all__ = ["NativeReActAgent", "REACT_SYSTEM_PROMPT"] diff --git a/src/openjarvis/agents/openhands.py b/src/openjarvis/agents/openhands.py index 10d97e18..2273ca9c 100644 --- a/src/openjarvis/agents/openhands.py +++ b/src/openjarvis/agents/openhands.py @@ -1,69 +1,28 @@ -"""OpenHandsAgent -- code-execution-centric agent.""" +"""OpenHandsAgent -- wraps the real openhands-sdk for AI-driven development. + +Requires the ``openhands-sdk`` package (``pip install openjarvis[openhands]``). +For the native CodeAct-style agent, see :mod:`openjarvis.agents.native_openhands`. +""" from __future__ import annotations -import json as _json -import re -from typing import Any, List, Optional +import os +from typing import Any, Optional from openjarvis.agents._stubs import AgentContext, AgentResult, BaseAgent -from openjarvis.core.events import EventBus, EventType +from openjarvis.core.events import EventBus from openjarvis.core.registry import AgentRegistry -from openjarvis.core.types import Message, Role, ToolCall, ToolResult from openjarvis.engine._stubs import InferenceEngine -from openjarvis.tools._stubs import BaseTool, ToolExecutor - -OPENHANDS_SYSTEM_PROMPT = """\ -You are an AI assistant with access to tools. You MUST use tools when they would help answer the user's question. - -## How to use tools - -To call a tool, write on its own lines: - -Action: -Action Input: - -You will receive the result, then continue your response. - -## Available tools - -{tool_descriptions} - -## Important rules - -- When the user asks you to look up, search, fetch, or summarize a URL or topic, you MUST use web_search. Do NOT say you cannot browse the web. -- When the user provides a URL, pass the FULL URL (including https://) as the query to web_search. Do NOT rewrite URLs into search keywords. -- When the user asks a math question, use calculator. -- When the user asks to read a file, use file_read. -- You CAN write Python code in ```python blocks and it will be executed. Use this for computation, data processing, or when no specific tool fits. -- If no tool or code is needed, respond directly with your answer. -- Do NOT include tags or internal reasoning in your response. Respond directly.\ -""" - - -def _build_tool_descriptions(tools: list) -> str: - """Build detailed tool descriptions from ToolSpec objects.""" - if not tools: - return "No tools available." - lines = [] - for t in tools: - s = t.spec - params = s.parameters.get("properties", {}) - required = s.parameters.get("required", []) - param_parts = [] - for pname, pinfo in params.items(): - req_mark = " (required)" if pname in required else "" - param_parts.append( - f" - {pname}{req_mark}: {pinfo.get('description', pinfo.get('type', ''))}" - ) - param_str = "\n".join(param_parts) if param_parts else " (no parameters)" - lines.append(f"### {s.name}\n{s.description}\nParameters:\n{param_str}") - return "\n\n".join(lines) @AgentRegistry.register("openhands") class OpenHandsAgent(BaseAgent): - """OpenHands CodeAct agent -- generates and executes Python code.""" + """Agent that wraps the real openhands-sdk package. + + This is a thin adapter that delegates to the ``openhands-sdk`` + library for AI-driven software development tasks. Requires + ``openhands-sdk`` to be installed. + """ agent_id = "openhands" @@ -72,129 +31,18 @@ class OpenHandsAgent(BaseAgent): engine: InferenceEngine, model: str, *, - tools: Optional[List[BaseTool]] = None, bus: Optional[EventBus] = None, - max_turns: int = 3, temperature: float = 0.7, - max_tokens: int = 2048, + max_tokens: int = 1024, + workspace: Optional[str] = None, + api_key: Optional[str] = None, ) -> None: - self._engine = engine - self._model = model - self._tools = tools or [] - self._executor = ToolExecutor(self._tools, bus=bus) - self._bus = bus - self._max_turns = max_turns - self._temperature = temperature - self._max_tokens = max_tokens - - @staticmethod - def _strip_think_tags(text: str) -> str: - """Remove thinking blocks from model output. - - Handles both ``...`` and the common distilled-model - pattern where the opening ```` is absent and the response - begins directly with reasoning text followed by ````. - """ - # Full ... blocks - text = re.sub(r".*?\s*", "", text, flags=re.DOTALL) - # Leading content before a bare (no opening tag) - text = re.sub(r"^.*?\s*", "", text, flags=re.DOTALL) - return text.strip() - - @staticmethod - def _expand_urls(text: str) -> tuple[str, bool]: - """If the user message contains a URL, fetch it and inline the content. - - Returns (possibly_expanded_text, was_expanded). - """ - import re as _re - - url_match = _re.search(r"https?://[^\s,;\"'<>]+", text) - if not url_match: - return text, False - url = url_match.group(0).rstrip(".,;)") - try: - from openjarvis.tools.web_search import WebSearchTool - - content = WebSearchTool._fetch_url(url, max_chars=4000) - expanded = text.replace(url, f"\n\n--- Content from {url} ---\n{content}\n--- End of content ---\n") - return expanded, True - except Exception: - return text, False - - def _truncate_if_needed(self, messages: list[Message], max_prompt_tokens: int = 3000) -> list[Message]: - """Truncate messages if estimated token count exceeds limit.""" - total_chars = sum(len(m.content) for m in messages) - estimated_tokens = total_chars // 4 - if estimated_tokens <= max_prompt_tokens: - return messages - # Find the last user message and truncate its content - for i in range(len(messages) - 1, -1, -1): - if messages[i].role == Role.USER: - excess_tokens = estimated_tokens - max_prompt_tokens - excess_chars = excess_tokens * 4 - original = messages[i].content - if len(original) > excess_chars + 200: - truncated = original[: len(original) - excess_chars] - messages[i] = Message( - role=Role.USER, - content=truncated + "\n\n[Input truncated to fit context window]", - ) - break - return messages - - def _extract_code(self, text: str) -> str | None: - """Extract Python code from markdown code blocks.""" - match = re.search(r"```python\n(.*?)```", text, re.DOTALL) - if match: - return match.group(1).strip() - return None - - def _extract_tool_call(self, text: str) -> tuple[str, str] | None: - """Extract tool call from structured output. - - Supports two formats: - 1. Action: tool_name / Action Input: {"key": "value"} - 2. tool_name\\n$key=value (XML-style) - """ - # Format 1: Action / Action Input - action_match = re.search(r"Action:\s*(.+)", text) - input_match = re.search( - r"Action Input:\s*(.+?)(?=\n\n|\Z)", text, re.DOTALL + super().__init__( + engine, model, bus=bus, + temperature=temperature, max_tokens=max_tokens, ) - if action_match: - return ( - action_match.group(1).strip(), - input_match.group(1).strip() if input_match else "{}", - ) - - # Format 2: tool_name ... or - xml_match = re.search( - r"\s*(\w+)\s*(.*?)", - text, - re.DOTALL, - ) - if xml_match: - tool_name = xml_match.group(1).strip() - raw_params = xml_match.group(2).strip() - # Parse $key=value or value params into JSON - params: dict[str, Any] = {} - # $key=value format - for m in re.finditer(r"\$(\w+)=(.+?)(?=\$|\n<|\n") - # value format - for m in re.finditer(r"<(\w+)>(.*?)", raw_params, re.DOTALL): - key, val = m.group(1), m.group(2).strip() - # Try to parse as int - try: - params[key] = int(val) - except ValueError: - params[key] = val - if params: - return (tool_name, _json.dumps(params)) - return (tool_name, "{}") - - return None + self._workspace = workspace or os.getcwd() + self._api_key = api_key or os.environ.get("LLM_API_KEY", "") def run( self, @@ -202,165 +50,28 @@ class OpenHandsAgent(BaseAgent): context: Optional[AgentContext] = None, **kwargs: Any, ) -> AgentResult: - bus = self._bus + try: + from openhands.sdk import Agent, Conversation, LLM # type: ignore[import-untyped] + except ImportError: + raise ImportError( + "OpenHandsAgent requires the openhands-sdk package. " + "Install it with: pip install openjarvis[openhands]" + ) from None - if bus: - bus.publish( - EventType.AGENT_TURN_START, - {"agent": self.agent_id, "input": input}, - ) + self._emit_turn_start(input) - tool_descriptions = _build_tool_descriptions(self._tools) - system_prompt = OPENHANDS_SYSTEM_PROMPT.format(tool_descriptions=tool_descriptions) + llm = LLM(model=self._model, api_key=self._api_key) + agent = Agent(llm=llm) + conversation = Conversation(agent=agent, workspace=self._workspace) + conversation.send_message(input) + conversation.run() - # Pre-fetch any URLs in the input so the LLM gets the content directly - input, url_expanded = self._expand_urls(input) + # Extract result from conversation + messages = conversation.get_messages() + content = messages[-1].content if messages else "" - # If URL content was inlined, skip the tool loop — just summarize directly - if url_expanded: - direct_messages: list[Message] = [ - Message(role=Role.SYSTEM, content="You are a helpful assistant. Respond directly to the user's request using the provided content. Do NOT include tags."), - Message(role=Role.USER, content=input), - ] - direct_messages = self._truncate_if_needed(direct_messages) - try: - result = self._engine.generate( - direct_messages, model=self._model, - temperature=self._temperature, - max_tokens=self._max_tokens, - ) - content = self._strip_think_tags(result.get("content", "")) - if bus: - bus.publish(EventType.AGENT_TURN_END, {"agent": self.agent_id, "turns": 1}) - return AgentResult(content=content, tool_results=[], turns=1) - except Exception as exc: - error_str = str(exc) - error_msg = ( - "The input is too long for the model's context window. Please try a shorter message." - if "400" in error_str - else f"The model returned an error: {error_str}" - ) - if bus: - bus.publish(EventType.AGENT_TURN_END, {"agent": self.agent_id, "turns": 1, "error": True}) - return AgentResult(content=error_msg, tool_results=[], turns=1, metadata={"error": True}) - - messages: list[Message] = [Message(role=Role.SYSTEM, content=system_prompt)] - if context and context.conversation.messages: - messages.extend(context.conversation.messages) - messages.append(Message(role=Role.USER, content=input)) - messages = self._truncate_if_needed(messages) - - all_tool_results: list[ToolResult] = [] - turns = 0 - last_content = "" - - for _turn in range(self._max_turns): - turns += 1 - # Truncate before every generate call — tool results may have - # expanded the context beyond what the model supports. - messages = self._truncate_if_needed(messages) - - try: - result = self._engine.generate( - messages, - model=self._model, - temperature=self._temperature, - max_tokens=self._max_tokens, - ) - except Exception as exc: - error_str = str(exc) - if "400" in error_str: - error_msg = ( - "The input is too long for the model's context window. " - "Please try a shorter message." - ) - else: - error_msg = f"The model returned an error: {error_str}" - if bus: - bus.publish( - EventType.AGENT_TURN_END, - {"agent": self.agent_id, "turns": turns, "error": True}, - ) - return AgentResult( - content=error_msg, - tool_results=all_tool_results, - turns=turns, - metadata={"error": True}, - ) - - content = result.get("content", "") - # Strip think tags so they don't interfere with parsing - content = self._strip_think_tags(content) - last_content = content - - # Try to extract code - code = self._extract_code(content) - if code: - messages.append(Message(role=Role.ASSISTANT, content=content)) - - # Execute via code_interpreter tool if available - tool_call = ToolCall( - id=f"code_{turns}", - name="code_interpreter", - arguments=_json.dumps({"code": code}), - ) - tool_result = self._executor.execute(tool_call) - all_tool_results.append(tool_result) - - obs_text = tool_result.content - if len(obs_text) > 4000: - obs_text = obs_text[:4000] + "\n\n[Output truncated]" - observation = f"Output:\n{obs_text}" - messages.append(Message(role=Role.USER, content=observation)) - continue - - # Try tool call - tool_info = self._extract_tool_call(content) - if tool_info: - action, action_input = tool_info - messages.append(Message(role=Role.ASSISTANT, content=content)) - - tool_call = ToolCall( - id=f"tool_{turns}", name=action, arguments=action_input - ) - tool_result = self._executor.execute(tool_call) - all_tool_results.append(tool_result) - - obs_text = tool_result.content - if len(obs_text) > 4000: - obs_text = obs_text[:4000] + "\n\n[Output truncated]" - observation = f"Result: {obs_text}" - messages.append(Message(role=Role.USER, content=observation)) - continue - - # No code or tool call -- this is the final answer - content = self._strip_think_tags(content) - if bus: - bus.publish( - EventType.AGENT_TURN_END, - {"agent": self.agent_id, "turns": turns}, - ) - return AgentResult( - content=content, tool_results=all_tool_results, turns=turns - ) - - # Max turns - if bus: - bus.publish( - EventType.AGENT_TURN_END, - { - "agent": self.agent_id, - "turns": turns, - "max_turns_exceeded": True, - }, - ) - - return AgentResult( - content=self._strip_think_tags(last_content) or "Maximum turns reached.", - tool_results=all_tool_results, - turns=turns, - metadata={"max_turns_exceeded": True}, - ) + self._emit_turn_end(turns=1) + return AgentResult(content=content, turns=1) __all__ = ["OpenHandsAgent"] diff --git a/src/openjarvis/agents/orchestrator.py b/src/openjarvis/agents/orchestrator.py index 69eeaf75..f6685d5e 100644 --- a/src/openjarvis/agents/orchestrator.py +++ b/src/openjarvis/agents/orchestrator.py @@ -15,16 +15,16 @@ from __future__ import annotations import re from typing import Any, List, Optional -from openjarvis.agents._stubs import AgentContext, AgentResult, BaseAgent -from openjarvis.core.events import EventBus, EventType +from openjarvis.agents._stubs import AgentContext, AgentResult, ToolUsingAgent +from openjarvis.core.events import EventBus from openjarvis.core.registry import AgentRegistry from openjarvis.core.types import Message, Role, ToolCall, ToolResult from openjarvis.engine._stubs import InferenceEngine -from openjarvis.tools._stubs import BaseTool, ToolExecutor +from openjarvis.tools._stubs import BaseTool @AgentRegistry.register("orchestrator") -class OrchestratorAgent(BaseAgent): +class OrchestratorAgent(ToolUsingAgent): """Multi-turn agent that routes between tools and the LLM. Implements a tool-calling loop: @@ -54,14 +54,11 @@ class OrchestratorAgent(BaseAgent): mode: str = "function_calling", system_prompt: Optional[str] = None, ) -> None: - self._engine = engine - self._model = model - self._tools = tools or [] - self._executor = ToolExecutor(self._tools, bus=bus) - self._bus = bus - self._max_turns = max_turns - self._temperature = temperature - self._max_tokens = max_tokens + super().__init__( + engine, model, tools=tools, bus=bus, + max_turns=max_turns, temperature=temperature, + max_tokens=max_tokens, + ) self._mode = mode self._system_prompt = system_prompt @@ -85,13 +82,7 @@ class OrchestratorAgent(BaseAgent): context: Optional[AgentContext] = None, **kwargs: Any, ) -> AgentResult: - bus = self._bus - - if bus: - bus.publish( - EventType.AGENT_TURN_START, - {"agent": self.agent_id, "input": input}, - ) + self._emit_turn_start(input) # Build system prompt if self._system_prompt: @@ -103,12 +94,7 @@ class OrchestratorAgent(BaseAgent): tool_names = [t.spec.name for t in self._tools] sys_prompt = build_system_prompt(tool_names) - messages: list[Message] = [ - Message(role=Role.SYSTEM, content=sys_prompt), - ] - if context and context.conversation.messages: - messages.extend(context.conversation.messages) - messages.append(Message(role=Role.USER, content=input)) + messages = self._build_messages(input, context, system_prompt=sys_prompt) all_tool_results: list[ToolResult] = [] turns = 0 @@ -116,30 +102,21 @@ class OrchestratorAgent(BaseAgent): for _turn in range(self._max_turns): turns += 1 - result = self._engine.generate( - messages, - model=self._model, - temperature=self._temperature, - max_tokens=self._max_tokens, - ) + result = self._generate(messages) content = result.get("content", "") parsed = self._parse_structured_response(content) - # FINAL_ANSWER → done + # FINAL_ANSWER -> done if parsed["final_answer"]: - if bus: - bus.publish( - EventType.AGENT_TURN_END, - {"agent": self.agent_id, "turns": turns}, - ) + self._emit_turn_end(turns=turns) return AgentResult( content=parsed["final_answer"], tool_results=all_tool_results, turns=turns, ) - # TOOL → execute + # TOOL -> execute if parsed["tool"]: messages.append( Message(role=Role.ASSISTANT, content=content) @@ -161,12 +138,8 @@ class OrchestratorAgent(BaseAgent): ) continue - # Neither → treat content as final answer - if bus: - bus.publish( - EventType.AGENT_TURN_END, - {"agent": self.agent_id, "turns": turns}, - ) + # Neither -> treat content as final answer + self._emit_turn_end(turns=turns) return AgentResult( content=content, tool_results=all_tool_results, @@ -174,21 +147,7 @@ class OrchestratorAgent(BaseAgent): ) # Max turns exceeded - if bus: - bus.publish( - EventType.AGENT_TURN_END, - { - "agent": self.agent_id, - "turns": turns, - "max_turns_exceeded": True, - }, - ) - return AgentResult( - content="Maximum turns reached without a final answer.", - tool_results=all_tool_results, - turns=turns, - metadata={"max_turns_exceeded": True}, - ) + return self._max_turns_result(all_tool_results, turns) @staticmethod def _parse_structured_response(text: str) -> dict: @@ -243,20 +202,10 @@ class OrchestratorAgent(BaseAgent): context: Optional[AgentContext] = None, **kwargs: Any, ) -> AgentResult: - bus = self._bus - - # Emit agent start - if bus: - bus.publish(EventType.AGENT_TURN_START, { - "agent": self.agent_id, - "input": input, - }) + self._emit_turn_start(input) # Build initial messages - messages: list[Message] = [] - if context and context.conversation.messages: - messages.extend(context.conversation.messages) - messages.append(Message(role=Role.USER, content=input)) + messages = self._build_messages(input, context) # Get OpenAI-format tool definitions openai_tools = self._executor.get_openai_tools() if self._tools else [] @@ -272,25 +221,14 @@ class OrchestratorAgent(BaseAgent): if openai_tools: gen_kwargs["tools"] = openai_tools - result = self._engine.generate( - messages, - model=self._model, - temperature=self._temperature, - max_tokens=self._max_tokens, - **gen_kwargs, - ) + result = self._generate(messages, **gen_kwargs) content = result.get("content", "") raw_tool_calls = result.get("tool_calls", []) - # No tool calls → final answer + # No tool calls -> final answer if not raw_tool_calls: - if bus: - bus.publish(EventType.AGENT_TURN_END, { - "agent": self.agent_id, - "turns": turns, - "content_length": len(content), - }) + self._emit_turn_end(turns=turns, content_length=len(content)) return AgentResult( content=content, tool_results=all_tool_results, @@ -328,23 +266,8 @@ class OrchestratorAgent(BaseAgent): )) # Max turns exceeded - if bus: - bus.publish(EventType.AGENT_TURN_END, { - "agent": self.agent_id, - "turns": turns, - "max_turns_exceeded": True, - }) - - # Try to provide last content or a warning - final_content = content if content else ( - "Maximum turns reached without a final answer." - ) - return AgentResult( - content=final_content, - tool_results=all_tool_results, - turns=turns, - metadata={"max_turns_exceeded": True}, - ) + final_content = content if content else "" + return self._max_turns_result(all_tool_results, turns, content=final_content) __all__ = ["OrchestratorAgent"] diff --git a/src/openjarvis/agents/react.py b/src/openjarvis/agents/react.py index 3ca1141e..e25a2154 100644 --- a/src/openjarvis/agents/react.py +++ b/src/openjarvis/agents/react.py @@ -1,184 +1,6 @@ -"""ReActAgent -- Thought-Action-Observation loop agent.""" +"""Backward-compat shim -- canonical location is agents.native_react.""" -from __future__ import annotations +from openjarvis.agents.native_react import NativeReActAgent as ReActAgent # noqa: F401 +from openjarvis.agents.native_react import REACT_SYSTEM_PROMPT # noqa: F401 -import re -from typing import Any, List, Optional - -from openjarvis.agents._stubs import AgentContext, AgentResult, BaseAgent -from openjarvis.core.events import EventBus, EventType -from openjarvis.core.registry import AgentRegistry -from openjarvis.core.types import Message, Role, ToolCall, ToolResult -from openjarvis.engine._stubs import InferenceEngine -from openjarvis.tools._stubs import BaseTool, ToolExecutor - -REACT_SYSTEM_PROMPT = """\ -You are a ReAct agent. For each step, respond with exactly one of: - -1. To think and act: -Thought: -Action: -Action Input: - -2. To give a final answer: -Thought: -Final Answer: - -Available tools: {tool_names}""" - - -@AgentRegistry.register("react") -class ReActAgent(BaseAgent): - """ReAct agent: Thought -> Action -> Observation loop.""" - - agent_id = "react" - - def __init__( - self, - engine: InferenceEngine, - model: str, - *, - tools: Optional[List[BaseTool]] = None, - bus: Optional[EventBus] = None, - max_turns: int = 10, - temperature: float = 0.7, - max_tokens: int = 1024, - ) -> None: - self._engine = engine - self._model = model - self._tools = tools or [] - self._executor = ToolExecutor(self._tools, bus=bus) - self._bus = bus - self._max_turns = max_turns - self._temperature = temperature - self._max_tokens = max_tokens - - def _parse_response(self, text: str) -> dict: - """Parse ReAct structured output.""" - result = {"thought": "", "action": "", "action_input": "", "final_answer": ""} - - # Extract Thought - thought_match = re.search( - r"Thought:\s*(.+?)(?=\nAction:|\nFinal Answer:|\Z)", text, re.DOTALL - ) - if thought_match: - result["thought"] = thought_match.group(1).strip() - - # Check for Final Answer - final_match = re.search(r"Final Answer:\s*(.+)", text, re.DOTALL) - if final_match: - result["final_answer"] = final_match.group(1).strip() - return result - - # Extract Action and Action Input - action_match = re.search(r"Action:\s*(.+)", text) - if action_match: - result["action"] = action_match.group(1).strip() - - input_match = re.search( - r"Action Input:\s*(.+?)(?=\n\n|\nThought:|\Z)", text, re.DOTALL - ) - if input_match: - result["action_input"] = input_match.group(1).strip() - - return result - - def run( - self, - input: str, - context: Optional[AgentContext] = None, - **kwargs: Any, - ) -> AgentResult: - bus = self._bus - - if bus: - bus.publish( - EventType.AGENT_TURN_START, - {"agent": self.agent_id, "input": input}, - ) - - # Build system prompt with available tools - tool_names = ( - ", ".join(t.spec.name for t in self._tools) if self._tools else "none" - ) - system_prompt = REACT_SYSTEM_PROMPT.format(tool_names=tool_names) - - messages: list[Message] = [Message(role=Role.SYSTEM, content=system_prompt)] - if context and context.conversation.messages: - messages.extend(context.conversation.messages) - messages.append(Message(role=Role.USER, content=input)) - - all_tool_results: list[ToolResult] = [] - turns = 0 - - for _turn in range(self._max_turns): - turns += 1 - - result = self._engine.generate( - messages, - model=self._model, - temperature=self._temperature, - max_tokens=self._max_tokens, - ) - - content = result.get("content", "") - parsed = self._parse_response(content) - - # Final answer? - if parsed["final_answer"]: - if bus: - bus.publish( - EventType.AGENT_TURN_END, - {"agent": self.agent_id, "turns": turns}, - ) - return AgentResult( - content=parsed["final_answer"], - tool_results=all_tool_results, - turns=turns, - ) - - # No action? Treat content as final answer - if not parsed["action"]: - if bus: - bus.publish( - EventType.AGENT_TURN_END, - {"agent": self.agent_id, "turns": turns}, - ) - return AgentResult( - content=content, tool_results=all_tool_results, turns=turns - ) - - # Execute action - messages.append(Message(role=Role.ASSISTANT, content=content)) - - tool_call = ToolCall( - id=f"react_{turns}", - name=parsed["action"], - arguments=parsed["action_input"] or "{}", - ) - tool_result = self._executor.execute(tool_call) - all_tool_results.append(tool_result) - - observation = f"Observation: {tool_result.content}" - messages.append(Message(role=Role.USER, content=observation)) - - # Max turns exceeded - if bus: - bus.publish( - EventType.AGENT_TURN_END, - { - "agent": self.agent_id, - "turns": turns, - "max_turns_exceeded": True, - }, - ) - - return AgentResult( - content="Maximum turns reached without a final answer.", - tool_results=all_tool_results, - turns=turns, - metadata={"max_turns_exceeded": True}, - ) - - -__all__ = ["ReActAgent"] +__all__ = ["ReActAgent", "REACT_SYSTEM_PROMPT"] diff --git a/src/openjarvis/agents/rlm.py b/src/openjarvis/agents/rlm.py index 0c27ec97..7a363fd3 100644 --- a/src/openjarvis/agents/rlm.py +++ b/src/openjarvis/agents/rlm.py @@ -11,9 +11,9 @@ from __future__ import annotations import re from typing import Any, List, Optional -from openjarvis.agents._stubs import AgentContext, AgentResult, BaseAgent +from openjarvis.agents._stubs import AgentContext, AgentResult, ToolUsingAgent from openjarvis.agents.rlm_repl import RLMRepl -from openjarvis.core.events import EventBus, EventType +from openjarvis.core.events import EventBus from openjarvis.core.registry import AgentRegistry from openjarvis.core.types import Message, Role, ToolCall, ToolResult from openjarvis.engine._stubs import InferenceEngine @@ -72,7 +72,7 @@ RLM_SYSTEM_PROMPT = ( @AgentRegistry.register("rlm") -class RLMAgent(BaseAgent): +class RLMAgent(ToolUsingAgent): """Recursive Language Model agent using a persistent REPL. The agent generates Python code that runs in a sandboxed REPL with @@ -100,14 +100,14 @@ class RLMAgent(BaseAgent): max_output_chars: int = 10000, system_prompt: Optional[str] = None, ) -> None: - self._engine = engine - self._model = model - self._tools = tools or [] - self._executor = ToolExecutor(self._tools, bus=bus) if self._tools else None - self._bus = bus - self._max_turns = max_turns - self._temperature = temperature - self._max_tokens = max_tokens + super().__init__( + engine, model, tools=tools, bus=bus, + max_turns=max_turns, temperature=temperature, + max_tokens=max_tokens, + ) + # Override executor: RLM only creates one if tools are provided + if not self._tools: + self._executor = None # type: ignore[assignment] self._sub_model = sub_model or model self._sub_temperature = sub_temperature self._sub_max_tokens = sub_max_tokens @@ -124,14 +124,7 @@ class RLMAgent(BaseAgent): context: Optional[AgentContext] = None, **kwargs: Any, ) -> AgentResult: - bus = self._bus - - # Emit agent start - if bus: - bus.publish( - EventType.AGENT_TURN_START, - {"agent": self.agent_id, "input": input}, - ) + self._emit_turn_start(input) # Create REPL with sub-LM callbacks repl = RLMRepl( @@ -146,12 +139,9 @@ class RLMAgent(BaseAgent): repl.set_variable("context", ctx_text) # Build conversation - messages: list[Message] = [ - Message(role=Role.SYSTEM, content=self._system_prompt), - ] - if context and context.conversation.messages: - messages.extend(context.conversation.messages) - messages.append(Message(role=Role.USER, content=input)) + messages = self._build_messages( + input, context, system_prompt=self._system_prompt, + ) all_tool_results: list[ToolResult] = [] turns = 0 @@ -159,12 +149,7 @@ class RLMAgent(BaseAgent): for _turn in range(self._max_turns): turns += 1 - result = self._engine.generate( - messages, - model=self._model, - temperature=self._temperature, - max_tokens=self._max_tokens, - ) + result = self._generate(messages) content = result.get("content", "") # Strip tags @@ -173,13 +158,9 @@ class RLMAgent(BaseAgent): # Extract code block code = self._extract_code(content) - # No code block → return content as final answer + # No code block -> return content as final answer if code is None: - if bus: - bus.publish( - EventType.AGENT_TURN_END, - {"agent": self.agent_id, "turns": turns}, - ) + self._emit_turn_end(turns=turns) return AgentResult( content=content, tool_results=all_tool_results, @@ -204,11 +185,7 @@ class RLMAgent(BaseAgent): if repl.is_terminated: final = repl.final_answer final_str = str(final) if final is not None else "" - if bus: - bus.publish( - EventType.AGENT_TURN_END, - {"agent": self.agent_id, "turns": turns}, - ) + self._emit_turn_end(turns=turns) return AgentResult( content=final_str, tool_results=all_tool_results, @@ -224,29 +201,14 @@ class RLMAgent(BaseAgent): ) messages.append(Message(role=Role.USER, content=feedback)) - # Max turns exceeded — check answer dict for partial result + # Max turns exceeded -- check answer dict for partial result answer = repl.get_variable("answer") if isinstance(answer, dict) and answer.get("value") is not None: final_content = str(answer["value"]) else: - final_content = "Maximum turns reached without a final answer." + final_content = "" - if bus: - bus.publish( - EventType.AGENT_TURN_END, - { - "agent": self.agent_id, - "turns": turns, - "max_turns_exceeded": True, - }, - ) - - return AgentResult( - content=final_content, - tool_results=all_tool_results, - turns=turns, - metadata={"max_turns_exceeded": True}, - ) + return self._max_turns_result(all_tool_results, turns, content=final_content) # ------------------------------------------------------------------ # Sub-LM callbacks @@ -330,11 +292,6 @@ class RLMAgent(BaseAgent): return m.group(1).strip() return None - @staticmethod - def _strip_think_tags(text: str) -> str: - """Remove ``...`` blocks from model output.""" - return re.sub(r".*?", "", text, flags=re.DOTALL).strip() - @staticmethod def _resolve_context(context: Optional[AgentContext]) -> Optional[str]: """Resolve context text from AgentContext metadata or memory_results.""" diff --git a/src/openjarvis/agents/simple.py b/src/openjarvis/agents/simple.py index 095e07e2..9af56b59 100644 --- a/src/openjarvis/agents/simple.py +++ b/src/openjarvis/agents/simple.py @@ -5,33 +5,15 @@ from __future__ import annotations from typing import Any, Optional from openjarvis.agents._stubs import AgentContext, AgentResult, BaseAgent -from openjarvis.core.events import EventBus, EventType from openjarvis.core.registry import AgentRegistry -from openjarvis.core.types import Message, Role -from openjarvis.engine._stubs import InferenceEngine @AgentRegistry.register("simple") class SimpleAgent(BaseAgent): - """Single-turn agent: query → model → response. No tool calling.""" + """Single-turn agent: query -> model -> response. No tool calling.""" agent_id = "simple" - def __init__( - self, - engine: InferenceEngine, - model: str, - *, - bus: Optional[EventBus] = None, - temperature: float = 0.7, - max_tokens: int = 1024, - ) -> None: - self._engine = engine - self._model = model - self._bus = bus - self._temperature = temperature - self._max_tokens = max_tokens - def run( self, input: str, @@ -39,37 +21,13 @@ class SimpleAgent(BaseAgent): **kwargs: Any, ) -> AgentResult: """Single-turn: build messages, call engine, return result.""" - bus = self._bus - - # Emit turn start - if bus: - bus.publish(EventType.AGENT_TURN_START, { - "agent": self.agent_id, - "input": input, - }) - - # Build messages from context conversation + user input - messages: list[Message] = [] - if context and context.conversation.messages: - messages.extend(context.conversation.messages) - messages.append(Message(role=Role.USER, content=input)) - - # Generate — telemetry is handled by InstrumentedEngine wrapper - result = self._engine.generate( - messages, - model=self._model, - temperature=self._temperature, - max_tokens=self._max_tokens, - ) + self._emit_turn_start(input) + messages = self._build_messages(input, context) + result = self._generate(messages) content = result.get("content", "") - # Emit turn end - if bus: - bus.publish(EventType.AGENT_TURN_END, { - "agent": self.agent_id, - "content_length": len(content), - }) + self._emit_turn_end(content_length=len(content)) return AgentResult(content=content, turns=1) diff --git a/src/openjarvis/cli/ask.py b/src/openjarvis/cli/ask.py index bce2217d..3a087106 100644 --- a/src/openjarvis/cli/ask.py +++ b/src/openjarvis/cli/ask.py @@ -116,7 +116,7 @@ def _run_agent( "temperature": temperature, "max_tokens": max_tokens, } - if agent_name == "orchestrator": + if getattr(agent_cls, "accepts_tools", False): agent_kwargs["tools"] = tools agent_kwargs["max_turns"] = config.agent.max_turns diff --git a/src/openjarvis/sdk.py b/src/openjarvis/sdk.py index 56942df1..414fe85d 100644 --- a/src/openjarvis/sdk.py +++ b/src/openjarvis/sdk.py @@ -363,7 +363,7 @@ class Jarvis: "temperature": temperature, "max_tokens": max_tokens, } - if agent_name == "orchestrator": + if getattr(agent_cls, "accepts_tools", False): agent_kwargs["tools"] = tool_objects agent_kwargs["max_turns"] = self._config.agent.max_turns diff --git a/src/openjarvis/system.py b/src/openjarvis/system.py index 9b468bdc..3e1efc76 100644 --- a/src/openjarvis/system.py +++ b/src/openjarvis/system.py @@ -122,10 +122,7 @@ class JarvisSystem: "temperature": temperature, "max_tokens": max_tokens, } - if agent_name == "orchestrator": - agent_kwargs["tools"] = agent_tools - agent_kwargs["max_turns"] = self.config.agent.max_turns - elif agent_name == "rlm": + if getattr(agent_cls, "accepts_tools", False): agent_kwargs["tools"] = agent_tools agent_kwargs["max_turns"] = self.config.agent.max_turns diff --git a/tests/agents/test_agent_routing_matrix.py b/tests/agents/test_agent_routing_matrix.py index 81fc48c4..dbc2ef03 100644 --- a/tests/agents/test_agent_routing_matrix.py +++ b/tests/agents/test_agent_routing_matrix.py @@ -7,9 +7,9 @@ from unittest.mock import MagicMock import pytest from openjarvis.agents._stubs import AgentResult -from openjarvis.agents.openhands import OpenHandsAgent +from openjarvis.agents.native_openhands import NativeOpenHandsAgent +from openjarvis.agents.native_react import NativeReActAgent from openjarvis.agents.orchestrator import OrchestratorAgent -from openjarvis.agents.react import ReActAgent from openjarvis.agents.simple import SimpleAgent from openjarvis.core.events import EventBus, EventType @@ -40,15 +40,15 @@ _ORCHESTRATOR_RESPONSE = "Hello!" AGENT_FACTORIES = { "simple": lambda e, m, bus: SimpleAgent(e, m, bus=bus), "orchestrator": lambda e, m, bus: OrchestratorAgent(e, m, bus=bus), - "react": lambda e, m, bus: ReActAgent(e, m, bus=bus), - "openhands": lambda e, m, bus: OpenHandsAgent(e, m, bus=bus), + "native_react": lambda e, m, bus: NativeReActAgent(e, m, bus=bus), + "native_openhands": lambda e, m, bus: NativeOpenHandsAgent(e, m, bus=bus), } AGENT_RESPONSES = { "simple": _SIMPLE_RESPONSE, "orchestrator": _ORCHESTRATOR_RESPONSE, - "react": _REACT_RESPONSE, - "openhands": _OPENHANDS_RESPONSE, + "native_react": _REACT_RESPONSE, + "native_openhands": _OPENHANDS_RESPONSE, } @@ -57,7 +57,7 @@ AGENT_RESPONSES = { # --------------------------------------------------------------------------- -@pytest.mark.parametrize("agent_key", ["simple", "orchestrator", "react", "openhands"]) +@pytest.mark.parametrize("agent_key", ["simple", "orchestrator", "native_react", "native_openhands"]) class TestAgentCommon: def test_runs_with_mock_engine(self, agent_key): """Each agent type can run with a mock engine.""" @@ -114,8 +114,8 @@ class TestAgentCommon: [ ("simple", "simple"), ("orchestrator", "orchestrator"), - ("react", "react"), - ("openhands", "openhands"), + ("native_react", "native_react"), + ("native_openhands", "native_openhands"), ], ) def test_agent_id(agent_key, expected_id): @@ -129,7 +129,7 @@ def test_agent_id(agent_key, expected_id): # --------------------------------------------------------------------------- -@pytest.mark.parametrize("agent_key", ["simple", "orchestrator", "react", "openhands"]) +@pytest.mark.parametrize("agent_key", ["simple", "orchestrator", "native_react", "native_openhands"]) @pytest.mark.parametrize("model", ["qwen3:8b", "llama3:70b", "gpt-oss:120b"]) def test_model_passthrough(agent_key, model): """Each agent passes the model name through to the engine.""" @@ -145,7 +145,7 @@ def test_model_passthrough(agent_key, model): # --------------------------------------------------------------------------- -@pytest.mark.parametrize("agent_key", ["simple", "orchestrator", "react", "openhands"]) +@pytest.mark.parametrize("agent_key", ["simple", "orchestrator", "native_react", "native_openhands"]) def test_no_bus(agent_key): """All agents work without an event bus.""" engine = _make_mock_engine(AGENT_RESPONSES[agent_key]) @@ -160,7 +160,7 @@ def test_no_bus(agent_key): # --------------------------------------------------------------------------- -@pytest.mark.parametrize("agent_key", ["simple", "orchestrator", "react", "openhands"]) +@pytest.mark.parametrize("agent_key", ["simple", "orchestrator", "native_react", "native_openhands"]) def test_single_turn_count(agent_key): """All agents report at least 1 turn for a simple query.""" engine = _make_mock_engine(AGENT_RESPONSES[agent_key]) @@ -174,7 +174,7 @@ def test_single_turn_count(agent_key): # --------------------------------------------------------------------------- -@pytest.mark.parametrize("agent_key", ["simple", "orchestrator", "react", "openhands"]) +@pytest.mark.parametrize("agent_key", ["simple", "orchestrator", "native_react", "native_openhands"]) def test_no_tool_results_for_simple_query(agent_key): """When no tools are used, tool_results should be empty.""" engine = _make_mock_engine(AGENT_RESPONSES[agent_key]) diff --git a/tests/agents/test_backward_compat.py b/tests/agents/test_backward_compat.py new file mode 100644 index 00000000..1d7a8e64 --- /dev/null +++ b/tests/agents/test_backward_compat.py @@ -0,0 +1,43 @@ +"""Tests for backward compatibility of renamed agents.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +from openjarvis.agents.native_react import NativeReActAgent +from openjarvis.core.registry import AgentRegistry + + +class TestReActBackwardCompat: + def test_old_import_path(self): + """Old import ``from openjarvis.agents.react import ReActAgent`` works.""" + from openjarvis.agents.react import ReActAgent + + # ReActAgent is actually NativeReActAgent + assert ReActAgent is NativeReActAgent + + def test_registry_alias(self): + """``AgentRegistry.get("react")`` returns NativeReActAgent.""" + # Ensure registration + AgentRegistry.register_value("native_react", NativeReActAgent) + if not AgentRegistry.contains("react"): + AgentRegistry.register_value("react", NativeReActAgent) + + react_cls = AgentRegistry.get("react") + native_cls = AgentRegistry.get("native_react") + assert react_cls is native_cls + + def test_old_class_instantiates(self): + """ReActAgent (alias) can be instantiated and has correct agent_id.""" + from openjarvis.agents.react import ReActAgent + + engine = MagicMock() + engine.engine_id = "mock" + agent = ReActAgent(engine, "test-model") + assert agent.agent_id == "native_react" + + def test_react_system_prompt_importable(self): + """REACT_SYSTEM_PROMPT can be imported from old path.""" + from openjarvis.agents.react import REACT_SYSTEM_PROMPT + + assert "ReAct" in REACT_SYSTEM_PROMPT diff --git a/tests/agents/test_base_agent.py b/tests/agents/test_base_agent.py new file mode 100644 index 00000000..ef3ba606 --- /dev/null +++ b/tests/agents/test_base_agent.py @@ -0,0 +1,275 @@ +"""Tests for BaseAgent helpers and ToolUsingAgent.""" + +from __future__ import annotations + +from typing import Any, Optional +from unittest.mock import MagicMock + +import pytest + +from openjarvis.agents._stubs import ( + AgentContext, + AgentResult, + BaseAgent, + ToolUsingAgent, +) +from openjarvis.core.events import EventBus, EventType +from openjarvis.core.types import Conversation, Message, Role, ToolResult +from openjarvis.tools._stubs import BaseTool, ToolSpec + + +# --------------------------------------------------------------------------- +# Concrete subclass for testing +# --------------------------------------------------------------------------- + + +class _ConcreteAgent(BaseAgent): + agent_id = "test_agent" + + def run(self, input, context=None, **kwargs): + return AgentResult(content="test", turns=1) + + +class _ConcreteToolAgent(ToolUsingAgent): + agent_id = "test_tool_agent" + + def run(self, input, context=None, **kwargs): + return AgentResult(content="test", turns=1) + + +class _DummyTool(BaseTool): + tool_id = "dummy" + + @property + def spec(self) -> ToolSpec: + return ToolSpec( + name="dummy", + description="Dummy tool.", + parameters={"type": "object", "properties": {}}, + ) + + def execute(self, **params) -> ToolResult: + return ToolResult(tool_name="dummy", content="ok", success=True) + + +# --------------------------------------------------------------------------- +# BaseAgent tests +# --------------------------------------------------------------------------- + + +class TestBaseAgentInit: + def test_stores_engine_and_model(self): + engine = MagicMock() + agent = _ConcreteAgent(engine, "test-model") + assert agent._engine is engine + assert agent._model == "test-model" + + def test_default_params(self): + engine = MagicMock() + agent = _ConcreteAgent(engine, "m") + assert agent._temperature == 0.7 + assert agent._max_tokens == 1024 + assert agent._bus is None + + def test_custom_params(self): + bus = EventBus() + engine = MagicMock() + agent = _ConcreteAgent( + engine, "m", bus=bus, temperature=0.1, max_tokens=256, + ) + assert agent._temperature == 0.1 + assert agent._max_tokens == 256 + assert agent._bus is bus + + +class TestAcceptsTools: + def test_base_agent_no_tools(self): + assert _ConcreteAgent.accepts_tools is False + + def test_tool_using_agent_accepts_tools(self): + assert _ConcreteToolAgent.accepts_tools is True + + +class TestEmitTurnStart: + def test_with_bus(self): + bus = EventBus(record_history=True) + engine = MagicMock() + agent = _ConcreteAgent(engine, "m", bus=bus) + agent._emit_turn_start("hello") + events = [e for e in bus.history if e.event_type == EventType.AGENT_TURN_START] + assert len(events) == 1 + assert events[0].data["agent"] == "test_agent" + assert events[0].data["input"] == "hello" + + def test_without_bus(self): + engine = MagicMock() + agent = _ConcreteAgent(engine, "m") + # Should not raise + agent._emit_turn_start("hello") + + +class TestEmitTurnEnd: + def test_with_bus(self): + bus = EventBus(record_history=True) + engine = MagicMock() + agent = _ConcreteAgent(engine, "m", bus=bus) + agent._emit_turn_end(turns=3, custom="val") + events = [e for e in bus.history if e.event_type == EventType.AGENT_TURN_END] + assert len(events) == 1 + assert events[0].data["agent"] == "test_agent" + assert events[0].data["turns"] == 3 + assert events[0].data["custom"] == "val" + + def test_without_bus(self): + engine = MagicMock() + agent = _ConcreteAgent(engine, "m") + agent._emit_turn_end(turns=1) + + +class TestBuildMessages: + def test_basic(self): + engine = MagicMock() + agent = _ConcreteAgent(engine, "m") + messages = agent._build_messages("hello") + assert len(messages) == 1 + assert messages[0].role == Role.USER + assert messages[0].content == "hello" + + def test_with_system_prompt(self): + engine = MagicMock() + agent = _ConcreteAgent(engine, "m") + messages = agent._build_messages("hello", system_prompt="Be helpful.") + assert len(messages) == 2 + assert messages[0].role == Role.SYSTEM + assert messages[0].content == "Be helpful." + assert messages[1].role == Role.USER + + def test_with_context(self): + engine = MagicMock() + agent = _ConcreteAgent(engine, "m") + conv = Conversation() + conv.add(Message(role=Role.USER, content="prev")) + conv.add(Message(role=Role.ASSISTANT, content="reply")) + ctx = AgentContext(conversation=conv) + messages = agent._build_messages("new", ctx) + assert len(messages) == 3 + assert messages[0].content == "prev" + assert messages[1].content == "reply" + assert messages[2].content == "new" + + def test_with_system_prompt_and_context(self): + engine = MagicMock() + agent = _ConcreteAgent(engine, "m") + conv = Conversation() + conv.add(Message(role=Role.USER, content="prev")) + ctx = AgentContext(conversation=conv) + messages = agent._build_messages( + "new", ctx, system_prompt="System.", + ) + assert len(messages) == 3 + assert messages[0].role == Role.SYSTEM + assert messages[1].content == "prev" + assert messages[2].content == "new" + + +class TestGenerate: + def test_delegates_to_engine(self): + engine = MagicMock() + engine.generate.return_value = {"content": "hi"} + agent = _ConcreteAgent(engine, "m", temperature=0.5, max_tokens=100) + result = agent._generate([Message(role=Role.USER, content="hi")]) + assert result["content"] == "hi" + engine.generate.assert_called_once() + call_kwargs = engine.generate.call_args[1] + assert call_kwargs["model"] == "m" + assert call_kwargs["temperature"] == 0.5 + assert call_kwargs["max_tokens"] == 100 + + def test_extra_kwargs(self): + engine = MagicMock() + engine.generate.return_value = {"content": "hi"} + agent = _ConcreteAgent(engine, "m") + agent._generate([Message(role=Role.USER, content="hi")], tools=["t"]) + call_kwargs = engine.generate.call_args[1] + assert call_kwargs["tools"] == ["t"] + + +class TestMaxTurnsResult: + def test_default_message(self): + bus = EventBus(record_history=True) + engine = MagicMock() + agent = _ConcreteAgent(engine, "m", bus=bus) + tr = [ToolResult(tool_name="t", content="x", success=True)] + result = agent._max_turns_result(tr, turns=5) + assert result.metadata["max_turns_exceeded"] is True + assert result.turns == 5 + assert "Maximum turns" in result.content + assert result.tool_results == tr + # Should also emit turn end + events = [e for e in bus.history if e.event_type == EventType.AGENT_TURN_END] + assert len(events) == 1 + assert events[0].data["max_turns_exceeded"] is True + + def test_custom_content(self): + engine = MagicMock() + agent = _ConcreteAgent(engine, "m") + result = agent._max_turns_result([], turns=3, content="custom msg") + assert result.content == "custom msg" + + +class TestStripThinkTags: + def test_full_think_block(self): + text = "internal reasoningAnswer here." + assert BaseAgent._strip_think_tags(text) == "Answer here." + + def test_bare_closing_tag(self): + text = "some reasoningAnswer here." + assert BaseAgent._strip_think_tags(text) == "Answer here." + + def test_no_tags(self): + text = "Just normal text." + assert BaseAgent._strip_think_tags(text) == "Just normal text." + + def test_multiline_think(self): + text = "\nline1\nline2\n\nFinal." + assert BaseAgent._strip_think_tags(text) == "Final." + + def test_empty_after_strip(self): + text = "all thinking" + assert BaseAgent._strip_think_tags(text) == "" + + +# --------------------------------------------------------------------------- +# ToolUsingAgent tests +# --------------------------------------------------------------------------- + + +class TestToolUsingAgent: + def test_creates_executor(self): + engine = MagicMock() + agent = _ConcreteToolAgent(engine, "m", tools=[_DummyTool()]) + assert agent._executor is not None + assert len(agent._tools) == 1 + + def test_default_max_turns(self): + engine = MagicMock() + agent = _ConcreteToolAgent(engine, "m") + assert agent._max_turns == 10 + + def test_custom_max_turns(self): + engine = MagicMock() + agent = _ConcreteToolAgent(engine, "m", max_turns=5) + assert agent._max_turns == 5 + + def test_empty_tools(self): + engine = MagicMock() + agent = _ConcreteToolAgent(engine, "m") + assert agent._tools == [] + + def test_inherits_base_helpers(self): + bus = EventBus(record_history=True) + engine = MagicMock() + agent = _ConcreteToolAgent(engine, "m", bus=bus) + agent._emit_turn_start("hi") + events = [e for e in bus.history if e.event_type == EventType.AGENT_TURN_START] + assert len(events) == 1 diff --git a/tests/agents/test_custom.py b/tests/agents/test_custom.py deleted file mode 100644 index 2ec419ff..00000000 --- a/tests/agents/test_custom.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Tests for the CustomAgent stub.""" - -from __future__ import annotations - -import pytest - -from openjarvis.agents.custom import CustomAgent - - -class TestCustomAgent: - def test_agent_id(self): - agent = CustomAgent() - assert agent.agent_id == "custom" - - def test_run_raises(self): - agent = CustomAgent() - with pytest.raises(NotImplementedError, match="template"): - agent.run("Hello") - - def test_error_message_helpful(self): - agent = CustomAgent() - with pytest.raises(NotImplementedError, match="Subclass"): - agent.run("test") - - def test_error_mentions_register(self): - agent = CustomAgent() - with pytest.raises(NotImplementedError, match="register"): - agent.run("test") diff --git a/tests/agents/test_native_openhands.py b/tests/agents/test_native_openhands.py new file mode 100644 index 00000000..aca4903b --- /dev/null +++ b/tests/agents/test_native_openhands.py @@ -0,0 +1,527 @@ +"""Tests for NativeOpenHandsAgent (formerly OpenHandsAgent).""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +from openjarvis.agents._stubs import AgentContext +from openjarvis.agents.native_openhands import NativeOpenHandsAgent +from openjarvis.core.events import EventBus, EventType +from openjarvis.core.registry import AgentRegistry +from openjarvis.core.types import Conversation, Message, Role, ToolResult +from openjarvis.tools._stubs import BaseTool, ToolSpec + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class _CodeInterpreterStub(BaseTool): + """Stub code_interpreter tool for testing.""" + + tool_id = "code_interpreter" + + @property + def spec(self) -> ToolSpec: + return ToolSpec( + name="code_interpreter", + description="Execute Python code.", + parameters={ + "type": "object", + "properties": {"code": {"type": "string"}}, + "required": ["code"], + }, + ) + + def execute(self, **params) -> ToolResult: + code = params.get("code", "") + # Simple simulation: if it contains print(), capture the content + if "print(" in code: + import re + match = re.search(r"print\((.+?)\)", code) + if match: + try: + val = eval(match.group(1)) # noqa: S307 + return ToolResult( + tool_name="code_interpreter", + content=str(val), + success=True, + ) + except Exception: + pass + return ToolResult( + tool_name="code_interpreter", + content=f"Executed: {code}", + success=True, + ) + + +class _CalculatorStub(BaseTool): + tool_id = "calculator" + + @property + def spec(self) -> ToolSpec: + return ToolSpec( + name="calculator", + description="Math calculator.", + parameters={ + "type": "object", + "properties": {"expression": {"type": "string"}}, + "required": ["expression"], + }, + ) + + def execute(self, **params) -> ToolResult: + expr = params.get("expression", "0") + try: + val = eval(expr) # noqa: S307 + except Exception as e: + return ToolResult(tool_name="calculator", content=str(e), success=False) + return ToolResult(tool_name="calculator", content=str(val), success=True) + + +def _engine_response(content, **extra): + base = { + "content": content, + "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}, + "model": "test-model", + "finish_reason": "stop", + } + base.update(extra) + return base + + +# --------------------------------------------------------------------------- +# Registration tests +# --------------------------------------------------------------------------- + + +class TestNativeOpenHandsRegistration: + def test_registration(self): + AgentRegistry.register_value("native_openhands", NativeOpenHandsAgent) + assert AgentRegistry.contains("native_openhands") + + def test_agent_id(self): + engine = MagicMock() + engine.engine_id = "mock" + agent = NativeOpenHandsAgent(engine, "test-model") + assert agent.agent_id == "native_openhands" + + def test_accepts_tools(self): + assert NativeOpenHandsAgent.accepts_tools is True + + +# --------------------------------------------------------------------------- +# Agent execution tests +# --------------------------------------------------------------------------- + + +class TestNativeOpenHandsAgent: + def test_simple_response(self): + """No code -> direct answer.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response("The answer is 42.") + bus = EventBus(record_history=True) + agent = NativeOpenHandsAgent(engine, "test-model", bus=bus) + result = agent.run("What is the meaning of life?") + assert result.content == "The answer is 42." + assert result.turns == 1 + assert result.tool_results == [] + + def test_code_generation_execution(self): + """Turn 1: returns code block -> code_interpreter executed. Turn 2: final.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response( + "Let me calculate:\n```python\nprint(2+2)\n```" + ), + _engine_response("The result is 4."), + ] + agent = NativeOpenHandsAgent( + engine, "test-model", + tools=[_CodeInterpreterStub()], + ) + result = agent.run("What is 2+2?") + assert result.content == "The result is 4." + assert result.turns == 2 + assert len(result.tool_results) == 1 + assert result.tool_results[0].tool_name == "code_interpreter" + + def test_multi_step_code(self): + """Multiple code blocks across turns.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response("Step 1:\n```python\nprint(1+1)\n```"), + _engine_response("Step 2:\n```python\nprint(3*3)\n```"), + _engine_response("First was 2, second was 9."), + ] + agent = NativeOpenHandsAgent( + engine, "test-model", + tools=[_CodeInterpreterStub()], + ) + result = agent.run("Two calculations") + assert result.turns == 3 + assert len(result.tool_results) == 2 + assert result.content == "First was 2, second was 9." + + def test_max_turns(self): + """Engine keeps generating code -> hits max_turns.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response( + "More code:\n```python\nprint('hello')\n```" + ) + agent = NativeOpenHandsAgent( + engine, "test-model", + tools=[_CodeInterpreterStub()], + max_turns=3, + ) + result = agent.run("Keep coding") + assert result.turns == 3 + assert result.metadata.get("max_turns_exceeded") is True + + def test_event_bus_emissions(self): + """Verify AGENT_TURN_START and AGENT_TURN_END events.""" + bus = EventBus(record_history=True) + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response("Direct answer.") + agent = NativeOpenHandsAgent(engine, "test-model", bus=bus) + agent.run("Hello") + event_types = [e.event_type for e in bus.history] + assert EventType.AGENT_TURN_START in event_types + assert EventType.AGENT_TURN_END in event_types + + def test_event_bus_tool_events(self): + """Tool execution should trigger TOOL_CALL_START/END events.""" + bus = EventBus(record_history=True) + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response("Code:\n```python\nprint(1)\n```"), + _engine_response("Done."), + ] + agent = NativeOpenHandsAgent( + engine, "test-model", + tools=[_CodeInterpreterStub()], + bus=bus, + ) + agent.run("Run code") + event_types = [e.event_type for e in bus.history] + assert EventType.TOOL_CALL_START in event_types + assert EventType.TOOL_CALL_END in event_types + + def test_context_passing(self): + """Pass AgentContext with conversation history.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response("Hello!") + conv = Conversation() + conv.add(Message(role=Role.USER, content="Previous")) + conv.add(Message(role=Role.ASSISTANT, content="Previous response")) + ctx = AgentContext(conversation=conv) + agent = NativeOpenHandsAgent(engine, "test-model") + agent.run("Hello", context=ctx) + call_args = engine.generate.call_args + messages = call_args[0][0] + # System prompt + 2 context + user input + assert len(messages) == 4 + assert messages[0].role == Role.SYSTEM + assert messages[1].content == "Previous" + assert messages[3].content == "Hello" + + def test_tool_fallback(self): + """Non-code tool use via Action: syntax.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response( + 'Action: calculator\nAction Input: {"expression": "7*6"}' + ), + _engine_response("The answer is 42."), + ] + agent = NativeOpenHandsAgent( + engine, "test-model", + tools=[_CalculatorStub()], + ) + result = agent.run("What is 7 times 6?") + assert result.content == "The answer is 42." + assert result.turns == 2 + assert len(result.tool_results) == 1 + assert result.tool_results[0].tool_name == "calculator" + assert result.tool_results[0].content == "42" + + def test_no_code_interpreter_tool(self): + """Agent has code but no code_interpreter -> tool not found.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response("```python\nprint(1)\n```"), + _engine_response("Could not run code."), + ] + # No tools at all + agent = NativeOpenHandsAgent(engine, "test-model") + result = agent.run("Run code") + assert result.turns == 2 + assert len(result.tool_results) == 1 + assert result.tool_results[0].success is False + assert "Unknown tool" in result.tool_results[0].content + + def test_no_bus_works(self): + """Agent runs correctly without an event bus.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response("Works!") + agent = NativeOpenHandsAgent(engine, "test-model") + result = agent.run("Hello") + assert result.content == "Works!" + + def test_system_prompt_includes_tool_names(self): + """System prompt should list available tool names.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response("Ok") + agent = NativeOpenHandsAgent( + engine, "test-model", + tools=[_CodeInterpreterStub(), _CalculatorStub()], + ) + agent.run("Hello") + call_args = engine.generate.call_args + messages = call_args[0][0] + system_msg = messages[0] + assert "code_interpreter" in system_msg.content + assert "calculator" in system_msg.content + + def test_max_turns_content_preserved(self): + """When max turns exceeded, last content should be preserved.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response( + "Still working:\n```python\nx = 1\n```" + ) + agent = NativeOpenHandsAgent( + engine, "test-model", + tools=[_CodeInterpreterStub()], + max_turns=2, + ) + result = agent.run("Loop") + assert result.metadata.get("max_turns_exceeded") is True + # Should have the last content, not the fallback message + assert "Still working" in result.content + + def test_observation_appended_to_messages(self): + """Code output is sent back to the engine as observation.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response("```python\nprint(42)\n```"), + _engine_response("Got 42."), + ] + agent = NativeOpenHandsAgent( + engine, "test-model", + tools=[_CodeInterpreterStub()], + ) + agent.run("Print 42") + second_call = engine.generate.call_args_list[1] + messages = second_call[0][0] + last_msg = messages[-1] + assert last_msg.role == Role.USER + assert "Output:" in last_msg.content + + def test_event_data_agent_turn_start(self): + """AGENT_TURN_START event data should include agent id and input.""" + bus = EventBus(record_history=True) + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response("Hi") + agent = NativeOpenHandsAgent(engine, "test-model", bus=bus) + agent.run("test input") + start_events = [ + e for e in bus.history + if e.event_type == EventType.AGENT_TURN_START + ] + assert len(start_events) == 1 + assert start_events[0].data["agent"] == "native_openhands" + assert start_events[0].data["input"] == "test input" + + def test_empty_input(self): + """Agent handles empty input.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response("Empty input received.") + agent = NativeOpenHandsAgent(engine, "test-model") + result = agent.run("") + assert result.content == "Empty input received." + + def test_error_400_handling(self): + """Agent catches 400 errors and returns friendly message.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = RuntimeError("HTTP 400 Bad Request") + agent = NativeOpenHandsAgent(engine, "test-model") + result = agent.run("Hello") + assert "too long" in result.content + assert result.metadata.get("error") is True + + def test_xml_tool_call_extraction(self): + """Agent parses XML-style tool calls.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response( + 'calculator\n$expression=7*6' + ), + _engine_response("The answer is 42."), + ] + agent = NativeOpenHandsAgent( + engine, "test-model", + tools=[_CalculatorStub()], + ) + result = agent.run("What is 7 times 6?") + assert result.content == "The answer is 42." + assert len(result.tool_results) == 1 + assert result.tool_results[0].content == "42" + + def test_observation_truncation(self): + """Long tool results are truncated in observations.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response( + 'Action: calculator\nAction Input: {"expression": "1+1"}' + ), + _engine_response("Done."), + ] + # Make calculator return very long output + long_calc = _CalculatorStub() + orig_execute = long_calc.execute + + def _long_execute(**params): + r = orig_execute(**params) + return ToolResult( + tool_name=r.tool_name, + content="x" * 10000, + success=True, + ) + + long_calc.execute = _long_execute + agent = NativeOpenHandsAgent(engine, "test-model", tools=[long_calc]) + agent.run("Compute") + # Check the observation message sent to the engine + second_call = engine.generate.call_args_list[1] + messages = second_call[0][0] + last_msg = messages[-1] + assert len(last_msg.content) < 5000 + assert "[Output truncated]" in last_msg.content + + +# --------------------------------------------------------------------------- +# Truncation tests +# --------------------------------------------------------------------------- + + +class TestTruncation: + def test_short_messages_unchanged(self): + """Messages under limit are not modified.""" + engine = MagicMock() + engine.engine_id = "mock" + agent = NativeOpenHandsAgent(engine, "test-model") + messages = [ + Message(role=Role.SYSTEM, content="System prompt"), + Message(role=Role.USER, content="Short query"), + ] + result = agent._truncate_if_needed(messages, max_prompt_tokens=1000) + assert result[1].content == "Short query" + + def test_long_messages_truncated(self): + """Messages over limit get the last user message truncated.""" + engine = MagicMock() + engine.engine_id = "mock" + agent = NativeOpenHandsAgent(engine, "test-model") + messages = [ + Message(role=Role.SYSTEM, content="System prompt"), + Message(role=Role.USER, content="x" * 20000), + ] + result = agent._truncate_if_needed(messages, max_prompt_tokens=1000) + assert len(result[1].content) < 20000 + assert "[Input truncated to fit context window]" in result[1].content + + def test_truncation_preserves_system_prompt(self): + """Truncation only modifies user message, not system prompt.""" + engine = MagicMock() + engine.engine_id = "mock" + agent = NativeOpenHandsAgent(engine, "test-model") + system = "Important system prompt" + messages = [ + Message(role=Role.SYSTEM, content=system), + Message(role=Role.USER, content="y" * 20000), + ] + result = agent._truncate_if_needed(messages, max_prompt_tokens=1000) + assert result[0].content == system + + +# --------------------------------------------------------------------------- +# URL expansion tests +# --------------------------------------------------------------------------- + + +class TestUrlExpansion: + def test_no_url_returns_false(self): + text, expanded = NativeOpenHandsAgent._expand_urls("What is 2+2?") + assert text == "What is 2+2?" + assert expanded is False + + def test_url_detected_returns_true(self, monkeypatch): + import httpx + + mock_resp = MagicMock() + mock_resp.text = "Page content" + mock_resp.headers = {"content-type": "text/html"} + mock_resp.raise_for_status = MagicMock() + monkeypatch.setattr(httpx, "get", MagicMock(return_value=mock_resp)) + + text, expanded = NativeOpenHandsAgent._expand_urls( + "Summarize: https://example.com/article" + ) + assert expanded is True + assert "Page content" in text + assert "Content from" in text + + def test_url_expansion_failure_returns_false(self, monkeypatch): + import httpx + + monkeypatch.setattr( + httpx, "get", + MagicMock(side_effect=Exception("Connection error")), + ) + text, expanded = NativeOpenHandsAgent._expand_urls( + "Read https://example.com/broken" + ) + assert expanded is False + + def test_url_expanded_uses_direct_path(self, monkeypatch): + """When URL is expanded, agent bypasses tool loop.""" + import httpx + + mock_resp = MagicMock() + mock_resp.text = "Article text here" + mock_resp.headers = {"content-type": "text/html"} + mock_resp.raise_for_status = MagicMock() + monkeypatch.setattr(httpx, "get", MagicMock(return_value=mock_resp)) + + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response("Summary of article.") + agent = NativeOpenHandsAgent(engine, "test-model") + result = agent.run("Summarize: https://example.com/article") + assert result.content == "Summary of article." + assert result.turns == 1 + # Only one generate call (direct, no tool loop) + assert engine.generate.call_count == 1 + # The message should contain the fetched content, not tool descriptions + call_messages = engine.generate.call_args[0][0] + system_msg = call_messages[0].content + assert "tool" not in system_msg.lower() diff --git a/tests/agents/test_native_react.py b/tests/agents/test_native_react.py new file mode 100644 index 00000000..814fcf3a --- /dev/null +++ b/tests/agents/test_native_react.py @@ -0,0 +1,477 @@ +"""Tests for NativeReActAgent (formerly ReActAgent).""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest + +from openjarvis.agents._stubs import AgentContext +from openjarvis.agents.native_react import NativeReActAgent +from openjarvis.core.events import EventBus, EventType +from openjarvis.core.registry import AgentRegistry +from openjarvis.core.types import Conversation, Message, Role, ToolResult +from openjarvis.tools._stubs import BaseTool, ToolSpec + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class _CalculatorStub(BaseTool): + tool_id = "calculator" + + @property + def spec(self) -> ToolSpec: + return ToolSpec( + name="calculator", + description="Math calculator.", + parameters={ + "type": "object", + "properties": {"expression": {"type": "string"}}, + "required": ["expression"], + }, + ) + + def execute(self, **params) -> ToolResult: + expr = params.get("expression", "0") + try: + val = eval(expr) # noqa: S307 + except Exception as e: + return ToolResult(tool_name="calculator", content=str(e), success=False) + return ToolResult(tool_name="calculator", content=str(val), success=True) + + +class _ThinkStub(BaseTool): + tool_id = "think" + + @property + def spec(self) -> ToolSpec: + return ToolSpec( + name="think", + description="Thinking tool.", + parameters={ + "type": "object", + "properties": {"thought": {"type": "string"}}, + }, + ) + + def execute(self, **params) -> ToolResult: + return ToolResult( + tool_name="think", + content=params.get("thought", ""), + success=True, + ) + + +def _engine_response(content, **extra): + """Helper to build an engine response dict.""" + base = { + "content": content, + "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}, + "model": "test-model", + "finish_reason": "stop", + } + base.update(extra) + return base + + +# --------------------------------------------------------------------------- +# Registration tests +# --------------------------------------------------------------------------- + + +class TestNativeReActRegistration: + def test_registration(self): + AgentRegistry.register_value("native_react", NativeReActAgent) + assert AgentRegistry.contains("native_react") + + def test_agent_id(self): + engine = MagicMock() + engine.engine_id = "mock" + agent = NativeReActAgent(engine, "test-model") + assert agent.agent_id == "native_react" + + def test_accepts_tools(self): + assert NativeReActAgent.accepts_tools is True + + +# --------------------------------------------------------------------------- +# Parsing tests +# --------------------------------------------------------------------------- + + +class TestNativeReActParsing: + def _parser(self): + engine = MagicMock() + engine.engine_id = "mock" + agent = NativeReActAgent(engine, "test-model") + return agent._parse_response + + def test_parse_thought_action(self): + parse = self._parser() + text = ( + 'Thought: I need to calculate 2+2.\n' + 'Action: calculator\n' + 'Action Input: {"expression": "2+2"}' + ) + result = parse(text) + assert result["thought"] == "I need to calculate 2+2." + assert result["action"] == "calculator" + assert "expression" in result["action_input"] + assert result["final_answer"] == "" + + def test_parse_final_answer(self): + parse = self._parser() + text = "Thought: I know the answer.\nFinal Answer: 42" + result = parse(text) + assert result["thought"] == "I know the answer." + assert result["final_answer"] == "42" + assert result["action"] == "" + + def test_parse_no_structure(self): + parse = self._parser() + text = "Just a plain response with no structure." + result = parse(text) + assert result["thought"] == "" + assert result["action"] == "" + assert result["final_answer"] == "" + + def test_parse_multiline_thought(self): + parse = self._parser() + text = ( + "Thought: First I need to think.\n" + "Then consider options.\n" + "Final Answer: done" + ) + result = parse(text) + assert result["final_answer"] == "done" + + def test_parse_action_without_input(self): + parse = self._parser() + text = "Thought: Let me check.\nAction: calculator" + result = parse(text) + assert result["action"] == "calculator" + assert result["action_input"] == "" + + +# --------------------------------------------------------------------------- +# Agent execution tests +# --------------------------------------------------------------------------- + + +class TestNativeReActAgent: + def test_simple_no_tool_response(self): + """Engine returns Final Answer on first call.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response( + "Thought: Simple greeting.\nFinal Answer: Hello!" + ) + bus = EventBus(record_history=True) + agent = NativeReActAgent(engine, "test-model", bus=bus) + result = agent.run("Hello") + assert result.content == "Hello!" + assert result.turns == 1 + assert result.tool_results == [] + + def test_thought_action_observation(self): + """Turn 1: action, Turn 2: final answer.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response( + 'Thought: I need to calculate.\n' + 'Action: calculator\n' + 'Action Input: {"expression": "2+2"}' + ), + _engine_response( + "Thought: The result is 4.\nFinal Answer: 4" + ), + ] + bus = EventBus(record_history=True) + agent = NativeReActAgent(engine, "test-model", tools=[_CalculatorStub()], bus=bus) + result = agent.run("What is 2+2?") + assert result.content == "4" + assert result.turns == 2 + assert len(result.tool_results) == 1 + assert result.tool_results[0].tool_name == "calculator" + assert result.tool_results[0].content == "4" + + def test_calculator_tool_use(self): + """Verify calculator tool execution produces correct result.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response( + 'Thought: Calculate.\nAction: calculator\n' + 'Action Input: {"expression": "3*7"}' + ), + _engine_response("Thought: Done.\nFinal Answer: 21"), + ] + agent = NativeReActAgent(engine, "test-model", tools=[_CalculatorStub()]) + result = agent.run("3 times 7") + assert result.tool_results[0].content == "21" + assert result.tool_results[0].success is True + + def test_multi_tool_turns(self): + """Three tool calls before final answer.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response( + 'Thought: Step 1.\nAction: calculator\n' + 'Action Input: {"expression": "1+1"}' + ), + _engine_response( + 'Thought: Step 2.\nAction: calculator\n' + 'Action Input: {"expression": "2+2"}' + ), + _engine_response( + 'Thought: Step 3.\nAction: think\n' + 'Action Input: {"thought": "combining results"}' + ), + _engine_response("Thought: All done.\nFinal Answer: Complete."), + ] + agent = NativeReActAgent( + engine, "test-model", + tools=[_CalculatorStub(), _ThinkStub()], + ) + result = agent.run("Multi step") + assert result.turns == 4 + assert len(result.tool_results) == 3 + assert result.content == "Complete." + + def test_max_turns_exceeded(self): + """Engine always returns actions -- hits max_turns.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response( + 'Thought: Keep going.\nAction: calculator\n' + 'Action Input: {"expression": "1+1"}' + ) + agent = NativeReActAgent( + engine, "test-model", + tools=[_CalculatorStub()], + max_turns=3, + ) + result = agent.run("Loop forever") + assert result.turns == 3 + assert result.metadata.get("max_turns_exceeded") is True + assert result.content == "Maximum turns reached without a final answer." + + def test_unknown_tool_error(self): + """Action references nonexistent tool -- ToolResult with success=False.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response( + 'Thought: Use a tool.\nAction: nonexistent\n' + 'Action Input: {}' + ), + _engine_response( + "Thought: Error occurred.\n" + "Final Answer: Could not run tool." + ), + ] + agent = NativeReActAgent(engine, "test-model", tools=[_CalculatorStub()]) + result = agent.run("Do something") + assert len(result.tool_results) == 1 + assert result.tool_results[0].success is False + assert "Unknown tool" in result.tool_results[0].content + + def test_event_bus_emissions(self): + """Verify AGENT_TURN_START and AGENT_TURN_END events.""" + bus = EventBus(record_history=True) + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response( + "Thought: Quick.\nFinal Answer: Done." + ) + agent = NativeReActAgent(engine, "test-model", bus=bus) + agent.run("Hello") + event_types = [e.event_type for e in bus.history] + assert EventType.AGENT_TURN_START in event_types + assert EventType.AGENT_TURN_END in event_types + + def test_event_bus_tool_events(self): + """Tool call should trigger TOOL_CALL_START and TOOL_CALL_END events.""" + bus = EventBus(record_history=True) + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response( + 'Thought: Calc.\nAction: calculator\n' + 'Action Input: {"expression": "1+1"}' + ), + _engine_response("Thought: Done.\nFinal Answer: 2"), + ] + agent = NativeReActAgent(engine, "test-model", tools=[_CalculatorStub()], bus=bus) + agent.run("Calc") + event_types = [e.event_type for e in bus.history] + assert EventType.TOOL_CALL_START in event_types + assert EventType.TOOL_CALL_END in event_types + + def test_context_passing(self): + """Pass AgentContext with conversation history.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response( + "Thought: Simple.\nFinal Answer: Hi!" + ) + conv = Conversation() + conv.add(Message(role=Role.USER, content="Previous message")) + conv.add(Message(role=Role.ASSISTANT, content="Previous response")) + ctx = AgentContext(conversation=conv) + agent = NativeReActAgent(engine, "test-model") + agent.run("Hello", context=ctx) + call_args = engine.generate.call_args + messages = call_args[0][0] + # System prompt + 2 context messages + user input + assert len(messages) == 4 + assert messages[0].role == Role.SYSTEM + assert messages[1].role == Role.USER + assert messages[1].content == "Previous message" + assert messages[3].role == Role.USER + assert messages[3].content == "Hello" + + def test_with_think_tool(self): + """Use think tool for internal reasoning.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response( + 'Thought: Let me reason.\nAction: think\n' + 'Action Input: {"thought": "The user wants a greeting"}' + ), + _engine_response("Thought: Now I know.\nFinal Answer: Greetings!"), + ] + agent = NativeReActAgent(engine, "test-model", tools=[_ThinkStub()]) + result = agent.run("Say hi") + assert result.content == "Greetings!" + assert result.tool_results[0].tool_name == "think" + assert result.tool_results[0].content == "The user wants a greeting" + + def test_no_bus_works(self): + """Agent runs correctly without an event bus.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response( + "Thought: Easy.\nFinal Answer: Works!" + ) + agent = NativeReActAgent(engine, "test-model") + result = agent.run("Hello") + assert result.content == "Works!" + + def test_plain_response_no_structure(self): + """If engine returns no ReAct structure, treat as final answer.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response("Just a plain answer.") + agent = NativeReActAgent(engine, "test-model") + result = agent.run("Hello") + assert result.content == "Just a plain answer." + assert result.turns == 1 + + def test_observation_appended_to_messages(self): + """Observation from tool result is sent back to the engine.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.side_effect = [ + _engine_response( + 'Thought: Calc.\nAction: calculator\n' + 'Action Input: {"expression": "5+5"}' + ), + _engine_response("Thought: Got it.\nFinal Answer: 10"), + ] + agent = NativeReActAgent(engine, "test-model", tools=[_CalculatorStub()]) + agent.run("What is 5+5?") + # Check second call messages + second_call = engine.generate.call_args_list[1] + messages = second_call[0][0] + # Last message should be the observation + last_msg = messages[-1] + assert last_msg.role == Role.USER + assert "Observation:" in last_msg.content + assert "10" in last_msg.content + + def test_system_prompt_includes_tool_names(self): + """System prompt should list available tool names.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response( + "Thought: Done.\nFinal Answer: ok" + ) + agent = NativeReActAgent( + engine, "test-model", + tools=[_CalculatorStub(), _ThinkStub()], + ) + agent.run("Hello") + call_args = engine.generate.call_args + messages = call_args[0][0] + system_msg = messages[0] + assert "calculator" in system_msg.content + assert "think" in system_msg.content + + def test_system_prompt_no_tools(self): + """System prompt should say 'none' when no tools are available.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response( + "Thought: No tools.\nFinal Answer: ok" + ) + agent = NativeReActAgent(engine, "test-model") + agent.run("Hello") + call_args = engine.generate.call_args + messages = call_args[0][0] + assert "none" in messages[0].content + + def test_max_turns_1(self): + """With max_turns=1 and an action, should stop after 1 turn.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response( + 'Thought: Go.\nAction: calculator\n' + 'Action Input: {"expression": "1"}' + ) + agent = NativeReActAgent( + engine, "test-model", + tools=[_CalculatorStub()], + max_turns=1, + ) + result = agent.run("Calc") + assert result.turns == 1 + assert result.metadata.get("max_turns_exceeded") is True + + def test_event_data_agent_turn_start(self): + """AGENT_TURN_START event data should include agent id and input.""" + bus = EventBus(record_history=True) + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response( + "Thought: Quick.\nFinal Answer: Hi" + ) + agent = NativeReActAgent(engine, "test-model", bus=bus) + agent.run("test input") + start_events = [ + e for e in bus.history + if e.event_type == EventType.AGENT_TURN_START + ] + assert len(start_events) == 1 + assert start_events[0].data["agent"] == "native_react" + assert start_events[0].data["input"] == "test input" + + +@pytest.mark.parametrize("model", ["qwen3:8b", "gpt-oss:120b"]) +def test_native_react_with_different_models(model): + """NativeReActAgent works with different model names.""" + engine = MagicMock() + engine.engine_id = "mock" + engine.generate.return_value = _engine_response( + "Thought: Responding.\nFinal Answer: Hello!" + ) + agent = NativeReActAgent(engine, model) + result = agent.run("Hello") + assert result.content == "Hello!" + call_kwargs = engine.generate.call_args[1] + assert call_kwargs["model"] == model diff --git a/tests/agents/test_openhands.py b/tests/agents/test_openhands.py index b33fbcc4..ebeb5c3e 100644 --- a/tests/agents/test_openhands.py +++ b/tests/agents/test_openhands.py @@ -1,103 +1,18 @@ -"""Tests for OpenHands agent.""" +"""Tests for OpenHandsAgent (real openhands-sdk wrapper).""" from __future__ import annotations from unittest.mock import MagicMock -from openjarvis.agents._stubs import AgentContext +import pytest + +from openjarvis.agents._stubs import BaseAgent from openjarvis.agents.openhands import OpenHandsAgent -from openjarvis.core.events import EventBus, EventType from openjarvis.core.registry import AgentRegistry -from openjarvis.core.types import Conversation, Message, Role, ToolResult -from openjarvis.tools._stubs import BaseTool, ToolSpec - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- -class _CodeInterpreterStub(BaseTool): - """Stub code_interpreter tool for testing.""" - - tool_id = "code_interpreter" - - @property - def spec(self) -> ToolSpec: - return ToolSpec( - name="code_interpreter", - description="Execute Python code.", - parameters={ - "type": "object", - "properties": {"code": {"type": "string"}}, - "required": ["code"], - }, - ) - - def execute(self, **params) -> ToolResult: - code = params.get("code", "") - # Simple simulation: if it contains print(), capture the content - if "print(" in code: - import re - match = re.search(r"print\((.+?)\)", code) - if match: - try: - val = eval(match.group(1)) # noqa: S307 - return ToolResult( - tool_name="code_interpreter", - content=str(val), - success=True, - ) - except Exception: - pass - return ToolResult( - tool_name="code_interpreter", - content=f"Executed: {code}", - success=True, - ) - - -class _CalculatorStub(BaseTool): - tool_id = "calculator" - - @property - def spec(self) -> ToolSpec: - return ToolSpec( - name="calculator", - description="Math calculator.", - parameters={ - "type": "object", - "properties": {"expression": {"type": "string"}}, - "required": ["expression"], - }, - ) - - def execute(self, **params) -> ToolResult: - expr = params.get("expression", "0") - try: - val = eval(expr) # noqa: S307 - except Exception as e: - return ToolResult(tool_name="calculator", content=str(e), success=False) - return ToolResult(tool_name="calculator", content=str(val), success=True) - - -def _engine_response(content, **extra): - base = { - "content": content, - "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}, - "model": "test-model", - "finish_reason": "stop", - } - base.update(extra) - return base - - -# --------------------------------------------------------------------------- -# Registration tests -# --------------------------------------------------------------------------- - - -class TestOpenHandsRegistration: - def test_registration(self): +class TestOpenHandsAgentRegistration: + def test_registered(self): AgentRegistry.register_value("openhands", OpenHandsAgent) assert AgentRegistry.contains("openhands") @@ -107,422 +22,36 @@ class TestOpenHandsRegistration: agent = OpenHandsAgent(engine, "test-model") assert agent.agent_id == "openhands" + def test_does_not_accept_tools(self): + """Real OpenHandsAgent doesn't use ToolUsingAgent base.""" + assert OpenHandsAgent.accepts_tools is False -# --------------------------------------------------------------------------- -# Agent execution tests -# --------------------------------------------------------------------------- + def test_is_base_agent(self): + assert issubclass(OpenHandsAgent, BaseAgent) -class TestOpenHandsAgent: - def test_simple_response(self): - """No code -> direct answer.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response("The answer is 42.") - bus = EventBus(record_history=True) - agent = OpenHandsAgent(engine, "test-model", bus=bus) - result = agent.run("What is the meaning of life?") - assert result.content == "The answer is 42." - assert result.turns == 1 - assert result.tool_results == [] - - def test_code_generation_execution(self): - """Turn 1: returns code block -> code_interpreter executed. Turn 2: final.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response( - "Let me calculate:\n```python\nprint(2+2)\n```" - ), - _engine_response("The result is 4."), - ] - agent = OpenHandsAgent( - engine, "test-model", - tools=[_CodeInterpreterStub()], - ) - result = agent.run("What is 2+2?") - assert result.content == "The result is 4." - assert result.turns == 2 - assert len(result.tool_results) == 1 - assert result.tool_results[0].tool_name == "code_interpreter" - - def test_multi_step_code(self): - """Multiple code blocks across turns.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response("Step 1:\n```python\nprint(1+1)\n```"), - _engine_response("Step 2:\n```python\nprint(3*3)\n```"), - _engine_response("First was 2, second was 9."), - ] - agent = OpenHandsAgent( - engine, "test-model", - tools=[_CodeInterpreterStub()], - ) - result = agent.run("Two calculations") - assert result.turns == 3 - assert len(result.tool_results) == 2 - assert result.content == "First was 2, second was 9." - - def test_max_turns(self): - """Engine keeps generating code -> hits max_turns.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response( - "More code:\n```python\nprint('hello')\n```" - ) - agent = OpenHandsAgent( - engine, "test-model", - tools=[_CodeInterpreterStub()], - max_turns=3, - ) - result = agent.run("Keep coding") - assert result.turns == 3 - assert result.metadata.get("max_turns_exceeded") is True - - def test_event_bus_emissions(self): - """Verify AGENT_TURN_START and AGENT_TURN_END events. - - INFERENCE_START/END are now published by InstrumentedEngine, - not by agents directly. - """ - bus = EventBus(record_history=True) - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response("Direct answer.") - agent = OpenHandsAgent(engine, "test-model", bus=bus) - agent.run("Hello") - event_types = [e.event_type for e in bus.history] - assert EventType.AGENT_TURN_START in event_types - assert EventType.AGENT_TURN_END in event_types - - def test_event_bus_tool_events(self): - """Tool execution should trigger TOOL_CALL_START/END events.""" - bus = EventBus(record_history=True) - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response("Code:\n```python\nprint(1)\n```"), - _engine_response("Done."), - ] - agent = OpenHandsAgent( - engine, "test-model", - tools=[_CodeInterpreterStub()], - bus=bus, - ) - agent.run("Run code") - event_types = [e.event_type for e in bus.history] - assert EventType.TOOL_CALL_START in event_types - assert EventType.TOOL_CALL_END in event_types - - def test_context_passing(self): - """Pass AgentContext with conversation history.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response("Hello!") - conv = Conversation() - conv.add(Message(role=Role.USER, content="Previous")) - conv.add(Message(role=Role.ASSISTANT, content="Previous response")) - ctx = AgentContext(conversation=conv) - agent = OpenHandsAgent(engine, "test-model") - agent.run("Hello", context=ctx) - call_args = engine.generate.call_args - messages = call_args[0][0] - # System prompt + 2 context + user input - assert len(messages) == 4 - assert messages[0].role == Role.SYSTEM - assert messages[1].content == "Previous" - assert messages[3].content == "Hello" - - def test_tool_fallback(self): - """Non-code tool use via Action: syntax.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response( - 'Action: calculator\nAction Input: {"expression": "7*6"}' - ), - _engine_response("The answer is 42."), - ] - agent = OpenHandsAgent( - engine, "test-model", - tools=[_CalculatorStub()], - ) - result = agent.run("What is 7 times 6?") - assert result.content == "The answer is 42." - assert result.turns == 2 - assert len(result.tool_results) == 1 - assert result.tool_results[0].tool_name == "calculator" - assert result.tool_results[0].content == "42" - - def test_no_code_interpreter_tool(self): - """Agent has code but no code_interpreter -> tool not found.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response("```python\nprint(1)\n```"), - _engine_response("Could not run code."), - ] - # No tools at all - agent = OpenHandsAgent(engine, "test-model") - result = agent.run("Run code") - assert result.turns == 2 - assert len(result.tool_results) == 1 - assert result.tool_results[0].success is False - assert "Unknown tool" in result.tool_results[0].content - - def test_no_bus_works(self): - """Agent runs correctly without an event bus.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response("Works!") - agent = OpenHandsAgent(engine, "test-model") - result = agent.run("Hello") - assert result.content == "Works!" - - def test_system_prompt_includes_tool_names(self): - """System prompt should list available tool names.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response("Ok") - agent = OpenHandsAgent( - engine, "test-model", - tools=[_CodeInterpreterStub(), _CalculatorStub()], - ) - agent.run("Hello") - call_args = engine.generate.call_args - messages = call_args[0][0] - system_msg = messages[0] - assert "code_interpreter" in system_msg.content - assert "calculator" in system_msg.content - - def test_max_turns_content_preserved(self): - """When max turns exceeded, last content should be preserved.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response( - "Still working:\n```python\nx = 1\n```" - ) - agent = OpenHandsAgent( - engine, "test-model", - tools=[_CodeInterpreterStub()], - max_turns=2, - ) - result = agent.run("Loop") - assert result.metadata.get("max_turns_exceeded") is True - # Should have the last content, not the fallback message - assert "Still working" in result.content - - def test_observation_appended_to_messages(self): - """Code output is sent back to the engine as observation.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response("```python\nprint(42)\n```"), - _engine_response("Got 42."), - ] - agent = OpenHandsAgent( - engine, "test-model", - tools=[_CodeInterpreterStub()], - ) - agent.run("Print 42") - second_call = engine.generate.call_args_list[1] - messages = second_call[0][0] - last_msg = messages[-1] - assert last_msg.role == Role.USER - assert "Output:" in last_msg.content - - def test_event_data_agent_turn_start(self): - """AGENT_TURN_START event data should include agent id and input.""" - bus = EventBus(record_history=True) - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response("Hi") - agent = OpenHandsAgent(engine, "test-model", bus=bus) - agent.run("test input") - start_events = [ - e for e in bus.history - if e.event_type == EventType.AGENT_TURN_START - ] - assert len(start_events) == 1 - assert start_events[0].data["agent"] == "openhands" - assert start_events[0].data["input"] == "test input" - - def test_empty_input(self): - """Agent handles empty input.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response("Empty input received.") - agent = OpenHandsAgent(engine, "test-model") - result = agent.run("") - assert result.content == "Empty input received." - - def test_error_400_handling(self): - """Agent catches 400 errors and returns friendly message.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = RuntimeError("HTTP 400 Bad Request") - agent = OpenHandsAgent(engine, "test-model") - result = agent.run("Hello") - assert "too long" in result.content - assert result.metadata.get("error") is True - - def test_xml_tool_call_extraction(self): - """Agent parses XML-style tool calls.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response( - 'calculator\n$expression=7*6' - ), - _engine_response("The answer is 42."), - ] - agent = OpenHandsAgent( - engine, "test-model", - tools=[_CalculatorStub()], - ) - result = agent.run("What is 7 times 6?") - assert result.content == "The answer is 42." - assert len(result.tool_results) == 1 - assert result.tool_results[0].content == "42" - - def test_observation_truncation(self): - """Long tool results are truncated in observations.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response( - 'Action: calculator\nAction Input: {"expression": "1+1"}' - ), - _engine_response("Done."), - ] - # Make calculator return very long output - long_calc = _CalculatorStub() - orig_execute = long_calc.execute - - def _long_execute(**params): - r = orig_execute(**params) - return ToolResult( - tool_name=r.tool_name, - content="x" * 10000, - success=True, - ) - - long_calc.execute = _long_execute - agent = OpenHandsAgent(engine, "test-model", tools=[long_calc]) - agent.run("Compute") - # Check the observation message sent to the engine - second_call = engine.generate.call_args_list[1] - messages = second_call[0][0] - last_msg = messages[-1] - assert len(last_msg.content) < 5000 - assert "[Output truncated]" in last_msg.content - - -# --------------------------------------------------------------------------- -# Truncation tests -# --------------------------------------------------------------------------- - - -class TestTruncation: - def test_short_messages_unchanged(self): - """Messages under limit are not modified.""" +class TestOpenHandsAgentImportError: + def test_run_without_sdk_raises(self): + """Running without openhands-sdk installed raises ImportError.""" engine = MagicMock() engine.engine_id = "mock" agent = OpenHandsAgent(engine, "test-model") - messages = [ - Message(role=Role.SYSTEM, content="System prompt"), - Message(role=Role.USER, content="Short query"), - ] - result = agent._truncate_if_needed(messages, max_prompt_tokens=1000) - assert result[1].content == "Short query" + with pytest.raises(ImportError, match="openhands-sdk"): + agent.run("Hello") - def test_long_messages_truncated(self): - """Messages over limit get the last user message truncated.""" + +class TestOpenHandsAgentConstructor: + def test_default_workspace(self): engine = MagicMock() - engine.engine_id = "mock" agent = OpenHandsAgent(engine, "test-model") - messages = [ - Message(role=Role.SYSTEM, content="System prompt"), - Message(role=Role.USER, content="x" * 20000), - ] - result = agent._truncate_if_needed(messages, max_prompt_tokens=1000) - assert len(result[1].content) < 20000 - assert "[Input truncated to fit context window]" in result[1].content + assert agent._workspace # should be cwd - def test_truncation_preserves_system_prompt(self): - """Truncation only modifies user message, not system prompt.""" + def test_custom_workspace(self): engine = MagicMock() - engine.engine_id = "mock" - agent = OpenHandsAgent(engine, "test-model") - system = "Important system prompt" - messages = [ - Message(role=Role.SYSTEM, content=system), - Message(role=Role.USER, content="y" * 20000), - ] - result = agent._truncate_if_needed(messages, max_prompt_tokens=1000) - assert result[0].content == system - - -# --------------------------------------------------------------------------- -# URL expansion tests -# --------------------------------------------------------------------------- - - -class TestUrlExpansion: - def test_no_url_returns_false(self): - text, expanded = OpenHandsAgent._expand_urls("What is 2+2?") - assert text == "What is 2+2?" - assert expanded is False - - def test_url_detected_returns_true(self, monkeypatch): - import httpx - - mock_resp = MagicMock() - mock_resp.text = "Page content" - mock_resp.headers = {"content-type": "text/html"} - mock_resp.raise_for_status = MagicMock() - monkeypatch.setattr(httpx, "get", MagicMock(return_value=mock_resp)) - - text, expanded = OpenHandsAgent._expand_urls( - "Summarize: https://example.com/article" - ) - assert expanded is True - assert "Page content" in text - assert "Content from" in text - - def test_url_expansion_failure_returns_false(self, monkeypatch): - import httpx - - monkeypatch.setattr( - httpx, "get", - MagicMock(side_effect=Exception("Connection error")), - ) - text, expanded = OpenHandsAgent._expand_urls( - "Read https://example.com/broken" - ) - assert expanded is False - - def test_url_expanded_uses_direct_path(self, monkeypatch): - """When URL is expanded, agent bypasses tool loop.""" - import httpx - - mock_resp = MagicMock() - mock_resp.text = "Article text here" - mock_resp.headers = {"content-type": "text/html"} - mock_resp.raise_for_status = MagicMock() - monkeypatch.setattr(httpx, "get", MagicMock(return_value=mock_resp)) + agent = OpenHandsAgent(engine, "test-model", workspace="/tmp/test") + assert agent._workspace == "/tmp/test" + def test_custom_api_key(self): engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response("Summary of article.") - agent = OpenHandsAgent(engine, "test-model") - result = agent.run("Summarize: https://example.com/article") - assert result.content == "Summary of article." - assert result.turns == 1 - # Only one generate call (direct, no tool loop) - assert engine.generate.call_count == 1 - # The message should contain the fetched content, not tool descriptions - call_messages = engine.generate.call_args[0][0] - system_msg = call_messages[0].content - assert "tool" not in system_msg.lower() + agent = OpenHandsAgent(engine, "test-model", api_key="sk-test") + assert agent._api_key == "sk-test" diff --git a/tests/agents/test_react.py b/tests/agents/test_react.py index be3ee5af..35e5af9c 100644 --- a/tests/agents/test_react.py +++ b/tests/agents/test_react.py @@ -1,478 +1,44 @@ -"""Tests for ReAct agent.""" +"""Backward-compat tests: ensure old import paths still work. + +The canonical tests are in test_native_react.py. This file verifies +that ``from openjarvis.agents.react import ReActAgent`` still works +and produces a working agent. +""" from __future__ import annotations from unittest.mock import MagicMock -import pytest - -from openjarvis.agents._stubs import AgentContext from openjarvis.agents.react import ReActAgent -from openjarvis.core.events import EventBus, EventType -from openjarvis.core.registry import AgentRegistry -from openjarvis.core.types import Conversation, Message, Role, ToolResult -from openjarvis.tools._stubs import BaseTool, ToolSpec - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- +from openjarvis.agents.native_react import NativeReActAgent -class _CalculatorStub(BaseTool): - tool_id = "calculator" - - @property - def spec(self) -> ToolSpec: - return ToolSpec( - name="calculator", - description="Math calculator.", - parameters={ - "type": "object", - "properties": {"expression": {"type": "string"}}, - "required": ["expression"], - }, - ) - - def execute(self, **params) -> ToolResult: - expr = params.get("expression", "0") - try: - val = eval(expr) # noqa: S307 - except Exception as e: - return ToolResult(tool_name="calculator", content=str(e), success=False) - return ToolResult(tool_name="calculator", content=str(val), success=True) - - -class _ThinkStub(BaseTool): - tool_id = "think" - - @property - def spec(self) -> ToolSpec: - return ToolSpec( - name="think", - description="Thinking tool.", - parameters={ - "type": "object", - "properties": {"thought": {"type": "string"}}, - }, - ) - - def execute(self, **params) -> ToolResult: - return ToolResult( - tool_name="think", - content=params.get("thought", ""), - success=True, - ) - - -def _engine_response(content, **extra): - """Helper to build an engine response dict.""" - base = { +def _engine_response(content): + return { "content": content, "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}, "model": "test-model", "finish_reason": "stop", } - base.update(extra) - return base -# --------------------------------------------------------------------------- -# Registration tests -# --------------------------------------------------------------------------- +class TestReActShim: + def test_is_native_react(self): + """ReActAgent imported from old path is NativeReActAgent.""" + assert ReActAgent is NativeReActAgent - -class TestReActRegistration: - def test_registration(self): - AgentRegistry.register_value("react", ReActAgent) - assert AgentRegistry.contains("react") - - def test_agent_id(self): + def test_can_instantiate(self): engine = MagicMock() engine.engine_id = "mock" agent = ReActAgent(engine, "test-model") - assert agent.agent_id == "react" + assert agent.agent_id == "native_react" - -# --------------------------------------------------------------------------- -# Parsing tests -# --------------------------------------------------------------------------- - - -class TestReActParsing: - def _parser(self): - engine = MagicMock() - engine.engine_id = "mock" - agent = ReActAgent(engine, "test-model") - return agent._parse_response - - def test_parse_thought_action(self): - parse = self._parser() - text = ( - 'Thought: I need to calculate 2+2.\n' - 'Action: calculator\n' - 'Action Input: {"expression": "2+2"}' - ) - result = parse(text) - assert result["thought"] == "I need to calculate 2+2." - assert result["action"] == "calculator" - assert "expression" in result["action_input"] - assert result["final_answer"] == "" - - def test_parse_final_answer(self): - parse = self._parser() - text = "Thought: I know the answer.\nFinal Answer: 42" - result = parse(text) - assert result["thought"] == "I know the answer." - assert result["final_answer"] == "42" - assert result["action"] == "" - - def test_parse_no_structure(self): - parse = self._parser() - text = "Just a plain response with no structure." - result = parse(text) - assert result["thought"] == "" - assert result["action"] == "" - assert result["final_answer"] == "" - - def test_parse_multiline_thought(self): - parse = self._parser() - text = ( - "Thought: First I need to think.\n" - "Then consider options.\n" - "Final Answer: done" - ) - result = parse(text) - assert result["final_answer"] == "done" - - def test_parse_action_without_input(self): - parse = self._parser() - text = "Thought: Let me check.\nAction: calculator" - result = parse(text) - assert result["action"] == "calculator" - assert result["action_input"] == "" - - -# --------------------------------------------------------------------------- -# Agent execution tests -# --------------------------------------------------------------------------- - - -class TestReActAgent: - def test_simple_no_tool_response(self): - """Engine returns Final Answer on first call.""" + def test_can_run(self): engine = MagicMock() engine.engine_id = "mock" engine.generate.return_value = _engine_response( - "Thought: Simple greeting.\nFinal Answer: Hello!" + "Thought: Simple.\nFinal Answer: Hello!" ) - bus = EventBus(record_history=True) - agent = ReActAgent(engine, "test-model", bus=bus) + agent = ReActAgent(engine, "test-model") result = agent.run("Hello") assert result.content == "Hello!" - assert result.turns == 1 - assert result.tool_results == [] - - def test_thought_action_observation(self): - """Turn 1: action, Turn 2: final answer.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response( - 'Thought: I need to calculate.\n' - 'Action: calculator\n' - 'Action Input: {"expression": "2+2"}' - ), - _engine_response( - "Thought: The result is 4.\nFinal Answer: 4" - ), - ] - bus = EventBus(record_history=True) - agent = ReActAgent(engine, "test-model", tools=[_CalculatorStub()], bus=bus) - result = agent.run("What is 2+2?") - assert result.content == "4" - assert result.turns == 2 - assert len(result.tool_results) == 1 - assert result.tool_results[0].tool_name == "calculator" - assert result.tool_results[0].content == "4" - - def test_calculator_tool_use(self): - """Verify calculator tool execution produces correct result.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response( - 'Thought: Calculate.\nAction: calculator\n' - 'Action Input: {"expression": "3*7"}' - ), - _engine_response("Thought: Done.\nFinal Answer: 21"), - ] - agent = ReActAgent(engine, "test-model", tools=[_CalculatorStub()]) - result = agent.run("3 times 7") - assert result.tool_results[0].content == "21" - assert result.tool_results[0].success is True - - def test_multi_tool_turns(self): - """Three tool calls before final answer.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response( - 'Thought: Step 1.\nAction: calculator\n' - 'Action Input: {"expression": "1+1"}' - ), - _engine_response( - 'Thought: Step 2.\nAction: calculator\n' - 'Action Input: {"expression": "2+2"}' - ), - _engine_response( - 'Thought: Step 3.\nAction: think\n' - 'Action Input: {"thought": "combining results"}' - ), - _engine_response("Thought: All done.\nFinal Answer: Complete."), - ] - agent = ReActAgent( - engine, "test-model", - tools=[_CalculatorStub(), _ThinkStub()], - ) - result = agent.run("Multi step") - assert result.turns == 4 - assert len(result.tool_results) == 3 - assert result.content == "Complete." - - def test_max_turns_exceeded(self): - """Engine always returns actions -- hits max_turns.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response( - 'Thought: Keep going.\nAction: calculator\n' - 'Action Input: {"expression": "1+1"}' - ) - agent = ReActAgent( - engine, "test-model", - tools=[_CalculatorStub()], - max_turns=3, - ) - result = agent.run("Loop forever") - assert result.turns == 3 - assert result.metadata.get("max_turns_exceeded") is True - assert result.content == "Maximum turns reached without a final answer." - - def test_unknown_tool_error(self): - """Action references nonexistent tool -- ToolResult with success=False.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response( - 'Thought: Use a tool.\nAction: nonexistent\n' - 'Action Input: {}' - ), - _engine_response( - "Thought: Error occurred.\n" - "Final Answer: Could not run tool." - ), - ] - agent = ReActAgent(engine, "test-model", tools=[_CalculatorStub()]) - result = agent.run("Do something") - assert len(result.tool_results) == 1 - assert result.tool_results[0].success is False - assert "Unknown tool" in result.tool_results[0].content - - def test_event_bus_emissions(self): - """Verify AGENT_TURN_START and AGENT_TURN_END events. - - INFERENCE_START/END are now published by InstrumentedEngine, - not by agents directly. - """ - bus = EventBus(record_history=True) - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response( - "Thought: Quick.\nFinal Answer: Done." - ) - agent = ReActAgent(engine, "test-model", bus=bus) - agent.run("Hello") - event_types = [e.event_type for e in bus.history] - assert EventType.AGENT_TURN_START in event_types - assert EventType.AGENT_TURN_END in event_types - - def test_event_bus_tool_events(self): - """Tool call should trigger TOOL_CALL_START and TOOL_CALL_END events.""" - bus = EventBus(record_history=True) - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response( - 'Thought: Calc.\nAction: calculator\n' - 'Action Input: {"expression": "1+1"}' - ), - _engine_response("Thought: Done.\nFinal Answer: 2"), - ] - agent = ReActAgent(engine, "test-model", tools=[_CalculatorStub()], bus=bus) - agent.run("Calc") - event_types = [e.event_type for e in bus.history] - assert EventType.TOOL_CALL_START in event_types - assert EventType.TOOL_CALL_END in event_types - - def test_context_passing(self): - """Pass AgentContext with conversation history.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response( - "Thought: Simple.\nFinal Answer: Hi!" - ) - conv = Conversation() - conv.add(Message(role=Role.USER, content="Previous message")) - conv.add(Message(role=Role.ASSISTANT, content="Previous response")) - ctx = AgentContext(conversation=conv) - agent = ReActAgent(engine, "test-model") - agent.run("Hello", context=ctx) - call_args = engine.generate.call_args - messages = call_args[0][0] - # System prompt + 2 context messages + user input - assert len(messages) == 4 - assert messages[0].role == Role.SYSTEM - assert messages[1].role == Role.USER - assert messages[1].content == "Previous message" - assert messages[3].role == Role.USER - assert messages[3].content == "Hello" - - def test_with_think_tool(self): - """Use think tool for internal reasoning.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response( - 'Thought: Let me reason.\nAction: think\n' - 'Action Input: {"thought": "The user wants a greeting"}' - ), - _engine_response("Thought: Now I know.\nFinal Answer: Greetings!"), - ] - agent = ReActAgent(engine, "test-model", tools=[_ThinkStub()]) - result = agent.run("Say hi") - assert result.content == "Greetings!" - assert result.tool_results[0].tool_name == "think" - assert result.tool_results[0].content == "The user wants a greeting" - - def test_no_bus_works(self): - """Agent runs correctly without an event bus.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response( - "Thought: Easy.\nFinal Answer: Works!" - ) - agent = ReActAgent(engine, "test-model") - result = agent.run("Hello") - assert result.content == "Works!" - - def test_plain_response_no_structure(self): - """If engine returns no ReAct structure, treat as final answer.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response("Just a plain answer.") - agent = ReActAgent(engine, "test-model") - result = agent.run("Hello") - assert result.content == "Just a plain answer." - assert result.turns == 1 - - def test_observation_appended_to_messages(self): - """Observation from tool result is sent back to the engine.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.side_effect = [ - _engine_response( - 'Thought: Calc.\nAction: calculator\n' - 'Action Input: {"expression": "5+5"}' - ), - _engine_response("Thought: Got it.\nFinal Answer: 10"), - ] - agent = ReActAgent(engine, "test-model", tools=[_CalculatorStub()]) - agent.run("What is 5+5?") - # Check second call messages - second_call = engine.generate.call_args_list[1] - messages = second_call[0][0] - # Last message should be the observation - last_msg = messages[-1] - assert last_msg.role == Role.USER - assert "Observation:" in last_msg.content - assert "10" in last_msg.content - - def test_system_prompt_includes_tool_names(self): - """System prompt should list available tool names.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response( - "Thought: Done.\nFinal Answer: ok" - ) - agent = ReActAgent( - engine, "test-model", - tools=[_CalculatorStub(), _ThinkStub()], - ) - agent.run("Hello") - call_args = engine.generate.call_args - messages = call_args[0][0] - system_msg = messages[0] - assert "calculator" in system_msg.content - assert "think" in system_msg.content - - def test_system_prompt_no_tools(self): - """System prompt should say 'none' when no tools are available.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response( - "Thought: No tools.\nFinal Answer: ok" - ) - agent = ReActAgent(engine, "test-model") - agent.run("Hello") - call_args = engine.generate.call_args - messages = call_args[0][0] - assert "none" in messages[0].content - - def test_max_turns_1(self): - """With max_turns=1 and an action, should stop after 1 turn.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response( - 'Thought: Go.\nAction: calculator\n' - 'Action Input: {"expression": "1"}' - ) - agent = ReActAgent( - engine, "test-model", - tools=[_CalculatorStub()], - max_turns=1, - ) - result = agent.run("Calc") - assert result.turns == 1 - assert result.metadata.get("max_turns_exceeded") is True - - def test_event_data_agent_turn_start(self): - """AGENT_TURN_START event data should include agent id and input.""" - bus = EventBus(record_history=True) - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response( - "Thought: Quick.\nFinal Answer: Hi" - ) - agent = ReActAgent(engine, "test-model", bus=bus) - agent.run("test input") - start_events = [ - e for e in bus.history - if e.event_type == EventType.AGENT_TURN_START - ] - assert len(start_events) == 1 - assert start_events[0].data["agent"] == "react" - assert start_events[0].data["input"] == "test input" - - -@pytest.mark.parametrize("model", ["qwen3:8b", "gpt-oss:120b"]) -def test_react_with_different_models(model): - """ReAct agent works with different model names.""" - engine = MagicMock() - engine.engine_id = "mock" - engine.generate.return_value = _engine_response( - "Thought: Responding.\nFinal Answer: Hello!" - ) - agent = ReActAgent(engine, model) - result = agent.run("Hello") - assert result.content == "Hello!" - call_kwargs = engine.generate.call_args[1] - assert call_kwargs["model"] == model diff --git a/tests/cli/test_ask_agent.py b/tests/cli/test_ask_agent.py index 31cadbc5..e4a1a3ad 100644 --- a/tests/cli/test_ask_agent.py +++ b/tests/cli/test_ask_agent.py @@ -30,7 +30,6 @@ def _mock_engine(content="Hello from engine"): def _register_agents(): """Re-register agents after registry clear.""" - from openjarvis.agents.custom import CustomAgent from openjarvis.agents.orchestrator import OrchestratorAgent from openjarvis.agents.simple import SimpleAgent from openjarvis.core.registry import AgentRegistry @@ -38,7 +37,6 @@ def _register_agents(): for name, cls in [ ("simple", SimpleAgent), ("orchestrator", OrchestratorAgent), - ("custom", CustomAgent), ]: if not AgentRegistry.contains(name): AgentRegistry.register_value(name, cls) diff --git a/tests/test_integration_extended.py b/tests/test_integration_extended.py index f27a8799..90e8aced 100644 --- a/tests/test_integration_extended.py +++ b/tests/test_integration_extended.py @@ -22,14 +22,14 @@ from openjarvis.core.types import ( def _register_all(): """Ensure agents and tools are registered.""" - from openjarvis.agents.openhands import OpenHandsAgent - from openjarvis.agents.react import ReActAgent + from openjarvis.agents.native_openhands import NativeOpenHandsAgent + from openjarvis.agents.native_react import NativeReActAgent from openjarvis.tools.calculator import CalculatorTool from openjarvis.tools.think import ThinkTool for key, cls in [ - ("react", ReActAgent), - ("openhands", OpenHandsAgent), + ("native_react", NativeReActAgent), + ("native_openhands", NativeOpenHandsAgent), ]: if not AgentRegistry.contains(key): AgentRegistry.register_value(key, cls) @@ -78,7 +78,7 @@ class TestReActPipeline: def test_react_with_calculator_e2e(self): _register_all() - from openjarvis.agents.react import ReActAgent + from openjarvis.agents.native_react import NativeReActAgent from openjarvis.tools.calculator import CalculatorTool responses = [ @@ -94,7 +94,7 @@ class TestReActPipeline: ] engine = _make_engine(responses) bus = EventBus(record_history=True) - agent = ReActAgent( + agent = NativeReActAgent( engine, "test-model", tools=[CalculatorTool()], bus=bus, ) @@ -108,7 +108,7 @@ class TestReActPipeline: def test_react_with_think_tool(self): _register_all() - from openjarvis.agents.react import ReActAgent + from openjarvis.agents.native_react import NativeReActAgent from openjarvis.tools.think import ThinkTool responses = [ @@ -123,7 +123,7 @@ class TestReActPipeline: ), ] engine = _make_engine(responses) - agent = ReActAgent( + agent = NativeReActAgent( engine, "test-model", tools=[ThinkTool()], ) result = agent.run("Analyze this.") @@ -133,7 +133,7 @@ class TestReActPipeline: def test_react_direct_answer(self): """ReAct returns immediately when no tool use is needed.""" _register_all() - from openjarvis.agents.react import ReActAgent + from openjarvis.agents.native_react import NativeReActAgent engine = _make_engine( _simple_response( @@ -141,7 +141,7 @@ class TestReActPipeline: "Final Answer: Hello!" ) ) - agent = ReActAgent(engine, "test-model") + agent = NativeReActAgent(engine, "test-model") result = agent.run("Say hello") assert result.content == "Hello!" assert result.turns == 1 @@ -153,7 +153,7 @@ class TestReActPipeline: not by agents directly. """ _register_all() - from openjarvis.agents.react import ReActAgent + from openjarvis.agents.native_react import NativeReActAgent engine = _make_engine( _simple_response( @@ -161,7 +161,7 @@ class TestReActPipeline: ) ) bus = EventBus(record_history=True) - agent = ReActAgent( + agent = NativeReActAgent( engine, "test-model", bus=bus, ) agent.run("Test") @@ -181,7 +181,7 @@ class TestOpenHandsPipeline: def test_openhands_code_execution_e2e(self): _register_all() - from openjarvis.agents.openhands import OpenHandsAgent + from openjarvis.agents.native_openhands import NativeOpenHandsAgent from openjarvis.tools.code_interpreter import ( CodeInterpreterTool, ) @@ -199,7 +199,7 @@ class TestOpenHandsPipeline: _simple_response("The result is 4."), ] engine = _make_engine(responses) - agent = OpenHandsAgent( + agent = NativeOpenHandsAgent( engine, "test-model", tools=[CodeInterpreterTool()], ) @@ -214,12 +214,12 @@ class TestOpenHandsPipeline: def test_openhands_direct_answer(self): """OpenHands returns directly when no code is needed.""" _register_all() - from openjarvis.agents.openhands import OpenHandsAgent + from openjarvis.agents.native_openhands import NativeOpenHandsAgent engine = _make_engine( _simple_response("Hello! How can I help?") ) - agent = OpenHandsAgent(engine, "test-model") + agent = NativeOpenHandsAgent(engine, "test-model") result = agent.run("Say hello") assert result.content == "Hello! How can I help?" assert result.turns == 1 @@ -227,13 +227,13 @@ class TestOpenHandsPipeline: def test_openhands_event_chain(self): """Verify event chain through OpenHands run.""" _register_all() - from openjarvis.agents.openhands import OpenHandsAgent + from openjarvis.agents.native_openhands import NativeOpenHandsAgent engine = _make_engine( _simple_response("Direct answer.") ) bus = EventBus(record_history=True) - agent = OpenHandsAgent( + agent = NativeOpenHandsAgent( engine, "test-model", bus=bus, ) agent.run("Test") @@ -344,7 +344,7 @@ class TestCrossEngineConsistency: def test_same_query_same_format(self): """All engines return the same result dict shape.""" _register_all() - from openjarvis.agents.react import ReActAgent + from openjarvis.agents.native_react import NativeReActAgent for engine_name in ["vllm", "ollama", "mock"]: engine = _make_engine( @@ -354,7 +354,7 @@ class TestCrossEngineConsistency: ) ) engine.engine_id = engine_name - agent = ReActAgent(engine, "test-model") + agent = NativeReActAgent(engine, "test-model") result = agent.run("Test query") assert isinstance(result, AgentResult) assert result.content == "Result" @@ -362,7 +362,7 @@ class TestCrossEngineConsistency: def test_tool_calls_across_engines(self): """Tool calling works regardless of engine mock.""" _register_all() - from openjarvis.agents.react import ReActAgent + from openjarvis.agents.native_react import NativeReActAgent from openjarvis.tools.calculator import CalculatorTool for engine_name in ["vllm", "ollama"]: @@ -379,7 +379,7 @@ class TestCrossEngineConsistency: ] engine = _make_engine(responses) engine.engine_id = engine_name - agent = ReActAgent( + agent = NativeReActAgent( engine, "test-model", tools=[CalculatorTool()], ) @@ -496,7 +496,7 @@ class TestAgentRoutingMatrix: """Agents run consistently across different configurations.""" @pytest.mark.parametrize( - "agent_key", ["react", "openhands"], + "agent_key", ["native_react", "native_openhands"], ) def test_agent_returns_valid_result(self, agent_key): _register_all() @@ -504,7 +504,7 @@ class TestAgentRoutingMatrix: engine = _make_engine( _simple_response( "Thought: done.\nFinal Answer: ok" - if agent_key == "react" + if agent_key == "native_react" else "The answer is ok" ) ) @@ -517,7 +517,7 @@ class TestAgentRoutingMatrix: assert len(result.content) > 0 @pytest.mark.parametrize( - "agent_key", ["react", "openhands"], + "agent_key", ["native_react", "native_openhands"], ) def test_agent_emits_events(self, agent_key): _register_all() @@ -525,7 +525,7 @@ class TestAgentRoutingMatrix: engine = _make_engine( _simple_response( "Thought: done.\nFinal Answer: ok" - if agent_key == "react" + if agent_key == "native_react" else "Direct answer" ) ) @@ -541,7 +541,7 @@ class TestAgentRoutingMatrix: def test_context_passing(self): """Agents accept and use AgentContext.""" _register_all() - from openjarvis.agents.react import ReActAgent + from openjarvis.agents.native_react import NativeReActAgent engine = _make_engine( _simple_response( @@ -555,7 +555,7 @@ class TestAgentRoutingMatrix: content="You are helpful.", )) ctx = AgentContext(conversation=conv) - agent = ReActAgent(engine, "test-model") + agent = NativeReActAgent(engine, "test-model") result = agent.run("Hello", context=ctx) assert result.content == "Got context." diff --git a/uv.lock b/uv.lock index 53744b8b..a9e52e3a 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,18 @@ resolution-markers = [ "python_full_version < '3.11'", ] +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "caio", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -284,6 +296,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, ] +[[package]] +name = "authlib" +version = "1.6.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/6c/c88eac87468c607f88bc24df1f3b31445ee6fc9ba123b09e666adf687cd9/authlib-1.6.8.tar.gz", hash = "sha256:41ae180a17cf672bc784e4a518e5c82687f1fe1e98b0cafaeda80c8e4ab2d1cb", size = 165074, upload-time = "2026-02-14T04:02:17.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/73/f7084bf12755113cd535ae586782ff3a6e710bfbe6a0d13d1c2f81ffbbfa/authlib-1.6.8-py2.py3-none-any.whl", hash = "sha256:97286fd7a15e6cfefc32771c8ef9c54f0ed58028f1322de6a2a7c969c3817888", size = 244116, upload-time = "2026-02-14T04:02:15.579Z" }, +] + [[package]] name = "babel" version = "2.18.0" @@ -316,6 +340,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, ] +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + [[package]] name = "bitarray" version = "3.8.0" @@ -411,6 +444,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] +[[package]] +name = "cachetools" +version = "7.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" }, +] + +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/80/ea4ead0c5d52a9828692e7df20f0eafe8d26e671ce4883a0a146bb91049e/caio-0.9.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", size = 36836, upload-time = "2025-12-26T15:22:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/36715c97c873649d1029001578f901b50250916295e3dddf20c865438865/caio-0.9.25-cp310-cp310-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", size = 79695, upload-time = "2025-12-26T15:22:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" }, + { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" }, + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, +] + [[package]] name = "certifi" version = "2026.1.4" @@ -837,6 +898,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/5e/db279a3bfbd18d59d0598922a3b3c1454908d0969e8372260afec9736376/cuda_pathfinder-1.3.4-py3-none-any.whl", hash = "sha256:fb983f6e0d43af27ef486e14d5989b5f904ef45cedf40538bfdcbffa6bb01fb2", size = 30878, upload-time = "2026-02-11T18:50:31.008Z" }, ] +[[package]] +name = "cyclopts" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.12'" }, + { name = "docstring-parser", marker = "python_full_version >= '3.12'" }, + { name = "rich", marker = "python_full_version >= '3.12'" }, + { name = "rich-rst", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/5c/88a4068c660a096bbe87efc5b7c190080c9e86919c36ec5f092cb08d852f/cyclopts-4.6.0.tar.gz", hash = "sha256:483c4704b953ea6da742e8de15972f405d2e748d19a848a4d61595e8e5360ee5", size = 162724, upload-time = "2026-02-23T15:44:49.286Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/eb/1e8337755a70dc7d7ff10a73dc8f20e9352c9ad6c2256ed863ac95cd3539/cyclopts-4.6.0-py3-none-any.whl", hash = "sha256:0a891cb55bfd79a3cdce024db8987b33316aba11071e5258c21ac12a640ba9f2", size = 200518, upload-time = "2026-02-23T15:44:47.854Z" }, +] + [[package]] name = "datasets" version = "4.5.0" @@ -864,6 +940,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/d5/0d563ea3c205eee226dc8053cf7682a8ac588db8acecd0eda2b587987a0b/datasets-4.5.0-py3-none-any.whl", hash = "sha256:b5d7e08096ffa407dd69e58b1c0271c9b2506140839b8d99af07375ad31b6726", size = 515196, upload-time = "2026-01-14T18:27:52.419Z" }, ] +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + [[package]] name = "dill" version = "0.4.0" @@ -895,6 +983,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" @@ -904,12 +1001,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.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython", marker = "python_full_version >= '3.12'" }, + { name = "idna", marker = "python_full_version >= '3.12'" }, +] +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.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or python_full_version == '3.12.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -959,6 +1078,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/dd/d0ee25348ac58245ee9f90b6f3cbb666bf01f69be7e0911f9851bddbda16/fastapi-0.129.0-py3-none-any.whl", hash = "sha256:b4946880e48f462692b31c083be0432275cbfb6e2274566b1be91479cc1a84ec", size = 102950, upload-time = "2026-02-12T13:54:54.528Z" }, ] +[[package]] +name = "fastmcp" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib", marker = "python_full_version >= '3.12'" }, + { name = "cyclopts", marker = "python_full_version >= '3.12'" }, + { name = "exceptiongroup", marker = "python_full_version >= '3.12'" }, + { name = "httpx", marker = "python_full_version >= '3.12'" }, + { name = "jsonref", marker = "python_full_version >= '3.12'" }, + { name = "jsonschema-path", marker = "python_full_version >= '3.12'" }, + { name = "mcp", marker = "python_full_version >= '3.12'" }, + { name = "openapi-pydantic", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-api", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "platformdirs", marker = "python_full_version >= '3.12'" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"], marker = "python_full_version >= '3.12'" }, + { name = "pydantic", extra = ["email"], marker = "python_full_version >= '3.12'" }, + { name = "pyperclip", marker = "python_full_version >= '3.12'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.12'" }, + { name = "pyyaml", marker = "python_full_version >= '3.12'" }, + { name = "rich", marker = "python_full_version >= '3.12'" }, + { name = "uvicorn", marker = "python_full_version >= '3.12'" }, + { name = "watchfiles", marker = "python_full_version >= '3.12'" }, + { name = "websockets", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/6b/1a7ec89727797fb07ec0928e9070fa2f45e7b35718e1fe01633a34c35e45/fastmcp-3.0.2.tar.gz", hash = "sha256:6bd73b4a3bab773ee6932df5249dcbcd78ed18365ed0aeeb97bb42702a7198d7", size = 17239351, upload-time = "2026-02-22T16:32:28.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/5a/f410a9015cfde71adf646dab4ef2feae49f92f34f6050fcfb265eb126b30/fastmcp-3.0.2-py3-none-any.whl", hash = "sha256:f513d80d4b30b54749fe8950116b1aab843f3c293f5cb971fc8665cb48dbb028", size = 606268, upload-time = "2026-02-22T16:32:30.992Z" }, +] + [[package]] name = "fastuuid" version = "0.14.0" @@ -1260,6 +1410,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/56/765eca90c781fedbe2a7e7dc873ef6045048e28ba5f2d4a5bcb13e13062b/google_genai-1.64.0-py3-none-any.whl", hash = "sha256:78a4d2deeb33b15ad78eaa419f6f431755e7f0e03771254f8000d70f717e940b", size = 728836, upload-time = "2026-02-19T02:06:11.655Z" }, ] +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + [[package]] name = "griffelib" version = "2.0.0" @@ -1268,6 +1430,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] +[[package]] +name = "grpcio" +version = "1.78.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/de/de568532d9907552700f80dcec38219d8d298ad9e71f5e0a095abaf2761e/grpcio-1.78.1.tar.gz", hash = "sha256:27c625532d33ace45d57e775edf1982e183ff8641c72e4e91ef7ba667a149d72", size = 12835760, upload-time = "2026-02-20T01:16:10.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/30/0534b643dafd54824769d6260b89c71d518e4ef8b5ad16b84d1ae9272978/grpcio-1.78.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:4393bef64cf26dc07cd6f18eaa5170ae4eebaafd4418e7e3a59ca9526a6fa30b", size = 5947661, upload-time = "2026-02-20T01:12:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f8/f678566655ab822da0f713789555e7eddca7ef93da99f480c63de3aa94b4/grpcio-1.78.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:917047c19cd120b40aab9a4b8a22e9ce3562f4a1343c0d62b3cd2d5199da3d67", size = 11819948, upload-time = "2026-02-20T01:12:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/a4b4210d946055f4e5a8430f2802202ae8f831b4b00d36d55055c5cf4b6a/grpcio-1.78.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff7de398bb3528d44d17e6913a7cfe639e3b15c65595a71155322df16978c5e1", size = 6519850, upload-time = "2026-02-20T01:12:42.715Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d9/a1e657a73000a71fa75ec7140ff3a8dc32eb3427560620e477c6a2735527/grpcio-1.78.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:15f6e636d1152667ddb4022b37534c161c8477274edb26a0b65b215dd0a81e97", size = 7198654, upload-time = "2026-02-20T01:12:46.164Z" }, + { url = "https://files.pythonhosted.org/packages/aa/28/a61c5bdf53c1638e657bb5eebb93c789837820e1fdb965145f05eccc2994/grpcio-1.78.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:27b5cb669603efb7883a882275db88b6b5d6b6c9f0267d5846ba8699b7ace338", size = 6727238, upload-time = "2026-02-20T01:12:48.472Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3e/aa143d0687801986a29d85788c96089449f36651cd4e2a493737ae0c5be9/grpcio-1.78.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:86edb3966778fa05bfdb333688fde5dc9079f9e2a9aa6a5c42e9564b7656ba04", size = 7300960, upload-time = "2026-02-20T01:12:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/30/d3/53e0f26b46417f28d14b5951fc6a1eff79c08c8a339e967c0a19ec7cf9e9/grpcio-1.78.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:849cc62eb989bc3be5629d4f3acef79be0d0ff15622201ed251a86d17fef6494", size = 8285274, upload-time = "2026-02-20T01:12:53.315Z" }, + { url = "https://files.pythonhosted.org/packages/29/d0/e0e9fd477ce86c07ed1ed1d5c34790f050b6d58bfde77b02b36e23f8b235/grpcio-1.78.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9a00992d6fafe19d648b9ccb4952200c50d8e36d0cce8cf026c56ed3fdc28465", size = 7726620, upload-time = "2026-02-20T01:12:56.498Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b5/e138a9f7810d196081b2e047c378ca12358c5906d79c42ddec41bb43d528/grpcio-1.78.1-cp310-cp310-win32.whl", hash = "sha256:f8759a1347f3b4f03d9a9d4ce8f9f31ad5e5d0144ba06ccfb1ffaeb0ba4c1e20", size = 4076778, upload-time = "2026-02-20T01:12:59.098Z" }, + { url = "https://files.pythonhosted.org/packages/4e/95/9b02316b85731df0943a635ca6d02f155f673c4f17e60be0c4892a6eb051/grpcio-1.78.1-cp310-cp310-win_amd64.whl", hash = "sha256:e840405a3f1249509892be2399f668c59b9d492068a2cf326d661a8c79e5e747", size = 4798925, upload-time = "2026-02-20T01:13:03.186Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/ad774af3b2c84f49c6d8c4a7bea4c40f02268ea8380630c28777edda463b/grpcio-1.78.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:3a8aa79bc6e004394c0abefd4b034c14affda7b66480085d87f5fbadf43b593b", size = 5951132, upload-time = "2026-02-20T01:13:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/48/9d/ad3c284bedd88c545e20675d98ae904114d8517a71b0efc0901e9166628f/grpcio-1.78.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8e1fcb419da5811deb47b7749b8049f7c62b993ba17822e3c7231e3e0ba65b79", size = 11831052, upload-time = "2026-02-20T01:13:09.604Z" }, + { url = "https://files.pythonhosted.org/packages/6d/08/20d12865e47242d03c3ade9bb2127f5b4aded964f373284cfb357d47c5ac/grpcio-1.78.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b071dccac245c32cd6b1dd96b722283b855881ca0bf1c685cf843185f5d5d51e", size = 6524749, upload-time = "2026-02-20T01:13:21.692Z" }, + { url = "https://files.pythonhosted.org/packages/c6/53/a8b72f52b253ec0cfdf88a13e9236a9d717c332b8aa5f0ba9e4699e94b55/grpcio-1.78.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d6fb962947e4fe321eeef3be1ba5ba49d32dea9233c825fcbade8e858c14aaf4", size = 7198995, upload-time = "2026-02-20T01:13:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/ac769c8ded1bcb26bb119fb472d3374b481b3cf059a0875db9fc77139c17/grpcio-1.78.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6afd191551fd72e632367dfb083e33cd185bf9ead565f2476bba8ab864ae496", size = 6730770, upload-time = "2026-02-20T01:13:26.522Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c3/2275ef4cc5b942314321f77d66179be4097ff484e82ca34bf7baa5b1ddbc/grpcio-1.78.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b2acd83186305c0802dbc4d81ed0ec2f3e8658d7fde97cfba2f78d7372f05b89", size = 7305036, upload-time = "2026-02-20T01:13:30.923Z" }, + { url = "https://files.pythonhosted.org/packages/91/cb/3c2aa99e12cbbfc72c2ed8aa328e6041709d607d668860380e6cd00ba17d/grpcio-1.78.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5380268ab8513445740f1f77bd966d13043d07e2793487e61fd5b5d0935071eb", size = 8288641, upload-time = "2026-02-20T01:13:39.42Z" }, + { url = "https://files.pythonhosted.org/packages/0d/b2/21b89f492260ac645775d9973752ca873acfd0609d6998e9d3065a21ea2f/grpcio-1.78.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:389b77484959bdaad6a2b7dda44d7d1228381dd669a03f5660392aa0e9385b22", size = 7730967, upload-time = "2026-02-20T01:13:41.697Z" }, + { url = "https://files.pythonhosted.org/packages/24/03/6b89eddf87fdffb8fa9d37375d44d3a798f4b8116ac363a5f7ca84caa327/grpcio-1.78.1-cp311-cp311-win32.whl", hash = "sha256:9dee66d142f4a8cca36b5b98a38f006419138c3c89e72071747f8fca415a6d8f", size = 4076680, upload-time = "2026-02-20T01:13:43.781Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a8/204460b1bc1dff9862e98f56a2d14be3c4171f929f8eaf8c4517174b4270/grpcio-1.78.1-cp311-cp311-win_amd64.whl", hash = "sha256:43b930cf4f9c4a2262bb3e5d5bc40df426a72538b4f98e46f158b7eb112d2d70", size = 4801074, upload-time = "2026-02-20T01:13:46.315Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ed/d2eb9d27fded1a76b2a80eb9aa8b12101da7e41ce2bac0ad3651e88a14ae/grpcio-1.78.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:41e4605c923e0e9a84a2718e4948a53a530172bfaf1a6d1ded16ef9c5849fca2", size = 5913389, upload-time = "2026-02-20T01:13:49.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/1b/40034e9ab010eeb3fa41ec61d8398c6dbf7062f3872c866b8f72700e2522/grpcio-1.78.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:39da1680d260c0c619c3b5fa2dc47480ca24d5704c7a548098bca7de7f5dd17f", size = 11811839, upload-time = "2026-02-20T01:13:51.839Z" }, + { url = "https://files.pythonhosted.org/packages/b4/69/fe16ef2979ea62b8aceb3a3f1e7a8bbb8b717ae2a44b5899d5d426073273/grpcio-1.78.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b5d5881d72a09b8336a8f874784a8eeffacde44a7bc1a148bce5a0243a265ef0", size = 6475805, upload-time = "2026-02-20T01:13:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5b/1e/069e0a9062167db18446917d7c00ae2e91029f96078a072bedc30aaaa8c3/grpcio-1.78.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:888ceb7821acd925b1c90f0cdceaed1386e69cfe25e496e0771f6c35a156132f", size = 7169955, upload-time = "2026-02-20T01:13:59.553Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/44a57e2bb4a755e309ee4e9ed2b85c9af93450b6d3118de7e69410ee05fa/grpcio-1.78.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8942bdfc143b467c264b048862090c4ba9a0223c52ae28c9ae97754361372e42", size = 6690767, upload-time = "2026-02-20T01:14:02.31Z" }, + { url = "https://files.pythonhosted.org/packages/b8/87/21e16345d4c75046d453916166bc72a3309a382c8e97381ec4b8c1a54729/grpcio-1.78.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:716a544969660ed609164aff27b2effd3ff84e54ac81aa4ce77b1607ca917d22", size = 7266846, upload-time = "2026-02-20T01:14:12.974Z" }, + { url = "https://files.pythonhosted.org/packages/11/df/d6261983f9ca9ef4d69893765007a9a3211b91d9faf85a2591063df381c7/grpcio-1.78.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d50329b081c223d444751076bb5b389d4f06c2b32d51b31a1e98172e6cecfb9", size = 8253522, upload-time = "2026-02-20T01:14:17.407Z" }, + { url = "https://files.pythonhosted.org/packages/de/7c/4f96a0ff113c5d853a27084d7590cd53fdb05169b596ea9f5f27f17e021e/grpcio-1.78.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e836778c13ff70edada16567e8da0c431e8818eaae85b80d11c1ba5782eccbb", size = 7698070, upload-time = "2026-02-20T01:14:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/17/3c/7b55c0b5af88fbeb3d0c13e25492d3ace41ac9dbd0f5f8f6c0fb613b6706/grpcio-1.78.1-cp312-cp312-win32.whl", hash = "sha256:07eb016ea7444a22bef465cce045512756956433f54450aeaa0b443b8563b9ca", size = 4066474, upload-time = "2026-02-20T01:14:22.602Z" }, + { url = "https://files.pythonhosted.org/packages/5d/17/388c12d298901b0acf10b612b650692bfed60e541672b1d8965acbf2d722/grpcio-1.78.1-cp312-cp312-win_amd64.whl", hash = "sha256:02b82dcd2fa580f5e82b4cf62ecde1b3c7cc9ba27b946421200706a6e5acaf85", size = 4797537, upload-time = "2026-02-20T01:14:25.444Z" }, + { url = "https://files.pythonhosted.org/packages/df/72/754754639cfd16ad04619e1435a518124b2d858e5752225376f9285d4c51/grpcio-1.78.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:2b7ad2981550ce999e25ce3f10c8863f718a352a2fd655068d29ea3fd37b4907", size = 5919437, upload-time = "2026-02-20T01:14:29.403Z" }, + { url = "https://files.pythonhosted.org/packages/5c/84/6267d1266f8bc335d3a8b7ccf981be7de41e3ed8bd3a49e57e588212b437/grpcio-1.78.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:409bfe22220889b9906739910a0ee4c197a967c21b8dd14b4b06dd477f8819ce", size = 11803701, upload-time = "2026-02-20T01:14:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/f3/56/c9098e8b920a54261cd605bbb040de0cde1ca4406102db0aa2c0b11d1fb4/grpcio-1.78.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:34b6cb16f4b67eeb5206250dc5b4d5e8e3db939535e58efc330e4c61341554bd", size = 6479416, upload-time = "2026-02-20T01:14:35.926Z" }, + { url = "https://files.pythonhosted.org/packages/86/cf/5d52024371ee62658b7ed72480200524087528844ec1b65265bbcd31c974/grpcio-1.78.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:39d21fd30d38a5afb93f0e2e71e2ec2bd894605fb75d41d5a40060c2f98f8d11", size = 7174087, upload-time = "2026-02-20T01:14:39.98Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/5e59551afad4279e27335a6d60813b8aa3ae7b14fb62cea1d329a459c118/grpcio-1.78.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09fbd4bcaadb6d8604ed1504b0bdf7ac18e48467e83a9d930a70a7fefa27e862", size = 6692881, upload-time = "2026-02-20T01:14:42.466Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/940062de2d14013c02f51b079eb717964d67d46f5d44f22038975c9d9576/grpcio-1.78.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:db681513a1bdd879c0b24a5a6a70398da5eaaba0e077a306410dc6008426847a", size = 7269092, upload-time = "2026-02-20T01:14:45.826Z" }, + { url = "https://files.pythonhosted.org/packages/09/87/9db657a4b5f3b15560ec591db950bc75a1a2f9e07832578d7e2b23d1a7bd/grpcio-1.78.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f81816faa426da461e9a597a178832a351d6f1078102590a4b32c77d251b71eb", size = 8252037, upload-time = "2026-02-20T01:14:48.57Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/b980e0265479ec65e26b6e300a39ceac33ecb3f762c2861d4bac990317cf/grpcio-1.78.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffbb760df1cd49e0989f9826b2fd48930700db6846ac171eaff404f3cfbe5c28", size = 7695243, upload-time = "2026-02-20T01:14:51.376Z" }, + { url = "https://files.pythonhosted.org/packages/98/46/5fc42c100ab702fa1ea41a75c890c563c3f96432b4a287d5a6369654f323/grpcio-1.78.1-cp313-cp313-win32.whl", hash = "sha256:1a56bf3ee99af5cf32d469de91bf5de79bdac2e18082b495fc1063ea33f4f2d0", size = 4065329, upload-time = "2026-02-20T01:14:53.952Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/806d60bb6611dfc16cf463d982bd92bd8b6bd5f87dfac66b0a44dfe20995/grpcio-1.78.1-cp313-cp313-win_amd64.whl", hash = "sha256:8991c2add0d8505178ff6c3ae54bd9386279e712be82fa3733c54067aae9eda1", size = 4797637, upload-time = "2026-02-20T01:14:57.276Z" }, + { url = "https://files.pythonhosted.org/packages/96/3a/2d2ec4d2ce2eb9d6a2b862630a0d9d4ff4239ecf1474ecff21442a78612a/grpcio-1.78.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:d101fe49b1e0fb4a7aa36ed0c3821a0f67a5956ef572745452d2cd790d723a3f", size = 5920256, upload-time = "2026-02-20T01:15:00.23Z" }, + { url = "https://files.pythonhosted.org/packages/9c/92/dccb7d087a1220ed358753945230c1ddeeed13684b954cb09db6758f1271/grpcio-1.78.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:5ce1855e8cfc217cdf6bcfe0cf046d7cf81ddcc3e6894d6cfd075f87a2d8f460", size = 11813749, upload-time = "2026-02-20T01:15:03.312Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/c20e87f87986da9998f30f14776ce27e61f02482a3a030ffe265089342c6/grpcio-1.78.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd26048d066b51f39fe9206e2bcc2cea869a5e5b2d13c8d523f4179193047ebd", size = 6488739, upload-time = "2026-02-20T01:15:14.349Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c2/088bd96e255133d7d87c3eed0d598350d16cde1041bdbe2bb065967aaf91/grpcio-1.78.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4b8d7fda614cf2af0f73bbb042f3b7fee2ecd4aea69ec98dbd903590a1083529", size = 7173096, upload-time = "2026-02-20T01:15:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/60/ce/168db121073a03355ce3552b3b1f790b5ded62deffd7d98c5f642b9d3d81/grpcio-1.78.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:656a5bd142caeb8b1efe1fe0b4434ecc7781f44c97cfc7927f6608627cf178c0", size = 6693861, upload-time = "2026-02-20T01:15:20.911Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d0/90b30ec2d9425215dd56922d85a90babbe6ee7e8256ba77d866b9c0d3aba/grpcio-1.78.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:99550e344482e3c21950c034f74668fccf8a546d50c1ecb4f717543bbdc071ba", size = 7278083, upload-time = "2026-02-20T01:15:23.698Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fb/73f9ba0b082bcd385d46205095fd9c917754685885b28fce3741e9f54529/grpcio-1.78.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8f27683ca68359bd3f0eb4925824d71e538f84338b3ae337ead2ae43977d7541", size = 8252546, upload-time = "2026-02-20T01:15:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/6a89ea3cb5db6c3d9ed029b0396c49f64328c0cf5d2630ffeed25711920a/grpcio-1.78.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a40515b69ac50792f9b8ead260f194ba2bb3285375b6c40c7ff938f14c3df17d", size = 7696289, upload-time = "2026-02-20T01:15:29.718Z" }, + { url = "https://files.pythonhosted.org/packages/3d/05/63a7495048499ef437b4933d32e59b7f737bd5368ad6fb2479e2bd83bf2c/grpcio-1.78.1-cp314-cp314-win32.whl", hash = "sha256:2c473b54ef1618f4fb85e82ff4994de18143b74efc088b91b5a935a3a45042ba", size = 4142186, upload-time = "2026-02-20T01:15:32.786Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/adfe7e5f701d503be7778291757452e3fab6b19acf51917c79f5d1cf7f8a/grpcio-1.78.1-cp314-cp314-win_amd64.whl", hash = "sha256:e2a6b33d1050dce2c6f563c5caf7f7cbeebf7fba8cde37ffe3803d50526900d1", size = 4932000, upload-time = "2026-02-20T01:15:36.127Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1334,6 +1557,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 = "huggingface-hub" version = "1.4.1" @@ -1394,6 +1626,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1512,6 +1786,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -1527,6 +1810,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] +[[package]] +name = "jsonschema-path" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable", marker = "python_full_version >= '3.12'" }, + { name = "pyyaml", marker = "python_full_version >= '3.12'" }, + { name = "referencing", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/da/1ebeb1c0ff579c330e200e8b06e6200653e3d0758136d8bd86762d63e7de/jsonschema_path-0.4.2.tar.gz", hash = "sha256:5f5ff183150030ea24bb51cf1ddac9bf5dbf030272e2792a7ffe8262f7eea2a5", size = 13417, upload-time = "2026-02-23T16:21:36.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/10/96f8fe82137979fcd1e46fff243ce7d80cd03b9e1cee8f22476ce780f38c/jsonschema_path-0.4.2-py3-none-any.whl", hash = "sha256:9c3d88e727cc4f1a88e51dbbed4211dbcd815d27799d2685efd904435c3d39e7", size = 16702, upload-time = "2026-02-23T16:21:35.119Z" }, +] + [[package]] name = "jsonschema-specifications" version = "2025.9.1" @@ -1539,6 +1836,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes", marker = "python_full_version >= '3.12'" }, + { name = "jaraco-context", marker = "python_full_version >= '3.12'" }, + { name = "jaraco-functools", marker = "python_full_version >= '3.12'" }, + { name = "jeepney", marker = "python_full_version >= '3.12' and sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "secretstorage", marker = "python_full_version >= '3.12' and sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + [[package]] name = "litellm" version = "1.81.14" @@ -1562,6 +1876,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/b3/e8fe151c1b81666575552835a3a79127c5aa6bd460fcecc51e032d2f4019/litellm-1.81.14-py3-none-any.whl", hash = "sha256:6394e61bbdef7121e5e3800349f6b01e9369e7cf611e034f1832750c481abfed", size = 14603260, upload-time = "2026-02-22T00:33:32.464Z" }, ] +[[package]] +name = "lmnr" +version = "0.7.42" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio", marker = "python_full_version >= '3.12'" }, + { name = "httpx", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-api", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-exporter-otlp-proto-http", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-instrumentation", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-instrumentation-threading", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-sdk", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-semantic-conventions-ai", marker = "python_full_version >= '3.12'" }, + { name = "orjson", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pydantic", marker = "python_full_version >= '3.12'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.12'" }, + { name = "tenacity", marker = "python_full_version >= '3.12'" }, + { name = "tqdm", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/95/5b9dac7022e81494871aa228ccb0578668781cbc4241b59bd89ebe37536e/lmnr-0.7.42.tar.gz", hash = "sha256:2bea334ca201470120f1c5dd18eccf6aee503c8d962addda2761dfa681f74845", size = 240415, upload-time = "2026-02-23T14:23:24.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/ae/276e3888d412e6bf993a04dc72fa6c914d3430a18e867f322c6453361622/lmnr-0.7.42-py3-none-any.whl", hash = "sha256:1e87f45870216526719491cd4edac5b20bd58157423a879bc075a20fc7a6ef7e", size = 316861, upload-time = "2026-02-23T14:23:21.239Z" }, +] + [[package]] name = "markdown" version = "3.10.2" @@ -1668,6 +2009,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.12'" }, + { name = "httpx", marker = "python_full_version >= '3.12'" }, + { name = "httpx-sse", marker = "python_full_version >= '3.12'" }, + { name = "jsonschema", marker = "python_full_version >= '3.12'" }, + { name = "pydantic", marker = "python_full_version >= '3.12'" }, + { name = "pydantic-settings", marker = "python_full_version >= '3.12'" }, + { name = "pyjwt", extra = ["crypto"], marker = "python_full_version >= '3.12'" }, + { name = "python-multipart", marker = "python_full_version >= '3.12'" }, + { name = "pywin32", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "sse-starlette", marker = "python_full_version >= '3.12'" }, + { name = "starlette", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.12'" }, + { name = "uvicorn", marker = "python_full_version >= '3.12' and 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" @@ -1806,6 +2172,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, ] +[[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 = "mpmath" version = "1.3.0" @@ -2348,6 +2723,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/56/0a89092a453bb2c676d66abee44f863e742b2110d4dbb1dbcca3f7e5fc33/openai-2.21.0-py3-none-any.whl", hash = "sha256:0bc1c775e5b1536c294eded39ee08f8407656537ccc71b1004104fe1602e267c", size = 1103065, upload-time = "2026-02-14T00:11:59.603Z" }, ] +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "python_full_version >= '3.12'" }, +] +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 = "openhands-sdk" +version = "1.11.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation", marker = "python_full_version >= '3.12'" }, + { name = "fastmcp", marker = "python_full_version >= '3.12'" }, + { name = "filelock", marker = "python_full_version >= '3.12'" }, + { name = "httpx", marker = "python_full_version >= '3.12'" }, + { name = "litellm", marker = "python_full_version >= '3.12'" }, + { name = "lmnr", marker = "python_full_version >= '3.12'" }, + { name = "pydantic", marker = "python_full_version >= '3.12'" }, + { name = "python-frontmatter", marker = "python_full_version >= '3.12'" }, + { name = "python-json-logger", marker = "python_full_version >= '3.12'" }, + { name = "tenacity", marker = "python_full_version >= '3.12'" }, + { name = "websockets", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/fc/cfb73768099be94c9f92b2e160f29d1f6d3a4dd84fea5e33a2b0984449cc/openhands_sdk-1.11.5.tar.gz", hash = "sha256:dd6225876b7b8dbb6c608559f2718c3d0bf44d0bb741e990b185c6cdc5150c5a", size = 295069, upload-time = "2026-02-20T22:16:47.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/6b/21df2a5b9ed756a48566d96ad491c81609af804b271c370106a4ced4ed5c/openhands_sdk-1.11.5-py3-none-any.whl", hash = "sha256:f949cd540cbecc339d90fb0cca2a5f29e1b62566b82b5aee82ef40f259d14e60", size = 377527, upload-time = "2026-02-20T22:16:48.165Z" }, +] + [[package]] name = "openjarvis" version = "1.0.0" @@ -2407,6 +2816,9 @@ memory-faiss = [ memory-pdf = [ { name = "pdfplumber" }, ] +openhands = [ + { name = "openhands-sdk", marker = "python_full_version >= '3.12'" }, +] orchestrator-training = [ { name = "torch" }, { name = "transformers" }, @@ -2436,6 +2848,7 @@ requires-dist = [ { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = ">=0.25" }, { name = "numpy", marker = "extra == 'memory-faiss'", specifier = ">=1.24" }, { name = "openai", marker = "extra == 'inference-cloud'", specifier = ">=1.30" }, + { name = "openhands-sdk", marker = "python_full_version >= '3.12' and extra == 'openhands'", specifier = ">=1.0" }, { name = "pdfplumber", marker = "extra == 'memory-pdf'", specifier = ">=0.10" }, { name = "pydantic", marker = "extra == 'server'", specifier = ">=2.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" }, @@ -2455,7 +2868,226 @@ requires-dist = [ { name = "transformers", marker = "extra == 'orchestrator-training'", specifier = ">=4.40" }, { name = "uvicorn", marker = "extra == 'server'", specifier = ">=0.30" }, ] -provides-extras = ["dev", "inference-ollama", "inference-vllm", "inference-llamacpp", "inference-cloud", "inference-google", "inference-litellm", "tools-search", "memory-faiss", "memory-colbert", "memory-pdf", "memory-bm25", "server", "agents", "learning", "orchestrator-training", "channel-telegram", "channel-discord", "channel-slack", "channel-webhook", "channel-email", "channel-whatsapp", "channel-signal", "channel-google-chat", "channel-irc", "channel-webchat", "channel-teams", "channel-matrix", "channel-mattermost", "channel-feishu", "channel-bluebubbles", "docs"] +provides-extras = ["dev", "inference-ollama", "inference-vllm", "inference-llamacpp", "inference-cloud", "inference-google", "inference-litellm", "tools-search", "memory-faiss", "memory-colbert", "memory-pdf", "memory-bm25", "server", "agents", "openhands", "learning", "orchestrator-training", "channel-telegram", "channel-discord", "channel-slack", "channel-webhook", "channel-email", "channel-whatsapp", "channel-signal", "channel-google-chat", "channel-irc", "channel-webchat", "channel-teams", "channel-matrix", "channel-mattermost", "channel-feishu", "channel-bluebubbles", "docs"] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", marker = "python_full_version >= '3.12'" }, + { name = "grpcio", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-api", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-exporter-otlp-proto-common", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-proto", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-sdk", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/48/b329fed2c610c2c32c9366d9dc597202c9d1e58e631c137ba15248d8850f/opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad", size = 24650, upload-time = "2025-12-11T13:32:41.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/a3/cc9b66575bd6597b98b886a2067eea2693408d2d5f39dad9ab7fc264f5f3/opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18", size = 19766, upload-time = "2025-12-11T13:32:21.027Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-api", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-exporter-otlp-proto-common", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-proto", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-sdk", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "wrapt", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-threading" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-instrumentation", marker = "python_full_version >= '3.12'" }, + { name = "wrapt", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/0a/e36123ec4c0910a3936b92982545a53e9bca5b26a28df06883751a783f84/opentelemetry_instrumentation_threading-0.60b1.tar.gz", hash = "sha256:20b18a68abe5801fa9474336b7c27487d4af3e00b66f6a8734e4fdd75c8b0b43", size = 8768, upload-time = "2025-12-11T13:37:16.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/a3/448738b927bcc1843ace7d4ed55dd54441a71363075eeeee89c5944dd740/opentelemetry_instrumentation_threading-0.60b1-py3-none-any.whl", hash = "sha256:92a52a60fee5e32bc6aa8f5acd749b15691ad0bc4457a310f5736b76a6d9d1de", size = 9312, upload-time = "2025-12-11T13:36:28.434Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.12'" }, + { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions-ai" +version = "0.4.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e6/40b59eda51ac47009fb47afcdf37c6938594a0bd7f3b9fadcbc6058248e3/opentelemetry_semantic_conventions_ai-0.4.13.tar.gz", hash = "sha256:94efa9fb4ffac18c45f54a3a338ffeb7eedb7e1bb4d147786e77202e159f0036", size = 5368, upload-time = "2025-08-22T10:14:17.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/b5/cf25da2218910f0d6cdf7f876a06bed118c4969eacaf60a887cbaef44f44/opentelemetry_semantic_conventions_ai-0.4.13-py3-none-any.whl", hash = "sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5", size = 6080, upload-time = "2025-08-22T10:14:16.477Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" }, + { url = "https://files.pythonhosted.org/packages/52/a2/fa129e749d500f9b183e8a3446a193818a25f60261e9ce143ad61e975208/orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67", size = 128670, upload-time = "2026-02-02T15:37:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/1e82011cd1e0bd051ef9d35bed1aa7fb4ea1f0a055dc2c841b46b43a9ebd/orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11", size = 123832, upload-time = "2026-02-02T15:37:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d8/a26b431ef962c7d55736674dddade876822f3e33223c1f47a36879350d04/orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc", size = 129171, upload-time = "2026-02-02T15:37:11.112Z" }, + { url = "https://files.pythonhosted.org/packages/a7/19/f47819b84a580f490da260c3ee9ade214cf4cf78ac9ce8c1c758f80fdfc9/orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16", size = 141967, upload-time = "2026-02-02T15:37:12.282Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cd/37ece39a0777ba077fdcdbe4cccae3be8ed00290c14bf8afdc548befc260/orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222", size = 130991, upload-time = "2026-02-02T15:37:13.465Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ed/f2b5d66aa9b6b5c02ff5f120efc7b38c7c4962b21e6be0f00fd99a5c348e/orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa", size = 133674, upload-time = "2026-02-02T15:37:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6e/baa83e68d1aa09fa8c3e5b2c087d01d0a0bd45256de719ed7bc22c07052d/orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e", size = 138722, upload-time = "2026-02-02T15:37:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/0c/47/7f8ef4963b772cd56999b535e553f7eb5cd27e9dd6c049baee6f18bfa05d/orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2", size = 409056, upload-time = "2026-02-02T15:37:17.895Z" }, + { url = "https://files.pythonhosted.org/packages/38/eb/2df104dd2244b3618f25325a656f85cc3277f74bbd91224752410a78f3c7/orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c", size = 144196, upload-time = "2026-02-02T15:37:19.349Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2a/ee41de0aa3a6686598661eae2b4ebdff1340c65bfb17fcff8b87138aab21/orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f", size = 134979, upload-time = "2026-02-02T15:37:20.906Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/92fc5d3d402b87a8b28277a9ed35386218a6a5287c7fe5ee9b9f02c53fb2/orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de", size = 127968, upload-time = "2026-02-02T15:37:23.178Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/a576bf36d73d60df06904d3844a9df08e25d59eba64363aaf8ec2f9bff41/orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993", size = 125128, upload-time = "2026-02-02T15:37:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +] [[package]] name = "packaging" @@ -2613,6 +3245,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" }, ] +[[package]] +name = "pathable" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, +] + [[package]] name = "pathspec" version = "1.0.4" @@ -2879,6 +3520,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "py-key-value-aio" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype", marker = "python_full_version >= '3.12'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, +] + +[package.optional-dependencies] +filetree = [ + { name = "aiofile", marker = "python_full_version >= '3.12'" }, + { name = "anyio", marker = "python_full_version >= '3.12'" }, +] +keyring = [ + { name = "keyring", marker = "python_full_version >= '3.12'" }, +] +memory = [ + { name = "cachetools", marker = "python_full_version >= '3.12'" }, +] + [[package]] name = "pyarrow" version = "23.0.1" @@ -2981,6 +3662,11 @@ wheels = [ { 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.optional-dependencies] +email = [ + { name = "email-validator", marker = "python_full_version >= '3.12'" }, +] + [[package]] name = "pydantic-core" version = "2.41.5" @@ -3099,6 +3785,20 @@ wheels = [ { 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", marker = "python_full_version >= '3.12'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.12'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.12'" }, +] +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]] name = "pygments" version = "2.19.2" @@ -3108,6 +3808,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyjwt" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography", marker = "python_full_version >= '3.12'" }, +] + [[package]] name = "pymdown-extensions" version = "10.21" @@ -3150,6 +3864,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/17/18ad82f070da18ab970928f730fbd44d9b05aafcb52a2ebb6470eaae53f9/pypdfium2-5.4.0-py3-none-win_arm64.whl", hash = "sha256:2b78ea216fb92e7709b61c46241ebf2cc0c60cf18ad2fb4633af665d7b4e21e6", size = 2938727, upload-time = "2026-02-08T16:54:06.814Z" }, ] +[[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 = "9.0.2" @@ -3217,6 +3940,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] +[[package]] +name = "python-frontmatter" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/de/910fa208120314a12f9a88ea63e03707261692af782c99283f1a2c8a5e6f/python-frontmatter-1.1.0.tar.gz", hash = "sha256:7118d2bd56af9149625745c58c9b51fb67e8d1294a0c76796dafdc72c36e5f6d", size = 16256, upload-time = "2024-01-16T18:50:04.052Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/87/3c8da047b3ec5f99511d1b4d7a5bc72d4b98751c7e78492d14dc736319c5/python_frontmatter-1.1.0-py3-none-any.whl", hash = "sha256:335465556358d9d0e6c98bbeb69b1c969f2a4a21360587b9873bfc3b213407c1", size = 9834, upload-time = "2024-01-16T18:50:00.911Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + +[[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-telegram-bot" version = "22.6" @@ -3239,6 +3992,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { 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]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -3503,6 +4287,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", marker = "python_full_version >= '3.12'" }, + { name = "rich", marker = "python_full_version >= '3.12'" }, +] +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 = "rpds-py" version = "0.30.0" @@ -3943,6 +4740,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/a5/df8f46ef7da168f1bc52cd86e09a9de5c6f19cc1da04454d51b7d4f43408/scipy-1.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:031121914e295d9791319a1875444d55079885bbae5bdc9c5e0f2ee5f09d34ff", size = 25246266, upload-time = "2026-01-10T21:30:45.923Z" }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "jeepney", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + [[package]] name = "sentence-transformers" version = "5.2.2" @@ -4019,6 +4829,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.12'" }, + { name = "starlette", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, +] + [[package]] name = "starlette" version = "0.52.1" @@ -4526,6 +5349,109 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + [[package]] name = "websockets" version = "15.0.1" @@ -4597,6 +5523,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, ] +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + [[package]] name = "xxhash" version = "3.6.0"