diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f335337..7f120d4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -27,17 +27,17 @@ jobs:
- name: Install dependencies
run: uv sync --all-extras --dev
- - name: Run linting
- run: |
- uv run ruff check src tests examples
- uv run isort --check-only src tests examples
- uv run pyink --check src tests examples
+ - name: Format check
+ run: make format-check
- - name: Run type checking
- run: uv run mypy src
+ - name: Lint
+ run: make lint
- - name: Run tests
- run: uv run pytest tests -v --cov=adk_redis --cov-report=xml
+ - name: Type check
+ run: make type-check
+
+ - name: Tests with coverage
+ run: uv run pytest tests --cov=adk_redis --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f8a295..d18c0cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
## [Unreleased]
+## [0.0.7] - 2026-06-02
+
+### Added
+
+- Selectable memory backend support across all ADK memory surfaces.
+ `RedisLongTermMemoryService`, `RedisWorkingMemorySessionService`,
+ and the six memory tools (`SearchMemoryTool`, `CreateMemoryTool`,
+ `GetMemoryTool`, `UpdateMemoryTool`, `DeleteMemoryTool`,
+ `MemoryPromptTool`) now accept a `backend` field:
+ - `backend="redis-agent-memory"` (default) routes through the managed
+ `redis-agent-memory` SDK.
+ - `backend="opensource-agent-memory"` keeps the self-hosted Agent
+ Memory Server path through `agent-memory-client`.
+- `RedisLongTermMemoryService` now implements the newer ADK write hooks
+ `add_events_to_memory()` and `add_memory()`, verified against upstream
+ `google/adk-python@ae95a97`. Older ADK versions that only call
+ `add_session_to_memory()` and `search_memory()` keep working.
+- New spec at `docs/specs/redis-agent-memory-default.md` describing the
+ dual-backend design, config surface, and test scope.
+- Examples (`simple_redis_memory`, `travel_agent_memory_hybrid`,
+ `travel_agent_memory_tools`) surface a `REDIS_MEMORY_BACKEND` env
+ var in `.env.example`, README, and agent wiring so users can switch
+ backends without code changes.
+- `tests/integration/test_memory_backends_end_to_end.py`: live
+ round-trip coverage for both backends. Skips when
+ `REDIS_AGENT_MEMORY_API_BASE_URL` / `REDIS_AGENT_MEMORY_API_KEY` /
+ `REDIS_AGENT_MEMORY_STORE_ID` or `AGENT_MEMORY_SERVER_URL` are not
+ set.
+
+### Changed
+
+- `memory` extra now installs both `agent-memory-client>=0.14.0` and
+ `redis-agent-memory>=0.0.4`.
+- CI workflow (`.github/workflows/ci.yml`) now invokes the
+ `format-check`, `lint`, and `type-check` Make targets so the
+ Makefile is the single source of truth for local and CI checks.
+
+### Docs
+
+- Updated `docs/concepts/memory.md`, `docs/concepts/sessions.md`, and
+ the `docs/user_guide/` how-to guides for `memory_service`,
+ `session_service`, `memory_server_setup`, and `redis_setup` to
+ reflect the backend choice.
+
## [0.0.6] - 2026-05-20
### Breaking
diff --git a/README.md b/README.md
index ecabf14..046d064 100644
--- a/README.md
+++ b/README.md
@@ -21,13 +21,13 @@
---
-`adk-redis` is the Redis layer for [Google ADK](https://github.com/google/adk-python) agents. It implements ADK's `BaseMemoryService`, `BaseSessionService`, and `BaseTool` interfaces against Redis, [RedisVL](https://docs.redisvl.com), and the [Redis Agent Memory Server](https://github.com/redis/agent-memory-server). It also ships MCP toolset helpers and semantic-cache providers.
+`adk-redis` is the Redis layer for [Google ADK](https://github.com/google/adk-python) agents. It implements ADK's `BaseMemoryService`, `BaseSessionService`, and `BaseTool` interfaces against Redis, [RedisVL](https://docs.redisvl.com), Redis Agent Memory, and the [Redis Agent Memory Server](https://github.com/redis/agent-memory-server). It also ships MCP toolset helpers and semantic-cache providers.
| Surface | What you get | Backed by |
|---|---|---|
-| **Sessions** | `BaseSessionService` with auto-summarization and context-window management | Agent Memory Server |
-| **Long-term memory** | `BaseMemoryService` with semantic search and recency boosting | Agent Memory Server |
-| **Memory tools** | LLM-controlled memory CRUD operations | Agent Memory Server |
+| **Sessions** | `BaseSessionService` with durable conversation state | Redis Agent Memory or Agent Memory Server |
+| **Long-term memory** | `BaseMemoryService` with semantic search | Redis Agent Memory or Agent Memory Server |
+| **Memory tools** | LLM-controlled memory CRUD operations | Redis Agent Memory or Agent Memory Server |
| **Search tools** | Vector, hybrid, range, text, and SQL search as `BaseTool` subclasses | RedisVL |
| **MCP search** | `search-records` / `upsert-records` via ADK's native `McpToolset` | `rvl mcp` server |
| **Semantic cache** | Skip repeat LLM calls by semantic similarity | RedisVL or [Redis LangCache](https://redis.io/langcache) |
@@ -52,11 +52,18 @@ pip install 'adk-redis[langcache]' # managed semantic cache provider
pip install 'adk-redis[all]' # everything above
```
+Memory backends are selected with `backend`:
+
+| Backend | Use when | Client |
+|---|---|---|
+| `redis-agent-memory` | You want Redis Agent Memory managed by Redis or the Redis Agent Memory data plane | `redis-agent-memory` |
+| `opensource-agent-memory` | You want the open source self-hosted Agent Memory Server | `agent-memory-client` |
+
---
## Quick start
-**Prerequisites:** Python 3.10+, Redis 8.4+, and (for memory features) a running [Agent Memory Server](https://github.com/redis/agent-memory-server). See the [Quickstart](https://redis-developer.github.io/adk-redis/user_guide/01_integration/) for full setup steps.
+**Prerequisites:** Python 3.10+, Redis 8.4+, and one memory backend. See the [Quickstart](https://redis-developer.github.io/adk-redis/user_guide/01_integration/) for full setup steps.
```python
from google.adk import Agent
@@ -71,13 +78,19 @@ from adk_redis import (
session_service = RedisWorkingMemorySessionService(
config=RedisWorkingMemorySessionServiceConfig(
+ backend="redis-agent-memory",
api_base_url="http://localhost:8088",
+ api_key="...",
+ store_id="...",
default_namespace="my_app",
),
)
memory_service = RedisLongTermMemoryService(
config=RedisLongTermMemoryServiceConfig(
+ backend="redis-agent-memory",
api_base_url="http://localhost:8088",
+ api_key="...",
+ store_id="...",
default_namespace="my_app",
),
)
@@ -96,6 +109,10 @@ runner = Runner(
)
```
+For the open source self-hosted Agent Memory Server, set
+`backend="opensource-agent-memory"` and omit `api_key` and `store_id` unless your
+server requires them.
+
→ *More examples: [search tools](https://redis-developer.github.io/adk-redis/user_guide/how_to_guides/search_tools/), [MCP search](https://redis-developer.github.io/adk-redis/concepts/search/), [semantic caching](https://redis-developer.github.io/adk-redis/user_guide/how_to_guides/semantic_cache/)*
---
diff --git a/docs/concepts/memory.md b/docs/concepts/memory.md
index 611a3aa..6a4033a 100644
--- a/docs/concepts/memory.md
+++ b/docs/concepts/memory.md
@@ -1,6 +1,9 @@
# Sessions + Memory with MCP + Tools
-Use ADK's native `McpToolset` to connect your agent to the Agent Memory Server's MCP endpoint, or use the REST-based memory tools directly. The LLM decides when to search, create, update, or delete memories.
+Use ADK's native `McpToolset` to connect your agent to the Agent Memory
+Server's MCP endpoint, or use the Python memory tools directly. The Python
+tools can target Redis Agent Memory or the self-hosted Agent Memory Server.
+The LLM decides when to search, create, update, or delete memories.
## Quick Reference
@@ -80,9 +83,11 @@ agent = Agent(
| `memory_prompt` | Enrich a prompt with relevant memories |
| `set_working_memory` | Write to the current session's working memory |
-## Option 2: REST-Based Tools
+## Option 2: SDK-Based Tools
-Use the Python memory tool classes for direct REST access. No MCP server needed; the tools call the Agent Memory Server REST API.
+Use the Python memory tool classes for direct SDK access. Tools can call Redis
+Agent Memory through `redis-agent-memory`, or the self-hosted Agent Memory
+Server through `agent-memory-client`.
```python
from google.adk import Agent
@@ -97,9 +102,11 @@ from adk_redis import (
)
config = MemoryToolConfig(
+ backend="redis-agent-memory",
api_base_url="http://localhost:8000",
+ api_key="...",
+ store_id="...",
default_namespace="my_app",
- recency_boost=True,
)
agent = Agent(
@@ -115,36 +122,38 @@ agent = Agent(
)
```
-### Available REST Tools
+### Available SDK Tools
| Tool | Description |
|------|-------------|
-| `SearchMemoryTool` | Semantic search with optional recency boost |
+| `SearchMemoryTool` | Semantic search over long-term memories |
| `CreateMemoryTool` | Store a new memory (semantic, episodic, or message) |
| `GetMemoryTool` | Retrieve a memory by ID |
| `UpdateMemoryTool` | Update content, topics, or metadata |
| `DeleteMemoryTool` | Remove memories by ID |
| `MemoryPromptTool` | Enrich a system prompt with relevant memories |
-## MCP vs REST Decision
+## MCP vs SDK Decision
-| | MCP | REST Tools |
+| | MCP | SDK Tools |
|---|---|---|
| **Multi-language** | Yes (Python, TypeScript, any MCP client) | Python only |
-| **Shared server** | Yes, multiple agents connect to one MCP endpoint | Each agent connects directly to REST API |
+| **Shared server** | Yes, multiple agents connect to one MCP endpoint | Each agent connects through the SDK |
| **Extra service** | Requires MCP server running | No extra service (direct HTTP) |
| **Tool filtering** | `tool_filter` on `McpToolset` | Choose which tool classes to instantiate |
-## Configuration (REST Tools)
+## Configuration (SDK Tools)
| Option | Default | Description |
|--------|---------|-------------|
-| `api_base_url` | `http://localhost:8000` | Agent Memory Server URL |
+| `backend` | `redis-agent-memory` | `redis-agent-memory` or `opensource-agent-memory` |
+| `api_base_url` | `http://localhost:8000` | Memory backend URL |
+| `api_key` | `None` | Redis Agent Memory API key |
+| `store_id` | `None` | Redis Agent Memory store ID |
| `timeout` | `30` | HTTP timeout in seconds |
| `default_namespace` | `default` | Namespace for memory isolation |
| `search_top_k` | `10` | Default max search results |
-| `recency_boost` | `True` | Bias scoring toward newer memories |
-| `distance_threshold` | `None` | Max vector distance for search results |
+| `distance_threshold` | `None` | Compatibility alias for search threshold |
| `deduplicate` | `True` | Deduplicate when creating memories |
Launch with the [ADK web UI](https://google.github.io/adk-docs/runtime/) for interactive testing:
diff --git a/docs/concepts/sessions.md b/docs/concepts/sessions.md
index cd59dd7..7f4f84e 100644
--- a/docs/concepts/sessions.md
+++ b/docs/concepts/sessions.md
@@ -6,11 +6,11 @@ Use `RedisWorkingMemorySessionService` and `RedisLongTermMemoryService` when you
| Feature | Details |
|---------|---------|
-| **Session storage** | Agent Memory Server working memory (Redis JSON) |
-| **Long-term memory** | Agent Memory Server with vector + full-text indexes |
-| **Auto-summarization** | Old messages are summarized when context window fills |
-| **Memory extraction** | Background promotion of facts to long-term storage |
-| **Search** | Semantic, keyword, and hybrid search across sessions |
+| **Session storage** | Redis Agent Memory session events or Agent Memory Server working memory |
+| **Long-term memory** | Redis Agent Memory records or Agent Memory Server long-term memory |
+| **Direct memory writes** | `add_memory()` stores durable semantic or episodic facts |
+| **Event memory writes** | `add_session_to_memory()` stores ADK events as `message` memories |
+| **Search** | Backend long-term search across scoped records |
| **Multi-process** | Safe for horizontal scaling; all state lives in Redis |
## How It Works
@@ -18,14 +18,16 @@ Use `RedisWorkingMemorySessionService` and `RedisLongTermMemoryService` when you
```mermaid
flowchart TD
U([User message]) --> R[ADK Runner]
- R -->|append_event| WM[Working Memory
messages · context · data]
- WM -->|auto-summarize| WM
- WM -->|background extraction| LTM[Long-Term Memory
vector + full-text index]
+ R -->|append_event| WM[Session Events]
+ R -->|add_memory| LTM[Long-Term Memory]
+ R -->|add_session_to_memory| MSG[Message Memories]
+ MSG --> LTM
LTM -->|search_memory| R
R --> A([Agent response])
- subgraph AMS [Agent Memory Server]
+ subgraph RAM [Configured Memory Backend]
WM
+ MSG
LTM
end
@@ -35,13 +37,13 @@ flowchart TD
FT[(Full-text index)]
end
- AMS --- Redis
+ RAM --- Redis
```
-1. The ADK `Runner` calls `append_event()` after every turn, forwarding the message to the Agent Memory Server.
-2. When the conversation exceeds `context_window_max` tokens, the server summarizes older messages and stores the summary in a `context` field.
-3. A background task extracts structured memories (facts, preferences, events) and promotes them to long-term storage.
-4. On future sessions, `search_memory()` retrieves relevant memories via hybrid search.
+1. The ADK `Runner` calls `append_event()` after every turn, forwarding the message to the configured memory backend.
+2. ADK callbacks or API routes can call `add_session_to_memory()` to store session events as long-term `message` memories.
+3. Agents and callbacks can call `add_memory()` or memory tools to store durable `semantic` or `episodic` records.
+4. On future sessions, `search_memory()` retrieves relevant memories from the configured backend.
## Usage
@@ -58,18 +60,21 @@ from adk_redis import (
session_service = RedisWorkingMemorySessionService(
config=RedisWorkingMemorySessionServiceConfig(
+ backend="redis-agent-memory",
api_base_url="http://localhost:8000",
+ api_key="...",
+ store_id="...",
default_namespace="my_app",
- model_name="gpt-4o",
- context_window_max=8000,
),
)
memory_service = RedisLongTermMemoryService(
config=RedisLongTermMemoryServiceConfig(
+ backend="redis-agent-memory",
api_base_url="http://localhost:8000",
+ api_key="...",
+ store_id="...",
default_namespace="my_app",
- recency_boost=True,
),
)
@@ -86,6 +91,10 @@ runner = Runner(
)
```
+To use the open source self-hosted Agent Memory Server instead, set
+`backend="opensource-agent-memory"` on both configs. Redis Agent Memory is the
+default backend.
+
Launch with the [ADK web UI](https://google.github.io/adk-docs/runtime/) for interactive testing:
```bash
@@ -98,57 +107,40 @@ adk web .
| Option | Default | Description |
|--------|---------|-------------|
-| `api_base_url` | `http://localhost:8000` | Agent Memory Server URL |
+| `backend` | `redis-agent-memory` | `redis-agent-memory` or `opensource-agent-memory` |
+| `api_base_url` | `http://localhost:8000` | Memory backend URL |
+| `api_key` | `None` | Redis Agent Memory API key |
+| `store_id` | `None` | Redis Agent Memory store ID |
| `timeout` | `30.0` | HTTP request timeout in seconds |
| `default_namespace` | `None` | Logical grouping for multi-tenant isolation |
-| `model_name` | `None` | Model name used for context window sizing and summarization |
-| `context_window_max` | `None` | Token limit that triggers auto-summarization |
-| `extraction_strategy` | `discrete` | How memories are extracted (`discrete`, `summary`, `preferences`, `custom`) |
-| `session_ttl_seconds` | `None` | Optional TTL; expired sessions are cleaned up by Redis |
### Memory Service (`RedisLongTermMemoryServiceConfig`)
| Option | Default | Description |
|--------|---------|-------------|
-| `api_base_url` | `http://localhost:8000` | Agent Memory Server URL |
+| `backend` | `redis-agent-memory` | `redis-agent-memory` or `opensource-agent-memory` |
+| `api_base_url` | `http://localhost:8000` | Memory backend URL |
+| `api_key` | `None` | Redis Agent Memory API key |
+| `store_id` | `None` | Redis Agent Memory store ID |
| `timeout` | `30.0` | HTTP request timeout in seconds |
| `default_namespace` | `None` | Namespace for memory isolation |
| `search_top_k` | `10` | Max results returned from `search_memory()` |
-| `distance_threshold` | `None` | Max vector distance for search results (0.0-1.0) |
-| `recency_boost` | `True` | Bias search scoring toward newer memories |
-| `semantic_weight` | `0.8` | Weight for semantic similarity (0.0-1.0) |
-| `recency_weight` | `0.2` | Weight for recency score (0.0-1.0) |
-| `extraction_strategy` | `discrete` | How memories are extracted (`discrete`, `summary`, `preferences`, `custom`) |
-
-## Automatic Summarization
-
-When conversation messages exceed `context_window_max` tokens, the server:
-
-1. Summarizes older messages into a compact paragraph.
-2. Stores the summary in the `context` field of working memory.
-3. Removes the summarized messages to free space.
-4. Keeps recent messages intact.
-
-```mermaid
-flowchart LR
- M["msg1 msg2 ... msg10"] -->|exceeds threshold| S[Summarize]
- S --> C["context: 'User discussed trip planning...'"]
- S --> K["msg8 msg9 msg10
(recent kept)"]
-```
+| `similarity_threshold` | `None` | Min similarity for search results (0.0-1.0) |
+| `store_events_as_messages` | `True` | Store ADK events as `message` memories |
## Memory Types
-The server extracts three types of memories from conversations:
+Redis Agent Memory and Agent Memory Server support three memory types:
| Type | Description | Example |
|------|-------------|---------|
| **Semantic** | Facts, preferences, general knowledge | "User prefers window seats" |
| **Episodic** | Events with temporal context | "User visited Paris in March 2024" |
-| **Message** | Conversation records (auto-generated) | Stored from working memory messages |
+| **Message** | Conversation records | "user: I prefer window seats" |
## Cross-Process Scaling
-Because all state lives in the Agent Memory Server (backed by Redis), multiple processes can share sessions:
+Because all state lives in Redis, multiple processes can share sessions:
- **Horizontal scaling**: deploy multiple agent replicas behind a load balancer.
- **Seamless failover**: if one instance goes down, another picks up the session.
diff --git a/docs/llms.txt b/docs/llms.txt
index fd6f8ae..51ac925 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -2,9 +2,8 @@
> Redis integrations for Google's Agent Development Kit. Provides ADK
> `BaseSessionService` and `BaseMemoryService` implementations, five
-> RedisVL-backed search tools, MCP toolsets for RedisVL and Agent Memory
-> Server, and semantic cache providers (self-hosted RedisVL or managed
-> Redis LangCache).
+> RedisVL-backed search tools, selectable Redis Agent Memory or Agent Memory
+> Server memory backends, MCP toolsets, and semantic cache providers.
For agents using the library, read [AGENTS.md](../AGENTS.md) first. For
the full API surface, browse [the API reference](api/python/index.md).
@@ -14,8 +13,8 @@ For task-oriented recipes, browse the
## Concepts
- [ADK overview](concepts/adk_overview.md): how ADK abstractions map onto Redis.
-- [Sessions](concepts/sessions.md): working memory backed by Agent Memory Server.
-- [Memory](concepts/memory.md): long-term semantic memory with recency boosting.
+- [Sessions](concepts/sessions.md): working memory backed by Redis Agent Memory or Agent Memory Server.
+- [Memory](concepts/memory.md): long-term semantic memory through Redis Agent Memory or Agent Memory Server.
- [Search](concepts/search.md): vector, hybrid, range, text, and SQL search over RedisVL.
- [Caching](concepts/caching.md): semantic caching with RedisVL or LangCache.
@@ -58,6 +57,8 @@ When generating code that uses adk-redis:
(e.g., `from adk_redis import RedisVectorSearchTool`).
- Construct services with their `Config` dataclasses
(`RedisLongTermMemoryServiceConfig`, etc.) so options are explicit.
+- Set `backend="redis-agent-memory"` for Redis Agent Memory, or
+ `backend="opensource-agent-memory"` for the open source self-hosted server.
- For the RedisVL MCP path, use ADK's native `McpToolset` with the
appropriate `*ConnectionParams` class. Set
`tool_filter=["search-records"]` to suppress writes, or pass
@@ -71,22 +72,20 @@ When generating code that uses adk-redis, never:
- Pass `epsilon=` to `RedisVectorQueryConfig`; it is `VECTOR_RANGE`-only
and lives on `RedisRangeQueryConfig`.
- Mix sync and async RedisVL `SearchIndex` instances in the same tool.
-- Cache the `MemoryAPIClient` across ADK `Runner.run` invocations; the
- session service builds a new client per call on purpose.
+- Cache Redis Agent Memory SDK clients across ADK `Runner.run` invocations;
+ the session and memory services build short-lived clients on purpose.
When the user asks for something this library does not cover:
- For LangChain agents over Redis, use [`langchain-redis`](https://github.com/redis-developer/langchain-redis).
- For LangGraph state and checkpointers, use [`langgraph-redis`](https://github.com/redis-developer/langgraph-redis).
- For raw RedisVL search without ADK, use [`redisvl`](https://docs.redisvl.com).
-- For the Redis Agent Memory Server itself, use
- [`agent-memory-server`](https://github.com/redis/agent-memory-server)
- and its `agent-memory-client` SDK.
+- For Redis Agent Memory itself, use the `redis-agent-memory` SDK.
When the user is comparing adk-redis to another library:
- adk-redis is the ADK-native surface for Redis. It does not replace
- RedisVL or Agent Memory Server; it adapts them to ADK's `BaseTool`,
+ RedisVL or Redis Agent Memory; it adapts them to ADK's `BaseTool`,
`BaseSessionService`, `BaseMemoryService`, and MCP interfaces.
- Pick this library when the agent runs on Google ADK and the data layer
is Redis. For other agent frameworks, the corresponding `*-redis`
diff --git a/docs/specs/examples-memory-coverage-handover.md b/docs/specs/examples-memory-coverage-handover.md
new file mode 100644
index 0000000..6e7c849
--- /dev/null
+++ b/docs/specs/examples-memory-coverage-handover.md
@@ -0,0 +1,232 @@
+# Handover: memory example coverage and runner alignment
+
+Status: open follow-up after PR #16 (selectable memory backends, release 0.0.7).
+
+## Background
+
+PR #16 made the memory stack backend-pluggable through a single config field:
+
+- `backend="redis-agent-memory"` (managed Redis Agent Memory, library default).
+- `backend="opensource-agent-memory"` (self-hosted Agent Memory Server via
+ `agent-memory-client`).
+
+See [`redis-agent-memory-default.md`](redis-agent-memory-default.md) for the
+backend selection design.
+
+The work below is open because the two backends do not have identical
+capabilities, and the shipped examples were originally written against the
+opensource backend. Before changing examples, the next person needs to
+confirm what the managed backend can and cannot do today, then decide what
+the example surface should look like.
+
+## Goal
+
+Decide what `examples/` should look like now that two memory backends are
+supported. Produce a concrete plan (and ideally PRs) that aligns examples
+with the library default, covers both backends honestly, and keeps the
+"clone, run, done" experience for new users.
+
+## How to approach this
+
+Work in two phases. Do not skip phase 1: the example decisions in phase 2
+are downstream of what you find in phase 1.
+
+### Phase 1: assess implementation coverage of the managed backend
+
+Goal: a written answer to "does every ADK memory surface we ship work
+end-to-end against `redis-agent-memory`, and if not, what is missing?"
+
+1. Read the current backend dispatch:
+ - `src/adk_redis/memory/long_term_memory.py`
+ - `src/adk_redis/sessions/working_memory.py`
+ - `src/adk_redis/tools/memory/` (all six tools plus `_base.py` and
+ `_config.py`)
+ - `src/adk_redis/memory/_backends.py`
+2. Read the `redis-agent-memory` SDK surface that we actually call. The
+ installed package lives under
+ `.venv/lib/python3.13/site-packages/redis_agent_memory/`. Start at
+ `sdk.py` (the `AgentMemory` class) and the `models/` directory.
+3. Read the `agent-memory-client` surface we call for the opensource path,
+ and the upstream `redis/agent-memory-server` README, so you know what
+ the opensource backend offers that the managed SDK may not.
+4. For each ADK surface we expose
+ (`RedisLongTermMemoryService.add_session_to_memory` / `add_memory` /
+ `add_events_to_memory` / `search_memory`,
+ `RedisWorkingMemorySessionService.create_session` / `get_session` /
+ `list_sessions` / `append_event` / `delete_session`, and each memory
+ tool), record:
+ - Does the managed SDK have a primitive we can call?
+ - If yes, is the behavior equivalent, or is there a semantic gap
+ (for example: explicit writes vs. server-side extraction,
+ summarization, recency, MCP, namespace/scope semantics, owner
+ IDs)?
+ - If no, is it a hard gap (no managed equivalent exists) or a soft
+ gap (managed has a different primitive that we have not wired
+ yet)?
+5. Cross-check against the existing live integration tests in
+ `tests/integration/test_memory_backends_end_to_end.py`. Anything not
+ exercised there is by definition not verified end-to-end on managed.
+
+Deliverable: a coverage table (ADK surface x backend) checked into
+`docs/specs/` or attached to the follow-up PR, plus a list of hard
+gaps that need either upstream work in `redis-agent-memory` or
+documentation in `adk-redis` so users are not surprised.
+
+### Phase 2: assess example coverage given the phase 1 findings
+
+Only start once phase 1 has a written conclusion. The example decisions
+are different depending on whether managed has full parity, partial
+parity, or a known feature gap.
+
+1. Enumerate the current examples and what each one actually exercises:
+ - Which use `RedisLongTermMemoryService` / `RedisWorkingMemorySessionService`?
+ - Which use the memory tools directly?
+ - Which use the Agent Memory Server MCP endpoint?
+ - Which use the `adk web` runner vs. a custom `main.py` via
+ `get_fast_api_app`?
+ - Which default `REDIS_MEMORY_BACKEND` is set in `.env.example`,
+ README, and agent wiring?
+2. For each example, answer:
+ - Can it run unchanged on managed today, given phase 1 findings?
+ - If not, is the blocker an example-level wiring issue (env vars,
+ defaults), a runner constraint (services need `get_fast_api_app`,
+ `adk web` cannot register them), or a real feature gap in managed?
+ - Should its default backend stay opensource, flip to managed, or be
+ duplicated so both backends are covered?
+3. Decide on the target example surface:
+ - Do we need a new managed-default example? If yes, what runner and
+ what feature set (services vs. tools-only)?
+ - Do any existing examples need to be split, renamed, or removed?
+ - Is there a missing top-level `examples/README.md` matrix that would
+ orient users to the runner/backend/infra trade-offs?
+ - Is `examples/simple_redis_memory/.env.example` (currently empty)
+ intentional or an oversight?
+
+Deliverable: a short plan ("add X, change Y, leave Z alone, document W")
+and the corresponding PRs.
+
+## Known starting observations
+
+These are observations from the PR #16 review and a read of the public
+Redis Agent Memory docs as of 2026-06-02. Treat them as inputs to phase
+1 / phase 2, not as conclusions. Verify each one against the current
+SDK and server behavior before acting on it: managed Redis Agent Memory
+is explicitly in **preview** ("Features and behavior are subject to
+change").
+
+### Example wiring
+
+- Every memory example currently defaults to
+ `REDIS_MEMORY_BACKEND=opensource-agent-memory`, which is the opposite
+ of the library default.
+- Only one memory example (`travel_agent_memory_tools`) uses the
+ `adk web` runner. The other two register services through
+ `google.adk.cli.service_registry.get_service_registry()`, which only
+ `get_fast_api_app` consults, so they ship a custom `main.py`. Whether
+ this is a permanent constraint or something that should be lifted
+ upstream in ADK is worth checking during phase 1.
+- `examples/simple_redis_memory/.env.example` is empty.
+
+### Capability differences from public docs
+
+Sources: ,
+,
+and . Numbers are starting
+points; phase 1 needs to confirm each against the installed SDK and the
+current server image.
+
+- **Auto-promotion is present on managed, but the policy is opaque.**
+ The public managed docs state "the Agent Memory model will
+ automatically promote relevant short-term memories to long-term
+ memory" when events are added through the session-memory endpoint.
+ There is no documented control surface (no extraction strategy
+ selector, no debounce). Opensource exposes `extraction_strategy`
+ (`discrete` / `summary` / `preferences`) and
+ `EXTRACTION_DEBOUNCE_SECONDS`. Phase 1 needs to check whether our
+ current managed dispatch routes session events through the endpoint
+ that triggers promotion, and what `RedisLongTermMemoryServiceConfig`
+ knobs become no-ops on managed.
+- **Auto-summarization of working memory is opensource only.**
+ Opensource summarizes older turns when token count crosses
+ `context_window_max` using the configured `model_name`. The managed
+ docs do not describe an equivalent. Confirm whether the managed
+ session-memory endpoint has any size-based behavior, and document any
+ `RedisWorkingMemorySessionServiceConfig` fields that become no-ops on
+ managed.
+- **Recency-boosted search is opensource only (as documented).**
+ Opensource search exposes `semantic_weight` / `recency_weight` /
+ `recency_boost`. Managed search exposes `similarityThreshold` plus
+ rich `filter` operators (`eq`, `ne`, `in`, `all`, `gt`, `lt`, `gte`,
+ `lte`) over `sessionId`, `ownerId`, `namespace`, `topics`,
+ `memoryType`, `createdAt`. Phase 1 needs to confirm how our
+ `recency_boost` / weight config degrades on managed.
+- **MCP server endpoint is opensource only.** Opensource ships
+ `agent-memory mcp --mode sse` and our `create_memory_mcp_toolset`
+ helper targets it. Managed has no documented MCP endpoint. Until
+ that changes, `fitness_coach_mcp` and any future MCP example stays
+ on opensource, and the docs need to say so.
+- **`MemoryPromptTool` parity.** Opensource MCP exposes a
+ `memory_prompt` tool. Confirm whether our managed dispatch in
+ `src/adk_redis/tools/memory/memory_prompt_tool.py` returns equivalent
+ data via the managed search endpoint, and document any behavior
+ difference.
+- **Identifier naming.** Managed uses `ownerId` and `storeId`;
+ opensource uses `user_id` and namespaces only. Our config field is
+ `default_owner_id`. Phase 1 should confirm the mapping is consistent
+ across all six tools, both services, and the integration tests, and
+ that ADK's `user_id` flows through to `ownerId` correctly.
+- **Session state.** ADK `Session.state` is a free-form dict. Managed
+ session events have `actorId`, `role`, `content[]`, `createdAt`, and
+ per-event `metadata`, but no documented session-level state field.
+ Phase 1 should check whether session state survives a
+ `create_session` / `get_session` round trip on managed and whether
+ this needs to be called out as a known gap.
+- **Event payload fidelity.** ADK events carry function calls, tool
+ responses, partials, and other fields beyond plain text. Managed
+ session events are modeled around text content with optional
+ metadata. Confirm what our `append_event` dispatch on managed does
+ with non-text payloads and whether anything is dropped.
+- **Bulk operations.** The managed SDK exposes
+ `bulk_create_long_term_memories` and `bulk_delete_long_term_memories`.
+ Check that we use bulk where it makes sense (`add_memory` already
+ does) and whether `DeleteMemoryTool` on managed could be made
+ bulk-aware.
+- **Documentation alignment.** The public ADK integration page
+ (`redis.io/docs/.../integrate/google-adk/redis-agent-memory/`) is
+ written as if the only backend is the Agent Memory Server. It does
+ not yet describe the managed backend selector introduced by PR #16.
+ Coordinate with whoever owns that page so it matches the library
+ default once phase 2 lands.
+
+## Out of scope for this handover
+
+- Changing the library default backend.
+- Live CI coverage for managed integration tests (covered separately by
+ the integration tests added in PR #16; gating them in CI is its own
+ follow-up).
+- Updates to the external `redis.io` ADK integration page (flagged
+ above for coordination, but not owned by this repo).
+
+## Out of scope for this handover
+
+- Changing the library default backend.
+- Live CI coverage for managed integration tests (covered separately by
+ the integration tests added in PR #16; gating them in CI is its own
+ follow-up).
+
+## Pointers
+
+- Backend selection spec: [`redis-agent-memory-default.md`](redis-agent-memory-default.md)
+- Backend dispatch:
+ - `src/adk_redis/memory/long_term_memory.py`
+ - `src/adk_redis/sessions/working_memory.py`
+ - `src/adk_redis/tools/memory/_config.py`
+ - `src/adk_redis/tools/memory/_base.py`
+- Live integration coverage: `tests/integration/test_memory_backends_end_to_end.py`
+- Managed SDK (installed): `.venv/.../site-packages/redis_agent_memory/sdk.py`
+- Opensource server:
+- Example wiring references:
+ - Tools-only + `adk web`: `examples/travel_agent_memory_tools/`
+ - Services + custom runner: `examples/travel_agent_memory_hybrid/main.py`,
+ `examples/simple_redis_memory/main.py`
+ - MCP via opensource: `examples/fitness_coach_mcp/`
diff --git a/docs/specs/redis-agent-memory-default.md b/docs/specs/redis-agent-memory-default.md
new file mode 100644
index 0000000..63b0027
--- /dev/null
+++ b/docs/specs/redis-agent-memory-default.md
@@ -0,0 +1,138 @@
+# Redis Agent Memory Dual Backend Spec
+
+## Goal
+
+Add Redis Agent Memory support to `adk-redis` while keeping the open source,
+self-hosted Agent Memory Server path available. Users choose the backend with a
+single config field.
+
+## Backend Choice
+
+All ADK memory surfaces accept:
+
+```python
+backend="redis-agent-memory" # default, uses redis-agent-memory
+backend="opensource-agent-memory" # self-hosted, uses agent-memory-client
+```
+
+The public class names stay unchanged:
+
+- `RedisWorkingMemorySessionService`
+- `RedisLongTermMemoryService`
+- `MemoryToolConfig`
+- `SearchMemoryTool`, `CreateMemoryTool`, `GetMemoryTool`,
+ `UpdateMemoryTool`, `DeleteMemoryTool`, `MemoryPromptTool`
+
+## ADK Upstream Check
+
+The latest `google/adk-python` source was checked from a fresh shallow clone at
+commit `ae95a97`. The memory service contract still includes
+`add_session_to_memory()` and `search_memory()`, and now also includes optional
+write paths:
+
+- `add_events_to_memory()`
+- `add_memory()`
+
+The implementation supports these newer hooks while remaining compatible with
+installed ADK versions that do not call them yet.
+
+## Redis Agent Memory Backend
+
+The default backend uses `redis_agent_memory.AgentMemory`.
+
+Session service:
+
+- `append_event()` writes Redis Agent Memory session events.
+- `get_session()` reconstructs ADK sessions from stored events.
+- `list_sessions()` stores ADK scope in deterministic internal session IDs.
+
+Long-term memory service:
+
+- `search_memory()` calls `search_long_term_memory_async()`.
+- Searches filter by `ownerId == user_id` and namespace.
+- `add_memory()` writes durable `semantic`, `episodic`, or `message` records.
+- `add_events_to_memory()` and `add_session_to_memory()` write ADK events as
+ long-term `message` memories for ADK compatibility.
+
+Tools:
+
+- Use the async Redis Agent Memory SDK methods for search, create, get, update,
+ delete, and prompt enrichment.
+- `default_owner_id` is available for tools that run outside an ADK user
+ context.
+
+## Agent Memory Server Backend
+
+The self-hosted backend keeps the previous `agent-memory-client` behavior.
+
+Session service:
+
+- Uses Working Memory APIs.
+- Preserves automatic summarization and extraction strategy settings.
+- Keeps incremental `append_messages_to_working_memory()` writes.
+
+Long-term memory service:
+
+- `add_session_to_memory()` stores session working memory so the server can
+ extract long-term memories.
+- `add_memory()` uses `add_memory_tool()` for explicit durable memory writes.
+- `search_memory()` uses `search_long_term_memory()` with namespace and user
+ filters, plus optional recency config.
+
+Tools:
+
+- Use the previous REST client methods.
+- Preserve recency settings and memory prompt behavior.
+
+## Configuration
+
+Shared options:
+
+- `backend`
+- `api_base_url`
+- `default_namespace`
+- `search_top_k`
+- `distance_threshold`
+- `timeout`
+
+Redis Agent Memory options:
+
+- `api_key`
+- `store_id`
+- `timeout_ms`
+- `similarity_threshold`
+- `default_owner_id` for tools
+
+Agent Memory Server options:
+
+- `recency_boost`
+- `semantic_weight`
+- `recency_weight`
+- `freshness_weight`
+- `novelty_weight`
+- `half_life_last_access_days`
+- `half_life_created_days`
+- `extraction_strategy`
+- `extraction_strategy_config`
+- `model_name`
+- `context_window_max`
+
+## Dependency
+
+The `memory` extra installs both backend clients:
+
+- `agent-memory-client>=0.14.0`
+- `redis-agent-memory>=0.0.4`
+
+## Tests
+
+Unit tests mock both clients and cover:
+
+- Config defaults.
+- Redis Agent Memory owner and namespace filters.
+- Self-hosted Agent Memory Server backend dispatch.
+- Redis Agent Memory session event writes.
+- Session reconstruction.
+- Tool create, search, update, and delete payloads.
+
+Normal unit tests do not require a live memory server.
diff --git a/docs/user_guide/01_integration.md b/docs/user_guide/01_integration.md
index 7e6c25b..0db02e6 100644
--- a/docs/user_guide/01_integration.md
+++ b/docs/user_guide/01_integration.md
@@ -8,33 +8,18 @@ three steps. For the concepts behind each feature, see the
## 1. Start infrastructure
-Follow the [Redis setup](how_to_guides/redis_setup.md) and
-[Agent Memory Server setup](how_to_guides/memory_server_setup.md) how-to guides,
-or use this minimal start:
+Provide Redis Agent Memory connection settings:
```bash
-# Redis 8.4
-docker run -d --name redis -p 6379:6379 redis:8.4-alpine
-
-# Agent Memory Server (dev mode)
-docker run -d --name agent-memory-server \
- -p 8000:8000 \
- -e REDIS_URL=redis://host.docker.internal:6379 \
- -e GEMINI_API_KEY=your-gemini-api-key \
- -e GENERATION_MODEL=gemini/gemini-2.0-flash-exp \
- -e EMBEDDING_MODEL=gemini/text-embedding-004 \
- -e EXTRACTION_DEBOUNCE_SECONDS=5 \
- -e DISABLE_AUTH=true \
- redislabs/agent-memory-server:0.13.2 \
- agent-memory api --host 0.0.0.0 --port 8000 --task-backend=asyncio
-
-# Verify
-curl http://localhost:8000/v1/health
+export REDIS_MEMORY_BACKEND="redis-agent-memory"
+export AGENT_MEMORY_SERVER_URL="https://..."
+export AGENT_MEMORY_STORE_ID="..."
+export AGENT_MEMORY_API_KEY="..."
```
-!!! note
- On Linux, replace `host.docker.internal` with `172.17.0.1` or use
- `--network host`.
+For the open source self-hosted Agent Memory Server, use
+`REDIS_MEMORY_BACKEND="opensource-agent-memory"` and point
+`AGENT_MEMORY_SERVER_URL` at your server.
## 2. Install dependencies
@@ -45,8 +30,13 @@ pip install google-adk "adk-redis[memory]"
## 3. Wire services into an agent
```python
+import os
+
from google.adk import Agent
+from google.adk.agents.callback_context import CallbackContext
from google.adk.runners import Runner
+from google.adk.tools import load_memory
+from google.adk.tools import preload_memory
from adk_redis import (
RedisWorkingMemorySessionService,
RedisWorkingMemorySessionServiceConfig,
@@ -56,22 +46,35 @@ from adk_redis import (
session_service = RedisWorkingMemorySessionService(
config=RedisWorkingMemorySessionServiceConfig(
- api_base_url="http://localhost:8000",
+ backend=os.getenv("REDIS_MEMORY_BACKEND", "redis-agent-memory"),
+ api_base_url=os.environ["AGENT_MEMORY_SERVER_URL"],
+ api_key=os.environ.get("AGENT_MEMORY_API_KEY"),
+ store_id=os.environ.get("AGENT_MEMORY_STORE_ID"),
default_namespace="my_app",
)
)
memory_service = RedisLongTermMemoryService(
config=RedisLongTermMemoryServiceConfig(
- api_base_url="http://localhost:8000",
+ backend=os.getenv("REDIS_MEMORY_BACKEND", "redis-agent-memory"),
+ api_base_url=os.environ["AGENT_MEMORY_SERVER_URL"],
+ api_key=os.environ.get("AGENT_MEMORY_API_KEY"),
+ store_id=os.environ.get("AGENT_MEMORY_STORE_ID"),
default_namespace="my_app",
)
)
+
+async def after_agent(callback_context: CallbackContext):
+ await callback_context.add_session_to_memory()
+
+
agent = Agent(
name="memory_agent",
model="gemini-2.0-flash",
instruction="You are a helpful assistant with long-term memory.",
+ tools=[preload_memory, load_memory],
+ after_agent_callback=after_agent,
)
runner = Runner(
@@ -92,8 +95,7 @@ adk web my_app
**Try it out:**
1. "Hi, I'm Alice. I love pizza and Python."
-2. Wait 5 seconds for background memory extraction.
-3. Start a new session: "What do you remember about me?"
+2. Start a new session: "What do you remember about me?"
## What next?
diff --git a/docs/user_guide/how_to_guides/memory_service.md b/docs/user_guide/how_to_guides/memory_service.md
index 196ec0f..b89cca9 100644
--- a/docs/user_guide/how_to_guides/memory_service.md
+++ b/docs/user_guide/how_to_guides/memory_service.md
@@ -1,16 +1,16 @@
# Memory Service
This guide shows how to wire `RedisLongTermMemoryService` into a Google ADK
-agent for persistent long-term memory backed by the
-[Agent Memory Server](memory_server_setup.md).
+agent for persistent long-term memory backed by Redis Agent Memory or the
+self-hosted Agent Memory Server.
For the concepts behind long-term memory, see
[Sessions + Memory Services](../../concepts/sessions.md).
## Prerequisites
-- Agent Memory Server running on `localhost:8000`
- (see [Memory server setup](memory_server_setup.md)).
+- Redis Agent Memory endpoint, store ID, and API key, or a self-hosted Agent
+ Memory Server endpoint.
- `adk-redis` with the memory extra: `pip install "adk-redis[memory]"`.
## Basic usage
@@ -27,14 +27,20 @@ from adk_redis import (
session_service = RedisWorkingMemorySessionService(
config=RedisWorkingMemorySessionServiceConfig(
+ backend="redis-agent-memory",
api_base_url="http://localhost:8000",
+ api_key="...",
+ store_id="...",
default_namespace="my_app",
)
)
memory_service = RedisLongTermMemoryService(
config=RedisLongTermMemoryServiceConfig(
+ backend="redis-agent-memory",
api_base_url="http://localhost:8000",
+ api_key="...",
+ store_id="...",
default_namespace="my_app",
recency_boost=True,
)
@@ -54,11 +60,14 @@ runner = Runner(
)
```
+For self-hosted Agent Memory Server, use `backend="opensource-agent-memory"` and
+omit `api_key` and `store_id` unless your deployment requires them.
+
## How memories flow
1. The agent converses with the user in a session.
-2. The Agent Memory Server extracts key facts in the background.
-3. Facts are stored as long-term memories with vector embeddings.
+2. `add_session_to_memory()` stores event text as long-term `message` memory.
+3. Use `add_memory()` or memory tools to write durable facts and preferences.
4. On future sessions, the runner calls `search_memory()` and injects
relevant memories into the prompt automatically.
@@ -66,12 +75,12 @@ runner = Runner(
| Option | Default | Description |
|--------|---------|-------------|
-| `api_base_url` | `http://localhost:8000` | Agent Memory Server URL |
+| `backend` | `redis-agent-memory` | `redis-agent-memory` or `opensource-agent-memory` |
+| `api_base_url` | `http://localhost:8000` | Memory backend URL |
+| `api_key` | `None` | Redis Agent Memory API key |
+| `store_id` | `None` | Redis Agent Memory store ID |
| `default_namespace` | `None` | Namespace for memory isolation |
| `search_top_k` | `10` | Max memories per search |
-| `distance_threshold` | `None` | Max distance for results (0.0-1.0) |
-| `recency_boost` | `True` | Enable recency-aware ranking |
-| `semantic_weight` | `0.8` | Weight for semantic similarity |
-| `recency_weight` | `0.2` | Weight for recency score |
-| `extraction_strategy` | `discrete` | `discrete`, `summary`, `preferences`, `custom` |
+| `similarity_threshold` | `None` | Min similarity for results (0.0-1.0) |
+| `store_events_as_messages` | `True` | Store ADK events as `message` memories |
| `timeout` | `30.0` | HTTP request timeout |
diff --git a/docs/user_guide/how_to_guides/session_service.md b/docs/user_guide/how_to_guides/session_service.md
index c7ce0d4..954ccb2 100644
--- a/docs/user_guide/how_to_guides/session_service.md
+++ b/docs/user_guide/how_to_guides/session_service.md
@@ -1,16 +1,16 @@
# Session Service
This guide shows how to wire `RedisWorkingMemorySessionService` into a Google
-ADK agent for durable, auto-summarizing session state backed by the
-[Agent Memory Server](memory_server_setup.md).
+ADK agent for durable session state backed by Redis Agent Memory session
+events or self-hosted Agent Memory Server working memory.
For the concepts behind sessions and working memory, see
[Sessions + Memory Services](../../concepts/sessions.md).
## Prerequisites
-- Agent Memory Server running on `localhost:8000`
- (see [Memory server setup](memory_server_setup.md)).
+- Redis Agent Memory endpoint, store ID, and API key, or a self-hosted Agent
+ Memory Server endpoint.
- `adk-redis` with the memory extra: `pip install "adk-redis[memory]"`.
## Basic usage
@@ -25,7 +25,10 @@ from adk_redis import (
session_service = RedisWorkingMemorySessionService(
config=RedisWorkingMemorySessionServiceConfig(
+ backend="redis-agent-memory",
api_base_url="http://localhost:8000",
+ api_key="...",
+ store_id="...",
default_namespace="my_app",
)
)
@@ -48,17 +51,19 @@ session = await session_service.create_session(
user_id="alice",
)
-# The session is persisted in Agent Memory Server and survives restarts
+# Events appended to the session persist in the configured backend
```
+For self-hosted Agent Memory Server, use `backend="opensource-agent-memory"` and
+omit `api_key` and `store_id` unless your deployment requires them.
+
## Configuration options
| Option | Default | Description |
|--------|---------|-------------|
-| `api_base_url` | `http://localhost:8000` | Agent Memory Server URL |
+| `backend` | `redis-agent-memory` | `redis-agent-memory` or `opensource-agent-memory` |
+| `api_base_url` | `http://localhost:8000` | Memory backend URL |
+| `api_key` | `None` | Redis Agent Memory API key |
+| `store_id` | `None` | Redis Agent Memory store ID |
| `default_namespace` | `None` | Namespace for session isolation |
-| `model_name` | `None` | LLM model for summarization |
-| `context_window_max` | `None` | Max tokens before auto-summarization |
-| `extraction_strategy` | `discrete` | `discrete`, `summary`, `preferences`, `custom` |
-| `session_ttl_seconds` | `None` | Session expiration time |
| `timeout` | `30.0` | HTTP request timeout |
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..eb3366f
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,48 @@
+# Examples
+
+Runnable agents built with [Google ADK](https://github.com/google/adk-python)
+and `adk-redis`. Each example ships with a README and `.env.example`.
+
+## Memory and sessions
+
+| Example | Backend default | Runner | What it exercises |
+|---------|-----------------|--------|-------------------|
+| [`managed_memory_quickstart`](managed_memory_quickstart/) | `redis-agent-memory` (managed) | `python main.py` | Minimal session + long-term memory services against managed Redis Agent Memory. No Docker. |
+| [`simple_redis_memory`](simple_redis_memory/) | `opensource-agent-memory` | `python main.py` | Full two-tier memory with auto-summarization and extraction via Agent Memory Server. |
+| [`travel_agent_memory_hybrid`](travel_agent_memory_hybrid/) | `opensource-agent-memory` | `python main.py` | Services + explicit memory tools + travel domain tools. |
+| [`travel_agent_memory_tools`](travel_agent_memory_tools/) | `opensource-agent-memory` | `adk web .` | Memory tools only (no framework services). Switch backend via `REDIS_MEMORY_BACKEND`. |
+| [`fitness_coach_mcp`](fitness_coach_mcp/) | `opensource-agent-memory` only | `adk web .` | MCP memory tools via Agent Memory Server SSE. Managed backend has no MCP endpoint. |
+
+### Runner notes
+
+- **`python main.py`** — Required when registering `RedisWorkingMemorySessionService`
+ and/or `RedisLongTermMemoryService` through `get_service_registry()`. ADK's
+ `get_fast_api_app` reads those registrations; `adk web` does not.
+- **`adk web .`** — Works when memory is wired as agent tools in `agent.py`
+ (no custom session/memory services).
+
+### Backend notes
+
+- **`redis-agent-memory`** — Managed Redis Agent Memory (library default). Needs
+ `REDIS_AGENT_MEMORY_API_BASE_URL`, `REDIS_AGENT_MEMORY_API_KEY`, and
+ `REDIS_AGENT_MEMORY_STORE_ID`. No local Agent Memory Server.
+- **`opensource-agent-memory`** — Self-hosted
+ [Agent Memory Server](https://github.com/redis/agent-memory-server). Supports
+ auto-summarization, extraction strategies, recency-boosted search, and MCP.
+
+Set `REDIS_MEMORY_BACKEND` in examples that support both backends.
+
+## Search
+
+| Example | Runner | What it exercises |
+|---------|--------|-------------------|
+| [`redis_search_tools`](redis_search_tools/) | `adk web .` | Vector, text, and range search tools |
+| [`redis_sql_search`](redis_sql_search/) | `adk web .` | `RedisSQLSearchTool` |
+| [`redisvl_mcp_search`](redisvl_mcp_search/) | `adk web .` | RedisVL data via `rvl mcp` |
+
+## Caching
+
+| Example | Runner | What it exercises |
+|---------|--------|-------------------|
+| [`semantic_cache`](semantic_cache/) | `python main.py` | `RedisVLCacheProvider` semantic cache |
+| [`langcache_cache`](langcache_cache/) | `python main.py` | Managed `LangCacheProvider` |
diff --git a/examples/managed_memory_quickstart/.env.example b/examples/managed_memory_quickstart/.env.example
new file mode 100644
index 0000000..19be318
--- /dev/null
+++ b/examples/managed_memory_quickstart/.env.example
@@ -0,0 +1,29 @@
+# =============================================================================
+# MANAGED REDIS AGENT MEMORY QUICKSTART
+# =============================================================================
+# Copy this file to .env and fill in your actual values.
+# =============================================================================
+
+# -----------------------------------------------------------------------------
+# Google Gemini API Key (REQUIRED)
+# -----------------------------------------------------------------------------
+GOOGLE_API_KEY=your-google-api-key-here
+
+# -----------------------------------------------------------------------------
+# Redis Agent Memory credentials (REQUIRED)
+# -----------------------------------------------------------------------------
+# Same variable names used by tests/integration/test_memory_backends_end_to_end.py
+REDIS_AGENT_MEMORY_API_BASE_URL=https://your-api-base-url
+REDIS_AGENT_MEMORY_API_KEY=your-api-key
+REDIS_AGENT_MEMORY_STORE_ID=your-store-id
+
+# -----------------------------------------------------------------------------
+# Namespace (OPTIONAL)
+# -----------------------------------------------------------------------------
+# Underscores are auto-converted to hyphens for the managed API.
+REDIS_MEMORY_NAMESPACE=managed-memory-quickstart
+
+# -----------------------------------------------------------------------------
+# Search tuning (OPTIONAL)
+# -----------------------------------------------------------------------------
+REDIS_MEMORY_SEARCH_TOP_K=5
diff --git a/examples/managed_memory_quickstart/README.md b/examples/managed_memory_quickstart/README.md
new file mode 100644
index 0000000..72e94c5
--- /dev/null
+++ b/examples/managed_memory_quickstart/README.md
@@ -0,0 +1,108 @@
+# Managed Redis Agent Memory Quickstart
+
+Minimal counterpart to [`simple_redis_memory`](../simple_redis_memory/). This
+example uses the **managed** `redis-agent-memory` backend (the library default)
+with no self-hosted Agent Memory Server or Docker setup.
+
+## What it demonstrates
+
+- `RedisWorkingMemorySessionService` for session persistence
+- `RedisLongTermMemoryService` for semantic memory search
+- ADK built-in `preload_memory` and `load_memory` tools
+
+This example intentionally avoids opensource-only features such as
+auto-summarization, extraction strategies, recency-boosted search, and MCP.
+
+## Prerequisites
+
+- Python 3.10+
+- A Redis Agent Memory store (`REDIS_AGENT_MEMORY_STORE_ID` + API key)
+- Google API key for Gemini
+
+## Setup
+
+### 1. Install dependencies
+
+From the repository root (editable install for local PR testing):
+
+```bash
+uv venv
+uv pip install -e ".[all,examples]"
+```
+
+Or use `make dev` if you also want test/lint tooling.
+
+> **Note:** Even memory-only examples import `adk_redis`, whose top-level
+> `__init__.py` currently pulls in search tools that require `redisvl`. The
+> `[all]` extra installs that dependency. `[memory]` alone is not enough today.
+
+### 2. Configure environment
+
+```bash
+cd examples/managed_memory_quickstart
+cp .env.example .env
+# Edit .env with your credentials
+```
+
+### 3. Start the agent
+
+```bash
+uv run python main.py
+```
+
+Open http://localhost:8080 in your browser.
+
+## Try it
+
+**Session 1** — share a few facts:
+
+```
+Hi! My name is Alex.
+My favorite color is teal.
+I have a cat named Sammy.
+```
+
+Wait for a reply after each message.
+
+**Session 2** — click **New Session** (keep the same `userId`, e.g. `user`) and
+ask:
+
+```
+What do you remember about me?
+What is my favorite color?
+```
+
+Cross-session recall uses long-term memory (`preload_memory` / `load_memory`).
+Facts are stored automatically after each turn via the agent's `after_agent`
+callback — there is no separate seed step.
+
+## Configuration
+
+| Variable | Required | Default | Description |
+|----------|----------|---------|-------------|
+| `GOOGLE_API_KEY` | Yes | — | Gemini API key |
+| `REDIS_AGENT_MEMORY_API_BASE_URL` | Yes | — | Managed API base URL |
+| `REDIS_AGENT_MEMORY_API_KEY` | Yes | — | Managed API key |
+| `REDIS_AGENT_MEMORY_STORE_ID` | Yes | — | Memory store ID |
+| `REDIS_MEMORY_NAMESPACE` | No | `managed_memory_quickstart` | Namespace for isolation |
+| `REDIS_MEMORY_SEARCH_TOP_K` | No | `5` | Results returned by `search_memory` |
+| `PORT` | No | `8080` | ADK web server port |
+
+## How to run
+
+This example uses a custom `main.py` with `get_fast_api_app` because ADK
+session and memory services must be registered through
+`get_service_registry()`. The `adk web` runner cannot register those services.
+
+For a tools-only example that runs with `adk web`, see
+[`travel_agent_memory_tools`](../travel_agent_memory_tools/).
+
+## Related examples
+
+| Example | Backend | Runner |
+|---------|---------|--------|
+| **This quickstart** | Managed (`redis-agent-memory`) | `python main.py` |
+| [`simple_redis_memory`](../simple_redis_memory/) | Self-hosted (default) | `python main.py` |
+| [`travel_agent_memory_tools`](../travel_agent_memory_tools/) | Either (env switch) | `adk web .` |
+
+See [`examples/README.md`](../README.md) for the full matrix.
diff --git a/examples/managed_memory_quickstart/main.py b/examples/managed_memory_quickstart/main.py
new file mode 100644
index 0000000..7f07ede
--- /dev/null
+++ b/examples/managed_memory_quickstart/main.py
@@ -0,0 +1,123 @@
+# Copyright 2025 Redis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Managed Redis Agent Memory quickstart.
+
+Registers RedisWorkingMemorySessionService and RedisLongTermMemoryService
+against the managed redis-agent-memory backend, then serves the agent via ADK's
+FastAPI runner.
+"""
+
+import os
+from urllib.parse import urlparse
+
+from dotenv import load_dotenv
+from fastapi import FastAPI
+from google.adk.cli.fast_api import get_fast_api_app
+from google.adk.cli.service_registry import get_service_registry
+import uvicorn
+
+from adk_redis.memory import RedisLongTermMemoryService
+from adk_redis.memory import RedisLongTermMemoryServiceConfig
+from adk_redis.sessions import RedisWorkingMemorySessionService
+from adk_redis.sessions import RedisWorkingMemorySessionServiceConfig
+
+load_dotenv()
+
+_BACKEND = "redis-agent-memory"
+
+
+def _api_base_url() -> str:
+ return os.environ["REDIS_AGENT_MEMORY_API_BASE_URL"]
+
+
+def parse_base_url(uri: str) -> str:
+ """Parse a service URI to extract the base URL."""
+ parsed = urlparse(uri)
+ location = parsed.netloc + parsed.path
+ return (
+ location
+ if location.startswith(("http://", "https://"))
+ else f"http://{location}"
+ )
+
+
+def redis_session_factory(uri: str, **kwargs):
+ """Factory for RedisWorkingMemorySessionService."""
+ base_url = parse_base_url(uri)
+ config = RedisWorkingMemorySessionServiceConfig(
+ backend=_BACKEND,
+ api_base_url=base_url,
+ api_key=os.environ["REDIS_AGENT_MEMORY_API_KEY"],
+ store_id=os.environ["REDIS_AGENT_MEMORY_STORE_ID"],
+ default_namespace=os.getenv(
+ "REDIS_MEMORY_NAMESPACE", "managed_memory_quickstart"
+ ),
+ )
+ return RedisWorkingMemorySessionService(config=config)
+
+
+def redis_memory_factory(uri: str, **kwargs):
+ """Factory for RedisLongTermMemoryService."""
+ base_url = parse_base_url(uri)
+ config = RedisLongTermMemoryServiceConfig(
+ backend=_BACKEND,
+ api_base_url=base_url,
+ api_key=os.environ["REDIS_AGENT_MEMORY_API_KEY"],
+ store_id=os.environ["REDIS_AGENT_MEMORY_STORE_ID"],
+ default_namespace=os.getenv(
+ "REDIS_MEMORY_NAMESPACE", "managed_memory_quickstart"
+ ),
+ search_top_k=int(os.getenv("REDIS_MEMORY_SEARCH_TOP_K", "5")),
+ )
+ return RedisLongTermMemoryService(config=config)
+
+
+registry = get_service_registry()
+registry.register_session_service("redis-working-memory", redis_session_factory)
+registry.register_memory_service("redis-long-term-memory", redis_memory_factory)
+
+api_host = _api_base_url().replace("http://", "").replace("https://", "")
+SESSION_SERVICE_URI = f"redis-working-memory://{api_host}"
+MEMORY_SERVICE_URI = f"redis-long-term-memory://{api_host}"
+
+app: FastAPI = get_fast_api_app(
+ agents_dir=".",
+ session_service_uri=SESSION_SERVICE_URI,
+ memory_service_uri=MEMORY_SERVICE_URI,
+ web=True,
+ auto_create_session=True,
+)
+
+
+if __name__ == "__main__":
+ port = int(os.environ.get("PORT", 8080))
+ namespace = os.getenv("REDIS_MEMORY_NAMESPACE", "managed_memory_quickstart")
+
+ print(
+ f"""
+Starting Managed Redis Agent Memory Quickstart (adk-redis)
+==========================================================
+ADK Server: http://localhost:{port}
+Memory Backend: {_BACKEND}
+API Base URL: {_api_base_url()}
+Store ID: {os.environ["REDIS_AGENT_MEMORY_STORE_ID"]}
+Namespace: {namespace}
+
+Services:
+ - Session: RedisWorkingMemorySessionService
+ - Memory: RedisLongTermMemoryService
+"""
+ )
+ uvicorn.run(app, host="0.0.0.0", port=port)
diff --git a/examples/managed_memory_quickstart/managed_memory_agent/__init__.py b/examples/managed_memory_quickstart/managed_memory_agent/__init__.py
new file mode 100644
index 0000000..0a24b26
--- /dev/null
+++ b/examples/managed_memory_quickstart/managed_memory_agent/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2025 Redis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import agent as agent
diff --git a/examples/managed_memory_quickstart/managed_memory_agent/agent.py b/examples/managed_memory_quickstart/managed_memory_agent/agent.py
new file mode 100644
index 0000000..a71dbbd
--- /dev/null
+++ b/examples/managed_memory_quickstart/managed_memory_agent/agent.py
@@ -0,0 +1,60 @@
+# Copyright 2025 Redis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Minimal agent using managed Redis Agent Memory services."""
+
+from datetime import datetime
+
+from google.adk import Agent
+from google.adk.agents.callback_context import CallbackContext
+from google.adk.tools import load_memory
+from google.adk.tools import preload_memory
+
+
+def before_agent(callback_context: CallbackContext):
+ """Update state before the agent runs."""
+ callback_context.state["_time"] = datetime.now().isoformat()
+
+
+async def after_agent(callback_context: CallbackContext):
+ """Persist session events to long-term memory after each turn."""
+ await callback_context.add_session_to_memory()
+
+
+root_agent = Agent(
+ model="gemini-2.5-flash",
+ name="managed_memory_agent",
+ description=(
+ "Minimal agent with managed Redis Agent Memory session and"
+ " long-term memory services."
+ ),
+ before_agent_callback=before_agent,
+ after_agent_callback=after_agent,
+ instruction="""You are a helpful assistant with Redis Agent Memory.
+
+## Memory
+
+1. **Working memory**: The current conversation is stored in your session.
+2. **Long-term memory**: Use `load_memory` to search facts from past sessions.
+ `preload_memory` may surface relevant memories automatically.
+
+## Guidelines
+
+- Be conversational and remember details the user shares.
+- When users share personal info, acknowledge it clearly.
+- If a memory search returns nothing, say so and ask a clarifying question.
+
+Current time: {_time}""",
+ tools=[preload_memory, load_memory],
+)
diff --git a/examples/simple_redis_memory/README.md b/examples/simple_redis_memory/README.md
index 2280ef2..46d3af8 100644
--- a/examples/simple_redis_memory/README.md
+++ b/examples/simple_redis_memory/README.md
@@ -109,11 +109,15 @@ Create `.env` in this directory:
```bash
GOOGLE_API_KEY=your-google-api-key
+REDIS_MEMORY_BACKEND=opensource-agent-memory
REDIS_MEMORY_SERVER_URL=http://localhost:8088
REDIS_MEMORY_NAMESPACE=adk_agent_memory
REDIS_MEMORY_EXTRACTION_STRATEGY=discrete
REDIS_MEMORY_CONTEXT_WINDOW=8000
REDIS_MEMORY_RECENCY_BOOST=true
+# Required only when REDIS_MEMORY_BACKEND=redis-agent-memory
+AGENT_MEMORY_STORE_ID=
+AGENT_MEMORY_API_KEY=
```
## Usage
@@ -171,7 +175,10 @@ User: What's my favorite coffee?
| Variable | Default | Description |
|----------|---------|-------------|
+| `REDIS_MEMORY_BACKEND` | `opensource-agent-memory` | `opensource-agent-memory` or `redis-agent-memory` |
| `REDIS_MEMORY_SERVER_URL` | `http://localhost:8088` | Memory server URL |
+| `AGENT_MEMORY_STORE_ID` | empty | Redis Agent Memory store ID, used only for `redis-agent-memory` |
+| `AGENT_MEMORY_API_KEY` | empty | Redis Agent Memory API key, used only for `redis-agent-memory` |
| `REDIS_MEMORY_NAMESPACE` | `adk_agent_memory` | Namespace for isolation |
| `REDIS_MEMORY_EXTRACTION_STRATEGY` | `discrete` | `discrete`, `summary`, `preferences` |
| `REDIS_MEMORY_CONTEXT_WINDOW` | `8000` | Max tokens before summarization |
diff --git a/examples/simple_redis_memory/main.py b/examples/simple_redis_memory/main.py
index 5866629..93a9d66 100644
--- a/examples/simple_redis_memory/main.py
+++ b/examples/simple_redis_memory/main.py
@@ -55,7 +55,10 @@ def redis_session_factory(uri: str, **kwargs):
"""Factory function for creating RedisWorkingMemorySessionService from URI."""
base_url = parse_base_url(uri)
config = RedisWorkingMemorySessionServiceConfig(
+ backend=os.getenv("REDIS_MEMORY_BACKEND", "opensource-agent-memory"),
api_base_url=base_url,
+ api_key=os.getenv("AGENT_MEMORY_API_KEY"),
+ store_id=os.getenv("AGENT_MEMORY_STORE_ID"),
default_namespace=os.getenv("REDIS_MEMORY_NAMESPACE", "adk_agent_memory"),
model_name=os.getenv("REDIS_MEMORY_MODEL_NAME", "gpt-4o"),
context_window_max=int(os.getenv("REDIS_MEMORY_CONTEXT_WINDOW", "8000")),
@@ -70,7 +73,10 @@ def redis_memory_factory(uri: str, **kwargs):
"""Factory function for creating RedisLongTermMemoryService from URI."""
base_url = parse_base_url(uri)
config = RedisLongTermMemoryServiceConfig(
+ backend=os.getenv("REDIS_MEMORY_BACKEND", "opensource-agent-memory"),
api_base_url=base_url,
+ api_key=os.getenv("AGENT_MEMORY_API_KEY"),
+ store_id=os.getenv("AGENT_MEMORY_STORE_ID"),
default_namespace=os.getenv("REDIS_MEMORY_NAMESPACE", "adk_agent_memory"),
extraction_strategy=os.getenv(
"REDIS_MEMORY_EXTRACTION_STRATEGY", "discrete"
@@ -110,6 +116,7 @@ def redis_memory_factory(uri: str, **kwargs):
port = int(os.environ.get("PORT", 8080))
namespace = os.getenv("REDIS_MEMORY_NAMESPACE", "adk_agent_memory")
server = os.getenv("REDIS_MEMORY_SERVER_URL", "http://localhost:8000")
+ backend = os.getenv("REDIS_MEMORY_BACKEND", "opensource-agent-memory")
extraction = os.getenv("REDIS_MEMORY_EXTRACTION_STRATEGY", "discrete")
context_window = os.getenv("REDIS_MEMORY_CONTEXT_WINDOW", "8000")
@@ -119,6 +126,7 @@ def redis_memory_factory(uri: str, **kwargs):
=========================================
ADK Server: http://localhost:{port}
Memory Server: {server}
+Memory Backend: {backend}
Namespace: {namespace}
Extraction Strategy: {extraction}
Context Window: {context_window} tokens
diff --git a/examples/travel_agent_memory_hybrid/.env.example b/examples/travel_agent_memory_hybrid/.env.example
index af71651..c54cab5 100644
--- a/examples/travel_agent_memory_hybrid/.env.example
+++ b/examples/travel_agent_memory_hybrid/.env.example
@@ -19,15 +19,29 @@ GOOGLE_API_KEY=your-google-api-key-here
TAVILY_API_KEY=your-tavily-api-key-here
# -----------------------------------------------------------------------------
-# Agent Memory Server URL (REQUIRED)
+# Memory Backend (OPTIONAL)
# -----------------------------------------------------------------------------
-# URL of the Redis Agent Memory Server
+# Use opensource-agent-memory for the self-hosted Agent Memory Server.
+# Use redis-agent-memory for Redis Agent Memory.
+REDIS_MEMORY_BACKEND=opensource-agent-memory
+
+# -----------------------------------------------------------------------------
+# Memory Backend URL (REQUIRED)
+# -----------------------------------------------------------------------------
+# URL of the selected memory backend
# Default: http://localhost:8088
#
# To start Agent Memory Server:
# docker run -p 8088:8088 -p 6379:6379 redis/agent-memory-server:latest
MEMORY_SERVER_URL=http://localhost:8088
+# -----------------------------------------------------------------------------
+# Redis Agent Memory Credentials (OPTIONAL)
+# -----------------------------------------------------------------------------
+# Required only when REDIS_MEMORY_BACKEND=redis-agent-memory
+AGENT_MEMORY_STORE_ID=
+AGENT_MEMORY_API_KEY=
+
# -----------------------------------------------------------------------------
# Redis URL (REQUIRED)
# -----------------------------------------------------------------------------
@@ -44,4 +58,3 @@ REDIS_URL=redis://localhost:6379
# Memory namespace for isolating different agent instances
# Default: travel_agent
NAMESPACE=travel_agent
-
diff --git a/examples/travel_agent_memory_hybrid/README.md b/examples/travel_agent_memory_hybrid/README.md
index 9e0f306..ebbf92d 100644
--- a/examples/travel_agent_memory_hybrid/README.md
+++ b/examples/travel_agent_memory_hybrid/README.md
@@ -104,8 +104,12 @@ cd examples/travel_agent_memory_hybrid
cat > .env << EOF
GOOGLE_API_KEY=your-google-api-key
TAVILY_API_KEY=your-tavily-api-key
+REDIS_MEMORY_BACKEND=opensource-agent-memory
MEMORY_SERVER_URL=http://localhost:8088
NAMESPACE=travel_agent_hybrid
+# Required only when REDIS_MEMORY_BACKEND=redis-agent-memory
+AGENT_MEMORY_STORE_ID=
+AGENT_MEMORY_API_KEY=
EOF
```
@@ -578,4 +582,3 @@ uv run adk web .
Copyright 2025 Google LLC and Redis, Inc.
Licensed under the Apache License, Version 2.0. See [LICENSE](../../LICENSE) for details.
-
diff --git a/examples/travel_agent_memory_hybrid/main.py b/examples/travel_agent_memory_hybrid/main.py
index 44d7f5c..f190aa8 100644
--- a/examples/travel_agent_memory_hybrid/main.py
+++ b/examples/travel_agent_memory_hybrid/main.py
@@ -56,7 +56,10 @@ def redis_session_factory(uri: str, **kwargs):
"""Factory function for creating RedisWorkingMemorySessionService from URI."""
base_url = parse_base_url(uri)
config = RedisWorkingMemorySessionServiceConfig(
+ backend=os.getenv("REDIS_MEMORY_BACKEND", "opensource-agent-memory"),
api_base_url=base_url,
+ api_key=os.getenv("AGENT_MEMORY_API_KEY"),
+ store_id=os.getenv("AGENT_MEMORY_STORE_ID"),
default_namespace=os.getenv("NAMESPACE", "travel_agent_memory_hybrid"),
model_name=os.getenv("REDIS_MEMORY_MODEL_NAME", "gpt-4o"),
context_window_max=int(os.getenv("REDIS_MEMORY_CONTEXT_WINDOW", "8000")),
@@ -71,7 +74,10 @@ def redis_memory_factory(uri: str, **kwargs):
"""Factory function for creating RedisLongTermMemoryService from URI."""
base_url = parse_base_url(uri)
config = RedisLongTermMemoryServiceConfig(
+ backend=os.getenv("REDIS_MEMORY_BACKEND", "opensource-agent-memory"),
api_base_url=base_url,
+ api_key=os.getenv("AGENT_MEMORY_API_KEY"),
+ store_id=os.getenv("AGENT_MEMORY_STORE_ID"),
default_namespace=os.getenv("NAMESPACE", "travel_agent_memory_hybrid"),
extraction_strategy=os.getenv(
"REDIS_MEMORY_EXTRACTION_STRATEGY", "discrete"
@@ -111,6 +117,7 @@ def redis_memory_factory(uri: str, **kwargs):
port = int(os.environ.get("PORT", 8080))
namespace = os.getenv("NAMESPACE", "travel_agent_memory_hybrid")
server = os.getenv("MEMORY_SERVER_URL", "http://localhost:8088")
+ backend = os.getenv("REDIS_MEMORY_BACKEND", "opensource-agent-memory")
extraction = os.getenv("REDIS_MEMORY_EXTRACTION_STRATEGY", "discrete")
context_window = os.getenv("REDIS_MEMORY_CONTEXT_WINDOW", "8000")
@@ -120,6 +127,7 @@ def redis_memory_factory(uri: str, **kwargs):
===============================
ADK Server: http://localhost:{port}
Memory Server: {server}
+Memory Backend: {backend}
Namespace: {namespace}
Extraction Strategy: {extraction}
Context Window: {context_window} tokens
diff --git a/examples/travel_agent_memory_hybrid/travel_agent/agent.py b/examples/travel_agent_memory_hybrid/travel_agent/agent.py
index 637c86c..6db51dc 100644
--- a/examples/travel_agent_memory_hybrid/travel_agent/agent.py
+++ b/examples/travel_agent_memory_hybrid/travel_agent/agent.py
@@ -45,6 +45,7 @@
# Configuration from environment
MEMORY_SERVER_URL = os.getenv("MEMORY_SERVER_URL", "http://localhost:8088")
+MEMORY_BACKEND = os.getenv("REDIS_MEMORY_BACKEND", "opensource-agent-memory")
NAMESPACE = os.getenv("NAMESPACE", "travel_agent_memory_hybrid")
@@ -59,7 +60,10 @@ async def after_agent(callback_context: CallbackContext):
# Configure memory tools
memory_config = MemoryToolConfig(
+ backend=MEMORY_BACKEND,
api_base_url=MEMORY_SERVER_URL,
+ api_key=os.getenv("AGENT_MEMORY_API_KEY"),
+ store_id=os.getenv("AGENT_MEMORY_STORE_ID"),
default_namespace=NAMESPACE,
recency_boost=True,
search_top_k=10,
diff --git a/examples/travel_agent_memory_tools/.env.example b/examples/travel_agent_memory_tools/.env.example
index af71651..c54cab5 100644
--- a/examples/travel_agent_memory_tools/.env.example
+++ b/examples/travel_agent_memory_tools/.env.example
@@ -19,15 +19,29 @@ GOOGLE_API_KEY=your-google-api-key-here
TAVILY_API_KEY=your-tavily-api-key-here
# -----------------------------------------------------------------------------
-# Agent Memory Server URL (REQUIRED)
+# Memory Backend (OPTIONAL)
# -----------------------------------------------------------------------------
-# URL of the Redis Agent Memory Server
+# Use opensource-agent-memory for the self-hosted Agent Memory Server.
+# Use redis-agent-memory for Redis Agent Memory.
+REDIS_MEMORY_BACKEND=opensource-agent-memory
+
+# -----------------------------------------------------------------------------
+# Memory Backend URL (REQUIRED)
+# -----------------------------------------------------------------------------
+# URL of the selected memory backend
# Default: http://localhost:8088
#
# To start Agent Memory Server:
# docker run -p 8088:8088 -p 6379:6379 redis/agent-memory-server:latest
MEMORY_SERVER_URL=http://localhost:8088
+# -----------------------------------------------------------------------------
+# Redis Agent Memory Credentials (OPTIONAL)
+# -----------------------------------------------------------------------------
+# Required only when REDIS_MEMORY_BACKEND=redis-agent-memory
+AGENT_MEMORY_STORE_ID=
+AGENT_MEMORY_API_KEY=
+
# -----------------------------------------------------------------------------
# Redis URL (REQUIRED)
# -----------------------------------------------------------------------------
@@ -44,4 +58,3 @@ REDIS_URL=redis://localhost:6379
# Memory namespace for isolating different agent instances
# Default: travel_agent
NAMESPACE=travel_agent
-
diff --git a/examples/travel_agent_memory_tools/README.md b/examples/travel_agent_memory_tools/README.md
index 5d5db60..3f4d6b8 100644
--- a/examples/travel_agent_memory_tools/README.md
+++ b/examples/travel_agent_memory_tools/README.md
@@ -75,7 +75,8 @@ docker run -d --name agent-memory-server -p 8088:8088 \
```bash
cd examples/travel_agent_memory_tools
cp .env.example .env
-# Edit .env and add your GOOGLE_API_KEY and TAVILY_API_KEY
+# Edit .env and add your GOOGLE_API_KEY and TAVILY_API_KEY.
+# REDIS_MEMORY_BACKEND defaults to opensource-agent-memory.
```
### (Optional) Seed Demo User Profiles
@@ -537,4 +538,3 @@ uv run adk web .
Copyright 2025 Google LLC and Redis, Inc.
Licensed under the Apache License, Version 2.0. See [LICENSE](../../LICENSE) for details.
-
diff --git a/examples/travel_agent_memory_tools/travel_agent/agent.py b/examples/travel_agent_memory_tools/travel_agent/agent.py
index e02171a..e80d359 100644
--- a/examples/travel_agent_memory_tools/travel_agent/agent.py
+++ b/examples/travel_agent_memory_tools/travel_agent/agent.py
@@ -40,6 +40,7 @@
# Configuration from environment
MEMORY_SERVER_URL = os.getenv("MEMORY_SERVER_URL", "http://localhost:8088")
+MEMORY_BACKEND = os.getenv("REDIS_MEMORY_BACKEND", "opensource-agent-memory")
NAMESPACE = os.getenv("NAMESPACE", "travel_agent_memory_tools")
@@ -54,7 +55,10 @@ async def after_agent(callback_context: CallbackContext):
# Configure memory tools
memory_config = MemoryToolConfig(
+ backend=MEMORY_BACKEND,
api_base_url=MEMORY_SERVER_URL,
+ api_key=os.getenv("AGENT_MEMORY_API_KEY"),
+ store_id=os.getenv("AGENT_MEMORY_STORE_ID"),
default_namespace=NAMESPACE,
recency_boost=True,
search_top_k=10,
diff --git a/pyproject.toml b/pyproject.toml
index f72de45..40eded4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "adk-redis"
-version = "0.0.6"
+version = "0.0.7"
description = "Redis integrations for Google's Agent Development Kit (ADK)"
readme = "README.md"
license = "Apache-2.0"
@@ -40,9 +40,10 @@ dependencies = [
]
[project.optional-dependencies]
-# Redis Agent Memory Server integration (long-term memory + working memory sessions)
+# Redis Agent Memory integrations (long-term memory + working memory sessions)
memory = [
"agent-memory-client>=0.14.0",
+ "redis-agent-memory>=0.0.4",
]
# RedisVL-based search tools
diff --git a/src/adk_redis/__init__.py b/src/adk_redis/__init__.py
index cd1bc56..b29c31d 100644
--- a/src/adk_redis/__init__.py
+++ b/src/adk_redis/__init__.py
@@ -54,13 +54,19 @@
# Configure memory service
memory_config = RedisLongTermMemoryServiceConfig(
+ backend="redis-agent-memory",
api_base_url="http://localhost:8000",
+ api_key="...",
+ store_id="...",
)
memory_service = RedisLongTermMemoryService(config=memory_config)
# Configure session service
session_config = RedisWorkingMemorySessionServiceConfig(
+ backend="redis-agent-memory",
api_base_url="http://localhost:8000",
+ api_key="...",
+ store_id="...",
)
session_service = RedisWorkingMemorySessionService(config=session_config)
```
diff --git a/src/adk_redis/memory/_backends.py b/src/adk_redis/memory/_backends.py
new file mode 100644
index 0000000..158cba2
--- /dev/null
+++ b/src/adk_redis/memory/_backends.py
@@ -0,0 +1,27 @@
+# Copyright 2025 Redis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Memory backend names."""
+
+from __future__ import annotations
+
+from typing import Literal
+
+REDIS_AGENT_MEMORY_BACKEND = "redis-agent-memory"
+OPENSOURCE_AGENT_MEMORY_BACKEND = "opensource-agent-memory"
+
+MemoryBackendName = Literal[
+ "redis-agent-memory",
+ "opensource-agent-memory",
+]
diff --git a/src/adk_redis/memory/_utils.py b/src/adk_redis/memory/_utils.py
index 48da723..48f55a1 100644
--- a/src/adk_redis/memory/_utils.py
+++ b/src/adk_redis/memory/_utils.py
@@ -16,11 +16,19 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
+from datetime import datetime
+from datetime import timezone
+import hashlib
+import re
+from typing import Any, TYPE_CHECKING
+
+from google.genai import types
if TYPE_CHECKING:
from google.adk.events.event import Event
+_MANAGED_IDENTIFIER_RE = re.compile(r"[^A-Za-z0-9-]+")
+
def extract_text_from_event(event: "Event") -> str:
"""Extracts text content from an event's content parts.
@@ -45,3 +53,99 @@ def extract_text_from_event(event: "Event") -> str:
if part.text and not part.thought
]
return " ".join(text_parts)
+
+
+def extract_text_from_content(content: types.Content | None) -> str:
+ """Extract text content from a GenAI content object.
+
+ Args:
+ content: Content object to read.
+
+ Returns:
+ Combined non-empty text parts.
+ """
+ if not content or not content.parts:
+ return ""
+
+ text_parts = [
+ part.text.strip()
+ for part in content.parts
+ if part.text and part.text.strip()
+ ]
+ return "\n".join(text_parts)
+
+
+def timestamp_to_datetime(timestamp: float) -> datetime:
+ """Convert a Unix timestamp to a UTC datetime.
+
+ Args:
+ timestamp: Unix timestamp in seconds.
+
+ Returns:
+ Timezone-aware UTC datetime.
+ """
+ return datetime.fromtimestamp(timestamp, tz=timezone.utc)
+
+
+def stable_memory_id(*parts: object) -> str:
+ """Build a deterministic memory ID from stable input parts.
+
+ Args:
+ *parts: Values that uniquely identify the memory.
+
+ Returns:
+ A deterministic Redis Agent Memory ID.
+ """
+ digest = hashlib.sha256(
+ ":".join(str(part) for part in parts).encode("utf-8")
+ ).hexdigest()
+ return f"adk-{digest[:32]}"
+
+
+def read_field(value: object, name: str, default: Any = None) -> Any:
+ """Read a field from either a dict or an object.
+
+ Args:
+ value: Mapping or object to inspect.
+ name: Field or attribute name.
+ default: Value returned when the field is absent.
+
+ Returns:
+ Field value or default.
+ """
+ if isinstance(value, dict):
+ return value.get(name, default)
+ return getattr(value, name, default)
+
+
+def sanitize_managed_identifier(value: str) -> str:
+ """Normalize a value for managed Redis Agent Memory identifiers.
+
+ Managed APIs accept only alphanumeric characters and hyphens in fields
+ such as namespace, sessionId, and actorId.
+
+ Args:
+ value: Raw identifier from ADK app names, namespaces, or authors.
+
+ Returns:
+ Sanitized identifier with unsupported characters replaced by hyphens.
+ """
+ sanitized = _MANAGED_IDENTIFIER_RE.sub("-", value.strip()).strip("-")
+ if not sanitized:
+ raise ValueError("Managed identifier must not be empty after sanitization")
+ return sanitized
+
+
+def is_not_found_error(exc: Exception) -> bool:
+ """Return True when a Redis Agent Memory exception represents a 404.
+
+ Args:
+ exc: Exception raised by the Redis Agent Memory SDK.
+
+ Returns:
+ True when the SDK reported a missing resource.
+ """
+ return (
+ getattr(exc, "status_code", None) == 404
+ or exc.__class__.__name__ == "NotFoundErrorResponseContent"
+ )
diff --git a/src/adk_redis/memory/long_term_memory.py b/src/adk_redis/memory/long_term_memory.py
index 0a8d9aa..6e89c4f 100644
--- a/src/adk_redis/memory/long_term_memory.py
+++ b/src/adk_redis/memory/long_term_memory.py
@@ -12,66 +12,124 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Redis Long-Term Memory Service for ADK.
-
-This module provides integration with the Redis Agent Memory Server,
-offering production-grade long-term memory with automatic summarization,
-topic/entity extraction, and recency-boosted search.
-
-Note: The classes were renamed from RedisAgentMemoryService to
-RedisLongTermMemoryService to better reflect their purpose of managing
-long-term memory via the Agent Memory Server.
-"""
+"""Redis long-term memory service for ADK."""
from __future__ import annotations
+from collections.abc import Mapping
+from collections.abc import Sequence
+from contextlib import asynccontextmanager
from functools import cached_property
+from importlib import import_module
import logging
-from typing import Any, Literal, TYPE_CHECKING
+from typing import Any, AsyncIterator, Literal
+from google.adk.events.event import Event
from google.adk.memory.base_memory_service import BaseMemoryService
from google.adk.memory.base_memory_service import SearchMemoryResponse
from google.adk.memory.memory_entry import MemoryEntry
+from google.adk.sessions.session import Session
from google.genai import types
from pydantic import BaseModel
from pydantic import Field
from typing_extensions import override
+from adk_redis.memory._backends import MemoryBackendName
+from adk_redis.memory._backends import OPENSOURCE_AGENT_MEMORY_BACKEND
+from adk_redis.memory._backends import REDIS_AGENT_MEMORY_BACKEND
+from adk_redis.memory._utils import extract_text_from_content
from adk_redis.memory._utils import extract_text_from_event
-
-if TYPE_CHECKING:
- from google.adk.sessions.session import Session
+from adk_redis.memory._utils import read_field
+from adk_redis.memory._utils import sanitize_managed_identifier
+from adk_redis.memory._utils import stable_memory_id
logger = logging.getLogger("adk_redis." + __name__)
+try:
+ _agent_memory_client_module = import_module("agent_memory_client")
+ _agent_memory_models_module = import_module("agent_memory_client.models")
+except ImportError:
+ _MemoryAPIClient: Any = None
+ _MemoryClientConfig: Any = None
+ _MemoryMessage: Any = None
+ _MemoryStrategyConfig: Any = None
+ _RecencyConfig: Any = None
+ _WorkingMemory: Any = None
+else:
+ _MemoryAPIClient = _agent_memory_client_module.MemoryAPIClient
+ _MemoryClientConfig = _agent_memory_client_module.MemoryClientConfig
+ _MemoryMessage = _agent_memory_models_module.MemoryMessage
+ _MemoryStrategyConfig = _agent_memory_models_module.MemoryStrategyConfig
+ _RecencyConfig = _agent_memory_models_module.RecencyConfig
+ _WorkingMemory = _agent_memory_models_module.WorkingMemory
+
+try:
+ _redis_agent_memory_module = import_module("redis_agent_memory")
+except ImportError:
+ _AgentMemory: Any = None
+else:
+ _AgentMemory = _redis_agent_memory_module.AgentMemory
+
+MemoryTypeName = Literal["semantic", "episodic", "message"]
+
class RedisLongTermMemoryServiceConfig(BaseModel):
- """Configuration for Redis Long-Term Memory Service.
+ """Configuration for Redis long-term memory.
Attributes:
- api_base_url: Base URL of the Agent Memory Server.
- timeout: HTTP request timeout in seconds.
+ backend: Memory backend to use.
+ api_base_url: Memory API base URL.
+ api_key: Redis Agent Memory API key.
+ store_id: Redis Agent Memory store ID.
+ timeout: HTTP timeout in seconds.
+ timeout_ms: Optional SDK timeout in milliseconds.
default_namespace: Default namespace for memory operations.
search_top_k: Maximum number of memories to retrieve per search.
- distance_threshold: Maximum distance threshold for search results (0.0-1.0).
- recency_boost: Enable recency-aware re-ranking of search results.
- semantic_weight: Weight for semantic similarity in recency boosting (0.0-1.0).
- recency_weight: Weight for recency score in recency boosting (0.0-1.0).
- freshness_weight: Weight for freshness component within recency score.
- novelty_weight: Weight for novelty component within recency score.
- half_life_last_access_days: Half-life in days for last_accessed decay.
- half_life_created_days: Half-life in days for created_at decay.
- extraction_strategy: Memory extraction strategy (discrete, summary, preferences, custom).
- extraction_strategy_config: Additional configuration for the extraction strategy.
- model_name: Model name for context window management and summarization.
- context_window_max: Maximum context window tokens (overrides model default).
+ similarity_threshold: Minimum similarity threshold for search results.
+ distance_threshold: Backward-compatible alias for similarity_threshold.
+ store_events_as_messages: Store ADK events as long-term message memories
+ when add_session_to_memory or add_events_to_memory is called.
+ default_memory_type: Default memory type for explicit add_memory writes.
+ default_topics: Topics attached to created memory records.
+ source: Source label used when building deterministic IDs.
+ recency_boost: Backward-compatible option retained from the previous
+ Agent Memory Server client.
+ semantic_weight: Backward-compatible option retained from the previous
+ Agent Memory Server client.
+ recency_weight: Backward-compatible option retained from the previous
+ Agent Memory Server client.
+ freshness_weight: Backward-compatible option retained from the previous
+ Agent Memory Server client.
+ novelty_weight: Backward-compatible option retained from the previous
+ Agent Memory Server client.
+ half_life_last_access_days: Backward-compatible option retained from the
+ previous Agent Memory Server client.
+ half_life_created_days: Backward-compatible option retained from the
+ previous Agent Memory Server client.
+ extraction_strategy: Backward-compatible option retained from the
+ previous Agent Memory Server client.
+ extraction_strategy_config: Backward-compatible option retained from the
+ previous Agent Memory Server client.
+ model_name: Backward-compatible option retained from the previous Agent
+ Memory Server client.
+ context_window_max: Backward-compatible option retained from the
+ previous Agent Memory Server client.
"""
+ backend: MemoryBackendName = "redis-agent-memory"
api_base_url: str = Field(default="http://localhost:8000")
+ api_key: str | None = None
+ store_id: str | None = None
timeout: float = Field(default=30.0, gt=0.0)
+ timeout_ms: int | None = Field(default=None, ge=1)
default_namespace: str | None = None
search_top_k: int = Field(default=10, ge=1)
+ similarity_threshold: float | None = Field(default=None, ge=0.0, le=1.0)
distance_threshold: float | None = Field(default=None, ge=0.0, le=1.0)
+ store_events_as_messages: bool = True
+ default_memory_type: MemoryTypeName = "semantic"
+ default_topics: list[str] = Field(default_factory=list)
+ source: str = "adk-redis"
recency_boost: bool = True
semantic_weight: float = Field(default=0.8, ge=0.0, le=1.0)
recency_weight: float = Field(default=0.2, ge=0.0, le=1.0)
@@ -88,79 +146,208 @@ class RedisLongTermMemoryServiceConfig(BaseModel):
class RedisLongTermMemoryService(BaseMemoryService):
- """Long-term memory service implementation using Redis Agent Memory Server.
-
- This service provides production-grade memory capabilities including:
- - Two-tier memory architecture (working memory + long-term memory)
- - Automatic memory extraction (semantic facts, episodic events, preferences)
- - Topic and entity extraction
- - Auto-summarization when context window is exceeded
- - Recency-boosted semantic search
- - Deduplication and memory compaction
- - https://github.com/redis/agent-memory-server
-
- Requires the `agent-memory-client` package to be installed.
-
- Example:
- ```python
- from adk_redis import (
- RedisLongTermMemoryService,
- RedisLongTermMemoryServiceConfig,
- )
+ """Long-term memory service backed by Redis memory backends.
- config = RedisLongTermMemoryServiceConfig(
- api_base_url="http://localhost:8000",
- default_namespace="my_app",
- recency_boost=True,
- )
- memory_service = RedisLongTermMemoryService(config=config)
+ The service implements ADK's memory interface using either Redis Agent
+ Memory or the self-hosted Agent Memory Server. Searches are scoped by user
+ ID and namespace for both backends.
- # Use with ADK agent
- agent = Agent(
- name="my_agent",
- memory_service=memory_service,
- )
- ```
+ Latest ADK versions add explicit `add_events_to_memory` and `add_memory`
+ hooks. This service implements them while staying compatible with older ADK
+ versions that only call `add_session_to_memory` and `search_memory`.
"""
def __init__(self, config: RedisLongTermMemoryServiceConfig | None = None):
- """Initialize the Redis Long-Term Memory Service.
+ """Initialize the Redis long-term memory service.
Args:
- config: Configuration for the service. If None, uses defaults.
-
- Raises:
- ImportError: If agent-memory-client package is not installed.
+ config: Configuration for the service. If None, defaults are used.
"""
self._config = config or RedisLongTermMemoryServiceConfig()
+ def _timeout_ms(self) -> int:
+ """Return the configured SDK timeout in milliseconds."""
+ return self._config.timeout_ms or int(self._config.timeout * 1000)
+
+ def _get_client(self) -> Any:
+ """Get a backend client for compatibility with existing tests."""
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ return self._get_agent_memory_server_client()
+ return self._get_redis_agent_memory_client()
+
+ def _get_redis_agent_memory_client(self) -> Any:
+ """Get a Redis Agent Memory SDK client."""
+ if _AgentMemory is None:
+ raise ImportError(
+ "redis-agent-memory package is required for "
+ "RedisLongTermMemoryService. "
+ "Install it with: pip install adk-redis[memory]"
+ )
+
+ return _AgentMemory(
+ self._config.api_base_url,
+ api_key=self._config.api_key,
+ store_id=self._config.store_id,
+ timeout_ms=self._timeout_ms(),
+ )
+
@cached_property
- def _client(self) -> Any:
- """Lazily initialize and return the MemoryAPIClient."""
- try:
- from agent_memory_client import MemoryAPIClient
- from agent_memory_client import MemoryClientConfig
- except ImportError as e:
+ def _agent_memory_server_client(self) -> Any:
+ """Lazily initialize and return the self-hosted memory client."""
+ if _MemoryAPIClient is None or _MemoryClientConfig is None:
raise ImportError(
- "agent-memory-client package is required for"
- " RedisLongTermMemoryService. Install it with: pip install"
- " adk-redis[memory]"
- ) from e
+ "agent-memory-client package is required for the "
+ "opensource-agent-memory backend. Install it with: "
+ "pip install adk-redis[memory]"
+ )
- client_config = MemoryClientConfig(
+ client_config = _MemoryClientConfig(
base_url=self._config.api_base_url,
timeout=self._config.timeout,
default_namespace=self._config.default_namespace,
default_model_name=self._config.model_name,
default_context_window_max=self._config.context_window_max,
)
- return MemoryAPIClient(client_config)
+ return _MemoryAPIClient(client_config)
+
+ def _get_agent_memory_server_client(self) -> Any:
+ """Get the self-hosted Agent Memory Server client."""
+ return self._agent_memory_server_client
+
+ @asynccontextmanager
+ async def _agent_memory(self) -> AsyncIterator[Any]:
+ """Yield a Redis Agent Memory client and close SDK resources."""
+ client = self._get_client()
+ if hasattr(client, "__aenter__"):
+ async with client as agent_memory:
+ yield agent_memory
+ else:
+ yield client
+
+ def _get_namespace(
+ self,
+ app_name: str,
+ custom_metadata: Mapping[str, object] | None = None,
+ ) -> str:
+ """Get namespace from metadata, config, or app_name."""
+ if custom_metadata:
+ namespace = custom_metadata.get("namespace")
+ if isinstance(namespace, str) and namespace:
+ resolved = namespace
+ else:
+ resolved = self._config.default_namespace or app_name
+ else:
+ resolved = self._config.default_namespace or app_name
+
+ if self._config.backend == REDIS_AGENT_MEMORY_BACKEND:
+ return sanitize_managed_identifier(resolved)
+ return resolved
+
+ def _search_threshold(self) -> float | None:
+ """Return the configured search threshold."""
+ return (
+ self._config.similarity_threshold
+ if self._config.similarity_threshold is not None
+ else self._config.distance_threshold
+ )
+
+ def _coerce_topics(
+ self,
+ *,
+ app_name: str,
+ custom_metadata: Mapping[str, object] | None = None,
+ memory: MemoryEntry | None = None,
+ ) -> list[str] | None:
+ """Build topics for a memory record."""
+ topics: list[str] = list(self._config.default_topics)
+ metadata_values = []
+ if custom_metadata:
+ metadata_values.append(custom_metadata.get("topics"))
+ if memory and memory.custom_metadata:
+ metadata_values.append(memory.custom_metadata.get("topics"))
+
+ for value in metadata_values:
+ if isinstance(value, str):
+ topics.append(value)
+ elif isinstance(value, Sequence) and not isinstance(value, str):
+ topics.extend(str(item) for item in value if item)
+
+ if app_name not in topics:
+ topics.append(app_name)
+ return topics or None
+
+ def _coerce_memory_type(
+ self,
+ value: object | None = None,
+ *,
+ default: MemoryTypeName | None = None,
+ ) -> MemoryTypeName:
+ """Normalize memory type aliases."""
+ raw = str(value or default or self._config.default_memory_type).lower()
+ memory_type_map = {
+ "preference": "semantic",
+ "fact": "semantic",
+ "event": "episodic",
+ "experience": "episodic",
+ "conversation": "message",
+ }
+ memory_type = memory_type_map.get(raw, raw)
+ if memory_type not in ("semantic", "episodic", "message"):
+ return self._config.default_memory_type
+ return memory_type # type: ignore[return-value]
+
+ def _memory_type_from_metadata(
+ self,
+ custom_metadata: Mapping[str, object] | None,
+ memory: MemoryEntry | None = None,
+ *,
+ default: MemoryTypeName | None = None,
+ ) -> MemoryTypeName:
+ """Read memory type from metadata."""
+ candidates = []
+ if memory and memory.custom_metadata:
+ candidates.append(memory.custom_metadata.get("memory_type"))
+ if custom_metadata:
+ candidates.append(custom_metadata.get("memory_type"))
+
+ for candidate in candidates:
+ if candidate:
+ return self._coerce_memory_type(candidate, default=default)
+ return self._coerce_memory_type(default=default)
+
+ def _build_recency_config(self) -> Any:
+ """Build RecencyConfig for the self-hosted memory backend."""
+ if _RecencyConfig is None:
+ raise ImportError(
+ "agent-memory-client package is required for recency search. "
+ "Install it with: pip install adk-redis[memory]"
+ )
+
+ return _RecencyConfig(
+ recency_boost=self._config.recency_boost,
+ semantic_weight=self._config.semantic_weight,
+ recency_weight=self._config.recency_weight,
+ freshness_weight=self._config.freshness_weight,
+ novelty_weight=self._config.novelty_weight,
+ half_life_last_access_days=self._config.half_life_last_access_days,
+ half_life_created_days=self._config.half_life_created_days,
+ )
- def _build_working_memory(self, session: "Session") -> Any:
- """Convert ADK Session to WorkingMemory for the Agent Memory Server."""
- from agent_memory_client.models import MemoryMessage
- from agent_memory_client.models import MemoryStrategyConfig
- from agent_memory_client.models import WorkingMemory
+ def _build_agent_memory_server_working_memory(
+ self,
+ session: Session,
+ custom_metadata: Mapping[str, object] | None = None,
+ ) -> Any:
+ """Convert an ADK session to self-hosted WorkingMemory."""
+ if (
+ _MemoryMessage is None
+ or _MemoryStrategyConfig is None
+ or _WorkingMemory is None
+ ):
+ raise ImportError(
+ "agent-memory-client package is required for working memory. "
+ "Install it with: pip install adk-redis[memory]"
+ )
messages = []
for event in session.events:
@@ -168,115 +355,454 @@ def _build_working_memory(self, session: "Session") -> Any:
if not text:
continue
role = "user" if event.author == "user" else "assistant"
- messages.append(MemoryMessage(role=role, content=text))
+ messages.append(_MemoryMessage(role=role, content=text))
- strategy_config = MemoryStrategyConfig(
+ strategy_config = _MemoryStrategyConfig(
strategy=self._config.extraction_strategy,
config=self._config.extraction_strategy_config,
)
- return WorkingMemory(
+ return _WorkingMemory(
session_id=session.id,
- namespace=self._config.default_namespace or session.app_name,
+ namespace=self._get_namespace(session.app_name, custom_metadata),
user_id=session.user_id,
messages=messages,
long_term_memory_strategy=strategy_config,
)
- @override
- async def add_session_to_memory(self, session: "Session") -> None:
- """Add a session's events to the Agent Memory Server.
+ async def _add_session_to_agent_memory_server(
+ self,
+ session: Session,
+ custom_metadata: Mapping[str, object] | None = None,
+ ) -> None:
+ """Store a session in the self-hosted Agent Memory Server."""
+ working_memory = self._build_agent_memory_server_working_memory(
+ session,
+ custom_metadata,
+ )
+ if not working_memory.messages:
+ logger.debug("No messages to store for session %s", session.id)
+ return
+
+ response = await self._get_agent_memory_server_client().put_working_memory(
+ session_id=session.id,
+ memory=working_memory,
+ user_id=session.user_id,
+ )
+ logger.info(
+ "Stored %d messages for session %s (context: %.1f%% used)",
+ len(working_memory.messages),
+ session.id,
+ getattr(response, "context_percentage_total_used", None) or 0,
+ )
+
+ async def _add_memory_to_agent_memory_server(
+ self,
+ *,
+ app_name: str,
+ user_id: str,
+ memories: Sequence[MemoryEntry],
+ custom_metadata: Mapping[str, object] | None,
+ ) -> None:
+ """Store explicit memories through the self-hosted memory API."""
+ namespace = self._get_namespace(app_name, custom_metadata)
+ client = self._get_agent_memory_server_client()
+
+ for index, memory in enumerate(memories):
+ text = extract_text_from_content(memory.content).strip()
+ if not text:
+ raise ValueError(f"memories[{index}] must include non-empty text")
+
+ merged_metadata: dict[str, object] = {}
+ if custom_metadata:
+ merged_metadata.update(custom_metadata)
+ if memory.custom_metadata:
+ merged_metadata.update(memory.custom_metadata)
+
+ session_id = str(
+ merged_metadata.get("session_id")
+ or stable_memory_id(
+ self._config.source,
+ "explicit",
+ namespace,
+ user_id,
+ text,
+ )
+ )
+ await client.add_memory_tool(
+ session_id=session_id,
+ text=text,
+ memory_type=self._memory_type_from_metadata(
+ custom_metadata,
+ memory,
+ ),
+ topics=self._coerce_topics(
+ app_name=app_name,
+ custom_metadata=custom_metadata,
+ memory=memory,
+ ),
+ namespace=namespace,
+ user_id=user_id,
+ )
+
+ def _event_record_text(self, event: Event) -> str:
+ """Build long-term message memory text for an ADK event."""
+ text = extract_text_from_event(event).strip()
+ if not text:
+ return ""
+ author = event.author or "agent"
+ return f"{author}: {text}"
+
+ def _build_event_records(
+ self,
+ *,
+ app_name: str,
+ user_id: str,
+ events: Sequence[Event],
+ session_id: str | None,
+ custom_metadata: Mapping[str, object] | None,
+ ) -> list[dict[str, Any]]:
+ """Convert ADK events into Redis Agent Memory records."""
+ namespace = self._get_namespace(app_name, custom_metadata)
+ memory_type = self._memory_type_from_metadata(
+ custom_metadata,
+ default="message",
+ )
+ records = []
+
+ for event in events:
+ text = self._event_record_text(event)
+ if not text:
+ continue
+ event_id = event.id or event.timestamp
+ records.append(
+ {
+ "id": stable_memory_id(
+ self._config.source,
+ "event",
+ namespace,
+ user_id,
+ session_id or "",
+ event_id,
+ text,
+ ),
+ "text": text,
+ "ownerId": user_id,
+ "namespace": namespace,
+ "sessionId": session_id,
+ "topics": self._coerce_topics(
+ app_name=app_name,
+ custom_metadata=custom_metadata,
+ ),
+ "memoryType": memory_type,
+ }
+ )
+
+ return records
+
+ def _build_memory_records(
+ self,
+ *,
+ app_name: str,
+ user_id: str,
+ memories: Sequence[MemoryEntry],
+ custom_metadata: Mapping[str, object] | None,
+ ) -> list[dict[str, Any]]:
+ """Convert ADK MemoryEntry objects into Redis Agent Memory records."""
+ namespace = self._get_namespace(app_name, custom_metadata)
+ records = []
+
+ for index, memory in enumerate(memories):
+ text = extract_text_from_content(memory.content).strip()
+ if not text:
+ raise ValueError(f"memories[{index}] must include non-empty text")
+
+ merged_metadata: dict[str, object] = {}
+ if custom_metadata:
+ merged_metadata.update(custom_metadata)
+ if memory.custom_metadata:
+ merged_metadata.update(memory.custom_metadata)
+
+ session_id = merged_metadata.get("session_id")
+ records.append(
+ {
+ "id": memory.id
+ or stable_memory_id(
+ self._config.source,
+ "memory",
+ namespace,
+ user_id,
+ text,
+ ),
+ "text": text,
+ "ownerId": user_id,
+ "namespace": namespace,
+ "sessionId": str(session_id) if session_id else None,
+ "topics": self._coerce_topics(
+ app_name=app_name,
+ custom_metadata=custom_metadata,
+ memory=memory,
+ ),
+ "memoryType": self._memory_type_from_metadata(
+ custom_metadata,
+ memory,
+ ),
+ }
+ )
- Converts ADK Session events to WorkingMemory messages and stores them
- in the Agent Memory Server. The server will automatically:
- - Extract semantic and episodic memories based on the configured strategy
- - Perform topic and entity extraction
- - Summarize context when the token limit is exceeded
- - Promote memories to long-term storage via background tasks
+ return records
+
+ async def _bulk_create(self, records: list[dict[str, Any]]) -> None:
+ """Create long-term memory records in Redis Agent Memory."""
+ if not records:
+ return
+ async with self._agent_memory() as agent_memory:
+ response = await agent_memory.bulk_create_long_term_memories_async(
+ memories=records
+ )
+ errors = read_field(response, "errors")
+ if errors:
+ logger.warning("Redis Agent Memory reported write errors: %s", errors)
+
+ @override
+ async def add_session_to_memory(self, session: Session) -> None:
+ """Add an ADK session to the configured long-term memory backend.
Args:
session: The ADK Session containing events to store.
"""
- try:
- working_memory = self._build_working_memory(session)
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ try:
+ await self._add_session_to_agent_memory_server(session)
+ except Exception as e:
+ logger.error(
+ "Failed to add session %s to memory: %s",
+ session.id,
+ e,
+ )
+ return
+
+ await self.add_events_to_memory(
+ app_name=session.app_name,
+ user_id=session.user_id,
+ events=session.events,
+ session_id=session.id,
+ )
+
+ async def add_events_to_memory(
+ self,
+ *,
+ app_name: str,
+ user_id: str,
+ events: Sequence[Event],
+ session_id: str | None = None,
+ custom_metadata: Mapping[str, object] | None = None,
+ ) -> None:
+ """Add ADK events to the configured memory backend."""
+ if not self._config.store_events_as_messages:
+ logger.debug("Event to memory storage is disabled")
+ return
- if not working_memory.messages:
- logger.debug("No messages to store for session %s", session.id)
+ try:
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ session = Session(
+ id=session_id
+ or stable_memory_id(
+ self._config.source,
+ "events",
+ app_name,
+ user_id,
+ len(events),
+ ),
+ app_name=app_name,
+ user_id=user_id,
+ events=list(events),
+ )
+ await self._add_session_to_agent_memory_server(
+ session,
+ custom_metadata,
+ )
return
- response = await self._client.put_working_memory(
- session_id=session.id,
- memory=working_memory,
- user_id=session.user_id,
+ records = self._build_event_records(
+ app_name=app_name,
+ user_id=user_id,
+ events=events,
+ session_id=session_id,
+ custom_metadata=custom_metadata,
)
+ await self._bulk_create(records)
+ logger.info("Stored %d event memory records", len(records))
+ except Exception as e:
+ logger.error("Failed to add events to memory: %s", e)
+
+ async def add_memory(
+ self,
+ *,
+ app_name: str,
+ user_id: str,
+ memories: Sequence[MemoryEntry],
+ custom_metadata: Mapping[str, object] | None = None,
+ ) -> None:
+ """Add explicit durable memories to the configured backend."""
+ try:
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ await self._add_memory_to_agent_memory_server(
+ app_name=app_name,
+ user_id=user_id,
+ memories=memories,
+ custom_metadata=custom_metadata,
+ )
+ logger.info("Stored %d explicit memory records", len(memories))
+ return
- logger.info(
- "Stored %d messages for session %s (context: %.1f%% used)",
- len(working_memory.messages),
- session.id,
- response.context_percentage_total_used or 0,
+ records = self._build_memory_records(
+ app_name=app_name,
+ user_id=user_id,
+ memories=memories,
+ custom_metadata=custom_metadata,
)
-
+ await self._bulk_create(records)
+ logger.info("Stored %d explicit memory records", len(records))
except Exception as e:
- logger.error(
- "Failed to add session %s to memory: %s",
- session.id,
- e,
- )
+ logger.error("Failed to add explicit memories: %s", e)
- def _build_recency_config(self) -> Any:
- """Build RecencyConfig from service configuration."""
- from agent_memory_client.models import RecencyConfig
+ def _coerce_memories(self, response: object) -> list[object]:
+ """Coerce SDK search responses into a list of memory records."""
+ if isinstance(response, dict):
+ return list(response.get("items", response.get("memories", [])) or [])
- return RecencyConfig(
- recency_boost=self._config.recency_boost,
- semantic_weight=self._config.semantic_weight,
- recency_weight=self._config.recency_weight,
- freshness_weight=self._config.freshness_weight,
- novelty_weight=self._config.novelty_weight,
- half_life_last_access_days=self._config.half_life_last_access_days,
- half_life_created_days=self._config.half_life_created_days,
+ items = getattr(response, "items", None)
+ if items is not None:
+ return list(items)
+
+ memories = getattr(response, "memories", None)
+ return list(memories or [])
+
+ def _memory_type_value(self, memory_type: object) -> str | None:
+ """Return a JSON-friendly memory type."""
+ if memory_type is None:
+ return None
+ return str(getattr(memory_type, "value", memory_type))
+
+ async def _search_agent_memory_server(
+ self, *, app_name: str, user_id: str, query: str
+ ) -> SearchMemoryResponse:
+ """Search memories through the self-hosted Agent Memory Server."""
+ recency_config = (
+ self._build_recency_config() if self._config.recency_boost else None
+ )
+ namespace = self._get_namespace(app_name)
+
+ results = (
+ await self._get_agent_memory_server_client().search_long_term_memory(
+ text=query,
+ namespace={"eq": namespace},
+ user_id={"eq": user_id},
+ distance_threshold=self._config.distance_threshold,
+ recency=recency_config,
+ limit=self._config.search_top_k,
+ )
)
+ memories = []
+ for record in getattr(results, "memories", []) or []:
+ text = str(getattr(record, "text", "") or "")
+ if not text:
+ continue
+
+ created_at = getattr(record, "created_at", None)
+ timestamp = created_at.isoformat() if created_at else None
+ content = types.Content(parts=[types.Part(text=text)])
+ memories.append(
+ MemoryEntry(
+ id=getattr(record, "id", None),
+ timestamp=timestamp,
+ content=content,
+ custom_metadata={
+ "namespace": getattr(record, "namespace", namespace),
+ "user_id": getattr(record, "user_id", user_id),
+ "session_id": getattr(record, "session_id", None),
+ "topics": getattr(record, "topics", []) or [],
+ "memory_type": getattr(record, "memory_type", None),
+ },
+ )
+ )
+
+ logger.info(
+ "Found %d memories for query '%s' (namespace=%s, user=%s)",
+ len(memories),
+ query[:50],
+ namespace,
+ user_id,
+ )
+ return SearchMemoryResponse(memories=memories)
+
@override
async def search_memory(
self, *, app_name: str, user_id: str, query: str
) -> SearchMemoryResponse:
- """Search for memories using the Agent Memory Server.
-
- Performs semantic search against long-term memory with optional
- recency boosting. Results are filtered by namespace (derived from
- app_name) and user_id.
+ """Search for memories using the configured memory backend.
Args:
- app_name: The application name (used as namespace if not configured).
- user_id: The user ID to filter memories.
- query: The search query for semantic matching.
+ app_name: Application name, used as namespace if no default is set.
+ user_id: Owner ID used to isolate memories.
+ query: Search query for semantic matching.
Returns:
SearchMemoryResponse containing matching MemoryEntry objects.
"""
try:
- recency_config = (
- self._build_recency_config() if self._config.recency_boost else None
- )
-
- namespace = self._config.default_namespace or app_name
-
- results = await self._client.search_long_term_memory(
- text=query,
- namespace={"eq": namespace},
- user_id={"eq": user_id},
- distance_threshold=self._config.distance_threshold,
- recency=recency_config,
- limit=self._config.search_top_k,
- )
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ return await self._search_agent_memory_server(
+ app_name=app_name,
+ user_id=user_id,
+ query=query,
+ )
+
+ namespace = self._get_namespace(app_name)
+ request: dict[str, Any] = {
+ "text": query,
+ "limit": self._config.search_top_k,
+ "filter": {
+ "ownerId": {"eq": user_id},
+ "namespace": {"eq": namespace},
+ },
+ "filterOp": "all",
+ }
+ threshold = self._search_threshold()
+ if threshold is not None:
+ request["similarityThreshold"] = threshold
+
+ async with self._agent_memory() as agent_memory:
+ results = await agent_memory.search_long_term_memory_async(
+ request=request
+ )
memories = []
- for record in results.memories:
- content = types.Content(parts=[types.Part(text=record.text)])
- memory_entry = MemoryEntry(content=content)
- memories.append(memory_entry)
+ for record in self._coerce_memories(results):
+ text = str(read_field(record, "text", "") or "")
+ if not text:
+ continue
+
+ created_at = read_field(record, "created_at")
+ timestamp = created_at.isoformat() if created_at else None
+ memory_type = self._memory_type_value(read_field(record, "memory_type"))
+ content = types.Content(parts=[types.Part(text=text)])
+ memories.append(
+ MemoryEntry(
+ id=read_field(record, "id"),
+ timestamp=timestamp,
+ content=content,
+ custom_metadata={
+ "namespace": read_field(record, "namespace"),
+ "owner_id": read_field(record, "owner_id"),
+ "session_id": read_field(record, "session_id"),
+ "topics": read_field(record, "topics", []) or [],
+ "memory_type": memory_type,
+ },
+ )
+ )
logger.info(
"Found %d memories for query '%s' (namespace=%s, user=%s)",
@@ -293,8 +819,9 @@ async def search_memory(
async def close(self) -> None:
"""Close the memory service and cleanup resources."""
- if "_client" in self.__dict__:
- # Check for initialized client without triggering cached_property
- await self._client.close()
- # Clear the cached property
- del self._client
+ if "_agent_memory_server_client" in self.__dict__:
+ client = self.__dict__["_agent_memory_server_client"]
+ close = getattr(client, "close", None)
+ if close:
+ await close()
+ del self.__dict__["_agent_memory_server_client"]
diff --git a/src/adk_redis/sessions/working_memory.py b/src/adk_redis/sessions/working_memory.py
index 732986e..559f29d 100644
--- a/src/adk_redis/sessions/working_memory.py
+++ b/src/adk_redis/sessions/working_memory.py
@@ -12,18 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Redis Working Memory Session Service for ADK.
-
-This module provides session management using the Redis Agent Memory Server's
-Working Memory API, offering automatic context summarization and background
-memory extraction.
-"""
+"""Redis working memory session service for ADK."""
from __future__ import annotations
+import base64
+from contextlib import asynccontextmanager
+import hashlib
+from importlib import import_module
import logging
import time
-from typing import Any, Literal
+from typing import Any, AsyncIterator, Literal
import uuid
from google.adk.events.event import Event
@@ -36,27 +35,79 @@
from pydantic import Field
from typing_extensions import override
+from adk_redis.memory._backends import MemoryBackendName
+from adk_redis.memory._backends import OPENSOURCE_AGENT_MEMORY_BACKEND
+from adk_redis.memory._backends import REDIS_AGENT_MEMORY_BACKEND
from adk_redis.memory._utils import extract_text_from_event
+from adk_redis.memory._utils import is_not_found_error
+from adk_redis.memory._utils import read_field
+from adk_redis.memory._utils import sanitize_managed_identifier
+from adk_redis.memory._utils import timestamp_to_datetime
logger = logging.getLogger("adk_redis." + __name__)
+_SESSION_ID_PREFIX = "adkredis"
+_MANAGED_SESSION_ID_LEN = 64
+
+try:
+ _agent_memory_client_module = import_module("agent_memory_client")
+ _agent_memory_exceptions_module = import_module(
+ "agent_memory_client.exceptions"
+ )
+ _agent_memory_models_module = import_module("agent_memory_client.models")
+except ImportError:
+ _MemoryAPIClient: Any = None
+ _MemoryClientConfig: Any = None
+
+ class _FallbackMemoryNotFoundError(Exception):
+ """Fallback used when agent-memory-client is not installed."""
+
+ _MemoryNotFoundError: Any = _FallbackMemoryNotFoundError
+ _MemoryMessage: Any = None
+ _MemoryStrategyConfig: Any = None
+else:
+ _MemoryAPIClient = _agent_memory_client_module.MemoryAPIClient
+ _MemoryClientConfig = _agent_memory_client_module.MemoryClientConfig
+ _MemoryNotFoundError = _agent_memory_exceptions_module.MemoryNotFoundError
+ _MemoryMessage = _agent_memory_models_module.MemoryMessage
+ _MemoryStrategyConfig = _agent_memory_models_module.MemoryStrategyConfig
+
+try:
+ _redis_agent_memory_module = import_module("redis_agent_memory")
+except ImportError:
+ _AgentMemory: Any = None
+else:
+ _AgentMemory = _redis_agent_memory_module.AgentMemory
+
class RedisWorkingMemorySessionServiceConfig(BaseModel):
- """Configuration for Redis Working Memory Session Service.
+ """Configuration for Redis working memory session service.
Attributes:
- api_base_url: Base URL of the Agent Memory Server.
+ backend: Memory backend to use.
+ api_base_url: Memory API base URL.
+ api_key: Redis Agent Memory API key.
+ store_id: Redis Agent Memory store ID.
timeout: HTTP request timeout in seconds.
- default_namespace: Default namespace for session operations.
- model_name: Model name for context window management and summarization.
- context_window_max: Maximum context window tokens.
- extraction_strategy: Memory extraction strategy.
- extraction_strategy_config: Additional config for extraction strategy.
- session_ttl_seconds: Optional TTL for session expiration.
+ timeout_ms: Optional SDK timeout in milliseconds.
+ default_namespace: Default namespace for session isolation.
+ model_name: Backward-compatible option retained from the previous client.
+ context_window_max: Backward-compatible option retained from the
+ previous client.
+ extraction_strategy: Backward-compatible option retained from the
+ previous client.
+ extraction_strategy_config: Backward-compatible option retained from the
+ previous client.
+ session_ttl_seconds: Backward-compatible option retained from the
+ previous client.
"""
+ backend: MemoryBackendName = "redis-agent-memory"
api_base_url: str = Field(default="http://localhost:8000")
+ api_key: str | None = None
+ store_id: str | None = None
timeout: float = Field(default=30.0, gt=0.0)
+ timeout_ms: int | None = Field(default=None, ge=1)
default_namespace: str | None = None
model_name: str | None = None
context_window_max: int | None = Field(default=None, ge=1)
@@ -68,177 +119,276 @@ class RedisWorkingMemorySessionServiceConfig(BaseModel):
class RedisWorkingMemorySessionService(BaseSessionService):
- """Session service using Redis Agent Memory Server's Working Memory API.
-
- This service provides session management backed by Agent Memory Server:
- - Session storage in Working Memory
- - Automatic context summarization when token limit exceeded
- - Background memory extraction to Long-Term Memory
- - Incremental message appending
- - https://github.com/redis/agent-memory-server
-
- Requires the `agent-memory-client` package to be installed.
-
- Example:
- ```python
- from adk_redis import (
- RedisWorkingMemorySessionService,
- RedisWorkingMemorySessionServiceConfig,
- )
+ """Session service backed by Redis memory backends.
- config = RedisWorkingMemorySessionServiceConfig(
- api_base_url="http://localhost:8000",
- default_namespace="my_app",
- )
- session_service = RedisWorkingMemorySessionService(config=config)
-
- # Use with ADK runner
- runner = Runner(
- agent=agent,
- session_service=session_service,
- )
- ```
+ Redis Agent Memory stores session events by session ID. The self-hosted
+ Agent Memory Server stores sessions in Working Memory. Returned ADK
+ sessions keep the original caller-facing session ID for both backends.
"""
def __init__(
self, config: RedisWorkingMemorySessionServiceConfig | None = None
):
- """Initialize the Redis Working Memory Session Service.
+ """Initialize the Redis Agent Memory session service.
Args:
- config: Configuration for the service. If None, uses defaults.
+ config: Configuration for the service. If None, defaults are used.
"""
self._config = config or RedisWorkingMemorySessionServiceConfig()
- def _get_client(self) -> Any:
- """Get a MemoryAPIClient instance.
+ def _timeout_ms(self) -> int:
+ """Return the configured SDK timeout in milliseconds."""
+ return self._config.timeout_ms or int(self._config.timeout * 1000)
- Note: We create a new client for each call instead of caching it because
- the ADK Runner creates a new event loop for each run() call, and cached
- async clients get tied to the first event loop and fail when it closes.
- """
- try:
- from agent_memory_client import MemoryAPIClient
- from agent_memory_client import MemoryClientConfig
- except ImportError as e:
+ def _get_client(self) -> Any:
+ """Get a backend client for compatibility with existing tests."""
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ return self._get_agent_memory_server_client()
+ return self._get_redis_agent_memory_client()
+
+ def _get_redis_agent_memory_client(self) -> Any:
+ """Get a Redis Agent Memory SDK client."""
+ if _AgentMemory is None:
raise ImportError(
- "agent-memory-client package is required for "
+ "redis-agent-memory package is required for "
"RedisWorkingMemorySessionService. "
"Install it with: pip install adk-redis[memory]"
- ) from e
+ )
+
+ return _AgentMemory(
+ self._config.api_base_url,
+ api_key=self._config.api_key,
+ store_id=self._config.store_id,
+ timeout_ms=self._timeout_ms(),
+ )
+
+ def _get_agent_memory_server_client(self) -> Any:
+ """Get a self-hosted Agent Memory Server client."""
+ if _MemoryAPIClient is None or _MemoryClientConfig is None:
+ raise ImportError(
+ "agent-memory-client package is required for the "
+ "opensource-agent-memory backend. Install it with: "
+ "pip install adk-redis[memory]"
+ )
- client_config = MemoryClientConfig(
+ client_config = _MemoryClientConfig(
base_url=self._config.api_base_url,
timeout=self._config.timeout,
default_namespace=self._config.default_namespace,
default_model_name=self._config.model_name,
default_context_window_max=self._config.context_window_max,
)
- return MemoryAPIClient(client_config)
+ return _MemoryAPIClient(client_config)
+
+ @asynccontextmanager
+ async def _agent_memory(self) -> AsyncIterator[Any]:
+ """Yield a Redis Agent Memory client and close SDK resources."""
+ client = self._get_client()
+ if hasattr(client, "__aenter__"):
+ async with client as agent_memory:
+ yield agent_memory
+ else:
+ yield client
def _get_namespace(self, app_name: str) -> str:
"""Get namespace from config or app_name."""
- return self._config.default_namespace or app_name
+ namespace = self._config.default_namespace or app_name
+ if self._config.backend == REDIS_AGENT_MEMORY_BACKEND:
+ return sanitize_managed_identifier(namespace)
+ return namespace
+
+ def _encode_segment(self, value: str) -> str:
+ """Encode one storage session ID segment."""
+ encoded = base64.urlsafe_b64encode(value.encode("utf-8")).decode("ascii")
+ return encoded.rstrip("=")
+
+ def _decode_segment(self, value: str) -> str:
+ """Decode one storage session ID segment."""
+ padding = "=" * (-len(value) % 4)
+ return base64.urlsafe_b64decode(f"{value}{padding}").decode("utf-8")
+
+ def _storage_session_id(
+ self, *, app_name: str, user_id: str, session_id: str
+ ) -> str:
+ """Build a managed Redis Agent Memory session ID.
+
+ Managed session IDs must be 1-64 chars and contain only alphanumeric
+ characters and hyphens. Encode ADK scope as a deterministic SHA-256 hex
+ digest so UUID session IDs and namespaces remain isolated per store.
+ """
+ namespace = self._get_namespace(app_name)
+ return hashlib.sha256(
+ f"{namespace}\0{user_id}\0{session_id}".encode("utf-8")
+ ).hexdigest()[:_MANAGED_SESSION_ID_LEN]
+
+ def _session_scope_from_response(
+ self, response: object
+ ) -> tuple[str, str, str] | None:
+ """Extract ADK session scope from a Redis Agent Memory session response."""
+ for redis_event in read_field(response, "events", []) or []:
+ metadata = read_field(redis_event, "metadata", {}) or {}
+ namespace = metadata.get("namespace")
+ stored_user_id = metadata.get("user_id")
+ adk_session_id = metadata.get("adk_session_id")
+ if namespace and stored_user_id and adk_session_id:
+ return (
+ str(namespace),
+ str(stored_user_id),
+ str(adk_session_id),
+ )
+ return None
+
+ def _parse_storage_session_id(
+ self, storage_session_id: str
+ ) -> tuple[str, str, str] | None:
+ """Parse an internal storage session ID.
+
+ Returns:
+ Tuple of namespace, user_id, and ADK session_id, or None.
+ """
+ parts = storage_session_id.split(":")
+ if len(parts) != 4 or parts[0] != _SESSION_ID_PREFIX:
+ return None
- def _event_to_message(self, event: Event) -> Any:
- """Convert ADK Event to MemoryMessage."""
- from datetime import datetime
- from datetime import timezone
+ try:
+ return (
+ self._decode_segment(parts[1]),
+ self._decode_segment(parts[2]),
+ self._decode_segment(parts[3]),
+ )
+ except Exception:
+ return None
+
+ def _event_role(self, event: Event) -> Any:
+ """Map an ADK event to a Redis Agent Memory role."""
+ content_role = event.content.role if event.content else None
+ if event.author == "user" or content_role == "user":
+ return "USER"
+ if content_role == "system":
+ return "SYSTEM"
+ return "ASSISTANT"
+
+ def _event_metadata(
+ self,
+ *,
+ event: Event,
+ app_name: str,
+ user_id: str,
+ session_id: str,
+ ) -> dict[str, Any]:
+ """Build portable metadata for an ADK event."""
+ metadata: dict[str, Any] = {
+ "source": "adk-redis",
+ "namespace": self._get_namespace(app_name),
+ "app_name": app_name,
+ "user_id": user_id,
+ "adk_session_id": session_id,
+ "adk_event_id": event.id,
+ "adk_author": event.author,
+ }
+ if event.actions and event.actions.state_delta:
+ metadata["state_delta"] = dict(event.actions.state_delta)
+ return metadata
+
+ def _event_actor_id(self, session: Session, event: Event) -> str:
+ """Return the Redis actor ID for an ADK event."""
+ if event.author == "user":
+ actor_id = session.user_id
+ else:
+ actor_id = event.author or session.app_name
+ if self._config.backend == REDIS_AGENT_MEMORY_BACKEND:
+ return sanitize_managed_identifier(actor_id)
+ return actor_id
+
+ def _build_agent_memory_server_strategy(self) -> Any:
+ """Build the self-hosted memory extraction strategy config."""
+ if _MemoryStrategyConfig is None:
+ raise ImportError(
+ "agent-memory-client package is required for memory strategies. "
+ "Install it with: pip install adk-redis[memory]"
+ )
+
+ return _MemoryStrategyConfig(
+ strategy=self._config.extraction_strategy,
+ config=self._config.extraction_strategy_config,
+ )
- from agent_memory_client.models import MemoryMessage
+ def _event_to_agent_memory_server_message(self, event: Event) -> Any:
+ """Convert an ADK event to a self-hosted memory message."""
+ if _MemoryMessage is None:
+ raise ImportError(
+ "agent-memory-client package is required for memory messages. "
+ "Install it with: pip install adk-redis[memory]"
+ )
text = extract_text_from_event(event)
if not text:
return None
role = "user" if event.author == "user" else "assistant"
- # Convert event timestamp (float) to datetime for MemoryMessage
- created_at = datetime.fromtimestamp(event.timestamp, tz=timezone.utc)
- return MemoryMessage(role=role, content=text, created_at=created_at)
+ return _MemoryMessage(
+ role=role,
+ content=text,
+ created_at=timestamp_to_datetime(event.timestamp),
+ )
- def _working_memory_response_to_session(
+ def _agent_memory_server_response_to_session(
self,
response: Any,
app_name: str,
user_id: str,
) -> Session:
- """Convert WorkingMemoryResponse to ADK Session."""
+ """Convert a self-hosted WorkingMemory response to an ADK Session."""
events = []
- for msg in response.messages or []:
- # For assistant messages, use app_name as the author since that matches
- # the agent name in ADK. Using session_id causes "Event from an unknown
- # agent" warnings and breaks conversation history handling.
- author = "user" if msg.role == "user" else app_name
- # Set the role on Content - "user" for user messages, "model" for assistant
- # This is required for ADK's content processor to include events in LLM context
- role = "user" if msg.role == "user" else "model"
- content = types.Content(role=role, parts=[types.Part(text=msg.content)])
- # Preserve original message timestamp if available
- timestamp = (
- msg.created_at.timestamp()
- if hasattr(msg, "created_at") and msg.created_at
- else time.time()
+ for message in getattr(response, "messages", None) or []:
+ role_raw = getattr(message, "role", "")
+ author = "user" if role_raw == "user" else app_name
+ content_role = "user" if role_raw == "user" else "model"
+ content = types.Content(
+ role=content_role,
+ parts=[types.Part(text=getattr(message, "content", ""))],
)
- event = Event(
- author=author,
- content=content,
- timestamp=timestamp,
+ created_at = getattr(message, "created_at", None)
+ timestamp = created_at.timestamp() if created_at else time.time()
+ events.append(
+ Event(
+ author=author,
+ content=content,
+ timestamp=timestamp,
+ )
)
- events.append(event)
return Session(
- id=response.session_id,
+ id=getattr(response, "session_id"),
app_name=app_name,
user_id=user_id,
events=events,
- state=response.data or {},
+ state=getattr(response, "data", None) or {},
last_update_time=time.time(),
)
- @override
- async def create_session(
+ async def _create_session_agent_memory_server(
self,
*,
app_name: str,
user_id: str,
- state: dict[str, Any] | None = None,
- session_id: str | None = None,
+ state: dict[str, Any] | None,
+ session_id: str | None,
) -> Session:
- """Create a new session in Working Memory.
-
- Uses get_or_create_working_memory to prevent accidental overwrites
- of existing sessions.
-
- Args:
- app_name: Application name (used as namespace if not configured).
- user_id: User identifier.
- state: Initial session state.
- session_id: Optional session ID (generated if not provided).
-
- Returns:
- The created Session.
- """
- from agent_memory_client.models import MemoryStrategyConfig
-
+ """Create a session in the self-hosted Agent Memory Server."""
session_id = (
session_id.strip()
if session_id and session_id.strip()
else str(uuid.uuid4())
)
namespace = self._get_namespace(app_name)
+ client = self._get_agent_memory_server_client()
- strategy_config = MemoryStrategyConfig(
- strategy=self._config.extraction_strategy,
- config=self._config.extraction_strategy_config,
- )
-
- # Use get_or_create to prevent accidental overwrites
- client = self._get_client()
created, working_memory = await client.get_or_create_working_memory(
session_id=session_id,
namespace=namespace,
user_id=user_id,
- long_term_memory_strategy=strategy_config,
+ long_term_memory_strategy=self._build_agent_memory_server_strategy(),
)
if not created:
@@ -247,12 +397,12 @@ async def create_session(
session_id,
namespace,
)
- # Return existing session data
- return self._working_memory_response_to_session(
- working_memory, app_name, user_id
+ return self._agent_memory_server_response_to_session(
+ working_memory,
+ app_name,
+ user_id,
)
- # Update with initial state and TTL if provided
if state or self._config.session_ttl_seconds:
if state:
working_memory.data = state
@@ -265,7 +415,6 @@ async def create_session(
)
logger.info("Created session %s in namespace %s", session_id, namespace)
-
return Session(
id=session_id,
app_name=app_name,
@@ -275,105 +424,271 @@ async def create_session(
last_update_time=time.time(),
)
- @override
- async def get_session(
+ async def _get_session_agent_memory_server(
self,
*,
app_name: str,
user_id: str,
session_id: str,
- config: GetSessionConfig | None = None,
+ config: GetSessionConfig | None,
) -> Session | None:
- """Retrieve a session from Working Memory.
-
- Uses get_or_create_working_memory and checks if session was newly created
- to determine if it exists. Passes model_name and context_window_max to
- enable automatic context summarization when token limit is exceeded.
-
- NOTE: For ADK Runner compatibility, this method now returns the session
- even if it was just created. The Runner expects get_session to either
- return an existing session OR return a newly created empty session.
- Returning None causes the Runner to fail with "Session not found".
-
- Args:
- app_name: Application name.
- user_id: User identifier.
- session_id: Session ID to retrieve.
- config: Optional configuration for filtering events.
-
- Returns:
- The Session (existing or newly created).
- """
- from agent_memory_client.exceptions import MemoryNotFoundError
-
+ """Retrieve a session from the self-hosted Agent Memory Server."""
try:
namespace = self._get_namespace(app_name)
- # Use get_or_create to avoid deprecated get_working_memory
- client = self._get_client()
- created, response = await client.get_or_create_working_memory(
+ client = self._get_agent_memory_server_client()
+ _, response = await client.get_or_create_working_memory(
session_id=session_id,
namespace=namespace,
user_id=user_id,
model_name=self._config.model_name,
context_window_max=self._config.context_window_max,
)
-
- # Return the session whether it was just created or already existed
- # This is required for ADK Runner compatibility
- session = self._working_memory_response_to_session(
- response, app_name, user_id
+ session = self._agent_memory_server_response_to_session(
+ response,
+ app_name,
+ user_id,
)
if config:
- if config.num_recent_events:
+ if config.num_recent_events is not None:
session.events = session.events[-config.num_recent_events :]
- if config.after_timestamp:
+ if config.after_timestamp is not None:
session.events = [
- e for e in session.events if e.timestamp > config.after_timestamp
+ event
+ for event in session.events
+ if event.timestamp >= config.after_timestamp
]
return session
- except MemoryNotFoundError:
+ except _MemoryNotFoundError:
return None
except Exception as e:
logger.error("Failed to get session %s: %s", session_id, e)
return None
- @override
- async def list_sessions(
- self, *, app_name: str, user_id: str | None = None
+ async def _list_sessions_agent_memory_server(
+ self, *, app_name: str, user_id: str | None
) -> ListSessionsResponse:
- """List all sessions for a user from Working Memory.
-
- Args:
- app_name: Application name.
- user_id: User identifier (required for this implementation).
-
- Returns:
- ListSessionsResponse containing sessions (without events).
-
- Raises:
- ValueError: If user_id is not provided.
- """
+ """List sessions from the self-hosted Agent Memory Server."""
if user_id is None:
raise ValueError(
"user_id is required for RedisWorkingMemorySessionService"
)
+
try:
namespace = self._get_namespace(app_name)
-
- # SDK method: list_sessions returns SessionListResponse
- # with sessions: list[str] (session IDs only)
- client = self._get_client()
- response = await client.list_sessions(
+ response = await self._get_agent_memory_server_client().list_sessions(
namespace=namespace,
user_id=user_id,
)
+ sessions = [
+ Session(
+ id=session_id,
+ app_name=app_name,
+ user_id=user_id,
+ state={},
+ events=[],
+ last_update_time=time.time(),
+ )
+ for session_id in getattr(response, "sessions", []) or []
+ ]
+ return ListSessionsResponse(sessions=sessions)
+
+ except Exception as e:
+ logger.error("Failed to list sessions: %s", e)
+ return ListSessionsResponse(sessions=[])
+
+ async def _delete_session_agent_memory_server(
+ self, *, app_name: str, user_id: str, session_id: str
+ ) -> None:
+ """Delete a session from the self-hosted Agent Memory Server."""
+ try:
+ await self._get_agent_memory_server_client().delete_working_memory(
+ session_id=session_id,
+ namespace=self._get_namespace(app_name),
+ user_id=user_id,
+ )
+ logger.info("Deleted session %s", session_id)
+ except Exception as e:
+ logger.error("Failed to delete session %s: %s", session_id, e)
+
+ async def _append_event_to_agent_memory_server(
+ self, session: Session, event: Event
+ ) -> None:
+ """Append one ADK event to the self-hosted Agent Memory Server."""
+ message = self._event_to_agent_memory_server_message(event)
+ if not message:
+ return
+
+ await self._get_agent_memory_server_client().append_messages_to_working_memory(
+ session_id=session.id,
+ messages=[message],
+ namespace=self._get_namespace(session.app_name),
+ user_id=session.user_id,
+ )
- sessions = []
- for session_id in response.sessions:
- session = Session(
+ async def _append_event_to_redis(
+ self, session: Session, event: Event
+ ) -> None:
+ """Append one ADK event to Redis Agent Memory."""
+ text = extract_text_from_event(event).strip()
+ if not text:
+ return
+
+ storage_session_id = self._storage_session_id(
+ app_name=session.app_name,
+ user_id=session.user_id,
+ session_id=session.id,
+ )
+ async with self._agent_memory() as agent_memory:
+ await agent_memory.add_session_event_async(
+ session_id=storage_session_id,
+ actor_id=self._event_actor_id(session, event),
+ role=self._event_role(event),
+ content=[{"text": text}],
+ created_at=timestamp_to_datetime(event.timestamp),
+ metadata=self._event_metadata(
+ event=event,
+ app_name=session.app_name,
+ user_id=session.user_id,
+ session_id=session.id,
+ ),
+ )
+
+ def _event_text(self, event: object) -> str:
+ """Extract text from a Redis Agent Memory session event."""
+ content = read_field(event, "content", [])
+ parts = []
+ for item in content or []:
+ text = read_field(item, "text", "")
+ if text:
+ parts.append(str(text))
+ return "\n".join(parts)
+
+ def _response_to_session(
+ self,
+ response: object,
+ *,
+ app_name: str,
+ user_id: str,
+ session_id: str,
+ ) -> Session:
+ """Convert a Redis Agent Memory session response to an ADK Session."""
+ events = []
+ state: dict[str, Any] = {}
+ response_events = read_field(response, "events", []) or []
+
+ for redis_event in response_events:
+ text = self._event_text(redis_event).strip()
+ if not text:
+ continue
+
+ role_raw = read_field(redis_event, "role", "")
+ role = str(getattr(role_raw, "value", role_raw)).lower()
+ author = "user" if role == "user" else app_name
+ content_role = "user" if role == "user" else "model"
+ content = types.Content(
+ role=content_role,
+ parts=[types.Part(text=text)],
+ )
+ created_at = read_field(redis_event, "created_at")
+ timestamp = created_at.timestamp() if created_at else time.time()
+ metadata = read_field(redis_event, "metadata", {}) or {}
+ state_delta = metadata.get("state_delta")
+ if isinstance(state_delta, dict):
+ state.update(state_delta)
+
+ events.append(
+ Event(
+ id=metadata.get("adk_event_id")
+ or read_field(redis_event, "event_id"),
+ author=metadata.get("adk_author") or author,
+ content=content,
+ timestamp=timestamp,
+ )
+ )
+
+ return Session(
+ id=session_id,
+ app_name=app_name,
+ user_id=user_id,
+ events=events,
+ state=state,
+ last_update_time=time.time(),
+ )
+
+ @override
+ async def create_session(
+ self,
+ *,
+ app_name: str,
+ user_id: str,
+ state: dict[str, Any] | None = None,
+ session_id: str | None = None,
+ ) -> Session:
+ """Create a new ADK session object.
+
+ Redis Agent Memory creates a stored session when the first event is
+ appended, so this method returns the ADK session without writing a marker
+ event.
+ """
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ return await self._create_session_agent_memory_server(
+ app_name=app_name,
+ user_id=user_id,
+ state=state,
+ session_id=session_id,
+ )
+
+ session_id = (
+ session_id.strip()
+ if session_id and session_id.strip()
+ else str(uuid.uuid4())
+ )
+ return Session(
+ id=session_id,
+ app_name=app_name,
+ user_id=user_id,
+ state=state or {},
+ events=[],
+ last_update_time=time.time(),
+ )
+
+ @override
+ async def get_session(
+ self,
+ *,
+ app_name: str,
+ user_id: str,
+ session_id: str,
+ config: GetSessionConfig | None = None,
+ ) -> Session | None:
+ """Retrieve an ADK session from Redis Agent Memory."""
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ return await self._get_session_agent_memory_server(
+ app_name=app_name,
+ user_id=user_id,
+ session_id=session_id,
+ config=config,
+ )
+
+ storage_session_id = self._storage_session_id(
+ app_name=app_name,
+ user_id=user_id,
+ session_id=session_id,
+ )
+
+ try:
+ async with self._agent_memory() as agent_memory:
+ response = await agent_memory.get_session_memory_async(
+ session_id=storage_session_id
+ )
+ except Exception as e:
+ if is_not_found_error(e):
+ # Managed sessions are materialized on first append_event. Return an
+ # empty ADK session so runners can start a new conversation.
+ return Session(
id=session_id,
app_name=app_name,
user_id=user_id,
@@ -381,7 +696,83 @@ async def list_sessions(
events=[],
last_update_time=time.time(),
)
- sessions.append(session)
+ logger.error("Failed to get session %s: %s", session_id, e)
+ return None
+
+ session = self._response_to_session(
+ response,
+ app_name=app_name,
+ user_id=user_id,
+ session_id=session_id,
+ )
+
+ if config:
+ if config.num_recent_events is not None:
+ session.events = session.events[-config.num_recent_events :]
+ if config.after_timestamp is not None:
+ session.events = [
+ event
+ for event in session.events
+ if event.timestamp >= config.after_timestamp
+ ]
+
+ return session
+
+ @override
+ async def list_sessions(
+ self, *, app_name: str, user_id: str | None = None
+ ) -> ListSessionsResponse:
+ """List stored sessions from Redis Agent Memory."""
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ return await self._list_sessions_agent_memory_server(
+ app_name=app_name,
+ user_id=user_id,
+ )
+
+ namespace = self._get_namespace(app_name)
+ sessions = []
+ page_token = None
+
+ try:
+ async with self._agent_memory() as agent_memory:
+ while True:
+ response = await agent_memory.list_sessions_async(
+ limit=100,
+ page_token=page_token,
+ )
+ for storage_session_id in read_field(response, "items", []) or []:
+ parsed = self._parse_storage_session_id(storage_session_id)
+ if parsed:
+ stored_namespace, stored_user_id, session_id = parsed
+ else:
+ try:
+ stored = await agent_memory.get_session_memory_async(
+ session_id=storage_session_id
+ )
+ except Exception:
+ continue
+ scope = self._session_scope_from_response(stored)
+ if not scope:
+ continue
+ stored_namespace, stored_user_id, session_id = scope
+ if stored_namespace != namespace:
+ continue
+ if user_id is not None and stored_user_id != user_id:
+ continue
+ sessions.append(
+ Session(
+ id=session_id,
+ app_name=app_name,
+ user_id=stored_user_id,
+ state={},
+ events=[],
+ last_update_time=time.time(),
+ )
+ )
+
+ page_token = read_field(response, "next_page_token")
+ if not page_token:
+ break
return ListSessionsResponse(sessions=sessions)
@@ -393,60 +784,52 @@ async def list_sessions(
async def delete_session(
self, *, app_name: str, user_id: str, session_id: str
) -> None:
- """Delete a session from Working Memory.
-
- Args:
- app_name: Application name.
- user_id: User identifier.
- session_id: Session ID to delete.
- """
- try:
- namespace = self._get_namespace(app_name)
- client = self._get_client()
- await client.delete_working_memory(
- session_id=session_id,
- namespace=namespace,
+ """Delete a session from Redis Agent Memory."""
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ await self._delete_session_agent_memory_server(
+ app_name=app_name,
user_id=user_id,
+ session_id=session_id,
)
+ return
+
+ storage_session_id = self._storage_session_id(
+ app_name=app_name,
+ user_id=user_id,
+ session_id=session_id,
+ )
+ try:
+ async with self._agent_memory() as agent_memory:
+ await agent_memory.delete_session_memory_async(
+ session_id=storage_session_id
+ )
logger.info("Deleted session %s", session_id)
except Exception as e:
- logger.error("Failed to delete session %s: %s", session_id, e)
+ if not is_not_found_error(e):
+ logger.error("Failed to delete session %s: %s", session_id, e)
@override
async def append_event(self, session: Session, event: Event) -> Event:
- """Append an event to the session in Working Memory.
-
- Uses the incremental append API to add a single message without
- resending the full conversation history.
-
- Args:
- session: The session to append to.
- event: The event to append.
-
- Returns:
- The appended event.
- """
- await super().append_event(session=session, event=event)
- session.last_update_time = event.timestamp
+ """Append an event to the ADK session and configured backend."""
+ appended_event = await super().append_event(session=session, event=event)
+ if appended_event.partial:
+ return appended_event
+ session.last_update_time = appended_event.timestamp
try:
- message = self._event_to_message(event)
- if message:
- namespace = self._get_namespace(session.app_name)
- client = self._get_client()
- await client.append_messages_to_working_memory(
- session_id=session.id,
- messages=[message],
- namespace=namespace,
- user_id=session.user_id,
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ await self._append_event_to_agent_memory_server(
+ session,
+ appended_event,
)
- logger.debug("Appended message to session %s", session.id)
+ else:
+ await self._append_event_to_redis(session, appended_event)
+ logger.debug("Appended message to session %s", session.id)
except Exception as e:
logger.error("Failed to append event to session %s: %s", session.id, e)
- return event
+ return appended_event
async def close(self) -> None:
"""Close the session service and cleanup resources."""
- # No longer caching client, so nothing to close
pass
diff --git a/src/adk_redis/tools/memory/__init__.py b/src/adk_redis/tools/memory/__init__.py
index 3868183..8c6f821 100644
--- a/src/adk_redis/tools/memory/__init__.py
+++ b/src/adk_redis/tools/memory/__init__.py
@@ -12,11 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Redis Agent Memory tools for ADK.
+"""Redis memory tools for ADK.
This module provides tools for explicit LLM-controlled memory operations
-with the Redis Agent Memory Server. These tools complement the automatic
-memory services by allowing the LLM to directly manage long-term memories.
+with Redis Agent Memory or the self-hosted Agent Memory Server. These tools
+complement the automatic memory services by allowing the LLM to directly manage
+long-term memories.
Available Tools:
- MemoryPromptTool: Enrich prompts with relevant memories
@@ -38,6 +39,7 @@
# Configure all tools
config = MemoryToolConfig(
+ backend="redis-agent-memory",
api_base_url="http://localhost:8000",
default_namespace="my_app",
recency_boost=True,
diff --git a/src/adk_redis/tools/memory/_base.py b/src/adk_redis/tools/memory/_base.py
index ce9db89..74da8a1 100644
--- a/src/adk_redis/tools/memory/_base.py
+++ b/src/adk_redis/tools/memory/_base.py
@@ -12,30 +12,54 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Base class for Redis Agent Memory tools."""
+"""Base class for Redis memory tools."""
from __future__ import annotations
+from contextlib import asynccontextmanager
+from importlib import import_module
import logging
-from typing import Any
+from typing import Any, AsyncIterator
from google.adk.tools.base_tool import BaseTool
+from adk_redis.memory._backends import OPENSOURCE_AGENT_MEMORY_BACKEND
+from adk_redis.memory._backends import REDIS_AGENT_MEMORY_BACKEND
+from adk_redis.memory._utils import sanitize_managed_identifier
from adk_redis.tools.memory._config import MemoryToolConfig
logger = logging.getLogger("adk_redis." + __name__)
+try:
+ _agent_memory_client_module = import_module("agent_memory_client")
+ _agent_memory_models_module = import_module("agent_memory_client.models")
+except ImportError:
+ _MemoryAPIClient: Any = None
+ _MemoryClientConfig: Any = None
+ _RecencyConfig: Any = None
+else:
+ _MemoryAPIClient = _agent_memory_client_module.MemoryAPIClient
+ _MemoryClientConfig = _agent_memory_client_module.MemoryClientConfig
+ _RecencyConfig = _agent_memory_models_module.RecencyConfig
+
+try:
+ _redis_agent_memory_module = import_module("redis_agent_memory")
+except ImportError:
+ _AgentMemory: Any = None
+else:
+ _AgentMemory = _redis_agent_memory_module.AgentMemory
+
class BaseMemoryTool(BaseTool):
- """Base class for all Redis Agent Memory tools.
+ """Base class for all Redis memory tools.
This class provides common functionality for memory tools:
- - Lazy initialization of the MemoryAPIClient
+ - Lazy initialization of memory backend clients
- Shared configuration management
- Standard error handling
Subclasses should implement their specific tool logic using
- the `_client` property to access the Agent Memory Server.
+ the configured backend helpers.
"""
def __init__(
@@ -53,49 +77,90 @@ def __init__(
description: The description of the tool (exposed to LLM).
Raises:
- ImportError: If agent-memory-client package is not installed.
+ ImportError: If the configured backend package is not installed.
"""
super().__init__(name=name, description=description)
self._config = config
def _get_client(self) -> Any:
- """Get a MemoryAPIClient instance.
+ """Get a backend client for compatibility with existing tests."""
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ return self._get_agent_memory_server_client()
+ return self._get_redis_agent_memory_client()
+
+ def _get_redis_agent_memory_client(self) -> Any:
+ """Get a Redis Agent Memory SDK client.
Note: We create a new client for each call instead of caching it because
the ADK Runner creates a new event loop for each run() call, and cached
async clients get tied to the first event loop and fail when it closes.
Returns:
- An initialized MemoryAPIClient instance.
+ An initialized Redis Agent Memory SDK client.
Raises:
- ImportError: If agent-memory-client package is not installed.
+ ImportError: If redis-agent-memory package is not installed.
"""
- try:
- from agent_memory_client import MemoryAPIClient
- from agent_memory_client import MemoryClientConfig
- except ImportError as e:
+ if _AgentMemory is None:
raise ImportError(
- "agent-memory-client package is required for memory tools. "
+ "redis-agent-memory package is required for memory tools. "
"Install it with: pip install adk-redis[memory]"
- ) from e
+ )
+
+ return _AgentMemory(
+ self._config.api_base_url,
+ api_key=self._config.api_key,
+ store_id=self._config.store_id,
+ timeout_ms=self._timeout_ms(),
+ )
+
+ def _get_agent_memory_server_client(self) -> Any:
+ """Get a self-hosted Agent Memory Server client."""
+ if _MemoryAPIClient is None or _MemoryClientConfig is None:
+ raise ImportError(
+ "agent-memory-client package is required for memory tools with "
+ "backend='opensource-agent-memory'. Install it with: "
+ "pip install adk-redis[memory]"
+ )
- client_config = MemoryClientConfig(
+ client_config = _MemoryClientConfig(
base_url=self._config.api_base_url,
timeout=self._config.timeout,
default_namespace=self._config.default_namespace,
)
- return MemoryAPIClient(client_config)
+ return _MemoryAPIClient(client_config)
+
+ def _timeout_ms(self) -> int:
+ """Return the configured SDK timeout in milliseconds."""
+ return self._config.timeout_ms or int(self._config.timeout * 1000)
+
+ @asynccontextmanager
+ async def _agent_memory(self) -> AsyncIterator[Any]:
+ """Yield a Redis Agent Memory client and close SDK resources."""
+ client = self._get_client()
+ if hasattr(client, "__aenter__"):
+ async with client as agent_memory:
+ yield agent_memory
+ else:
+ yield client
def _build_recency_config(self) -> Any:
"""Build RecencyConfig from tool configuration.
Returns:
- A RecencyConfig object for use with search operations.
+ A RecencyConfig object for the self-hosted backend, or None for
+ Redis Agent Memory.
"""
- from agent_memory_client.models import RecencyConfig
+ if self._config.backend != OPENSOURCE_AGENT_MEMORY_BACKEND:
+ return None
- return RecencyConfig(
+ if _RecencyConfig is None:
+ raise ImportError(
+ "agent-memory-client package is required for recency search. "
+ "Install it with: pip install adk-redis[memory]"
+ )
+
+ return _RecencyConfig(
recency_boost=self._config.recency_boost,
semantic_weight=self._config.semantic_weight,
recency_weight=self._config.recency_weight,
@@ -114,7 +179,10 @@ def _get_namespace(self, namespace: str | None = None) -> str:
Returns:
The namespace to use (override or default).
"""
- return namespace or self._config.default_namespace
+ resolved = namespace or self._config.default_namespace
+ if self._config.backend == REDIS_AGENT_MEMORY_BACKEND:
+ return sanitize_managed_identifier(resolved)
+ return resolved
def _get_user_id(self, user_id: str | None = None) -> str | None:
"""Get the user ID to use for operations.
@@ -125,4 +193,6 @@ def _get_user_id(self, user_id: str | None = None) -> str | None:
Returns:
The user ID to use (override or default).
"""
- return user_id or self._config.default_user_id
+ return (
+ user_id or self._config.default_owner_id or self._config.default_user_id
+ )
diff --git a/src/adk_redis/tools/memory/_config.py b/src/adk_redis/tools/memory/_config.py
index f195945..442b44e 100644
--- a/src/adk_redis/tools/memory/_config.py
+++ b/src/adk_redis/tools/memory/_config.py
@@ -19,18 +19,25 @@
from pydantic import BaseModel
from pydantic import Field
+from adk_redis.memory._backends import MemoryBackendName
+
class MemoryToolConfig(BaseModel):
- """Shared configuration for all Redis Agent Memory tools.
+ """Shared configuration for all Redis memory tools.
This configuration is used by all memory tools to connect to the
- Agent Memory Server and manage memory operations.
+ configured memory backend and manage memory operations.
Attributes:
- api_base_url: Base URL of the Agent Memory Server.
+ backend: Memory backend to use.
+ api_base_url: Memory API base URL.
+ api_key: Redis Agent Memory API key.
+ store_id: Redis Agent Memory store ID.
timeout: HTTP request timeout in seconds.
+ timeout_ms: Optional SDK timeout in milliseconds.
default_namespace: Default namespace for memory operations.
default_user_id: Default user ID for memory operations (optional).
+ default_owner_id: Default owner ID for memory operations (optional).
search_top_k: Default maximum number of memories to retrieve.
distance_threshold: Maximum distance threshold for search (0.0-1.0).
recency_boost: Enable recency-aware re-ranking of search results.
@@ -54,10 +61,15 @@ class MemoryToolConfig(BaseModel):
```
"""
+ backend: MemoryBackendName = "redis-agent-memory"
api_base_url: str = Field(default="http://localhost:8000")
+ api_key: str | None = None
+ store_id: str | None = None
timeout: float = Field(default=30.0, gt=0.0)
+ timeout_ms: int | None = Field(default=None, ge=1)
default_namespace: str = Field(default="default")
default_user_id: str | None = None
+ default_owner_id: str | None = None
search_top_k: int = Field(default=10, ge=1)
distance_threshold: float | None = Field(default=None, ge=0.0, le=1.0)
recency_boost: bool = True
diff --git a/src/adk_redis/tools/memory/create.py b/src/adk_redis/tools/memory/create.py
index 3f07f73..8ffc4f0 100644
--- a/src/adk_redis/tools/memory/create.py
+++ b/src/adk_redis/tools/memory/create.py
@@ -18,9 +18,13 @@
import logging
from typing import Any
+import uuid
from google.genai import types
+from adk_redis.memory._backends import OPENSOURCE_AGENT_MEMORY_BACKEND
+from adk_redis.memory._utils import read_field
+from adk_redis.memory._utils import stable_memory_id
from adk_redis.tools.memory._base import BaseMemoryTool
from adk_redis.tools.memory._config import MemoryToolConfig
@@ -150,37 +154,66 @@ async def run_async(self, **kwargs: Any) -> dict[str, Any]:
memory_type = "semantic" # Default fallback
try:
- # Use add_memory_tool which creates a memory in a session context
- # We'll use a temporary session ID for standalone memory creation
- import uuid
-
- session_id = f"standalone_{uuid.uuid4().hex[:8]}"
-
- client = self._get_client()
- response = await client.add_memory_tool(
- session_id=session_id,
- text=content,
- memory_type=memory_type,
- topics=topics if topics else None,
- namespace=namespace,
- user_id=user_id,
- )
-
- # Response is a dict with 'success' key and summary
- if response.get("success"):
- # Extract memory ID from summary if available, or use session_id as fallback
- memory_id = response.get("memory_id", session_id)
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ session_id = f"standalone_{uuid.uuid4().hex[:8]}"
+ response = await self._get_agent_memory_server_client().add_memory_tool(
+ session_id=session_id,
+ text=content,
+ memory_type=memory_type,
+ topics=topics if topics else None,
+ namespace=namespace,
+ user_id=user_id,
+ )
+
+ if response.get("success"):
+ return {
+ "status": "success",
+ "memory_id": response.get("memory_id", session_id),
+ "message": response.get(
+ "summary",
+ "Memory created successfully",
+ ),
+ }
return {
- "status": "success",
- "memory_id": memory_id,
- "message": response.get("summary", "Memory created successfully"),
+ "status": "error",
+ "message": response.get("summary", "Failed to create memory"),
}
- else:
+
+ memory_id = stable_memory_id(
+ "tool",
+ namespace,
+ user_id or "",
+ memory_type,
+ content,
+ )
+ record = {
+ "id": memory_id,
+ "text": content,
+ "ownerId": user_id,
+ "namespace": namespace,
+ "topics": topics if topics else None,
+ "memoryType": memory_type,
+ }
+
+ async with self._agent_memory() as agent_memory:
+ response = await agent_memory.bulk_create_long_term_memories_async(
+ memories=[record]
+ )
+
+ errors = read_field(response, "errors")
+ if errors:
return {
"status": "error",
- "message": response.get("summary", "Failed to create memory"),
+ "message": f"Failed to create memory: {errors}",
}
+ created = read_field(response, "created", []) or [memory_id]
+ return {
+ "status": "success",
+ "memory_id": created[0],
+ "message": "Memory created successfully",
+ }
+
except Exception as e:
logger.error("Failed to create memory: %s", e)
return {
diff --git a/src/adk_redis/tools/memory/delete.py b/src/adk_redis/tools/memory/delete.py
index a5384f8..fc443c9 100644
--- a/src/adk_redis/tools/memory/delete.py
+++ b/src/adk_redis/tools/memory/delete.py
@@ -17,10 +17,13 @@
from __future__ import annotations
import logging
+import re
from typing import Any
from google.genai import types
+from adk_redis.memory._backends import OPENSOURCE_AGENT_MEMORY_BACKEND
+from adk_redis.memory._utils import read_field
from adk_redis.tools.memory._base import BaseMemoryTool
from adk_redis.tools.memory._config import MemoryToolConfig
@@ -122,27 +125,32 @@ async def run_async(self, **kwargs: Any) -> dict[str, Any]:
return {"status": "error", "message": "memory_ids is required"}
try:
- # delete_long_term_memories only takes memory_ids
- client = self._get_client()
- response = await client.delete_long_term_memories(
- memory_ids=memory_ids,
- )
-
- # Response is AckResponse with 'status' field containing message like "ok, deleted 2 memories"
- status_msg = response.status
-
- # Parse the deleted count from the status message
- import re
-
- match = re.search(r"deleted (\d+)", status_msg)
- deleted_count = int(match.group(1)) if match else 0
-
- is_success = "ok" in status_msg.lower()
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ response = await self._get_agent_memory_server_client().delete_long_term_memories(
+ memory_ids=memory_ids,
+ )
+ status_msg = response.status
+ match = re.search(r"deleted (\d+)", status_msg)
+ deleted_count = int(match.group(1)) if match else 0
+ is_success = "ok" in status_msg.lower()
+
+ return {
+ "status": "success" if is_success else "error",
+ "deleted_count": deleted_count,
+ "message": status_msg,
+ }
+
+ async with self._agent_memory() as agent_memory:
+ response = await agent_memory.bulk_delete_long_term_memories_async(
+ memory_ids=memory_ids,
+ )
+ deleted = read_field(response, "deleted", []) or []
+ errors = read_field(response, "errors")
return {
- "status": "success" if is_success else "error",
- "deleted_count": deleted_count,
- "message": status_msg,
+ "status": "error" if errors else "success",
+ "deleted_count": len(deleted),
+ "message": "Deleted memories" if not errors else str(errors),
}
except Exception as e:
diff --git a/src/adk_redis/tools/memory/get.py b/src/adk_redis/tools/memory/get.py
index 6fa359a..9745015 100644
--- a/src/adk_redis/tools/memory/get.py
+++ b/src/adk_redis/tools/memory/get.py
@@ -21,6 +21,8 @@
from google.genai import types
+from adk_redis.memory._backends import OPENSOURCE_AGENT_MEMORY_BACKEND
+from adk_redis.memory._utils import read_field
from adk_redis.tools.memory._base import BaseMemoryTool
from adk_redis.tools.memory._config import MemoryToolConfig
@@ -110,26 +112,57 @@ async def run_async(self, **kwargs: Any) -> dict[str, Any]:
return {"status": "error", "message": "memory_id is required"}
try:
- client = self._get_client()
- memory = await client.get_long_term_memory(memory_id=memory_id)
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ memory = (
+ await (
+ self._get_agent_memory_server_client().get_long_term_memory(
+ memory_id=memory_id
+ )
+ )
+ )
+
+ return {
+ "status": "success",
+ "memory": {
+ "id": memory.id,
+ "content": memory.text,
+ "topics": memory.topics or [],
+ "entities": memory.entities or [],
+ "memory_type": memory.memory_type,
+ "namespace": memory.namespace,
+ "user_id": memory.user_id,
+ "session_id": memory.session_id,
+ "created_at": str(memory.created_at)
+ if memory.created_at
+ else None,
+ "last_accessed": str(memory.last_accessed)
+ if memory.last_accessed
+ else None,
+ },
+ }
+
+ async with self._agent_memory() as agent_memory:
+ memory = await agent_memory.get_long_term_memory_async(
+ memory_id=memory_id
+ )
+ memory_type = read_field(memory, "memory_type")
+ created_at = read_field(memory, "created_at")
+ updated_at = read_field(memory, "updated_at")
return {
"status": "success",
"memory": {
- "id": memory.id,
- "content": memory.text,
- "topics": memory.topics or [],
- "entities": memory.entities or [],
- "memory_type": memory.memory_type,
- "namespace": memory.namespace,
- "user_id": memory.user_id,
- "session_id": memory.session_id,
- "created_at": str(memory.created_at)
- if memory.created_at
- else None,
- "last_accessed": str(memory.last_accessed)
- if memory.last_accessed
+ "id": read_field(memory, "id"),
+ "content": read_field(memory, "text"),
+ "topics": read_field(memory, "topics", []) or [],
+ "memory_type": str(getattr(memory_type, "value", memory_type))
+ if memory_type
else None,
+ "namespace": read_field(memory, "namespace"),
+ "user_id": read_field(memory, "owner_id"),
+ "session_id": read_field(memory, "session_id"),
+ "created_at": str(created_at) if created_at else None,
+ "updated_at": str(updated_at) if updated_at else None,
},
}
diff --git a/src/adk_redis/tools/memory/prompt.py b/src/adk_redis/tools/memory/prompt.py
index 62a1a15..ace4a5f 100644
--- a/src/adk_redis/tools/memory/prompt.py
+++ b/src/adk_redis/tools/memory/prompt.py
@@ -21,6 +21,8 @@
from google.genai import types
+from adk_redis.memory._backends import OPENSOURCE_AGENT_MEMORY_BACKEND
+from adk_redis.memory._utils import read_field
from adk_redis.tools.memory._base import BaseMemoryTool
from adk_redis.tools.memory._config import MemoryToolConfig
@@ -130,26 +132,56 @@ async def run_async(self, **kwargs: Any) -> dict[str, Any]:
return {"status": "error", "message": "query is required"}
try:
- # memory_prompt requires either session_id or long_term_search
- # We'll use long_term_search to search long-term memories
- long_term_search = {
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ response = await self._get_agent_memory_server_client().memory_prompt(
+ query=query,
+ user_id=user_id,
+ namespace=namespace,
+ long_term_search={"limit": self._config.search_top_k},
+ )
+ enriched_prompt = response.get("prompt", query)
+ if system_prompt:
+ enriched_prompt = f"{system_prompt}\n\n{enriched_prompt}"
+
+ return {
+ "status": "success",
+ "enriched_prompt": enriched_prompt,
+ "memories_used": len(response.get("memories", [])),
+ }
+
+ filters: dict[str, Any] = {"namespace": {"eq": namespace}}
+ if user_id:
+ filters["ownerId"] = {"eq": user_id}
+ request = {
+ "text": query,
"limit": self._config.search_top_k,
+ "filter": filters,
+ "filterOp": "all",
}
- client = self._get_client()
- response = await client.memory_prompt(
- query=query,
- user_id=user_id,
- namespace=namespace,
- long_term_search=long_term_search,
- )
+ async with self._agent_memory() as agent_memory:
+ response = await agent_memory.search_long_term_memory_async(
+ request=request
+ )
+
+ memory_lines = [
+ f"- {read_field(memory, 'text')}"
+ for memory in read_field(response, "items", []) or []
+ if read_field(memory, "text")
+ ]
+ if memory_lines:
+ memory_context = "\n".join(memory_lines)
+ enriched_prompt = (
+ "Relevant long-term memories:\n"
+ f"{memory_context}\n\nCurrent query:\n{query}"
+ )
+ else:
+ enriched_prompt = query
- # Extract the enriched prompt and combine with system prompt if provided
- enriched_prompt = response.get("prompt", query)
if system_prompt:
enriched_prompt = f"{system_prompt}\n\n{enriched_prompt}"
- memories_used = len(response.get("memories", []))
+ memories_used = len(memory_lines)
return {
"status": "success",
diff --git a/src/adk_redis/tools/memory/search.py b/src/adk_redis/tools/memory/search.py
index 88fcc4f..421e512 100644
--- a/src/adk_redis/tools/memory/search.py
+++ b/src/adk_redis/tools/memory/search.py
@@ -16,16 +16,28 @@
from __future__ import annotations
+from importlib import import_module
import logging
from typing import Any
from google.genai import types
+from adk_redis.memory._backends import OPENSOURCE_AGENT_MEMORY_BACKEND
+from adk_redis.memory._utils import read_field
from adk_redis.tools.memory._base import BaseMemoryTool
from adk_redis.tools.memory._config import MemoryToolConfig
logger = logging.getLogger("adk_redis." + __name__)
+try:
+ _agent_memory_filters_module = import_module("agent_memory_client.filters")
+except ImportError:
+ _Namespace: Any = None
+ _UserId: Any = None
+else:
+ _Namespace = _agent_memory_filters_module.Namespace
+ _UserId = _agent_memory_filters_module.UserId
+
class SearchMemoryTool(BaseMemoryTool):
"""Tool for searching long-term memories.
@@ -129,34 +141,82 @@ async def run_async(self, **kwargs: Any) -> dict[str, Any]:
return {"status": "error", "message": "query is required"}
try:
- # Use search_long_term_memory which supports namespace filtering
- client = self._get_client()
- from agent_memory_client.filters import Namespace
- from agent_memory_client.filters import UserId
-
- ns = Namespace(eq=namespace)
- uid = UserId(eq=user_id) if user_id else None
- response = await client.search_long_term_memory(
- text=query,
- namespace=ns,
- user_id=uid,
- distance_threshold=self._config.distance_threshold,
- limit=limit,
- )
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ if _Namespace is None or _UserId is None:
+ raise ImportError(
+ "agent-memory-client package is required for memory search. "
+ "Install it with: pip install adk-redis[memory]"
+ )
+
+ namespace_filter = _Namespace(eq=namespace)
+ user_filter = _UserId(eq=user_id) if user_id else None
+ response = (
+ await (
+ self._get_agent_memory_server_client().search_long_term_memory(
+ text=query,
+ namespace=namespace_filter,
+ user_id=user_filter,
+ distance_threshold=self._config.distance_threshold,
+ limit=limit,
+ recency=self._build_recency_config()
+ if self._config.recency_boost
+ else None,
+ )
+ )
+ )
+
+ memories = []
+ for memory in response.memories:
+ memories.append(
+ {
+ "id": memory.id,
+ "content": memory.text,
+ "score": getattr(memory, "dist", 0.0),
+ "topics": memory.topics or [],
+ "memory_type": memory.memory_type,
+ "created_at": str(memory.created_at)
+ if memory.created_at
+ else None,
+ }
+ )
+
+ return {
+ "status": "success",
+ "memories": memories,
+ "count": len(memories),
+ }
+
+ filters: dict[str, Any] = {"namespace": {"eq": namespace}}
+ if user_id:
+ filters["ownerId"] = {"eq": user_id}
+ request: dict[str, Any] = {
+ "text": query,
+ "limit": limit,
+ "filter": filters,
+ "filterOp": "all",
+ }
+ if self._config.distance_threshold is not None:
+ request["similarityThreshold"] = self._config.distance_threshold
+
+ async with self._agent_memory() as agent_memory:
+ response = await agent_memory.search_long_term_memory_async(
+ request=request
+ )
- # Response is a MemoryRecordResults object with .memories attribute
memories = []
- for memory in response.memories:
+ for memory in read_field(response, "items", []) or []:
+ memory_type = read_field(memory, "memory_type")
+ created_at = read_field(memory, "created_at")
memories.append(
{
- "id": memory.id,
- "content": memory.text,
- "score": getattr(memory, "dist", 0.0),
- "topics": memory.topics or [],
- "memory_type": memory.memory_type,
- "created_at": str(memory.created_at)
- if memory.created_at
+ "id": read_field(memory, "id"),
+ "content": read_field(memory, "text"),
+ "score": read_field(memory, "score", 0.0),
+ "topics": read_field(memory, "topics", []) or [],
+ "memory_type": str(getattr(memory_type, "value", memory_type))
+ if memory_type
else None,
+ "created_at": str(created_at) if created_at else None,
}
)
diff --git a/src/adk_redis/tools/memory/update.py b/src/adk_redis/tools/memory/update.py
index 1cdd6cc..b909d52 100644
--- a/src/adk_redis/tools/memory/update.py
+++ b/src/adk_redis/tools/memory/update.py
@@ -21,6 +21,8 @@
from google.genai import types
+from adk_redis.memory._backends import OPENSOURCE_AGENT_MEMORY_BACKEND
+from adk_redis.memory._utils import read_field
from adk_redis.tools.memory._base import BaseMemoryTool
from adk_redis.tools.memory._config import MemoryToolConfig
@@ -127,8 +129,8 @@ async def run_async(self, **kwargs: Any) -> dict[str, Any]:
memory_id = args.get("memory_id")
content = args.get("content")
topics = args.get("topics")
- self._get_namespace(args.get("namespace"))
- self._get_user_id(args.get("user_id"))
+ namespace = args.get("namespace")
+ user_id = args.get("user_id")
if not memory_id:
return {"status": "error", "message": "memory_id is required"}
@@ -140,24 +142,49 @@ async def run_async(self, **kwargs: Any) -> dict[str, Any]:
}
try:
- # Build updates dict - use 'text' instead of 'content'
- updates = {}
+ if self._config.backend == OPENSOURCE_AGENT_MEMORY_BACKEND:
+ updates: dict[str, Any] = {}
+ if content:
+ updates["text"] = content
+ if topics is not None:
+ updates["topics"] = topics
+
+ response = (
+ await (
+ self._get_agent_memory_server_client().edit_long_term_memory(
+ memory_id=memory_id,
+ updates=updates,
+ )
+ )
+ )
+ return {
+ "status": "success",
+ "memory_id": response.id,
+ "message": f"Memory {response.id} updated successfully",
+ }
+
+ update_kwargs: dict[str, Any] = {}
if content:
- updates["text"] = content
+ update_kwargs["text"] = content
if topics is not None:
- updates["topics"] = topics
-
- # edit_long_term_memory only takes memory_id and updates dict
- client = self._get_client()
- response = await client.edit_long_term_memory(
- memory_id=memory_id,
- updates=updates,
- )
+ update_kwargs["topics"] = topics
+ if namespace:
+ update_kwargs["namespace"] = namespace
+ if user_id:
+ update_kwargs["owner_id"] = user_id
+
+ async with self._agent_memory() as agent_memory:
+ response = await agent_memory.update_long_term_memory_async(
+ memory_id=memory_id,
+ **update_kwargs,
+ )
return {
"status": "success",
- "memory_id": response.id,
- "message": f"Memory {response.id} updated successfully",
+ "memory_id": read_field(response, "id"),
+ "message": (
+ f"Memory {read_field(response, 'id')} updated successfully"
+ ),
}
except Exception as e:
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index 1d532bd..c2c6437 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -55,6 +55,35 @@ def _redis_reachable(url: str) -> bool:
)
+REDIS_AGENT_MEMORY_URL = os.environ.get("REDIS_AGENT_MEMORY_API_BASE_URL")
+REDIS_AGENT_MEMORY_API_KEY = os.environ.get("REDIS_AGENT_MEMORY_API_KEY")
+REDIS_AGENT_MEMORY_STORE_ID = os.environ.get("REDIS_AGENT_MEMORY_STORE_ID")
+
+REQUIRES_REDIS_AGENT_MEMORY = pytest.mark.skipif(
+ not (
+ REDIS_AGENT_MEMORY_URL
+ and REDIS_AGENT_MEMORY_API_KEY
+ and REDIS_AGENT_MEMORY_STORE_ID
+ ),
+ reason=(
+ "Redis Agent Memory env vars not set. Set "
+ "REDIS_AGENT_MEMORY_API_BASE_URL, REDIS_AGENT_MEMORY_API_KEY, and "
+ "REDIS_AGENT_MEMORY_STORE_ID to enable."
+ ),
+)
+
+
+AGENT_MEMORY_SERVER_URL = os.environ.get("AGENT_MEMORY_SERVER_URL")
+
+REQUIRES_AGENT_MEMORY_SERVER = pytest.mark.skipif(
+ not AGENT_MEMORY_SERVER_URL,
+ reason=(
+ "Agent Memory Server not configured. Set AGENT_MEMORY_SERVER_URL "
+ "(for example http://localhost:8000) to enable."
+ ),
+)
+
+
@pytest.fixture(scope="session")
def redis_url() -> str:
"""Redis URL for integration tests."""
@@ -65,3 +94,15 @@ def redis_url() -> str:
def unique_index_name() -> str:
"""Unique index name to isolate test runs."""
return f"adk_redis_it_{uuid.uuid4().hex[:8]}"
+
+
+@pytest.fixture
+def unique_namespace() -> str:
+ """Unique namespace to isolate memory-backend test runs."""
+ return f"adk_redis_it_{uuid.uuid4().hex[:8]}"
+
+
+@pytest.fixture
+def unique_user_id() -> str:
+ """Unique owner/user ID to isolate memory-backend test runs."""
+ return f"user_{uuid.uuid4().hex[:8]}"
diff --git a/tests/integration/test_memory_backends_end_to_end.py b/tests/integration/test_memory_backends_end_to_end.py
new file mode 100644
index 0000000..e7b8258
--- /dev/null
+++ b/tests/integration/test_memory_backends_end_to_end.py
@@ -0,0 +1,159 @@
+# Copyright 2025 Redis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""End-to-end integration tests for the selectable memory backends.
+
+Round-trips RedisLongTermMemoryService and RedisWorkingMemorySessionService
+against real backends. Tests skip when env vars are not set:
+
+- redis-agent-memory (managed): REDIS_AGENT_MEMORY_API_BASE_URL,
+ REDIS_AGENT_MEMORY_API_KEY, REDIS_AGENT_MEMORY_STORE_ID
+- opensource-agent-memory (self-hosted): AGENT_MEMORY_SERVER_URL
+"""
+
+from __future__ import annotations
+
+import uuid
+
+from google.adk.events.event import Event
+from google.adk.memory.memory_entry import MemoryEntry
+from google.genai import types
+import pytest
+
+from adk_redis import RedisLongTermMemoryService
+from adk_redis import RedisLongTermMemoryServiceConfig
+from adk_redis import RedisWorkingMemorySessionService
+from adk_redis import RedisWorkingMemorySessionServiceConfig
+from tests.integration.conftest import AGENT_MEMORY_SERVER_URL
+from tests.integration.conftest import REDIS_AGENT_MEMORY_API_KEY
+from tests.integration.conftest import REDIS_AGENT_MEMORY_STORE_ID
+from tests.integration.conftest import REDIS_AGENT_MEMORY_URL
+from tests.integration.conftest import REQUIRES_AGENT_MEMORY_SERVER
+from tests.integration.conftest import REQUIRES_REDIS_AGENT_MEMORY
+
+pytest.importorskip("agent_memory_client")
+pytest.importorskip("redis_agent_memory")
+
+_APP_NAME = "adk_redis_it"
+_QUERY = "what color does the user like?"
+_MEMORY_TEXT = "The user prefers the color teal."
+
+
+def _text_event(author: str, text: str) -> Event:
+ return Event(
+ author=author,
+ content=types.Content(role=author, parts=[types.Part(text=text)]),
+ )
+
+
+def _memory_entry(text: str) -> MemoryEntry:
+ return MemoryEntry(
+ content=types.Content(parts=[types.Part(text=text)]),
+ custom_metadata={"memory_type": "semantic"},
+ )
+
+
+def _text_of(parts_owner) -> str:
+ if parts_owner and parts_owner.content and parts_owner.content.parts:
+ return (parts_owner.content.parts[0].text or "").lower()
+ return ""
+
+
+@REQUIRES_REDIS_AGENT_MEMORY
+class TestRedisAgentMemoryBackendEndToEnd:
+ """Round-trip the managed Redis Agent Memory backend."""
+
+ def _long_term(self, namespace: str) -> RedisLongTermMemoryService:
+ return RedisLongTermMemoryService(
+ config=RedisLongTermMemoryServiceConfig(
+ backend="redis-agent-memory",
+ api_base_url=REDIS_AGENT_MEMORY_URL,
+ api_key=REDIS_AGENT_MEMORY_API_KEY,
+ store_id=REDIS_AGENT_MEMORY_STORE_ID,
+ default_namespace=namespace,
+ search_top_k=5,
+ )
+ )
+
+ async def test_add_memory_then_search(
+ self, unique_namespace: str, unique_user_id: str
+ ) -> None:
+ service = self._long_term(unique_namespace)
+ await service.add_memory(
+ app_name=_APP_NAME,
+ user_id=unique_user_id,
+ memories=[_memory_entry(_MEMORY_TEXT)],
+ )
+ response = await service.search_memory(
+ app_name=_APP_NAME, user_id=unique_user_id, query=_QUERY
+ )
+ assert any("teal" in _text_of(m) for m in response.memories)
+
+ async def test_session_append_and_get(
+ self, unique_namespace: str, unique_user_id: str
+ ) -> None:
+ sessions = RedisWorkingMemorySessionService(
+ config=RedisWorkingMemorySessionServiceConfig(
+ backend="redis-agent-memory",
+ api_base_url=REDIS_AGENT_MEMORY_URL,
+ api_key=REDIS_AGENT_MEMORY_API_KEY,
+ store_id=REDIS_AGENT_MEMORY_STORE_ID,
+ default_namespace=unique_namespace,
+ )
+ )
+ session_id = f"sess_{uuid.uuid4().hex[:8]}"
+ session = await sessions.create_session(
+ app_name=_APP_NAME, user_id=unique_user_id, session_id=session_id
+ )
+ await sessions.append_event(
+ session=session, event=_text_event("user", "hi from integration test")
+ )
+ fetched = await sessions.get_session(
+ app_name=_APP_NAME, user_id=unique_user_id, session_id=session_id
+ )
+ assert fetched is not None and fetched.id == session_id
+ assert any("integration test" in _text_of(e) for e in fetched.events)
+ await sessions.delete_session(
+ app_name=_APP_NAME, user_id=unique_user_id, session_id=session_id
+ )
+
+
+@REQUIRES_AGENT_MEMORY_SERVER
+class TestAgentMemoryServerBackendEndToEnd:
+ """Round-trip the self-hosted Agent Memory Server backend."""
+
+ async def test_add_memory_then_search(
+ self, unique_namespace: str, unique_user_id: str
+ ) -> None:
+ service = RedisLongTermMemoryService(
+ config=RedisLongTermMemoryServiceConfig(
+ backend="opensource-agent-memory",
+ api_base_url=AGENT_MEMORY_SERVER_URL,
+ default_namespace=unique_namespace,
+ search_top_k=5,
+ recency_boost=False,
+ )
+ )
+ try:
+ await service.add_memory(
+ app_name=_APP_NAME,
+ user_id=unique_user_id,
+ memories=[_memory_entry(_MEMORY_TEXT)],
+ )
+ response = await service.search_memory(
+ app_name=_APP_NAME, user_id=unique_user_id, query=_QUERY
+ )
+ assert isinstance(response.memories, list)
+ finally:
+ await service.close()
diff --git a/tests/memory/test_long_term_memory.py b/tests/memory/test_long_term_memory.py
index be248d1..58622cc 100644
--- a/tests/memory/test_long_term_memory.py
+++ b/tests/memory/test_long_term_memory.py
@@ -14,23 +14,68 @@
"""Tests for RedisLongTermMemoryService."""
-from unittest.mock import AsyncMock
-from unittest.mock import MagicMock
+from datetime import datetime
+from datetime import timezone
+from types import SimpleNamespace
from unittest.mock import patch
+from google.adk.memory.memory_entry import MemoryEntry
+from google.adk.sessions.session import Session
+from google.genai import types
import pytest
from adk_redis.memory import RedisLongTermMemoryService
from adk_redis.memory import RedisLongTermMemoryServiceConfig
+class FakeAgentMemory:
+ """Fake async Redis Agent Memory client."""
+
+ def __init__(self):
+ self.created_records = []
+ self.search_request = None
+ self.search_response = SimpleNamespace(items=[])
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ return None
+
+ async def bulk_create_long_term_memories_async(self, *, memories):
+ self.created_records.extend(memories)
+ return SimpleNamespace(
+ created=[memory["id"] for memory in memories],
+ errors=None,
+ )
+
+ async def search_long_term_memory_async(self, *, request):
+ self.search_request = request
+ return self.search_response
+
+
+class FakeAgentMemoryServerClient:
+ """Fake self-hosted Agent Memory Server client."""
+
+ def __init__(self):
+ self.search_kwargs = None
+ self.search_response = SimpleNamespace(memories=[])
+
+ async def search_long_term_memory(self, **kwargs):
+ self.search_kwargs = kwargs
+ return self.search_response
+
+
class TestRedisLongTermMemoryServiceConfig:
"""Tests for RedisLongTermMemoryServiceConfig."""
def test_default_values(self):
"""Test default configuration values."""
config = RedisLongTermMemoryServiceConfig()
+ assert config.backend == "redis-agent-memory"
assert config.api_base_url == "http://localhost:8000"
+ assert config.api_key is None
+ assert config.store_id is None
assert config.timeout == 30.0
assert config.default_namespace is None
assert config.recency_boost is True
@@ -38,11 +83,14 @@ def test_default_values(self):
assert config.semantic_weight == 0.8
assert config.extraction_strategy == "discrete"
assert config.extraction_strategy_config == {}
+ assert config.store_events_as_messages is True
def test_custom_values(self):
"""Test custom configuration values."""
config = RedisLongTermMemoryServiceConfig(
api_base_url="http://custom:9000",
+ api_key="key",
+ store_id="store",
timeout=60.0,
default_namespace="test_ns",
recency_weight=0.5,
@@ -51,6 +99,8 @@ def test_custom_values(self):
extraction_strategy_config={"max_length": 100},
)
assert config.api_base_url == "http://custom:9000"
+ assert config.api_key == "key"
+ assert config.store_id == "store"
assert config.timeout == 60.0
assert config.default_namespace == "test_ns"
assert config.recency_weight == 0.5
@@ -58,6 +108,11 @@ def test_custom_values(self):
assert config.extraction_strategy == "summary"
assert config.extraction_strategy_config == {"max_length": 100}
+ def test_opensource_backend_value(self):
+ """Test the self-hosted backend value is accepted."""
+ config = RedisLongTermMemoryServiceConfig(backend="opensource-agent-memory")
+ assert config.backend == "opensource-agent-memory"
+
class TestRedisLongTermMemoryServiceInit:
"""Tests for RedisLongTermMemoryService initialization."""
@@ -80,40 +135,139 @@ class TestRedisLongTermMemoryServiceMethods:
"""Tests for RedisLongTermMemoryService methods."""
@pytest.fixture
- def service(self):
+ def fake_client(self):
+ """Create a fake Redis Agent Memory client."""
+ return FakeAgentMemory()
+
+ @pytest.fixture
+ def service(self, fake_client):
"""Create a service instance for testing."""
- return RedisLongTermMemoryService()
+ service = RedisLongTermMemoryService(
+ RedisLongTermMemoryServiceConfig(default_namespace="test_ns")
+ )
+ with patch.object(service, "_get_client", return_value=fake_client):
+ yield service
@pytest.mark.asyncio
- async def test_search_memory_returns_empty_on_error(self, service):
- """Test search_memory returns empty response on error."""
- with patch.object(
- service,
- "_client",
- create=True,
- new_callable=lambda: MagicMock(
- search_long_term_memory=AsyncMock(
- side_effect=Exception("Test error")
+ async def test_search_memory_uses_owner_and_namespace_filter(
+ self, service, fake_client
+ ):
+ """Test search_memory scopes requests by owner and namespace."""
+ fake_client.search_response = SimpleNamespace(
+ items=[
+ SimpleNamespace(
+ id="memory-1",
+ text="The user prefers window seats.",
+ owner_id="alice",
+ namespace="test_ns",
+ session_id="session-1",
+ topics=["travel"],
+ memory_type="semantic",
+ created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
)
+ ]
+ )
+
+ result = await service.search_memory(
+ app_name="app",
+ user_id="alice",
+ query="seat preference",
+ )
+
+ assert fake_client.search_request["filter"] == {
+ "ownerId": {"eq": "alice"},
+ "namespace": {"eq": "test-ns"},
+ }
+ assert result.memories[0].id == "memory-1"
+ assert (
+ result.memories[0].content.parts[0].text
+ == "The user prefers window seats."
+ )
+
+ @pytest.mark.asyncio
+ async def test_add_memory_creates_long_term_records(
+ self, service, fake_client
+ ):
+ """Test add_memory writes explicit Redis Agent Memory records."""
+ memory = MemoryEntry(
+ id="memory-1",
+ content=types.Content(
+ parts=[types.Part(text="The user prefers window seats.")]
),
- ):
- result = await service.search_memory(
- app_name="test_app",
- user_id="test_user",
- query="test query",
- )
- assert result.memories == []
+ custom_metadata={"topics": ["travel"]},
+ )
+
+ await service.add_memory(
+ app_name="app",
+ user_id="alice",
+ memories=[memory],
+ )
+
+ assert fake_client.created_records == [
+ {
+ "id": "memory-1",
+ "text": "The user prefers window seats.",
+ "ownerId": "alice",
+ "namespace": "test-ns",
+ "sessionId": None,
+ "topics": ["travel", "app"],
+ "memoryType": "semantic",
+ }
+ ]
@pytest.mark.asyncio
- async def test_close_cleans_up_client(self, service):
- """Test close method cleans up client."""
- mock_client = MagicMock()
- mock_client.close = AsyncMock()
+ async def test_add_session_to_memory_creates_message_records(
+ self, service, fake_client
+ ):
+ """Test add_session_to_memory stores event text as message memory."""
+ session = Session(
+ id="session-1",
+ app_name="app",
+ user_id="alice",
+ events=[],
+ )
- # Manually set the cached property
- service.__dict__["_client"] = mock_client
+ await service.add_session_to_memory(session)
- await service.close()
+ assert fake_client.created_records == []
+
+ @pytest.mark.asyncio
+ async def test_search_memory_can_use_agent_memory_server_backend(self):
+ """Test search_memory can use the self-hosted backend."""
+ fake_client = FakeAgentMemoryServerClient()
+ fake_client.search_response = SimpleNamespace(
+ memories=[
+ SimpleNamespace(
+ id="memory-1",
+ text="The user prefers aisle seats.",
+ namespace="test_ns",
+ user_id="alice",
+ session_id="session-1",
+ topics=["travel"],
+ memory_type="semantic",
+ created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
+ )
+ ]
+ )
+ service = RedisLongTermMemoryService(
+ RedisLongTermMemoryServiceConfig(
+ backend="opensource-agent-memory",
+ default_namespace="test_ns",
+ recency_boost=False,
+ )
+ )
+
+ with patch.object(
+ service,
+ "_get_agent_memory_server_client",
+ return_value=fake_client,
+ ):
+ result = await service.search_memory(
+ app_name="app",
+ user_id="alice",
+ query="seat preference",
+ )
- mock_client.close.assert_called_once()
- assert "_client" not in service.__dict__
+ assert fake_client.search_kwargs["namespace"] == {"eq": "test_ns"}
+ assert fake_client.search_kwargs["user_id"] == {"eq": "alice"}
+ assert result.memories[0].id == "memory-1"
diff --git a/tests/sessions/test_working_memory.py b/tests/sessions/test_working_memory.py
index 5da0646..42e3bb6 100644
--- a/tests/sessions/test_working_memory.py
+++ b/tests/sessions/test_working_memory.py
@@ -14,23 +14,75 @@
"""Tests for RedisWorkingMemorySessionService."""
-from unittest.mock import AsyncMock
-from unittest.mock import MagicMock
+from datetime import datetime
+from datetime import timezone
+from types import SimpleNamespace
from unittest.mock import patch
+from google.adk.events.event import Event
+from google.adk.sessions.session import Session
+from google.genai import types
import pytest
from adk_redis.sessions import RedisWorkingMemorySessionService
from adk_redis.sessions import RedisWorkingMemorySessionServiceConfig
+class FakeAgentMemory:
+ """Fake async Redis Agent Memory client."""
+
+ def __init__(self):
+ self.added_events = []
+ self.deleted_session_id = None
+ self.get_response = None
+ self.list_response = SimpleNamespace(items=[], next_page_token=None)
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ return None
+
+ async def add_session_event_async(self, **kwargs):
+ self.added_events.append(kwargs)
+ return SimpleNamespace(event=kwargs)
+
+ async def get_session_memory_async(self, *, session_id):
+ if self.get_response is None:
+ error = RuntimeError("not found")
+ error.status_code = 404 # type: ignore[attr-defined]
+ raise error
+ return self.get_response
+
+ async def delete_session_memory_async(self, *, session_id):
+ self.deleted_session_id = session_id
+
+ async def list_sessions_async(self, *, limit, page_token):
+ return self.list_response
+
+
+class FakeAgentMemoryServerClient:
+ """Fake self-hosted Agent Memory Server client."""
+
+ def __init__(self):
+ self.list_kwargs = None
+ self.list_response = SimpleNamespace(sessions=[])
+
+ async def list_sessions(self, **kwargs):
+ self.list_kwargs = kwargs
+ return self.list_response
+
+
class TestRedisWorkingMemorySessionServiceConfig:
"""Tests for RedisWorkingMemorySessionServiceConfig."""
def test_default_values(self):
"""Test default configuration values."""
config = RedisWorkingMemorySessionServiceConfig()
+ assert config.backend == "redis-agent-memory"
assert config.api_base_url == "http://localhost:8000"
+ assert config.api_key is None
+ assert config.store_id is None
assert config.timeout == 30.0
assert config.default_namespace is None
assert config.model_name is None
@@ -43,6 +95,8 @@ def test_custom_values(self):
"""Test custom configuration values."""
config = RedisWorkingMemorySessionServiceConfig(
api_base_url="http://custom:9000",
+ api_key="key",
+ store_id="store",
timeout=60.0,
default_namespace="test_ns",
model_name="gpt-4",
@@ -51,6 +105,8 @@ def test_custom_values(self):
session_ttl_seconds=3600,
)
assert config.api_base_url == "http://custom:9000"
+ assert config.api_key == "key"
+ assert config.store_id == "store"
assert config.timeout == 60.0
assert config.default_namespace == "test_ns"
assert config.model_name == "gpt-4"
@@ -58,6 +114,13 @@ def test_custom_values(self):
assert config.extraction_strategy == "summary"
assert config.session_ttl_seconds == 3600
+ def test_opensource_backend_value(self):
+ """Test the self-hosted backend value is accepted."""
+ config = RedisWorkingMemorySessionServiceConfig(
+ backend="opensource-agent-memory"
+ )
+ assert config.backend == "opensource-agent-memory"
+
class TestRedisWorkingMemorySessionServiceInit:
"""Tests for RedisWorkingMemorySessionService initialization."""
@@ -80,30 +143,227 @@ class TestRedisWorkingMemorySessionServiceMethods:
"""Tests for RedisWorkingMemorySessionService methods."""
@pytest.fixture
- def service(self):
+ def fake_client(self):
+ """Create a fake Redis Agent Memory client."""
+ return FakeAgentMemory()
+
+ @pytest.fixture
+ def service(self, fake_client):
"""Create a service instance for testing."""
- return RedisWorkingMemorySessionService()
+ service = RedisWorkingMemorySessionService(
+ RedisWorkingMemorySessionServiceConfig(default_namespace="test_ns")
+ )
+ with patch.object(service, "_get_client", return_value=fake_client):
+ yield service
+
+ def test_storage_session_id_uses_managed_safe_hex(self, service):
+ """Test managed storage session IDs fit API charset and length limits."""
+ storage_id = service._storage_session_id(
+ app_name="app",
+ user_id="alice",
+ session_id="session-1",
+ )
+
+ assert len(storage_id) == 64
+ assert storage_id.isalnum()
+ assert (
+ service._storage_session_id(
+ app_name="app",
+ user_id="alice",
+ session_id="session-1",
+ )
+ == storage_id
+ )
+
+ def test_storage_session_id_hashes_uuid_session_ids(self, service):
+ """Test ADK UUID session IDs map to deterministic managed storage IDs."""
+ storage_id = service._storage_session_id(
+ app_name="app",
+ user_id="user",
+ session_id="28a7d6e2-0f0f-43b6-b22c-47e03635ed34",
+ )
+
+ assert len(storage_id) == 64
+ assert storage_id.isalnum()
@pytest.mark.asyncio
- async def test_list_sessions_returns_empty_on_error(self, service):
- """Test list_sessions returns empty list on error."""
- with patch.object(
- service,
- "_client",
- create=True,
- new_callable=lambda: MagicMock(
- list_sessions=AsyncMock(side_effect=Exception("Test error"))
+ async def test_append_event_writes_session_event(self, service, fake_client):
+ """Test append_event stores a Redis Agent Memory session event."""
+ session = Session(
+ id="session-1",
+ app_name="app",
+ user_id="alice",
+ events=[],
+ )
+ event = Event(
+ id="event-1",
+ author="user",
+ content=types.Content(
+ role="user",
+ parts=[types.Part(text="I prefer window seats.")],
),
- ):
- result = await service.list_sessions(
- app_name="test_app",
- user_id="test_user",
- )
- assert result.sessions == []
+ timestamp=1700000000,
+ )
+
+ await service.append_event(session, event)
+
+ assert len(fake_client.added_events) == 1
+ stored = fake_client.added_events[0]
+ assert stored["actor_id"] == "alice"
+ assert stored["content"] == [{"text": "I prefer window seats."}]
+ assert stored["metadata"]["namespace"] == "test-ns"
+ assert stored["metadata"]["adk_event_id"] == "event-1"
@pytest.mark.asyncio
- async def test_close_cleans_up_client(self, service):
+ async def test_get_session_returns_empty_before_first_event(
+ self, service, fake_client
+ ):
+ """Test get_session returns an empty session before Redis materializes it."""
+ session = await service.get_session(
+ app_name="app",
+ user_id="alice",
+ session_id="fa58803d-9a11-4466-8f3f-40baf61b41c0",
+ )
+
+ assert session is not None
+ assert session.id == "fa58803d-9a11-4466-8f3f-40baf61b41c0"
+ assert session.events == []
+
+ @pytest.mark.asyncio
+ async def test_get_session_reconstructs_adk_events(
+ self, service, fake_client
+ ):
+ """Test get_session converts Redis events into ADK events."""
+ storage_id = service._storage_session_id(
+ app_name="app",
+ user_id="alice",
+ session_id="session-1",
+ )
+ fake_client.get_response = SimpleNamespace(
+ session_id=storage_id,
+ owner_id="alice",
+ events=[
+ SimpleNamespace(
+ event_id="redis-event-1",
+ actor_id="alice",
+ role="user",
+ content=[{"text": "I prefer window seats."}],
+ created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
+ metadata={
+ "adk_event_id": "event-1",
+ "adk_author": "user",
+ "state_delta": {"seat": "window"},
+ },
+ )
+ ],
+ )
+
+ session = await service.get_session(
+ app_name="app",
+ user_id="alice",
+ session_id="session-1",
+ )
+
+ assert session is not None
+ assert session.id == "session-1"
+ assert session.state == {"seat": "window"}
+ assert session.events[0].id == "event-1"
+ assert session.events[0].content.parts[0].text == "I prefer window seats."
+
+ @pytest.mark.asyncio
+ async def test_list_sessions_resolves_storage_ids_from_metadata(
+ self, service, fake_client
+ ):
+ """Test list_sessions resolves managed storage IDs from event metadata."""
+ storage_id = service._storage_session_id(
+ app_name="app",
+ user_id="alice",
+ session_id="28a7d6e2-0f0f-43b6-b22c-47e03635ed34",
+ )
+ fake_client.get_response = SimpleNamespace(
+ session_id=storage_id,
+ owner_id="alice",
+ events=[
+ SimpleNamespace(
+ metadata={
+ "namespace": "test-ns",
+ "user_id": "alice",
+ "adk_session_id": "28a7d6e2-0f0f-43b6-b22c-47e03635ed34",
+ }
+ )
+ ],
+ )
+ fake_client.list_response = SimpleNamespace(
+ items=[storage_id],
+ next_page_token=None,
+ )
+
+ response = await service.list_sessions(app_name="app", user_id="alice")
+
+ assert [session.id for session in response.sessions] == [
+ "28a7d6e2-0f0f-43b6-b22c-47e03635ed34"
+ ]
+
+ @pytest.mark.asyncio
+ async def test_list_sessions_filters_internal_ids(self, service, fake_client):
+ """Test list_sessions filters by namespace and user."""
+ storage_id = service._storage_session_id(
+ app_name="app",
+ user_id="alice",
+ session_id="session-1",
+ )
+ fake_client.get_response = SimpleNamespace(
+ session_id=storage_id,
+ owner_id="alice",
+ events=[
+ SimpleNamespace(
+ metadata={
+ "namespace": "test-ns",
+ "user_id": "alice",
+ "adk_session_id": "session-1",
+ }
+ )
+ ],
+ )
+ fake_client.list_response = SimpleNamespace(
+ items=[storage_id],
+ next_page_token=None,
+ )
+
+ response = await service.list_sessions(app_name="app", user_id="alice")
+
+ assert [session.id for session in response.sessions] == ["session-1"]
+ response_other_user = await service.list_sessions(
+ app_name="app", user_id="bob"
+ )
+ assert response_other_user.sessions == []
+
+ @pytest.mark.asyncio
+ async def test_close_completes_without_error(self, service):
"""Test close method completes without error."""
- # The close method no longer caches or closes clients
- # Just verify it can be called without error
await service.close()
+
+ @pytest.mark.asyncio
+ async def test_list_sessions_can_use_agent_memory_server_backend(self):
+ """Test list_sessions can use the self-hosted backend."""
+ fake_client = FakeAgentMemoryServerClient()
+ fake_client.list_response = SimpleNamespace(sessions=["session-1"])
+ service = RedisWorkingMemorySessionService(
+ RedisWorkingMemorySessionServiceConfig(
+ backend="opensource-agent-memory",
+ default_namespace="test_ns",
+ )
+ )
+
+ with patch.object(
+ service,
+ "_get_agent_memory_server_client",
+ return_value=fake_client,
+ ):
+ response = await service.list_sessions(app_name="app", user_id="alice")
+
+ assert fake_client.list_kwargs == {
+ "namespace": "test_ns",
+ "user_id": "alice",
+ }
+ assert [session.id for session in response.sessions] == ["session-1"]
diff --git a/tests/tools/test_memory_tools.py b/tests/tools/test_memory_tools.py
new file mode 100644
index 0000000..048c3e5
--- /dev/null
+++ b/tests/tools/test_memory_tools.py
@@ -0,0 +1,192 @@
+# Copyright 2025 Redis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for Redis Agent Memory tools."""
+
+from types import SimpleNamespace
+from unittest.mock import patch
+
+import pytest
+
+from adk_redis.tools.memory import CreateMemoryTool
+from adk_redis.tools.memory import DeleteMemoryTool
+from adk_redis.tools.memory import MemoryToolConfig
+from adk_redis.tools.memory import SearchMemoryTool
+from adk_redis.tools.memory import UpdateMemoryTool
+
+
+class FakeAgentMemory:
+ """Fake async Redis Agent Memory client."""
+
+ def __init__(self):
+ self.created_records = []
+ self.deleted_ids = []
+ self.search_request = None
+ self.update_kwargs = None
+
+ async def __aenter__(self):
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ return None
+
+ async def bulk_create_long_term_memories_async(self, *, memories):
+ self.created_records.extend(memories)
+ return SimpleNamespace(
+ created=[memory["id"] for memory in memories],
+ errors=None,
+ )
+
+ async def search_long_term_memory_async(self, *, request):
+ self.search_request = request
+ return SimpleNamespace(
+ items=[
+ SimpleNamespace(
+ id="memory-1",
+ text="The user prefers window seats.",
+ topics=["travel"],
+ memory_type="semantic",
+ created_at=None,
+ )
+ ]
+ )
+
+ async def update_long_term_memory_async(self, **kwargs):
+ self.update_kwargs = kwargs
+ return SimpleNamespace(id=kwargs["memory_id"])
+
+ async def bulk_delete_long_term_memories_async(self, *, memory_ids):
+ self.deleted_ids.extend(memory_ids)
+ return SimpleNamespace(deleted=memory_ids, errors=None)
+
+
+class FakeAgentMemoryServerClient:
+ """Fake self-hosted Agent Memory Server client."""
+
+ def __init__(self):
+ self.add_memory_kwargs = None
+
+ async def add_memory_tool(self, **kwargs):
+ self.add_memory_kwargs = kwargs
+ return {
+ "success": True,
+ "memory_id": "memory-1",
+ "summary": "Memory created successfully",
+ }
+
+
+@pytest.fixture
+def config():
+ """Create memory tool config."""
+ return MemoryToolConfig(
+ default_namespace="test_ns",
+ default_owner_id="alice",
+ )
+
+
+@pytest.fixture
+def fake_client():
+ """Create fake Redis Agent Memory client."""
+ return FakeAgentMemory()
+
+
+def test_memory_tool_config_accepts_opensource_backend():
+ """Memory tool config accepts the self-hosted backend value."""
+ assert (
+ MemoryToolConfig(backend="opensource-agent-memory").backend
+ == "opensource-agent-memory"
+ )
+
+
+@pytest.mark.asyncio
+async def test_create_memory_tool_writes_record(config, fake_client):
+ """CreateMemoryTool writes a Redis Agent Memory record."""
+ assert config.backend == "redis-agent-memory"
+ tool = CreateMemoryTool(config=config)
+ with patch.object(tool, "_get_client", return_value=fake_client):
+ result = await tool.run_async(args={"content": "User likes tea."})
+
+ assert result["status"] == "success"
+ assert fake_client.created_records[0]["text"] == "User likes tea."
+ assert fake_client.created_records[0]["ownerId"] == "alice"
+ assert fake_client.created_records[0]["namespace"] == "test-ns"
+ assert fake_client.created_records[0]["memoryType"] == "semantic"
+
+
+@pytest.mark.asyncio
+async def test_search_memory_tool_uses_owner_and_namespace_filter(
+ config, fake_client
+):
+ """SearchMemoryTool scopes search by owner and namespace."""
+ tool = SearchMemoryTool(config=config)
+ with patch.object(tool, "_get_client", return_value=fake_client):
+ result = await tool.run_async(args={"query": "seat"})
+
+ assert result["status"] == "success"
+ assert fake_client.search_request["filter"] == {
+ "namespace": {"eq": "test-ns"},
+ "ownerId": {"eq": "alice"},
+ }
+
+
+@pytest.mark.asyncio
+async def test_update_memory_tool_calls_update(config, fake_client):
+ """UpdateMemoryTool calls Redis Agent Memory update."""
+ tool = UpdateMemoryTool(config=config)
+ with patch.object(tool, "_get_client", return_value=fake_client):
+ result = await tool.run_async(
+ args={"memory_id": "memory-1", "content": "Updated"}
+ )
+
+ assert result["status"] == "success"
+ assert fake_client.update_kwargs == {
+ "memory_id": "memory-1",
+ "text": "Updated",
+ }
+
+
+@pytest.mark.asyncio
+async def test_delete_memory_tool_calls_bulk_delete(config, fake_client):
+ """DeleteMemoryTool deletes memory IDs."""
+ tool = DeleteMemoryTool(config=config)
+ with patch.object(tool, "_get_client", return_value=fake_client):
+ result = await tool.run_async(args={"memory_ids": ["memory-1"]})
+
+ assert result["status"] == "success"
+ assert result["deleted_count"] == 1
+ assert fake_client.deleted_ids == ["memory-1"]
+
+
+@pytest.mark.asyncio
+async def test_create_memory_tool_can_use_agent_memory_server_backend():
+ """CreateMemoryTool can write through the self-hosted backend."""
+ fake_client = FakeAgentMemoryServerClient()
+ config = MemoryToolConfig(
+ backend="opensource-agent-memory",
+ default_namespace="test_ns",
+ default_user_id="alice",
+ )
+ tool = CreateMemoryTool(config=config)
+
+ with patch.object(
+ tool,
+ "_get_agent_memory_server_client",
+ return_value=fake_client,
+ ):
+ result = await tool.run_async(args={"content": "User likes tea."})
+
+ assert result["status"] == "success"
+ assert fake_client.add_memory_kwargs["text"] == "User likes tea."
+ assert fake_client.add_memory_kwargs["namespace"] == "test_ns"
+ assert fake_client.add_memory_kwargs["user_id"] == "alice"