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/README.md b/contributing/samples/postgres_session_service/README.md new file mode 100644 index 0000000000..cd2459d1bf --- /dev/null +++ b/contributing/samples/postgres_session_service/README.md @@ -0,0 +1,201 @@ +# 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 Python packages: + +```bash +pip install google-adk asyncpg greenlet +``` + +## 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= +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. 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..b9930a384a --- /dev/null +++ b/contributing/samples/postgres_session_service/agent.py @@ -0,0 +1,43 @@ +# 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 datetime import timezone + +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 8601 format. + """ + return datetime.now(timezone.utc).isoformat() + + +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..1e3a588b07 --- /dev/null +++ b/contributing/samples/postgres_session_service/main.py @@ -0,0 +1,95 @@ +# 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())