From 532675300f3b39e3bd435cc3ba47d14fe9153c97 Mon Sep 17 00:00:00 2001 From: ajspig Date: Thu, 15 Jan 2026 10:19:04 -0500 Subject: [PATCH] fix: formatting toolkits to be more structural and come specifically from the perspective of the agent using it. --- .../python/examples/multi_peer_example.py | 195 +++++++++++++ .../python/examples/multi_tool_example.py | 48 +++- .../agno/python/examples/simple_example.py | 52 +++- examples/agno/python/pyproject.toml | 16 ++ .../agno/python/src/honcho_agno/__init__.py | 10 +- examples/agno/python/src/honcho_agno/tools.py | 109 ++++--- examples/agno/python/tests/test_tools.py | 270 ++++++++++++------ examples/agno/python/uv.lock | 136 ++++++++- 8 files changed, 669 insertions(+), 167 deletions(-) create mode 100644 examples/agno/python/examples/multi_peer_example.py diff --git a/examples/agno/python/examples/multi_peer_example.py b/examples/agno/python/examples/multi_peer_example.py new file mode 100644 index 00000000..fa882b4d --- /dev/null +++ b/examples/agno/python/examples/multi_peer_example.py @@ -0,0 +1,195 @@ +""" +Multi-Peer Honcho + Agno Example + +A realistic multi-agent scenario using Agno's patterns: +- A coordinator agent routes questions to specialists +- Each specialist has its own HonchoTools (identity) +- All share the same session for conversation continuity +- The coordinator uses specialists as tools + +Environment Variables: + OPENAI_API_KEY or LLM_OPENAI_API_KEY: OpenAI API key + HONCHO_ENVIRONMENT: 'local' or 'production' (default: production) + HONCHO_API_KEY: Required for production environment +""" + +import os +import uuid + +from dotenv import load_dotenv + +from agno.agent import Agent +from agno.models.openai import OpenAIChat +from agno.tools import tool + +from honcho import Honcho +from honcho_agno import HonchoTools + +load_dotenv() + +if not os.getenv("OPENAI_API_KEY") and os.getenv("LLM_OPENAI_API_KEY"): + os.environ["OPENAI_API_KEY"] = os.getenv("LLM_OPENAI_API_KEY") + + +def create_advisor_system(honcho_env: str, session_id: str): + """ + Creates a multi-agent advisory system where: + - Each specialist agent has its own identity (HonchoTools) + - A coordinator routes to specialists and synthesizes responses + - All agents share the same conversation session + """ + model_id = os.getenv("OPENAI_MODEL", "gpt-4o") + + # Shared Honcho client and session + honcho = Honcho(workspace_id="advisory-system", environment=honcho_env) + session = honcho.session(session_id) + user_peer = honcho.peer("user") + + # === SPECIALIST AGENTS === + # Each has its own identity via HonchoTools + + tech_tools = HonchoTools( + app_id="advisory-system", + peer_id="tech-specialist", + session_id=session_id, + environment=honcho_env, + honcho_client=honcho, + ) + + tech_agent = Agent( + name="Tech Specialist", + model=OpenAIChat(id=model_id), + tools=[tech_tools], + description="Technical advisor for architecture, implementation, and technology choices.", + instructions=[ + "Focus on technical feasibility and implementation details.", + "Use get_context to understand what's been discussed.", + "Save key technical recommendations with add_message.", + "Be concise - you're part of a team.", + ], + ) + + business_tools = HonchoTools( + app_id="advisory-system", + peer_id="business-specialist", + session_id=session_id, + environment=honcho_env, + honcho_client=honcho, + ) + + business_agent = Agent( + name="Business Specialist", + model=OpenAIChat(id=model_id), + tools=[business_tools], + description="Business advisor for strategy, market fit, and ROI.", + instructions=[ + "Focus on business viability and market considerations.", + "Use get_context to understand what's been discussed.", + "Save key business insights with add_message.", + "Be concise - you're part of a team.", + ], + ) + + # === COORDINATOR TOOLS === + # Wrap specialists as tools the coordinator can invoke + + @tool + def consult_tech_specialist(question: str) -> str: + """ + Consult the technical specialist for architecture, implementation, + or technology-related questions. + + Args: + question: The technical question to ask. + + Returns: + Technical specialist's response. + """ + response = tech_agent.run(question) + return response.content + + @tool + def consult_business_specialist(question: str) -> str: + """ + Consult the business specialist for strategy, market fit, + or ROI-related questions. + + Args: + question: The business question to ask. + + Returns: + Business specialist's response. + """ + response = business_agent.run(question) + return response.content + + # Coordinator has its own identity too + coordinator_tools = HonchoTools( + app_id="advisory-system", + peer_id="coordinator", + session_id=session_id, + environment=honcho_env, + honcho_client=honcho, + ) + + coordinator = Agent( + name="Advisory Coordinator", + model=OpenAIChat(id=model_id), + tools=[coordinator_tools, consult_tech_specialist, consult_business_specialist], + description="Coordinates between specialists to provide comprehensive advice.", + instructions=[ + "Use get_context to understand the full conversation history.", + "Route technical questions to the tech specialist.", + "Route business questions to the business specialist.", + "Synthesize specialist inputs into actionable recommendations.", + "Save your final synthesis with add_message.", + ], + ) + + return coordinator, session, user_peer + + +def main(test_mode: bool = False): + honcho_env = os.getenv("HONCHO_ENVIRONMENT", "production") + session_id = f"advisory-{uuid.uuid4().hex[:8]}" + + print(f"Session: {session_id}") + print("=" * 60) + + coordinator, session, user_peer = create_advisor_system(honcho_env, session_id) + + if test_mode: + # Non-interactive test + test_question = "I want to build a SaaS product for small businesses. What should I consider?" + print(f"\n[TEST MODE] User: {test_question}\n") + + session.add_messages([user_peer.message(test_question)]) + response = coordinator.run(test_question) + print(f"Advisor: {response.content}\n") + print("=" * 60) + print("Test completed successfully!") + return + + # Interactive chat loop + print("\nAdvisory System Ready") + print("Ask questions about building a product. Type 'quit' to exit.\n") + + while True: + user_input = input("You: ").strip() + if not user_input: + continue + if user_input.lower() in ("quit", "exit", "q"): + break + + # Save user message to session + session.add_messages([user_peer.message(user_input)]) + + # Coordinator handles routing and synthesis + response = coordinator.run(user_input) + print(f"\nAdvisor: {response.content}\n") + + +if __name__ == "__main__": + import sys + test_mode = "--test" in sys.argv + main(test_mode=test_mode) diff --git a/examples/agno/python/examples/multi_tool_example.py b/examples/agno/python/examples/multi_tool_example.py index fd09800f..a9e0ea8f 100644 --- a/examples/agno/python/examples/multi_tool_example.py +++ b/examples/agno/python/examples/multi_tool_example.py @@ -2,10 +2,15 @@ Honcho Multi-Tool Example Demonstrates using all Honcho tools with an Agno agent: -- add_message: Store conversation history +- add_message: Store agent responses (attributed to the toolkit's peer) - get_context: Retrieve session context - search_messages: Semantic search -- query_user: Dialectic API queries +- query_peer: Dialectic API queries about any peer + +Pattern: toolkit = agent identity +- HonchoTools represents the assistant's identity +- User messages are added via Honcho directly +- The agent uses tools to query context and save its responses Environment Variables: OPENAI_API_KEY or LLM_OPENAI_API_KEY: OpenAI API key @@ -21,6 +26,7 @@ from dotenv import load_dotenv from agno.agent import Agent from agno.models.openai import OpenAIChat +from honcho import Honcho from honcho_agno import HonchoTools load_dotenv() @@ -38,16 +44,25 @@ def main(): # Get environment settings honcho_env = os.getenv("HONCHO_ENVIRONMENT", "production") - # Setup Honcho tools with a specific session - honcho_tools = HonchoTools( - app_id="travel-app", - user_id="traveler-42", - session_id="trip-planning-session", + # Initialize Honcho for managing the session and user peer + honcho = Honcho( + workspace_id="travel-app", environment=honcho_env, ) + session = honcho.session("trip-planning-session") + user_peer = honcho.peer("traveler-42") - # Pre-populate with travel preferences - print("Adding travel preferences to memory...") + # Setup Honcho tools - this IS the assistant's identity + honcho_tools = HonchoTools( + app_id="travel-app", + peer_id="travel-assistant", # The toolkit speaks as "travel-assistant" + session_id="trip-planning-session", + environment=honcho_env, + honcho_client=honcho, + ) + + # Pre-populate with user's travel preferences (via Honcho directly) + print("Adding user's travel preferences to memory...") messages = [ "I'm planning a trip to Japan in March", "I love trying authentic local cuisine", @@ -57,8 +72,8 @@ def main(): ] for msg in messages: - result = honcho_tools.add_message(msg, role="user") - print(f" Added: {msg[:50]}...") + session.add_messages([user_peer.message(msg)]) + print(f" [traveler-42]: {msg[:50]}...") print("\n" + "-" * 70 + "\n") @@ -70,13 +85,14 @@ def main(): description=( "A travel planning expert with access to memory tools. " "Use get_context for recent conversation, search_messages to find " - "specific preferences, and query_user to understand the traveler." + "specific preferences, and query_peer to understand the traveler." ), instructions=[ "Always retrieve relevant context before making recommendations", "Use search to find specific preferences mentioned", - "Query the user's knowledge to understand their travel style", + "Use query_peer with target_peer_id='traveler-42' to understand their travel style", "Be specific and actionable in your recommendations", + "Use add_message to save your recommendations to the conversation", ], ) @@ -100,6 +116,12 @@ def main(): search_result = honcho_tools.search_messages("budget money cost", limit=5) print(search_result) + # Show full conversation context + print("\n" + "=" * 70) + print("FULL SESSION CONTEXT") + print("=" * 70) + print(honcho_tools.get_context()) + if __name__ == "__main__": main() diff --git a/examples/agno/python/examples/simple_example.py b/examples/agno/python/examples/simple_example.py index 5d6e800c..2ee4aad1 100644 --- a/examples/agno/python/examples/simple_example.py +++ b/examples/agno/python/examples/simple_example.py @@ -1,8 +1,6 @@ """ Simple Honcho + Agno Example -A minimal example showing how to use HonchoTools with Agno agents. - Environment Variables: OPENAI_API_KEY or LLM_OPENAI_API_KEY: OpenAI API key HONCHO_ENVIRONMENT: 'local' or 'production' (default: production) @@ -10,12 +8,14 @@ Environment Variables: """ import os +import uuid from dotenv import load_dotenv from agno.agent import Agent from agno.models.openai import OpenAIChat +from honcho import Honcho from honcho_agno import HonchoTools load_dotenv() @@ -29,18 +29,24 @@ def main(): # Get environment settings honcho_env = os.getenv("HONCHO_ENVIRONMENT", "production") - # Initialize Honcho tools with user context - honcho_tools = HonchoTools( - app_id="agno-demo", - user_id="demo-user", + # Create shared session + session_id = f"simple-{uuid.uuid4().hex[:8]}" + + # Initialize Honcho directly for managing user messages + honcho = Honcho( + workspace_id="agno-demo", environment=honcho_env, ) + session = honcho.session(session_id) + user_peer = honcho.peer("user") - # Add some conversation history manually - print("Adding conversation history...") - honcho_tools.add_message("I'm learning Python programming", role="user") - honcho_tools.add_message( - "I'm also interested in web development with FastAPI", role="user" + # Initialize HonchoTools - this IS the assistant's identity + honcho_tools = HonchoTools( + app_id="agno-demo", + peer_id="assistant", # The toolkit speaks as "assistant" + session_id=session_id, # Same session as user + environment=honcho_env, + honcho_client=honcho, # Reuse client ) # Create an agent with memory tools @@ -50,16 +56,24 @@ def main(): tools=[honcho_tools], description="A programming mentor that remembers user interests and progress.", instructions=[ - "Use the memory tools to understand the user's background", - "Provide personalized recommendations based on their interests", + "Use get_context to understand the conversation history", + "Use query_peer to ask about the user's preferences", + "Use add_message to save your responses to the conversation", ], ) + # Add user messages via Honcho directly + print("Adding user messages to conversation...") + session.add_messages([ + user_peer.message("I'm learning Python programming"), + user_peer.message("I'm also interested in web development with FastAPI"), + ]) + # The agent can now query memories and provide personalized responses print("\nAsking the agent for recommendations...") response = agent.run( - "Based on what you know about me, what should I learn next? " - "Use your memory tools to check my interests first." + "Based on what you know about the user, what should they learn next? " + "Use get_context to see the conversation history first." ) print("\n" + "=" * 60) @@ -67,6 +81,14 @@ def main(): print("=" * 60) print(response.content) + # Show the full context + print("\n" + "=" * 60) + print("SESSION CONTEXT") + print("=" * 60) + print(honcho_tools.get_context()) + if __name__ == "__main__": + import sys + # Run directly - test mode is default for non-interactive execution main() diff --git a/examples/agno/python/pyproject.toml b/examples/agno/python/pyproject.toml index 024254b6..b943a36b 100644 --- a/examples/agno/python/pyproject.toml +++ b/examples/agno/python/pyproject.toml @@ -50,9 +50,25 @@ Changelog = "https://github.com/plastic-labs/honcho/blob/main/CHANGELOG.md" requires = ["hatchling"] build-backend = "hatchling.build" +[dependency-groups] +dev = [ + "basedpyright>=1.29.4", + "pytest>=8.2.2", +] + [tool.hatch.build.targets.wheel] packages = ["src/honcho_agno"] +[tool.basedpyright] +include = ["src", "tests", "examples"] +venvPath = "." +venv = ".venv" +pythonVersion = "3.11" +reportMissingTypeStubs = false +reportAny = false +reportExplicitAny = false +reportUnusedCallResult = false + [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] diff --git a/examples/agno/python/src/honcho_agno/__init__.py b/examples/agno/python/src/honcho_agno/__init__.py index b6840c43..d4a8b3b0 100644 --- a/examples/agno/python/src/honcho_agno/__init__.py +++ b/examples/agno/python/src/honcho_agno/__init__.py @@ -4,6 +4,9 @@ Honcho Agno Integration This package provides seamless integration between Honcho and Agno, enabling AI agents to maintain persistent memory across conversations. +Each HonchoTools instance represents ONE agent identity (peer). The toolkit +speaks as that peer when adding messages or querying the dialectic. + Example: ```python from agno.agent import Agent @@ -13,7 +16,8 @@ Example: # Create Honcho tools honcho_tools = HonchoTools( app_id="my-app", - user_id="user-123", + peer_id="assistant", # The identity for the agent using this toolkit + session_id="session-123", ) # Create agent with memory @@ -24,8 +28,8 @@ Example: description="An assistant with persistent memory powered by Honcho.", ) - # Run the agent - response = agent.run("Remember that I prefer Python over JavaScript") + # Run the agent - messages saved via add_message() are attributed to "assistant" + response = agent.run("What do you know about the user?") ``` """ diff --git a/examples/agno/python/src/honcho_agno/tools.py b/examples/agno/python/src/honcho_agno/tools.py index e7947461..60c07a86 100644 --- a/examples/agno/python/src/honcho_agno/tools.py +++ b/examples/agno/python/src/honcho_agno/tools.py @@ -3,11 +3,16 @@ Honcho Tools for Agno This module provides a Toolkit that allows Agno agents to interact with Honcho's memory system, including session context, semantic search, and dialectic API. + +Each HonchoTools instance represents ONE agent identity (peer). The toolkit +speaks as that peer when adding messages or querying the dialectic. For +multi-peer conversations, create separate toolkit instances or use Honcho +directly to manage other peers. """ import logging import uuid -from typing import Literal, Optional +from typing import Any, Literal from agno.tools import Toolkit from honcho import Honcho @@ -19,8 +24,14 @@ class HonchoTools(Toolkit): """ Honcho toolkit for Agno agents. - Provides memory tools for session context retrieval, semantic search, - message storage, and querying user knowledge via the Dialectic API. + Each toolkit instance represents ONE agent identity. The peer_id parameter + defines who this toolkit "speaks as" - all messages added through this + toolkit are attributed to that peer. + + For multi-peer conversations: + - Create one HonchoTools per agent, each with a different peer_id + - Share the same session_id across toolkits + - Use Honcho directly for peers not represented by an agent Example: ```python @@ -28,9 +39,11 @@ class HonchoTools(Toolkit): from agno.models.openai import OpenAIChat from honcho_agno import HonchoTools + # This toolkit IS the assistant - it speaks as "assistant" honcho_tools = HonchoTools( app_id="my-app", - user_id="user-123", + peer_id="assistant", + session_id="shared-session", ) agent = Agent( @@ -43,22 +56,25 @@ class HonchoTools(Toolkit): def __init__( self, app_id: str = "default", - user_id: str = "default", - session_id: Optional[str] = None, - api_key: Optional[str] = None, - base_url: Optional[str] = None, - environment: Optional[Literal["local", "production"]] = "production", - honcho_client: Optional[Honcho] = None, + peer_id: str = "assistant", + session_id: str | None = None, + api_key: str | None = None, + base_url: str | None = None, + environment: Literal["local", "production"] | None = "production", + honcho_client: Honcho | None = None, ) -> None: """ - Initialize the Honcho toolkit. + Initialize the Honcho toolkit for a specific agent identity. Args: app_id: Application/workspace ID for scoping operations. Maps to Honcho's workspace_id. - user_id: User identifier. Creates a peer with this ID. + peer_id: The identity this toolkit represents. All messages + added through this toolkit are attributed to this peer. + This is who the agent "is" in the conversation. session_id: Optional session ID. If not provided, a new UUID - will be generated. + will be generated. Share this across toolkits for multi-peer + conversations. api_key: Optional API key for Honcho. If not provided, will attempt to read from HONCHO_API_KEY environment variable. base_url: Optional base URL for the Honcho API. @@ -70,10 +86,11 @@ class HonchoTools(Toolkit): super().__init__(name="honcho") # Initialize Honcho client + self.honcho: Honcho if honcho_client is not None: self.honcho = honcho_client else: - client_kwargs: dict = {"workspace_id": app_id} + client_kwargs: dict[str, Any] = {"workspace_id": app_id} if api_key is not None: client_kwargs["api_key"] = api_key if base_url is not None: @@ -83,49 +100,47 @@ class HonchoTools(Toolkit): self.honcho = Honcho(**client_kwargs) # Store identifiers - self.app_id = app_id - self.user_id = user_id - self.session_id = session_id or str(uuid.uuid4()) + self.app_id: str = app_id + self.peer_id: str = peer_id + self.session_id: str = session_id or str(uuid.uuid4()) - # Create peers for user and assistant - self.user = self.honcho.peer(user_id) - self.assistant = self.honcho.peer("assistant") + # Create the peer this toolkit represents + # This is THE identity of this toolkit - one toolkit = one voice + self.peer: Peer = self.honcho.peer(peer_id) # Create or get session - self.session = self.honcho.session(self.session_id) + self.session: Session = self.honcho.session(self.session_id) # Register tools self.register(self.add_message) self.register(self.get_context) self.register(self.search_messages) - self.register(self.query_user) + self.register(self.query_peer) - def add_message(self, content: str, role: str = "user") -> str: + def add_message(self, content: str) -> str: """ - Store a message in the current session. + Store a message in the current session as this agent. - Use this tool to save important information from the conversation - that should be remembered for future interactions. + Use this tool to save your responses or important information + to the conversation history. The message is attributed to this + toolkit's peer identity. Args: content: The message content to store. - role: The role of the message sender. Use 'user' for user messages - or 'assistant' for AI responses. Returns: Confirmation message indicating the memory was saved. """ try: - peer = self.user if role == "user" else self.assistant - self.session.add_messages([peer.message(content)]) - return f"Message saved successfully to session {self.session_id}" + self.session.add_messages([self.peer.message(content)]) + return f"Message saved as '{self.peer_id}' to session {self.session_id}" except Exception as e: logger.exception("Error saving message") return f"Error saving message: {e!s}" def get_context( self, - tokens: Optional[int] = None, + tokens: int | None = None, include_summary: bool = True, ) -> str: """ @@ -148,7 +163,7 @@ class HonchoTools(Toolkit): tokens=tokens, ) - result = [] + result: list[str] = [] # Add summary if present if context.summary: @@ -216,23 +231,31 @@ class HonchoTools(Toolkit): logger.exception("Error searching messages") return f"Error searching messages: {e!s}" - def query_user(self, query: str) -> str: + def query_peer(self, query: str, target_peer_id: str | None = None) -> str: """ - Query the system's knowledge about the user. + Query the system's knowledge about a peer in the conversation. - Use this tool to ask questions about user preferences, interests, - or past interactions. The system uses dialectic reasoning to - provide insights based on the user's long-term representation. + Use this tool to ask questions about any participant's preferences, + interests, or past interactions. The system uses dialectic reasoning + to provide insights based on the peer's long-term representation. Args: - query: Natural language question about the user. + query: Natural language question about the peer. Examples: "What does the user like?", "What are their preferences?" + target_peer_id: Optional peer ID to query about. If not provided, + queries about this toolkit's own peer identity. Returns: - Response from the dialectic API with insights about the user. + Response from the dialectic API with insights about the peer. """ try: - response = self.user.chat( + # Query about a specific peer, or self if not specified + if target_peer_id: + target = self.honcho.peer(target_peer_id) + else: + target = self.peer + + response = target.chat( query=query, stream=False, session=self.session_id, @@ -241,8 +264,8 @@ class HonchoTools(Toolkit): return str(response) if response else "No relevant information found." except Exception as e: - logger.exception("Error querying user knowledge") - return f"Error querying user knowledge: {e!s}" + logger.exception("Error querying peer knowledge") + return f"Error querying peer knowledge: {e!s}" def reset_session(self) -> str: """ diff --git a/examples/agno/python/tests/test_tools.py b/examples/agno/python/tests/test_tools.py index f1cecff8..f761d394 100644 --- a/examples/agno/python/tests/test_tools.py +++ b/examples/agno/python/tests/test_tools.py @@ -3,12 +3,14 @@ Tests for Honcho Agno Tools Tests the Agno-Honcho tool integration layer using real Honcho SDK. Focuses on tool interface compliance and result formatting. + +Note: Each HonchoTools instance represents ONE agent identity (peer_id). +Messages added via add_message() are attributed to that peer. """ import uuid -import pytest - +from honcho import Honcho from honcho_agno import HonchoTools @@ -23,7 +25,7 @@ class TestHonchoToolsInitialization: assert tools.name == "honcho" assert tools.honcho is not None assert tools.session_id is not None - assert tools.user_id == "default" + assert tools.peer_id == "assistant" assert tools.app_id == "default" def test_custom_initialization(self): @@ -31,11 +33,11 @@ class TestHonchoToolsInitialization: custom_session = str(uuid.uuid4()) tools = HonchoTools( app_id="test-app", - user_id="test-user", + peer_id="custom-agent", session_id=custom_session, ) - assert tools.user_id == "test-user" + assert tools.peer_id == "custom-agent" assert tools.app_id == "test-app" assert tools.session_id == custom_session @@ -43,63 +45,119 @@ class TestHonchoToolsInitialization: """Test that session_id is auto-generated if not provided.""" tools = HonchoTools( app_id="test-app", - user_id="test-user", + peer_id="test-agent", ) assert tools.session_id is not None # Should be a valid UUID format uuid.UUID(tools.session_id) + def test_toolkit_represents_single_peer(self): + """Test that toolkit has exactly one peer identity.""" + tools = HonchoTools(peer_id="my-agent") + + # Should have exactly one peer + assert tools.peer is not None + assert tools.peer_id == "my-agent" + # Should NOT have separate user/assistant peers + assert not hasattr(tools, "user") + assert not hasattr(tools, "assistant") + class TestAddMessage: """Tests for add_message tool.""" - def test_add_user_message(self): - """Test adding a user message.""" + def test_add_message_as_peer(self): + """Test adding a message attributed to the toolkit's peer.""" tools = HonchoTools( app_id="test-app", - user_id=f"add-msg-user-{uuid.uuid4()}", - session_id=f"add-msg-session-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"session-{uuid.uuid4().hex[:8]}", ) - result = tools.add_message("Test message content", role="user") - - assert isinstance(result, str) - assert "saved" in result.lower() or "success" in result.lower() - - def test_add_assistant_message(self): - """Test adding an assistant message.""" - tools = HonchoTools( - app_id="test-app", - user_id=f"add-msg-user-{uuid.uuid4()}", - session_id=f"add-msg-session-{uuid.uuid4()}", - ) - - result = tools.add_message("Assistant response", role="assistant") + result = tools.add_message("Test message content") assert isinstance(result, str) assert "saved" in result.lower() or "success" in result.lower() + assert tools.peer_id in result # Should mention the peer def test_add_multiple_messages(self): """Test adding multiple messages in sequence.""" tools = HonchoTools( app_id="test-app", - user_id=f"multi-msg-user-{uuid.uuid4()}", - session_id=f"multi-msg-session-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"session-{uuid.uuid4().hex[:8]}", ) messages = [ - ("Hello, I need help", "user"), - ("Of course! How can I assist?", "assistant"), - ("I want to learn Python", "user"), + "First message from agent", + "Second message from agent", + "Third message from agent", ] - for content, role in messages: - result = tools.add_message(content, role=role) + for content in messages: + result = tools.add_message(content) assert isinstance(result, str) assert "error" not in result.lower() +class TestMultiPeerConversation: + """Tests for multi-peer conversation patterns.""" + + def test_multiple_toolkits_same_session(self): + """Test multiple toolkits (agents) sharing a session.""" + session_id = f"shared-session-{uuid.uuid4().hex[:8]}" + honcho = Honcho(workspace_id="test-app") + + # Two agents with different identities + agent1_tools = HonchoTools( + app_id="test-app", + peer_id="agent-alpha", + session_id=session_id, + honcho_client=honcho, + ) + + agent2_tools = HonchoTools( + app_id="test-app", + peer_id="agent-beta", + session_id=session_id, + honcho_client=honcho, + ) + + # Both add messages to the same session + result1 = agent1_tools.add_message("Message from Alpha") + result2 = agent2_tools.add_message("Message from Beta") + + assert "agent-alpha" in result1 + assert "agent-beta" in result2 + assert agent1_tools.session_id == agent2_tools.session_id + + def test_user_messages_via_honcho_directly(self): + """Test adding user messages via Honcho while agent uses toolkit.""" + session_id = f"mixed-session-{uuid.uuid4().hex[:8]}" + honcho = Honcho(workspace_id="test-app") + + # User messages added directly via Honcho + session = honcho.session(session_id) + user_peer = honcho.peer("user") + session.add_messages([user_peer.message("Hello from user")]) + + # Agent uses toolkit + agent_tools = HonchoTools( + app_id="test-app", + peer_id="assistant", + session_id=session_id, + honcho_client=honcho, + ) + + result = agent_tools.add_message("Hello from assistant") + assert "assistant" in result + + # Both should be in context + context = agent_tools.get_context() + assert isinstance(context, str) + + class TestGetContext: """Tests for get_context tool.""" @@ -107,8 +165,8 @@ class TestGetContext: """Test getting context from empty session.""" tools = HonchoTools( app_id="test-app", - user_id=f"context-user-{uuid.uuid4()}", - session_id=f"empty-context-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"empty-session-{uuid.uuid4().hex[:8]}", ) result = tools.get_context() @@ -119,13 +177,13 @@ class TestGetContext: """Test getting context after adding messages.""" tools = HonchoTools( app_id="test-app", - user_id=f"context-user-{uuid.uuid4()}", - session_id=f"context-session-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"context-session-{uuid.uuid4().hex[:8]}", ) # Add messages first - tools.add_message("I like pizza", role="user") - tools.add_message("Great choice!", role="assistant") + tools.add_message("I like pizza") + tools.add_message("Great choice!") result = tools.get_context() @@ -136,11 +194,11 @@ class TestGetContext: """Test getting context with token limit.""" tools = HonchoTools( app_id="test-app", - user_id=f"token-user-{uuid.uuid4()}", - session_id=f"token-session-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"token-session-{uuid.uuid4().hex[:8]}", ) - tools.add_message("This is a test message", role="user") + tools.add_message("This is a test message") result = tools.get_context(tokens=1000) @@ -150,11 +208,11 @@ class TestGetContext: """Test getting context without summary.""" tools = HonchoTools( app_id="test-app", - user_id=f"nosummary-user-{uuid.uuid4()}", - session_id=f"nosummary-session-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"nosummary-session-{uuid.uuid4().hex[:8]}", ) - tools.add_message("Test message", role="user") + tools.add_message("Test message") result = tools.get_context(include_summary=False) @@ -168,12 +226,12 @@ class TestSearchMessages: """Test that search returns formatted results.""" tools = HonchoTools( app_id="test-app", - user_id=f"search-user-{uuid.uuid4()}", - session_id=f"search-session-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"search-session-{uuid.uuid4().hex[:8]}", ) # Add searchable content - tools.add_message("I enjoy Python programming and data science", role="user") + tools.add_message("I enjoy Python programming and data science") result = tools.search_messages("programming", limit=5) @@ -184,13 +242,13 @@ class TestSearchMessages: """Test search with custom limit.""" tools = HonchoTools( app_id="test-app", - user_id=f"search-limit-user-{uuid.uuid4()}", - session_id=f"search-limit-session-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"search-limit-session-{uuid.uuid4().hex[:8]}", ) # Add multiple messages for i in range(5): - tools.add_message(f"Test message number {i} about coding", role="user") + tools.add_message(f"Test message number {i} about coding") result = tools.search_messages("coding", limit=3) @@ -200,8 +258,8 @@ class TestSearchMessages: """Test search with no matching results.""" tools = HonchoTools( app_id="test-app", - user_id=f"search-empty-user-{uuid.uuid4()}", - session_id=f"search-empty-session-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"search-empty-session-{uuid.uuid4().hex[:8]}", ) result = tools.search_messages("xyznonexistent123abcdef", limit=5) @@ -211,35 +269,60 @@ class TestSearchMessages: assert "No messages found" in result or "0 found" in result.lower() -class TestQueryUser: - """Tests for query_user tool.""" +class TestQueryPeer: + """Tests for query_peer tool.""" def test_query_returns_response(self): """Test that query returns a response string.""" tools = HonchoTools( app_id="test-app", - user_id=f"query-user-{uuid.uuid4()}", - session_id=f"query-session-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"query-session-{uuid.uuid4().hex[:8]}", ) # Add context first - tools.add_message("I love hiking and outdoor activities", role="user") - tools.add_message("I also enjoy photography", role="user") + tools.add_message("The user loves hiking and outdoor activities") + tools.add_message("They also enjoy photography") - result = tools.query_user("What does the user enjoy?") + result = tools.query_peer("What does this person enjoy?") assert isinstance(result, str) assert len(result) > 0 def test_query_without_context(self): - """Test query on user with minimal context.""" + """Test query with minimal context.""" tools = HonchoTools( app_id="test-app", - user_id=f"query-empty-user-{uuid.uuid4()}", - session_id=f"query-empty-session-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"query-empty-session-{uuid.uuid4().hex[:8]}", ) - result = tools.query_user("What are the user's preferences?") + result = tools.query_peer("What are the user's preferences?") + + assert isinstance(result, str) + + def test_query_specific_peer(self): + """Test querying about a specific peer by ID.""" + session_id = f"query-peer-session-{uuid.uuid4().hex[:8]}" + honcho = Honcho(workspace_id="test-app") + + # Add user messages directly + session = honcho.session(session_id) + user_peer = honcho.peer("user") + session.add_messages([ + user_peer.message("I love hiking"), + user_peer.message("Photography is my hobby"), + ]) + + # Agent queries about the user + tools = HonchoTools( + app_id="test-app", + peer_id="assistant", + session_id=session_id, + honcho_client=honcho, + ) + + result = tools.query_peer("What are their interests?", target_peer_id="user") assert isinstance(result, str) @@ -251,8 +334,8 @@ class TestResetSession: """Test that reset creates a new session.""" tools = HonchoTools( app_id="test-app", - user_id=f"reset-user-{uuid.uuid4()}", - session_id=f"original-session-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"original-session-{uuid.uuid4().hex[:8]}", ) original_session = tools.session_id @@ -271,14 +354,12 @@ class TestToolsIntegration: """Test using all tools in a realistic sequence.""" tools = HonchoTools( app_id="test-app", - user_id=f"integration-user-{uuid.uuid4()}", - session_id=f"integration-session-{uuid.uuid4()}", + peer_id=f"agent-{uuid.uuid4().hex[:8]}", + session_id=f"integration-session-{uuid.uuid4().hex[:8]}", ) - # Add messages - add_result = tools.add_message( - "I'm interested in AI and machine learning", role="user" - ) + # Add message + add_result = tools.add_message("I'm interested in AI and machine learning") assert isinstance(add_result, str) assert "error" not in add_result.lower() @@ -290,33 +371,42 @@ class TestToolsIntegration: search_result = tools.search_messages("AI", limit=10) assert isinstance(search_result, str) - # Query user - query_result = tools.query_user("What topics interest the user?") + # Query peer + query_result = tools.query_peer("What topics are mentioned?") assert isinstance(query_result, str) - def test_multiple_sessions_same_user(self): - """Test using multiple sessions for the same user.""" - user_id = f"multi-session-user-{uuid.uuid4()}" + def test_multi_agent_conversation(self): + """Test realistic multi-agent conversation.""" + session_id = f"multi-agent-{uuid.uuid4().hex[:8]}" + honcho = Honcho(workspace_id="test-app") - # First session - tools1 = HonchoTools( + # User peer managed directly + session = honcho.session(session_id) + user = honcho.peer("user") + + # Two agent toolkits + tech_agent = HonchoTools( app_id="test-app", - user_id=user_id, - session_id=f"session-1-{uuid.uuid4()}", + peer_id="tech-advisor", + session_id=session_id, + honcho_client=honcho, ) - tools1.add_message("I like Python", role="user") - # Second session - tools2 = HonchoTools( + biz_agent = HonchoTools( app_id="test-app", - user_id=user_id, - session_id=f"session-2-{uuid.uuid4()}", + peer_id="business-advisor", + session_id=session_id, + honcho_client=honcho, ) - tools2.add_message("I also like JavaScript", role="user") - # Both sessions should work independently - context1 = tools1.get_context() - context2 = tools2.get_context() + # Conversation flow + session.add_messages([user.message("I want to build a SaaS product")]) + tech_agent.add_message("Consider microservices architecture") + biz_agent.add_message("Focus on a niche market first") - assert isinstance(context1, str) - assert isinstance(context2, str) + # Both agents can see full context + tech_context = tech_agent.get_context() + biz_context = biz_agent.get_context() + + assert isinstance(tech_context, str) + assert isinstance(biz_context, str) diff --git a/examples/agno/python/uv.lock b/examples/agno/python/uv.lock index 8a6418b2..747e743c 100644 --- a/examples/agno/python/uv.lock +++ b/examples/agno/python/uv.lock @@ -49,6 +49,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] +[[package]] +name = "basedpyright" +version = "1.37.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodejs-wheel-binaries" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/b0/fbba81ea29eed1274e965cd0445f0d6020b467ff4d3393791e4d6ae02e64/basedpyright-1.37.1.tar.gz", hash = "sha256:1f47bc6f45cbcc5d6f8619d60aa42128e4b38942f5118dcd4bc20c3466c5e02f", size = 25235384, upload-time = "2026-01-08T14:42:46.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/d6/6b33bb49f08d761d7c958a1e3cecfb3ffbdcf4ba6bbed65b23ab47516b75/basedpyright-1.37.1-py3-none-any.whl", hash = "sha256:caf3adfe54f51623241712f8b4367adb51ef8a8c2288e3e1ec4118319661340d", size = 12297397, upload-time = "2026-01-08T14:42:50.306Z" }, +] + [[package]] name = "certifi" version = "2026.1.4" @@ -166,6 +178,12 @@ dependencies = [ { name = "python-dotenv" }, ] +[package.dev-dependencies] +dev = [ + { name = "basedpyright" }, + { name = "pytest" }, +] + [package.metadata] requires-dist = [ { name = "agno", specifier = ">=1.4.0" }, @@ -174,6 +192,12 @@ requires-dist = [ { name = "python-dotenv", specifier = ">=1.0.0" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "basedpyright", specifier = ">=1.29.4" }, + { name = "pytest", specifier = ">=8.2.2" }, +] + [[package]] name = "honcho-ai" version = "1.6.0" @@ -190,7 +214,7 @@ wheels = [ [[package]] name = "honcho-core" -version = "1.9.0" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -200,9 +224,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/be/4496fa3deb447e958d06490e6a18ae173c8ddb427a86a60a2bcaa9bba649/honcho_core-1.9.0.tar.gz", hash = "sha256:baaa61be3826e9fd3489037a5606c6d87e6ff6ac0c2d17139ed2153691f9bae8", size = 144184, upload-time = "2026-01-12T22:11:27.159Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/ea/c0949bbac5a9f20625bdb152b7da2350e89ffc15b5862cd6094b464cde14/honcho_core-1.8.0.tar.gz", hash = "sha256:ffe0840639651640722ad0ed38d193cc9402b077dac3e6726ac7be551398d952", size = 142469, upload-time = "2025-12-15T19:27:59.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/64/99a96c61e15e6aaa696ea67a752bdcda0472712de2d5f5a0f827f0de22d9/honcho_core-1.9.0-py3-none-any.whl", hash = "sha256:b80f1215b5f9f5e134421b7243865eea6620e66fbe39629e3d59cc688031cf90", size = 138655, upload-time = "2026-01-12T22:11:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/5aba73353c7e70d331a21e01a931c951c5bd8688fb25e5fc318517f2adf9/honcho_core-1.8.0-py3-none-any.whl", hash = "sha256:30a44b7d421328dfac015e8a6ecbe09c89b6cac9f3a913262244e7d15698a8a8", size = 140580, upload-time = "2025-12-15T19:27:58.562Z" }, ] [[package]] @@ -265,6 +289,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jiter" version = "0.12.0" @@ -383,6 +416,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "nodejs-wheel-binaries" +version = "24.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/f1/73182280e2c05f49a7c2c8dbd46144efe3f74f03f798fb90da67b4a93bbf/nodejs_wheel_binaries-24.13.0.tar.gz", hash = "sha256:766aed076e900061b83d3e76ad48bfec32a035ef0d41bd09c55e832eb93ef7a4", size = 8056, upload-time = "2026-01-14T11:05:33.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/dc/4d7548aa74a5b446d093f03aff4fb236b570959d793f21c9c42ab6ad870a/nodejs_wheel_binaries-24.13.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:356654baa37bfd894e447e7e00268db403ea1d223863963459a0fbcaaa1d9d48", size = 55133268, upload-time = "2026-01-14T11:05:05.335Z" }, + { url = "https://files.pythonhosted.org/packages/24/8a/8a4454d28339487240dd2232f42f1090e4a58544c581792d427f6239798c/nodejs_wheel_binaries-24.13.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:92fdef7376120e575f8b397789bafcb13bbd22a1b4d21b060d200b14910f22a5", size = 55314800, upload-time = "2026-01-14T11:05:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/fb/46c600fcc748bd13bc536a735f11532a003b14f5c4dfd6865f5911672175/nodejs_wheel_binaries-24.13.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3f619ac140e039ecd25f2f71d6e83ad1414017a24608531851b7c31dc140cdfd", size = 59666320, upload-time = "2026-01-14T11:05:12.369Z" }, + { url = "https://files.pythonhosted.org/packages/85/47/d48f11fc5d1541ace5d806c62a45738a1db9ce33e85a06fe4cd3d9ce83f6/nodejs_wheel_binaries-24.13.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:dfb31ebc2c129538192ddb5bedd3d63d6de5d271437cd39ea26bf3fe229ba430", size = 60162447, upload-time = "2026-01-14T11:05:16.003Z" }, + { url = "https://files.pythonhosted.org/packages/b1/74/d285c579ae8157c925b577dde429543963b845e69cd006549e062d1cf5b6/nodejs_wheel_binaries-24.13.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fdd720d7b378d5bb9b2710457bbc880d4c4d1270a94f13fbe257198ac707f358", size = 61659994, upload-time = "2026-01-14T11:05:19.68Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/88b4254a2ff93ed2eaed725f77b7d3d2d8d7973bf134359ce786db894faf/nodejs_wheel_binaries-24.13.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ad6383613f3485a75b054647a09f1cd56d12380d7459184eebcf4a5d403f35c", size = 62244373, upload-time = "2026-01-14T11:05:23.987Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c3/0e13a3da78f08cb58650971a6957ac7bfef84164b405176e53ab1e3584e2/nodejs_wheel_binaries-24.13.0-py2.py3-none-win_amd64.whl", hash = "sha256:605be4763e3ef427a3385a55da5a1bcf0a659aa2716eebbf23f332926d7e5f23", size = 41345528, upload-time = "2026-01-14T11:05:27.67Z" }, + { url = "https://files.pythonhosted.org/packages/a3/f1/0578d65b4e3dc572967fd702221ea1f42e1e60accfb6b0dd8d8f15410139/nodejs_wheel_binaries-24.13.0-py2.py3-none-win_arm64.whl", hash = "sha256:2e3431d869d6b2dbeef1d469ad0090babbdcc8baaa72c01dd3cc2c6121c96af5", size = 39054688, upload-time = "2026-01-14T11:05:30.739Z" }, +] + [[package]] name = "openai" version = "2.15.0" @@ -411,6 +460,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -567,6 +625,24 @@ 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 = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.1" @@ -689,6 +765,60 @@ 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 = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + [[package]] name = "tqdm" version = "4.67.1"