From f975c561cdcdfad1fc686c26355d7f003deb4eb5 Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Tue, 19 May 2026 19:59:45 -0400 Subject: [PATCH 1/8] chore: release 0.0.5 - Bump version 0.0.4 -> 0.0.5 in pyproject.toml. - Roll CHANGELOG: cut a 0.0.5 release section covering the redisvl 0.18.2 integration (PR #10): epsilon removal, llmcache import migration, new RedisSQLSearchTool, new create_redisvl_mcp_toolset helper, new integration test suite, two runnable examples. - Stop tracking three stale blog-post draft files (blog_post.docx / blog_post.md / blog_post_0.md); they were committed earlier but never referenced from the repo. Files remain on disk locally; only removed from the git index. --- CHANGELOG.md | 2 + blog_post.docx | Bin 26004 -> 0 bytes blog_post.md | 540 ------------------------------------------------- blog_post_0.md | 527 ----------------------------------------------- pyproject.toml | 2 +- 5 files changed, 3 insertions(+), 1068 deletions(-) delete mode 100644 blog_post.docx delete mode 100644 blog_post.md delete mode 100644 blog_post_0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 104b884..d8262a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [0.0.5] - 2026-05-19 + ### Breaking - Removed `epsilon` from `RedisVectorQueryConfig`. `EPSILON` is a diff --git a/blog_post.docx b/blog_post.docx deleted file mode 100644 index 541f1502d21b952a9b1ec30a90c024b976d6bf5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26004 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 Fe*j;J_0|9Y diff --git a/blog_post.md b/blog_post.md deleted file mode 100644 index db2e074..0000000 --- a/blog_post.md +++ /dev/null @@ -1,540 +0,0 @@ -# Redis as the Memory Layer for Google ADK Agents - -Google's Agent Development Kit (ADK) provides clean abstractions for building AI agents. It defines interfaces for memory, sessions, tools, and callbacks. But the default implementations store everything in process memory, which means state disappears on restart and there is no path from prototype to production without replacing the storage layer. - -We built `adk-redis` to be that storage layer. It is a Python package that implements ADK's `BaseMemoryService`, `BaseSessionService`, and tool interfaces using Redis, giving agents persistent two-tier memory, semantic search for RAG, and response caching, all without requiring changes to agent logic. The package connects to the Redis Agent Memory Server for long-term memory extraction and to RedisVL, the Redis Vector Library that provides a Python client for vector similarity, hybrid search, and semantic caching on top of Redis, for all retrieval and caching operations. Together, these let agents built with ADK move from a local demo to a deployed system by swapping a few service configurations. - -For teams already running Redis, this means first-class ADK integration on top of infrastructure you already operate and understand. And for teams that want to avoid tying their agent stack to a single cloud provider, `adk-redis` provides a production-quality alternative that runs anywhere Redis runs, whether that is Docker on a laptop, a managed Redis service on any cloud, or bare-metal servers in your own data center. - -In this post, we walk through the architecture and implementation of `adk-redis`. We cover how the memory system works, the four search tools available, how semantic caching works, and the three distinct integration patterns for connecting agents to memory. We build up from individual components to a fully wired travel planning agent that uses all of them together. - -## What `adk-redis` Actually Provides - -Before diving into implementation, it helps to see the full landscape. The package is organized around six capabilities. - -**Memory Services** implement ADK's `BaseMemoryService`. This is long-term memory. The service connects to the Redis Agent Memory Server, which handles semantic search, automatic fact extraction, and recency-boosted retrieval across all of your agent's past conversations. - -**Session Services** implement ADK's `BaseSessionService`. This is working memory. Sessions store the current conversation, manage session state, and automatically summarize older messages when the context window gets too large. - -**Memory Tools** give the LLM explicit, fine-grained control over long-term memory through ADK-compatible tool calls. Rather than relying on the framework to manage memory automatically, the agent can search, create, update, and delete memories on its own. These tools communicate with the Agent Memory Server over REST and include `SearchMemoryTool`, `CreateMemoryTool`, `UpdateMemoryTool`, `DeleteMemoryTool`, `GetMemoryTool`, and `MemoryPromptTool` (which enriches a prompt with relevant memories before sending it to the model). - -**MCP Tools** expose the same memory operations through the Model Context Protocol (MCP) instead of REST. You point ADK's `McpToolset` at the Agent Memory Server's SSE endpoint, and tool discovery happens automatically. This is useful when you want a standardized protocol layer between your agent and its memory backend, or when you are already using MCP for other tool integrations. - -**Search Tools** wrap RedisVL (the Redis Vector Library) into ADK-compatible tools that your agent's LLM can call directly. There are four variants covering vector search, hybrid search, text search, and range search. - -**Semantic Caching** intercepts LLM calls and tool executions, checking whether a semantically similar prompt has been seen before. If so, it returns the cached response instead of making a new API call. This works through ADK's callback system, so enabling it requires no changes to your agent's core logic. - -The package is modular. You install only what you need. - -```bash -pip install adk-redis[memory] # Memory and session services -pip install adk-redis[search] # Search tools via RedisVL -pip install adk-redis[langcache] # Managed semantic caching -pip install adk-redis[all] # Everything -``` - -## The Two-Tier Memory Architecture - -The central design idea behind `adk-redis` is a two-tier memory system that mirrors how human memory works. There is a fast, limited working memory for the current conversation, and a slower, persistent long-term memory for facts and preferences that should survive across sessions. - -### Tier 1 (Working Memory via `RedisWorkingMemorySessionService`) - -Working memory handles the current session. Every message exchanged between the user and the agent is stored in the Redis Agent Memory Server. When the conversation grows long enough to approach the model's context window limit, the service automatically summarizes older messages, compressing them into a summary while preserving the most recent exchanges in full. - -This is a surprisingly important feature. Without it, you face a hard tradeoff. Either you truncate old messages and lose context, or you send the full conversation and hit token limits (and costs). Auto-summarization gives you a middle path. - -Here is how you configure it. - -```python -from adk_redis.sessions import ( - RedisWorkingMemorySessionService, - RedisWorkingMemorySessionServiceConfig, -) - -session_config = RedisWorkingMemorySessionServiceConfig( - api_base_url="http://localhost:8088", - default_namespace="my_app", - model_name="gpt-4o", - context_window_max=8000, -) -session_service = RedisWorkingMemorySessionService(config=session_config) -``` - -The `context_window_max` parameter is what triggers summarization. When the token count of stored messages crosses this threshold, the Agent Memory Server uses the model specified in `model_name` to summarize older turns. The `default_namespace` isolates your application's data from other applications sharing the same Redis instance. - -Under the hood, the session service implements all of ADK's required methods. `create_session`, `get_session`, `list_sessions`, `delete_session`, and `append_event`. The `append_event` method is particularly worth noting. Rather than re-sending the entire conversation on every turn, it uses an incremental append API, sending only the new message. This keeps network overhead proportional to the message size, not the conversation length. - -### Tier 2 (Long-Term Memory via `RedisLongTermMemoryService`) - -Long-term memory is where the real intelligence lives. After each conversation (or on a configurable debounce), the Agent Memory Server extracts structured information from the dialogue. "The user prefers window seats." "The user is allergic to shellfish." "The user visited Tokyo last March." These extracted memories are embedded as vectors and stored in Redis, where they become searchable across all past sessions. - -```python -from adk_redis.memory import ( - RedisLongTermMemoryService, - RedisLongTermMemoryServiceConfig, -) - -memory_config = RedisLongTermMemoryServiceConfig( - api_base_url="http://localhost:8088", - default_namespace="my_app", - extraction_strategy="discrete", - recency_boost=True, - semantic_weight=0.7, - recency_weight=0.3, -) -memory_service = RedisLongTermMemoryService(config=memory_config) -``` - - -The `extraction_strategy` parameter controls how the server breaks down conversations into storable facts. The `"discrete"` strategy extracts individual facts as separate memories, which makes them independently searchable. Other options include `"summary"` (a narrative summary of the conversation) and `"preferences"` (focused on user preferences). - -Recency boosting deserves a closer look. When searching memories, raw semantic similarity alone often isn't enough. A user might have said "I love Italian food" three years ago, and "Actually, I've been getting into Japanese cuisine lately" last week. Both are semantically relevant to a query about food preferences, but the recent one matters more. - -The recency boosting system addresses this by combining two scores. The `semantic_weight` controls how much the vector similarity matters, while `recency_weight` controls how much recency matters. Within the recency score itself, `freshness_weight` favors memories that were recently accessed, and `novelty_weight` favors memories that were recently created. The `half_life_last_access_days` and `half_life_created_days` parameters control how quickly each signal decays. A half-life of 7 days means that a memory's freshness score drops to 50% after a week of not being accessed. - -This is a thoughtful design. It avoids the common failure mode of semantic search systems that return stale information with high confidence. - -### Wiring Both Tiers Together - -With both services configured, you connect them to an ADK `Runner`. - -```python -from google.adk import Agent -from google.adk.runners import Runner - -agent = Agent( - name="memory_agent", - model="gemini-2.5-flash", - instruction="You are a helpful assistant with long-term memory.", -) - -runner = Runner( - agent=agent, - app_name="my_app", - session_service=session_service, - memory_service=memory_service, -) -``` - -The flow is now automatic. Messages are stored in working memory as the conversation happens. When the agent finishes a turn, a callback can trigger `add_session_to_memory()`, which pushes the conversation to the Agent Memory Server for background extraction. On subsequent sessions, the memory service's `search_memory` method retrieves relevant facts from across all past conversations. - - -## Three Ways to Integrate Memory - -One of the more interesting design decisions in `adk-redis` is that it offers three distinct approaches for connecting agents to memory. Each approach has different tradeoffs around control, complexity, and standardization. - -### Approach 1. ADK Services (Framework-Managed) - -This is what we covered in the two-tier memory section. You configure `RedisWorkingMemorySessionService` and `RedisLongTermMemoryService`, pass them to the `Runner`, and the framework handles everything automatically. Memory extraction happens in the background. Search happens before each agent turn. The agent code itself never directly interacts with memory. - -This approach is the simplest to implement and the hardest to customize. The agent has no explicit control over *what* gets stored or *when* it searches. It is best for applications where you want memory to be invisible infrastructure. - -### Approach 2. REST Tools (LLM-Controlled) - -Instead of (or in addition to) framework-managed services, you can give the agent explicit memory tools. These are ADK tools that the LLM calls like any other function. - -```python -from adk_redis.tools.memory import ( - SearchMemoryTool, CreateMemoryTool, - UpdateMemoryTool, DeleteMemoryTool, - MemoryToolConfig, -) - -memory_config = MemoryToolConfig( - api_base_url="http://localhost:8088", - default_namespace="my_app", - recency_boost=True, -) - -tools = [ - SearchMemoryTool(config=memory_config), - CreateMemoryTool(config=memory_config), - UpdateMemoryTool(config=memory_config), - DeleteMemoryTool(config=memory_config), -] -``` - -With this approach, the LLM decides when to search memory, what to store, and what to update. The agent prompt needs to instruct the LLM on memory management strategy. This requires more prompt engineering, but it gives the agent genuine autonomy over its own memory. - -The travel agent example in the repo uses a hybrid of both approaches. Framework services handle session persistence and automatic background extraction. Memory tools give the LLM explicit CRUD control over long-term memories. This is arguably the most powerful configuration, because the agent gets both automatic memory management and the ability to deliberately store or retrieve specific facts. - -### Approach 3. MCP Tools (Model Context Protocol) - -MCP is a standardized protocol for connecting agents to tools via Server-Sent Events (SSE). Instead of REST-based tool implementations, you point the agent at the Agent Memory Server's MCP endpoint and let ADK's `McpToolset` handle tool discovery automatically. - -```python -from adk_redis.tools.mcp_memory import create_memory_mcp_toolset - -memory_tools = create_memory_mcp_toolset( - server_url="http://localhost:9000", - tool_filter=["search_long_term_memory", "create_long_term_memories"], -) - -agent = Agent( - model="gemini-2.5-flash", - name="fitness_coach", - tools=[memory_tools], -) -``` - -The `tool_filter` parameter controls which MCP tools are exposed to the LLM. The Agent Memory Server exposes seven tools through MCP, including `search_long_term_memory`, `create_long_term_memories`, `get_long_term_memory`, `edit_long_term_memory`, `delete_long_term_memories`, `memory_prompt`, and `set_working_memory`. - -The fitness coach example in the repo demonstrates this approach. It connects to memory via MCP and stores both semantic memories (user profile, injuries, equipment) and episodic memories (workouts with timestamps, milestones). The distinction between semantic and episodic memory types is particularly useful. Semantic memories represent timeless facts ("user has a knee injury"), while episodic memories represent events ("user completed 3x12 rows on March 9th"). - -MCP is the most standardized approach and makes it easy to swap memory backends without changing agent code. The tradeoff is that it requires running the Agent Memory Server with MCP support enabled on a separate port. - - -## Search Tools for RAG - -Memory services handle what the agent remembers from past conversations. But what about external knowledge? Product catalogs, documentation, knowledge bases? This is the domain of retrieval-augmented generation (RAG), and `adk-redis` provides four search tools that plug directly into ADK's tool system. - -Each tool wraps a RedisVL query type and exposes itself as a function the LLM can call. The LLM sees a function declaration with a `query` parameter, decides when to use it, and gets back structured results. - -### RedisVectorSearchTool - -The most straightforward option. It embeds the query using a vectorizer, performs K-nearest-neighbor search against a Redis index, and returns the top results. - -```python -from redisvl.index import SearchIndex -from redisvl.utils.vectorize import HFTextVectorizer -from adk_redis.tools import RedisVectorSearchTool, RedisVectorQueryConfig - -vectorizer = HFTextVectorizer(model="redis/langcache-embed-v2") -index = SearchIndex.from_existing("products", redis_url="redis://localhost:6379") - -search_tool = RedisVectorSearchTool( - index=index, - vectorizer=vectorizer, - config=RedisVectorQueryConfig( - vector_field_name="embedding", - return_fields=["name", "description", "price"], - num_results=5, - ), - name="search_product_catalog", - description="Find products by semantic similarity to a description.", -) -``` - -The `name` and `description` parameters matter more than they might seem. These are what the LLM reads to decide whether and when to call the tool. A vague description like "search documents" will lead to the LLM calling it at the wrong times. A specific one like "Find products by semantic similarity to a description" gives the LLM the context it needs. - -### RedisHybridSearchTool - -Hybrid search combines vector similarity with BM25 keyword matching. This is valuable when queries contain specific terms (product IDs, technical acronyms, exact names) that semantic search alone might miss. - -The tool auto-detects whether your Redis server and RedisVL version support native hybrid search (Redis 8.4+ with RedisVL 0.13+). If they do, it uses the server-side `FT.HYBRID` command. If not, it falls back to a client-side aggregation approach. This version detection happens at initialization, so you don't need to think about it. - -```python -from adk_redis.tools import RedisHybridSearchTool, RedisHybridQueryConfig - -hybrid_tool = RedisHybridSearchTool( - index=index, - vectorizer=vectorizer, - config=RedisHybridQueryConfig( - text_field_name="content", - combination_method="LINEAR", - linear_alpha=0.7, - ), - name="search_legal_documents", - description="Search legal documents using both semantic and keyword matching.", -) -``` - -### RedisTextSearchTool and RedisRangeSearchTool - -`RedisTextSearchTool` performs pure BM25 keyword search. No embeddings, no vectorizer needed. It is the right choice when the query is about exact terms, error messages, or API names. - -`RedisRangeSearchTool` is a less common but useful variant. Instead of returning the top-K results, it returns all documents within a distance threshold. This is useful for exhaustive retrieval, such as "find everything related to authentication in our documentation," where you want comprehensive coverage rather than a ranked list. - -Here is a concrete example from the `redis_search_tools` example in the repo, which wires all three search modalities into a single agent. - -```python -from adk_redis.tools import ( - RedisVectorSearchTool, RedisVectorQueryConfig, - RedisTextSearchTool, RedisTextQueryConfig, - RedisRangeSearchTool, RedisRangeQueryConfig, -) - -tools = [ - RedisVectorSearchTool( - name="semantic_search", - description="Semantic similarity search for conceptual queries.", - index=index, vectorizer=vectorizer, - config=RedisVectorQueryConfig(num_results=5), - return_fields=["title", "content", "category"], - ), - RedisTextSearchTool( - name="keyword_search", - description="Keyword search for exact terms and phrases.", - index=index, - config=RedisTextQueryConfig( - text_field_name="content", text_scorer="BM25STD" - ), - return_fields=["title", "content", "category"], - ), - RedisRangeSearchTool( - name="range_search", - description="Returns ALL documents within a semantic distance threshold.", - index=index, vectorizer=vectorizer, - config=RedisRangeQueryConfig(distance_threshold=0.5), - return_fields=["title", "content", "category"], - ), -] - -agent = Agent( - model="gemini-2.5-flash", - name="search_agent", - instruction=( - "You have three search tools. Use semantic_search for conceptual " - "queries, keyword_search for exact terms, range_search for exhaustive " - "retrieval." - ), - tools=tools, -) -``` - -The instruction prompt is doing real work here. It teaches the LLM when to use each tool and what to expect from each. This kind of prompt engineering is not optional. Without it, the LLM will default to calling whichever tool appears first or whichever has the most generic description. - -## Semantic Caching - -LLM API calls are slow and expensive. If your agent handles support queries, a significant fraction of incoming questions will be semantically similar. "How do I reset my password?" and "I need to change my password" should produce the same response, and there is no reason to pay for two LLM calls. - -`adk-redis` provides semantic caching at two levels, LLM response caching and tool result caching, both backed by Redis. - -### LLM Response Cache - -The LLM cache intercepts calls to the language model through ADK's callback system. Before each model call, it checks whether a semantically similar prompt already exists in Redis. If it does, it returns the cached response immediately, skipping the LLM entirely. If it doesn't, it lets the call proceed and stores the response for future lookups. - -```python -from redisvl.utils.vectorize import HFTextVectorizer -from adk_redis.cache import ( - RedisVLCacheProvider, RedisVLCacheProviderConfig, - LLMResponseCache, LLMResponseCacheConfig, - create_llm_cache_callbacks, -) - -vectorizer = HFTextVectorizer(model="redis/langcache-embed-v1") - -provider = RedisVLCacheProvider( - config=RedisVLCacheProviderConfig( - redis_url="redis://localhost:6379", - name="my_llm_cache", - ttl=3600, - distance_threshold=0.1, - ), - vectorizer=vectorizer, -) - -llm_cache = LLMResponseCache( - provider=provider, - config=LLMResponseCacheConfig( - first_message_only=True, - include_app_name=True, - include_user_id=True, - ), -) - -before_cb, after_cb = create_llm_cache_callbacks(llm_cache) - -agent = Agent( - name="cached_agent", - model="gemini-2.0-flash", - instruction="You are a helpful assistant.", - before_model_callback=before_cb, - after_model_callback=after_cb, -) -``` - -A few design decisions are worth noting here. - -The `distance_threshold` parameter (set to 0.1 in this example) controls how similar two prompts need to be for a cache hit. A value of 0.0 means exact match only. A value of 0.1 allows small variations in phrasing. Going much higher risks returning cached responses for genuinely different questions. Tuning this threshold is application-specific and worth experimenting with. - -The `first_message_only` option is a practical default. In a multi-turn conversation, later messages depend heavily on prior context, making semantic cache hits unreliable. Caching only the first message (which is typically a standalone question) avoids returning contextually wrong responses. - -The cache is also smart about what it does *not* cache. Function call responses (where the LLM is invoking a tool) are skipped, as are error responses. This prevents caching intermediate steps that shouldn't be reused. - -### Managed Caching with LangCache - -If you'd rather not manage your own Redis instance and embedding model for caching, `adk-redis` also supports LangCache, a managed semantic caching service from Redis. With LangCache, embeddings are generated server-side, so you don't need a local vectorizer at all. - -```python -from adk_redis.cache import LangCacheProvider, LangCacheProviderConfig - -provider = LangCacheProvider( - config=LangCacheProviderConfig( - cache_id="your-cache-id", - api_key="your-api-key", - ttl=3600, - ) -) -``` - -The same `LLMResponseCache` and `ToolCache` classes work with either provider. You just swap the backend. - -### Tool Result Cache - -The tool cache follows the same pattern but for tool executions rather than LLM calls. If your agent calls an external API with the same arguments repeatedly, the tool cache can short-circuit the call and return the cached result. - -```python -from adk_redis.cache import ToolCache, ToolCacheConfig, create_tool_cache_callbacks - -tool_cache = ToolCache( - provider=provider, - config=ToolCacheConfig( - tool_names={"web_search", "get_weather"}, - ), -) - -before_tool_cb, after_tool_cb = create_tool_cache_callbacks(tool_cache) -``` - -The `tool_names` set lets you specify exactly which tools should be cached. This is important because not all tools are idempotent. You probably want to cache `get_weather` (same city, same hour, same result) but not `send_email` (same arguments, but each call should actually execute). - -## Walking Through the Travel Agent - -To make all of this concrete, let's trace through the `travel_agent_memory_hybrid` example, which is the most complete example in the repo. It combines framework-managed services, LLM-controlled memory tools, web search, itinerary planning, and calendar export into a single agent. - -### The Entrypoint - -The `main.py` file sets up the infrastructure. It registers custom service factories with ADK's service registry, creates both the session service and memory service, and launches a FastAPI app with the ADK web runner. - -```python -from adk_redis.memory import RedisLongTermMemoryService, RedisLongTermMemoryServiceConfig -from adk_redis.sessions import RedisWorkingMemorySessionService, RedisWorkingMemorySessionServiceConfig - -# Register factories so ADK can instantiate them from URIs -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) - -# Build URIs and create the FastAPI app -app = get_fast_api_app( - agents_dir=".", - session_service_uri="redis-working-memory://localhost:8088", - memory_service_uri="redis-long-term-memory://localhost:8088", - web=True, -) -``` - -The URI-based factory pattern is worth noting. ADK's service registry lets you register custom service implementations behind URI schemes. This means you can switch between in-memory and Redis-backed services by changing a URI string, without modifying any agent code. - -### The Agent - -The agent itself is defined in `agent.py`. It assembles a rich set of tools spanning memory, search, and planning. - -```python -from adk_redis.tools.memory import ( - SearchMemoryTool, CreateMemoryTool, - UpdateMemoryTool, DeleteMemoryTool, - MemoryToolConfig, -) -from google.adk.tools import preload_memory, load_memory - -memory_config = MemoryToolConfig( - api_base_url="http://localhost:8088", - default_namespace="travel_agent_memory_hybrid", - recency_boost=True, - search_top_k=10, -) - -tools = [ - SearchMemoryTool(config=memory_config), - CreateMemoryTool(config=memory_config), - UpdateMemoryTool(config=memory_config), - DeleteMemoryTool(config=memory_config), - preload_memory, - load_memory, - CalendarExportTool(), - ItineraryPlannerTool(), -] -``` - -Notice the layered memory strategy. `preload_memory` and `load_memory` are ADK's built-in tools that hook into the `RedisLongTermMemoryService` we configured in `main.py`. These provide automatic, framework-controlled memory retrieval. The `SearchMemoryTool`, `CreateMemoryTool`, and friends give the LLM explicit control on top of that. - -The agent also has an `after_agent_callback` that calls `add_session_to_memory()` after each turn. This is what triggers background extraction of facts and preferences into long-term memory. - -```python -async def after_agent(callback_context: CallbackContext): - await callback_context.add_session_to_memory() - -root_agent = Agent( - model="gemini-2.5-flash", - name="travel_agent", - tools=tools, - after_agent_callback=after_agent, - instruction="...", # Detailed prompt with memory management strategy -) -``` - -### What Happens at Runtime - -When a user starts a conversation, the following sequence plays out. - -1. ADK creates (or retrieves) a session via `RedisWorkingMemorySessionService`. The session is stored in the Agent Memory Server. -2. The agent's `preload_memory` tool automatically searches long-term memory for context relevant to the current conversation. -3. The user sends a message. The message is appended to working memory via the incremental append API. -4. The LLM generates a response. If it needs travel information, it can call web search tools. If it wants to check the user's preferences, it calls `SearchMemoryTool`. If the user shares a new preference, the LLM calls `CreateMemoryTool`. -5. The response is appended to working memory. -6. The `after_agent_callback` fires, sending the conversation to the Agent Memory Server for background extraction. The server pulls out facts like "user prefers direct flights" or "user wants to visit Japan in spring" and stores them as searchable long-term memories. -7. If the conversation grows long, the working memory service automatically summarizes older turns to stay within the context window. - -All of this happens with a `pip install adk-redis[memory]`, a running Redis instance, and a running Agent Memory Server. The agent's Python code is clean, focused on domain logic rather than infrastructure plumbing. - -## Getting Started - -To run any of the examples, you need two things running. - -**Redis 8.4** provides the storage backend for everything. Vector indices, session data, cache entries. - -```bash -docker run -d --name redis -p 6379:6379 redis:8.4-alpine -``` - -**Redis Agent Memory Server** handles memory extraction, summarization, and the working memory API. It sits between your agent and Redis, adding the intelligence layer. - -```bash -docker run -d --name agent-memory-server -p 8088:8088 \ - -e REDIS_URL=redis://host.docker.internal:6379 \ - -e GEMINI_API_KEY=your-key \ - -e GENERATION_MODEL=gemini/gemini-2.0-flash \ - -e EMBEDDING_MODEL=gemini/text-embedding-004 \ - redislabs/agent-memory-server:0.13.2 \ - agent-memory api --host 0.0.0.0 --port 8088 --task-backend=asyncio -``` - -The Agent Memory Server uses LiteLLM under the hood, which means it supports 100+ LLM providers. You can swap in OpenAI, Anthropic, AWS Bedrock, or even local models via Ollama. - -Then install the package and run an example. - -```bash -pip install adk-redis[all] -cd examples/simple_redis_memory -python main.py -``` - -## Conclusion - -`adk-redis` is a focused library that solves a specific problem well. It takes the interfaces that ADK defines, `BaseMemoryService`, `BaseSessionService`, the tool system, the callback system, and provides Redis-backed implementations that are production-grade rather than toy-grade. - -The key ideas worth taking away from this are the following. - -The **two-tier memory architecture** (working memory for sessions, long-term memory for persistent facts) is a pattern that scales well. It mirrors how real applications need to manage state, keeping the current context fast and small while maintaining a durable knowledge base. - -The **three integration approaches** (framework services, REST tools, MCP tools) give you a spectrum from fully automatic to fully LLM-controlled memory management. The hybrid approach, combining framework services with LLM-controlled tools, is particularly effective. - -**Semantic caching** is a straightforward way to reduce costs and latency, and `adk-redis` makes it easy to enable without changing your agent's core logic. - -The **search tools** provide a clean abstraction over RedisVL's query types, making it simple to add RAG capabilities to any ADK agent. - -All of this runs on Redis, a system that most teams already know how to operate, monitor, and scale. - -The [GitHub repository](https://github.com/redis-developer/adk-redis) includes seven complete examples, each focused on a different capability described in this post. - -- **`simple_redis_memory`** is the minimal starting point. It wires up `RedisWorkingMemorySessionService` and `RedisLongTermMemoryService` with a basic conversational agent, demonstrating two-tier memory with no other moving parts. -- **`travel_agent_memory_hybrid`** is the most complete example. It combines framework-managed memory services with LLM-controlled memory tools, web search, itinerary planning, and calendar export into a single agent (this is the example we walked through above). -- **`travel_agent_memory_tools`** uses the REST-based memory tools exclusively, without framework-managed services. The LLM has full control over when to search, create, update, and delete memories. -- **`fitness_coach_mcp`** demonstrates MCP-based memory integration. The agent connects to the Agent Memory Server via SSE and manages semantic and episodic memories for workout tracking. -- **`redis_search_tools`** shows all four RedisVL search tools (vector, hybrid, text, and range) plugged into a single agent with a product catalog dataset. -- **`semantic_cache`** demonstrates local semantic caching using RedisVL, including both LLM response caching and tool result caching with ADK callbacks. -- **`langcache_cache`** uses the managed LangCache service for semantic caching, with server-side embeddings and no local vectorizer required. - -The [Redis Agent Memory Server documentation](https://github.com/redis/agent-memory-server) covers the memory backend in detail, and the [RedisVL documentation](https://docs.redisvl.com) covers the vector search and caching capabilities that power the tools and cache providers. \ No newline at end of file diff --git a/blog_post_0.md b/blog_post_0.md deleted file mode 100644 index 6f38b29..0000000 --- a/blog_post_0.md +++ /dev/null @@ -1,527 +0,0 @@ -# Give Your AI Agents a Brain with Redis - -## How `adk-redis` Brings Persistent Memory, Semantic Search, and Caching to Google's Agent Development Kit - -AI agents are only as useful as what they can remember. An agent that forgets your name between sessions, re-fetches the same data on every call, or can't search its own knowledge base isn't really an agent. It's a stateless function with a chat interface. - -Google's Agent Development Kit (ADK) provides strong abstractions for building agents, but it leaves a critical question unanswered out of the box. Where does the state actually live? ADK defines interfaces like `BaseMemoryService` and `BaseSessionService`, but the default implementations store everything in memory. Restart the process, and everything is gone. - -`adk-redis` is a Python package that fills this gap. It implements ADK's core interfaces using Redis as the storage backbone, giving your agents persistent memory, intelligent session management, production-grade search, and semantic caching. The result is that you can go from a toy demo to a production-ready agent by swapping in a few Redis-backed services, without changing your agent logic at all. - -This post walks through the full surface area of `adk-redis`. We will cover its two-tier memory architecture, the four search tools it provides for RAG, how semantic caching can cut your LLM costs, and the three distinct approaches for integrating memory into your agents. Along the way, we will build up from simple examples to a fully wired travel planning agent. - -## What `adk-redis` Actually Provides - -Before diving into implementation, it helps to see the full landscape. The package is organized around four pillars. - -**Memory Services** implement ADK's `BaseMemoryService`. This is long-term memory. The service connects to the Redis Agent Memory Server, which handles semantic search, automatic fact extraction, and recency-boosted retrieval across all of your agent's past conversations. - -**Session Services** implement ADK's `BaseSessionService`. This is working memory. Sessions store the current conversation, manage session state, and automatically summarize older messages when the context window gets too large. - -**Search Tools** wrap RedisVL (the Redis Vector Library) into ADK-compatible tools that your agent's LLM can call directly. There are four variants covering vector search, hybrid search, text search, and range search. - -**Semantic Caching** intercepts LLM calls and tool executions, checking whether a semantically similar prompt has been seen before. If so, it returns the cached response instead of making a new API call. This works through ADK's callback system, so enabling it requires no changes to your agent's core logic. - -The package is modular. You install only what you need. - -```bash -pip install adk-redis[memory] # Memory and session services -pip install adk-redis[search] # Search tools via RedisVL -pip install adk-redis[langcache] # Managed semantic caching -pip install adk-redis[all] # Everything -``` - -## The Two-Tier Memory Architecture - -The central design idea behind `adk-redis` is a two-tier memory system that mirrors how human memory works. There is a fast, limited working memory for the current conversation, and a slower, persistent long-term memory for facts and preferences that should survive across sessions. - -### Tier 1 (Working Memory via `RedisWorkingMemorySessionService`) - -Working memory handles the current session. Every message exchanged between the user and the agent is stored in the Redis Agent Memory Server. When the conversation grows long enough to approach the model's context window limit, the service automatically summarizes older messages, compressing them into a summary while preserving the most recent exchanges in full. - -This is a surprisingly important feature. Without it, you face a hard tradeoff. Either you truncate old messages and lose context, or you send the full conversation and hit token limits (and costs). Auto-summarization gives you a middle path. - -Here is how you configure it. - -```python -from adk_redis.sessions import ( - RedisWorkingMemorySessionService, - RedisWorkingMemorySessionServiceConfig, -) - -session_config = RedisWorkingMemorySessionServiceConfig( - api_base_url="http://localhost:8088", - default_namespace="my_app", - model_name="gpt-4o", - context_window_max=8000, -) -session_service = RedisWorkingMemorySessionService(config=session_config) -``` - -The `context_window_max` parameter is what triggers summarization. When the token count of stored messages crosses this threshold, the Agent Memory Server uses the model specified in `model_name` to summarize older turns. The `default_namespace` isolates your application's data from other applications sharing the same Redis instance. - -Under the hood, the session service implements all of ADK's required methods. `create_session`, `get_session`, `list_sessions`, `delete_session`, and `append_event`. The `append_event` method is particularly worth noting. Rather than re-sending the entire conversation on every turn, it uses an incremental append API, sending only the new message. This keeps network overhead proportional to the message size, not the conversation length. - -### Tier 2 (Long-Term Memory via `RedisLongTermMemoryService`) - -Long-term memory is where the real intelligence lives. After each conversation (or on a configurable debounce), the Agent Memory Server extracts structured information from the dialogue. "The user prefers window seats." "The user is allergic to shellfish." "The user visited Tokyo last March." These extracted memories are embedded as vectors and stored in Redis, where they become searchable across all past sessions. - -```python -from adk_redis.memory import ( - RedisLongTermMemoryService, - RedisLongTermMemoryServiceConfig, -) - -memory_config = RedisLongTermMemoryServiceConfig( - api_base_url="http://localhost:8088", - default_namespace="my_app", - extraction_strategy="discrete", - recency_boost=True, - semantic_weight=0.7, - recency_weight=0.3, -) -memory_service = RedisLongTermMemoryService(config=memory_config) -``` - - -The `extraction_strategy` parameter controls how the server breaks down conversations into storable facts. The `"discrete"` strategy extracts individual facts as separate memories, which makes them independently searchable. Other options include `"summary"` (a narrative summary of the conversation) and `"preferences"` (focused on user preferences). - -Recency boosting deserves a closer look. When searching memories, raw semantic similarity alone often isn't enough. A user might have said "I love Italian food" three years ago, and "Actually, I've been getting into Japanese cuisine lately" last week. Both are semantically relevant to a query about food preferences, but the recent one matters more. - -The recency boosting system addresses this by combining two scores. The `semantic_weight` controls how much the vector similarity matters, while `recency_weight` controls how much recency matters. Within the recency score itself, `freshness_weight` favors memories that were recently accessed, and `novelty_weight` favors memories that were recently created. The `half_life_last_access_days` and `half_life_created_days` parameters control how quickly each signal decays. A half-life of 7 days means that a memory's freshness score drops to 50% after a week of not being accessed. - -This is a thoughtful design. It avoids the common failure mode of semantic search systems that return stale information with high confidence. - -### Wiring Both Tiers Together - -With both services configured, you connect them to an ADK `Runner`. - -```python -from google.adk import Agent -from google.adk.runners import Runner - -agent = Agent( - name="memory_agent", - model="gemini-2.5-flash", - instruction="You are a helpful assistant with long-term memory.", -) - -runner = Runner( - agent=agent, - app_name="my_app", - session_service=session_service, - memory_service=memory_service, -) -``` - -The flow is now automatic. Messages are stored in working memory as the conversation happens. When the agent finishes a turn, a callback can trigger `add_session_to_memory()`, which pushes the conversation to the Agent Memory Server for background extraction. On subsequent sessions, the memory service's `search_memory` method retrieves relevant facts from across all past conversations. - - -## Search Tools for RAG - -Memory services handle what the agent remembers from past conversations. But what about external knowledge? Product catalogs, documentation, knowledge bases? This is the domain of retrieval-augmented generation (RAG), and `adk-redis` provides four search tools that plug directly into ADK's tool system. - -Each tool wraps a RedisVL query type and exposes itself as a function the LLM can call. The LLM sees a function declaration with a `query` parameter, decides when to use it, and gets back structured results. - -### RedisVectorSearchTool - -The most straightforward option. It embeds the query using a vectorizer, performs K-nearest-neighbor search against a Redis index, and returns the top results. - -```python -from redisvl.index import SearchIndex -from redisvl.utils.vectorize import HFTextVectorizer -from adk_redis.tools import RedisVectorSearchTool, RedisVectorQueryConfig - -vectorizer = HFTextVectorizer(model="redis/langcache-embed-v2") -index = SearchIndex.from_existing("products", redis_url="redis://localhost:6379") - -search_tool = RedisVectorSearchTool( - index=index, - vectorizer=vectorizer, - config=RedisVectorQueryConfig( - vector_field_name="embedding", - return_fields=["name", "description", "price"], - num_results=5, - ), - name="search_product_catalog", - description="Find products by semantic similarity to a description.", -) -``` - -The `name` and `description` parameters matter more than they might seem. These are what the LLM reads to decide whether and when to call the tool. A vague description like "search documents" will lead to the LLM calling it at the wrong times. A specific one like "Find products by semantic similarity to a description" gives the LLM the context it needs. - -### RedisHybridSearchTool - -Hybrid search combines vector similarity with BM25 keyword matching. This is valuable when queries contain specific terms (product IDs, technical acronyms, exact names) that semantic search alone might miss. - -The tool auto-detects whether your Redis server and RedisVL version support native hybrid search (Redis 8.4+ with RedisVL 0.13+). If they do, it uses the server-side `FT.HYBRID` command. If not, it falls back to a client-side aggregation approach. This version detection happens at initialization, so you don't need to think about it. - -```python -from adk_redis.tools import RedisHybridSearchTool, RedisHybridQueryConfig - -hybrid_tool = RedisHybridSearchTool( - index=index, - vectorizer=vectorizer, - config=RedisHybridQueryConfig( - text_field_name="content", - combination_method="LINEAR", - linear_alpha=0.7, - ), - name="search_legal_documents", - description="Search legal documents using both semantic and keyword matching.", -) -``` - -### RedisTextSearchTool and RedisRangeSearchTool - -`RedisTextSearchTool` performs pure BM25 keyword search. No embeddings, no vectorizer needed. It is the right choice when the query is about exact terms, error messages, or API names. - -`RedisRangeSearchTool` is a less common but useful variant. Instead of returning the top-K results, it returns all documents within a distance threshold. This is useful for exhaustive retrieval, such as "find everything related to authentication in our documentation," where you want comprehensive coverage rather than a ranked list. - -Here is a concrete example from the `redis_search_tools` example in the repo, which wires all three search modalities into a single agent. - -```python -from adk_redis.tools import ( - RedisVectorSearchTool, RedisVectorQueryConfig, - RedisTextSearchTool, RedisTextQueryConfig, - RedisRangeSearchTool, RedisRangeQueryConfig, -) - -tools = [ - RedisVectorSearchTool( - name="semantic_search", - description="Semantic similarity search for conceptual queries.", - index=index, vectorizer=vectorizer, - config=RedisVectorQueryConfig(num_results=5), - return_fields=["title", "content", "category"], - ), - RedisTextSearchTool( - name="keyword_search", - description="Keyword search for exact terms and phrases.", - index=index, - config=RedisTextQueryConfig( - text_field_name="content", text_scorer="BM25STD" - ), - return_fields=["title", "content", "category"], - ), - RedisRangeSearchTool( - name="range_search", - description="Returns ALL documents within a semantic distance threshold.", - index=index, vectorizer=vectorizer, - config=RedisRangeQueryConfig(distance_threshold=0.5), - return_fields=["title", "content", "category"], - ), -] - -agent = Agent( - model="gemini-2.5-flash", - name="search_agent", - instruction=( - "You have three search tools. Use semantic_search for conceptual " - "queries, keyword_search for exact terms, range_search for exhaustive " - "retrieval." - ), - tools=tools, -) -``` - -The instruction prompt is doing real work here. It teaches the LLM when to use each tool and what to expect from each. This kind of prompt engineering is not optional. Without it, the LLM will default to calling whichever tool appears first or whichever has the most generic description. - -## Semantic Caching - -LLM API calls are slow and expensive. If your agent handles support queries, a significant fraction of incoming questions will be semantically similar. "How do I reset my password?" and "I need to change my password" should produce the same response, and there is no reason to pay for two LLM calls. - -`adk-redis` provides semantic caching at two levels, LLM response caching and tool result caching, both backed by Redis. - -### LLM Response Cache - -The LLM cache intercepts calls to the language model through ADK's callback system. Before each model call, it checks whether a semantically similar prompt already exists in Redis. If it does, it returns the cached response immediately, skipping the LLM entirely. If it doesn't, it lets the call proceed and stores the response for future lookups. - -```python -from redisvl.utils.vectorize import HFTextVectorizer -from adk_redis.cache import ( - RedisVLCacheProvider, RedisVLCacheProviderConfig, - LLMResponseCache, LLMResponseCacheConfig, - create_llm_cache_callbacks, -) - -vectorizer = HFTextVectorizer(model="redis/langcache-embed-v1") - -provider = RedisVLCacheProvider( - config=RedisVLCacheProviderConfig( - redis_url="redis://localhost:6379", - name="my_llm_cache", - ttl=3600, - distance_threshold=0.1, - ), - vectorizer=vectorizer, -) - -llm_cache = LLMResponseCache( - provider=provider, - config=LLMResponseCacheConfig( - first_message_only=True, - include_app_name=True, - include_user_id=True, - ), -) - -before_cb, after_cb = create_llm_cache_callbacks(llm_cache) - -agent = Agent( - name="cached_agent", - model="gemini-2.0-flash", - instruction="You are a helpful assistant.", - before_model_callback=before_cb, - after_model_callback=after_cb, -) -``` - -A few design decisions are worth noting here. - -The `distance_threshold` parameter (set to 0.1 in this example) controls how similar two prompts need to be for a cache hit. A value of 0.0 means exact match only. A value of 0.1 allows small variations in phrasing. Going much higher risks returning cached responses for genuinely different questions. Tuning this threshold is application-specific and worth experimenting with. - -The `first_message_only` option is a practical default. In a multi-turn conversation, later messages depend heavily on prior context, making semantic cache hits unreliable. Caching only the first message (which is typically a standalone question) avoids returning contextually wrong responses. - -The cache is also smart about what it does *not* cache. Function call responses (where the LLM is invoking a tool) are skipped, as are error responses. This prevents caching intermediate steps that shouldn't be reused. - -### Managed Caching with LangCache - -If you'd rather not manage your own Redis instance and embedding model for caching, `adk-redis` also supports LangCache, a managed semantic caching service from Redis. With LangCache, embeddings are generated server-side, so you don't need a local vectorizer at all. - -```python -from adk_redis.cache import LangCacheProvider, LangCacheProviderConfig - -provider = LangCacheProvider( - config=LangCacheProviderConfig( - cache_id="your-cache-id", - api_key="your-api-key", - ttl=3600, - ) -) -``` - -The same `LLMResponseCache` and `ToolCache` classes work with either provider. You just swap the backend. - -### Tool Result Cache - -The tool cache follows the same pattern but for tool executions rather than LLM calls. If your agent calls an external API with the same arguments repeatedly, the tool cache can short-circuit the call and return the cached result. - -```python -from adk_redis.cache import ToolCache, ToolCacheConfig, create_tool_cache_callbacks - -tool_cache = ToolCache( - provider=provider, - config=ToolCacheConfig( - tool_names={"web_search", "get_weather"}, - ), -) - -before_tool_cb, after_tool_cb = create_tool_cache_callbacks(tool_cache) -``` - -The `tool_names` set lets you specify exactly which tools should be cached. This is important because not all tools are idempotent. You probably want to cache `get_weather` (same city, same hour, same result) but not `send_email` (same arguments, but each call should actually execute). - -## Three Ways to Integrate Memory - -One of the more interesting design decisions in `adk-redis` is that it offers three distinct approaches for connecting agents to memory. Each approach has different tradeoffs around control, complexity, and standardization. - -### Approach 1. ADK Services (Framework-Managed) - -This is what we covered in the two-tier memory section. You configure `RedisWorkingMemorySessionService` and `RedisLongTermMemoryService`, pass them to the `Runner`, and the framework handles everything automatically. Memory extraction happens in the background. Search happens before each agent turn. The agent code itself never directly interacts with memory. - -This approach is the simplest to implement and the hardest to customize. The agent has no explicit control over *what* gets stored or *when* it searches. It is best for applications where you want memory to be invisible infrastructure. - -### Approach 2. REST Tools (LLM-Controlled) - -Instead of (or in addition to) framework-managed services, you can give the agent explicit memory tools. These are ADK tools that the LLM calls like any other function. - -```python -from adk_redis.tools.memory import ( - SearchMemoryTool, CreateMemoryTool, - UpdateMemoryTool, DeleteMemoryTool, - MemoryToolConfig, -) - -memory_config = MemoryToolConfig( - api_base_url="http://localhost:8088", - default_namespace="my_app", - recency_boost=True, -) - -tools = [ - SearchMemoryTool(config=memory_config), - CreateMemoryTool(config=memory_config), - UpdateMemoryTool(config=memory_config), - DeleteMemoryTool(config=memory_config), -] -``` - -With this approach, the LLM decides when to search memory, what to store, and what to update. The agent prompt needs to instruct the LLM on memory management strategy. This requires more prompt engineering, but it gives the agent genuine autonomy over its own memory. - -The travel agent example in the repo uses a hybrid of both approaches. Framework services handle session persistence and automatic background extraction. Memory tools give the LLM explicit CRUD control over long-term memories. This is arguably the most powerful configuration, because the agent gets both automatic memory management and the ability to deliberately store or retrieve specific facts. - -### Approach 3. MCP Tools (Model Context Protocol) - -MCP is a standardized protocol for connecting agents to tools via Server-Sent Events (SSE). Instead of REST-based tool implementations, you point the agent at the Agent Memory Server's MCP endpoint and let ADK's `McpToolset` handle tool discovery automatically. - -```python -from adk_redis.tools.mcp_memory import create_memory_mcp_toolset - -memory_tools = create_memory_mcp_toolset( - server_url="http://localhost:9000", - tool_filter=["search_long_term_memory", "create_long_term_memories"], -) - -agent = Agent( - model="gemini-2.5-flash", - name="fitness_coach", - tools=[memory_tools], -) -``` - -The `tool_filter` parameter controls which MCP tools are exposed to the LLM. The Agent Memory Server exposes seven tools through MCP, including `search_long_term_memory`, `create_long_term_memories`, `get_long_term_memory`, `edit_long_term_memory`, `delete_long_term_memories`, `memory_prompt`, and `set_working_memory`. - -The fitness coach example in the repo demonstrates this approach. It connects to memory via MCP and stores both semantic memories (user profile, injuries, equipment) and episodic memories (workouts with timestamps, milestones). The distinction between semantic and episodic memory types is particularly useful. Semantic memories represent timeless facts ("user has a knee injury"), while episodic memories represent events ("user completed 3x12 rows on March 9th"). - -MCP is the most standardized approach and makes it easy to swap memory backends without changing agent code. The tradeoff is that it requires running the Agent Memory Server with MCP support enabled on a separate port. - -## Walking Through the Travel Agent - -To make all of this concrete, let's trace through the `travel_agent_memory_hybrid` example, which is the most complete example in the repo. It combines framework-managed services, LLM-controlled memory tools, web search, itinerary planning, and calendar export into a single agent. - -### The Entrypoint - -The `main.py` file sets up the infrastructure. It registers custom service factories with ADK's service registry, creates both the session service and memory service, and launches a FastAPI app with the ADK web runner. - -```python -from adk_redis.memory import RedisLongTermMemoryService, RedisLongTermMemoryServiceConfig -from adk_redis.sessions import RedisWorkingMemorySessionService, RedisWorkingMemorySessionServiceConfig - -# Register factories so ADK can instantiate them from URIs -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) - -# Build URIs and create the FastAPI app -app = get_fast_api_app( - agents_dir=".", - session_service_uri="redis-working-memory://localhost:8088", - memory_service_uri="redis-long-term-memory://localhost:8088", - web=True, -) -``` - -The URI-based factory pattern is worth noting. ADK's service registry lets you register custom service implementations behind URI schemes. This means you can switch between in-memory and Redis-backed services by changing a URI string, without modifying any agent code. - -### The Agent - -The agent itself is defined in `agent.py`. It assembles a rich set of tools spanning memory, search, and planning. - -```python -from adk_redis.tools.memory import ( - SearchMemoryTool, CreateMemoryTool, - UpdateMemoryTool, DeleteMemoryTool, - MemoryToolConfig, -) -from google.adk.tools import preload_memory, load_memory - -memory_config = MemoryToolConfig( - api_base_url="http://localhost:8088", - default_namespace="travel_agent_memory_hybrid", - recency_boost=True, - search_top_k=10, -) - -tools = [ - SearchMemoryTool(config=memory_config), - CreateMemoryTool(config=memory_config), - UpdateMemoryTool(config=memory_config), - DeleteMemoryTool(config=memory_config), - preload_memory, - load_memory, - CalendarExportTool(), - ItineraryPlannerTool(), -] -``` - -Notice the layered memory strategy. `preload_memory` and `load_memory` are ADK's built-in tools that hook into the `RedisLongTermMemoryService` we configured in `main.py`. These provide automatic, framework-controlled memory retrieval. The `SearchMemoryTool`, `CreateMemoryTool`, and friends give the LLM explicit control on top of that. - -The agent also has an `after_agent_callback` that calls `add_session_to_memory()` after each turn. This is what triggers background extraction of facts and preferences into long-term memory. - -```python -async def after_agent(callback_context: CallbackContext): - await callback_context.add_session_to_memory() - -root_agent = Agent( - model="gemini-2.5-flash", - name="travel_agent", - tools=tools, - after_agent_callback=after_agent, - instruction="...", # Detailed prompt with memory management strategy -) -``` - -### What Happens at Runtime - -When a user starts a conversation, the following sequence plays out. - -1. ADK creates (or retrieves) a session via `RedisWorkingMemorySessionService`. The session is stored in the Agent Memory Server. -2. The agent's `preload_memory` tool automatically searches long-term memory for context relevant to the current conversation. -3. The user sends a message. The message is appended to working memory via the incremental append API. -4. The LLM generates a response. If it needs travel information, it can call web search tools. If it wants to check the user's preferences, it calls `SearchMemoryTool`. If the user shares a new preference, the LLM calls `CreateMemoryTool`. -5. The response is appended to working memory. -6. The `after_agent_callback` fires, sending the conversation to the Agent Memory Server for background extraction. The server pulls out facts like "user prefers direct flights" or "user wants to visit Japan in spring" and stores them as searchable long-term memories. -7. If the conversation grows long, the working memory service automatically summarizes older turns to stay within the context window. - -All of this happens with a `pip install adk-redis[memory]`, a running Redis instance, and a running Agent Memory Server. The agent's Python code is clean, focused on domain logic rather than infrastructure plumbing. - -## Getting Started - -To run any of the examples, you need two things running. - -**Redis 8.4** provides the storage backend for everything. Vector indices, session data, cache entries. - -```bash -docker run -d --name redis -p 6379:6379 redis:8.4-alpine -``` - -**Redis Agent Memory Server** handles memory extraction, summarization, and the working memory API. It sits between your agent and Redis, adding the intelligence layer. - -```bash -docker run -d --name agent-memory-server -p 8088:8088 \ - -e REDIS_URL=redis://host.docker.internal:6379 \ - -e GEMINI_API_KEY=your-key \ - -e GENERATION_MODEL=gemini/gemini-2.0-flash \ - -e EMBEDDING_MODEL=gemini/text-embedding-004 \ - redislabs/agent-memory-server:0.13.2 \ - agent-memory api --host 0.0.0.0 --port 8088 --task-backend=asyncio -``` - -The Agent Memory Server uses LiteLLM under the hood, which means it supports 100+ LLM providers. You can swap in OpenAI, Anthropic, AWS Bedrock, or even local models via Ollama. - -Then install the package and run an example. - -```bash -pip install adk-redis[all] -cd examples/simple_redis_memory -python main.py -``` - -## Conclusion - -`adk-redis` is a focused library that solves a specific problem well. It takes the interfaces that ADK defines, `BaseMemoryService`, `BaseSessionService`, the tool system, the callback system, and provides Redis-backed implementations that are production-grade rather than toy-grade. - -The key ideas worth taking away from this are the following. - -The **two-tier memory architecture** (working memory for sessions, long-term memory for persistent facts) is a pattern that scales well. It mirrors how real applications need to manage state, keeping the current context fast and small while maintaining a durable knowledge base. - -The **three integration approaches** (framework services, REST tools, MCP tools) give you a spectrum from fully automatic to fully LLM-controlled memory management. The hybrid approach, combining framework services with LLM-controlled tools, is particularly effective. - -**Semantic caching** is a straightforward way to reduce costs and latency, and `adk-redis` makes it easy to enable without changing your agent's core logic. - -The **search tools** provide a clean abstraction over RedisVL's query types, making it simple to add RAG capabilities to any ADK agent. - -All of this runs on Redis, a system that most teams already know how to operate, monitor, and scale. - -If you want to dive deeper, the [GitHub repository](https://github.com/redis-developer/adk-redis) has seven complete examples covering every feature described here. The [Redis Agent Memory Server documentation](https://github.com/redis/agent-memory-server) covers the memory backend in detail, and the [RedisVL documentation](https://docs.redisvl.com) covers the vector search and caching capabilities that power the tools and cache providers. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 20bce38..6169e4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "adk-redis" -version = "0.0.4" +version = "0.0.5" description = "Redis integrations for Google's Agent Development Kit (ADK)" readme = "README.md" license = "Apache-2.0" From 718ddd7eb2040fb02deee0bbba67f27950e8148e Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Tue, 19 May 2026 20:18:02 -0400 Subject: [PATCH 2/8] docs: symmetric search card + mirror MCP example to redis_search_tools README: - Restructure the Search Tools section. Open with a two-row decision matrix ("In-process" vs "MCP toolset"), then keep the in-process table, then describe the MCP toolset path in the same section. Calls out that range and SQL have no MCP equivalent today. - Update the top-of-README 3-column feature card so the Search Tools sub-card mentions both in-process and `rvl mcp` paths, and the MCP Toolsets sub-card lists AMS memory + RedisVL search side by side. examples/redisvl_mcp_search/: - Rewrite as the MCP-path mirror of `redis_search_tools/`. Same knowledge-base corpus, same kinds of prompts, embedded via `redis/langcache-embed-v2`. - schema.yaml: add a 768-dim vector field with HNSW + cosine. - load_data.py: embed each doc and load with stable keys so re-running is idempotent. - mcp_config.yaml: switch from fulltext to `search.type: hybrid` with a vectorizer block and LINEAR fusion (50/50 text/vector). Disable nltk stopwords so the server runs without that optional dep. - agent.py: refresh instruction to describe hybrid behavior. - README.md: side-by-side comparison table vs `redis_search_tools/`, and a "see also" footer pointing at the in-process and SQL examples. Verification: - `rvl mcp` server started against the new corpus and responded correctly to three prompts (`What is Redis?`, `Tell me about HNSW`, `FT.HYBRID command`); top hits were the expected articles ranked by hybrid_score. End-to-end ADK Runner smoke deferred (Gemini free-tier daily quota was exhausted during testing); the no-LLM probe via `McpToolset.run_async` proves the agent->MCP->Redis pipeline is wired correctly. - make check clean. 124 tests pass. --- README.md | 38 ++-- examples/redisvl_mcp_search/README.md | 168 +++++++++------ examples/redisvl_mcp_search/load_data.py | 191 +++++++++++++----- examples/redisvl_mcp_search/mcp_config.yaml | 23 ++- .../redisvl_mcp_search_agent/agent.py | 43 ++-- examples/redisvl_mcp_search/schema.yaml | 33 ++- scripts/smoke_adk_mcp_runner.py | 6 +- 7 files changed, 352 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index 85979eb..33d72fd 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ | Cross-session knowledge retrieval | search, create, update, delete | Configurable distance threshold | | Recency-boosted search | Namespace & user isolation | TTL-based expiration | | **Session Service**
*Working memory via Agent Memory Server* | **Search Tools**
*RAG via RedisVL* | **Tool Cache**
*Avoid redundant calls* | -| Context window management | Vector, hybrid, text, range search | Cache tool execution results | -| Auto-summarization | Multiple vectorizers supported | Reduce API calls | -| Background memory promotion | **MCP Tools**
*Model Context Protocol* | **LangCache**
*Managed semantic cache* | -| | SSE-based tool discovery | Cloud-hosted, no local vectorizer | +| Context window management | Vector, hybrid, range, text, SQL | Cache tool execution results | +| Auto-summarization | In-process or via `rvl mcp` server | Reduce API calls | +| Background memory promotion | **MCP Toolsets**
*Model Context Protocol* | **LangCache**
*Managed semantic cache* | +| | AMS memory (SSE) + RedisVL search (stdio/sse/streamable-http) | Cloud-hosted, no local vectorizer | @@ -374,21 +374,35 @@ Implements ADK's `BaseSessionService` interface for conversation management: ### Search Tools -Five specialized search tools for different RAG use cases: +Two parallel paths for RAG over a Redis index. Pick by deployment shape, not by feature. + +| Path | Use when | +|---|---| +| **In-process** (Python `BaseTool` subclasses) | Single ADK process, fast onboarding, Python-side `FilterExpression` composition, per-tool customization. Five tools: vector, hybrid, range, text, SQL. | +| **MCP toolset** (`create_redisvl_mcp_toolset` against `rvl mcp`) | One Redis index served to multiple agents (Python, JS, Claude Desktop). Server-side `--read-only` / bearer auth. Schema-aware tool descriptions. Two tools: `search-records`, `upsert-records`. | + +#### In-process tools | Tool | Best For | Key Features | |------|----------|--------------| | **`RedisVectorSearchTool`** | Semantic similarity | Vector embeddings, KNN search, metadata filtering | -| **`RedisHybridSearchTool`** | Combined search | Vector + text search, Redis 8.4+ native support, aggregation fallback | -| **`RedisRangeSearchTool`** | Threshold-based retrieval | Distance-based filtering, similarity radius | -| **`RedisTextSearchTool`** | Keyword search | Full-text search, no embeddings required | -| **`RedisSQLSearchTool`** | SQL-style filters | `SELECT ... WHERE` against a bound index, parameterized queries (requires `adk-redis[sql]`) | +| **`RedisHybridSearchTool`** | Combined search | Vector + text search, Redis 8.4+ native `FT.HYBRID`, aggregation fallback | +| **`RedisRangeSearchTool`** | Threshold-based retrieval | Distance-based filtering, similarity radius (no MCP equivalent) | +| **`RedisTextSearchTool`** | Keyword search | Full-text BM25 search, no embeddings required | +| **`RedisSQLSearchTool`** | SQL-style filters | `SELECT ... WHERE` against a bound index, `:param` placeholders (requires `adk-redis[sql]`, no MCP equivalent) | + +All five support multiple vectorizers (OpenAI, HuggingFace, Cohere, Mistral, Voyage AI, etc.) and arbitrary `FilterExpression` objects from `redisvl.query.filter`. + +#### MCP toolset + +`create_redisvl_mcp_toolset(...)` returns an ADK `McpToolset` wired to a running [RedisVL MCP server](https://docs.redisvl.com) (`rvl mcp`). The server is configured per index via YAML and exposes: -> All search tools support multiple vectorizers (OpenAI, HuggingFace, Cohere, Mistral, Voyage AI, etc.) and advanced filtering. +- `search-records`: `vector`, `fulltext`, or `hybrid` mode (chosen at server start). Tool description includes filter and return-field hints derived from the bound index schema. +- `upsert-records`: write path (suppress with `--read-only`). -### RedisVL MCP toolset +Supports `stdio`, `sse`, and `streamable-http` transports; bearer auth on HTTP. Requires `adk-redis[mcp-search]` and a `rvl mcp` server. See [`examples/redisvl_mcp_search/`](examples/redisvl_mcp_search/) for a runnable demo and [the search-tools guide](docs/user_guide/how_to_guides/search_tools.md) for the full decision matrix. -`create_redisvl_mcp_toolset(...)` returns an ADK `McpToolset` wired to RedisVL's own MCP server (`rvl mcp`). The server exposes schema-aware `search-records` and `upsert-records` tools whose descriptions include filter and return-field hints derived from the bound index. Use it when you want one Redis index served to multiple agents (Python, JS, Claude Desktop) over `stdio`, `sse`, or `streamable-http`. Requires `adk-redis[mcp-search]` and a Redis-side `rvl mcp` server (or YAML config). See [the search-tools guide](docs/user_guide/how_to_guides/search_tools.md) for the decision matrix vs the in-process tools above. +> **Coverage note:** range and SQL search have **no MCP equivalent today**; if you need either, you must use the in-process tool. ### Semantic Caching diff --git a/examples/redisvl_mcp_search/README.md b/examples/redisvl_mcp_search/README.md index 6bcbf04..425a782 100644 --- a/examples/redisvl_mcp_search/README.md +++ b/examples/redisvl_mcp_search/README.md @@ -1,44 +1,38 @@ # RedisVL MCP Search Agent -This sample shows an ADK agent that talks to a separately-running -**RedisVL MCP server** (`rvl mcp`) via the new -`create_redisvl_mcp_toolset(...)` helper. The MCP server is configured -to expose BM25 fulltext search over a small corpus of Redis articles. +The **MCP-path mirror** of [`redis_search_tools/`](../redis_search_tools/). +Same knowledge-base corpus, same kinds of prompts, but search is served +by a separately-running `rvl mcp` server and the agent calls it via +`create_redisvl_mcp_toolset(...)` over MCP. + +Use this example to compare the two deployment shapes side by side: + +| | `redis_search_tools/` | `redisvl_mcp_search/` (this) | +|---|---|---| +| Topology | One process: agent + index in-process | Two processes: agent connects to `rvl mcp` over MCP | +| Tool count | 3 (semantic / keyword / range) | 1 (`search-records`, configured for hybrid) | +| Search modes covered | vector, BM25, range | vector + BM25 fused via FT.HYBRID | +| Where the vectorizer runs | In the agent process | In the `rvl mcp` server process | +| Filter shape | Python `FilterExpression` | JSON filter object parsed server-side | +| Use when | Single agent, fast onboarding, complex filters | Multi-agent / polyglot, server-side ops gates | ## What this sample shows -- Configuring `rvl mcp` for a Redis search index with a YAML file. +- Configuring `rvl mcp` for hybrid search via a YAML config. - Connecting ADK to that server with `create_redisvl_mcp_toolset(...)` over the `streamable-http` transport. - Using a `tool_filter` to expose only `search-records` (no upserts). - Reading the schema-aware tool description that RedisVL produces. -## Architecture - -``` - +-------------------+ - "search- | rvl mcp server | - records" | (streamable-http | - ^^^^^^^^>| on :8765) |----> Redis 8.4+ (RediSearch) - MCP +-------------------+ - protocol ^ - | - +-------------------+ - | ADK agent | - | (`adk web`) | - | create_redisvl_ | - | mcp_toolset(...) | - +-------------------+ -``` - ## Prerequisites -1. **Redis 8.4** running locally or in Redis Cloud. The repo root has - `./scripts/start-redis.sh` for a one-shot start. +1. **Redis 8.4** running locally (or Redis Cloud with the RediSearch + module enabled). Native `FT.HYBRID` requires 8.4+. 2. **A Gemini API key**. Get one at [aistudio.google.com](https://aistudio.google.com/app/apikey). -3. **The `mcp-search` extra** so the helper and `rvl mcp` CLI are - installed. +3. **The `mcp-search` extra** for the helper and the `rvl mcp` CLI; the + loader and the MCP server also need `sentence-transformers` (pulled + in by `redisvl`'s HuggingFace vectorizer dependency). ## Setup @@ -50,8 +44,8 @@ From the repository root: uv pip install 'adk-redis[mcp-search,examples]' ``` -The `mcp-search` extra pulls in `redisvl[mcp]>=0.18.2`, which provides -the `rvl mcp` CLI and the FastMCP server. +This pulls in `redisvl[mcp]>=0.18.2` plus the `rvl mcp` CLI and the +FastMCP server. ### 2. Start Redis 8.4 @@ -62,19 +56,23 @@ docker exec redis redis-cli ping # -> PONG ### 3. Set your Gemini API key -Copy `.env.example` to `.env` and fill in `GOOGLE_API_KEY`. Optionally -set `REDISVL_MCP_URL` if you plan to run the MCP server somewhere other -than `http://127.0.0.1:8765/mcp`. +Copy `.env.example` to `.env` and fill in `GOOGLE_API_KEY`. Optional: + +- `REDIS_URL` to point the loader at a non-default Redis. +- `REDISVL_MCP_URL` if you run the MCP server somewhere other than + `http://127.0.0.1:8765/mcp`. +- `REDISVL_MCP_AUTH_TOKEN` to attach a bearer token to MCP requests. -### 4. Load the article index +### 4. Load the knowledge base ```bash cd examples/redisvl_mcp_search python load_data.py ``` -This creates the `adk_mcp_articles` index and loads six short articles -about Redis search, MCP, semantic caching, and agent memory. +The loader creates the `adk_mcp_knowledge_base` index, embeds the +documents with `redis/langcache-embed-v2` (768 dims), and writes them +to Redis with stable keys so re-running is idempotent. ### 5. Start the RedisVL MCP server @@ -87,9 +85,9 @@ rvl mcp --config mcp_config.yaml \ --host 127.0.0.1 --port 8765 ``` -The server inspects the configured index, registers its `search-records` -tool with schema-aware filter hints, and starts listening on -`http://127.0.0.1:8765/mcp`. +The server inspects the configured index, registers a single hybrid +`search-records` tool with schema-aware filter and return-field hints, +and listens on `http://127.0.0.1:8765/mcp`. ### 6. Run the agent @@ -100,35 +98,91 @@ adk web redisvl_mcp_search_agent ADK web opens at `http://127.0.0.1:8000`. Pick the `redisvl_mcp_search_agent` app from the dropdown. -## Try these prompts +## Example queries -- "Find articles about FT.HYBRID." -- "What does the MCP server expose?" -- "Explain semantic caching." -- "Tell me about HNSW runtime parameters." +Mirror the prompts from `redis_search_tools/` so you can see the MCP path +return analogous results: -The agent decides on a keyword phrase, calls `search-records` over MCP, -and summarizes the matches with title and URL citations. +- **Semantic-leaning:** "What is Redis?", "How does RAG work?", "What is + a vector database?" +- **Keyword-leaning:** "Tell me about HNSW.", "Explain BM25 scoring.", + "FT.HYBRID command." +- **Mixed:** "What are RAG best practices?", "How do I build an + intelligent assistant?" + +Because the server is configured for hybrid mode, a single query +exercises both the BM25 path (term matches in `content`) and the vector +path (semantic similarity to the query embedding), then fuses with +`LINEAR` weighting (50% text, 50% vector by default). + +## Files + +| File | Purpose | +|------|---------| +| `schema.yaml` | RedisVL index schema (text + tag + vector fields). | +| `load_data.py` | Embeds and loads the knowledge-base corpus. | +| `mcp_config.yaml` | `rvl mcp` server configuration: hybrid search + vectorizer + runtime field names. | +| `redisvl_mcp_search_agent/agent.py` | The ADK agent. | +| `.env.example` | Template for `GOOGLE_API_KEY` and optional overrides. | ## How it works -`create_redisvl_mcp_toolset(...)` returns an ADK `McpToolset` with the -right connection-params type for the transport you choose: +1. **Agent constructs an MCP toolset.** `create_redisvl_mcp_toolset(...)` + returns an `McpToolset` wired to the running `rvl mcp` server. + `tool_filter=["search-records"]` hides `upsert-records` so the agent + cannot write. +2. **Agent emits a query.** The LLM calls `search-records({"query": + "...", "limit": 5})`. ADK relays the call to the MCP server. +3. **MCP server runs hybrid search.** The server embeds the query with + `redis/langcache-embed-v2`, builds a `HybridQuery` against the + configured index, runs `FT.HYBRID` on Redis, normalizes scores, and + returns structured results with `{title, content, url, ...}` per + match. +4. **Agent summarizes.** The LLM cites each match's title and url. + +## Customization + +### Switch fusion method + +Edit `mcp_config.yaml`: + +```yaml +search: + type: hybrid + params: + combination_method: RRF + rrf_window: 20 + rrf_constant: 60 +``` -- `transport="stdio"` (passes a `config_path`): spawns - `rvl mcp --config --read-only` over stdio. -- `transport="streamable-http"` (default, passes a `url`): connects to - a long-running server. Bearer auth is added to headers when - `auth_token` is set. -- `transport="sse"` (passes a `url`): same as streamable-http but over - the SSE transport. +### Add a bearer token -The agent in this sample uses the streamable-http path so the MCP server -can stay up between agent invocations. Switch to stdio if you prefer a -single process; the helper handles it. +Run the server behind a proxy that injects auth, then: + +```bash +export REDISVL_MCP_AUTH_TOKEN=... +``` + +The agent's `auth_token` argument flows through as +`Authorization: Bearer ` on every MCP request. + +### Connect to Redis Cloud + +Set `REDIS_URL` before running both the loader and the MCP server. The +config YAML uses `${REDIS_URL:-redis://localhost:6379}` so the override +flows through automatically. ## Cleanup ```bash docker stop redis && docker rm redis ``` + +## See also + +- [`redis_search_tools/`](../redis_search_tools/) for the in-process + Python version of the same demo. +- [`redis_sql_search/`](../redis_sql_search/) for SQL-style filters + (in-process only; no MCP equivalent today). +- [Search tools how-to](../../docs/user_guide/how_to_guides/search_tools.md) + for the full decision matrix. diff --git a/examples/redisvl_mcp_search/load_data.py b/examples/redisvl_mcp_search/load_data.py index b41f950..60334f8 100644 --- a/examples/redisvl_mcp_search/load_data.py +++ b/examples/redisvl_mcp_search/load_data.py @@ -12,91 +12,168 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Load sample articles for the redisvl_mcp_search demo. +"""Load the same knowledge base as redis_search_tools, but for MCP search. -The rvl mcp server is configured to expose BM25 fulltext search over -`content`, so the dataset is short prose suited to keyword matches. +The corpus mirrors `examples/redis_search_tools/load_data.py` so the +in-process and MCP demos answer the same questions on the same data. +Documents are embedded with `redis/langcache-embed-v2` (768 dims) so the +configured `rvl mcp` server can run vector or hybrid search against +them. """ import os from pathlib import Path from redisvl.index import SearchIndex +from redisvl.utils.vectorize import HFTextVectorizer -SAMPLE_ARTICLES = [ +SAMPLE_DOCS = [ + # === SEMANTIC SEARCH DEMOS === { - "title": "Vector Similarity Search in Redis", + "title": "Introduction to Redis", "content": ( - "Redis supports approximate nearest neighbor search via FLAT and " - "HNSW indexes. HNSW trades index size and build time for sub-linear " - "query latency at high recall. Each algorithm has runtime parameters " - "such as EF for HNSW that tune the accuracy-latency tradeoff." + "Redis is a lightning-fast in-memory data store. It excels at" + " caching, session management, and real-time analytics. Think of" + " it as a Swiss Army knife for data: versatile, quick, and" + " reliable." + ), + "url": "https://redis.io/docs/about/", + "category": "redis", + "doc_type": "reference", + "difficulty": "beginner", + }, + { + "title": "Understanding Vector Databases", + "content": ( + "Vector databases store numerical representations of data called" + " embeddings. These embeddings capture semantic meaning, enabling" + " similarity search. Applications include recommendation engines," + " image search, and chatbots." ), - "topic": "vectors", "url": "https://redis.io/docs/vectors/", + "category": "concepts", + "doc_type": "reference", + "difficulty": "intermediate", + }, + { + "title": "Building Intelligent Assistants", + "content": ( + "Modern AI assistants combine language models with external" + " knowledge. They can search databases, call APIs, and maintain" + " conversation context. The key is giving them the right tools" + " for each task." + ), + "url": "https://google.github.io/adk-docs/agents/", + "category": "adk", + "doc_type": "tutorial", + "difficulty": "intermediate", + }, + # === KEYWORD-FRIENDLY DEMOS === + { + "title": "HNSW Algorithm Deep Dive", + "content": ( + "HNSW (Hierarchical Navigable Small World) is the algorithm Redis" + " uses for approximate nearest neighbor search. It builds a" + " multi-layer graph where each layer has exponentially fewer" + " nodes. Search starts at the top layer and navigates down." + " Parameters: M (connections per node), EF (search width)." + ), + "url": "https://redis.io/docs/hnsw/", + "category": "redis", + "doc_type": "reference", + "difficulty": "advanced", + }, + { + "title": "BM25 Scoring Explained", + "content": ( + "BM25 (Best Matching 25) is a ranking function for full-text" + " search. It improves on TF-IDF by adding document length" + " normalization and term frequency saturation. Redis supports" + " BM25STD and BM25 scorers." + ), + "url": "https://redis.io/docs/bm25/", + "category": "redis", + "doc_type": "reference", + "difficulty": "advanced", }, { "title": "Hybrid Search with FT.HYBRID", "content": ( - "Hybrid search combines BM25 text scoring with vector similarity. " - "Redis 8.4 introduced the FT.HYBRID command for server-side fusion " - "using either LINEAR weighting or Reciprocal Rank Fusion (RRF). " - "Older Redis versions can fall back to client-side aggregation." + "Hybrid search combines BM25 text scoring with vector similarity." + " Redis 8.4 introduced the FT.HYBRID command for server-side" + " fusion using either LINEAR weighting or Reciprocal Rank Fusion" + " (RRF). Older Redis versions fall back to client-side" + " aggregation." ), - "topic": "search", "url": "https://redis.io/docs/hybrid/", + "category": "redis", + "doc_type": "reference", + "difficulty": "advanced", }, + # === RAG-FOCUSED === { - "title": "Semantic Caching for LLMs", + "title": "RAG Architecture Overview", "content": ( - "A semantic cache stores prompt-response pairs keyed by the prompt " - "embedding. On a cache lookup the new prompt is embedded and the " - "nearest stored entry is returned when its distance is below the " - "configured threshold. This skips the LLM call for repeated or " - "near-duplicate requests." + "Retrieval-Augmented Generation (RAG) enhances LLMs with external" + " knowledge. Step 1: embed the user query. Step 2: search the" + " vector database for relevant documents. Step 3: include" + " retrieved context in the LLM prompt. Step 4: generate a" + " grounded response." ), - "topic": "caching", - "url": "https://redis.io/langcache", + "url": "https://redis.io/solutions/rag/", + "category": "concepts", + "doc_type": "tutorial", + "difficulty": "intermediate", }, { - "title": "Long-Term Memory for Agents", + "title": "RAG Best Practices", "content": ( - "Agent memory layers working memory and long-term memory. Working " - "memory holds the active conversation; promoted facts move to " - "long-term memory where recency-boosted semantic search retrieves " - "them on demand. Background extraction keeps the layers in sync." + "Tips for effective RAG: chunk documents appropriately (512 to" + " 1024 tokens), use hybrid search for better recall, rerank" + " results before prompting, include metadata for filtering, and" + " monitor retrieval quality metrics." ), - "topic": "memory", - "url": "https://github.com/redis/agent-memory-server", + "url": "https://redis.io/solutions/rag/best-practices/", + "category": "concepts", + "doc_type": "tutorial", + "difficulty": "intermediate", }, + # === MCP-FOCUSED === { "title": "RedisVL MCP Server", "content": ( - "The RedisVL MCP server exposes a configured Redis index over the " - "Model Context Protocol. Search and upsert tools are wired with " - "schema-aware descriptions so agents see allowed filters and " - "return fields. The server supports stdio, SSE, and " - "streamable-http transports and ships a read-only flag." + "The RedisVL MCP server exposes a configured Redis index over the" + " Model Context Protocol. Search and upsert tools are wired with" + " schema-aware descriptions so agents see allowed filters and" + " return fields. The server supports stdio, SSE, and" + " streamable-http transports and ships a read-only flag." + ), + "url": ( + "https://docs.redisvl.com/en/stable/user_guide/how_to_guides/mcp.html" ), - "topic": "mcp", - "url": "https://docs.redisvl.com/en/stable/user_guide/how_to_guides/mcp.html", + "category": "redis", + "doc_type": "reference", + "difficulty": "intermediate", }, + # === FAQ STYLE === { - "title": "Index Schemas in RedisVL", + "title": "FAQ: Embedding Dimensions Mismatch", "content": ( - "An IndexSchema declares the fields stored in a Redis search index. " - "Fields include text, tag, numeric, geo, and vector types. The " - "schema drives how documents are loaded, how filters are parsed, " - "and which fields are projected by default." + "Q: Dimension mismatch error? A: Ensure query embeddings match" + " index dimensions. Common dimensions: OpenAI ada-002 (1536)," + " langcache-embed-v2 (768), sentence-transformers (384 to 768)." + " Check the schema dims field." ), - "topic": "schemas", - "url": "https://docs.redisvl.com/en/stable/user_guide/schemas.html", + "url": "https://redis.io/docs/faq/vectors/", + "category": "redis", + "doc_type": "faq", + "difficulty": "beginner", }, ] def load_data() -> None: - """Create the article index and load sample documents.""" + """Build the index and load documents with embeddings.""" schema_path = Path(__file__).parent / "schema.yaml" redis_url = os.getenv("REDIS_URL", "redis://localhost:6379") @@ -107,19 +184,35 @@ def load_data() -> None: print("Creating index (will overwrite if exists)...") index.create(overwrite=True, drop=True) - print(f"Loading {len(SAMPLE_ARTICLES)} articles...") - keys = [f"{index.prefix}:{i:04d}" for i in range(len(SAMPLE_ARTICLES))] - index.load(SAMPLE_ARTICLES, keys=keys) + print("Generating embeddings (redis/langcache-embed-v2)...") + vectorizer = HFTextVectorizer(model="redis/langcache-embed-v2") + + docs_with_embeddings = [] + for doc in SAMPLE_DOCS: + embedding = vectorizer.embed(doc["content"], as_buffer=True) + docs_with_embeddings.append({**doc, "embedding": embedding}) + print(f" [{doc['doc_type']:9}] {doc['title']}") + + print(f"\nLoading {len(SAMPLE_DOCS)} docs into Redis...") + # Stable keys so re-running the loader overwrites instead of duplicating. + keys = [f"{index.prefix}:{i:04d}" for i in range(len(SAMPLE_DOCS))] + index.load(docs_with_embeddings, keys=keys) print( """ -Loaded articles. Next: +Loaded knowledge base. Next: 1. Start the rvl mcp server in another terminal: rvl mcp --config mcp_config.yaml \\ --transport streamable-http --host 127.0.0.1 --port 8765 2. Run the agent: adk web redisvl_mcp_search_agent + +Try prompts like the in-process redis_search_tools demo: + - "What is Redis?" (semantic) + - "Tell me about HNSW" (keyword) + - "How does RAG work?" (semantic) + - "FT.HYBRID command" (mixed semantic + keyword) """ ) diff --git a/examples/redisvl_mcp_search/mcp_config.yaml b/examples/redisvl_mcp_search/mcp_config.yaml index 98a3324..f435cb1 100644 --- a/examples/redisvl_mcp_search/mcp_config.yaml +++ b/examples/redisvl_mcp_search/mcp_config.yaml @@ -3,23 +3,32 @@ # rvl mcp --config mcp_config.yaml --transport streamable-http \ # --host 127.0.0.1 --port 8765 --read-only # -# The MCP server inspects an existing Redis search index (adk_mcp_articles) -# and serves `search-records` over BM25 fulltext on the `content` field. +# Configured for hybrid search (BM25 text + vector similarity) over the +# `adk_mcp_knowledge_base` index built by load_data.py. The server embeds +# user queries via the vectorizer block and runs FT.HYBRID on Redis 8.4+. server: # Override with REDIS_URL env var if your Redis is not on localhost:6379. redis_url: ${REDIS_URL:-redis://localhost:6379} indexes: - articles: - redis_name: adk_mcp_articles + knowledge_base: + redis_name: adk_mcp_knowledge_base + vectorizer: + class: HFTextVectorizer + model: redis/langcache-embed-v2 search: - type: fulltext + type: hybrid params: - text_scorer: BM25STD - # Disable nltk-based stopword removal so the server runs without nltk. + # LINEAR fusion: 50% text BM25, 50% vector similarity. Adjust to + # taste. Set to "RRF" for Reciprocal Rank Fusion. + combination_method: LINEAR + linear_text_weight: 0.5 + # Disable nltk-based stopword removal so the server runs without + # the optional nltk dependency. stopwords: null runtime: text_field_name: content + vector_field_name: embedding default_limit: 5 max_limit: 20 diff --git a/examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py b/examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py index 5f9dc66..4150e1d 100644 --- a/examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py +++ b/examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py @@ -14,11 +14,11 @@ """RedisVL MCP search agent. -Connects an ADK agent to a running `rvl mcp` server via -`create_redisvl_mcp_toolset(...)` over the streamable-http transport. -The MCP server (configured by `../mcp_config.yaml`) exposes one -`search-records` tool over BM25 fulltext on the `content` field of the -`adk_mcp_articles` index. +The MCP-path mirror of `examples/redis_search_tools/`. It targets the +same knowledge-base corpus but routes search through a separately-running +`rvl mcp` server via `create_redisvl_mcp_toolset(...)`. The server is +configured for hybrid (BM25 + vector) search, so a single MCP tool +covers both semantic and keyword retrieval. """ import os @@ -30,18 +30,33 @@ from adk_redis import create_redisvl_mcp_toolset -INSTRUCTION = """You are a Redis docs assistant. You have a single MCP tool, -`search-records`, that runs BM25 fulltext search over a Redis index of -articles about Redis search, caching, memory, and the MCP server itself. +INSTRUCTION = """You are a helpful assistant with a technical knowledge base +served via an MCP server. You have one tool: `search-records`, configured +for hybrid search (BM25 text + vector similarity) over the +`adk_mcp_knowledge_base` index. -For any question: +## When to call search-records -1. Decide which keywords from the user's question are most likely to appear - in a relevant article (e.g., "HNSW", "FT.HYBRID", "semantic cache"). -2. Call `search-records` with that query. You can pass `limit` (default 5). -3. Summarize the top matches and cite each article's title and URL. +- Conceptual questions ("how does RAG work?") -> hybrid will lean on vector + similarity. +- Technical terms / acronyms ("HNSW", "FT.HYBRID", "BM25") -> hybrid keeps + exact keyword matches via the BM25 component. +- Comparative or "everything about X" questions -> hybrid combines both + paths and ranks by the configured fusion method (LINEAR by default). -If the tool returns no matches, say so plainly. Do not fabricate articles. +Pass a natural-language query in the `query` argument. The MCP server +embeds it server-side using `redis/langcache-embed-v2`. Optionally pass +`limit` (default 5). + +## Response style + +After calling `search-records`, summarize the matches for the user. Cite +each document's title and url. If the tool returns no matches, say so +plainly; do not fabricate results. + +Available document categories: redis, adk, concepts, tutorials. +Document types: reference, tutorial, faq, api. +Difficulty levels: beginner, intermediate, advanced. """ diff --git a/examples/redisvl_mcp_search/schema.yaml b/examples/redisvl_mcp_search/schema.yaml index 7668b10..8c31f15 100644 --- a/examples/redisvl_mcp_search/schema.yaml +++ b/examples/redisvl_mcp_search/schema.yaml @@ -12,15 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Article index for the RedisVL MCP search demo. Plain text + tag fields -# so the rvl mcp server can serve BM25 fulltext search without needing a -# vectorizer or embedding model at the server. +# Redis index schema for the redisvl_mcp_search sample. +# +# Mirrors `examples/redis_search_tools/schema.yaml` so users can compare +# the in-process Python tool path with the MCP toolset path on the same +# knowledge base. The `rvl mcp` server reads the live index for its +# search-records description; the schema below shapes those hints. version: "0.1.0" index: - name: adk_mcp_articles - prefix: article + name: adk_mcp_knowledge_base + prefix: mcp_doc fields: - name: title @@ -29,8 +32,22 @@ fields: - name: content type: text - - name: topic - type: tag - - name: url type: tag + + - name: category + type: tag # redis, adk, concepts, tutorials + + - name: doc_type + type: tag # reference, tutorial, faq, api + + - name: difficulty + type: tag # beginner, intermediate, advanced + + - name: embedding + type: vector + attrs: + algorithm: hnsw + dims: 768 + distance_metric: cosine + datatype: float32 diff --git a/scripts/smoke_adk_mcp_runner.py b/scripts/smoke_adk_mcp_runner.py index 3d081d7..47d576e 100644 --- a/scripts/smoke_adk_mcp_runner.py +++ b/scripts/smoke_adk_mcp_runner.py @@ -44,9 +44,9 @@ from redisvl_mcp_search_agent.agent import root_agent # noqa: E402 PROMPTS = [ - # Use a keyword-friendly prompt so the BM25 tokenizer matches cleanly. - # The corpus uses words like 'hybrid', 'caching', 'memory'. - "Find articles about hybrid search.", + # Hybrid search: semantic + keyword. The corpus is the knowledge base + # from redis_search_tools, embedded for vector + BM25 fusion. + "How does hybrid search work in Redis?", ] From 7b1b49b40b45a78bb96faea43e1010300e84ae39 Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Tue, 19 May 2026 20:33:05 -0400 Subject: [PATCH 3/8] docs: restructure README Consolidates a fragmented 623-line README into a focused 385-line walkthrough (-38%). Same scope of content, less duplication, accurate references throughout. Structural changes: - Replace the 3-column emoji feature card with a single flat "Surface" table that lists every shipping component (sessions, long-term memory, memory tools, AMS MCP, search tools, RedisVL MCP, cache providers) in one row each. - Collapse the three competing onboarding ramps ("Getting Started", "Quick Start", "Features Overview") into one Quick Start with Prerequisites + four representative snippets (sessions+memory, in-process search, MCP search, semantic cache). - Consolidate three overlapping memory tables ("Memory Integration Approaches", "MCP Integration", "Travel Agent Examples Comparison") into one "Memory backends" matrix. - Drop the long "Customizing Tool Prompts" block; keep one line in the Quick Start search snippet and the search-tools section. Correctness fixes: - Replace three stale doc paths (`docs/redis-setup.md`, `docs/agent-memory-server-setup.md`, `docs/integration-guide.md`) that were broken by the mkdocs migration with the current `docs/user_guide/...` paths. - Update code samples to repo-canonical model and vectorizer: `gemini-2.5-flash` and `redis/langcache-embed-v2`. - Update Requirements: ADK is now "1.0+ (tested through 2.0 GA)"; AMS Docker example uses gemini-2.5-flash. - Semantic cache snippet uses the public `create_llm_cache_callbacks` helper (was a placeholder comment). - Add `redis_sql_search` and `redisvl_mcp_search` rows to the Examples table. - Rename Development "Quick Start" -> "Setup" to remove the duplicate header. make check clean. 124 tests pass. --- README.md | 642 +++++++++++++++++------------------------------------- 1 file changed, 202 insertions(+), 440 deletions(-) diff --git a/README.md b/README.md index 33d72fd..8976014 100644 --- a/README.md +++ b/README.md @@ -21,534 +21,311 @@ --- -## Introduction - -**adk-redis** provides Redis integrations for Google's Agent Development Kit (ADK). Implements ADK's `BaseMemoryService`, `BaseSessionService`, tool interfaces, and semantic caching using Redis Agent Memory Server and RedisVL. - -

- -| 🔌 [**ADK Services**](#memory-services) | 🔧 [**Agent Tools**](#search-tools) | ⚡ [**Semantic Caching**](#semantic-caching) | -|:---:|:---:|:---:| -| **Memory Service**
*Long-term memory via Agent Memory Server* | **Memory Tools**
*LLM-controlled memory operations* | **LLM Response Cache**
*Reduce latency & costs* | -| Semantic search & auto-extraction | REST API or MCP protocol | Similarity-based cache lookup | -| Cross-session knowledge retrieval | search, create, update, delete | Configurable distance threshold | -| Recency-boosted search | Namespace & user isolation | TTL-based expiration | -| **Session Service**
*Working memory via Agent Memory Server* | **Search Tools**
*RAG via RedisVL* | **Tool Cache**
*Avoid redundant calls* | -| Context window management | Vector, hybrid, range, text, SQL | Cache tool execution results | -| Auto-summarization | In-process or via `rvl mcp` server | Reduce API calls | -| Background memory promotion | **MCP Toolsets**
*Model Context Protocol* | **LangCache**
*Managed semantic cache* | -| | AMS memory (SSE) + RedisVL search (stdio/sse/streamable-http) | Cloud-hosted, no local vectorizer | - -
+## What it does +`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. +| Surface | What you get | Backed by | +|---|---|---| +| **Sessions** (`RedisWorkingMemorySessionService`) | `BaseSessionService` with auto-summarization and context-window management | Agent Memory Server (REST) | +| **Long-term memory** (`RedisLongTermMemoryService`) | `BaseMemoryService` with semantic search and recency boosting | Agent Memory Server (REST) | +| **Memory tools** (`SearchMemoryTool`, `CreateMemoryTool`, ...) | LLM-controlled memory operations | Agent Memory Server (REST) | +| **AMS MCP toolset** (`create_memory_mcp_toolset`) | Same memory tools surfaced over MCP/SSE | Agent Memory Server (MCP) | +| **Search tools** (5 in-process tools) | Vector, hybrid, range, text, SQL search as `BaseTool` subclasses | RedisVL (Python) | +| **RedisVL MCP toolset** (`create_redisvl_mcp_toolset`) | One Redis index served to many agents over stdio / sse / streamable-http | `rvl mcp` server | +| **Semantic cache** (`RedisVLCacheProvider`, `LangCacheProvider`) | Skip repeat LLM calls and tool calls by semantic similarity | RedisVL `SemanticCache` or [Redis LangCache](https://redis.io/langcache) | --- ## Installation -### Install from PyPI - ```bash pip install adk-redis ``` -### Optional Dependencies - -Install with optional features based on your use case: +Optional extras (combine as needed): ```bash -# Memory & session services (Redis Agent Memory Server integration) -pip install adk-redis[memory] - -# Search tools (RedisVL integration) -pip install adk-redis[search] - -# LangCache (managed semantic cache service) -pip install adk-redis[langcache] - -# SQL-to-Redis search tool (RedisSQLSearchTool, requires sql-redis) -pip install adk-redis[sql] - -# RedisVL MCP toolset helper (`create_redisvl_mcp_toolset`) -pip install adk-redis[mcp-search] - -# All library features -pip install adk-redis[all] - -# Running the examples (adds python-dotenv and other example dependencies) -pip install adk-redis[all,examples] +pip install 'adk-redis[memory]' # sessions + long-term memory services +pip install 'adk-redis[search]' # RedisVL-backed search tools +pip install 'adk-redis[sql]' # RedisSQLSearchTool (sql-redis) +pip install 'adk-redis[mcp-search]' # create_redisvl_mcp_toolset helper +pip install 'adk-redis[langcache]' # managed semantic cache provider +pip install 'adk-redis[all]' # all of the above +pip install 'adk-redis[all,examples]' # plus dotenv etc. for running examples ``` -### Verify Installation +### Verify ```bash python -c "from adk_redis import __version__; print(__version__)" ``` -### Development Installation - -For contributors or those who want the latest unreleased changes: +### Development install ```bash -# Clone the repository git clone https://github.com/redis-developer/adk-redis.git cd adk-redis - -# Install with uv (recommended for development) pip install uv uv sync --all-extras - -# Or install directly from GitHub -pip install git+https://github.com/redis-developer/adk-redis.git@main ``` --- -## Getting Started - -### Prerequisites - -**For memory/session services:** -- [Redis Agent Memory Server](https://github.com/redis/agent-memory-server) (port 8088) -- Redis 8.4+ or Redis Cloud (backend for Agent Memory Server) - -**For search tools:** -- Redis 8.4+ or Redis Cloud with Search capability - -**Quick start:** - -#### 1. Start Redis 8.4 - -Redis is required for all examples in this repository. Choose one of the following options: - -**Option A: Automated setup (recommended)** - -```bash -# Run from the repository root -./scripts/start-redis.sh -``` - -This script will: -- Check if Docker is installed and running -- Check if Redis is already running on port 6379 -- Start Redis 8.4 in a Docker container with health checks -- Verify the Redis container is healthy and accepting connections -- Provide helpful commands for managing Redis - -**Option B: Manual setup** - -```bash -docker run -d --name redis -p 6379:6379 redis:8.4-alpine -``` - -> **Note**: Redis 8.4 includes the Redis Query Engine (evolved from RediSearch) with native support for vector search, full-text search, and JSON operations. Docker will automatically download the image (~40MB) on first run. - -**Verify Redis is running:** - -```bash -# Check container status -docker ps | grep redis - -# Test connection -docker exec redis redis-cli ping -# Should return: PONG - -# Or if you have redis-cli installed locally -redis-cli -p 6379 ping -``` - -**Common Redis commands:** - -```bash -# View logs -docker logs redis -docker logs -f redis # Follow logs in real-time - -# Stop Redis -docker stop redis - -# Restart Redis -docker restart redis - -# Remove Redis (stops and deletes container) -docker rm -f redis -``` - -**Troubleshooting:** - -- **Port 6379 already in use**: Another process is using the port. Find it with `lsof -i :6379` or use a different port: `docker run -d --name redis -p 6380:6379 redis:8.4-alpine` -- **Docker not running**: Start Docker Desktop or the Docker daemon -- **Permission denied**: Run with `sudo` or add your user to the docker group -- **Container won't start**: Check logs with `docker logs redis` - -#### 2. Start Agent Memory Server - -```bash -docker run -d --name agent-memory-server -p 8088:8088 \ - -e REDIS_URL=redis://host.docker.internal:6379 \ - -e GEMINI_API_KEY=your-gemini-api-key \ - -e GENERATION_MODEL=gemini/gemini-2.0-flash \ - -e EMBEDDING_MODEL=gemini/text-embedding-004 \ - -e FAST_MODEL=gemini/gemini-2.0-flash \ - -e SLOW_MODEL=gemini/gemini-2.0-flash \ - -e EXTRACTION_DEBOUNCE_SECONDS=5 \ - redislabs/agent-memory-server:0.13.2 \ - agent-memory api --host 0.0.0.0 --port 8088 --task-backend=asyncio -``` - -> **Configuration Options:** -> - **LLM Provider**: Agent Memory Server uses [LiteLLM](https://docs.litellm.ai/) and supports 100+ providers (OpenAI, Gemini, Anthropic, AWS Bedrock, Ollama, etc.). Set the appropriate environment variables for your provider (e.g., `GEMINI_API_KEY`, `GENERATION_MODEL=gemini/gemini-2.0-flash`). See the [Agent Memory Server LLM Providers docs](https://redis.github.io/agent-memory-server/llm-providers/) for details. -> - **Model Configuration**: Set `GENERATION_MODEL`, `FAST_MODEL` (for quick tasks like extraction), and `SLOW_MODEL` (for complex tasks) to your preferred models. All default to OpenAI models if not specified. -> - **Memory Extraction Debounce**: `EXTRACTION_DEBOUNCE_SECONDS` controls how long to wait before extracting memories from a conversation (default: 300 seconds). Lower values (e.g., 5) provide faster memory extraction, while higher values reduce API calls. -> - **Embedding Models**: Agent Memory Server also uses LiteLLM for embeddings. For local/offline embeddings, use Ollama (e.g., `EMBEDDING_MODEL=ollama/nomic-embed-text`, `REDISVL_VECTOR_DIMENSIONS=768`). See [Embedding Providers docs](https://redis.github.io/agent-memory-server/embedding-providers/) for all options. - -**See detailed setup guides:** -- [Redis Setup Guide](docs/redis-setup.md) - All Redis deployment options -- [Agent Memory Server Setup](docs/agent-memory-server-setup.md) - Complete configuration -- [Integration Guide](docs/integration-guide.md) - End-to-end setup with code examples - ---- - ## Quick Start -### Two-Tier Memory Architecture +### Prerequisites -Uses both working memory (session-scoped) and long-term memory (persistent): +- Python 3.10+ +- Redis 8.4+ with the Redis Query Engine (Search). Local Docker: + ```bash + docker run -d --name redis -p 6379:6379 redis:8.4 + docker exec redis redis-cli ping # -> PONG + ``` +- For session / memory services: a running [Agent Memory Server](https://github.com/redis/agent-memory-server) (default port 8088). Quick start: + ```bash + docker run -d --name agent-memory-server -p 8088:8088 \ + -e REDIS_URL=redis://host.docker.internal:6379 \ + -e GEMINI_API_KEY=YOUR_KEY \ + -e GENERATION_MODEL=gemini/gemini-2.5-flash \ + -e EMBEDDING_MODEL=gemini/text-embedding-004 \ + redislabs/agent-memory-server:0.13.2 \ + agent-memory api --host 0.0.0.0 --port 8088 --task-backend=asyncio + ``` + AMS supports 100+ LLM and embedding providers via [LiteLLM](https://docs.litellm.ai/). See [Agent Memory Server setup](docs/user_guide/how_to_guides/memory_server_setup.md) for the full configuration matrix. + +For Redis Cloud, Redis Enterprise, or troubleshooting, see [Redis setup](docs/user_guide/how_to_guides/redis_setup.md). + +### Sessions + long-term memory + +Two-tier memory: working memory (per session) and long-term memory (cross-session). Both implement ADK's service interfaces and slot into any `Runner`. ```python from google.adk import Agent from google.adk.runners import Runner -from adk_redis.memory import RedisLongTermMemoryService, RedisLongTermMemoryServiceConfig -from adk_redis.sessions import ( +from adk_redis import ( + RedisLongTermMemoryService, + RedisLongTermMemoryServiceConfig, RedisWorkingMemorySessionService, RedisWorkingMemorySessionServiceConfig, ) -# Configure session service (Tier 1: Working Memory) -session_config = RedisWorkingMemorySessionServiceConfig( - api_base_url="http://localhost:8088", # Agent Memory Server URL - default_namespace="my_app", - model_name="gpt-4o", # Model for auto-summarization - context_window_max=8000, # Trigger summarization at this token count +session_service = RedisWorkingMemorySessionService( + config=RedisWorkingMemorySessionServiceConfig( + api_base_url="http://localhost:8088", + default_namespace="my_app", + model_name="gpt-4o", + context_window_max=8000, + ), ) -session_service = RedisWorkingMemorySessionService(config=session_config) - -# Configure memory service (Tier 2: Long-Term Memory) -memory_config = RedisLongTermMemoryServiceConfig( - api_base_url="http://localhost:8088", - default_namespace="my_app", - extraction_strategy="discrete", # Extract individual facts - recency_boost=True, # Prioritize recent memories in search +memory_service = RedisLongTermMemoryService( + config=RedisLongTermMemoryServiceConfig( + api_base_url="http://localhost:8088", + default_namespace="my_app", + extraction_strategy="discrete", + recency_boost=True, + ), ) -memory_service = RedisLongTermMemoryService(config=memory_config) -# Create agent agent = Agent( + model="gemini-2.5-flash", name="memory_agent", - model="gemini-2.0-flash", instruction="You are a helpful assistant with long-term memory.", ) -# Create runner with both services runner = Runner( - agent=agent, app_name="my_app", + agent=agent, session_service=session_service, memory_service=memory_service, ) ``` -**How it works:** +How it works: the session service stores conversation events in working memory and auto-summarizes when the token budget is hit; the memory service runs background extraction to long-term memory and surfaces a recency-boosted semantic search. -1. **Working Memory**: Stores session messages, state, and handles auto-summarization -2. **Background Extraction**: Automatically promotes important information to long-term memory -3. **Long-Term Memory**: Provides semantic search across all sessions for relevant context -4. **Recency Boosting**: Prioritizes recent memories while maintaining access to historical knowledge - -### Vector Search Tools - -RAG with semantic search using RedisVL: +### Search over a Redis index (in-process) ```python from google.adk import Agent from redisvl.index import SearchIndex from redisvl.utils.vectorize import HFTextVectorizer -from adk_redis.tools import RedisVectorSearchTool, RedisVectorQueryConfig +from adk_redis import RedisVectorQueryConfig, RedisVectorSearchTool -# Create a vectorizer (HuggingFace, OpenAI, Cohere, Mistral, Voyage AI, etc.) -vectorizer = HFTextVectorizer(model="sentence-transformers/all-MiniLM-L6-v2") - -# Connect to existing search index +vectorizer = HFTextVectorizer(model="redis/langcache-embed-v2") index = SearchIndex.from_existing("products", redis_url="redis://localhost:6379") -# Create the search tool with custom name and description search_tool = RedisVectorSearchTool( index=index, vectorizer=vectorizer, - config=RedisVectorQueryConfig( - vector_field_name="embedding", - return_fields=["name", "description", "price"], - num_results=5, - ), - # Customize the tool name and description for your domain + config=RedisVectorQueryConfig(num_results=5), + return_fields=["name", "description", "price"], name="search_product_catalog", - description="Search to find relevant products in the product catalog by description semantic similarity", + description="Find products by semantic similarity to the user's query.", ) -# Use with an ADK agent agent = Agent( + model="gemini-2.5-flash", name="search_agent", - model="gemini-2.0-flash", - instruction="Help users find products using semantic search.", + instruction="Help users find products.", tools=[search_tool], ) ``` -**Customizing Tool Prompts:** +All five search tools accept custom `name` / `description` so the LLM sees a domain-specific tool rather than a generic search helper. -All search tools (`RedisVectorSearchTool`, `RedisHybridSearchTool`, `RedisTextSearchTool`, `RedisRangeSearchTool`, `RedisSQLSearchTool`) support custom `name` and `description` parameters to make them domain-specific: +### Search over a Redis index (MCP) + +Run `rvl mcp --config mcp_config.yaml` separately and let the agent connect over MCP: ```python -# Example: Medical knowledge base -medical_search = RedisVectorSearchTool( - index=medical_index, - vectorizer=vectorizer, - name="search_medical_knowledge", - description="Search medical literature and clinical guidelines for relevant information", -) +from google.adk import Agent +from pydantic import SecretStr + +from adk_redis import create_redisvl_mcp_toolset -# Example: Customer support FAQ -faq_search = RedisTextSearchTool( - index=faq_index, - name="search_support_articles", - description="Search customer support articles and FAQs by keywords", +search_tools = create_redisvl_mcp_toolset( + url="http://localhost:8765/mcp", + auth_token=SecretStr("..."), # optional bearer + read_only=True, ) -# Example: Legal document search -legal_search = RedisHybridSearchTool( - index=legal_index, - vectorizer=vectorizer, - name="search_legal_documents", - description="Search legal documents using both semantic similarity and keyword matching", +agent = Agent( + model="gemini-2.5-flash", + name="mcp_search_agent", + tools=[search_tools], ) ``` -> **Note:** RedisVL supports many vectorizers including OpenAI, HuggingFace, Cohere, Mistral, Voyage AI, and more. See [RedisVL documentation](https://docs.redisvl.com/) for the full list. - -> **Future Enhancement:** We plan to add native support for ADK embeddings classes through a union type or wrapper, allowing seamless integration with ADK's embedding infrastructure alongside RedisVL vectorizers. +See [examples/redisvl_mcp_search/](examples/redisvl_mcp_search/) for a runnable demo (knowledge-base corpus, hybrid mode, paired with the in-process [examples/redis_search_tools/](examples/redis_search_tools/) example). ---- - -## Features Overview - -### Memory Services +### Semantic cache -Implements ADK's `BaseMemoryService` interface for persistent agent memory: - -| Feature | Description | -|---------|-------------| -| **Semantic Search** | Vector-based similarity search across all sessions | -| **Recency Boosting** | Prioritize recent memories while maintaining historical access | -| **Auto-Extraction** | LLM-based extraction of facts, preferences, and episodic memories | -| **Cross-Session Retrieval** | Access knowledge from any previous conversation | -| **Background Processing** | Non-blocking memory promotion and indexing | +```python +from google.adk import Agent +from redisvl.utils.vectorize import HFTextVectorizer -**Implementation:** `RedisLongTermMemoryService` +from adk_redis import ( + LLMResponseCache, + RedisVLCacheProvider, + RedisVLCacheProviderConfig, + create_llm_cache_callbacks, +) -### Session Services +provider = RedisVLCacheProvider( + config=RedisVLCacheProviderConfig( + redis_url="redis://localhost:6379", + ttl=3600, + distance_threshold=0.1, + ), + vectorizer=HFTextVectorizer(model="redis/langcache-embed-v2"), +) +llm_cache = LLMResponseCache(provider=provider) +before_cb, after_cb = create_llm_cache_callbacks(llm_cache) -Implements ADK's `BaseSessionService` interface for conversation management: +agent = Agent( + model="gemini-2.5-flash", + name="cached_agent", + before_model_callback=before_cb, + after_model_callback=after_cb, +) +``` -| Feature | Description | -|---------|-------------| -| **Message Storage** | Persist conversation messages and session state | -| **Auto-Summarization** | Automatic summarization when context window limits are exceeded | -| **Memory Promotion** | Trigger background extraction to long-term memory | -| **State Management** | Store and retrieve arbitrary session state | -| **Token Tracking** | Monitor context window usage | +For a managed alternative that needs no local vectorizer, swap in `LangCacheProvider` / `LangCacheProviderConfig` from `adk_redis`. -**Implementation:** `RedisWorkingMemorySessionService` +--- -### Search Tools +## Search tools -Two parallel paths for RAG over a Redis index. Pick by deployment shape, not by feature. +Two parallel paths for RAG over a Redis index. Pick by deployment shape. | Path | Use when | |---|---| -| **In-process** (Python `BaseTool` subclasses) | Single ADK process, fast onboarding, Python-side `FilterExpression` composition, per-tool customization. Five tools: vector, hybrid, range, text, SQL. | -| **MCP toolset** (`create_redisvl_mcp_toolset` against `rvl mcp`) | One Redis index served to multiple agents (Python, JS, Claude Desktop). Server-side `--read-only` / bearer auth. Schema-aware tool descriptions. Two tools: `search-records`, `upsert-records`. | +| **In-process** | Single ADK process, fast onboarding, Python-side `FilterExpression` composition, per-tool customization. | +| **MCP toolset** (`create_redisvl_mcp_toolset` against `rvl mcp`) | One Redis index served to multiple agents (Python, JS, Claude Desktop). Server-side `--read-only` / bearer auth. Schema-aware tool descriptions. | -#### In-process tools +### In-process tools -| Tool | Best For | Key Features | -|------|----------|--------------| -| **`RedisVectorSearchTool`** | Semantic similarity | Vector embeddings, KNN search, metadata filtering | -| **`RedisHybridSearchTool`** | Combined search | Vector + text search, Redis 8.4+ native `FT.HYBRID`, aggregation fallback | -| **`RedisRangeSearchTool`** | Threshold-based retrieval | Distance-based filtering, similarity radius (no MCP equivalent) | -| **`RedisTextSearchTool`** | Keyword search | Full-text BM25 search, no embeddings required | -| **`RedisSQLSearchTool`** | SQL-style filters | `SELECT ... WHERE` against a bound index, `:param` placeholders (requires `adk-redis[sql]`, no MCP equivalent) | +| Tool | Best for | Notes | +|---|---|---| +| `RedisVectorSearchTool` | Semantic similarity | KNN vector search with metadata filters | +| `RedisHybridSearchTool` | Combined search | Vector + BM25; native `FT.HYBRID` on Redis 8.4+, aggregation fallback below | +| `RedisRangeSearchTool` | Threshold retrieval | Distance-bounded vector search. **No MCP equivalent.** | +| `RedisTextSearchTool` | Keyword search | BM25 full-text; no embeddings needed | +| `RedisSQLSearchTool` | SQL-style filters | `SELECT ... WHERE` with `:param` placeholders. Requires `adk-redis[sql]`. **No MCP equivalent.** | -All five support multiple vectorizers (OpenAI, HuggingFace, Cohere, Mistral, Voyage AI, etc.) and arbitrary `FilterExpression` objects from `redisvl.query.filter`. +All five accept any vectorizer supported by RedisVL (OpenAI, HuggingFace, Cohere, Mistral, Voyage AI, custom) and any `FilterExpression` from `redisvl.query.filter`. -#### MCP toolset +### MCP toolset -`create_redisvl_mcp_toolset(...)` returns an ADK `McpToolset` wired to a running [RedisVL MCP server](https://docs.redisvl.com) (`rvl mcp`). The server is configured per index via YAML and exposes: +`create_redisvl_mcp_toolset(...)` returns an ADK `McpToolset` wired to a [RedisVL MCP server](https://docs.redisvl.com) (`rvl mcp`). The server is configured per index via YAML and exposes: -- `search-records`: `vector`, `fulltext`, or `hybrid` mode (chosen at server start). Tool description includes filter and return-field hints derived from the bound index schema. +- `search-records`: `vector`, `fulltext`, or `hybrid` (chosen at server start). Tool description includes filter and return-field hints derived from the bound index schema. - `upsert-records`: write path (suppress with `--read-only`). -Supports `stdio`, `sse`, and `streamable-http` transports; bearer auth on HTTP. Requires `adk-redis[mcp-search]` and a `rvl mcp` server. See [`examples/redisvl_mcp_search/`](examples/redisvl_mcp_search/) for a runnable demo and [the search-tools guide](docs/user_guide/how_to_guides/search_tools.md) for the full decision matrix. +Supports `stdio`, `sse`, and `streamable-http` transports; bearer auth on HTTP. Requires `adk-redis[mcp-search]` and a `rvl mcp` server. -> **Coverage note:** range and SQL search have **no MCP equivalent today**; if you need either, you must use the in-process tool. +For the full decision matrix and runnable demo, see [docs/user_guide/how_to_guides/search_tools.md](docs/user_guide/how_to_guides/search_tools.md). -### Semantic Caching +--- -Reduce latency and costs with similarity-based caching: +## Memory backends -| Feature | Description | -|---------|-------------| -| **LLM Response Cache** | Cache LLM responses and return similar cached results | -| **Tool Result Cache** | Cache tool execution results to avoid redundant calls | -| **Similarity Threshold** | Configurable distance threshold for cache hits | -| **TTL Support** | Time-based cache expiration | -| **Multiple Vectorizers** | Support for OpenAI, HuggingFace, local embeddings, etc. | -| **LangCache (Managed)** | Cloud-hosted semantic cache — no local vectorizer needed | +Three ways to ingest, store, and retrieve memory with Agent Memory Server, all interoperable: -**Cache Providers:** +| Approach | What it is | Reach for it when | +|---|---|---| +| **ADK Services** (`RedisLongTermMemoryService`, `RedisWorkingMemorySessionService`) | The `BaseMemoryService` and `BaseSessionService` implementations. ADK calls AMS for you. | You want framework-managed sessions and automatic memory extraction. Most production cases. | +| **REST tools** (`MemoryPromptTool`, `SearchMemoryTool`, `CreateMemoryTool`, `UpdateMemoryTool`, `DeleteMemoryTool`, `GetMemoryTool`) | `BaseTool` subclasses that call AMS REST directly. The LLM decides when to invoke them. | You want the agent to control when memory is read or written. | +| **MCP toolset** (`create_memory_mcp_toolset`) | Same memory operations surfaced over MCP/SSE. Standard MCP tool discovery. | You want one AMS instance shared across many agents, or you prefer MCP wiring over Python tool wrappers. | -| Provider | Description | Vectorizer Required | -|----------|-------------|:-------------------:| -| `RedisVLCacheProvider` | Self-hosted semantic cache using RedisVL | Yes | -| `LangCacheProvider` | Managed semantic cache via [Redis LangCache](https://redis.io/langcache) | No (server-side) | +The protocol is REST in both of the first two; MCP for the third. All three operate on the same underlying memory. -Both providers implement `BaseCacheProvider` and work with `LLMResponseCache` and `ToolCache`. +--- -**LangCache Quick Start:** +## Semantic cache -```python -from adk_redis import LangCacheProvider, LangCacheProviderConfig, LLMResponseCache +Two providers, both implementing `BaseCacheProvider`. Pair either with `LLMResponseCache` or `ToolCache` and wire via `create_llm_cache_callbacks` / `create_tool_cache_callbacks`. -# Configure LangCache (managed — no local embeddings needed) -langcache_config = LangCacheProviderConfig( - cache_id="your-cache-id", - api_key="your-api-key", - ttl=3600, -) -cache_provider = LangCacheProvider(config=langcache_config) +| Provider | Hosted | Vectorizer | Best for | +|---|---|---|---| +| `RedisVLCacheProvider` | Self-hosted Redis | Required (any RedisVL vectorizer) | Full control, your data stays in your Redis | +| `LangCacheProvider` | Managed via [Redis LangCache](https://redis.io/langcache) | Server-side (none needed locally) | Zero-infra cache; no local Redis needed | -# Use with LLM response caching -llm_cache = LLMResponseCache(provider=cache_provider) -``` +Both honor a configurable `distance_threshold` and per-entry `ttl`. --- ## Requirements -- **Python** 3.10, 3.11, 3.12, or 3.13 -- **Google ADK** 1.0.0+ (validated against 1.23.0; 2.0.0 GA support tracked in the next release) -- **RedisVL** 0.18.2+ (when the `search`, `langcache`, `sql`, or `mcp-search` extra is installed) -- **For memory/session services:** [Redis Agent Memory Server](https://github.com/redis/agent-memory-server) -- **For search tools:** Redis 8.4+ or Redis Cloud with Search capability -- **For `RedisSQLSearchTool`:** `sql-redis` (installed by `adk-redis[sql]`) -- **For RedisVL MCP toolset:** `redisvl[mcp]` and the `rvl mcp` CLI +- Python 3.10, 3.11, 3.12, or 3.13 +- Google ADK 1.0+ (tested through 2.0 GA) +- RedisVL 0.18.2+ when the `search`, `langcache`, `sql`, or `mcp-search` extra is installed +- Redis 8.4+ (or Redis Cloud with Search) when using search tools or the cache providers +- For session / memory services: a running [Agent Memory Server](https://github.com/redis/agent-memory-server) +- For `RedisSQLSearchTool`: `sql-redis` (installed by `adk-redis[sql]`) +- For the RedisVL MCP toolset: `redisvl[mcp]` and the `rvl mcp` CLI (installed by `adk-redis[mcp-search]`) --- ## Examples -Complete working examples with ADK web runner integration: - -| Example | Description | Features | -|---------|-------------|----------| -| **[simple_redis_memory](examples/simple_redis_memory/)** | Agent with two-tier memory architecture | Working memory, long-term memory, auto-summarization, semantic search | -| **[semantic_cache](examples/semantic_cache/)** | Semantic caching for LLM responses | Vector-based cache, reduced latency, cost optimization, local embeddings | -| **[langcache_cache](examples/langcache_cache/)** | Managed semantic caching via LangCache | Cloud-hosted cache, no local vectorizer, no Redis instance needed | -| **[redis_search_tools](examples/redis_search_tools/)** | RAG with search tools | Vector search, hybrid search, range search, text search | -| **[travel_agent_memory_hybrid](examples/travel_agent_memory_hybrid/)** | Travel agent with framework-managed memory | Redis session + memory services, automatic memory extraction, web search, calendar export, itinerary planning | -| **[travel_agent_memory_tools](examples/travel_agent_memory_tools/)** | Travel agent with LLM-controlled memory | REST memory tools (search/create/update/delete), in-memory session, web search, calendar export, itinerary planning | -| **[fitness_coach_mcp](examples/fitness_coach_mcp/)** | Fitness coach with MCP memory tools | MCP-based memory via SSE, semantic + episodic memory, workout tracking, injury awareness | - -### Memory Integration Approaches - -There are **three ways** to integrate memory with ADK agents using Redis Agent Memory Server: - -| Approach | Example | Protocol | Best For | -|----------|---------|----------|----------| -| **ADK Services** | `simple_redis_memory`, `travel_agent_memory_hybrid` | REST | Full framework integration (`BaseSessionService` + `BaseMemoryService`) | -| **REST Tools** | `travel_agent_memory_tools` | REST | LLM-controlled memory with explicit tool calls | -| **MCP Tools** | `fitness_coach_mcp` | SSE | Standard MCP protocol, automatic tool discovery | - -### MCP Integration - -MCP (Model Context Protocol) provides a standardized way to connect agents to tools. The `fitness_coach_mcp` example demonstrates this approach: - -```python -from adk_redis import create_memory_mcp_toolset +All examples run via `adk web` and ship with a README and `.env.example`. -memory_tools = create_memory_mcp_toolset( - server_url="http://localhost:8088", - tool_filter=["search_long_term_memory", "create_long_term_memories"], -) - -agent = Agent(model="gemini-2.0-flash", tools=[memory_tools]) -``` - -**Available MCP Tools:** -- `search_long_term_memory` - Semantic search across memories -- `create_long_term_memories` - Store new memories (semantic or episodic) -- `get_long_term_memory` - Retrieve memory by ID -- `edit_long_term_memory` - Update existing memories -- `delete_long_term_memories` - Remove memories -- `memory_prompt` - Get context-enriched prompts -- `set_working_memory` - Update working memory - -For a complete MCP example, see the [fitness_coach_mcp example](examples/fitness_coach_mcp/). - -#### RedisVL MCP server - -In addition to Agent Memory Server MCP tools, you can connect an agent to RedisVL's own MCP server (one Redis index per server) with `create_redisvl_mcp_toolset(...)`: - -```python -from pydantic import SecretStr -from adk_redis import create_redisvl_mcp_toolset - -# Remote server, streamable-http (default), read-only by default. -search_tools = create_redisvl_mcp_toolset( - url="http://localhost:8000/mcp", - auth_token=SecretStr("..."), # optional bearer -) - -# Or spawn `rvl mcp --config ` in-process over stdio. -search_tools = create_redisvl_mcp_toolset( - transport="stdio", - config_path="/etc/redisvl/mcp.yaml", - read_only=True, -) - -agent = Agent(model="gemini-2.0-flash", tools=[search_tools]) -``` - -Available tool names: `search-records`, `upsert-records` (also exported as `REDISVL_MCP_TOOL_SEARCH` / `REDISVL_MCP_TOOL_UPSERT`). - -### Travel Agent Examples Comparison - -Both travel agent examples use **Redis Agent Memory Server** for long-term memory. The difference is in how they integrate with ADK: - -| Aspect | `travel_agent_memory_hybrid` | `travel_agent_memory_tools` | -|--------|------------------------------|----------------------------| -| **How to Run** | `python main.py` (custom FastAPI) | `adk web .` (standard ADK CLI) | -| **Session Service** | `RedisWorkingMemorySessionService` (Redis-backed) | ADK default (in-memory) | -| **Memory Service** | `RedisLongTermMemoryService` (ADK interface) | REST tools only | -| **Best For** | Full ADK service integration | Tool-based integration | - -Each example includes: -- Complete runnable code -- ADK web runner integration -- Configuration examples -- Setup instructions +| Example | Demonstrates | +|---|---| +| [`simple_redis_memory`](examples/simple_redis_memory/) | Two-tier memory + auto-summarization | +| [`travel_agent_memory_hybrid`](examples/travel_agent_memory_hybrid/) | Framework-managed memory: `RedisWorkingMemorySessionService` + `RedisLongTermMemoryService` in a custom FastAPI runner | +| [`travel_agent_memory_tools`](examples/travel_agent_memory_tools/) | LLM-controlled memory: REST `SearchMemoryTool` / `CreateMemoryTool` / etc. in a default ADK runner | +| [`fitness_coach_mcp`](examples/fitness_coach_mcp/) | AMS memory exposed over MCP via `create_memory_mcp_toolset` | +| [`redis_search_tools`](examples/redis_search_tools/) | In-process RAG with vector + text + range search tools | +| [`redis_sql_search`](examples/redis_sql_search/) | `RedisSQLSearchTool` answering catalog questions via parameterized SQL | +| [`redisvl_mcp_search`](examples/redisvl_mcp_search/) | Same knowledge base as `redis_search_tools/`, served via `rvl mcp` over MCP | +| [`semantic_cache`](examples/semantic_cache/) | Self-hosted semantic cache via `RedisVLCacheProvider` | +| [`langcache_cache`](examples/langcache_cache/) | Managed semantic cache via `LangCacheProvider` | + +The two travel-agent examples use the same Agent Memory Server backend; the difference is whether the agent talks to AMS through framework services (`hybrid`) or LLM-driven tool calls (`tools`). --- @@ -556,68 +333,53 @@ Each example includes: This project follows the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), matching the [ADK-Python core](https://github.com/google/adk-python) project conventions. -### Quick Start +### Setup ```bash -# Clone the repository git clone https://github.com/redis-developer/adk-redis.git cd adk-redis - -# Install development dependencies -make dev - -# Run all checks (format, lint, type-check, test) -make check +make dev # install with all extras + dev deps +make check # format-check + lint + type-check + test ``` -### Available Commands +### Targets ```bash -make format # Format code with pyink and isort -make lint # Run ruff linter -make type-check # Run mypy type checker -make test # Run pytest test suite -make coverage # Generate coverage report +make format # apply pyink + isort +make lint # ruff check +make type-check # mypy +make test # all tests (unit + integration) +make test-unit # unit tests only (no Redis required) +make test-integration # integration suite (needs Redis 8.4+ at REDIS_URL) +make redis-up # start a redis:8.4 container on :6399 for integration +make redis-down # stop and remove that container +make test-cov # coverage report ``` -### Code Quality -See **[CONTRIBUTING.md](CONTRIBUTING.md)** for coding style, type hints, testing, and PR guidelines. +See [CONTRIBUTING.md](CONTRIBUTING.md) for testing, style, and PR conventions. --- ## Contributing -Please help us by contributing PRs, opening GitHub issues for bugs or new feature ideas, improving documentation, or increasing test coverage. See the following steps for contributing: - -1. [Open an issue](https://github.com/redis-developer/adk-redis/issues) for bugs or feature requests -2. Read [CONTRIBUTING.md](CONTRIBUTING.md) and submit a pull request -3. Improve documentation and examples +Open an [issue](https://github.com/redis-developer/adk-redis/issues) for bugs and feature requests, or submit a PR following [CONTRIBUTING.md](CONTRIBUTING.md). Documentation and example contributions are equally welcome. --- ## License -Apache 2.0 - See [LICENSE](LICENSE) for details. +Apache 2.0. See [LICENSE](LICENSE). --- -## Helpful Links - -### Documentation & Resources -- **[PyPI Package](https://pypi.org/project/adk-redis/)** - Install with `pip install adk-redis` -- **[GitHub Repository](https://github.com/redis-developer/adk-redis)** - Source code and issue tracking -- **[Examples](examples/)** - Complete working examples with ADK web runner -- **[Contributing Guide](CONTRIBUTING.md)** - How to contribute to the project - -### Setup Guides -- **[Redis Setup Guide](docs/redis-setup.md)** - All Redis deployment options -- **[Agent Memory Server Setup](docs/agent-memory-server-setup.md)** - Complete configuration -- **[Integration Guide](docs/integration-guide.md)** - End-to-end setup with code examples - -### Related Projects -- **[Google ADK](https://github.com/google/adk-python)** - Agent Development Kit framework -- **[Redis Agent Memory Server](https://github.com/redis/agent-memory-server)** - Memory layer for AI agents -- **[RedisVL](https://docs.redisvl.com/)** - Redis Vector Library documentation -- **[Redis](https://redis.io/)** - Redis 8.4+ with Search, JSON, and vector capabilities - ---- +## Helpful links + +- [PyPI](https://pypi.org/project/adk-redis/) — install with `pip install adk-redis` +- [Redis setup](docs/user_guide/how_to_guides/redis_setup.md) — local, Docker, and Redis Cloud +- [Agent Memory Server setup](docs/user_guide/how_to_guides/memory_server_setup.md) — full AMS configuration +- [Integration walkthrough](docs/user_guide/01_integration.md) — end-to-end wiring +- [Search tools guide](docs/user_guide/how_to_guides/search_tools.md) — in-process vs MCP, decision matrix +- [Google ADK](https://github.com/google/adk-python) — agent framework +- [Agent Memory Server](https://github.com/redis/agent-memory-server) — memory backend +- [RedisVL](https://docs.redisvl.com/) — Redis Vector Library +- [Redis LangCache](https://redis.io/langcache) — managed semantic cache From 6628e4ac76c91dd38b91696bf8aabe6a5294cd1b Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Tue, 19 May 2026 20:34:58 -0400 Subject: [PATCH 4/8] docs: surface what the MCP toolsets actually expose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Surface table's MCP rows said "Same memory tools surfaced over MCP/SSE" and "One Redis index served to many agents over stdio / sse / streamable-http" — both describe deployment shape rather than what the server exposes. Replace with the actual tool names and capabilities: - AMS MCP row: lists the seven AMS tools (search/create/edit/delete/get long-term memory, memory_prompt, set_working_memory). - RedisVL MCP row: lists the two tools (search-records, upsert-records), the search modes (vector / fulltext / hybrid), the schema-aware filter and return-field hints, the three transports, bearer auth, and the read-only flag. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8976014..298c712 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ | **Sessions** (`RedisWorkingMemorySessionService`) | `BaseSessionService` with auto-summarization and context-window management | Agent Memory Server (REST) | | **Long-term memory** (`RedisLongTermMemoryService`) | `BaseMemoryService` with semantic search and recency boosting | Agent Memory Server (REST) | | **Memory tools** (`SearchMemoryTool`, `CreateMemoryTool`, ...) | LLM-controlled memory operations | Agent Memory Server (REST) | -| **AMS MCP toolset** (`create_memory_mcp_toolset`) | Same memory tools surfaced over MCP/SSE | Agent Memory Server (MCP) | +| **AMS MCP toolset** (`create_memory_mcp_toolset`) | Exposes `search_long_term_memory`, `create_long_term_memories`, `edit_long_term_memory`, `delete_long_term_memories`, `get_long_term_memory`, `memory_prompt`, and `set_working_memory` over SSE | Agent Memory Server (MCP) | | **Search tools** (5 in-process tools) | Vector, hybrid, range, text, SQL search as `BaseTool` subclasses | RedisVL (Python) | -| **RedisVL MCP toolset** (`create_redisvl_mcp_toolset`) | One Redis index served to many agents over stdio / sse / streamable-http | `rvl mcp` server | +| **RedisVL MCP toolset** (`create_redisvl_mcp_toolset`) | Exposes `search-records` (vector / fulltext / hybrid, chosen per server) and `upsert-records` with schema-aware filter and return-field hints; transports: stdio / sse / streamable-http; supports bearer auth and `--read-only` | `rvl mcp` server | | **Semantic cache** (`RedisVLCacheProvider`, `LangCacheProvider`) | Skip repeat LLM calls and tool calls by semantic similarity | RedisVL `SemanticCache` or [Redis LangCache](https://redis.io/langcache) | --- From edb426b8148293e07d60dcd70305e2082d8fc496 Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Tue, 19 May 2026 20:35:34 -0400 Subject: [PATCH 5/8] docs: group MCP toolsets together in the Surface table Move the RedisVL MCP toolset row above the in-process search tools so the two MCP rows (AMS memory MCP, RedisVL search MCP) sit together. Reads more naturally and makes the MCP option visible before the in-process search alternative. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 298c712..70c697f 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ | **Long-term memory** (`RedisLongTermMemoryService`) | `BaseMemoryService` with semantic search and recency boosting | Agent Memory Server (REST) | | **Memory tools** (`SearchMemoryTool`, `CreateMemoryTool`, ...) | LLM-controlled memory operations | Agent Memory Server (REST) | | **AMS MCP toolset** (`create_memory_mcp_toolset`) | Exposes `search_long_term_memory`, `create_long_term_memories`, `edit_long_term_memory`, `delete_long_term_memories`, `get_long_term_memory`, `memory_prompt`, and `set_working_memory` over SSE | Agent Memory Server (MCP) | -| **Search tools** (5 in-process tools) | Vector, hybrid, range, text, SQL search as `BaseTool` subclasses | RedisVL (Python) | | **RedisVL MCP toolset** (`create_redisvl_mcp_toolset`) | Exposes `search-records` (vector / fulltext / hybrid, chosen per server) and `upsert-records` with schema-aware filter and return-field hints; transports: stdio / sse / streamable-http; supports bearer auth and `--read-only` | `rvl mcp` server | +| **Search tools** (5 in-process tools) | Vector, hybrid, range, text, SQL search as `BaseTool` subclasses | RedisVL (Python) | | **Semantic cache** (`RedisVLCacheProvider`, `LangCacheProvider`) | Skip repeat LLM calls and tool calls by semantic similarity | RedisVL `SemanticCache` or [Redis LangCache](https://redis.io/langcache) | --- From 8e82a08428efa7d9c0aae074492c042fc6341d76 Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Tue, 19 May 2026 20:37:44 -0400 Subject: [PATCH 6/8] docs: expand RedisVL MCP row with search capability bullets - Rename "RedisVL MCP toolset" -> "RedisVL MCP" in the surface row. - Expand the "What you get" cell into bullets covering tools exposed, the three search modes (vector / fulltext / hybrid), server-side embedding, schema-aware descriptions, JSON filter language, transports, and bearer auth. - Rename "Search tools" -> "Search tools with REST" for parallelism with the MCP rows. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 70c697f..bd4e27b 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ | **Long-term memory** (`RedisLongTermMemoryService`) | `BaseMemoryService` with semantic search and recency boosting | Agent Memory Server (REST) | | **Memory tools** (`SearchMemoryTool`, `CreateMemoryTool`, ...) | LLM-controlled memory operations | Agent Memory Server (REST) | | **AMS MCP toolset** (`create_memory_mcp_toolset`) | Exposes `search_long_term_memory`, `create_long_term_memories`, `edit_long_term_memory`, `delete_long_term_memories`, `get_long_term_memory`, `memory_prompt`, and `set_working_memory` over SSE | Agent Memory Server (MCP) | -| **RedisVL MCP toolset** (`create_redisvl_mcp_toolset`) | Exposes `search-records` (vector / fulltext / hybrid, chosen per server) and `upsert-records` with schema-aware filter and return-field hints; transports: stdio / sse / streamable-http; supports bearer auth and `--read-only` | `rvl mcp` server | -| **Search tools** (5 in-process tools) | Vector, hybrid, range, text, SQL search as `BaseTool` subclasses | RedisVL (Python) | +| **RedisVL MCP** (`create_redisvl_mcp_toolset`) |
  • Tools exposed: `search-records`, `upsert-records` (gate writes with `--read-only`).
  • Search modes (one per server, chosen via YAML): `vector` KNN, `fulltext` BM25, or `hybrid` (LINEAR or RRF fusion).
  • Server-side query embedding via a configured RedisVL vectorizer; agents never load one locally.
  • Schema-aware tool descriptions: filter and return-field hints derived from the bound `IndexSchema`.
  • JSON filter language with tag, text, and numeric operators (`eq`, `in`, `between`, `gt`, `lt`, `ne`).
  • Transports: stdio, sse, streamable-http. Bearer auth on HTTP. Pagination via `limit` / `offset`.
| `rvl mcp` server | +| **Search tools with REST** (5 in-process tools) | Vector, hybrid, range, text, SQL search as `BaseTool` subclasses | RedisVL (Python) | | **Semantic cache** (`RedisVLCacheProvider`, `LangCacheProvider`) | Skip repeat LLM calls and tool calls by semantic similarity | RedisVL `SemanticCache` or [Redis LangCache](https://redis.io/langcache) | --- From b747a5e03d3d291fc80c47be12b0e515de3f39cc Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Tue, 19 May 2026 22:32:52 -0400 Subject: [PATCH 7/8] refactor: drop create_redisvl_mcp_toolset; use native ADK McpToolset Per adk-docs PR #1777 review feedback (koverholt): every catalog MCP integration page wires `McpToolset` natively rather than through a third-party wrapper. The `create_redisvl_mcp_toolset(...)` helper saved about five lines of boilerplate (transport switch, bearer-header construction, --read-only default, tool-name constants), but it tells a different story than the rest of the catalog and the maintainer prefers the native pattern. Since 0.0.5 hasn't shipped to PyPI yet, removing the helper now is not a breaking change. Removed: - src/adk_redis/tools/mcp_search.py (170 LOC). - tests/tools/test_mcp_search.py (16 unit tests). - `adk-redis[mcp-search]` extra from pyproject.toml; updated `all`. - Re-exports of create_redisvl_mcp_toolset, ALL_REDISVL_MCP_TOOLS, REDISVL_MCP_TOOL_SEARCH, REDISVL_MCP_TOOL_UPSERT from `adk_redis` and `adk_redis.tools`. Rewritten to native McpToolset: - examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py now builds `McpToolset(connection_params=...)` directly, picking StdioConnectionParams (default) or StreamableHTTPConnectionParams based on env vars. Verified end-to-end: rvl mcp server returns correct hybrid-search hits ("Hybrid Search with FT.HYBRID" at score 1.31 for "FT.HYBRID command"). - tests/integration/test_adk_agent_registration.py now constructs McpToolset directly for both stdio and streamable-http registration smokes. Docs updated to remove every mention of the helper and to recommend native ADK MCP wiring: - README.md: Surface table row, Quick Start MCP snippet, Search Tools section, install extras, Requirements. - SKILL.md: When-to-use, install snippet, Core Pattern #4 (RedisVL MCP), Common Gotchas, Agent Execution Policy. - docs/concepts/search.md. - docs/user_guide/how_to_guides/search_tools.md: full rewrite of the MCP section + decision matrix. - docs/for-ais-only/REPOSITORY_MAP.md: drop mcp_search.py + tests. - docs/for-ais-only/FAILURE_MODES.md: replace "two MCP helpers" note with "one MCP helper, not two" explaining the design choice. - docs/llms.txt: replace "Prefer create_redisvl_mcp_toolset..." with "Use ADK's native McpToolset for RedisVL MCP". - examples/redisvl_mcp_search/README.md: rewrite to describe the native pattern. - CHANGELOG.md: walk back the "Added" entry; replace with the runnable example using the standard ADK pattern. Verified: - make check clean (format, lint, mypy). - 108 pytest pass (down from 124; 16 fewer for the deleted helper tests). - rvl mcp end-to-end: example agent's `_build_toolset()` connects, runs `search-records` with hybrid mode, returns expected results. --- CHANGELOG.md | 19 +- README.md | 53 ++++-- SKILL.md | 43 +++-- docs/concepts/search.md | 2 +- docs/for-ais-only/FAILURE_MODES.md | 16 +- docs/for-ais-only/REPOSITORY_MAP.md | 7 +- docs/llms.txt | 7 +- docs/user_guide/how_to_guides/search_tools.md | 58 +++--- examples/redisvl_mcp_search/README.md | 36 ++-- .../redisvl_mcp_search_agent/agent.py | 68 +++++-- pyproject.toml | 7 +- src/adk_redis/__init__.py | 8 - src/adk_redis/tools/__init__.py | 8 - src/adk_redis/tools/mcp_search.py | 171 ------------------ .../test_adk_agent_registration.py | 42 +++-- tests/tools/test_mcp_search.py | 170 ----------------- 16 files changed, 221 insertions(+), 494 deletions(-) delete mode 100644 src/adk_redis/tools/mcp_search.py delete mode 100644 tests/tools/test_mcp_search.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d8262a0..42af325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,14 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/). statements (with optional `params` for placeholders) against a bound Redis index. Installed via the new `adk-redis[sql]` extra (`redisvl[sql-redis]>=0.18.2`). -- `create_redisvl_mcp_toolset(...)` (`adk_redis.tools.mcp_search`): helper - that returns an ADK `McpToolset` wired to RedisVL's own MCP server - (`rvl mcp`). Supports `stdio`, `sse`, and `streamable-http` transports; - bearer auth on HTTP transports; `--read-only` default for stdio. - Installed via the new `adk-redis[mcp-search]` extra - (`redisvl[mcp]>=0.18.2`). -- Module constants `REDISVL_MCP_TOOL_SEARCH` and `REDISVL_MCP_TOOL_UPSERT` - for symbolic tool filtering. +- New `examples/redisvl_mcp_search/`: the MCP-path mirror of + `examples/redis_search_tools/`. Same knowledge-base corpus, served by + a `rvl mcp` server in hybrid (BM25 + vector) mode; the agent connects + via ADK's standard `McpToolset`. No adk-redis wrapper is needed; users + wire `StdioConnectionParams` / `SseConnectionParams` / + `StreamableHTTPConnectionParams` directly, matching the pattern used + by every catalog MCP integration page. - `make redis-up` / `make redis-down` / `make test-integration` targets for the new `tests/integration/` suite. Integration tests skip cleanly when no Redis with the RediSearch module is reachable at @@ -51,8 +50,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). (`tests/tools/test_vector_search.py::TestRedisVectorQueryConfigEpsilonRemoval`). - New cache provider tests (`tests/cache/test_provider.py`) including a no-`DeprecationWarning` assertion on the import path. -- New unit tests for `RedisSQLSearchTool` (`tests/tools/test_sql_search.py`) - and `create_redisvl_mcp_toolset` (`tests/tools/test_mcp_search.py`). +- New unit tests for `RedisSQLSearchTool` + (`tests/tools/test_sql_search.py`). - New integration suite under `tests/integration/` that round-trips vector, text, range, native hybrid, and SQL queries plus a cache round-trip against a real Redis 8.4 container, and confirms tools diff --git a/README.md b/README.md index bd4e27b..e3b9413 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ | **Long-term memory** (`RedisLongTermMemoryService`) | `BaseMemoryService` with semantic search and recency boosting | Agent Memory Server (REST) | | **Memory tools** (`SearchMemoryTool`, `CreateMemoryTool`, ...) | LLM-controlled memory operations | Agent Memory Server (REST) | | **AMS MCP toolset** (`create_memory_mcp_toolset`) | Exposes `search_long_term_memory`, `create_long_term_memories`, `edit_long_term_memory`, `delete_long_term_memories`, `get_long_term_memory`, `memory_prompt`, and `set_working_memory` over SSE | Agent Memory Server (MCP) | -| **RedisVL MCP** (`create_redisvl_mcp_toolset`) |
  • Tools exposed: `search-records`, `upsert-records` (gate writes with `--read-only`).
  • Search modes (one per server, chosen via YAML): `vector` KNN, `fulltext` BM25, or `hybrid` (LINEAR or RRF fusion).
  • Server-side query embedding via a configured RedisVL vectorizer; agents never load one locally.
  • Schema-aware tool descriptions: filter and return-field hints derived from the bound `IndexSchema`.
  • JSON filter language with tag, text, and numeric operators (`eq`, `in`, `between`, `gt`, `lt`, `ne`).
  • Transports: stdio, sse, streamable-http. Bearer auth on HTTP. Pagination via `limit` / `offset`.
| `rvl mcp` server | +| **RedisVL MCP** (native `McpToolset` against `rvl mcp`) |
  • Tools exposed: `search-records`, `upsert-records` (gate writes with `--read-only`).
  • Search modes (one per server, chosen via YAML): `vector` KNN, `fulltext` BM25, or `hybrid` (LINEAR or RRF fusion).
  • Server-side query embedding via a configured RedisVL vectorizer; agents never load one locally.
  • Schema-aware tool descriptions: filter and return-field hints derived from the bound `IndexSchema`.
  • JSON filter language with tag, text, and numeric operators (`eq`, `in`, `between`, `gt`, `lt`, `ne`).
  • Transports: stdio, sse, streamable-http. Bearer auth on HTTP. Pagination via `limit` / `offset`.
| `rvl mcp` server (`redisvl[mcp]`) | | **Search tools with REST** (5 in-process tools) | Vector, hybrid, range, text, SQL search as `BaseTool` subclasses | RedisVL (Python) | | **Semantic cache** (`RedisVLCacheProvider`, `LangCacheProvider`) | Skip repeat LLM calls and tool calls by semantic similarity | RedisVL `SemanticCache` or [Redis LangCache](https://redis.io/langcache) | @@ -49,10 +49,12 @@ Optional extras (combine as needed): pip install 'adk-redis[memory]' # sessions + long-term memory services pip install 'adk-redis[search]' # RedisVL-backed search tools pip install 'adk-redis[sql]' # RedisSQLSearchTool (sql-redis) -pip install 'adk-redis[mcp-search]' # create_redisvl_mcp_toolset helper pip install 'adk-redis[langcache]' # managed semantic cache provider pip install 'adk-redis[all]' # all of the above pip install 'adk-redis[all,examples]' # plus dotenv etc. for running examples + +# For the RedisVL MCP server (used with ADK's native McpToolset): +pip install 'redisvl[mcp]>=0.18.2' ``` ### Verify @@ -177,27 +179,40 @@ All five search tools accept custom `name` / `description` so the LLM sees a dom ### Search over a Redis index (MCP) -Run `rvl mcp --config mcp_config.yaml` separately and let the agent connect over MCP: +Run `rvl mcp --config mcp_config.yaml` separately, then connect the agent with ADK's standard `McpToolset`: ```python from google.adk import Agent -from pydantic import SecretStr - -from adk_redis import create_redisvl_mcp_toolset - -search_tools = create_redisvl_mcp_toolset( - url="http://localhost:8765/mcp", - auth_token=SecretStr("..."), # optional bearer - read_only=True, -) +from google.adk.tools.mcp_tool import McpToolset +from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams +from mcp import StdioServerParameters agent = Agent( model="gemini-2.5-flash", name="mcp_search_agent", - tools=[search_tools], + instruction="Use the search-records tool to answer questions.", + tools=[ + McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command="rvl", + args=[ + "mcp", + "--config", + "/path/to/mcp_config.yaml", + "--read-only", + ], + ), + timeout=30, + ), + tool_filter=["search-records"], + ), + ], ) ``` +For an already-running remote server, swap `StdioConnectionParams` for `StreamableHTTPConnectionParams(url="http://localhost:8765/mcp", headers={"Authorization": "Bearer ..."})`. + See [examples/redisvl_mcp_search/](examples/redisvl_mcp_search/) for a runnable demo (knowledge-base corpus, hybrid mode, paired with the in-process [examples/redis_search_tools/](examples/redis_search_tools/) example). ### Semantic cache @@ -243,7 +258,7 @@ Two parallel paths for RAG over a Redis index. Pick by deployment shape. | Path | Use when | |---|---| | **In-process** | Single ADK process, fast onboarding, Python-side `FilterExpression` composition, per-tool customization. | -| **MCP toolset** (`create_redisvl_mcp_toolset` against `rvl mcp`) | One Redis index served to multiple agents (Python, JS, Claude Desktop). Server-side `--read-only` / bearer auth. Schema-aware tool descriptions. | +| **MCP** (ADK's `McpToolset` against `rvl mcp`) | One Redis index served to multiple agents (Python, JS, Claude Desktop). Server-side `--read-only` / bearer auth. Schema-aware tool descriptions. | ### In-process tools @@ -257,14 +272,14 @@ Two parallel paths for RAG over a Redis index. Pick by deployment shape. All five accept any vectorizer supported by RedisVL (OpenAI, HuggingFace, Cohere, Mistral, Voyage AI, custom) and any `FilterExpression` from `redisvl.query.filter`. -### MCP toolset +### MCP -`create_redisvl_mcp_toolset(...)` returns an ADK `McpToolset` wired to a [RedisVL MCP server](https://docs.redisvl.com) (`rvl mcp`). The server is configured per index via YAML and exposes: +Use ADK's standard `McpToolset` against a running [RedisVL MCP server](https://docs.redisvl.com) (`rvl mcp`). The server is configured per index via YAML and exposes: - `search-records`: `vector`, `fulltext`, or `hybrid` (chosen at server start). Tool description includes filter and return-field hints derived from the bound index schema. - `upsert-records`: write path (suppress with `--read-only`). -Supports `stdio`, `sse`, and `streamable-http` transports; bearer auth on HTTP. Requires `adk-redis[mcp-search]` and a `rvl mcp` server. +Supports `stdio`, `sse`, and `streamable-http` transports; bearer auth on HTTP. Requires `redisvl[mcp]` and a `rvl mcp` server. See the [Quick Start MCP snippet](#search-over-a-redis-index-mcp) above for the wiring. For the full decision matrix and runnable demo, see [docs/user_guide/how_to_guides/search_tools.md](docs/user_guide/how_to_guides/search_tools.md). @@ -301,11 +316,11 @@ Both honor a configurable `distance_threshold` and per-entry `ttl`. - Python 3.10, 3.11, 3.12, or 3.13 - Google ADK 1.0+ (tested through 2.0 GA) -- RedisVL 0.18.2+ when the `search`, `langcache`, `sql`, or `mcp-search` extra is installed +- RedisVL 0.18.2+ when the `search`, `langcache`, or `sql` extra is installed - Redis 8.4+ (or Redis Cloud with Search) when using search tools or the cache providers - For session / memory services: a running [Agent Memory Server](https://github.com/redis/agent-memory-server) - For `RedisSQLSearchTool`: `sql-redis` (installed by `adk-redis[sql]`) -- For the RedisVL MCP toolset: `redisvl[mcp]` and the `rvl mcp` CLI (installed by `adk-redis[mcp-search]`) +- For the RedisVL MCP server: install `redisvl[mcp]>=0.18.2` and use the `rvl mcp` CLI; connect from ADK with `McpToolset` --- diff --git a/SKILL.md b/SKILL.md index bb100c3..52b176a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -27,9 +27,9 @@ links: hybrid, range, BM25 text, or SQL `SELECT` over a RedisVL index). - The user wants persistent ADK sessions or long-term memory and is willing to run [Redis Agent Memory Server](https://github.com/redis/agent-memory-server). -- The user wants to expose a Redis index to ADK via MCP, either through - the `rvl mcp` server (`create_redisvl_mcp_toolset`) or Agent Memory - Server's MCP endpoint (`create_memory_mcp_toolset`). +- The user wants to expose a Redis index to ADK via MCP. For the index + itself, point ADK's native `McpToolset` at a `rvl mcp` server. For + Agent Memory Server's MCP endpoint, use `create_memory_mcp_toolset`. - The user wants semantic caching for an ADK agent (self-hosted via RedisVL or managed via Redis LangCache). @@ -50,7 +50,6 @@ Optional extras (combine as needed): pip install 'adk-redis[memory]' # sessions + long-term memory services pip install 'adk-redis[search]' # RedisVL-backed search tools pip install 'adk-redis[sql]' # RedisSQLSearchTool (sql-redis) -pip install 'adk-redis[mcp-search]' # create_redisvl_mcp_toolset helper pip install 'adk-redis[langcache]' # managed semantic cache provider pip install 'adk-redis[all]' # all of the above ``` @@ -124,18 +123,25 @@ runner = Runner( ) ``` -### 4. RedisVL MCP toolset +### 4. RedisVL MCP (native McpToolset) ```python -from pydantic import SecretStr -from adk_redis import create_redisvl_mcp_toolset - -mcp_tools = create_redisvl_mcp_toolset( - url="http://localhost:8000/mcp", - auth_token=SecretStr("..."), - read_only=True, +from google.adk.tools.mcp_tool import McpToolset +from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams +from mcp import StdioServerParameters + +mcp_tools = McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command="rvl", + args=["mcp", "--config", "/path/to/mcp_config.yaml", "--read-only"], + ), + timeout=30, + ), + tool_filter=["search-records"], ) ``` +For a remote server, swap in `StreamableHTTPConnectionParams(url=..., headers={"Authorization": "Bearer ..."})`. ### 5. Semantic cache for LLM responses @@ -177,8 +183,10 @@ root_agent = Agent( (KNN). It lives on `RedisRangeQueryConfig`. - **Stopwords**: `RedisTextQueryConfig.stopwords` defaults to `"english"` which requires `nltk`. Set to `None` if `nltk` is unavailable. -- **MCP transports**: `create_redisvl_mcp_toolset` accepts only - `"stdio"`, `"sse"`, `"streamable-http"`; unknown values raise. +- **MCP transports**: ADK's `McpToolset` accepts + `StdioConnectionParams`, `SseConnectionParams`, or + `StreamableHTTPConnectionParams`. Pick the connection-params class + for your transport rather than passing a string. - **Vector dtype**: must match the index schema. Default is `"float32"`. - **Async loops**: the session service builds a new `MemoryAPIClient` per call to avoid event-loop bleed across `Runner.run` invocations; do not @@ -191,9 +199,10 @@ When this skill is loaded: 1. Confirm whether the user already has a Redis index. If not, walk them through `IndexSchema.from_yaml(...)` + `SearchIndex.create(overwrite=True)` before introducing any search tool. -2. Prefer the helper (`create_redisvl_mcp_toolset`) over hand-rolled - `StdioConnectionParams` for the RedisVL MCP path. The helper does - transport validation, bearer auth, and `--read-only` defaults. +2. 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 `--read-only` to the `rvl mcp` invocation + in stdio mode. 3. Never invent class or method names. Only those documented at `links.docs`. 4. For breaking-change questions, consult `CHANGELOG.md` in the repo. diff --git a/docs/concepts/search.md b/docs/concepts/search.md index c03d130..a385c7c 100644 --- a/docs/concepts/search.md +++ b/docs/concepts/search.md @@ -22,7 +22,7 @@ The search tools use RedisVL to perform vector similarity search over a Redis in | `redis_text_search` | Keyword full-text search via BM25 | | `redis_sql_search` | SQL `SELECT` against a bound index via `redisvl.query.SQLQuery`. Requires the `adk-redis[sql]` extra. | -In addition to the in-process Python tools, you can connect an agent to RedisVL's own MCP server (one index per server) with `create_redisvl_mcp_toolset(...)`. The server exposes schema-aware `search-records` and `upsert-records` tools and is useful when the same index needs to be served to multiple agents or non-Python clients. See the [search tools how-to](../user_guide/how_to_guides/search_tools.md) for the decision matrix. +In addition to the in-process Python tools, you can connect an agent to RedisVL's own MCP server (one index per server) using ADK's standard `McpToolset` pointed at a running `rvl mcp` instance. The server exposes schema-aware `search-records` and `upsert-records` tools and is useful when the same index needs to be served to multiple agents or non-Python clients. See the [search tools how-to](../user_guide/how_to_guides/search_tools.md) for the decision matrix and code samples. ## Indexing diff --git a/docs/for-ais-only/FAILURE_MODES.md b/docs/for-ais-only/FAILURE_MODES.md index 6f41f98..a3e8ff0 100644 --- a/docs/for-ais-only/FAILURE_MODES.md +++ b/docs/for-ais-only/FAILURE_MODES.md @@ -41,12 +41,16 @@ The `redisvl.extensions.llmcache` path still works but emits a to the old path; the regression test in `tests/cache/test_provider.py` asserts no `DeprecationWarning` fires. -## Two MCP toolset helpers exist on purpose - -`create_memory_mcp_toolset(...)` targets Agent Memory Server; it is the -memory surface. `create_redisvl_mcp_toolset(...)` targets RedisVL's own -MCP server (`rvl mcp`); it is the index/search surface. They are not -interchangeable. Do not merge them. +## One MCP helper, not two + +`create_memory_mcp_toolset(...)` exists for Agent Memory Server because +AMS's MCP URL has a non-trivial `/sse` suffix and the tool-name vocabulary +is bespoke. There is **no** matching helper for the RedisVL MCP server +(`rvl mcp`): users wire it with ADK's native `McpToolset` plus +`StdioConnectionParams` / `SseConnectionParams` / +`StreamableHTTPConnectionParams`. The maintainers chose this on purpose +to keep the MCP wiring story aligned with every other ADK catalog +integration. Do not reintroduce a `create_redisvl_mcp_toolset` wrapper. ## Two cache providers exist on purpose diff --git a/docs/for-ais-only/REPOSITORY_MAP.md b/docs/for-ais-only/REPOSITORY_MAP.md index 151c253..aa7fba9 100644 --- a/docs/for-ais-only/REPOSITORY_MAP.md +++ b/docs/for-ais-only/REPOSITORY_MAP.md @@ -35,9 +35,6 @@ src/adk_redis/ UpdateMemoryTool). mcp_memory.py MCP tool surface for the same memory operations (Agent Memory Server). - mcp_search.py create_redisvl_mcp_toolset(...) for RedisVL's - own MCP server (rvl mcp). Supports stdio, sse, - streamable-http; bearer auth on HTTP transports. cache/ __init__.py Re-exports the cache providers. _provider.py Provider protocol and base class. @@ -64,8 +61,6 @@ tests/ test_range_search.py RedisRangeSearchTool. test_text_search.py RedisTextSearchTool. test_sql_search.py RedisSQLSearchTool. - test_mcp_search.py create_redisvl_mcp_toolset (validation, - three transports, bearer auth, tool filter). cache/ test_provider.py RedisVLCacheProvider (incl. no-DeprecationWarning regression for the cache.llm import path). @@ -87,7 +82,7 @@ tests/ | Long-term memory + Memory Server proxy | `memory/long_term_memory.py`, `memory/_utils.py` | | ADK Memory tools (FunctionTool wrappers) | `tools/memory/` | | MCP memory tool surface (Agent Memory Server) | `tools/mcp_memory.py` | -| MCP search tool surface (RedisVL MCP server) | `tools/mcp_search.py` | +| MCP search (RedisVL `rvl mcp` server) | Use ADK's native `McpToolset` directly; no adk-redis wrapper. | | Vector / Hybrid / Range / Text / SQL search tools | `tools/search/` | | LLM and tool semantic caching | `cache/llm_cache.py`, `cache/tool_cache.py` | | Cache provider abstraction (RedisVL / LangCache) | `cache/_provider.py` | diff --git a/docs/llms.txt b/docs/llms.txt index 1281ed8..17d6e6a 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -56,9 +56,10 @@ 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. -- Prefer `create_redisvl_mcp_toolset(...)` over hand-rolled - `StdioConnectionParams`; the helper does transport validation and - bearer-auth wiring. +- 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 + `--read-only` to `rvl mcp` in stdio mode. - Set `stopwords=None` on text-search configs if `nltk` is not installed. - Pin `redisvl>=0.18.2` when documenting installs; the deprecated `redisvl.extensions.llmcache` path is on the way out. diff --git a/docs/user_guide/how_to_guides/search_tools.md b/docs/user_guide/how_to_guides/search_tools.md index f51895a..c4a2bbb 100644 --- a/docs/user_guide/how_to_guides/search_tools.md +++ b/docs/user_guide/how_to_guides/search_tools.md @@ -114,41 +114,57 @@ WHERE category = 'electronics' AND price < :max_price with `params={"max_price": 50}`. Install with `pip install 'adk-redis[sql]'`. -## RedisVL MCP toolset +## RedisVL MCP server -`create_redisvl_mcp_toolset(...)` returns an ADK `McpToolset` that talks to RedisVL's own MCP server (`rvl mcp`). The server exposes two tools per index: +Connect an ADK agent to RedisVL's own MCP server (`rvl mcp`) using ADK's standard `McpToolset`. The server exposes two tools per index: -- `search-records`: schema-aware search with filter hints embedded in the tool description. -- `upsert-records`: write path. Suppress with `read_only=True` (the default for stdio mode). +- `search-records`: schema-aware search (vector / fulltext / hybrid, chosen at server start). Filter and return-field hints come from the bound index schema. +- `upsert-records`: write path. Suppress with `--read-only` on the server, or with `tool_filter=["search-records"]` on the toolset. ```python -from pydantic import SecretStr -from adk_redis import create_redisvl_mcp_toolset - -# Remote server (default transport is streamable-http). -toolset = create_redisvl_mcp_toolset( - url="http://localhost:8000/mcp", - auth_token=SecretStr("..."), # optional bearer +from google.adk import Agent +from google.adk.tools.mcp_tool import McpToolset +from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams +from mcp import StdioServerParameters + +# In-process stdio: spawn `rvl mcp --config ` next to the agent. +agent = Agent( + model="gemini-2.5-flash", + name="redis_mcp_agent", + tools=[ + McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command="rvl", + args=[ + "mcp", + "--config", + "/etc/redisvl/mcp.yaml", + "--read-only", + ], + ), + timeout=30, + ), + tool_filter=["search-records"], + ), + ], ) +``` -# In-process stdio: spawn `rvl mcp --config `. -toolset = create_redisvl_mcp_toolset( - transport="stdio", - config_path="/etc/redisvl/mcp.yaml", - read_only=True, -) +For an already-running remote server, swap `StdioConnectionParams` for `StreamableHTTPConnectionParams(url=..., headers={"Authorization": "Bearer ..."})` (or `SseConnectionParams`). -agent = Agent(model="gemini-2.0-flash", tools=[toolset]) -``` +Install the MCP CLI with `pip install 'redisvl[mcp]>=0.18.2'` and start the server with `rvl mcp --config --transport streamable-http` (or `stdio`). -Install with `pip install 'adk-redis[mcp-search]'`. The server-side dependency is shipped by `redisvl[mcp]` and started with `rvl mcp --config --transport streamable-http`. +!!! note + For other ADK language SDKs (TypeScript, etc.), see + [Custom MCP Tools](https://adk.dev/tools-custom/mcp-tools/). ## When to use which | Path | Use when | |---|---| | In-process tools (`RedisVectorSearchTool`, etc.) | Single Python agent, fine-grained control, no extra service to operate. | -| `create_redisvl_mcp_toolset(...)` | Multiple agents (Python, JS, Claude Desktop) share one index, schema-aware tool descriptions, you want `--read-only` as a deployment-level guardrail. | +| `McpToolset` against `rvl mcp` | Multiple agents (Python, JS, Claude Desktop) share one index, schema-aware tool descriptions, you want `--read-only` as a deployment-level guardrail. | ## Notes diff --git a/examples/redisvl_mcp_search/README.md b/examples/redisvl_mcp_search/README.md index 425a782..5162d82 100644 --- a/examples/redisvl_mcp_search/README.md +++ b/examples/redisvl_mcp_search/README.md @@ -3,7 +3,8 @@ The **MCP-path mirror** of [`redis_search_tools/`](../redis_search_tools/). Same knowledge-base corpus, same kinds of prompts, but search is served by a separately-running `rvl mcp` server and the agent calls it via -`create_redisvl_mcp_toolset(...)` over MCP. +ADK's standard `McpToolset` over MCP. No adk-redis wrapper involved; +this is the same pattern every MCP integration in the ADK catalog uses. Use this example to compare the two deployment shapes side by side: @@ -19,8 +20,8 @@ Use this example to compare the two deployment shapes side by side: ## What this sample shows - Configuring `rvl mcp` for hybrid search via a YAML config. -- Connecting ADK to that server with `create_redisvl_mcp_toolset(...)` - over the `streamable-http` transport. +- Connecting ADK to that server with ADK's native `McpToolset` + one of + `StdioConnectionParams` / `StreamableHTTPConnectionParams`. - Using a `tool_filter` to expose only `search-records` (no upserts). - Reading the schema-aware tool description that RedisVL produces. @@ -30,9 +31,9 @@ Use this example to compare the two deployment shapes side by side: module enabled). Native `FT.HYBRID` requires 8.4+. 2. **A Gemini API key**. Get one at [aistudio.google.com](https://aistudio.google.com/app/apikey). -3. **The `mcp-search` extra** for the helper and the `rvl mcp` CLI; the - loader and the MCP server also need `sentence-transformers` (pulled - in by `redisvl`'s HuggingFace vectorizer dependency). +3. **`redisvl[mcp]>=0.18.2`** for the `rvl mcp` CLI, plus + `sentence-transformers` (the loader and the MCP server both embed + docs / queries with a HuggingFace vectorizer). ## Setup @@ -41,12 +42,9 @@ Use this example to compare the two deployment shapes side by side: From the repository root: ```bash -uv pip install 'adk-redis[mcp-search,examples]' +uv pip install 'adk-redis[examples]' 'redisvl[mcp]>=0.18.2' sentence-transformers ``` -This pulls in `redisvl[mcp]>=0.18.2` plus the `rvl mcp` CLI and the -FastMCP server. - ### 2. Start Redis 8.4 ```bash @@ -127,8 +125,10 @@ path (semantic similarity to the query embedding), then fuses with ## How it works -1. **Agent constructs an MCP toolset.** `create_redisvl_mcp_toolset(...)` - returns an `McpToolset` wired to the running `rvl mcp` server. +1. **Agent constructs an MCP toolset.** ADK's `McpToolset` is wired to + the running `rvl mcp` server with either `StdioConnectionParams` + (default in this example, spawns `rvl mcp --config `) or + `StreamableHTTPConnectionParams` (`REDISVL_MCP_URL` env var). `tool_filter=["search-records"]` hides `upsert-records` so the agent cannot write. 2. **Agent emits a query.** The LLM calls `search-records({"query": @@ -157,14 +157,10 @@ search: ### Add a bearer token -Run the server behind a proxy that injects auth, then: - -```bash -export REDISVL_MCP_AUTH_TOKEN=... -``` - -The agent's `auth_token` argument flows through as -`Authorization: Bearer ` on every MCP request. +Run the server behind a proxy that injects auth, then set +`REDISVL_MCP_URL` and `REDISVL_MCP_AUTH_TOKEN`. The example agent reads +both and attaches `Authorization: Bearer ` to every MCP request +via `StreamableHTTPConnectionParams(headers=...)`. ### Connect to Redis Cloud diff --git a/examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py b/examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py index 4150e1d..4a26761 100644 --- a/examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py +++ b/examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py @@ -16,9 +16,13 @@ The MCP-path mirror of `examples/redis_search_tools/`. It targets the same knowledge-base corpus but routes search through a separately-running -`rvl mcp` server via `create_redisvl_mcp_toolset(...)`. The server is +`rvl mcp` server via ADK's native ``McpToolset``. The server is configured for hybrid (BM25 + vector) search, so a single MCP tool covers both semantic and keyword retrieval. + +The agent does not depend on any adk-redis MCP wrapper; it uses the +standard ADK MCP pattern shown by every catalog integration page so the +same shape works against any MCP server. """ import os @@ -26,9 +30,12 @@ from dotenv import load_dotenv from google.adk import Agent -from pydantic import SecretStr - -from adk_redis import create_redisvl_mcp_toolset +from google.adk.tools.mcp_tool import McpToolset +from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams +from google.adk.tools.mcp_tool.mcp_session_manager import ( + StreamableHTTPConnectionParams, +) +from mcp import StdioServerParameters INSTRUCTION = """You are a helpful assistant with a technical knowledge base served via an MCP server. You have one tool: `search-records`, configured @@ -59,26 +66,52 @@ Difficulty levels: beginner, intermediate, advanced. """ +DEFAULT_MCP_CONFIG_PATH = str(Path(__file__).parent.parent / "mcp_config.yaml") + + +def _build_toolset() -> McpToolset: + """Pick stdio or streamable-http based on env vars. + + - If `REDISVL_MCP_URL` is set, connect to the running server over + streamable-http. Optional `REDISVL_MCP_AUTH_TOKEN` becomes a bearer + header. + - Otherwise, spawn `rvl mcp --config --read-only` over stdio. + `REDISVL_MCP_CONFIG` overrides the default config path. + """ + remote_url = os.getenv("REDISVL_MCP_URL") + if remote_url: + auth_token = os.getenv("REDISVL_MCP_AUTH_TOKEN") + headers = {"Authorization": f"Bearer {auth_token}"} if auth_token else None + return McpToolset( + connection_params=StreamableHTTPConnectionParams( + url=remote_url, + headers=headers, + timeout=30, + ), + tool_filter=["search-records"], + ) + + config_path = os.getenv("REDISVL_MCP_CONFIG", DEFAULT_MCP_CONFIG_PATH) + return McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command="rvl", + args=["mcp", "--config", config_path, "--read-only"], + ), + timeout=30, + ), + tool_filter=["search-records"], + ) + def create_agent() -> Agent: """Create the RedisVL MCP search agent.""" load_dotenv(Path(__file__).parent.parent / ".env") - - mcp_url = os.getenv("REDISVL_MCP_URL", "http://127.0.0.1:8765/mcp") - auth_token = os.getenv("REDISVL_MCP_AUTH_TOKEN") - - toolset = create_redisvl_mcp_toolset( - url=mcp_url, - transport="streamable-http", - auth_token=SecretStr(auth_token) if auth_token else None, - tool_filter=["search-records"], - ) - return Agent( model="gemini-2.5-flash", name="redisvl_mcp_search_agent", instruction=INSTRUCTION, - tools=[toolset], + tools=[_build_toolset()], ) @@ -87,5 +120,6 @@ def create_agent() -> Agent: if __name__ == "__main__": print( - f"Agent '{root_agent.name}' loaded with {len(root_agent.tools)} toolset(s)" + f"Agent '{root_agent.name}' loaded with" + f" {len(root_agent.tools)} toolset(s)" ) diff --git a/pyproject.toml b/pyproject.toml index 6169e4d..0f52b1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,11 +61,6 @@ sql = [ "redisvl[sql-redis]>=0.18.2", ] -# RedisVL MCP server toolset (create_redisvl_mcp_toolset) -mcp-search = [ - "redisvl[mcp]>=0.18.2", -] - # Example runner dependencies examples = [ "python-dotenv", @@ -73,7 +68,7 @@ examples = [ # All Redis integrations all = [ - "adk-redis[memory,search,langcache,sql,mcp-search]", + "adk-redis[memory,search,langcache,sql]", ] # Development dependencies diff --git a/src/adk_redis/__init__.py b/src/adk_redis/__init__.py index 1f13774..2d5d3e7 100644 --- a/src/adk_redis/__init__.py +++ b/src/adk_redis/__init__.py @@ -97,10 +97,6 @@ # Memory tools (MCP-based) from adk_redis.tools.mcp_memory import ALL_MCP_TOOLS from adk_redis.tools.mcp_memory import create_memory_mcp_toolset -from adk_redis.tools.mcp_search import ALL_REDISVL_MCP_TOOLS -from adk_redis.tools.mcp_search import REDISVL_MCP_TOOL_SEARCH -from adk_redis.tools.mcp_search import REDISVL_MCP_TOOL_UPSERT -from adk_redis.tools.mcp_search import create_redisvl_mcp_toolset # Semantic caching from adk_redis.cache import BaseCacheProvider from adk_redis.cache import CacheEntry @@ -150,10 +146,6 @@ # MCP tools "create_memory_mcp_toolset", "ALL_MCP_TOOLS", - "create_redisvl_mcp_toolset", - "ALL_REDISVL_MCP_TOOLS", - "REDISVL_MCP_TOOL_SEARCH", - "REDISVL_MCP_TOOL_UPSERT", # Semantic caching "BaseCacheProvider", "CacheEntry", diff --git a/src/adk_redis/tools/__init__.py b/src/adk_redis/tools/__init__.py index a0dc0e6..a1ae2ca 100644 --- a/src/adk_redis/tools/__init__.py +++ b/src/adk_redis/tools/__init__.py @@ -23,10 +23,6 @@ from adk_redis.tools.memory import UpdateMemoryTool from adk_redis.tools.mcp_memory import ALL_MCP_TOOLS from adk_redis.tools.mcp_memory import create_memory_mcp_toolset -from adk_redis.tools.mcp_search import ALL_REDISVL_MCP_TOOLS -from adk_redis.tools.mcp_search import REDISVL_MCP_TOOL_SEARCH -from adk_redis.tools.mcp_search import REDISVL_MCP_TOOL_UPSERT -from adk_redis.tools.mcp_search import create_redisvl_mcp_toolset from adk_redis.tools.search import BaseRedisSearchTool from adk_redis.tools.search import RedisAggregatedHybridQueryConfig from adk_redis.tools.search import RedisHybridQueryConfig @@ -67,8 +63,4 @@ # MCP tools "create_memory_mcp_toolset", "ALL_MCP_TOOLS", - "create_redisvl_mcp_toolset", - "ALL_REDISVL_MCP_TOOLS", - "REDISVL_MCP_TOOL_SEARCH", - "REDISVL_MCP_TOOL_UPSERT", ] diff --git a/src/adk_redis/tools/mcp_search.py b/src/adk_redis/tools/mcp_search.py deleted file mode 100644 index c1dbd7f..0000000 --- a/src/adk_redis/tools/mcp_search.py +++ /dev/null @@ -1,171 +0,0 @@ -# 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. - -"""Helper for binding ADK to a RedisVL MCP server (`rvl mcp`). - -This module exposes ``create_redisvl_mcp_toolset(...)``, which returns an -``McpToolset`` wired to a RedisVL MCP server. The server is shipped by the -``redisvl[mcp]`` extra and exposes index-aware ``search-records`` and -``upsert-records`` tools whose descriptions include filter and return-field -hints derived from the bound index schema. - -Three transport modes are supported: - -- ``stdio``: spawn ``rvl mcp --config `` in-process. Pass ``config_path``. -- ``streamable-http``: connect to a remote server. Pass ``url``. (default) -- ``sse``: connect to a remote SSE server. Pass ``url``. - -Read-only mode is on by default and is the safer choice for agents that -should not write. -""" - -from __future__ import annotations - -from typing import Any, Literal, TYPE_CHECKING - -from pydantic import SecretStr - -if TYPE_CHECKING: - from google.adk.tools.mcp_tool import McpToolset - - -REDISVL_MCP_TOOL_SEARCH = "search-records" -REDISVL_MCP_TOOL_UPSERT = "upsert-records" - -ALL_REDISVL_MCP_TOOLS = [ - REDISVL_MCP_TOOL_SEARCH, - REDISVL_MCP_TOOL_UPSERT, -] - - -def create_redisvl_mcp_toolset( - *, - url: str | None = None, - config_path: str | None = None, - transport: Literal["stdio", "sse", "streamable-http"] = "streamable-http", - read_only: bool = True, - auth_token: SecretStr | None = None, - tool_filter: list[str] | None = None, - timeout: float = 5.0, -) -> "McpToolset": - """Create an MCP toolset pointed at a RedisVL MCP server. - - Args: - url: URL of a running RedisVL MCP server. Required for ``sse`` and - ``streamable-http`` transports. Mutually exclusive with - ``config_path``. - config_path: Path to a RedisVL MCP YAML config. When set, the helper - spawns ``rvl mcp --config `` over stdio. Required for the - ``stdio`` transport. Mutually exclusive with ``url``. - transport: Transport to use. Defaults to ``streamable-http``. - read_only: Whether to pass ``--read-only`` to the spawned server. - Only relevant in stdio mode. Default ``True``. - auth_token: Optional bearer token for HTTP transports. Sent as - ``Authorization: Bearer ``. - tool_filter: Optional list of MCP tool names to expose. Use - ``REDISVL_MCP_TOOL_SEARCH`` / ``REDISVL_MCP_TOOL_UPSERT`` for - symbolic filtering. - timeout: Connection timeout in seconds. - - Returns: - A configured ``McpToolset``. - - Raises: - ValueError: If ``url`` and ``config_path`` are both set or both unset, - or if a transport / param combination is invalid. - ImportError: If ``google-adk`` was installed without MCP support. - - Example: - ```python - from google.adk import Agent - from adk_redis.tools.mcp_search import create_redisvl_mcp_toolset - - # Remote server, read-only. - toolset = create_redisvl_mcp_toolset( - url="http://localhost:8000/mcp", - ) - - # Local in-process stdio. - toolset = create_redisvl_mcp_toolset( - transport="stdio", - config_path="/etc/redisvl/mcp.yaml", - ) - - agent = Agent(model="gemini-2.5-flash", tools=[toolset]) - ``` - """ - _VALID_TRANSPORTS = ("stdio", "sse", "streamable-http") - if transport not in _VALID_TRANSPORTS: - raise ValueError( - f"Unknown transport {transport!r}. " - f"Expected one of: {', '.join(_VALID_TRANSPORTS)}." - ) - if url is None and config_path is None: - raise ValueError( - "create_redisvl_mcp_toolset requires either url or config_path." - ) - if url is not None and config_path is not None: - raise ValueError( - "url and config_path are mutually exclusive: stdio uses config_path," - " HTTP/SSE transports use url." - ) - if transport == "stdio" and config_path is None: - raise ValueError("stdio transport requires config_path.") - if transport in ("sse", "streamable-http") and url is None: - raise ValueError(f"{transport} transport requires url.") - - try: - from google.adk.tools.mcp_tool import McpToolset - from google.adk.tools.mcp_tool.mcp_session_manager import ( - SseConnectionParams, - ) - from google.adk.tools.mcp_tool.mcp_session_manager import ( - StdioConnectionParams, - ) - from google.adk.tools.mcp_tool.mcp_session_manager import ( - StreamableHTTPConnectionParams, - ) - from mcp import StdioServerParameters - except ImportError as e: - raise ImportError( - "google-adk with MCP support is required. Install it with: " - "pip install 'google-adk[mcp]'" - ) from e - - connection_params: Any - if transport == "stdio": - args = ["mcp", "--config", str(config_path)] - if read_only: - args.append("--read-only") - connection_params = StdioConnectionParams( - server_params=StdioServerParameters(command="rvl", args=args), - timeout=timeout, - ) - else: - headers: dict[str, str] | None = None - if auth_token is not None: - headers = {"Authorization": f"Bearer {auth_token.get_secret_value()}"} - if transport == "sse": - connection_params = SseConnectionParams( - url=str(url), headers=headers, timeout=timeout - ) - else: - connection_params = StreamableHTTPConnectionParams( - url=str(url), headers=headers, timeout=timeout - ) - - return McpToolset( - connection_params=connection_params, - tool_filter=tool_filter, - ) diff --git a/tests/integration/test_adk_agent_registration.py b/tests/integration/test_adk_agent_registration.py index d636a2b..87b8f07 100644 --- a/tests/integration/test_adk_agent_registration.py +++ b/tests/integration/test_adk_agent_registration.py @@ -14,9 +14,10 @@ """End-to-end ADK Agent registration tests. -These tests confirm the new and existing search tools register cleanly -with ``google.adk.Agent`` and surface a usable ``FunctionDeclaration``. -They do not call any LLM, so they run without API keys. +These tests confirm search tools register cleanly with +``google.adk.Agent`` and surface a usable ``FunctionDeclaration``. They +also confirm a native ADK ``McpToolset`` pointed at the RedisVL MCP +server attaches to an Agent. No LLM calls; runs without API keys. """ from __future__ import annotations @@ -30,9 +31,14 @@ from google.adk import Agent from google.adk.agents.readonly_context import ReadonlyContext +from google.adk.tools.mcp_tool import McpToolset +from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams +from google.adk.tools.mcp_tool.mcp_session_manager import ( + StreamableHTTPConnectionParams, +) +from mcp import StdioServerParameters from redisvl.index import SearchIndex -from adk_redis import create_redisvl_mcp_toolset from adk_redis import RedisSQLSearchTool from adk_redis import RedisTextQueryConfig from adk_redis import RedisTextSearchTool @@ -77,23 +83,37 @@ async def test_text_search_tool_registers(self): class TestRedisVLMcpToolsetRegistersWithAgent: - """The MCP toolset registers as an Agent tool source.""" + """A native ADK McpToolset against rvl mcp attaches to an Agent.""" def test_streamable_http_toolset_registers(self): - toolset = create_redisvl_mcp_toolset(url="http://localhost:8000/mcp") + toolset = McpToolset( + connection_params=StreamableHTTPConnectionParams( + url="http://localhost:8000/mcp", + ), + tool_filter=["search-records"], + ) agent = Agent( model="gemini-2.5-flash", name="test_agent", tools=[toolset], ) - # The toolset is held on the agent (no LLM dispatch required). assert toolset in agent.tools def test_stdio_toolset_registers(self): - toolset = create_redisvl_mcp_toolset( - transport="stdio", - config_path="/etc/redisvl/mcp.yaml", - read_only=True, + toolset = McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command="rvl", + args=[ + "mcp", + "--config", + "/etc/redisvl/mcp.yaml", + "--read-only", + ], + ), + timeout=30, + ), + tool_filter=["search-records"], ) agent = Agent( model="gemini-2.5-flash", diff --git a/tests/tools/test_mcp_search.py b/tests/tools/test_mcp_search.py deleted file mode 100644 index 2968337..0000000 --- a/tests/tools/test_mcp_search.py +++ /dev/null @@ -1,170 +0,0 @@ -# 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 create_redisvl_mcp_toolset.""" - -from __future__ import annotations - -from pydantic import SecretStr -import pytest - -pytest.importorskip("google.adk.tools.mcp_tool") - -from google.adk.tools.mcp_tool import McpToolset -from google.adk.tools.mcp_tool.mcp_session_manager import SseConnectionParams -from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams -from google.adk.tools.mcp_tool.mcp_session_manager import ( - StreamableHTTPConnectionParams, -) - -from adk_redis.tools.mcp_search import create_redisvl_mcp_toolset -from adk_redis.tools.mcp_search import REDISVL_MCP_TOOL_SEARCH -from adk_redis.tools.mcp_search import REDISVL_MCP_TOOL_UPSERT - - -class TestCreateRedisVLMcpToolsetValidation: - """Argument validation.""" - - def test_requires_url_or_config_path(self): - with pytest.raises(ValueError, match="url.*config_path"): - create_redisvl_mcp_toolset() - - def test_url_and_config_path_are_mutually_exclusive(self): - with pytest.raises(ValueError, match="mutually exclusive"): - create_redisvl_mcp_toolset( - url="http://localhost:8000", - config_path="/etc/redisvl.yaml", - ) - - def test_stdio_requires_config_path(self): - with pytest.raises(ValueError, match="config_path"): - create_redisvl_mcp_toolset(transport="stdio", url="http://x") - - def test_url_transports_reject_config_path(self): - with pytest.raises(ValueError): - create_redisvl_mcp_toolset( - transport="sse", config_path="/etc/redisvl.yaml" - ) - - def test_unknown_transport_raises_value_error(self): - """Regression: typo in `transport` must fail loudly, not silently fall through.""" - with pytest.raises(ValueError, match="transport"): - create_redisvl_mcp_toolset( - url="http://localhost:8000/mcp", - transport="stdioo", # type: ignore[arg-type] - ) - - -class TestCreateRedisVLMcpToolsetStdio: - """Stdio transport: spawn `rvl mcp --config `.""" - - def test_stdio_returns_mcp_toolset(self): - toolset = create_redisvl_mcp_toolset( - transport="stdio", - config_path="/etc/redisvl.yaml", - ) - assert isinstance(toolset, McpToolset) - - def test_stdio_connection_params_shape(self): - toolset = create_redisvl_mcp_toolset( - transport="stdio", - config_path="/etc/redisvl.yaml", - ) - params = toolset._connection_params - assert isinstance(params, StdioConnectionParams) - assert params.server_params.command == "rvl" - assert "mcp" in params.server_params.args - assert "--config" in params.server_params.args - assert "/etc/redisvl.yaml" in params.server_params.args - - def test_stdio_read_only_flag_propagates(self): - toolset = create_redisvl_mcp_toolset( - transport="stdio", - config_path="/etc/redisvl.yaml", - read_only=True, - ) - params = toolset._connection_params - assert "--read-only" in params.server_params.args - - def test_stdio_no_read_only_when_false(self): - toolset = create_redisvl_mcp_toolset( - transport="stdio", - config_path="/etc/redisvl.yaml", - read_only=False, - ) - params = toolset._connection_params - assert "--read-only" not in params.server_params.args - - -class TestCreateRedisVLMcpToolsetStreamableHttp: - """Streamable-HTTP transport: connect to a remote server.""" - - def test_streamable_http_default(self): - toolset = create_redisvl_mcp_toolset(url="http://localhost:8000/mcp") - params = toolset._connection_params - assert isinstance(params, StreamableHTTPConnectionParams) - assert params.url == "http://localhost:8000/mcp" - - def test_streamable_http_bearer_token_in_headers(self): - toolset = create_redisvl_mcp_toolset( - url="http://localhost:8000/mcp", - auth_token=SecretStr("s3cret"), - ) - params = toolset._connection_params - assert params.headers is not None - assert params.headers.get("Authorization") == "Bearer s3cret" - - def test_streamable_http_no_headers_without_token(self): - toolset = create_redisvl_mcp_toolset(url="http://localhost:8000/mcp") - params = toolset._connection_params - assert params.headers is None or "Authorization" not in ( - params.headers or {} - ) - - -class TestCreateRedisVLMcpToolsetSse: - """SSE transport: connect to a remote SSE server.""" - - def test_sse_returns_correct_params(self): - toolset = create_redisvl_mcp_toolset( - url="http://localhost:8000/sse", - transport="sse", - ) - params = toolset._connection_params - assert isinstance(params, SseConnectionParams) - assert params.url == "http://localhost:8000/sse" - - def test_sse_bearer_token_in_headers(self): - toolset = create_redisvl_mcp_toolset( - url="http://localhost:8000/sse", - transport="sse", - auth_token=SecretStr("t"), - ) - params = toolset._connection_params - assert params.headers.get("Authorization") == "Bearer t" - - -class TestCreateRedisVLMcpToolsetFilterAndConstants: - """Tool filter and exported tool-name constants.""" - - def test_tool_filter_passthrough(self): - toolset = create_redisvl_mcp_toolset( - url="http://localhost:8000/mcp", - tool_filter=[REDISVL_MCP_TOOL_SEARCH], - ) - assert toolset.tool_filter == [REDISVL_MCP_TOOL_SEARCH] - - def test_exports_known_tool_constants(self): - assert REDISVL_MCP_TOOL_SEARCH == "search-records" - assert REDISVL_MCP_TOOL_UPSERT == "upsert-records" From 362444bff2f5f57cdba97ac0329c1cc68f0f6fe5 Mon Sep 17 00:00:00 2001 From: Nitin Kanukolanu Date: Tue, 19 May 2026 22:47:22 -0400 Subject: [PATCH 8/8] fix(ci): gate MCP integration tests on import availability; doc nits CI failure on Python 3.11 was caused by my module-level imports in tests/integration/test_adk_agent_registration.py: from google.adk.tools.mcp_tool import McpToolset google-adk's `mcp_tool/__init__.py` wraps every MCP symbol in a single try/except that silently swallows ImportError. On installs where any nested import fails (e.g., the `mcp` package isn't fully wired on a specific Python version), the module ends up empty and pytest fails to collect the test file. Fix: move the MCP imports into a try/except in the test module and gate the `TestRedisVLMcpToolsetRegistersWithAgent` class with `@pytest.mark.skipif(not _MCP_AVAILABLE, ...)`. Search-tool tests in the same file stay un-gated; they don't depend on MCP. PR review (Copilot) drive-by fixes also addressed in the same commit: - README.md: `host.docker.internal` quick-start note now flags that Linux needs `--network=host` or the Docker-bridge gateway IP. - README.md: Helpful links list em dashes replaced with colons (project style forbids em dashes in prose). - examples/redisvl_mcp_search/{README,load_data,agent,schema}: soften the "same knowledge-base corpus" language to "counterpart" / "overlapping with MCP-specific docs added", since the MCP loader curates the corpus differently from redis_search_tools/. Verified locally with the exact CI command: uv run pytest tests -v --cov=adk_redis --cov-report=xml 108 tests pass, coverage.xml emitted, make check clean. --- README.md | 20 ++++++------- examples/redisvl_mcp_search/README.md | 15 ++++++---- examples/redisvl_mcp_search/load_data.py | 15 +++++----- .../redisvl_mcp_search_agent/agent.py | 11 ++++---- examples/redisvl_mcp_search/schema.yaml | 9 +++--- .../test_adk_agent_registration.py | 28 +++++++++++++++---- 6 files changed, 61 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index e3b9413..b5a319d 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ uv sync --all-extras redislabs/agent-memory-server:0.13.2 \ agent-memory api --host 0.0.0.0 --port 8088 --task-backend=asyncio ``` - AMS supports 100+ LLM and embedding providers via [LiteLLM](https://docs.litellm.ai/). See [Agent Memory Server setup](docs/user_guide/how_to_guides/memory_server_setup.md) for the full configuration matrix. + On Linux, `host.docker.internal` is not routable by default; use `--network=host` and `REDIS_URL=redis://127.0.0.1:6379`, or set `REDIS_URL` to the Docker-bridge gateway (typically `redis://172.17.0.1:6379`). AMS supports 100+ LLM and embedding providers via [LiteLLM](https://docs.litellm.ai/). See [Agent Memory Server setup](docs/user_guide/how_to_guides/memory_server_setup.md) for the full configuration matrix. For Redis Cloud, Redis Enterprise, or troubleshooting, see [Redis setup](docs/user_guide/how_to_guides/redis_setup.md). @@ -389,12 +389,12 @@ Apache 2.0. See [LICENSE](LICENSE). ## Helpful links -- [PyPI](https://pypi.org/project/adk-redis/) — install with `pip install adk-redis` -- [Redis setup](docs/user_guide/how_to_guides/redis_setup.md) — local, Docker, and Redis Cloud -- [Agent Memory Server setup](docs/user_guide/how_to_guides/memory_server_setup.md) — full AMS configuration -- [Integration walkthrough](docs/user_guide/01_integration.md) — end-to-end wiring -- [Search tools guide](docs/user_guide/how_to_guides/search_tools.md) — in-process vs MCP, decision matrix -- [Google ADK](https://github.com/google/adk-python) — agent framework -- [Agent Memory Server](https://github.com/redis/agent-memory-server) — memory backend -- [RedisVL](https://docs.redisvl.com/) — Redis Vector Library -- [Redis LangCache](https://redis.io/langcache) — managed semantic cache +- [PyPI](https://pypi.org/project/adk-redis/): install with `pip install adk-redis` +- [Redis setup](docs/user_guide/how_to_guides/redis_setup.md): local, Docker, and Redis Cloud +- [Agent Memory Server setup](docs/user_guide/how_to_guides/memory_server_setup.md): full AMS configuration +- [Integration walkthrough](docs/user_guide/01_integration.md): end-to-end wiring +- [Search tools guide](docs/user_guide/how_to_guides/search_tools.md): in-process vs MCP, decision matrix +- [Google ADK](https://github.com/google/adk-python): agent framework +- [Agent Memory Server](https://github.com/redis/agent-memory-server): memory backend +- [RedisVL](https://docs.redisvl.com/): Redis Vector Library +- [Redis LangCache](https://redis.io/langcache): managed semantic cache diff --git a/examples/redisvl_mcp_search/README.md b/examples/redisvl_mcp_search/README.md index 5162d82..617d32d 100644 --- a/examples/redisvl_mcp_search/README.md +++ b/examples/redisvl_mcp_search/README.md @@ -1,10 +1,15 @@ # RedisVL MCP Search Agent -The **MCP-path mirror** of [`redis_search_tools/`](../redis_search_tools/). -Same knowledge-base corpus, same kinds of prompts, but search is served -by a separately-running `rvl mcp` server and the agent calls it via -ADK's standard `McpToolset` over MCP. No adk-redis wrapper involved; -this is the same pattern every MCP integration in the ADK catalog uses. +The **MCP-path counterpart** of [`redis_search_tools/`](../redis_search_tools/). +A similar Redis knowledge-base corpus and the same kinds of prompts, but +search is served by a separately-running `rvl mcp` server and the agent +calls it via ADK's standard `McpToolset` over MCP. No adk-redis wrapper +involved; this is the same pattern every MCP integration in the ADK +catalog uses. + +(The MCP corpus is curated for hybrid demos and includes a couple of +MCP-specific articles, so the dataset is overlapping rather than +identical with `redis_search_tools/load_data.py`.) Use this example to compare the two deployment shapes side by side: diff --git a/examples/redisvl_mcp_search/load_data.py b/examples/redisvl_mcp_search/load_data.py index 60334f8..2eb9ee7 100644 --- a/examples/redisvl_mcp_search/load_data.py +++ b/examples/redisvl_mcp_search/load_data.py @@ -12,13 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Load the same knowledge base as redis_search_tools, but for MCP search. - -The corpus mirrors `examples/redis_search_tools/load_data.py` so the -in-process and MCP demos answer the same questions on the same data. -Documents are embedded with `redis/langcache-embed-v2` (768 dims) so the -configured `rvl mcp` server can run vector or hybrid search against -them. +"""Load a Redis knowledge base for the MCP-path search demo. + +The corpus is curated for the MCP demo and overlaps with +`examples/redis_search_tools/load_data.py` so the in-process and MCP +demos answer similar questions, with MCP-specific docs added here +(e.g., the "RedisVL MCP Server" entry). Documents are embedded with +`redis/langcache-embed-v2` (768 dims) so the configured `rvl mcp` +server can run vector or hybrid search against them. """ import os diff --git a/examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py b/examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py index 4a26761..0b8fe27 100644 --- a/examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py +++ b/examples/redisvl_mcp_search/redisvl_mcp_search_agent/agent.py @@ -14,11 +14,12 @@ """RedisVL MCP search agent. -The MCP-path mirror of `examples/redis_search_tools/`. It targets the -same knowledge-base corpus but routes search through a separately-running -`rvl mcp` server via ADK's native ``McpToolset``. The server is -configured for hybrid (BM25 + vector) search, so a single MCP tool -covers both semantic and keyword retrieval. +The MCP-path counterpart of `examples/redis_search_tools/`. It targets +a similar Redis knowledge-base corpus (overlapping, with MCP-specific +docs added in the loader) and routes search through a +separately-running `rvl mcp` server via ADK's native ``McpToolset``. +The server is configured for hybrid (BM25 + vector) search, so a +single MCP tool covers both semantic and keyword retrieval. The agent does not depend on any adk-redis MCP wrapper; it uses the standard ADK MCP pattern shown by every catalog integration page so the diff --git a/examples/redisvl_mcp_search/schema.yaml b/examples/redisvl_mcp_search/schema.yaml index 8c31f15..1366610 100644 --- a/examples/redisvl_mcp_search/schema.yaml +++ b/examples/redisvl_mcp_search/schema.yaml @@ -14,10 +14,11 @@ # Redis index schema for the redisvl_mcp_search sample. # -# Mirrors `examples/redis_search_tools/schema.yaml` so users can compare -# the in-process Python tool path with the MCP toolset path on the same -# knowledge base. The `rvl mcp` server reads the live index for its -# search-records description; the schema below shapes those hints. +# Parallels `examples/redis_search_tools/schema.yaml` so users can +# compare the in-process Python tool path with the MCP toolset path on +# a similar (overlapping) knowledge base. The `rvl mcp` server reads +# the live index for its search-records description; the schema below +# shapes those hints. version: "0.1.0" diff --git a/tests/integration/test_adk_agent_registration.py b/tests/integration/test_adk_agent_registration.py index 87b8f07..07343f5 100644 --- a/tests/integration/test_adk_agent_registration.py +++ b/tests/integration/test_adk_agent_registration.py @@ -31,18 +31,30 @@ from google.adk import Agent from google.adk.agents.readonly_context import ReadonlyContext -from google.adk.tools.mcp_tool import McpToolset -from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams -from google.adk.tools.mcp_tool.mcp_session_manager import ( - StreamableHTTPConnectionParams, -) -from mcp import StdioServerParameters from redisvl.index import SearchIndex from adk_redis import RedisSQLSearchTool from adk_redis import RedisTextQueryConfig from adk_redis import RedisTextSearchTool +# google-adk exposes its MCP surface from google.adk.tools.mcp_tool only when +# the optional `mcp` dependency is importable. The package's __init__.py +# silently swallows ImportError on partial installs, so test it by attempting +# the actual symbol import; gate the MCP-specific tests on that. +try: + from google.adk.tools.mcp_tool import McpToolset + from google.adk.tools.mcp_tool.mcp_session_manager import ( + StdioConnectionParams, + ) + from google.adk.tools.mcp_tool.mcp_session_manager import ( + StreamableHTTPConnectionParams, + ) + from mcp import StdioServerParameters + + _MCP_AVAILABLE = True +except ImportError: + _MCP_AVAILABLE = False + class TestSearchToolsRegisterWithAgent: """Agent.canonical_tools surfaces every search tool it was handed.""" @@ -82,6 +94,10 @@ async def test_text_search_tool_registers(self): assert "redis_text_search" in names +@pytest.mark.skipif( + not _MCP_AVAILABLE, + reason="google-adk MCP support not available in this install", +) class TestRedisVLMcpToolsetRegistersWithAgent: """A native ADK McpToolset against rvl mcp attaches to an Agent."""