fix: formatting toolkits to be more structural and come specifically from the perspective of the agent using it.

This commit is contained in:
ajspig 2026-01-15 10:19:04 -05:00
parent 4886c30f25
commit 532675300f
8 changed files with 669 additions and 167 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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"]

View File

@ -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?")
```
"""

View File

@ -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:
"""

View File

@ -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)

View File

@ -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"