{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": "![Redis](https://redis.io/wp-content/uploads/2024/04/Logotype.svg?auto=webp&quality=85,75&width=120)\n\n# Redis Cloud Agent Memory with the NVIDIA NeMo Agent Toolkit\n\n## Introduction\n\nThis is the managed-cloud counterpart of `06_nemo_agent_toolkit_redis.ipynb`. Instead of running the open-source [Agent Memory Server](https://github.com/redis/agent-memory-server) ourselves, we point the [**NVIDIA NeMo Agent Toolkit**](https://github.com/NVIDIA/NeMo-Agent-Toolkit) at **[Redis Cloud Agent Memory](https://redis.io/agent-memory/)**, a fully managed service, via [**nemo-agent-toolkit-redis**](https://github.com/redis-developer/nemo-agent-toolkit-redis).\n\nThe cloud service exposes a different API, so the memory is wired differently than notebook 06 (see the next section) — but the agent still remembers facts across turns.\n\nNo Docker, no server to operate — just an endpoint, a store ID, and an API key." }, { "cell_type": "markdown", "metadata": {}, "source": [ "## How this differs from the self-hosted recipe\n", "\n", "Notebook 06 runs the open-source Agent Memory Server and uses its **auto-memory** workflow, which hydrates the prompt from *working memory* on every turn. Redis Cloud Agent Memory exposes a **different API** (a dedicated SDK with an `api_key` + `store_id`), and the toolkit integrates it as a **long-term memory store**.\n", "\n", "So this recipe uses the toolkit's **tool-based memory** pattern instead: a NAT `react_agent` that decides when to call two tools — `add_memory` (store a fact) and `get_memory` (recall facts) — both backed by the managed cloud store.\n", "\n", "| | Self-hosted (nb 06) | Redis Cloud (this nb) |\n", "|---|---|---|\n", "| Server | you run it via Docker | fully managed |\n", "| Backend `_type` | `redis_agent_memory_backend` | `cloud_redis_agent_memory` |\n", "| Auth | disabled (dev) | `api_key` + `store_id` |\n", "| Memory model | automatic working-memory hydration | explicit `add_memory` / `get_memory` tools |\n", "| Workflow | `redis_agent_memory_auto_memory` | `react_agent` |\n", "\n", "The behavior a user sees is the same — the agent remembers facts across turns — but memory is managed through explicit tool calls rather than automatic hydration." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Let's Begin\n", "\"Open" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "1. [Create a database on Redis Cloud](https://redis.io/docs/latest/operate/rc/databases/create-database) (a [free account](https://redis.io/try-free/) works).\n", "2. [Create an Agent Memory service](https://redis.io/docs/latest/operate/rc/context-engine/agent-memory/create-service) for that database.\n", "3. From the service's **Configuration** page, grab the **API endpoint**, the **Store ID**, and the **API key** (shown only once at creation — [regenerate](https://redis.io/docs/latest/operate/rc/context-engine/agent-memory/view-service#replace-service-api-key) if lost).\n", "4. An **OpenAI API key** for the chat LLM." ] }, { "cell_type": "code", "execution_count": 6, "id": "52295e37", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Note: you may need to restart the kernel to use updated packages.\n" ] } ], "source": [ "# NBVAL_SKIP\n", "# Cloud backend requires nemo-agent-toolkit-redis >= 0.3.0 (adds cloud_redis_agent_memory).\n", "# nvidia-nat-langchain provides the react_agent used for tool-based memory.\n", "%pip install -q \"nemo-agent-toolkit-redis>=0.3.0\" nvidia-nat-langchain requests" ] }, { "cell_type": "markdown", "id": "3ae2f67e", "metadata": {}, "source": [ "## Set environment variables\n", "\n", "Point the toolkit at your managed endpoint and supply the API key." ] }, { "cell_type": "code", "execution_count": 7, "id": "09be4542", "metadata": {}, "outputs": [], "source": [ "# NBVAL_SKIP\n", "import os, getpass\n", "\n", "if not os.environ.get(\"OPENAI_API_KEY\"):\n", " os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API key: \")\n", "\n", "# From your Agent Memory service Configuration page on Redis Cloud:\n", "# - API endpoint (base URL) and Store ID from the service settings\n", "# - API key is shown only once, when you create/replace the service key\n", "if not os.environ.get(\"AGENT_MEMORY_ENDPOINT\"):\n", " os.environ[\"AGENT_MEMORY_ENDPOINT\"] = input(\"Agent Memory API endpoint (e.g. https://.memory.redis.io): \").strip()\n", "if not os.environ.get(\"AGENT_MEMORY_STORE_ID\"):\n", " os.environ[\"AGENT_MEMORY_STORE_ID\"] = input(\"Agent Memory Store ID: \").strip()\n", "if not os.environ.get(\"AGENT_MEMORY_API_KEY\"):\n", " os.environ[\"AGENT_MEMORY_API_KEY\"] = getpass.getpass(\"Agent Memory API key: \")\n", "\n", "os.environ[\"NAT_OPENAI_MODEL\"] = \"gpt-4o-mini\"\n", "\n", "# One user identity for the whole demo, so stored facts are retrievable across turns.\n", "USER_ID = \"demo-user\"\n", "SESSION_ID = \"demo-session\"" ] }, { "cell_type": "markdown", "id": "d08a6aab", "metadata": {}, "source": [ "## Connect to the managed service\n", "\n", "We'll use the cloud SDK directly (via the toolkit's `CloudRedisAgentMemoryEditor`) both to confirm connectivity now and to inspect/clean up memories later. A search that returns without error means the endpoint, store ID, and API key are all good." ] }, { "cell_type": "code", "execution_count": 8, "id": "d1d44df9", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Package metadata not found for nvidia-nat\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Connected to Redis Cloud Agent Memory ✓\n" ] } ], "source": [ "# NBVAL_SKIP\n", "from contextlib import asynccontextmanager\n", "\n", "from redis_agent_memory import AgentMemory\n", "from nvidia_nat_redis.cloud_redis_agent_memory import CloudRedisAgentMemoryEditor\n", "\n", "\n", "@asynccontextmanager\n", "async def cloud_memory_editor():\n", " \"\"\"Open a CloudRedisAgentMemoryEditor against the managed service.\"\"\"\n", " client = AgentMemory(\n", " os.environ[\"AGENT_MEMORY_ENDPOINT\"],\n", " store_id=os.environ[\"AGENT_MEMORY_STORE_ID\"],\n", " api_key=os.environ[\"AGENT_MEMORY_API_KEY\"],\n", " )\n", " try:\n", " yield CloudRedisAgentMemoryEditor(client)\n", " finally:\n", " await client.__aexit__(None, None, None)\n", "\n", "\n", "async with cloud_memory_editor() as editor:\n", " await editor.search(query=\"connectivity check\", user_id=USER_ID, top_k=1)\n", "print(\"Connected to Redis Cloud Agent Memory ✓\")" ] }, { "cell_type": "markdown", "id": "158f4d8b", "metadata": {}, "source": "## Define the NAT workflow\n\nThe config wires three things together:\n\n- **`memory.redis_memory`** — the `cloud_redis_agent_memory` backend, pointed at your managed endpoint with `api_key` + `store_id`.\n- **`get_memory` / `add_memory`** — the two memory tools, both bound to that backend.\n- **`react_agent`** — the agent that calls those tools. We enable `use_native_tool_calling` (OpenAI function calling, more reliable than ReAct text parsing) and bake the demo `user_id` into `additional_instructions` so the agent stores and recalls under one consistent identity.\n\nEnvironment variables (`${...}`) are resolved from the values set above.\n\n> **Note:** memory routing depends on the model following these instructions. Both tools take a `user_id`, and search prefers the id the agent supplies over the runtime context — so if you swap `NAT_OPENAI_MODEL` for a weaker model, verify it still passes the correct `user_id`, or stored facts will silently fail to come back." }, { "cell_type": "code", "execution_count": null, "id": "b97640c9", "metadata": {}, "outputs": [], "source": "# The agent must pass a consistent user_id to the memory tools; inject the same\n# USER_ID set above so there is a single source of truth (search prefers the\n# agent-supplied id over the runtime context).\nconfig_yaml = \"\"\"general:\n telemetry:\n enabled: false\n\nllms:\n openai_llm:\n _type: openai\n model_name: ${NAT_OPENAI_MODEL:-gpt-4o-mini}\n temperature: 0.0\n\nmemory:\n redis_memory:\n _type: cloud_redis_agent_memory\n base_url: ${AGENT_MEMORY_ENDPOINT}\n api_key: ${AGENT_MEMORY_API_KEY}\n store_id: ${AGENT_MEMORY_STORE_ID}\n\nfunctions:\n get_memory:\n _type: get_memory\n memory: redis_memory\n description: \"Retrieve stored facts about the user. Call before answering anything personal.\"\n add_memory:\n _type: add_memory\n memory: redis_memory\n description: \"Store a durable fact or preference the user shares.\"\n\nworkflow:\n _type: react_agent\n tool_names: [get_memory, add_memory]\n description: \"A chat agent using Redis Cloud Agent Memory for long-term memory.\"\n llm_name: openai_llm\n use_native_tool_calling: true\n additional_instructions: >-\n You assist the user whose user_id is \"__USER_ID__\". ALWAYS pass\n user_id=\"__USER_ID__\" to get_memory and add_memory. Never ask the user for an\n id. When the user shares any durable preference or fact, immediately call\n add_memory with that fact. Before answering any question about the user's\n preferences or history, first call get_memory to retrieve relevant facts.\n\"\"\".replace(\"__USER_ID__\", USER_ID)\n\nwith open(\"nat_config.yml\", \"w\") as f:\n f.write(config_yaml)\nprint(\"wrote nat_config.yml\")" }, { "cell_type": "markdown", "id": "1b51ef89", "metadata": {}, "source": [ "## Run the agent\n", "\n", "`run_workflow` runs a single turn. We pass `session_kwargs` so NAT sets the runtime `user_id` / `conversation_id`; the memory tools use that `user_id` to scope what they store and recall in the cloud." ] }, { "cell_type": "code", "execution_count": null, "id": "32e4fb34", "metadata": {}, "outputs": [], "source": "# NBVAL_SKIP\nfrom pathlib import Path\n\nfrom nat.utils import run_workflow\n\nCONFIG_FILE = Path(\"nat_config.yml\").resolve()\n\n\nasync def chat(prompt: str, user_id: str = USER_ID, conversation_id: str = SESSION_ID) -> str:\n \"\"\"Run one turn through the react_agent.\n\n The agent decides when to call add_memory / get_memory, both backed by the\n managed Redis Cloud store and scoped to this user_id.\n \"\"\"\n result = await run_workflow(\n config_file=CONFIG_FILE,\n prompt=prompt,\n to_type=str,\n session_kwargs={\"conversation_id\": conversation_id, \"user_id\": user_id},\n )\n print(f\"User: {prompt}\")\n print(f\"Assistant: {result}\\n\")\n return result" }, { "cell_type": "code", "execution_count": 11, "id": "27c0dedb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "User: Hi! My name is Justin and my favorite city is Lisbon.\n", "Assistant: Hi Justin! It's great to meet you. I see that your favorite city is Lisbon. What do you love most about it?\n", "\n", "User: I'm a vegetarian, by the way.\n", "Assistant: The user is a vegetarian.\n", "\n", "User: Where should I plan a food trip, and what should I keep in mind?\n", "Assistant: When planning a food trip, consider destinations known for their vegetarian cuisine. Since your favorite city is Lisbon, you might explore local vegetarian restaurants and markets there. Keep in mind to research the best vegetarian-friendly spots, check for seasonal ingredients, and perhaps look for food festivals that celebrate plant-based dishes. Additionally, consider the local culture and how it influences vegetarian options, as well as any dietary restrictions you may have.\n", "\n" ] }, { "data": { "text/plain": [ "'When planning a food trip, consider destinations known for their vegetarian cuisine. Since your favorite city is Lisbon, you might explore local vegetarian restaurants and markets there. Keep in mind to research the best vegetarian-friendly spots, check for seasonal ingredients, and perhaps look for food festivals that celebrate plant-based dishes. Additionally, consider the local culture and how it influences vegetarian options, as well as any dietary restrictions you may have.'" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# NBVAL_SKIP\n", "# A multi-turn conversation. The third turn relies on facts stored in turns 1-2.\n", "await chat(\"Hi! My name is Justin and my favorite city is Lisbon.\")\n", "await chat(\"I'm a vegetarian, by the way.\")\n", "# New turn -> the agent calls get_memory and recalls the earlier facts from Redis Cloud\n", "await chat(\"Where should I plan a food trip, and what should I keep in mind?\")" ] }, { "cell_type": "markdown", "id": "b59bb837", "metadata": {}, "source": [ "The third answer reflects the favorite city and diet from earlier turns — recalled from the managed Cloud store via `get_memory`, not from anything passed back in.\n", "\n", "## Inspect long-term memory\n", "\n", "Query the store directly through the same cloud editor to see what the agent persisted." ] }, { "cell_type": "code", "execution_count": 12, "id": "9da6bfbe", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "- User's name is Justin and favorite city is Lisbon.\n", "- User's name is Justin and favorite city is Lisbon.\n", "- User is a vegetarian.\n", "- User is a vegetarian.\n" ] } ], "source": [ "# NBVAL_SKIP\n", "# Inspect what the agent stored, straight from the managed store.\n", "async with cloud_memory_editor() as editor:\n", " memories = await editor.search(query=\"favorite city and diet\", user_id=USER_ID, top_k=5)\n", "\n", "for m in memories:\n", " print(f\"- {m.memory}\")\n", "if not memories:\n", " print(\"(no memories found for this user yet)\")" ] }, { "cell_type": "markdown", "id": "f70686b0", "metadata": {}, "source": "## Cleanup\n\nNothing to tear down locally. We delete the demo user's memories to keep the store tidy. Memories here are scoped only by `user_id` (no namespace), so this removes **every** memory stored for `USER_ID` in this store — not just this notebook's. To stop incurring cost entirely, delete or pause the Agent Memory service (and its database) from the Redis Cloud console when you're done." }, { "cell_type": "code", "id": "64886245", "source": "# NBVAL_SKIP\n# Delete all long-term memories stored for the demo user.\nasync with cloud_memory_editor() as editor:\n await editor.remove_items(user_id=USER_ID)\nprint(f\"Deleted long-term memories for user '{USER_ID}'\")", "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\n", "\n", "Same NeMo Agent Toolkit agent, same memory behavior — now backed by managed [Redis Cloud Agent Memory](https://redis.io/agent-memory/). Moving from the self-hosted server to production was a change of `base_url` and an API key, nothing more." ] } ], "metadata": { "kernelspec": { "display_name": "redis-ai-res", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.9" } }, "nbformat": 4, "nbformat_minor": 5 }