|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Persistent multi-agent conversations with the OpenAI Agents SDK and MongoDB" |
| 4 | +date: 2026-04-27 12:50:00 -0400 |
| 5 | +comments: true |
| 6 | +categories: MongoDB |
| 7 | +tags: [mongodb, ai, python] |
| 8 | +image: /images/mongodb-ai.png |
| 9 | +--- |
| 10 | + |
| 11 | +*Version 0.14.2 added a `MongoDBSession` backend; here's a working multi-agent customer-support demo that uses it, and the documents it leaves behind.* |
| 12 | + |
| 13 | +The OpenAI Agents SDK has shipped session backends for SQLite, SQLAlchemy, Redis, and Dapr for a while now. With **0.14.2** (April 2026), [`MongoDBSession` joined that list](https://openai.github.io/openai-agents-python/sessions/), and 0.14.6 added the docs page. If you're already running MongoDB for application data, this is the moment to stop standing up a second store just to remember what the agent said three turns ago. The demo for this walkthrough is a small e-commerce support app with three handoff-connected agents and one MongoDB instance behind everything: customers, orders, support articles, **and** the conversation history. |
| 14 | + |
| 15 | +Repo: <https://github.com/alexbevi/mongodb-openai-agents-sdk-example>. |
| 16 | + |
| 17 | +## What you'll build |
| 18 | + |
| 19 | +A CLI customer-support agent that identifies the user from MongoDB, hands off between a triage agent, an order-support agent, and a knowledge-base agent, and persists every turn (user message, tool call, tool output, assistant reply, handoff) to MongoDB via `MongoDBSession`. You quit, restart the process, log in with the same email, and the agent picks up the thread — no re-explaining the return you started yesterday. |
| 20 | + |
| 21 | +## Why MongoDB for sessions |
| 22 | + |
| 23 | +A session backend has three jobs: store one item per turn, return them in order on the next run, and not corrupt itself when two processes write at once. The interesting part for MongoDB is how naturally each of those maps to things the database already does. |
| 24 | + |
| 25 | +**Items in a session are heterogeneous.** A turn can be a user message, a tool call, a tool result, an assistant message, or a handoff record — each with its own shape. A document store takes those payloads as-is. There's no `messages` table you have to migrate every time the SDK adds a new run-item type, and no JSON column to parse around. |
| 26 | + |
| 27 | +**Ordering is the hard part, and `$inc` is built for it.** `MongoDBSession` stamps each message with a monotonically increasing `seq` counter — the SDK docs call this out explicitly: it preserves ordering across concurrent writers and processes. That's a single-document atomic increment, not a distributed lock or an optimistic-retry loop. Two FastAPI workers handling the same `session_id` won't interleave. |
| 28 | + |
| 29 | +**One store, one connection pool.** This is the angle the demo actually showcases. The `ecommerce_support` database holds `customers`, `orders`, and `support_articles` *next to* `agent_sessions` and `agent_messages`. Tools query operational data, the SDK persists turns, and they share the same `AsyncMongoClient`. Adding session memory cost zero new infrastructure. |
| 30 | + |
| 31 | +## Walkthrough |
| 32 | + |
| 33 | +### 1. Prerequisites |
| 34 | + |
| 35 | +Python 3.10+, an OpenAI API key, and either a local `mongod` or a [MongoDB Atlas](https://www.mongodb.com/cloud/atlas/register) cluster. Nothing in the demo requires Atlas-only features — a 27017 on localhost is fine. |
| 36 | + |
| 37 | +### 2. Install |
| 38 | + |
| 39 | +`requirements.txt` pins the new extra: |
| 40 | + |
| 41 | +```text |
| 42 | +openai-agents[mongodb]>=0.14.2 |
| 43 | +python-dotenv>=1.0.0 |
| 44 | +pymongo>=4.13 |
| 45 | +``` |
| 46 | + |
| 47 | +```bash |
| 48 | +pip install -r requirements.txt |
| 49 | +``` |
| 50 | + |
| 51 | +The `[mongodb]` extra pulls in `pymongo`'s async client; the `MongoDBSession` class lives at `agents.extensions.memory.MongoDBSession`. |
| 52 | + |
| 53 | +### 3. Connect |
| 54 | + |
| 55 | +The demo uses one shared `AsyncMongoClient` per process (the right pattern — sessions don't own the client, they share its pool): |
| 56 | + |
| 57 | +```python |
| 58 | +from pymongo.asynchronous.mongo_client import AsyncMongoClient |
| 59 | + |
| 60 | +MONGODB_URI = os.environ.get("MONGODB_URI", "mongodb://localhost:27017") |
| 61 | +DB_NAME = "ecommerce_support" |
| 62 | + |
| 63 | +mongo_client = AsyncMongoClient(MONGODB_URI) |
| 64 | +db = mongo_client[DB_NAME] |
| 65 | + |
| 66 | +try: |
| 67 | + await mongo_client.admin.command("ping") |
| 68 | +except Exception as exc: |
| 69 | + print(f"\nCannot connect to MongoDB ({MONGODB_URI}):\n {exc}") |
| 70 | + return |
| 71 | +``` |
| 72 | + |
| 73 | +### 4. Seed and identify |
| 74 | + |
| 75 | +`python seed_data.py` loads three demo customers, five products, five orders with embedded line items, and seven support articles indexed for `$text` search. Then `main.py` looks the customer up so the triage agent doesn't have to ask for an email it already knows. |
| 76 | + |
| 77 | +### 5. Instantiate the session |
| 78 | + |
| 79 | +This is the integration: |
| 80 | + |
| 81 | +```python |
| 82 | +session_id = f"support_{email.replace('@', '_at_').replace('.', '_')}" |
| 83 | +session = MongoDBSession( |
| 84 | + session_id=session_id, |
| 85 | + client=mongo_client, |
| 86 | + database=DB_NAME, |
| 87 | +) |
| 88 | + |
| 89 | +if not await session.ping(): |
| 90 | + print("Warning: MongoDB session storage is unavailable.") |
| 91 | + |
| 92 | +existing = await session.get_items() |
| 93 | +``` |
| 94 | + |
| 95 | +Constructing with `client=` (rather than `MongoDBSession.from_uri(...)`) means the session shares the app's connection pool and `session.close()` becomes a no-op — the lifecycle stays with you. `session.ping()` is a real round-trip against MongoDB, useful for liveness probes. |
| 96 | + |
| 97 | +### 6. Run |
| 98 | + |
| 99 | +Pass `session=` to the runner. Everything else is the same SDK you already know: |
| 100 | + |
| 101 | +```python |
| 102 | +with trace("Customer Support", group_id=conversation_id): |
| 103 | + result = await Runner.run( |
| 104 | + current_agent, |
| 105 | + input=user_input, |
| 106 | + context=ctx, |
| 107 | + session=session, # MongoDB stores every turn automatically |
| 108 | + ) |
| 109 | +``` |
| 110 | + |
| 111 | +Have a conversation, `quit`, run `python main.py` again with the same email, and the next message gets the full prior context prepended automatically. |
| 112 | + |
| 113 | +## What MongoDB actually stored |
| 114 | + |
| 115 | +After a few turns with `alice@example.com`, two collections show up in the `ecommerce_support` database. The interesting one is `agent_messages`. A representative document, abridged: |
| 116 | + |
| 117 | +```js |
| 118 | +{ |
| 119 | + _id: ObjectId("6620d1f4..."), |
| 120 | + session_id: "support_alice_at_example_com", // partition key for this conversation |
| 121 | + seq: 7, // monotonically increasing turn order |
| 122 | + message_data: { // the SDK's run-item, stored as-is |
| 123 | + type: "function_call_output", |
| 124 | + call_id: "call_8b2...", |
| 125 | + output: "Return initiated for order ORD-1001.\nReason: Not powerful enough...\nEstimated refund: $1,484.98 (includes 10% Gold loyalty bonus)" |
| 126 | + }, |
| 127 | + created_at: ISODate("2026-04-26T19:14:08.221Z") |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +Three fields earn their keep: |
| 132 | + |
| 133 | +- **`session_id`** is the only field every read filters on. It's the partition key for "this conversation." |
| 134 | +- **`seq`** is the integer that makes ordering deterministic. The SDK reads with `sort({ seq: 1 })` and writes with an atomic `$inc` against the matching `agent_sessions` document, which is what makes concurrent workers safe without a distributed lock. |
| 135 | +- **`message_data`** is the SDK's run-item — a user message, tool call, tool output, assistant message, or handoff. Different shape every time. The document model just stores it. |
| 136 | + |
| 137 | +`agent_sessions` holds one document per `session_id` with the current high-water `seq` and timestamps — that's the counter `$inc` operates on. |
| 138 | + |
| 139 | +The SDK creates its indexes on first use (per the [sessions docs](https://openai.github.io/openai-agents-python/sessions/)). You'll see a compound index on `(session_id, seq)` on `agent_messages` (the only access pattern the SDK has — fetch ordered history for one session) and a unique index on `session_id` in `agent_sessions`. |
| 140 | + |
| 141 | +## Production notes |
| 142 | + |
| 143 | +For Atlas, swap the URI for `mongodb+srv://...` — `MongoDBSession` accepts it without any other change. If abandoned conversations accumulate, add a TTL index on `agent_messages.created_at` and old turns retire on their own. |
| 144 | + |
| 145 | +Connection lifetime matters: keep one `AsyncMongoClient` per process, construct `MongoDBSession(client=...)` per request, and let the Runner do the rest. Don't reach for `MongoDBSession.from_uri(...)` in a web handler — it builds and tears down a client every call. The session needs read/write on the two configured collections (defaults `agent_sessions` and `agent_messages`, both overridable via `sessions_collection=` and `messages_collection=`). The `seq` counter keeps concurrent writers safe, but fanning the same `session_id` across processes will interleave their turns — safe, but probably not what the user meant. |
| 146 | + |
| 147 | +## Try it yourself |
| 148 | + |
| 149 | +```bash |
| 150 | +git clone https://github.com/alexbevi/mongodb-openai-agents-sdk-example |
| 151 | +cd mongodb-openai-agents-sdk-example |
| 152 | +pip install -r requirements.txt |
| 153 | +cp env.example .env # set OPENAI_API_KEY and MONGODB_URI |
| 154 | +python seed_data.py |
| 155 | +python main.py |
| 156 | +``` |
| 157 | + |
| 158 | +Required env vars: `OPENAI_API_KEY`, `MONGODB_URI` (defaults to `mongodb://localhost:27017`). Demo accounts: `alice@example.com` (Gold), `bob@example.com` (Standard), `carol@example.com` (Platinum). |
| 159 | + |
| 160 | +## Where to go next |
| 161 | + |
| 162 | +- The full session API surface — `get_items`, `add_items`, `pop_item`, `clear_session`, `ping` — is documented in the [Sessions overview](https://openai.github.io/openai-agents-python/sessions/), including the MongoDB-specific notes on collection naming and Atlas URIs. |
| 163 | +- Wrap your `MongoDBSession` in [`OpenAIResponsesCompactionSession`](https://openai.github.io/openai-agents-python/sessions/) once threads grow long; it summarizes old turns server-side and rewrites the underlying session. |
| 164 | +- The natural next MongoDB feature for this demo is [Atlas Vector Search](https://www.mongodb.com/docs/atlas/atlas-vector-search/) — store embeddings on `support_articles` and replace the `$text` query in `search_knowledge_base` with `$vectorSearch`. Same database, same client, one new index. |
0 commit comments