-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat(memory): Added OpenMemoryService for self-hosted memory integration #3387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Introduced OpenMemoryService and OpenMemoryServiceConfig to the memory module. - Updated pyproject.toml to include httpx as a dependency with version constraints. - Added import error handling for OpenMemoryService, providing guidance for installation if httpx is not present. This enhancement allows for improved memory management capabilities within the application.
… in OpenMemoryService - Changed the timestamp assignment to use event.timestamp instead of a formatted string. - Refactored the memory addition logic to ensure user_id is included as a top-level field in the payload for better server-side filtering. - Improved error handling and logging during memory addition for clarity and debugging purposes. These changes enhance the accuracy of memory data and improve the overall efficiency of the memory service operations.
…tions - Refactored the memory addition process in OpenMemoryService to create the HTTP client once for all events, enhancing performance. - Updated the agent's instruction format for better readability and maintainability. - Changed import statements in the README to reflect the updated Runner class. These changes optimize the memory service operations and improve the clarity of the agent's instructions.
- Updated the agent's instructions to include connection details for OpenMemory, allowing users to connect using various URI formats. - Implemented an `openmemory_factory` method to create OpenMemoryService instances from specified URIs, enhancing flexibility in service registration. - Added a no-op `close` method in OpenMemoryService for API consistency. These changes improve the usability and configurability of the OpenMemory service within the application.
- Improved the URI parsing logic in the _register_builtin_services function to support various OpenMemory URI formats, including handling cases where the netloc is a scheme. - Updated comments for clarity on how different URI formats are processed, ensuring better understanding and maintainability of the code. These changes enhance the flexibility and robustness of service registration for OpenMemory connections.
Summary of ChangesHello @rakshith-git, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a new memory service to the ADK framework, enabling seamless integration with OpenMemory, a self-hosted and open-source AI memory system. This enhancement addresses the need for on-premise, cost-effective, and data-sovereign memory solutions, providing users with greater control and flexibility beyond cloud-dependent services. The implementation includes a dedicated service, CLI support, and a comprehensive sample to facilitate adoption. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This PR introduces a new memory service for OpenMemory, which is a great addition for users looking for self-hosted options. The implementation is well-structured, using httpx for direct API communication and providing a clear configuration model. The CLI integration via a custom URI scheme is consistent with existing services in ADK. The inclusion of a sample agent and comprehensive unit tests is also excellent.
My review focuses on improving exception handling, simplifying some complex logic, and fixing a few critical issues in the unit tests to ensure they correctly validate the implementation. Overall, this is a high-quality contribution.
| except Exception as e: | ||
| logger.error("Failed to add memory for event %s: %s", event.id, e) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Catching a generic Exception is too broad and can mask underlying issues or swallow exceptions that should be propagated. It's better to catch more specific exceptions related to the HTTP requests, such as httpx.HTTPStatusError for bad responses (4xx, 5xx) and httpx.RequestError for network-level problems. This provides better error handling and debuggability.
except httpx.HTTPStatusError as e:
logger.error(
"Failed to add memory for event %s due to HTTP error: %s - %s",
event.id,
e.response.status_code,
e.response.text,
)
except httpx.RequestError as e:
logger.error(
"Failed to add memory for event %s due to request error: %s", event.id, e
)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add_session_to_memory method (lines 223-235):
Added specific httpx.HTTPStatusError handler with detailed error logging (status code + response text)
Added specific httpx.RequestError handler for network-level issues
Kept generic Exception handler as fallback for unexpected errors
| except Exception as e: | ||
| logger.error("Failed to search memories: %s", e) | ||
| return SearchMemoryResponse(memories=[]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the add_session_to_memory method, catching a generic Exception here is too broad. It's better to handle specific httpx exceptions to distinguish between HTTP errors and other network issues. This will improve error reporting and make debugging easier.
except httpx.HTTPStatusError as e:
logger.error(
"Failed to search memories due to HTTP error: %s - %s",
e.response.status_code,
e.response.text,
)
return SearchMemoryResponse(memories=[])
except httpx.RequestError as e:
logger.error("Failed to search memories due to request error: %s", e)
return SearchMemoryResponse(memories=[])There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
search_memory method (lines 369-381):
Same pattern: specific httpx.HTTPStatusError and httpx.RequestError handlers
More informative error messages with status codes
Generic Exception handler as fallback
| # Extract base URL | ||
| # Handle different URI formats: | ||
| # - openmemory://localhost:3000 -> http://localhost:3000 | ||
| # - openmemory://https://example.com -> https://example.com | ||
| # - openmemory://http://localhost:3000 -> http://localhost:3000 | ||
| netloc = parsed.netloc or "" | ||
| path = parsed.path | ||
|
|
||
| # Check if netloc is a scheme (e.g., "https:" or "http:") | ||
| # This happens when URI is like openmemory://https://example.com | ||
| if netloc.endswith(":") and path.startswith("//"): | ||
| # Reconstruct the full URL: scheme from netloc + path | ||
| scheme = netloc.rstrip(":") | ||
| # path is like "//example.com", we want "https://example.com" | ||
| base_url = f"{scheme}://{path[2:]}" # Remove "//" prefix | ||
| elif path.startswith("//"): | ||
| # Path contains a full URL (e.g., openmemory:////http://localhost:3000) | ||
| full_url = path.lstrip("/") | ||
| if full_url.startswith(("http://", "https://")): | ||
| base_url = full_url | ||
| else: | ||
| base_url = f"http://{full_url}" | ||
| elif netloc.startswith(("http://", "https://")): | ||
| # Netloc itself is a full URL (shouldn't happen with proper URL parsing, but handle it) | ||
| base_url = netloc | ||
| else: | ||
| # Construct URL from netloc and path (default case) | ||
| if netloc: | ||
| base_url = f"http://{netloc}{path}" | ||
| else: | ||
| raise ValueError( | ||
| f"Invalid OpenMemory URI: {uri}. Expected format:" | ||
| " openmemory://localhost:3000 or openmemory://https://example.com" | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for parsing the openmemory:// URI is quite complex and handles many edge cases that make it hard to read. This could be simplified to be more maintainable while still supporting the required URI formats (openmemory://localhost:3000 and openmemory://https://example.com).
A simpler approach would be to treat the part of the URI after openmemory:// as the location, check if it includes a scheme, and prepend http:// if it doesn't. This avoids the complex branching with urlparse on a non-standard URI structure.
# The part after "openmemory://"
location_part = uri[len("openmemory://"):]
# Remove query string for base_url construction
base_url_part = location_part.split('?')[0].rstrip('/')
if not base_url_part:
raise ValueError(
f"Invalid OpenMemory URI: {uri}. Expected format:"
" openmemory://localhost:3000 or openmemory://https://example.com"
)
if not base_url_part.startswith(("http://", "https://")):
base_url = f"http://{base_url_part}"
else:
base_url = base_url_partThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback! I've addressed both suggestions:
-
Simplified the URI parsing logic as suggested - replaced the complex branching with a cleaner approach that extracts the location part and prepends
http://if no scheme is present. -
Simplified the tag creation logic - now builds the tags list explicitly and only appends the author tag if
event.authorexists, avoiding empty author tags.
The changes are now pushed to the PR. Please let me know if you'd like any further adjustments!
| memory_data["tags"] = [ | ||
| f"session:{session.id}", | ||
| f"app:{session.app_name}", | ||
| f"author:{event.author}" if event.author else None | ||
| ] | ||
| # Remove None values | ||
| memory_data["tags"] = [t for t in memory_data["tags"] if t] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current logic for creating tags can be simplified and made more robust. Using a list comprehension with a ternary operator for optional items can be less readable than a simple if statement. Also, if event.author is an empty string, it would result in a tag like 'author:' which is probably not intended.
| memory_data["tags"] = [ | |
| f"session:{session.id}", | |
| f"app:{session.app_name}", | |
| f"author:{event.author}" if event.author else None | |
| ] | |
| # Remove None values | |
| memory_data["tags"] = [t for t in memory_data["tags"] if t] | |
| tags = [ | |
| f"session:{session.id}", | |
| f"app:{session.app_name}", | |
| ] | |
| if event.author: | |
| tags.append(f"author:{event.author}") | |
| memory_data["tags"] = tags |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback! I've addressed both suggestions:
-
Simplified the URI parsing logic as suggested - replaced the complex branching with a cleaner approach that extracts the location part and prepends
http://if no scheme is present. -
Simplified the tag creation logic - now builds the tags list explicitly and only appends the author tag if
event.authorexists, avoiding empty author tags.
The changes are now pushed to the PR. Please let me know if you'd like any further adjustments!
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
- Simplified openmemory URI parsing logic to be more maintainable - Improved tag creation to avoid empty author tags - Fixed test assertions to match actual API field names (k vs top_k, filter vs filters)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
- Replace generic Exception catches with specific httpx.HTTPStatusError and httpx.RequestError handlers - Provide more detailed error messages with status codes and response text - Keep generic Exception handler as fallback for unexpected errors
|
Thanks for the contribution! This should go into : https://github.com/google/adk-python-community. Please move the PR there. Closing this one for now. |
Summary
This PR adds support for OpenMemory, a self-hosted, open-source memory system for AI agents, as a new memory service in ADK. OpenMemory provides brain-inspired multi-sector embeddings, graceful memory decay, and server-side filtering for efficient multi-user agent deployments. This PR includes:
OpenMemoryServiceimplementingBaseMemoryServiceinterface--memory_service_uriflag, enabling seamless integration withadk webandadk api_serverclose()method implementationShowcase
Related PRs
adk-docsLink to Issue or Description of Change
2. No existing issue - new feature contribution
Problem:
ADK currently supports Vertex AI-based memory services, but users need self-hostable, open-source alternatives for:
Solution:
OpenMemory fills this gap by providing a production-ready, self-hosted memory backend that integrates seamlessly with ADK's
BaseMemoryServiceinterface. This implementation uses directhttpxcalls for maximum control and reliability, and includes CLI support matching the patterns used by other memory services.Changes
Core Implementation
src/google/adk/memory/open_memory_service.py(405 lines)OpenMemoryService: ImplementsBaseMemoryServiceinterfaceOpenMemoryServiceConfig: Pydantic config model for user-configurable behaviorhttpxintegration (no SDK dependency)user_idfor multi-tenant isolationclose()method for API consistencyCLI Integration
Updated:
src/google/adk/cli/service_registry.pyopenmemory_factory()function to parseopenmemory://URI schemeopenmemoryscheme in memory service registryopenmemory://localhost:3000,openmemory://localhost:3000?api_key=secret,openmemory://https://example.comUpdated:
src/google/adk/cli/cli_tools_click.py--memory_service_urihelp text to include OpenMemory URI format examplesKey Features
--memory_service_uri="openmemory://localhost:3000"search_top_k: Number of memories to retrieve (default: 10)timeout: Request timeout in seconds (default: 30.0)user_content_salience: Importance score for user messages (default: 0.8)model_content_salience: Importance score for model responses (default: 0.7)default_salience: Fallback salience value (default: 0.6)enable_metadata_tags: Toggle session/app tagging (default: true)Dependencies
httpx>=0.27.0, <1.0.0in[project.optional-dependencies.openmemory]Sample Agent
contributing/samples/open_memory/agent.py: Sample agent with memory-enabled configurationmain.py: Demonstrates OpenMemoryService integration with ADK RunnerREADME.md: Setup instructions for OpenMemory backend and ADK integrationDocumentation Updates
src/google/adk/memory/__init__.pyto export new classesgoogle-adk[openmemory]extraTechnical Decisions
Direct HTTP Integration with httpx
This implementation uses
httpxfor direct REST API calls rather than a client SDK. This architectural decision provides several benefits:1. Flexibility and Control:
2. Pattern for Future Memory Services:
This approach establishes a reusable pattern for integrating other self-hosted memory services that expose REST APIs, such as:
3. Reduced Dependencies:
httpxis already widely used in Python)This makes it straightforward for future contributors to add new memory service integrations following the same approach.
CLI URI Pattern Consistency
The CLI integration follows the same URI pattern used by other memory services (
rag://,agentengine://), making it intuitive for users already familiar with ADK's CLI interface. Theopenmemory://scheme integrates seamlessly with the existing service registry architecture.URI Parsing Logic:
The factory function handles multiple URI formats:
openmemory://localhost:3000→ constructshttp://localhost:3000openmemory://https://example.com→ uses full URL directlyopenmemory://localhost:3000?api_key=secret→ extracts API key from query stringThis flexibility allows users to configure OpenMemory in the way that best fits their deployment setup.
Enriched Content Format
Since OpenMemory's query endpoint returns lightweight results (for performance), we embed author/timestamp directly in content during storage:
On retrieval, regex parsing extracts this metadata and returns clean content to users. This design:
Server-Side Filtering
user_idis passed as a top-level parameter (not metadata) to leverage OpenMemory's indexed database column for fast, secure multi-user isolation. This ensures efficient queries and proper tenant isolation in production deployments.Performance Optimization
The HTTP client (
httpx.AsyncClient) is created once per session and reused for all events, rather than creating a new client for each event. This reduces connection overhead and improves performance when adding multiple memories from a single session.API Consistency with
close()MethodThe
close()method is implemented as a no-op becausehttpx.AsyncClientuses context managers (async with), which automatically handle connection cleanup. However, providing this method:close()without errorsTesting Plan
Unit Tests
pytest results:
All unit tests use mocked
httpx.AsyncClientto avoid external dependencies. Tests cover:Manual End-to-End (E2E) Tests
Setup:
Install and start OpenMemory backend:
Configure OpenMemory environment variables:
Create a
.envfile inOpenMemory/backend/with:Start OpenMemory server:
npm start # Server will run on http://localhost:3000Configure ADK environment variables:
In your ADK project root (where you run the sample), create a
.envfile:Install ADK with OpenMemory support:
Run the sample agent:
cd contributing/samples/open_memory python main.pyTest Scenario:
The sample agent (
main.py) performs the following:CLI Testing:
Results:
Logs:
Checklist
Additional Context
Installation & Usage
Install ADK with OpenMemory support:
Setup OpenMemory Backend:
Configure ADK Agent:
Option 1: Using CLI
adk web agents_dir --memory_service_uri="openmemory://localhost:3000"Option 2: Using Python Code
Code Quality
pylintclean)from __future__ import annotations)TYPE_CHECKINGfor forward referencesRelated Documentation
A companion PR in
adk-docs(google/adk-docs#859) adds comprehensive documentation for OpenMemory, including:Future Enhancements