Skip to content

feat(memory): add DatabaseMemoryService with SQL backend and agent scratchpad#1

Open
Raman369AI wants to merge 7 commits intomainfrom
feat/database-memory-service
Open

feat(memory): add DatabaseMemoryService with SQL backend and agent scratchpad#1
Raman369AI wants to merge 7 commits intomainfrom
feat/database-memory-service

Conversation

@Raman369AI
Copy link
Copy Markdown
Owner

Replicates upstream PR google#98 inside this fork for visibility and fork-local checks.

Upstream PR: google#98

Summary:

  • Adds DatabaseMemoryService, a durable SQLAlchemy-backed memory service.
  • Adds pluggable memory search backend support with KeywordSearchBackend.
  • Adds scratchpad KV/log support and agent-callable scratchpad tools.
  • Adds optional SQLAlchemy/aiosqlite dependencies and unit coverage.
  • Includes follow-up fixes for the bot review feedback:
    • Atomic scratchpad KV UPSERTs for SQLite/PostgreSQL/MySQL/MariaDB.
    • PostgreSQL-only ILIKE usage.
    • Simplified search tokenization.
    • Temp state filtering before Redis session persistence.

Local verification:

  • uv run pytest tests/unittests -q -> 67 passed

…ratchpad

Adds a durable, RDBMS-backed memory service compatible with any
SQLAlchemy-supported async database (SQLite, PostgreSQL, MySQL, MariaDB)
as a community alternative to the volatile InMemoryMemoryService.

Key additions:
- DatabaseMemoryService: implements BaseMemoryService with lazy table
  creation, idempotent session ingest, and delta event ingestion
- MemorySearchBackend ABC + KeywordSearchBackend: LIKE/ILIKE search
  with AND-first → OR-fallback tokenisation strategy
- Scratchpad KV store and append-only log for intermediate agent state
- Four agent-callable BaseTool subclasses in adk_community.tools:
  ScratchpadGetTool, ScratchpadSetTool, ScratchpadAppendLogTool,
  ScratchpadGetLogTool (plus ready-to-use singleton instances)
- 38 unit tests covering all methods, tool happy-paths, wrong-service
  errors, multi-user isolation, and session scoping
- Optional sqlalchemy extra in pyproject.toml
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new SQL-backed memory service, DatabaseMemoryService, which supports SQLAlchemy-compatible databases like SQLite, PostgreSQL, and MySQL. It includes a scratchpad for key-value storage and append-only logging, along with agent-callable tools to interact with these features. Additionally, the PR updates RedisSessionService and OpenMemoryService with formatting improvements and minor logic refinements. Feedback focuses on improving search accuracy by not aggressively stripping non-alphanumeric characters from query tokens, ensuring consistent UTC timestamp formatting, and excluding internal model 'thought' parts from searchable memory to maintain data relevance.

Comment on lines +88 to +92
tokens = [
cleaned
for raw in query.split()
if (cleaned := re.sub(r'[^\w]', '', raw).lower())
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Aggressively stripping all non-alphanumeric characters from query tokens causes search failures for terms containing punctuation (e.g., 'Gemini-1.5', 'C#', '3.10'). Since the search_text in the database is stored as raw text, the query tokens should not be stripped of these characters to ensure LIKE matching works as expected.

    tokens = [t.lower() for t in query.split() if t.strip()]

from collections.abc import Mapping
from collections.abc import Sequence
from contextlib import asynccontextmanager
from datetime import datetime
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add timezone to the imports to support UTC timestamp formatting, ensuring consistency across different environments.

Suggested change
from datetime import datetime
from datetime import datetime
from datetime import timezone



def _format_timestamp(timestamp: float) -> str:
return datetime.fromtimestamp(timestamp).isoformat()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using datetime.fromtimestamp(timestamp) without an explicit timezone uses the local system time, which can lead to inconsistent timestamps in distributed or cloud environments. It is recommended to use UTC for all stored timestamps.

Suggested change
return datetime.fromtimestamp(timestamp).isoformat()
return datetime.fromtimestamp(timestamp, tz=timezone.utc).isoformat()

"""Join all text parts of a Content into a single searchable string."""
if not content or not content.parts:
return ''
return ' '.join(part.text for part in content.parts if part.text)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The search_text should exclude thought parts of the content. These parts represent internal model reasoning and are typically not intended to be part of the searchable memory. This change ensures consistency with the logic used in OpenMemoryService via utils.py.

Suggested change
return ' '.join(part.text for part in content.parts if part.text)
return ' '.join(
part.text for part in content.parts if part.text and not part.thought
)

"""Return True if the event has no usable text content."""
if not event.content or not event.content.parts:
return True
return not any(part.text for part in event.content.parts if part.text)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When determining if an event should be skipped for memory indexing, thought parts should be excluded. This ensures that events containing only internal reasoning are not added to the searchable memory database.

Suggested change
return not any(part.text for part in event.content.parts if part.text)
return not any(
part.text for part in event.content.parts if part.text and not part.thought
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant