Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.maximem.ai/llms.txt

Use this file to discover all available pages before exploring further.

Add persistent, per-user memory to a Pydantic AI agent in two lines. The integration leans on Pydantic AI’s dependency-injection model — scope and SDK travel through deps, and the search/store tools are registered onto the agent automatically.

Overview

This guide shows how to add Synap to a Pydantic AI application to build agents that:
  • Recall user-specific facts, preferences, and past conversations
  • Persist new information surfaced during a conversation
  • Stay type-safe and testable end-to-end — every dependency is a dataclass
The Synap Pydantic AI integration ships two exports — a deps dataclass and a one-shot registration function.
ExportPurpose
SynapDepsDataclass holding the SDK instance and user scope
register_synap_tools(agent)Registers synap_search and synap_store tools plus a system-prompt fragment

Setup

Install the package alongside Pydantic AI:
pip install maximem-synap-pydantic-ai pydantic-ai
Configure your API key. Generate one from the Synap Dashboard.
.env
SYNAP_API_KEY=synap_your_key_here
OPENAI_API_KEY=your-openai-api-key
Initialize the SDK once at application startup:
from maximem_synap import MaximemSynapSDK

sdk = MaximemSynapSDK()
await sdk.initialize()
See SDK Initialization for the full lifecycle and configuration options.

Basic integration

Declare an agent with deps_type=SynapDeps, call register_synap_tools(agent) once, then pass SynapDeps per request:
from pydantic_ai import Agent
from synap_pydantic_ai import SynapDeps, register_synap_tools

agent: Agent[SynapDeps, str] = Agent(
    "openai:gpt-4o",
    deps_type=SynapDeps,
    system_prompt="You are a helpful assistant with long-term memory.",
)

register_synap_tools(agent)

deps = SynapDeps(sdk=sdk, user_id="alice", customer_id="acme")
result = await agent.run("What do you remember about my project?", deps=deps)
print(result.data)
register_synap_tools does three things:
  1. Registers synap_search — a tool the agent can call to retrieve memories
  2. Registers synap_store — a tool the agent can call to persist new memories
  3. Appends a system-prompt fragment instructing the agent to use both tools
The agent never sees user_id or customer_id directly — it pulls them from RunContext[SynapDeps] inside the tool implementations, so the model cannot spoof scope.

Core concepts

SynapDeps

SynapDeps is the dependency container Pydantic AI injects into every tool call. It carries the SDK and the scoping triple:
from dataclasses import dataclass
from maximem_synap import MaximemSynapSDK

@dataclass
class SynapDeps:
    sdk: MaximemSynapSDK
    user_id: str
    customer_id: str | None = None
    conversation_id: str | None = None
You construct a fresh SynapDeps per request, which means the same agent instance can serve any number of users without bleeding scope between them.

register_synap_tools

register_synap_tools(agent) attaches the two memory tools to your agent. After registration, every run that passes SynapDeps exposes:
  • synap_search(query: str, max_results: int = 5) — returns a list of memory objects scoped to deps.user_id (and deps.customer_id if set)
  • synap_store(content: str, memory_type: str = "fact") — persists a new memory under the same scope
from pydantic_ai import Agent
from synap_pydantic_ai import SynapDeps, register_synap_tools

agent: Agent[SynapDeps, str] = Agent(
    "openai:gpt-4o",
    deps_type=SynapDeps,
    system_prompt="You answer questions about the user's history.",
)

register_synap_tools(agent)
Tool descriptions are written so the model calls synap_search for recall questions and synap_store when the user shares a new fact.

Complete example: per-user assistant

The following pattern is what most production deployments end up with: a single Agent defined at module load, fresh SynapDeps per inbound request, and a thin handler around it.
from pydantic_ai import Agent
from synap_pydantic_ai import SynapDeps, register_synap_tools


# Define the agent once at startup
agent: Agent[SynapDeps, str] = Agent(
    "openai:gpt-4o",
    deps_type=SynapDeps,
    system_prompt=(
        "You are a personal assistant with long-term memory.\n"
        "Call synap_search before answering any question about the user.\n"
        "Call synap_store whenever the user shares a fact, preference, or "
        "decision worth remembering.\n"
        "If synap_search returns no results, say you don't know yet."
    ),
)
register_synap_tools(agent)


# Per-request handler — fresh SynapDeps each time
async def handle_request(
    sdk,
    user_id: str,
    message: str,
    customer_id: str | None = None,
    conversation_id: str | None = None,
) -> str:
    deps = SynapDeps(
        sdk=sdk,
        user_id=user_id,
        customer_id=customer_id,
        conversation_id=conversation_id,
    )
    result = await agent.run(message, deps=deps)
    return result.data


# Usage
reply = await handle_request(sdk, user_id="alice", message="Am I on the Pro plan?")
Three things to notice in this pattern:
  1. The agent is defined once. Pydantic AI’s dependency injection makes it safe to share — each run() gets its own deps.
  2. Scope is request-local. Different users get different SynapDeps; the model only ever sees a scope-stripped tool surface.
  3. The system prompt is the policy. Want recall to be optional? Loosen the prompt. Want stores to be aggressive? Tighten it. The tools obey whatever the prompt tells the model.

Advanced patterns

Multi-tenant scoping

SynapDeps carries the same scoping triple every Synap integration uses — user_id (required), optional customer_id, optional conversation_id. customer_id is required on B2B Synap instances and ignored on single-tenant ones. See Memory Scopes.
# User-only
deps = SynapDeps(sdk=sdk, user_id="alice")

# Organization-scoped (user sees org-shared memories too)
deps = SynapDeps(sdk=sdk, user_id="alice", customer_id="acme-corp")

# Pinned to a conversation (biases retrieval ranking)
deps = SynapDeps(sdk=sdk, user_id="alice", conversation_id="conv-001")

Testing with mock deps

Because SynapDeps is just a dataclass, you can substitute a fake SDK in tests:
@dataclass
class FakeSdk:
    async def memories_search(self, **kwargs):
        return [{"content": "User is on Pro plan", "type": "fact"}]

deps = SynapDeps(sdk=FakeSdk(), user_id="test-user")
result = await agent.run("What plan am I on?", deps=deps)

Failure semantics

The integration follows the Synap-wide contract:
  • synap_search degrades gracefully — returns [] and logs an error if Synap is unreachable, so the agent can continue.
  • synap_store surfaces failures — raises SynapIntegrationError so the caller knows persistence failed.
This is by design: read failures shouldn’t break a user-facing answer, but silent write failures would let the memory drift away from reality.

Next steps

OpenAI Agents

Function tools for the OpenAI Agents SDK.

CrewAI

Storage backend for CrewAI crews.

Context Fetch

The retrieval API that powers synap_search — modes, scopes, and response shapes.

Memory Scopes

How user_id, customer_id, and conversation_id interact across reads.