From 6a992ae3fa003e116e9c8d131e5698fe34d538d0 Mon Sep 17 00:00:00 2001 From: Hiroaki Sano Date: Tue, 16 Dec 2025 15:39:56 +0900 Subject: [PATCH 1/3] docs: Add PostgreSQL session storage sample and documentation Add a comprehensive sample demonstrating how to use DatabaseSessionService with PostgreSQL, including: - README with schema documentation and configuration guide - Sample agent and main.py for session persistence demo - Docker Compose file for local PostgreSQL setup Related to google/adk-python#3916 --- .../postgres_session_service/README.md | 207 ++++++++++++++++++ .../postgres_session_service/__init__.py | 16 ++ .../samples/postgres_session_service/agent.py | 42 ++++ .../postgres_session_service/compose.yml | 14 ++ .../samples/postgres_session_service/main.py | 94 ++++++++ 5 files changed, 373 insertions(+) create mode 100644 contributing/samples/postgres_session_service/README.md create mode 100644 contributing/samples/postgres_session_service/__init__.py create mode 100644 contributing/samples/postgres_session_service/agent.py create mode 100644 contributing/samples/postgres_session_service/compose.yml create mode 100644 contributing/samples/postgres_session_service/main.py diff --git a/contributing/samples/postgres_session_service/README.md b/contributing/samples/postgres_session_service/README.md new file mode 100644 index 0000000000..d0323bcfb7 --- /dev/null +++ b/contributing/samples/postgres_session_service/README.md @@ -0,0 +1,207 @@ +# Using PostgreSQL with DatabaseSessionService + +This sample demonstrates how to configure `DatabaseSessionService` to use PostgreSQL for persisting sessions, events, and state. + +## Overview + +ADK's `DatabaseSessionService` supports multiple database backends through SQLAlchemy. This guide shows how to: + +- Set up PostgreSQL as the session storage backend +- Configure async connections with `asyncpg` +- Understand the auto-generated schema +- Run the sample agent with persistent sessions + +## Prerequisites + +- **PostgreSQL Database**: A running PostgreSQL instance (local or cloud) +- **asyncpg**: Async PostgreSQL driver for Python + +## Installation + +Install the required async PostgreSQL driver: + +```bash +pip install asyncpg +``` + +## Database Schema + +`DatabaseSessionService` automatically creates the following tables on first use: + +### sessions + +| Column | Type | Description | +| ----------- | ------------ | ------------------------------ | +| app_name | VARCHAR(128) | Application identifier (PK) | +| user_id | VARCHAR(128) | User identifier (PK) | +| id | VARCHAR(128) | Session UUID (PK) | +| state | JSONB | Session state as JSON | +| create_time | TIMESTAMP | Creation timestamp | +| update_time | TIMESTAMP | Last update timestamp | + +### events + +| Column | Type | Description | +| ------------------ | ------------ | ------------------------------ | +| id | VARCHAR(256) | Event UUID (PK) | +| app_name | VARCHAR(128) | Application identifier (PK) | +| user_id | VARCHAR(128) | User identifier (PK) | +| session_id | VARCHAR(128) | Session reference (PK, FK) | +| invocation_id | VARCHAR(256) | Invocation identifier | +| author | VARCHAR(256) | Event author | +| actions | BYTEA | Pickled EventActions | +| timestamp | TIMESTAMP | Event timestamp | +| content | JSONB | Event content | +| grounding_metadata | JSONB | Grounding metadata | +| custom_metadata | JSONB | Custom metadata | +| usage_metadata | JSONB | Token usage metadata | +| citation_metadata | JSONB | Citation metadata | +| partial | BOOLEAN | Partial event flag | +| turn_complete | BOOLEAN | Turn completion flag | +| error_code | VARCHAR(256) | Error code if any | +| error_message | TEXT | Error message if any | +| interrupted | BOOLEAN | Interruption flag | + +### app_states + +| Column | Type | Description | +| ----------- | ------------ | ------------------------------ | +| app_name | VARCHAR(128) | Application identifier (PK) | +| state | JSONB | Application-level state | +| update_time | TIMESTAMP | Last update timestamp | + +### user_states + +| Column | Type | Description | +| ----------- | ------------ | ------------------------------ | +| app_name | VARCHAR(128) | Application identifier (PK) | +| user_id | VARCHAR(128) | User identifier (PK) | +| state | JSONB | User-level state | +| update_time | TIMESTAMP | Last update timestamp | + +## Configuration + +### Connection URL Format + +```python +postgresql+asyncpg://username:password@host:port/database +``` + +### Basic Usage + +```python +from google.adk.sessions.database_session_service import DatabaseSessionService +from google.adk.runners import Runner + +# Initialize with PostgreSQL URL +session_service = DatabaseSessionService( + "postgresql+asyncpg://user:password@localhost:5432/adk_sessions" +) + +# Use with Runner +runner = Runner( + app_name="my_app", + agent=my_agent, + session_service=session_service, +) +``` + +### Advanced Configuration + +Pass additional SQLAlchemy engine options: + +```python +session_service = DatabaseSessionService( + "postgresql+asyncpg://user:password@localhost:5432/adk_sessions", + pool_size=10, + max_overflow=20, + pool_timeout=30, + pool_recycle=1800, +) +``` + +## Running the Sample + +### 1. Start PostgreSQL + +Using Docker: + +```bash +docker compose up -d +``` + +Or use an existing PostgreSQL instance. + +### 2. Configure Connection + +Create a `.env` file: + +```bash +POSTGRES_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/adk_sessions +GOOGLE_CLOUD_PROJECT={{ your project }} +GOOGLE_CLOUD_LOCATION=us-central1 +GOOGLE_GENAI_USE_VERTEXAI=true +``` + +Or run export command. + +```bash +export POSTGRES_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/adk_sessions +export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project) +export GOOGLE_CLOUD_LOCATION=us-central1 +export GOOGLE_GENAI_USE_VERTEXAI=true +``` + +### 3. Install the required modules + +```bash +pip install google-adk asyncpg greenlet +``` + +### 4. Run the Agent + +```bash +python main.py +``` + +Or use the ADK: + +```bash +adk run . +``` + +## Session Persistence + +Sessions and events are persisted across application restarts: + +```python +# First run - creates a new session +session = await session_service.create_session( + app_name="my_app", + user_id="user1", + session_id="persistent-session-123", +) + +# Later run - retrieves the existing session +session = await session_service.get_session( + app_name="my_app", + user_id="user1", + session_id="persistent-session-123", +) +``` + +## State Management + +PostgreSQL's JSONB type provides efficient storage for state data: + +- **Session state**: Stored in `sessions.state` +- **User state**: Stored in `user_states.state` +- **App state**: Stored in `app_states.state` + +## Production Considerations + +1. **Connection Pooling**: Use `pool_size` and `max_overflow` for high-traffic applications +2. **SSL/TLS**: Always use encrypted connections in production +3. **Backups**: Implement regular backup strategies for session data +4. **Indexing**: The default schema includes primary key indexes; add additional indexes based on query patterns +5. **Monitoring**: Monitor connection pool usage and query performance diff --git a/contributing/samples/postgres_session_service/__init__.py b/contributing/samples/postgres_session_service/__init__.py new file mode 100644 index 0000000000..7d5bb0b1c6 --- /dev/null +++ b/contributing/samples/postgres_session_service/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2025 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. + + +from . import agent diff --git a/contributing/samples/postgres_session_service/agent.py b/contributing/samples/postgres_session_service/agent.py new file mode 100644 index 0000000000..3c228b78f1 --- /dev/null +++ b/contributing/samples/postgres_session_service/agent.py @@ -0,0 +1,42 @@ +# Copyright 2025 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. + +"""Sample agent demonstrating PostgreSQL session persistence.""" + +from datetime import datetime + +from google.adk.agents.llm_agent import Agent + + +def get_current_time() -> str: + """Get the current time. + + Returns: + A string with the current time in ISO format. + """ + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +root_agent = Agent( + model="gemini-2.0-flash", + name="postgres_session_agent", + description="A sample agent demonstrating PostgreSQL session persistence.", + instruction=""" + You are a helpful assistant that demonstrates session persistence. + You can remember previous conversations within the same session. + Use the get_current_time tool when asked about the time. + When the user asks what you remember, summarize the previous conversation. + """, + tools=[get_current_time], +) diff --git a/contributing/samples/postgres_session_service/compose.yml b/contributing/samples/postgres_session_service/compose.yml new file mode 100644 index 0000000000..54862cbff4 --- /dev/null +++ b/contributing/samples/postgres_session_service/compose.yml @@ -0,0 +1,14 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: adk_sessions + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: diff --git a/contributing/samples/postgres_session_service/main.py b/contributing/samples/postgres_session_service/main.py new file mode 100644 index 0000000000..1171b09a02 --- /dev/null +++ b/contributing/samples/postgres_session_service/main.py @@ -0,0 +1,94 @@ +# Copyright 2025 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. + +"""Example demonstrating PostgreSQL session persistence with DatabaseSessionService.""" + +import asyncio +import os + +import agent +from dotenv import load_dotenv +from google.adk.runners import Runner +from google.adk.sessions.database_session_service import DatabaseSessionService +from google.adk.sessions.session import Session +from google.genai import types + +load_dotenv(override=True) + + +async def main(): + """Main function demonstrating PostgreSQL session persistence.""" + postgres_url = os.environ.get("POSTGRES_URL") + if not postgres_url: + raise ValueError( + "POSTGRES_URL environment variable not set. " + "Please create a .env file with POSTGRES_URL=postgresql+asyncpg://user:password@localhost:5432/adk_sessions" + ) + + app_name = "postgres_session_demo" + user_id = "demo_user" + session_id = "persistent-session" + + # Initialize PostgreSQL-backed session service + session_service = DatabaseSessionService(postgres_url) + + runner = Runner( + app_name=app_name, + agent=agent.root_agent, + session_service=session_service, + ) + + # Try to get existing session or create new one + session = await session_service.get_session( + app_name=app_name, + user_id=user_id, + session_id=session_id, + ) + + if session: + print(f"Resuming existing session: {session.id}") + print(f"Previous events count: {len(session.events)}") + else: + session = await session_service.create_session( + app_name=app_name, + user_id=user_id, + session_id=session_id, + ) + print(f"Created new session: {session.id}") + + async def run_prompt(session: Session, new_message: str): + """Send a prompt to the agent and print the response.""" + content = types.Content( + role="user", parts=[types.Part.from_text(text=new_message)] + ) + print(f"User: {new_message}") + async for event in runner.run_async( + user_id=user_id, + session_id=session.id, + new_message=content, + ): + if event.content and event.content.parts and event.content.parts[0].text: + print(f"{event.author}: {event.content.parts[0].text}") + + print("------------------------------------") + await run_prompt(session, "What time is it? Please remember this.") + print("------------------------------------") + await run_prompt(session, "What did I just ask you?") + print("------------------------------------") + + print("\nSession persisted to PostgreSQL. Run again to see event history.") + + +if __name__ == "__main__": + asyncio.run(main()) From 66bc9444705e724cfedd194adfd3d9093f1f59ca Mon Sep 17 00:00:00 2001 From: Hiroaki Sano Date: Tue, 16 Dec 2025 16:18:26 +0900 Subject: [PATCH 2/3] docs: Address review comments for PostgreSQL sample - Consolidate installation instructions in Installation section - Remove duplicate step 3 and renumber steps - Use standard placeholder format () - Use timezone-aware datetime with ISO 8601 format --- .../samples/postgres_session_service/README.md | 14 ++++---------- .../samples/postgres_session_service/agent.py | 6 +++--- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/contributing/samples/postgres_session_service/README.md b/contributing/samples/postgres_session_service/README.md index d0323bcfb7..cd2459d1bf 100644 --- a/contributing/samples/postgres_session_service/README.md +++ b/contributing/samples/postgres_session_service/README.md @@ -18,10 +18,10 @@ ADK's `DatabaseSessionService` supports multiple database backends through SQLAl ## Installation -Install the required async PostgreSQL driver: +Install the required Python packages: ```bash -pip install asyncpg +pip install google-adk asyncpg greenlet ``` ## Database Schema @@ -138,7 +138,7 @@ Create a `.env` file: ```bash POSTGRES_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/adk_sessions -GOOGLE_CLOUD_PROJECT={{ your project }} +GOOGLE_CLOUD_PROJECT= GOOGLE_CLOUD_LOCATION=us-central1 GOOGLE_GENAI_USE_VERTEXAI=true ``` @@ -152,13 +152,7 @@ export GOOGLE_CLOUD_LOCATION=us-central1 export GOOGLE_GENAI_USE_VERTEXAI=true ``` -### 3. Install the required modules - -```bash -pip install google-adk asyncpg greenlet -``` - -### 4. Run the Agent +### 3. Run the Agent ```bash python main.py diff --git a/contributing/samples/postgres_session_service/agent.py b/contributing/samples/postgres_session_service/agent.py index 3c228b78f1..26b99f700b 100644 --- a/contributing/samples/postgres_session_service/agent.py +++ b/contributing/samples/postgres_session_service/agent.py @@ -14,7 +14,7 @@ """Sample agent demonstrating PostgreSQL session persistence.""" -from datetime import datetime +from datetime import datetime, timezone from google.adk.agents.llm_agent import Agent @@ -23,9 +23,9 @@ def get_current_time() -> str: """Get the current time. Returns: - A string with the current time in ISO format. + A string with the current time in ISO 8601 format. """ - return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + return datetime.now(timezone.utc).isoformat() root_agent = Agent( From 30bbad2cb796ca47591e8d5be4ba34384661cdc2 Mon Sep 17 00:00:00 2001 From: Hiroaki Sano Date: Thu, 18 Dec 2025 10:19:43 +0900 Subject: [PATCH 3/3] chore: Run autoformat.sh to fix import style --- contributing/samples/gepa/experiment.py | 1 - contributing/samples/gepa/run_experiment.py | 1 - contributing/samples/postgres_session_service/agent.py | 3 ++- contributing/samples/postgres_session_service/main.py | 3 ++- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contributing/samples/gepa/experiment.py b/contributing/samples/gepa/experiment.py index 2f5d03a772..f68b349d9c 100644 --- a/contributing/samples/gepa/experiment.py +++ b/contributing/samples/gepa/experiment.py @@ -43,7 +43,6 @@ from tau_bench.types import EnvRunResult from tau_bench.types import RunConfig import tau_bench_agent as tau_bench_agent_lib - import utils diff --git a/contributing/samples/gepa/run_experiment.py b/contributing/samples/gepa/run_experiment.py index cfd850b3a3..1bc4ee58c8 100644 --- a/contributing/samples/gepa/run_experiment.py +++ b/contributing/samples/gepa/run_experiment.py @@ -25,7 +25,6 @@ from absl import flags import experiment from google.genai import types - import utils _OUTPUT_DIR = flags.DEFINE_string( diff --git a/contributing/samples/postgres_session_service/agent.py b/contributing/samples/postgres_session_service/agent.py index 26b99f700b..b9930a384a 100644 --- a/contributing/samples/postgres_session_service/agent.py +++ b/contributing/samples/postgres_session_service/agent.py @@ -14,7 +14,8 @@ """Sample agent demonstrating PostgreSQL session persistence.""" -from datetime import datetime, timezone +from datetime import datetime +from datetime import timezone from google.adk.agents.llm_agent import Agent diff --git a/contributing/samples/postgres_session_service/main.py b/contributing/samples/postgres_session_service/main.py index 1171b09a02..1e3a588b07 100644 --- a/contributing/samples/postgres_session_service/main.py +++ b/contributing/samples/postgres_session_service/main.py @@ -33,7 +33,8 @@ async def main(): if not postgres_url: raise ValueError( "POSTGRES_URL environment variable not set. " - "Please create a .env file with POSTGRES_URL=postgresql+asyncpg://user:password@localhost:5432/adk_sessions" + "Please create a .env file with" + " POSTGRES_URL=postgresql+asyncpg://user:password@localhost:5432/adk_sessions" ) app_name = "postgres_session_demo"