Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,11 @@ optional-dependencies.all = [
"opentelemetry-instrumentation-httpx>=0.54b0,<1",
"opentelemetry-resourcedetector-gcp>=1.9.0a0,<2",
"pandas>=2.2.3",
"pgvector>=0.3",
"pillow>=10.3",
"protobuf>=6",
"psycopg-pool>=3.2",
"psycopg[binary]>=3.1",
"pyarrow>=14",
"pymongo>=4.9,<5",
"pypika>=0.50",
Expand Down Expand Up @@ -275,6 +278,11 @@ optional-dependencies.otel-gcp = [
"opentelemetry-instrumentation-grpc>=0.43b0,<1",
"opentelemetry-instrumentation-httpx>=0.54b0,<1",
]
optional-dependencies.pgvector = [
"pgvector>=0.3", # registers the pgvector type on the connection.
"psycopg[binary]>=3.1", # async driver used by PgVectorMemoryService.
"psycopg-pool>=3.2", # AsyncConnectionPool.
]
optional-dependencies.redis = [
"redis>=4.2", # 4.2 is where redis.asyncio landed, the only API used.
]
Expand Down
104 changes: 104 additions & 0 deletions src/google/adk/integrations/pgvector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# PostgreSQL / pgvector Memory Integration for ADK

This integration provides a self-hosted, semantic memory service for the Google
Agent Development Kit (ADK), backed by PostgreSQL with the
[pgvector](https://github.com/pgvector/pgvector) extension.

It complements the built-in memory services:

- `InMemoryMemoryService` is keyword-only and loses data on restart (prototyping
only).
- `VertexAiMemoryBankService` and `VertexAiRagMemoryService` provide semantic
memory but require Google Cloud.

`PgVectorMemoryService` gives you semantic (vector) memory that persists across
restarts and runs entirely on a database you control, which suits on-premise,
air-gapped, and data-residency-constrained deployments.

## Features

- **Semantic search:** Ranks memories by cosine similarity over embeddings using
a pgvector HNSW index, instead of keyword overlap.
- **Self-hosted:** Vectors live in your own PostgreSQL database. No managed Cloud
service is required for storage.
- **Idempotent ingestion:** Re-ingesting a session updates existing rows in
place rather than creating duplicates.
- **Scoped memory:** Memories are isolated per `(app_name, user_id)`.
- **Pluggable embeddings:** Embeds through the `google-genai` client by default,
or any provider via an injected `embedder` callable.

## Installation / Dependencies

Install the optional `pgvector` extra alongside ADK:

```bash
pip install "google-adk[pgvector]"
```

You also need a PostgreSQL database with the `vector` extension available. The
service creates the extension, table, and indexes on first use.

## Quick Start

```python
from google.adk.integrations.pgvector import PgVectorMemoryService
from google.adk.integrations.pgvector import PgVectorMemoryServiceConfig
from google.adk.runners import Runner

# 1. Configure the memory service.
memory_service = PgVectorMemoryService(
PgVectorMemoryServiceConfig(
dsn="postgresql://user:password@localhost:5432/adk",
embedding_model="gemini-embedding-001",
embedding_dimension=768,
)
)

# 2. Wire it into your Runner.
runner = Runner(
app_name="my_app",
agent=agent,
memory_service=memory_service,
)

# Ingesting and searching:
await memory_service.add_session_to_memory(session)
response = await memory_service.search_memory(
app_name="my_app", user_id="user1", query="what did we decide about billing?"
)
```

## Configuration

`PgVectorMemoryServiceConfig` supports the following options:

| Option | Default | Description |
| --- | --- | --- |
| `dsn` | `None` | PostgreSQL connection string. Required unless a `connection_pool` is injected. |
| `table_name` | `adk_memory_entries` | Table that stores memory entries. |
| `embedding_model` | `gemini-embedding-001` | `google-genai` embedding model. |
| `embedding_dimension` | `768` | Stored vector dimension; must match the model output and stays fixed for the table. |
| `top_k` | `10` | Maximum memories returned per search. |
| `hnsw_m` | `16` | pgvector HNSW `m` parameter. |
| `hnsw_ef_construction` | `64` | pgvector HNSW `ef_construction` parameter. |
| `distance_threshold` | `None` | Optional cosine-distance ceiling; farther memories are dropped. |

## Advanced usage

Inject a pre-configured pool or a non-Google embedding provider:

```python
from psycopg_pool import AsyncConnectionPool

pool = AsyncConnectionPool("postgresql://user:password@localhost:5432/adk")

async def my_embedder(texts):
# Return one vector per text from any provider.
...

memory_service = PgVectorMemoryService(
PgVectorMemoryServiceConfig(embedding_dimension=1024),
connection_pool=pool,
embedder=my_embedder,
)
```
25 changes: 25 additions & 0 deletions src/google/adk/integrations/pgvector/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2026 Google LLC
#
# 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.

"""PostgreSQL/pgvector integrations for ADK."""

from __future__ import annotations

from ._config import PgVectorMemoryServiceConfig
from ._pgvector_memory_service import PgVectorMemoryService

__all__ = [
"PgVectorMemoryService",
"PgVectorMemoryServiceConfig",
]
80 changes: 80 additions & 0 deletions src/google/adk/integrations/pgvector/_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright 2026 Google LLC
#
# 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.

"""Configuration for the PostgreSQL/pgvector integrations."""

from __future__ import annotations

from typing import Optional

from pydantic import BaseModel
from pydantic import Field


class PgVectorMemoryServiceConfig(BaseModel):
"""Configuration for PgVectorMemoryService.

The embedding model is called through the ``google-genai`` client, but the
vectors themselves live in a PostgreSQL database you control, so the memory
store does not depend on any managed Google Cloud service.
"""

dsn: Optional[str] = Field(
default=None,
description=(
"PostgreSQL connection string, e.g."
" postgresql://user:password@host:5432/dbname. Required unless a"
" pre-configured connection pool is passed to the service."
),
)
table_name: str = Field(
default="adk_memory_entries",
description="Name of the table that stores memory entries.",
)
embedding_model: str = Field(
default="gemini-embedding-001",
description=(
"google-genai embedding model used to embed events and queries."
),
)
embedding_dimension: int = Field(
default=768,
description=(
"Dimension of the stored embedding vectors. Must match the output"
" dimension of embedding_model and stays fixed for the life of the"
" table."
),
)
top_k: int = Field(
default=10,
description="Maximum number of memories returned by a search.",
)
hnsw_m: int = Field(
default=16,
description="pgvector HNSW index parameter m (graph degree).",
)
hnsw_ef_construction: int = Field(
default=64,
description=(
"pgvector HNSW index parameter ef_construction (build effort)."
),
)
distance_threshold: Optional[float] = Field(
default=None,
description=(
"Optional cosine-distance ceiling in [0, 2]. When set, memories whose"
" distance to the query is greater than this are dropped from the"
" results. When None, the closest top_k memories are always returned."
),
)
Loading
Loading