From dff432f41957c20bb5eb4bcdd32d675287aed119 Mon Sep 17 00:00:00 2001 From: ajspig Date: Thu, 15 Jan 2026 13:31:32 -0500 Subject: [PATCH] fix: making things consistent --- examples/agno/python/README.md | 125 ++++++--- .../agno/python/src/honcho_agno/__init__.py | 26 +- .../agno/python/src/honcho_agno/exceptions.py | 26 -- examples/agno/python/src/honcho_agno/tools.py | 26 +- examples/agno/python/tests/test_tools.py | 245 ++++++++---------- 5 files changed, 208 insertions(+), 240 deletions(-) delete mode 100644 examples/agno/python/src/honcho_agno/exceptions.py diff --git a/examples/agno/python/README.md b/examples/agno/python/README.md index 43e13100..15286a84 100644 --- a/examples/agno/python/README.md +++ b/examples/agno/python/README.md @@ -19,15 +19,23 @@ uv add honcho-agno ```python from agno.agent import Agent from agno.models.openai import OpenAIChat +from honcho import Honcho from honcho_agno import HonchoTools -# Create Honcho tools with user context +# Initialize Honcho client +honcho = Honcho(workspace_id="my-app") + +# Create Honcho tools for the agent honcho_tools = HonchoTools( - app_id="my-app", - user_id="user-123", + peer_id="assistant", + session_id="session-123", + honcho_client=honcho, ) -# Create an agent with memory +# Create user peer for orchestration +user_peer = honcho.peer("user") + +# Create an agent with memory tools agent = Agent( name="Memory Agent", model=OpenAIChat(id="gpt-4o"), @@ -35,49 +43,63 @@ agent = Agent( description="An assistant with persistent memory powered by Honcho.", ) -# The agent can now use memory tools -response = agent.run("Remember that I prefer Python over JavaScript") +# Add user message via orchestration +honcho_tools.session.add_messages([user_peer.message("I prefer Python over JavaScript")]) + +# Run the agent +response = agent.run("What programming language does the user prefer?") + +# Save assistant response via orchestration +honcho_tools.session.add_messages([honcho_tools.peer.message(str(response.content))]) ``` ## Features -The `HonchoTools` toolkit provides four memory tools: +The `HonchoTools` toolkit provides three memory tools: | Tool | Description | |------|-------------| -| `add_message` | Store messages in the session for future recall | | `get_context` | Retrieve conversation context within token limits | | `search_messages` | Semantic search through past messages | -| `query_user` | Query the dialectic API for user insights | +| `chat` | Query Honcho for synthesized insights about the conversation | ## Configuration ### Basic Configuration ```python +from honcho import Honcho from honcho_agno import HonchoTools +# Create shared Honcho client +honcho = Honcho(workspace_id="my-app") + +# Create toolkit for an agent tools = HonchoTools( - app_id="my-app", # Workspace/application identifier - user_id="user-123", # User identifier - session_id="session-456", # Optional: specific session ID + peer_id="assistant", # Identity for this agent + session_id="session-456", # Optional: specific session ID + honcho_client=honcho, # Shared Honcho client ) ``` -### With API Key +### Without Pre-configured Client ```python +from honcho_agno import HonchoTools + +# Creates its own Honcho client internally tools = HonchoTools( - app_id="my-app", - user_id="user-123", - api_key="your-api-key", - base_url="https://api.honcho.dev", # Optional: custom endpoint + app_id="my-app", # Workspace ID (used to create internal client) + peer_id="assistant", # Identity for this agent + session_id="session-456", # Optional: auto-generated if not provided ) ``` +Note: When `honcho_client` is provided, `app_id` is ignored since the client already has its workspace configured. + ### Environment Variables -Copy `.env.template` to `.env` and configure: +Configure via `.env` file in the root honcho directory: **Honcho Settings:** @@ -92,26 +114,14 @@ Copy `.env.template` to `.env` and configure: ## Tool Details -### add_message - -Store a message in the current session. - -```python -# The agent can call this tool to save information -result = honcho_tools.add_message( - content="User prefers dark mode", - role="user" # or "assistant" -) -``` - ### get_context Retrieve recent conversation context. ```python context = honcho_tools.get_context( - tokens=2000, # Max tokens to include - include_summary=True, # Include session summary + tokens=2000, # Max tokens to include (optional) + include_summary=True, # Include session summary (default: True) ) ``` @@ -122,26 +132,69 @@ Search through past messages semantically. ```python results = honcho_tools.search_messages( query="programming preferences", - limit=10, + limit=10, # Max results (default: 10) ) ``` -### query_user +### chat -Ask questions about the user via the dialectic API. +Ask questions about the conversation using Honcho's reasoning. ```python -insights = honcho_tools.query_user( +insights = honcho_tools.chat( query="What programming languages does the user prefer?" ) ``` +## Multi-Peer Conversations + +For multi-agent systems, create separate `HonchoTools` instances for each agent, sharing the same session: + +```python +from honcho import Honcho +from honcho_agno import HonchoTools + +# Shared Honcho client and session +honcho = Honcho(workspace_id="advisory-app") +session_id = "shared-session-123" + +# Tech advisor agent +tech_tools = HonchoTools( + peer_id="tech-advisor", + session_id=session_id, + honcho_client=honcho, +) + +# Business advisor agent +biz_tools = HonchoTools( + peer_id="biz-advisor", + session_id=session_id, + honcho_client=honcho, +) + +# User peer for orchestration +user = honcho.peer("user") + +# Add messages via orchestration (not toolkit methods) +tech_tools.session.add_messages([user.message("How should I scale my startup?")]) +tech_tools.session.add_messages([tech_tools.peer.message("Consider microservices...")]) +biz_tools.session.add_messages([biz_tools.peer.message("Focus on unit economics...")]) +``` + +## Architecture Notes + +- **Read-only toolkit**: `HonchoTools` provides read access to Honcho (context, search, chat) +- **Orchestration pattern**: Message saving is handled by your orchestration code, not the toolkit +- **One peer per toolkit**: Each `HonchoTools` instance represents one agent identity +- **Shared sessions**: Multiple toolkits can share a session for multi-agent conversations + ## Examples See the [examples](./examples) directory for complete working examples: - `simple_example.py`: Basic usage with HonchoTools - `multi_tool_example.py`: Using all tools together +- `multi_peer_example.py`: Multi-agent conversation with different perspectives ## Development diff --git a/examples/agno/python/src/honcho_agno/__init__.py b/examples/agno/python/src/honcho_agno/__init__.py index d4a8b3b0..60e397ab 100644 --- a/examples/agno/python/src/honcho_agno/__init__.py +++ b/examples/agno/python/src/honcho_agno/__init__.py @@ -5,19 +5,24 @@ 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. +provides read access to Honcho for querying conversation context. +Orchestration code handles saving messages to avoid duplicates. Example: ```python from agno.agent import Agent from agno.models.openai import OpenAIChat + from honcho import Honcho from honcho_agno import HonchoTools - # Create Honcho tools + # Shared Honcho client + honcho = Honcho(workspace_id="my-app") + + # Create Honcho tools for the assistant honcho_tools = HonchoTools( - app_id="my-app", - peer_id="assistant", # The identity for the agent using this toolkit + peer_id="assistant", session_id="session-123", + honcho_client=honcho, ) # Create agent with memory @@ -28,22 +33,17 @@ Example: description="An assistant with persistent memory powered by Honcho.", ) - # Run the agent - messages saved via add_message() are attributed to "assistant" + # Run the agent response = agent.run("What do you know about the user?") + + # Save messages via orchestration (not the toolkit) + honcho_tools.session.add_messages([honcho_tools.peer.message(str(response.content))]) ``` """ -from honcho_agno.exceptions import ( - HonchoDependencyError, - HonchoSessionError, - HonchoToolError, -) from honcho_agno.tools import HonchoTools __version__ = "0.1.0" __all__ = [ "HonchoTools", - "HonchoDependencyError", - "HonchoSessionError", - "HonchoToolError", ] diff --git a/examples/agno/python/src/honcho_agno/exceptions.py b/examples/agno/python/src/honcho_agno/exceptions.py deleted file mode 100644 index 0632b5cf..00000000 --- a/examples/agno/python/src/honcho_agno/exceptions.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Exception classes for Honcho Agno integration. -""" - - -class HonchoDependencyError(ImportError): - """Raised when required Agno dependencies are not installed.""" - - def __init__(self, framework: str, install_command: str) -> None: - self.framework = framework - self.install_command = install_command - super().__init__( - f"{framework} dependencies not found. Install with: {install_command}" - ) - - -class HonchoSessionError(Exception): - """Raised when there is an error with Honcho session operations.""" - - pass - - -class HonchoToolError(Exception): - """Raised when a Honcho tool operation fails.""" - - pass diff --git a/examples/agno/python/src/honcho_agno/tools.py b/examples/agno/python/src/honcho_agno/tools.py index 5224e5cc..4acd0bb2 100644 --- a/examples/agno/python/src/honcho_agno/tools.py +++ b/examples/agno/python/src/honcho_agno/tools.py @@ -64,15 +64,15 @@ class HonchoTools(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. + app_id: Workspace ID for creating an internal Honcho client. + Ignored if honcho_client is provided. peer_id: The identity this toolkit represents. This is who the agent "is" when querying peer knowledge. session_id: Optional session ID. If not provided, a new UUID will be generated. Share this across toolkits for multi-peer conversations. honcho_client: Optional pre-configured Honcho client instance. - If provided, app_id is ignored. + When provided, uses this client directly (app_id is ignored). """ super().__init__(name="honcho") @@ -82,8 +82,6 @@ class HonchoTools(Toolkit): else: self.honcho = Honcho(workspace_id=app_id) - # Store identifiers - self.app_id: str = app_id self.peer_id: str = peer_id self.session_id: str = session_id or str(uuid.uuid4()) @@ -188,21 +186,3 @@ class HonchoTools(Toolkit): except Exception as e: logger.exception("Error querying conversation") return f"Error querying conversation: {e!s}" - - def reset_session(self) -> str: - """ - Create a new session, clearing the conversation history. - - Use this tool to start a fresh conversation while maintaining - the user's long-term memory and representation. - - Returns: - Confirmation with the new session ID. - """ - try: - self.session_id = str(uuid.uuid4()) - self.session = self.honcho.session(self.session_id) - return f"Session reset. New session ID: {self.session_id}" - except Exception as e: - logger.exception("Error resetting session") - return f"Error resetting session: {e!s}" diff --git a/examples/agno/python/tests/test_tools.py b/examples/agno/python/tests/test_tools.py index f761d394..ba262ec8 100644 --- a/examples/agno/python/tests/test_tools.py +++ b/examples/agno/python/tests/test_tools.py @@ -5,7 +5,7 @@ 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. +The toolkit provides read access; orchestration code handles message saving. """ import uuid @@ -26,7 +26,6 @@ class TestHonchoToolsInitialization: assert tools.honcho is not None assert tools.session_id is not None assert tools.peer_id == "assistant" - assert tools.app_id == "default" def test_custom_initialization(self): """Test initialization with custom parameters.""" @@ -38,7 +37,6 @@ class TestHonchoToolsInitialization: ) assert tools.peer_id == "custom-agent" - assert tools.app_id == "test-app" assert tools.session_id == custom_session def test_session_auto_generation(self): @@ -63,42 +61,17 @@ class TestHonchoToolsInitialization: assert not hasattr(tools, "user") assert not hasattr(tools, "assistant") - -class TestAddMessage: - """Tests for add_message tool.""" - - def test_add_message_as_peer(self): - """Test adding a message attributed to the toolkit's peer.""" + def test_honcho_client_used_directly(self): + """Test that honcho_client is used when provided.""" + honcho = Honcho(workspace_id="client-workspace") tools = HonchoTools( - app_id="test-app", - peer_id=f"agent-{uuid.uuid4().hex[:8]}", - session_id=f"session-{uuid.uuid4().hex[:8]}", + app_id="ignored-app", # Ignored when honcho_client provided + peer_id="test-agent", + honcho_client=honcho, ) - 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", - peer_id=f"agent-{uuid.uuid4().hex[:8]}", - session_id=f"session-{uuid.uuid4().hex[:8]}", - ) - - messages = [ - "First message from agent", - "Second message from agent", - "Third message from agent", - ] - - for content in messages: - result = tools.add_message(content) - assert isinstance(result, str) - assert "error" not in result.lower() + # The client should be the one we passed + assert tools.honcho is honcho class TestMultiPeerConversation: @@ -124,37 +97,33 @@ class TestMultiPeerConversation: 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 + # Both should share the same session assert agent1_tools.session_id == agent2_tools.session_id + # But have different peer identities + assert agent1_tools.peer_id != agent2_tools.peer_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]}" + def test_messages_added_via_orchestration(self): + """Test that messages are added via session, not toolkit methods.""" + session_id = f"orch-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( + # Create toolkit + 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 + # User messages added directly via Honcho (orchestration pattern) + user_peer = honcho.peer("user") + tools.session.add_messages([user_peer.message("Hello from user")]) - # Both should be in context - context = agent_tools.get_context() + # Agent messages also added via orchestration + tools.session.add_messages([tools.peer.message("Hello from assistant")]) + + # Context should show both messages + context = tools.get_context() assert isinstance(context, str) @@ -175,15 +144,22 @@ class TestGetContext: def test_get_context_with_messages(self): """Test getting context after adding messages.""" + session_id = f"context-session-{uuid.uuid4().hex[:8]}" + honcho = Honcho(workspace_id="test-app") + tools = HonchoTools( app_id="test-app", - peer_id=f"agent-{uuid.uuid4().hex[:8]}", - session_id=f"context-session-{uuid.uuid4().hex[:8]}", + peer_id="assistant", + session_id=session_id, + honcho_client=honcho, ) - # Add messages first - tools.add_message("I like pizza") - tools.add_message("Great choice!") + # Add messages via orchestration + user = honcho.peer("user") + tools.session.add_messages([ + user.message("I like pizza"), + tools.peer.message("Great choice!"), + ]) result = tools.get_context() @@ -192,13 +168,17 @@ class TestGetContext: def test_get_context_with_token_limit(self): """Test getting context with token limit.""" + session_id = f"token-session-{uuid.uuid4().hex[:8]}" + honcho = Honcho(workspace_id="test-app") + tools = HonchoTools( app_id="test-app", - peer_id=f"agent-{uuid.uuid4().hex[:8]}", - session_id=f"token-session-{uuid.uuid4().hex[:8]}", + peer_id="assistant", + session_id=session_id, + honcho_client=honcho, ) - tools.add_message("This is a test message") + tools.session.add_messages([tools.peer.message("This is a test message")]) result = tools.get_context(tokens=1000) @@ -206,13 +186,17 @@ class TestGetContext: def test_get_context_without_summary(self): """Test getting context without summary.""" + session_id = f"nosummary-session-{uuid.uuid4().hex[:8]}" + honcho = Honcho(workspace_id="test-app") + tools = HonchoTools( app_id="test-app", - peer_id=f"agent-{uuid.uuid4().hex[:8]}", - session_id=f"nosummary-session-{uuid.uuid4().hex[:8]}", + peer_id="assistant", + session_id=session_id, + honcho_client=honcho, ) - tools.add_message("Test message") + tools.session.add_messages([tools.peer.message("Test message")]) result = tools.get_context(include_summary=False) @@ -224,14 +208,20 @@ class TestSearchMessages: def test_search_returns_formatted_string(self): """Test that search returns formatted results.""" + session_id = f"search-session-{uuid.uuid4().hex[:8]}" + honcho = Honcho(workspace_id="test-app") + tools = HonchoTools( app_id="test-app", - peer_id=f"agent-{uuid.uuid4().hex[:8]}", - session_id=f"search-session-{uuid.uuid4().hex[:8]}", + peer_id="assistant", + session_id=session_id, + honcho_client=honcho, ) # Add searchable content - tools.add_message("I enjoy Python programming and data science") + tools.session.add_messages([ + tools.peer.message("I enjoy Python programming and data science") + ]) result = tools.search_messages("programming", limit=5) @@ -240,15 +230,19 @@ class TestSearchMessages: def test_search_with_limit(self): """Test search with custom limit.""" + session_id = f"search-limit-session-{uuid.uuid4().hex[:8]}" + honcho = Honcho(workspace_id="test-app") + tools = HonchoTools( app_id="test-app", - peer_id=f"agent-{uuid.uuid4().hex[:8]}", - session_id=f"search-limit-session-{uuid.uuid4().hex[:8]}", + peer_id="assistant", + session_id=session_id, + honcho_client=honcho, ) # Add multiple messages - for i in range(5): - tools.add_message(f"Test message number {i} about coding") + messages = [tools.peer.message(f"Test message number {i} about coding") for i in range(5)] + tools.session.add_messages(messages) result = tools.search_messages("coding", limit=3) @@ -269,52 +263,14 @@ class TestSearchMessages: assert "No messages found" in result or "0 found" in result.lower() -class TestQueryPeer: - """Tests for query_peer tool.""" +class TestChat: + """Tests for chat tool.""" - def test_query_returns_response(self): - """Test that query returns a response string.""" - tools = HonchoTools( - app_id="test-app", - peer_id=f"agent-{uuid.uuid4().hex[:8]}", - session_id=f"query-session-{uuid.uuid4().hex[:8]}", - ) - - # Add context first - tools.add_message("The user loves hiking and outdoor activities") - tools.add_message("They also enjoy photography") - - 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 with minimal context.""" - tools = HonchoTools( - app_id="test-app", - peer_id=f"agent-{uuid.uuid4().hex[:8]}", - session_id=f"query-empty-session-{uuid.uuid4().hex[:8]}", - ) - - 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]}" + def test_chat_returns_response(self): + """Test that chat returns a response string.""" + session_id = f"chat-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", @@ -322,29 +278,29 @@ class TestQueryPeer: honcho_client=honcho, ) - result = tools.query_peer("What are their interests?", target_peer_id="user") + # Add context first + user = honcho.peer("user") + tools.session.add_messages([ + user.message("The user loves hiking and outdoor activities"), + user.message("They also enjoy photography"), + ]) + + result = tools.chat("What does this person enjoy?") assert isinstance(result, str) + assert len(result) > 0 - -class TestResetSession: - """Tests for reset_session tool.""" - - def test_reset_creates_new_session(self): - """Test that reset creates a new session.""" + def test_chat_without_context(self): + """Test chat with minimal context.""" tools = HonchoTools( app_id="test-app", peer_id=f"agent-{uuid.uuid4().hex[:8]}", - session_id=f"original-session-{uuid.uuid4().hex[:8]}", + session_id=f"chat-empty-session-{uuid.uuid4().hex[:8]}", ) - original_session = tools.session_id - - result = tools.reset_session() + result = tools.chat("What are the user's preferences?") assert isinstance(result, str) - assert tools.session_id != original_session - assert "reset" in result.lower() or "new" in result.lower() class TestToolsIntegration: @@ -352,16 +308,21 @@ class TestToolsIntegration: def test_all_tools_in_sequence(self): """Test using all tools in a realistic sequence.""" + session_id = f"integration-session-{uuid.uuid4().hex[:8]}" + honcho = Honcho(workspace_id="test-app") + tools = HonchoTools( app_id="test-app", - peer_id=f"agent-{uuid.uuid4().hex[:8]}", - session_id=f"integration-session-{uuid.uuid4().hex[:8]}", + peer_id="assistant", + session_id=session_id, + honcho_client=honcho, ) - # 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() + # Add message via orchestration + user = honcho.peer("user") + tools.session.add_messages([ + user.message("I'm interested in AI and machine learning") + ]) # Get context context_result = tools.get_context() @@ -371,9 +332,9 @@ class TestToolsIntegration: search_result = tools.search_messages("AI", limit=10) assert isinstance(search_result, str) - # Query peer - query_result = tools.query_peer("What topics are mentioned?") - assert isinstance(query_result, str) + # Chat + chat_result = tools.chat("What topics are mentioned?") + assert isinstance(chat_result, str) def test_multi_agent_conversation(self): """Test realistic multi-agent conversation.""" @@ -399,10 +360,10 @@ class TestToolsIntegration: honcho_client=honcho, ) - # Conversation flow + # Conversation flow via orchestration 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") + session.add_messages([tech_agent.peer.message("Consider microservices architecture")]) + session.add_messages([biz_agent.peer.message("Focus on a niche market first")]) # Both agents can see full context tech_context = tech_agent.get_context()