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.
One Python script, chat.py, that you run in the terminal. It asks for a user id once. Then, for each message you type, it does four things:
Looks up what Mem0 already remembers about this user.
Searches the web with Tavily for your message.
Sends the memories and search results to Gemini 3.8 Flash as context, and prints the reply.
Saves your message to Mem0, so the next turn knows more about this user.
If one turn fails, the script prints a short message and waits for your next message instead of stopping. If Gemini's free quota is used up, it says so, with the wait time Google gives. Type quit to stop. All three services have a free plan you can test this on.
Before you start
Set up each piece first, in this order. Each one has its own integration and its own test you can run alone.
Google Gemini. Needs GEMINI_API_KEY. Create a key in Google AI Studio at https://aistudio.google.com/apikey. Google's billing page says new accounts start on the free tier, and only higher tiers need a linked billing account. Google's pricing page lists gemini-3.8-flash as "Free of charge" on the free tier. The same page marks free-tier content as used to improve Google's products, and paid-tier content as not used. Do not send anything private while testing on the free tier.
Tavily web search (Python version). Needs TAVILY_API_KEY. Get one at https://app.tavily.com. Tavily's docs say you get 1,000 free API credits every month, with no credit card required. A basic search costs 1 credit.
Put all three keys in a .env file next to chat.py. The script 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:
Google's docs say the client also reads GOOGLE_API_KEY, and that it wins if both are set. If you have an old GOOGLE_API_KEY set, remove it or set it to the same key.
Connect them
Install the packages. These are the latest versions on PyPI today:
On Windows you may need py -m pip install ... instead.
Create chat.py:
Python
import osimport sysfrom dotenv import load_dotenvfrom google import genaifrom google.genai import typesfrom mem0 import MemoryClientfrom tavily import TavilyClientload_dotenv()sys.stdout.reconfigure(encoding="utf-8")# Timeout is in milliseconds: 120_000 is 120 seconds, for each HTTP attempt.# retry_options: the SDK retries only a 503 (Gemini busy), and only once.# A 429 (quota used up) is not retried, so it fails at once instead of# waiting for hours.gemini = genai.Client( http_options=types.HttpOptions( timeout=120_000, retry_options=types.HttpRetryOptions(attempts=1, http_status_codes=[503]), ))tavily_client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])memory = MemoryClient()def 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"] ], }def is_malformed_tool_call(error): # Gemini's 400 error with code "malformed_tool_call" asks you to retry. body = getattr(error, "body", None) if getattr(error, "status_code", None) != 400 or not isinstance(body, dict): return False details = body.get("error") return isinstance(details, dict) and details.get("code") == "malformed_tool_call"class GeminiQuotaError(Exception): def __init__(self, minutes): super().__init__("Gemini quota used up") self.minutes = minutesdef quota_wait_minutes(error): # Gemini's 429 reply has a Retry-After header: the wait in seconds. response = getattr(error, "response", None) headers = getattr(response, "headers", None) or {} try: seconds = float(headers.get("retry-after", "")) except ValueError: return None return max(1, round(seconds / 60))def ask_gemini(system, message): for attempt in range(3): # the first try plus up to 2 retries try: interaction = gemini.interactions.create( model="gemini-3.8-flash", system_instruction=system, input=message, ) return interaction.output_text except Exception as error: if attempt < 2 and is_malformed_tool_call(error): print("(Gemini sent a broken reply. Trying again.)") continue if getattr(error, "status_code", None) == 429: raise GeminiQuotaError(quota_wait_minutes(error)) from error raiseuser_id = input("User id: ").strip()print('Type a message. Type "quit" to stop.')while True: message = input("\nYou: ").strip() if message.lower() == "quit": break if not message: continue try: # 1. What do we already know about this user? found = memory.search(message, filters={"user_id": user_id}) memories = [m["memory"] for m in found["results"]] # 2. What does the web say? search = web_search(message) # 3. Put both into the system instruction. 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"] ) ) reply = ask_gemini(system, message) print(f"\nBot: {reply}") # 4. Save only the user's message, so Mem0 learns facts about the user. memory.add( [{"role": "user", "content": message}], user_id=user_id, ) except GeminiQuotaError as error: wait = f" (Google said: about {error.minutes} minutes)" if error.minutes else "" print(f"\nGemini's free quota is used up for now. Try again later{wait}.") except Exception as error: print(f"\nThat turn failed ({type(error).__name__}). Try again, or type quit.")
Run it:
Shell
python chat.py
On Windows the command is usually py chat.py.
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 printed replies UTF-8, so characters like dashes do not come out garbled, or crash the script, on Windows.
Clients.genai.Client() reads GEMINI_API_KEY, as in the Gemini piece. HttpOptions(timeout=120_000) stops a Gemini request after 120 seconds; the SDK takes the timeout in milliseconds. The limit applies to each HTTP attempt, not to the SDK's waits between attempts. That is why retry_options is set too: HttpRetryOptions(attempts=1, http_status_codes=[503]) lets the SDK retry only a 503 (Gemini busy), once. Without it, the SDK retries a 429 by itself and waits the full time Google asks for (in testing, almost 8 hours) before each retry, so the chat froze with no output. TavilyClient gets TAVILY_API_KEY passed in, as in the Tavily piece. MemoryClient() reads MEM0_API_KEY, as in the Mem0 piece.
User id. Asked once at the start. Every Mem0 read and write uses it, so each user gets their own memories.
with returns a list. The code keeps only the text of each item.
Each turn is sent to Gemini on its own. Gemini does not see earlier turns directly. It only sees what Mem0 has saved about the user. Google's docs show previous_interaction_id for chaining turns, but this integration does not use it.
Test it
Run python chat.py (or py chat.py on Windows). At the User id: prompt, type user123.
Tell it something about yourself:
Text
You: I am a vegetarian. Any news about plant-based protein this year?
You should see a reply that uses web results, with URLs.
Wait a few seconds, then ask something that needs the memory:
Text
You: Suggest a quick dinner for me.
The reply should take your diet into account. Type quit. Run the script again with a different user id, such as user456, and ask the same dinner question. This time the reply should not know your diet.
In testing, the free tier's Gemini quota ran out after one round of tests (about 10 to 15 calls). If you see "Gemini's free quota is used up for now", the chat is working as meant: wait the time it shows, or use a paid-tier key.
Not confirmed
The combined chat.py was run end to end once by hand and worked: the memory carried across turns, and each user had separate memories. Before the fixes on this page, it failed 2 of 2 automated runs. The next version was tested with a fake Gemini key: each failed turn printed its message and the chat kept going. A full run was blocked because the free quota was used up, and that version froze for hours on the 429 (fixed on this page with retry_options). This version, with the quota message, has not been run yet. The malformed_tool_call retry, the UTF-8 output, and saving only the user's message have not been checked in a run either.
In testing, Gemini answered a plain text request with HTTP 400 and code malformed_tool_call, although no tools were sent. The cause is unknown. It may be set off by the web search text in the system instruction, but that was not confirmed.
The retry check reads status_code and body from the error. Both come from the google-genai 2.32.0 source on GitHub (google/genai/_gaos/lib/compat_errors.py), not from Google's docs. In that version the error class (BadRequestError) only lives under a private module path, so the script checks those two values instead of importing the class. If a later SDK version changes them, the retry stops working, but the per-turn error handling still keeps the chat going.
Gemini free-tier calls were slow in an earlier test: 100 to 190 seconds each, and one hung for more than 5 minutes. They may have been the SDK waiting between 429 retries as the quota ran low, but that is not confirmed. The 120-second timeout and its milliseconds unit come from the google-genai 2.32.0 source (HttpOptions.timeout). Testing showed it applies to each HTTP attempt, and the SDK's waits between retries are not covered by it.
The retry setting comes from the google-genai 2.32.0 source, not from Google's docs pages. HttpRetryOptions and HttpOptions.retry_options are public types, but how they reach client.interactions was read in google/genai/_gaos/google_genai.py (_translate_retry_config). In that version, attempts becomes the number of retries for Interactions calls, so attempts=1 gives one retry, although the field's own description says 1 means no retries. attempts=0 does not turn retries off there either: the SDK changes 0 to 1 when the client is created (google/genai/_api_client.py, retry_args). There is no per-call retry setting on interactions.create in 2.32.0. If a later SDK version changes this, the 429 may be retried again.
Memory search.
memory.search(...)
filters={"user_id": ...}
results
memory
Web search.web_search is the function from the Tavily piece, unchanged. It returns a short answer and up to 5 sources.
Gemini call. The Gemini piece used client.interactions.create to get JSON. Here the same call returns plain text, as Google's text generation page shows. system_instruction carries the memories and search results. input carries your message. interaction.output_text is the reply. The piece's response_format is dropped.
Retry. Gemini sometimes answers with HTTP 400 and the code malformed_tool_call, and its error text says to retry. ask_gemini tries again up to 2 more times on that error only. This is the script's own loop, separate from the SDK's retries. A 429 (quota used up) becomes GeminiQuotaError, which carries Google's wait time in minutes, read from the error's Retry-After header. Any other error is passed on.
Saving. Only your 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.
Error handling. Each turn runs inside try. If Gemini's quota is used up, the script prints "Gemini's free quota is used up for now. Try again later (Google said: about N minutes)." If Gemini, Tavily or Mem0 fails in any other way, it prints a short message with the error type. Either way it waits for your next message.
Loop. Typing quit ends it. Empty lines are skipped.
On that one 503 retry, the SDK waits for Google's Retry-After time if the reply has one, with no upper limit. No 503 with Retry-After was seen in testing.
The 429 check reads status_code and response.headers from the error (the RateLimitError class in compat_errors.py, source only). Google gave about 28,557 seconds (about 7.9 hours) in Retry-After in testing. If the header is missing or is a date instead of seconds, the message leaves out the wait time.
If Gemini, Tavily or Mem0 fails, that turn is not saved to Mem0. If the save itself fails after the reply is shown, the reply stays on screen but nothing is remembered.
In one test 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.
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.
Mem0's pricing page lists a free Hobby plan but does not say whether a credit card is needed to sign up. Google's billing page says no billing account is needed for the free tier but does not mention a credit card. Only Tavily says "no credit card required" in plain words.
Free-tier limits for gemini-3.8-flash are not on the pages used here. In testing, the quota ran out after one round of tests (about 10 to 15 calls), and Google's error gave the time until it resets.
The Gemini integration's own sample, extract.py, can freeze the same way when the quota is used up, and it has no timeout. In testing it hung with no output. The fix on this page (retry_options) was not added there.
The Mem0 quickstart does not say whether a memory is searchable right after add returns. If the second message 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"]).
The Gemini piece was checked with google-genai 2.31.0. This integration pins 2.32.0, the latest on PyPI today. The calls used were not checked against the 2.32.0 changelog.
The PowerShell $env: lines and py -m pip are standard Windows usage. The providers' pages used here do not show Windows commands.