A FastAPI chatbot that searches the web and remembers each user
One FastAPI endpoint that loads the user's Mem0 memories, searches the web with Tavily, streams a Claude reply with both as context, then saves the user's message.
Tavily uses its Python version, which is a Pro feature (free accounts can use it during their free week).
Your keys
0 of 3 ready
Create each key on its own page, paste it below, then copy the finished lines into your project's .env file. What you paste stays in this tab. It is never sent to Wirestack or saved.
Put all three keys in a .env file next to main.py. The app loads that file itself. If you saved the keys on this computer with npx wirestack keys add <tool>, run npx wirestack keys fill in the project folder and it writes them into .env. Or paste them in by hand:
import osimport sysimport anthropicfrom dotenv import load_dotenvfrom fastapi import FastAPIfrom fastapi.responses import StreamingResponsefrom mem0 import MemoryClientfrom pydantic import BaseModelfrom tavily import TavilyClientload_dotenv()sys.stdout.reconfigure(encoding="utf-8")app = FastAPI()claude = anthropic.AsyncAnthropic()tavily_client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])memory = MemoryClient()class ChatIn(BaseModel): user_id: str prompt: strdef web_search(query): response = tavily_client.search(query, max_results=5, include_answer=True) return { "answer": response.get("answer"), "sources": [ {"title": r["title"], "url": r["url"], "content": r["content"]} for r in response["results"] ], }@app.post("/chat")async def chat(body: ChatIn): # 1. What do we already know about this user? found = memory.search(body.prompt, filters={"user_id": body.user_id}) memories = [m["memory"] for m in found["results"]] # 2. What does the web say? search = web_search(body.prompt) # 3. Put both into the system prompt. system = ( "You are a helpful assistant.\n\n" "What you know about this user:\n" + ("\n".join(f"- {m}" for m in memories) or "- Nothing yet.") + "\n\nWeb search results. Cite the URLs you use:\n" + f"Summary: {search['answer']}\n" + "\n".join( f"- {s['title']} ({s['url']}): {s['content']}" for s in search["sources"] ) ) async def gen(): async with claude.messages.stream( max_tokens=1024, system=system, messages=[{"role": "user", "content": body.prompt}], model="claude-sonnet-5-5", ) as stream: async for text in stream.text_stream: yield text # 4. Save only the user's message, so Mem0 learns facts about the user. memory.add( [{"role": "user", "content": body.prompt}], user_id=body.user_id, ) return StreamingResponse(gen(), media_type="text/plain")
Run it:
Shell
uvicorn main:app --reload
What each part does:
Keys and output.load_dotenv() reads your keys from .env before any client is created. sys.stdout.reconfigure(encoding="utf-8") makes anything the app prints UTF-8, so characters like dashes do not come out garbled, or crash it, on Windows.
Clients.AsyncAnthropic() reads ANTHROPIC_API_KEY. TavilyClient gets TAVILY_API_KEY passed in, as in the Tavily piece. MemoryClient() reads MEM0_API_KEY, as in the Mem0 piece.
Request body. The Claude piece only took prompt. This version adds user_id, so Mem0 knows whose memories to read and write.
Memory search.memory.search(...) with filters={"user_id": ...} returns a results list. The code keeps only the memory text of each item.
Web search.web_search is the function from the Tavily piece, unchanged. It returns a short answer and up to 5 sources.
Test it
Tell it something about yourself:
Shell
curl -N -X POST http://localhost:8000/chat \ -H "Content-Type: application/json" \ -d '{"user_id":"user123","prompt":"I am a vegetarian. Any good news about plant-based protein this year?"}'
You should see the reply arrive bit by bit, with links from the web search.
Wait a few seconds, then ask something that needs the memory:
Shell
curl -N -X POST http://localhost:8000/chat \ -H "Content-Type: application/json" \ -d '{"user_id":"user123","prompt":"Suggest a quick dinner for me."}'
The reply should take your diet into account. Send the same question with a different user_id and it should not.
Not confirmed
This combined main.py has not been run. Each piece was written from its official docs, but the wiring between them was not tested together.
The Tavily and Mem0 clients used here are the regular (not async) ones from the pieces. Their calls block the server while they wait. That is fine for testing. For many users at once you would need async clients, which this integration does not cover. Tavily's SDK reference lists an AsyncTavilyClient; the Mem0 quickstart does not mention an async client.
The Mem0 quickstart does not say whether a memory is searchable right after add returns. If the second request does not use the memory, wait a little longer and try again.
MemoryClient() reading MEM0_API_KEY by itself comes from the Mem0 SDK source on GitHub, not the docs page. If it fails, use MemoryClient(api_key=os.environ["MEM0_API_KEY"]).
If Claude's stream fails partway, nothing is saved to Mem0. The code has no error handling.
The export lines are for macOS and Linux shells. The Windows equivalent is not shown.
load_dotenv() does not replace a key that is already set in the terminal, per python-dotenv's README. If a key is set in both places, the terminal value wins.
In a test of the free version of this integration (which also uses mem0ai) on Python 3.14, installing the packages took more than 10 minutes, because some dependencies built from source. An earlier Python 3 version may install faster. This comes from one test and is not confirmed.
The package versions the pieces were checked with are anthropic 1.17.0, fastapi 0.156.7, uvicorn 0.54.0 and mem0ai 2.2.1. No tavily-python version was checked.
System prompt. Memories and search results go into system, a top-level string on the Claude request. The user's message stays in messages.
Streaming. Same as the Claude piece. Each text chunk is sent to the client as it arrives.
Saving. After the stream ends, only the user's message goes to memory.add(...) under the same user_id. Saving only the user's message is our choice, not Mem0's guidance (Mem0's quickstart saves both sides), because in testing Mem0 stored the bot's news and dinner ideas as facts about the user.