From 55ba72f67e9770861637d5de14b7f7f8ffac3ad6 Mon Sep 17 00:00:00 2001 From: Juan Franco <91078895+m1lestones@users.noreply.github.com> Date: Fri, 10 Apr 2026 01:36:34 -0400 Subject: [PATCH 1/2] feat: add Pydantic AI memory integration example --- examples/pydantic-ai/README.md | 113 +++++++++++++ examples/pydantic-ai/python/main.py | 152 ++++++++++++++++++ examples/pydantic-ai/python/pyproject.toml | 14 ++ examples/pydantic-ai/python/tools/__init__.py | 0 examples/pydantic-ai/python/tools/client.py | 50 ++++++ .../pydantic-ai/python/tools/get_context.py | 30 ++++ .../pydantic-ai/python/tools/save_memory.py | 38 +++++ 7 files changed, 397 insertions(+) create mode 100644 examples/pydantic-ai/README.md create mode 100644 examples/pydantic-ai/python/main.py create mode 100644 examples/pydantic-ai/python/pyproject.toml create mode 100644 examples/pydantic-ai/python/tools/__init__.py create mode 100644 examples/pydantic-ai/python/tools/client.py create mode 100644 examples/pydantic-ai/python/tools/get_context.py create mode 100644 examples/pydantic-ai/python/tools/save_memory.py diff --git a/examples/pydantic-ai/README.md b/examples/pydantic-ai/README.md new file mode 100644 index 000000000..e83750794 --- /dev/null +++ b/examples/pydantic-ai/README.md @@ -0,0 +1,113 @@ +# Honcho Memory Integration for Pydantic AI + +Give your [Pydantic AI](https://ai.pydantic.dev) agents persistent memory using [Honcho](https://honcho.dev). + +## Features + +- **Persistent Memory**: Every conversation turn is saved to Honcho and automatically injected into the agent's system prompt on the next turn. +- **Natural Language Recall**: The agent can query Honcho's Dialectic API to answer questions like "What are my hobbies?" or "What did we talk about last time?" +- **Context Injection**: Conversation history is retrieved from Honcho and appended to the system prompt via `@agent.system_prompt`. +- **In-Session Coherence**: Pydantic AI's `message_history` parameter keeps the agent coherent within a single session, complementing Honcho's cross-session memory. + +## Structure + +``` +pydantic-ai/ +├── README.md +└── python/ + ├── main.py + ├── pyproject.toml + └── tools/ + ├── client.py + ├── save_memory.py + └── get_context.py +``` + +## Environment Variables + +Create a `.env` file in the `python/` directory: + +```env +HONCHO_API_KEY=your-honcho-api-key +HONCHO_WORKSPACE_ID=default +OPENAI_API_KEY=your-openai-api-key +``` + +Get your Honcho API key at [honcho.dev](https://honcho.dev). + +## Installation + +```bash +pip install pydantic-ai honcho-ai python-dotenv +``` + +Or with uv: + +```bash +uv add pydantic-ai honcho-ai python-dotenv +``` + +## Quick Start + +```python +import asyncio +from main import chat + +async def main(): + message_history = [] + # First turn + response, message_history = await chat("alice", "I love hiking in the mountains", "session-1", message_history) + print(response) + # Second turn — history is threaded automatically + response, message_history = await chat("alice", "What do you remember about me?", "session-1", message_history) + print(response) + +asyncio.run(main()) +``` + +## Run the Demo + +```bash +cd python +python main.py +``` + +## How It Works + +### 1. Dynamic System Prompt + +The `@agent.system_prompt` decorator registers `honcho_system_prompt()`, which is called by Pydantic AI before every LLM request. It fetches recent messages from Honcho and appends them to the system prompt: + +``` +You are a helpful assistant with persistent memory powered by Honcho. + +## Conversation History +User: I love hiking +Assistant: That sounds wonderful! Do you have a favorite trail? +``` + +### 2. Memory Tool + +The `@agent.tool` decorator registers `query_memory()`, which calls Honcho's Dialectic API. When the user asks "What do you remember about me?", the agent invokes this tool to query the semantic memory layer. + +### 3. Message History Threading + +`chat()` returns `(response, result.all_messages())`. Pass the returned history back on the next call to maintain in-session coherence. Honcho provides cross-session memory; `message_history` provides within-session context. + +### 4. Auto-Save + +The `chat()` function saves the user message before the agent runs and the assistant response after, keeping Honcho in sync with every turn. + +## Concept Mapping + +| Pydantic AI | Honcho | +|---|---| +| `deps.ctx.user_id` | Peer (human) | +| `deps.ctx.assistant_id` | Peer (agent) | +| `deps.ctx.session_id` | Session | +| `message_history` | In-session context | +| Agent input | Message | + +## License + +AGPL-3.0-or-later diff --git a/examples/pydantic-ai/python/main.py b/examples/pydantic-ai/python/main.py new file mode 100644 index 000000000..8add12f1d --- /dev/null +++ b/examples/pydantic-ai/python/main.py @@ -0,0 +1,152 @@ +"""Pydantic AI + Honcho persistent memory integration. + +Demonstrates a conversational agent that remembers users across sessions. +Honcho stores every message and builds a long-term representation of the user; +the agent injects that context into its system prompt on every turn and can +query memory on demand via the ``query_memory`` tool. + +Usage: + python main.py + +Environment variables: + HONCHO_API_KEY Required. Your Honcho API key from honcho.dev. + HONCHO_WORKSPACE_ID Optional. Workspace ID (default: "default"). + OPENAI_API_KEY Required. Your OpenAI API key. +""" + +from __future__ import annotations + +import asyncio +from dataclasses import dataclass + +from pydantic_ai import Agent, RunContext +from pydantic_ai.messages import ModelMessage + +from tools.client import HonchoContext, get_client +from tools.get_context import get_context +from tools.save_memory import save_memory + + +@dataclass +class HonchoAgentDeps: + """Dependencies injected into every Pydantic AI agent call. + + Attributes: + ctx: Honcho identity for the current conversation turn. + """ + + ctx: HonchoContext + + +honcho_agent: Agent[HonchoAgentDeps, str] = Agent( + "openai:gpt-4.1-mini", + deps_type=HonchoAgentDeps, + result_type=str, + system_prompt=( + "You are a helpful assistant with persistent memory powered by Honcho. " + "You remember users across conversations. " + "When a user asks what you remember about them, use the query_memory tool." + ), +) + + +@honcho_agent.system_prompt +def honcho_system_prompt(run_ctx: RunContext[HonchoAgentDeps]) -> str: + """Append Honcho conversation history to the system prompt. + + Called by Pydantic AI before every LLM request. Returns an additional + system-prompt segment containing the recent session history fetched from + Honcho. Returns an empty string when the session has no history yet. + + Args: + run_ctx: The run context exposing ``HonchoAgentDeps``. + + Returns: + A formatted history string, or ``""`` if no history exists. + """ + history = get_context(run_ctx.deps.ctx, tokens=2000) + if not history: + return "" + formatted = "\n".join(f"{m['role'].title()}: {m['content']}" for m in history) + return f"\n\n## Conversation History\n{formatted}" + + +@honcho_agent.tool +def query_memory(run_ctx: RunContext[HonchoAgentDeps], query: str) -> str: + """Query Honcho's Dialectic API to recall facts about the current user. + + Use this when the user asks what you remember about them or their past + conversations. + + Args: + run_ctx: The run context exposing ``HonchoAgentDeps``. + query: Natural language question about the user. + + Returns: + A natural language answer from Honcho's memory. + """ + ctx = run_ctx.deps.ctx + honcho = get_client() + peer = honcho.peer(ctx.user_id) + response = peer.chat(query=query) + return str(response) if response else "No relevant information found in memory." + + +async def chat( + user_id: str, + message: str, + session_id: str, + message_history: list[ModelMessage] | None = None, +) -> tuple[str, list[ModelMessage]]: + """Run one conversation turn with persistent Honcho memory. + + Pydantic AI's ``message_history`` parameter lets the agent maintain + in-session coherence across turns — it is separate from Honcho's + long-term cross-session memory. + + Args: + user_id: Unique identifier for the user. + message: The user's input message. + session_id: Identifier for the current conversation session. + message_history: Prior messages for in-session coherence. + + Returns: + Tuple of ``(response_text, updated_message_history)``. + """ + ctx = HonchoContext(user_id=user_id, session_id=session_id) + deps = HonchoAgentDeps(ctx=ctx) + + save_memory(user_id, message, "user", session_id) + + result = await honcho_agent.run( + message, + deps=deps, + message_history=message_history or [], + ) + response = str(result.output) + + save_memory(user_id, response, "assistant", session_id) + + return response, result.all_messages() + + +async def main() -> None: + print("Pydantic AI HonchoMemoryAgent — type 'quit' to exit\n") + user_id = "demo-user" + session_id = "demo-session" + message_history: list[ModelMessage] = [] + + while True: + user_input = input("You: ").strip() + if not user_input: + continue + if user_input.lower() in ("quit", "exit"): + break + response, message_history = await chat( + user_id, user_input, session_id, message_history + ) + print(f"Agent: {response}\n") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/pydantic-ai/python/pyproject.toml b/examples/pydantic-ai/python/pyproject.toml new file mode 100644 index 000000000..fc2b14e03 --- /dev/null +++ b/examples/pydantic-ai/python/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "honcho-pydantic-ai-example" +version = "1.0.0" +description = "Pydantic AI integration with Honcho for persistent memory" +requires-python = ">=3.10" +dependencies = [ + "pydantic-ai>=0.0.14", + "honcho-ai>=2.0.0", + "python-dotenv>=1.0.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/pydantic-ai/python/tools/__init__.py b/examples/pydantic-ai/python/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/pydantic-ai/python/tools/client.py b/examples/pydantic-ai/python/tools/client.py new file mode 100644 index 000000000..c4e7d5ca4 --- /dev/null +++ b/examples/pydantic-ai/python/tools/client.py @@ -0,0 +1,50 @@ +"""Honcho client initialization and context for Pydantic AI integration.""" + +from __future__ import annotations + +import os +from dataclasses import dataclass, field + +from dotenv import load_dotenv +from honcho import Honcho + +load_dotenv() + + +@dataclass +class HonchoContext: + """Holds Honcho identity for a single conversation turn. + + Attributes: + user_id: Unique identifier for the human peer. + session_id: Identifier for the current conversation session. + assistant_id: Peer ID for the assistant. Defaults to ``"assistant"``. + """ + + user_id: str + session_id: str + assistant_id: str = field(default="assistant") + + +def get_client(workspace_id: str | None = None) -> Honcho: + """Initialize and return a Honcho client. + + Args: + workspace_id: Optional workspace ID override. Falls back to + ``HONCHO_WORKSPACE_ID`` env var, then to ``"default"``. + + Returns: + Configured Honcho client instance. + + Raises: + ValueError: If ``HONCHO_API_KEY`` is not set. + """ + api_key = os.getenv("HONCHO_API_KEY") + if not api_key: + raise ValueError( + "HONCHO_API_KEY is required. Set it in your environment or .env file." + ) + + env_workspace = os.getenv("HONCHO_WORKSPACE_ID") + resolved_workspace = workspace_id or env_workspace or "default" + return Honcho(api_key=api_key, workspace_id=resolved_workspace) diff --git a/examples/pydantic-ai/python/tools/get_context.py b/examples/pydantic-ai/python/tools/get_context.py new file mode 100644 index 000000000..cb8eee747 --- /dev/null +++ b/examples/pydantic-ai/python/tools/get_context.py @@ -0,0 +1,30 @@ +"""Retrieve Honcho conversation context formatted for LLM injection.""" + +from __future__ import annotations + +from .client import HonchoContext, get_client + + +def get_context( + ctx: HonchoContext, + tokens: int = 2000, +) -> list[dict[str, str]]: + """Retrieve conversation context ready for injection into an LLM prompt. + + Args: + ctx: ``HonchoContext`` holding the user, session, and assistant IDs. + tokens: Maximum number of tokens to include. Defaults to ``2000``. + + Returns: + A list of message dicts: ``[{"role": "user" | "assistant", "content": "..."}]``. + Returns an empty list if the session has no messages yet. + """ + honcho = get_client() + user_peer = honcho.peer(ctx.user_id) + assistant_peer = honcho.peer(ctx.assistant_id) + session = honcho.session(ctx.session_id) + + session.add_peers([user_peer, assistant_peer]) + + context = session.context(tokens=tokens) + return context.to_openai(assistant=ctx.assistant_id) diff --git a/examples/pydantic-ai/python/tools/save_memory.py b/examples/pydantic-ai/python/tools/save_memory.py new file mode 100644 index 000000000..f642f672a --- /dev/null +++ b/examples/pydantic-ai/python/tools/save_memory.py @@ -0,0 +1,38 @@ +"""Save a conversation message to Honcho memory.""" + +from .client import get_client + + +def save_memory( + user_id: str, + content: str, + role: str, + session_id: str, + assistant_id: str = "assistant", +) -> str: + """Save a single conversation turn to Honcho memory. + + Args: + user_id: Unique identifier for the user peer. + content: Text content of the message to save. + role: Either ``"user"`` or ``"assistant"``. + session_id: Identifier for the conversation session. + assistant_id: Peer ID for the assistant. Defaults to ``"assistant"``. + + Returns: + A confirmation string describing what was saved. + """ + if not content: + raise ValueError("content must not be empty") + + honcho = get_client() + user_peer = honcho.peer(user_id) + assistant_peer = honcho.peer(assistant_id) + session = honcho.session(session_id) + + session.add_peers([user_peer, assistant_peer]) + + sender = assistant_peer if role == "assistant" else user_peer + session.add_messages([sender.message(content)]) + + return f"Saved {role} message to session '{session_id}' for user '{user_id}'." From c2cfc3caa8230fb6cfd73af02134cfb8b54f25c4 Mon Sep 17 00:00:00 2001 From: Juan Franco <91078895+m1lestones@users.noreply.github.com> Date: Fri, 10 Apr 2026 01:54:12 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20CodeRabbit=20review=20?= =?UTF-8?q?=E2=80=94=20role=20validation,=20tokens=20guard,=20save=5Fmemor?= =?UTF-8?q?y=20error=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/pydantic-ai/python/main.py | 10 ++++++++-- examples/pydantic-ai/python/tools/get_context.py | 3 +++ examples/pydantic-ai/python/tools/save_memory.py | 4 ++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/pydantic-ai/python/main.py b/examples/pydantic-ai/python/main.py index 8add12f1d..2c9e8da82 100644 --- a/examples/pydantic-ai/python/main.py +++ b/examples/pydantic-ai/python/main.py @@ -116,7 +116,10 @@ async def chat( ctx = HonchoContext(user_id=user_id, session_id=session_id) deps = HonchoAgentDeps(ctx=ctx) - save_memory(user_id, message, "user", session_id) + try: + save_memory(user_id, message, "user", session_id) + except Exception as exc: + print(f"Warning: failed to save user message — {exc}") result = await honcho_agent.run( message, @@ -125,7 +128,10 @@ async def chat( ) response = str(result.output) - save_memory(user_id, response, "assistant", session_id) + try: + save_memory(user_id, response, "assistant", session_id) + except Exception as exc: + print(f"Warning: failed to save assistant response — {exc}") return response, result.all_messages() diff --git a/examples/pydantic-ai/python/tools/get_context.py b/examples/pydantic-ai/python/tools/get_context.py index cb8eee747..609bdd1eb 100644 --- a/examples/pydantic-ai/python/tools/get_context.py +++ b/examples/pydantic-ai/python/tools/get_context.py @@ -19,6 +19,9 @@ def get_context( A list of message dicts: ``[{"role": "user" | "assistant", "content": "..."}]``. Returns an empty list if the session has no messages yet. """ + if tokens <= 0: + raise ValueError("tokens must be a positive integer") + honcho = get_client() user_peer = honcho.peer(ctx.user_id) assistant_peer = honcho.peer(ctx.assistant_id) diff --git a/examples/pydantic-ai/python/tools/save_memory.py b/examples/pydantic-ai/python/tools/save_memory.py index f642f672a..1c32bf132 100644 --- a/examples/pydantic-ai/python/tools/save_memory.py +++ b/examples/pydantic-ai/python/tools/save_memory.py @@ -2,6 +2,8 @@ from .client import get_client +_VALID_ROLES = {"user", "assistant"} + def save_memory( user_id: str, @@ -24,6 +26,8 @@ def save_memory( """ if not content: raise ValueError("content must not be empty") + if role not in _VALID_ROLES: + raise ValueError(f"role must be 'user' or 'assistant', got '{role}'") honcho = get_client() user_peer = honcho.peer(user_id)