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*(.*?)\w+>",
+ 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<||$)", raw_params, re.DOTALL):
+ params[m.group(1)] = m.group(2).strip().rstrip(">\n")
+ # value format
+ for m in re.finditer(r"<(\w+)>(.*?)\1>", 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*(.*?)\w+>",
- 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<||$)", raw_params, re.DOTALL):
- params[m.group(1)] = m.group(2).strip().rstrip(">\n")
- # value format
- for m in re.finditer(r"<(\w+)>(.*?)\1>", 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"