Integrations: taking the trip planner to production
Integrations are the production versions of the pieces this course ran in memory: a saved checkpointer, a persistent store, a real search tool and a hosted model, each a swap of one argument.
Last updated: 29 Sep, 2026 · Deep Agents 0.7
Most lessons ran on in-memory pieces and a fixed catalog, so each output could be checked. This lesson maps each one to what the docs recommend in production, and swaps two of them for real: the checkpointer and the search tool.
What the course used and the real versions
| Used in the course | In production | Package |
|---|---|---|
InMemorySaver | SqliteSaver, PostgresSaver | langgraph-checkpoint-sqlite, langgraph-checkpoint-postgres |
InMemoryStore | PostgresStore, or the store LangSmith Deployment provides | langgraph-checkpoint-postgres |
StateBackend for files | CompositeBackend with a store route, or a sandbox backend | deepagents, sandbox integrations |
search_travel on a fixed catalog | Tavily search, or a provider's built-in web search | tavily-python |
Groq's free openai/gpt-oss-120b | Any tool-calling model: OpenAI, Anthropic, Gemini | langchain-openai, langchain-anthropic, ... |
| Printed steps | LangSmith tracing | set LANGSMITH_TRACING=true |
Swapping InMemorySaver for SqliteSaver
pip install "langgraph-checkpoint-sqlite==3.1.1"SqliteSaver saves every checkpoint to a SQLite file. The test: write a plan with one agent, then build a new agent, as after a restart, and ask it about the file on the same thread.
from deepagents import create_deep_agent
from langchain.chat_models import init_chat_model
model = init_chat_model("groq:openai/gpt-oss-120b", temperature=0, max_retries=6)import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver
def build_agent():
saver = SqliteSaver(sqlite3.connect("trips.db", check_same_thread=False)) # a file on disk
return create_deep_agent(model=model, checkpointer=saver,
system_prompt="You are a travel planner. Reply in one short sentence.")thread = {"configurable": {"thread_id": "paris-trip"}}
build_agent().invoke({"messages": [{"role": "user", "content": "Save a plan with one line, Day 1: Eiffel Tower, to /trip/plan.md."}]}, config=thread)
restarted = build_agent() # a new agent, as after a restart
result = restarted.invoke({"messages": [{"role": "user", "content": "What does /trip/plan.md say?"}]}, config=thread)
print("files:", list(result["files"]))
print(result["messages"][-1].text)files: ['/trip/plan.md'] It contains “Day 1: Eiffel Tower”.
The second agent was built from scratch, yet its result lists /trip/plan.md and it answered from it: the thread's state, files included, came back from trips.db.
Switching search_travel to web search
The drop-in for real data is web_search, the Tavily tool from Tools: a travel search the agent can call; it needs tavily-python and a TAVILY_API_KEY, both set up there. Pass it in tools next to search_travel: the catalog keeps the prices every lesson checks, and the web answers what the catalog does not hold. Start live_trip.py with search_travel, the catalog tool from the same lesson:
from langchain.tools import tool
CATALOG = {
"paris": {
"flight": ["Return flight Delhi to Paris: 42,000 rupees"],
"hotel": ["Seine Budget Inn, Latin Quarter: 5,200 rupees a night",
"Hotel Lumiere, Montmartre: 7,500 rupees a night",
"Le Grand Opera Hotel: 16,000 rupees a night"],
"sight": ["Eiffel Tower summit: 3,100 rupees", "Louvre Museum: 2,000 rupees",
"Seine river cruise: 1,500 rupees", "Versailles day trip: 2,600 rupees",
"Montmartre walking tour: free"],
"food": ["Cafe breakfast and bistro dinner: 3,000 rupees a day"],
},
}
@tool
def search_travel(city: str, kind: str) -> str:
"""Search the travel catalog. kind is "flight", "hotel", "sight" or "food". Prices are in rupees."""
entries = CATALOG.get(city.lower(), {}).get(kind)
return "\n".join(entries) if entries else f"The catalog has no {kind} entries for {city}."Then web_search, as written there:
import os
from tavily import TavilyClient
from typing import Literal
tavily_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
def web_search(query: str, max_results: int = 5,
topic: Literal["general", "sports", "news", "finance"] = "general"):
"""Run a web search"""
return tavily_client.search(query, max_results=min(max_results, 5), topic=topic)The agent gets both tools, and its prompt says which one answers what. The question needs both: a price the catalog holds and an opening day it does not.
from deepagents import create_deep_agent
from langchain.chat_models import init_chat_model
model = init_chat_model("groq:openai/gpt-oss-120b", temperature=0, max_retries=6)
agent = create_deep_agent(
model=model,
tools=[search_travel, web_search],
system_prompt="You are a travel planner. Use search_travel for prices and web_search for anything the "
"catalog does not have. Use only what the tools return. Answer in two short sentences.",
)
result = agent.invoke({"messages": [{"role": "user", "content": "What is the cheapest hotel in Paris, and on which day of the week is the Louvre closed?"}]})
for message in result["messages"]:
if message.type == "tool":
print(f"tool {message.name} returned {len(message.text)} characters")
else:
print(f"{message.type:<5}", message.text or [(c["name"], c["args"]) for c in message.tool_calls])human What is the cheapest hotel in Paris, and on which day of the week is the Louvre closed?
ai [('search_travel', {'city': 'Paris', 'kind': 'hotel'})]
tool search_travel returned 145 characters
ai [('web_search', {'query': 'Louvre museum closed day of week'})]
tool web_search returned 6928 characters
ai The cheapest hotel in Paris is the Seine Budget Inn in the Latin Quarter at 5,200 rupees per night. The Louvre Museum is closed on Tuesdays.How the agent split the question
- search_travel answered the price part: the catalog's three Paris hotels, 145 characters.
- web_search answered the part the catalog does not hold, with the query "Louvre museum closed day of week"; Tavily sent back about 6,900 characters of results.
- The answer takes the price from the catalog, the Seine Budget Inn at 5,200 rupees a night, and the closing day from the web, Tuesday.
- Search results change from day to day, so your run will find other pages and word its answer differently.
Topics the course did not cover
| Topic | What it is | Docs |
|---|---|---|
Deep Agents Code (dcode) | A terminal coding agent built on the SDK | Deep Agents Code |
| Sandboxes | Backends that add an execute tool in an isolated machine | Sandboxes |
| Interpreters | An eval tool that runs JavaScript in QuickJS | Interpreters |
| Async subagents | Background subagents you can check, steer and cancel | Async subagents |
| Forked subagents | Subagents that inherit the parent's conversation (beta) | Forked subagents |
| Event streaming v3 | Typed streams per subagent with stream_events | Event streaming |
| Profiles | Per-provider and per-model defaults | Profiles |
| Grading rubrics | LLM-as-a-judge loops until work meets a rubric | Grading rubrics |
| Semantic memory search | Deep Agents memory is file-based; vector search over memories lives in the LangGraph store | Memory |
| ACP, A2A and AG-UI | Serving a deep agent to editors, other agents and UIs | ACP |
Where each swap matters
- A checkpointer on disk or in Postgres as soon as conversations must survive a restart.
- A persistent store once memories under
/memories/must last longer than the process. - Tracing from the first day real users arrive, to see every step of a slow or wrong run.
SqliteSaver with check_same_thread=False is fine for one process; for a server with many workers use Postgres.Related
- Previous: Trip planner: the finished deep agent
- Reference: Going to production
- Run the example twice and check that the second run still finds the plan from the first.
- Change the thread id in the second call and read the answer.
- Open
trips.dbwith thesqlite3command and list its tables.
You understood something today that you didn't yesterday.