# Redis Real Time Context Engine : Build a memory-aware AI agent with Redis Agent Memory and Redis Context Retriever

**Authors:** Prasan Rajpurohit | **Category:** For AI | **Published:** 2026-05-18 | **Updated:** 2026-05-18

> **TL;DR:** AI agents forget everything when a conversation ends, and can't query structured data without custom code. Redis solves both. **Redis Context Retriever** turns your entity data into auto-generated MCP tools any agent can discover and call. **Redis Agent Memory** gives agents persistent session memory and cross-session long-term memory backed by vector search. Together they make Redis a full context engine for AI applications.

![Redis Agent Memory Explorer — wealth advisor demo showing session memory, long-term memory, and AI copilot panels](https://cdn.sanity.io/images/sy1jschh/production/068068d3c025cbe890c569692103a21a59cd2573-2622x1302.png)

> **Note:** This tutorial uses the code from the following git repository:
>
> `https://github.com/redis-developer/redis-agent-memory-explorer`

When a wealth advisor meets with a client every month, they're expected to remember what was discussed last quarter, the client's risk tolerance, their family situation, and every commitment made across a dozen previous meetings. Without a memory system, every LLM-powered assistant starts blank. The context window fills up fast, the session ends, and everything is forgotten.

This is the memory problem — and it has two distinct parts:

- **Conversational memory**: what was said, what was decided, what the client revealed across many sessions over time
- **Structured knowledge**: portfolio holdings, financial goals, pending action items — data that belongs in a queryable store, not a chat log

In this tutorial, you'll build the **Wealth Advisor Agent Memory Explorer** — a demo that solves both problems using two new Redis Cloud capabilities: **Redis Context Retriever** for structured data as MCP tools, and **Redis Agent Memory** for persistent conversational memory. By the end, you'll have a running LangGraph-powered chatbot agent that can answer questions by querying both data layers and showing exactly where each answer came from.

---

## How the two data layers compare

Before diving in, here's what each layer does and when to reach for it:

|                          | Redis Context Retriever                       | Redis Agent Memory                                        |
| ------------------------ | --------------------------------------------- | --------------------------------------------------------- |
| **What it stores**       | Structured records (entities, facts, records) | Conversational events and extracted memories              |
| **Data source**          | Hand-authored JSON, loaded once               | Generated from conversation during playback               |
| **Query style**          | TAG filters, full-text search, range queries  | Semantic vector search, session scoping                   |
| **How agents access it** | Auto-generated MCP tools                      | SDK methods (`searchLongTermMemory`, `buildMemoryPrompt`) |
| **Grows over time**      | Static per dataset load                       | Yes — every session adds more memories                    |
| **Best for**             | "What are James's holdings?"                  | "What did James say about bonds last month?"              |

---

## Prerequisites

- A [Redis Cloud](https://redis.io/try-free/) account with Redis Agent Memory and Redis Context Retriever enabled
- An [OpenAI API key](https://platform.openai.com/)
- [Docker](https://www.docker.com/) and Docker Compose
- [Node.js](https://nodejs.org/) 18+

---

## Setup

Clone the repo and copy the environment template:

```bash
git clone https://github.com/redis-developer/redis-agent-memory-explorer.git
cd redis-agent-memory-explorer
cp .env.example .env
```

Fill in your `.env`:

```bash
# Redis Agent Memory
RAM_ENDPOINT=https://your-endpoint.redis.io
RAM_API_KEY=your-api-key
RAM_STORE_ID=your-store-id

# Redis Context Retriever
CTX_ADMIN_KEY=your-admin-key

# OpenAI
OPENAI_API_KEY=sk-...

# Local Redis (for copilot auxiliary stores)
REDIS_URL=redis://localhost:6379
```

> **Note:** Leave `CTX_SURFACE_ID` and `MCP_AGENT_KEY` blank — the app creates and writes them on first run.

Start the app:

```bash
docker compose up -d
```

Open `http://localhost:3001`. On first run, the backend creates a Redis Context Retriever, loads entity records from `data/wealth-advisor/client-data.json`, and writes `CTX_SURFACE_ID` and `MCP_AGENT_KEY` back to `.env`. Subsequent runs skip this step and reuse the existing retriever(surface).

---

## What you'll build

The **Wealth Advisor Agent Memory Explorer** is a demo where Sarah Chen, a relationship manager at Acme Bank, plays back recorded meeting transcripts with her client James Morrison. The app stores those conversations in Redis Agent Memory, extracts long-term facts automatically, and exposes structured client data via Redis Context Retriever. A LangGraph ReAct chatbot can then query both layers to answer questions with source attribution.

### Architecture

```
Frontend (Next.js)              Backend (Node.js)                Redis Cloud
──────────────────              ─────────────────                ───────────

TranscriptPanel                 appendWorkingMemory      ──►    Redis Agent Memory
  (play transcript)               addSessionEvent                 Session events
                                                                  LTM (auto-extracted)

MemoryExplorerPanel  ◄──        getWorkingMemory         ◄──    Redis Agent Memory
  Session memory tab              searchLongTermMemory
  Long-term memory tab

AI Copilot tab       ──►        generateSuggestion       ──►    buildMemoryPrompt → LLM

CopilotKit Sidebar   ──►        /copilotkit (proxy)
  (chatbot)                       │
                                  ▼
                              LangGraph Server (port 2024)
                                ReAct agent
                                  ├─ RAM tools         ──►     Redis Agent Memory
                                  └─ Context Retriever   ──►     Redis Context Retriever MCP
```

> **Note:** The app runs as two processes: the API server (port 3001) and the LangGraph server (port 2024). In Docker, these are two separate containers from the same image. Both initialize their own Redis Agent Memory and Redis Context Retriever clients on startup.

### Tech stack

| Layer                  | Technology                                                  |
| ---------------------- | ----------------------------------------------------------- |
| Frontend               | Next.js 14 (App Router), React, CopilotKit                  |
| Backend                | Node.js, Express                                            |
| Chatbot agent          | LangGraph, LangChain, `createReactAgent`                    |
| Memory                 | Redis Agent Memory (Redis Cloud)                            |
| Structured data        | Redis Context Retriever (Redis Cloud)                       |
| LLM                    | OpenAI `gpt-4o-mini`                                        |
| Local auxiliary stores | Redis (JSON store — suggestions, topics, transcript chunks) |

---

## Redis Context Retriever

### What is Redis Context Retriever?

Redis Context Retriever is a Redis Cloud service that takes your structured entity data and turns it into **auto-generated MCP (Model Context Protocol) tools** that any agent can discover and call. You define an entity schema, load records, and Redis handles the rest — generating a full set of query tools without any custom API code.

For the wealth advisor demo, the entity schema defines four entities: `Client`, `Holding`, `FinancialGoal`, and `ActionItem`. Redis Context Retriever generates tools like `filter_holding_by_asset_class`, `search_financialgoal_by_text`, and `find_holding_by_current_value_range` — each self-describing, each callable by the LangGraph agent at runtime.

### Why this matters

| Traditional approach                                   | With Redis Context Retriever                                         |
| ------------------------------------------------------ | -------------------------------------------------------------------- |
| Build and maintain a custom API for every data source  | Auto-generated MCP tools, no custom API code                         |
| Hardcode entity knowledge in the agent's system prompt | Tools are self-describing; the agent discovers them at runtime       |
| Adding a new queryable field requires code changes     | Update the schema and reload records                                 |
| One integration per data source                        | One retriever (surface) per dataset; the same agent queries them all |

### Key concepts

- **Retriever (Surface)**: A named collection tied to a data source (Redis) and an entity schema. Think of it as a queryable namespace. You can view and manage all your surfaces in the [Redis Cloud Console](https://cloud.redis.io/#/context-retriever).
- **Entity schema**: Defines your data model — field names, types, and descriptions. This drives tool generation.
- **Admin key**: Used to create and manage surfaces and issue agent keys. Generate one from the [Context Retriever admin keys page](https://cloud.redis.io/#/context-retriever/admin-keys) in Redis Cloud and set it as `CTX_ADMIN_KEY` in your `.env`.
- **Agent key**: A scoped key the agent uses to call MCP tools, separate from the admin key used for retriever (surface) management.
- **MCP tools**: Auto-generated and self-describing. Tool names encode the query pattern — `filter_<entity>_by_<field>`, `search_<entity>_by_text`, `get_<entity>_by_id`, `find_<entity>_by_<field>_range`.

Once the wealth-advisor surface is created, the [Redis Cloud Console](https://cloud.redis.io/#/context-retriever) shows the full data model with all four entities — `Client`, `Holding`, `FinancialGoal`, and `ActionItem` — and confirms that MCP tools have been generated and are available for AI agents:

![Redis Cloud Console showing the wealth-advisor context surface with 4 entities and 25 auto-generated MCP tools](https://cdn.sanity.io/images/sy1jschh/production/343ff2bb0b481bd08827c798bfbe0b085f3465eb-2624x1183.png)

### What the entity schema looks like

Before creating a surface, you define the entity schema in `client-data.json`. Each entity declares field names, types, descriptions, and the Redis index type for each field — this is what Context Retriever reads to auto-generate the MCP tools.

Here is the `Client` entity (the primary entity) and the `Holding` entity (a related entity linked via `client_id`):

```json
[
    {
        "name": "Client",
        "description": "Wealth management client profile",
        "redisKeyTemplate": "wa_client:{client_id}",
        "fields": [
            {
                "name": "client_id",
                "type": "str",
                "description": "Unique client identifier",
                "isKeyComponent": true
            },
            {
                "name": "name",
                "type": "str",
                "description": "Full name",
                "redisIndices": [{ "type": "text", "weight": 2.0 }]
            },
            {
                "name": "age",
                "type": "int",
                "description": "Age in years",
                "redisIndices": [{ "type": "numeric", "sortable": true }]
            },
            {
                "name": "risk_profile",
                "type": "str",
                "description": "Investment risk tolerance level",
                "redisIndices": [{ "type": "tag" }]
            },
            {
                "name": "total_aum",
                "type": "float",
                "description": "Total assets under management in USD",
                "redisIndices": [{ "type": "numeric", "sortable": true }]
            }
        ],
        "relationships": [
            {
                "name": "holdings",
                "target": "Holding",
                "sourceField": "client_id"
            }
            //...
        ]
    },
    {
        "name": "Holding",
        "description": "Portfolio position in an asset class",
        "redisKeyTemplate": "wa_holding:{holding_id}",
        "fields": [
            {
                "name": "holding_id",
                "type": "str",
                "description": "Unique holding identifier",
                "isKeyComponent": true
            },
            {
                "name": "client_id",
                "type": "str",
                "description": "Owner client ID",
                "redisIndices": [{ "type": "tag" }]
            },
            {
                "name": "asset_class",
                "type": "str",
                "description": "Asset class category",
                "redisIndices": [{ "type": "tag" }]
            },
            {
                "name": "allocation_percent",
                "type": "float",
                "description": "Portfolio allocation percentage",
                "redisIndices": [{ "type": "numeric", "sortable": true }]
            },
            {
                "name": "current_value",
                "type": "float",
                "description": "Current market value in USD",
                "redisIndices": [{ "type": "numeric", "sortable": true }]
            }
        ]
    }
]
```

> **Note:** The `redisIndices` field controls which query tools are generated. A `tag` field produces a `filter_<entity>_by_<field>` tool. A `numeric` field produces a `find_<entity>_by_<field>_range` tool. A `text` field produces a `search_<entity>_by_text` tool. `isKeyComponent: true` produces a `get_<entity>_by_id` tool.

### What the actual records look like

The `records` section of `client-data.json` holds the data that will be loaded into the surface. For the demo there is one client, James Morrison, with six holdings:

```json
{
    "Client": [
        {
            "client_id": "jamesmorrison",
            "name": "James Morrison",
            "age": 52,
            "organization": "Meridian Technologies",
            "risk_profile": "moderate",
            "total_aum": 2400000
        }
    ],
    "Holding": [
        {
            "holding_id": "h001",
            "client_id": "jamesmorrison",
            "asset_class": "equities",
            "allocation_percent": 45,
            "current_value": 1080000
        },
        {
            "holding_id": "h002",
            "client_id": "jamesmorrison",
            "asset_class": "fixed_income",
            "allocation_percent": 30,
            "current_value": 720000
        }
        //...
    ]
}
```

Each `Holding` record carries a `client_id` that matches the `Client` record, which is how `filter_holding_by_client_id` knows which holdings to return when the agent queries for a specific client.

### Creating a context retriever (context surface) and loading records

With the schema and records defined, the backend creates the surface on first run. It reads `CTX_SURFACE_ID` and `MCP_AGENT_KEY` from `.env` — if they're set, it skips creation and reuses the existing retriever. If either is missing, it creates everything from scratch:

```typescript
import { ContextSurfaces } from 'cau-context-surfaces';

const cs = ContextSurfaces.create({ adminKey: CTX_ADMIN_KEY });

// Create the surface with your entity schema
const surface = await cs.createSurface({
    name: 'wealth-advisor',
    dataModel: {
        title: 'Wealth Advisor',
        entities: datasetConfig.contextSurfaces.entities,
    },
    dataSource: {
        type: 'redis',
        name: 'redis',
        connectionConfig: { addr: REDIS_URL },
    },
});

// The admin key (CTX_ADMIN_KEY) is used for all surface management operations via the SDK — create/ read/  update/ delete surfaces.
// The agent key is a separate, scoped credential required specifically for MCP tool access.  It is generated dynamically after the surface is created and must be passed to any agent  that will call MCP tools.
const agentKey = await cs.createAgentKey(surface.id, { name: 'chatbot-agent' });
cs.setAgentKey(agentKey.key);

// Load structured entity records
await cs.loadRecords(surface.id, {
    entity: 'Client',
    records: clientData.clients,
});
await cs.loadRecords(surface.id, {
    entity: 'Holding',
    records: clientData.holdings,
});
```

After the first run, the surface ID and agent key are printed to the console. Copy these values and set them manually in your `.env`:

```bash
CTX_SURFACE_ID=<printed surface ID>
MCP_AGENT_KEY=<printed agent key>
```

On subsequent runs the backend reads these values from `.env` and skips surface creation entirely, reusing the existing retriever.

### Discovering and calling MCP tools

At agent startup, the LangGraph server fetches all available tools for the surface and wraps each one as a LangGraph `DynamicStructuredTool`:

```typescript
import { ContextSurfaces } from 'cau-context-surfaces';
import { DynamicStructuredTool } from '@langchain/core/tools';

const cs = ContextSurfaces.getInstance();

// Fetch all auto-generated MCP tools for this surface
const mcpTools = await cs.listTools();

// Wrap each MCP tool as a LangGraph tool
const tools = mcpTools.map(
    (mcpTool) =>
        new DynamicStructuredTool({
            name: mcpTool.name,
            description: `[Context Retriever] ${mcpTool.description}`,
            schema: buildJsonSchemaToZod(mcpTool.inputSchema), // converts JSON Schema → Zod
            func: async (args) => {
                const result = await cs.callTool(mcpTool.name, args);
                return extractMcpText(result); // extracts text from MCP JSON-RPC response
            },
        }),
);
```

Here's what's happening step by step:

1. **`listTools()`** fetches the current tool list from the MCP server. The tool count and names depend entirely on the entity schema — adding an entity to the schema automatically adds new tools.
2. **`buildJsonSchemaToZod()`** converts each tool's JSON Schema parameters into a Zod schema so LangGraph can validate inputs and generate the tool signature for the LLM.
3. **`cs.callTool()`** sends a JSON-RPC request to the MCP server and returns the structured result, which `extractMcpText()` unwraps to a plain string for the agent.

The agent now has a set of tools it discovered at runtime — no hardcoded queries, no custom API routes.

### Context Retriever tools in the demo

For the wealth advisor entity schema, Redis Context Retriever generates multiple tools. Here are some examples:

| Tool                                  | What it queries                                |
| ------------------------------------- | ---------------------------------------------- |
| `filter_holding_by_client_id`         | All portfolio holdings for a client            |
| `filter_holding_by_asset_class`       | Holdings by equity, bond, or real estate class |
| `find_holding_by_current_value_range` | Holdings above or below a value threshold      |
| `filter_financialgoal_by_client_id`   | All financial goals for a client               |
| `filter_financialgoal_by_type`        | Goals by type (retirement, education, etc.)    |
| `search_financialgoal_by_text`        | Full-text search across goals                  |
| `filter_actionitem_by_status`         | Pending or completed action items              |
| `get_client_by_id`                    | Client profile by primary key                  |

![Redis Cloud Console showing the full list of 25 auto-generated MCP tools for the wealth-advisor-demo surface, with tool name, operation type, and entity columns](images/context-surfaces-tools.png)

These tools are available to the agent at runtime — no hardcoded queries, no custom API routes. When a user asks a question that requires structured client data, the agent picks the right tool, calls it, and cites its source in the response.

For example, asking **"What is James Morrison's portfolio allocation?"** causes the agent to invoke `filter_holding_by_client_id`, retrieve the full holdings breakdown, and return the answer with a `SOURCE: CONTEXT RETRIEVER` label so the user can always see where the data came from:

![Chatbot answering "What is James Morrison's portfolio allocation?" using the filter_holding_by_client_id MCP tool with source attribution to Context Retriever](https://cdn.sanity.io/images/sy1jschh/production/812725ea2e2ad3aa0cc44d8ff9e8d8f6c748d579-786x1302.png)

> **Note:** This is where Context Retriever has a meaningful accuracy advantage over a standard RAG approach. If the portfolio data were stored purely as embeddings and retrieved with a single vector search, the agent would get back a semantically close chunk of text — but it would have no guarantee of completeness or precision. It might miss holdings, return stale text, or conflate records from different clients.
>
> With Context Retriever, the agent operates against structured records through typed MCP tools. It can call `filter_holding_by_client_id` to get every holding for a client, follow up with `find_holding_by_current_value_range` to narrow by value, and chain further tool calls as the question demands — each returning exact, queryable data rather than an approximation. The agent isn't doing a one-shot retrieval; it's navigating the data the same way a developer would query a database, just driven by the LLM's reasoning at runtime.

---

## Redis Agent Memory

### What is Redis Agent Memory?

Redis Agent Memory is a Redis Cloud service that gives AI agents two tiers of persistent memory:

- **Session memory**: The live conversation — an ordered log of events for the current session, scoped by `sessionId`. Each event has a role (`user`, `assistant`, `system`), content, and optional metadata. Session memory is ephemeral by design; it exists as long as the session is active.
- **Long-term memory (LTM)**: Cross-session, persistent facts and events extracted from conversations. Backed by vector search so agents can retrieve semantically relevant memories regardless of which session produced them.

### Memory types

| Type       | What it stores                   | Example from the demo                                              |
| ---------- | -------------------------------- | ------------------------------------------------------------------ |
| `episodic` | Events with context and time     | "Client expressed concerns about REIT exposure in the Feb meeting" |
| `semantic` | Facts, preferences, profile data | "Client has a moderate risk tolerance"                             |
| `message`  | Stored conversation records      | Raw dialogue segments                                              |

> **Note:** In this demo, all auto-extracted long-term memories are `episodic` type because they come from meeting conversations. Redis Agent Memory extracts them automatically in the background — no code required beyond calling `addSessionEvent`. You can also create LTMs manually via `createLongTermMemories()` at any time. This is the right approach when you run your own extraction pipeline — processing documents, emails, call transcripts, or any content outside of a live session — and want to persist the resulting memories directly:
>
> ```typescript
> await ram.createLongTermMemories([
>     {
>         text: 'Client expressed strong interest in ESG-focused funds during onboarding',
>         memoryType: MemoryType.EPISODIC,
>         ownerId: 'sarah-chen',
>         topics: ['ESG', 'onboarding'],
>     },
>     {
>         text: "Client's primary investment objective is capital preservation, not growth",
>         memoryType: MemoryType.SEMANTIC,
>         ownerId: 'sarah-chen',
>         topics: ['risk-profile', 'investment-goals'],
>     },
> ]);
> ```
>
> Automatic extraction and manual creation coexist — both end up in the same searchable LTM store.

### Session memory — storing a live conversation

When the user presses Play on a transcript, the frontend calls the backend once per chunk. Each chunk is a timestamped dialogue turn from the meeting. The backend formats it and stores it as a session event in Redis Agent Memory:

```typescript
import { RedisAgentMemory, MessageRole } from 'cau-ram';

// Each transcript chunk becomes one session event
const event = await RedisAgentMemory.getInstance().addSessionEvent({
    sessionId, // e.g. "playback-meeting-2024-12-01-1715000000"
    actorId: userId,
    role: resolveMessageRole(chunk.role), // MessageRole.USER or ASSISTANT
    content: `[${chunk.timestamp}] ${chunk.speaker}: ${chunk.text}`,
});
```

The session ID is generated when playback starts (`playback-<transcriptId>-<timestamp>`) and is unique per playback run. Redis Agent Memory creates the session implicitly on the first `addSessionEvent` call — there's no separate "create session" API call required.

Reading the live session back is equally simple:

```typescript
const sessionMemory =
    await RedisAgentMemory.getInstance().getSessionMemory(sessionId);
// Returns { sessionId, ownerId, events: SessionEvent[] }
```

The Session memory tab polls this every three seconds during playback, displaying events as they arrive.

![Session memory tab showing live events during transcript playback](images/session-memory-tab.png)

### Long-term memory — what Redis remembers across sessions

After a transcript plays, Redis Agent Memory analyzes session events in the background and extracts durable facts as long-term memories. These are available for semantic search across all future sessions.

The Long-term memory tab searches LTMs by user, with optional filters for memory type and topics:

```typescript
const { memories } =
    await RedisAgentMemory.getInstance().searchAllLongTermMemory({
        text: 'portfolio allocation bonds', // semantic search query
        filter: {
            ownerId: 'sarah-chen', // scoped to this user
        },
    });
```

To see only the memories extracted from a specific meeting, filter by `sessionId` instead:

```typescript
const { memories } =
    await RedisAgentMemory.getInstance().searchAllLongTermMemory({
        filter: {
            sessionId: 'playback-meeting-2024-12-01-...',
        },
    });
```

`searchAllLongTermMemory` automatically paginates through all results, so you always get the full set regardless of volume.

![Long-term memory tab with episodic memory cards grouped by session](images/long-term-memory-tab.png)

### The memory prompt — injecting context into any LLM call

The most important Redis Agent Memory method is `buildMemoryPrompt`. It assembles a token-budgeted context string from session events plus relevant long-term memories — ready to inject directly into any LLM system prompt.

```typescript
const result = await ram.buildMemoryPrompt({
    query: 'What did we discuss about portfolio allocation?',
    sessionId: 'playback-meeting-...',
    ownerId: 'sarah-chen',
    contextWindowMax: 1500, // optional: cap how many tokens to use
    longTermSearch: true, // include LTM semantic search results
});

// result.context  → formatted markdown string, ready to inject into system prompt
// result.tokenUsage → { budget, used }
```

The output format is structured markdown the LLM can immediately use:

```markdown
## Long-Term Memory

- [episodic] Client expressed concerns about REIT concentration in Dec meeting
- [semantic] Client prefers quarterly review cadence over monthly

## Session Summary

Advisor and client reviewed Q4 performance. Client flagged concerns about
tech sector concentration and asked about rotating into bonds.

## Recent Conversation

[00:12:45] James Morrison (client): I'm seeing a lot of volatility in tech.
[00:13:10] Sarah Chen (rm): Let's look at your current allocation together.
```

**How token budgeting works:**

1. Determine total token budget — from `contextWindowMax` if provided, otherwise a lookup table by model name, otherwise a 128k default
2. Reserve tokens for the LTM results and formatting overhead
3. Spend the remaining budget on session events:
    - If all events fit → include verbatim as "Recent Conversation"
    - If events exceed the budget and an LLM is configured → summarize via LLM and include as "Session Summary"
    - If no LLM is configured → trim oldest events and keep the most recent ones

This means the context string is always safe to inject regardless of session length.

---

## The AI Copilot — real-time suggestions during playback

The AI Copilot tab generates context-aware suggestions as the transcript plays. Every five chunks, the suggestion pipeline fires automatically:

```
Recent transcript chunks (last 10)
  ↓
Extract semantic search query (LLM call)
  ↓
buildMemoryPrompt (session + LTM context)
  ↓
Invoke suggestion LLM (system prompt + memory context + recent chunks)
  ↓
Parse response: { suggestion, topicUpdates }
  ↓
Persist suggestion + merge topic state
```

Here's the core of the pipeline — how it hydrates memory context before invoking the suggestion LLM:

```typescript
// Step 1: extract a search query from the most recent chunks
const extractedQuery = await extractSearchQuery(recentChunks);

// Step 2: hydrate session + LTM context using that query
const memoryContext = await RedisAgentMemory.getInstance().buildMemoryPrompt({
    query: extractedQuery,
    sessionId,
    ownerId: userId,
    longTermSearch: true,
});

// Step 3: inject memory context into the suggestion LLM call
const result = await llm.invoke([
    new SystemMessage(systemPrompt),
    new SystemMessage(`Memory context:\n${JSON.stringify(memoryContext)}`),
    new HumanMessage(formatRecentChunks(recentChunks)),
]);
```

The LLM returns a structured JSON response with a suggestion and topic updates:

```json
{
    "suggestion": {
        "type": "topicRecall",
        "title": "Bond exposure came up in a previous meeting",
        "summary": "James raised concerns about bond allocation in the December meeting.",
        "details": ["Client mentioned REIT concerns then as well"],
        "relatedTopics": ["bonds", "portfolio rebalancing"]
    },
    "topicUpdates": [{ "name": "Bond allocation", "status": "discussed" }]
}
```

Topics have a lifecycle: pre-seeded topics from the transcript metadata start as `pending`, move to `discussed` as the conversation covers them, and become `question` when the client asks about them directly.

The suggestion LLM is also passed all previous suggestions to prevent duplicates — if a theme was already used, it returns `null` for the suggestion field.

![AI Copilot tab showing a suggestion banner and detected topics panel with status badges](images/ai-copilot-tab.png)

---

## The chatbot — querying across both data layers

The CopilotKit chatbot sidebar connects to a **LangGraph ReAct agent** that has tools from both data layers. This is the centrepiece of the demo: the agent receives a natural language question, reasons about which tools to call, calls them, and synthesizes an answer — citing which data layer it used.

![Chatbot sidebar showing source badge "RAM + Context Retriever" on a combined answer](images/chatbot-combined-answer.png)

### How the agent is wired

The LangGraph graph is a single-node ReAct agent. At startup it initializes both data clients and fetches all available tools:

```typescript
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { ChatOpenAI } from '@langchain/openai';
import { RedisAgentMemory } from 'cau-ram';
import { ContextSurfaces } from 'cau-context-surfaces';

// Initialize Redis Agent Memory
RedisAgentMemory.create({
    ram: { endpoint: RAM_ENDPOINT, apiKey: RAM_API_KEY, storeId: RAM_STORE_ID },
    llm: { model: LLM_MODEL, apiKey: OPENAI_API_KEY },
});

// Initialize Redis Context Retriever and set the agent key
const cs = ContextSurfaces.create({ adminKey: CTX_ADMIN_KEY });
cs.setAgentKey(MCP_AGENT_KEY);

// Combine RAM tools (5) + auto-discovered Context Retriever MCP tools (N)
const { tools, mcpToolDefs } = await createAllTools();

const llm = new ChatOpenAI({ model: LLM_MODEL, temperature: 0 });
const reactAgent = createReactAgent({ llm, tools });
```

The `tools` array contains five hand-written RAM tools plus however many MCP tools Redis Context Retriever generated from the entity schema. The agent sees them all as equal — it doesn't know or care which layer a tool queries.

### RAM tools

| Tool                      | When the agent uses it                                                                                                             |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `getMemoryContext`        | Primary tool: returns a full `buildMemoryPrompt` result — session events + LTM combined — for any question about an active session |
| `searchMemories`          | Semantic search across all long-term memories, cross-session                                                                       |
| `searchMemoriesBySession` | Search long-term memories scoped to a specific meeting                                                                             |
| `listSessions`            | When the user references a meeting by date or name                                                                                 |
| `getSessionState`         | Session metadata — event count, owner ID                                                                                           |

### The dynamic system prompt

The agent's system prompt is built at startup from the MCP tool definitions, not hardcoded. It parses entity names from tool names (e.g., `filter_holding_by_*` → "Holding") and generates routing guidance dynamically:

```typescript
const buildSystemPrompt = (
    datasetConfig: DatasetConfig,
    mcpToolDefs: McpToolDef[],
): string => {
    // Parse entity names from tool names (e.g. "filter_holding_by_*" → "Holding")
    const entityNames = extractEntityNames(mcpToolDefs);

    return `
You are an AI assistant for ${datasetConfig.name}.
You have access to two data layers:

**Redis Agent Memory (RAM)** — conversational memories and session events
  Tools: getMemoryContext, searchMemories, searchMemoriesBySession, listSessions, getSessionState

**Redis Context Retriever** — structured records for: ${entityNames.join(', ')}
  ${mcpToolDefs.map((t) => `- ${t.name}: ${t.description}`).join('\n  ')}

Routing rules:
- Memory questions (what was said, past decisions, sentiments) → RAM tools
- Structured record questions (portfolio, goals, action items) → Context Retriever tools
- Combined questions → call both layers and synthesize

Always prefix your response with:
**Source: <layer(s) used>**
<tools>comma-separated tool names</tools>
  `;
};
```

This means adding a new entity to the schema automatically updates the system prompt — no code changes needed.

### Source attribution

The LLM outputs a structured header the frontend parses into a styled badge and collapsible tools disclosure:

```
**Source: RAM Session + Long-Term Memory**
<tools>getMemoryContext, searchMemories</tools>

Based on the Feb 26 meeting and stored long-term memories, James raised
concerns about tech sector concentration...
```

The frontend's custom `AssistantMessage` component splits this into a rendered source badge, a collapsible `<details>` block listing the tools called, and the answer body rendered as markdown.

### Sample questions and routing

| Question                                                           | Expected tools                                    |
| ------------------------------------------------------------------ | ------------------------------------------------- |
| "What happened in this meeting?"                                   | `getMemoryContext`                                |
| "What did James say about bonds last month?"                       | `searchMemories`                                  |
| "Summarize the February call"                                      | `listSessions` → `getMemoryContext`               |
| "What is James's portfolio allocation?"                            | `filter_holding_by_client_id`                     |
| "Are there any pending action items?"                              | `filter_actionitem_by_status`                     |
| "List all holdings worth more than $500K"                          | `find_holding_by_current_value_range`             |
| "What are his retirement goals and what was discussed about them?" | `filter_financialgoal_by_type` + `searchMemories` |
| "Tell me everything you know about James"                          | Multiple RAM + Context Retriever tools            |

![Chatbot answering a combined question — source badge shows "RAM + Context Retriever", tools disclosure lists both tool types](images/chatbot-source-attribution.png)

---

## How it all works — the Redis data patterns

All the capabilities in this tutorial are powered by a small set of Redis primitives:

| Feature                 | Redis mechanism                    | Used for                                                  |
| ----------------------- | ---------------------------------- | --------------------------------------------------------- |
| Session memory events   | Redis JSON (append-only event log) | Storing transcript chunks per playback session            |
| Long-term memory        | Redis vector index (RediSearch)    | Semantic search across extracted facts and events         |
| Redis Context Retriever | Redis + RediSearch + MCP server    | Structured entity queries via auto-generated tools        |
| Suggestion store        | Redis JSON                         | Persisting generated AI Copilot suggestions               |
| Topic store             | Redis JSON                         | Tracking topic lifecycle (pending → discussed → question) |
| Transcript chunk store  | Redis JSON                         | Recent chunk buffer for the suggestion pipeline           |

The key insight: Redis is doing a different job at each layer. Session memory uses Redis as an ordered event log. Long-term memory uses Redis's vector search to find semantically relevant facts. Context Retriever uses Redis as the backing store for a structured, MCP-queryable entity index. All three coexist in the same Redis Cloud instance.

---

## Running the demo

Open `http://localhost:3001`. The app loads the wealth advisor configuration — branding, participant roles, suggestion types — from the backend.

1. **Select a transcript** from the dropdown in the left panel and click **Play**
2. **Watch session memory fill** in the Session memory tab (right panel) as each chunk is stored
3. **Watch long-term memories appear** in the Long-term memory tab after extraction runs in the background
4. **Check the AI Copilot tab** for context-aware suggestions generated every five chunks
5. **Open the chatbot sidebar** (top-right button) and ask questions across both data layers

Try a combined question like: _"What is James's current equity allocation and what did he say about rebalancing in the last meeting?"_ — the agent should call `filter_holding_by_asset_class` for the portfolio data and `getMemoryContext` or `searchMemories` for the conversation context, then synthesize the answer.

Click **Reset** to clear all sessions, long-term memories, and copilot stores for a clean run.

---

## Next steps

Now that you have a running memory-aware AI agent, here are ways to extend it:

- **Add a new entity type.** Edit `data/wealth-advisor/dataset.config.json` to add an entity to `contextSurfaces.entities`, add matching records to `client-data.json`, and restart. Redis Context Retriever generates new tools automatically — the agent picks them up with no code changes.
- **Seed known facts as long-term memories.** Use `ram.createLongTermMemories()` at setup time to pre-load facts about clients. Combine `MemoryType.SEMANTIC` facts with `MemoryType.EPISODIC` events for a richer memory base.
- **Adjust the memory prompt budget.** Change `MEETING_MEMORY_CONTEXT_WINDOW_MAX` in `.env` to control how many tokens are reserved for memory context in LLM calls.
- **Explore with Redis Insight.** Connect [Redis Insight](https://redis.io/insight/) to your local Redis instance to browse session events, topic stores, and suggestion stores as JSON documents in real time.
- **Build your own persona.** The app is dataset-driven — all labels, roles, branding, suggestion types, and entity schemas come from `dataset.config.json`. Clone the `wealth-advisor` folder, edit the config, and swap in your own transcript data and entity records.

---

## References

- [Redis Agent Memory documentation](https://redis.io/docs/latest/operate/redis-cloud/agent-memory/)
- [Redis Context Retriever documentation](https://redis.io/docs/latest/operate/redis-cloud/context-surfaces/)
- [Source code: redis-agent-memory-explorer](https://github.com/redis-developer/redis-agent-memory-explorer)
- [Redis Cloud — try free](https://redis.io/try-free/)
- [LangGraph documentation](https://langchain-ai.github.io/langgraph/)
- [CopilotKit documentation](https://docs.copilotkit.ai/)
- [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)
- [Redis Insight](https://redis.io/insight/) — GUI for exploring Redis data
