> ## 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.

# Pydantic AI

> Type-safe dependency dataclass and auto-registered tools for Pydantic AI agents.

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.

<Note>
  Requires Python 3.11+.
</Note>

## 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, with every dependency a dataclass

The Synap Pydantic AI integration ships two exports: a `deps` dataclass and a one-shot registration function.

| Export                        | Purpose                                                                        |
| ----------------------------- | ------------------------------------------------------------------------------ |
| `SynapDeps`                   | Dataclass 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:

<CodeGroup>
  ```bash pip theme={null}
  pip install maximem-synap-pydantic-ai pydantic-ai
  ```

  ```bash uv theme={null}
  uv add maximem-synap-pydantic-ai pydantic-ai
  # pip-compatible (existing venv): uv pip install maximem-synap-pydantic-ai pydantic-ai
  ```
</CodeGroup>

<Note>
  The pip package is `maximem-synap-pydantic-ai`, but the import drops the `maximem-` prefix and uses underscores: `from synap_pydantic_ai import ...`.
</Note>

Configure your API key. Generate one from the [Synap Dashboard](https://synap.maximem.ai).

```bash .env theme={null}
SYNAP_API_KEY=synap_your_key_here
OPENAI_API_KEY=your-openai-api-key
```

Initialize the SDK once at application startup:

```python theme={null}
from maximem_synap import MaximemSynapSDK

sdk = MaximemSynapSDK()
await sdk.initialize()
```

See [SDK Initialization](/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:

```python theme={null}
# pip install maximem-synap-pydantic-ai pydantic-ai
from maximem_synap import MaximemSynapSDK
from pydantic_ai import Agent
from synap_pydantic_ai import SynapDeps, register_synap_tools

sdk = MaximemSynapSDK()
await sdk.initialize()

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:

```python theme={null}
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

```python theme={null}
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.

```python theme={null}
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](/concepts/memory-scopes).

```python theme={null}
# 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).
# conversation_id must be a valid UUID.
import uuid
deps = SynapDeps(sdk=sdk, user_id="alice", conversation_id=str(uuid.uuid4()))
```

### Testing with mock deps

Because `SynapDeps` is just a dataclass, you can substitute a fake SDK in tests:

```python theme={null}
@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.

***

## Going further

* [Patterns overview](/patterns/overview): reusable memory patterns across frameworks.
* [Cookbook overview](/cookbook/overview): end-to-end worked examples.

***

## Next steps

<CardGroup cols={2}>
  <Card title="OpenAI Agents" icon="robot" href="/integrations/openai-agents">
    Function tools for the OpenAI Agents SDK.
  </Card>

  <Card title="CrewAI" icon="users-gear" href="/integrations/crewai">
    Storage backend for CrewAI crews.
  </Card>

  <Card title="Context Fetch" icon="search" href="/sdk/context-fetch">
    The retrieval API that powers `synap_search`: modes, scopes, and response shapes.
  </Card>

  <Card title="Memory Scopes" icon="layer-group" href="/concepts/memory-scopes">
    How `user_id`, `customer_id`, and `conversation_id` interact across reads.
  </Card>
</CardGroup>
