chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:13 +08:00
commit ec2b666284
2231 changed files with 491535 additions and 0 deletions
@@ -0,0 +1,131 @@
# Cache Analysis Research Assistant
## Overview
This sample demonstrates ADK context caching features using a comprehensive research assistant agent designed to test both Gemini 2.0 Flash and 2.5 Flash context caching capabilities. The sample showcases the difference between explicit ADK caching and Google's built-in implicit caching.
### Key Features
- **App-Level Cache Configuration**: Context cache settings applied at the App level following ADK best practices.
- **Large Context Instructions**: Over 4,200 tokens in system instructions to trigger context caching thresholds.
- **Comprehensive Tool Suite**: 7 specialized research and analysis tools.
- **Multi-Model Support**: Compatible with any Gemini model, automatically adapting the experiment type.
- **Performance Metrics**: Detailed token usage tracking, including `cached_content_token_count`.
## Sample Inputs
- `Hello, what can you do for me?`
*General question that does not trigger function calls, serving as a baseline query.*
- `What is artificial intelligence and how does it work in modern applications?`
*General question exploring domain knowledge without specific tool requests.*
- `Use benchmark_performance with system_name='E-commerce Platform', metrics=['latency', 'throughput'], duration='standard', load_profile='realistic'.`
*Specific request triggering the benchmark_performance tool with explicit parameters.*
- `Call analyze_user_behavior_patterns with user_segment='premium_customers', time_period='last_30_days', metrics=['engagement', 'conversion'].`
*Specific request triggering data analysis tools with required parameters.*
## Graph
```mermaid
graph TD
Agent[Agent: cache_analysis_assistant] --> Tool1[Tool: analyze_data_patterns]
Agent --> Tool2[Tool: research_literature]
Agent --> Tool3[Tool: generate_test_scenarios]
Agent --> Tool4[Tool: benchmark_performance]
Agent --> Tool5[Tool: optimize_system_performance]
Agent --> Tool6[Tool: analyze_security_vulnerabilities]
Agent --> Tool7[Tool: design_scalability_architecture]
```
## How To
### 1. Cache Configuration
Context caching is configured at the App level using `ContextCacheConfig`. This ensures that the agent's extensive system instructions and tool definitions are cached for repeated invocations.
```python
from google.adk.agents.context_cache_config import ContextCacheConfig
from google.adk.apps.app import App
cache_analysis_app = App(
name="cache_analysis",
root_agent=cache_analysis_agent,
context_cache_config=ContextCacheConfig(
min_tokens=4096,
ttl_seconds=600, # 10 minutes for research sessions
cache_intervals=3, # Maximum invocations before cache refresh
),
)
```
### 2. Run Cache Experiments
The `run_cache_experiments.py` script automates the execution of prompts and compares caching performance between models:
```bash
# Test any Gemini model - script automatically determines experiment type
python run_cache_experiments.py <model_name> --output results.json
# Examples:
python run_cache_experiments.py gemini-2.5-flash --output gemini_2_5_results.json
# Run multiple iterations for averaged results
python run_cache_experiments.py gemini-2.5-flash --repeat 3 --output averaged_results.json
```
### 3. Direct Agent Usage
You can also run or debug the agent directly using the ADK CLI:
```bash
# Run the agent directly
adk run contributing/samples/cache_analysis/agent.py
# Web interface for debugging
adk web contributing/samples/cache_analysis
```
### 4. Experiment Types
The script automatically adapts the experiment based on the specified model name:
#### Models with "2.5" (e.g., `gemini-2.5-flash`)
- **Explicit Caching**: ADK explicit caching + Google's implicit caching.
- **Implicit Only**: Google's built-in implicit caching alone.
- **Measures**: The added benefit and performance differences of explicit caching over built-in implicit caching.
#### Other Models (e.g., `gemini-2.0-flash`)
- **Explicit Caching**: ADK explicit caching enabled.
- **Uncached**: Caching completely disabled.
- **Measures**: Baseline performance and cost benefits of context caching.
### 5. Expected Results
- **Performance Improvements**: Simple text agents typically see a 30-70% latency reduction with caching. Tool-heavy agents may experience slight cache setup overhead but still provide significant cost benefits.
- **Cost Savings**: Up to 75% reduction in input token costs for cached content (paying only 25% of normal input cost).
- **Token Metrics**: Successful cache hits are indicated by non-zero `cached_content_token_count` values.
### 6. Troubleshooting
#### Zero Cached Tokens
If `cached_content_token_count` is always `0`:
- Verify model names match exactly (e.g., `gemini-2.5-flash`).
- Check that the `min_tokens` threshold (4,096 tokens) is met by the prompt and system instructions.
- Ensure proper App-based configuration is used rather than passing standalone agents without App wrappers.
#### Session Errors
If you encounter "Session not found" errors:
- Verify `runner.app_name` is used for session creation.
- Ensure correct initialization of `InMemoryRunner` with the `App` object.
@@ -0,0 +1,17 @@
# 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.
from . import agent
__all__ = ['agent']
@@ -0,0 +1,853 @@
# 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.
"""Cache Analysis Research Assistant Agent.
This agent is designed to test ADK context caching features with a large prompt
that exceeds 2048 tokens to meet both implicit and explicit cache requirements.
"""
import random
import time
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from dotenv import load_dotenv
from google.adk import Agent
from google.adk.agents.context_cache_config import ContextCacheConfig
from google.adk.apps.app import App
# Load environment variables from .env file
load_dotenv()
def analyze_data_patterns(
data: str, analysis_type: str = "comprehensive"
) -> Dict[str, Any]:
"""Analyze data patterns and provide insights.
This tool performs comprehensive data analysis including statistical analysis,
trend identification, anomaly detection, correlation analysis, and predictive
modeling. It can handle various data formats including CSV, JSON, XML, and
plain text data structures.
Args:
data: The input data to analyze. Can be structured (JSON, CSV) or
unstructured text data. For structured data, include column headers
and ensure proper formatting. For time series data, include
timestamps in ISO format.
analysis_type: Type of analysis to perform. Options include:
- "comprehensive": Full statistical and trend analysis
- "statistical": Basic statistical measures only
- "trends": Time series and trend analysis
- "anomalies": Outlier and anomaly detection
- "correlations": Correlation and relationship analysis
- "predictive": Forecasting and prediction models
Returns:
Dictionary containing analysis results with the following structure:
{
"summary": "High-level summary of findings",
"statistics": {...}, # Statistical measures
"trends": {...}, # Trend analysis results
"anomalies": [...], # List of detected anomalies
"correlations": {...}, # Correlation matrix and relationships
"predictions": {...}, # Forecasting results if applicable
"recommendations": [...] # Actionable insights and recommendations
}
"""
# Simulate analysis processing time
time.sleep(0.1)
return {
"summary": f"Analyzed {len(data)} characters of {analysis_type} data",
"statistics": {
"data_points": len(data.split()),
"analysis_type": analysis_type,
"processing_time": "0.1 seconds",
},
"recommendations": [
"Continue monitoring data trends",
"Consider additional data sources for correlation analysis",
],
}
def research_literature(
topic: str,
sources: Optional[List[str]] = None,
depth: str = "comprehensive",
time_range: str = "recent",
) -> Dict[str, Any]:
"""Research academic and professional literature on specified topics.
This tool performs comprehensive literature research across multiple academic
databases, professional journals, conference proceedings, and industry reports.
It can analyze research trends, identify key authors and institutions, extract
methodological approaches, and synthesize findings across multiple sources.
The tool supports various research methodologies including systematic reviews,
meta-analyses, bibliometric analysis, and citation network analysis. It can
identify research gaps, emerging trends, and future research directions in
the specified field of study.
Args:
topic: The research topic or query. Can be specific (e.g., "context caching
in large language models") or broad (e.g., "machine learning optimization").
Use specific keywords and phrases for better results. Boolean operators
(AND, OR, NOT) are supported for complex queries.
sources: List of preferred sources to search. Options include:
- "academic": Peer-reviewed academic journals and papers
- "conference": Conference proceedings and presentations
- "industry": Industry reports and white papers
- "patents": Patent databases and intellectual property
- "preprints": ArXiv, bioRxiv and other preprint servers
- "books": Academic and professional books
depth: Research depth level:
- "comprehensive": Full literature review with detailed analysis
- "focused": Targeted search on specific aspects
- "overview": High-level survey of the field
- "technical": Deep technical implementation details
time_range: Time range for literature search:
- "recent": Last 2 years
- "current": Last 5 years
- "historical": All available time periods
- "decade": Last 10 years
Returns:
Dictionary containing research results:
{
"summary": "Executive summary of findings",
"key_papers": [...], # Most relevant papers found
"authors": [...], # Key researchers in the field
"institutions": [...], # Leading research institutions
"trends": {...}, # Research trends and evolution
"methodologies": [...], # Common research approaches
"gaps": [...], # Identified research gaps
"citations": {...}, # Citation network analysis
"recommendations": [...] # Future research directions
}
"""
if sources is None:
sources = ["academic", "conference", "industry"]
# Simulate research processing
time.sleep(0.2)
return {
"summary": f"Conducted {depth} literature research on '{topic}'",
"key_papers": [
f"Recent advances in {topic.lower()}: A systematic review",
f"Methodological approaches to {topic.lower()} optimization",
f"Future directions in {topic.lower()} research",
],
"trends": {
"emerging_topics": [f"{topic} optimization", f"{topic} scalability"],
"methodology_trends": [
"experimental validation",
"theoretical analysis",
],
},
"recommendations": [
f"Focus on practical applications of {topic}",
"Consider interdisciplinary approaches",
"Investigate scalability challenges",
],
}
def generate_test_scenarios(
system_type: str,
complexity: str = "medium",
coverage: Optional[List[str]] = None,
constraints: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Generate comprehensive test scenarios for system validation.
This tool creates detailed test scenarios, test cases, and validation protocols
for various types of systems including software applications, AI models,
distributed systems, and hardware components. It supports multiple testing
methodologies including unit testing, integration testing, performance testing,
security testing, and user acceptance testing.
The tool can generate both positive and negative test cases, edge cases,
boundary conditions, stress tests, and failure scenarios. It incorporates
industry best practices and testing frameworks to ensure comprehensive
coverage and reliable validation results.
Args:
system_type: Type of system to test. Supported types include:
- "software": Software applications and services
- "ai_model": Machine learning and AI model testing
- "distributed": Distributed systems and microservices
- "database": Database systems and data integrity
- "api": API endpoints and web services
- "hardware": Hardware components and embedded systems
- "security": Security systems and protocols
complexity: Test complexity level:
- "basic": Essential functionality tests only
- "medium": Standard test suite with common scenarios
- "advanced": Comprehensive testing with edge cases
- "expert": Exhaustive testing with stress and chaos scenarios
coverage: List of testing areas to cover:
- "functionality": Core feature testing
- "performance": Speed, throughput, and scalability
- "security": Authentication, authorization, data protection
- "usability": User experience and interface testing
- "compatibility": Cross-platform and integration testing
- "reliability": Fault tolerance and recovery testing
constraints: Testing constraints and requirements:
{
"time_limit": "Maximum testing duration",
"resources": "Available testing resources",
"environment": "Testing environment specifications",
"compliance": "Regulatory or standard requirements"
}
Returns:
Dictionary containing generated test scenarios:
{
"overview": "Test plan summary and objectives",
"scenarios": [...], # Detailed test scenarios
"test_cases": [...], # Individual test cases
"edge_cases": [...], # Boundary and edge conditions
"performance_tests": [...], # Performance validation tests
"security_tests": [...], # Security and vulnerability tests
"automation": {...}, # Test automation recommendations
"metrics": {...}, # Success criteria and metrics
"schedule": {...} # Recommended testing timeline
}
"""
if coverage is None:
coverage = ["functionality", "performance", "security"]
if constraints is None:
constraints = {"time_limit": "standard", "resources": "adequate"}
# Simulate test generation
time.sleep(0.15)
num_scenarios = {"basic": 5, "medium": 10, "advanced": 20, "expert": 35}.get(
complexity, 10
)
return {
"overview": (
f"Generated {num_scenarios} test scenarios for {system_type} system"
),
"scenarios": [
f"Test scenario {i+1}:"
f" {system_type} {coverage[i % len(coverage)]} validation"
for i in range(num_scenarios)
],
"test_cases": [
f"Verify {system_type} handles normal operations",
f"Test {system_type} error handling and recovery",
f"Validate {system_type} performance under load",
],
"metrics": {
"coverage_target": f"{75 + complexity.index(complexity) * 5}%",
"success_criteria": "All critical tests pass",
"performance_benchmark": f"{system_type} specific benchmarks",
},
}
def optimize_system_performance(
system_type: str,
current_metrics: Dict[str, Any],
target_improvements: Dict[str, Any],
constraints: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Analyze system performance and provide detailed optimization recommendations.
This tool performs comprehensive system performance analysis including bottleneck
identification, resource utilization assessment, scalability planning, and provides
specific optimization strategies tailored to the system type and constraints.
Args:
system_type: Type of system to optimize:
- "web_application": Frontend and backend web services
- "database": Relational, NoSQL, or distributed databases
- "ml_pipeline": Machine learning training and inference systems
- "distributed_cache": Caching layers and distributed memory systems
- "microservices": Service-oriented architectures
- "data_processing": ETL, stream processing, batch systems
- "api_gateway": Request routing and API management systems
current_metrics: Current performance metrics including:
{
"response_time_p95": "95th percentile response time in ms",
"throughput_rps": "Requests per second",
"cpu_utilization": "Average CPU usage percentage",
"memory_usage": "Memory consumption in GB",
"error_rate": "Error percentage",
"availability": "System uptime percentage"
}
target_improvements: Desired performance targets:
{
"response_time_improvement": "Target reduction in response time",
"throughput_increase": "Desired increase in throughput",
"cost_reduction": "Target cost optimization percentage",
"availability_target": "Desired uptime percentage"
}
constraints: Operational constraints:
{
"budget_limit": "Maximum budget for improvements",
"timeline": "Implementation timeline constraints",
"technology_restrictions": "Required or forbidden technologies",
"compliance_requirements": "Security/regulatory constraints"
}
Returns:
Comprehensive optimization analysis:
{
"performance_analysis": {
"bottlenecks_identified": ["Critical performance bottlenecks"],
"root_cause_analysis": "Detailed analysis of performance issues",
"current_vs_target": "Gap analysis between current and target metrics"
},
"optimization_recommendations": {
"infrastructure_changes": ["Hardware/cloud resource recommendations"],
"architecture_improvements": ["System design optimizations"],
"code_optimizations": ["Software-level improvements"],
"configuration_tuning": ["Parameter and setting adjustments"]
},
"implementation_roadmap": {
"phase_1_quick_wins": ["Immediate improvements (0-2 weeks)"],
"phase_2_medium_term": ["Medium-term optimizations (1-3 months)"],
"phase_3_strategic": ["Long-term architectural changes (3-12 months)"]
},
"expected_outcomes": {
"performance_improvements": "Projected performance gains",
"cost_implications": "Expected costs and savings",
"risk_assessment": "Implementation risks and mitigation strategies"
}
}
"""
# Simulate comprehensive performance optimization analysis
optimization_areas = [
"Database query optimization",
"Caching layer enhancement",
"Load balancing improvements",
"Resource scaling strategies",
"Code-level optimizations",
"Infrastructure upgrades",
]
return {
"system_analyzed": system_type,
"optimization_areas": random.sample(
optimization_areas, k=min(4, len(optimization_areas))
),
"performance_score": random.randint(65, 95),
"implementation_complexity": random.choice(["Low", "Medium", "High"]),
"estimated_improvement": f"{random.randint(15, 45)}%",
"recommendations": [
"Implement distributed caching for frequently accessed data",
"Optimize database queries and add strategic indexes",
"Configure auto-scaling based on traffic patterns",
"Implement asynchronous processing for heavy operations",
],
}
def analyze_security_vulnerabilities(
system_components: List[str],
security_scope: str = "comprehensive",
compliance_frameworks: Optional[List[str]] = None,
threat_model: str = "enterprise",
) -> Dict[str, Any]:
"""Perform comprehensive security vulnerability analysis and risk assessment.
This tool conducts detailed security analysis including vulnerability identification,
threat modeling, compliance gap analysis, and provides prioritized remediation
strategies based on risk levels and business impact.
Args:
system_components: List of system components to analyze:
- "web_frontend": User interfaces, SPAs, mobile apps
- "api_endpoints": REST/GraphQL APIs, microservices
- "database_layer": Data storage and access systems
- "authentication": User auth, SSO, identity management
- "data_processing": ETL, analytics, ML pipelines
- "infrastructure": Servers, containers, cloud services
- "network_layer": Load balancers, firewalls, CDNs
security_scope: Analysis depth:
- "basic": Standard vulnerability scanning
- "comprehensive": Full security assessment
- "compliance_focused": Regulatory compliance analysis
- "threat_modeling": Advanced threat analysis
compliance_frameworks: Required compliance standards:
["SOC2", "GDPR", "HIPAA", "PCI-DSS", "ISO27001"]
threat_model: Threat landscape consideration:
- "startup": Basic threat model for early-stage companies
- "enterprise": Corporate threat landscape
- "high_security": Government/financial sector threats
- "public_facing": Internet-exposed systems
Returns:
Security analysis results:
{
"vulnerability_assessment": {
"critical_vulnerabilities": ["High-priority security issues"],
"moderate_risks": ["Medium-priority concerns"],
"informational": ["Low-priority observations"],
"risk_score": "Overall security risk rating (1-10)"
},
"threat_analysis": {
"attack_vectors": ["Potential attack methods"],
"threat_actors": ["Relevant threat actor profiles"],
"attack_likelihood": "Probability assessment",
"potential_impact": "Business impact analysis"
},
"compliance_status": {
"framework_compliance": "Compliance percentage per framework",
"gaps_identified": ["Non-compliant areas"],
"certification_readiness": "Readiness for compliance audits"
},
"remediation_plan": {
"immediate_actions": ["Critical fixes (0-2 weeks)"],
"short_term_improvements": ["Important fixes (1-2 months)"],
"strategic_initiatives": ["Long-term security enhancements"],
"resource_requirements": "Personnel and budget needs"
}
}
"""
# Simulate security vulnerability analysis
vulnerability_types = [
"SQL Injection",
"Cross-Site Scripting (XSS)",
"Authentication Bypass",
"Insecure Direct Object References",
"Security Misconfiguration",
"Sensitive Data Exposure",
"Insufficient Logging",
"CSRF",
]
return {
"components_analyzed": len(system_components),
"critical_vulnerabilities": random.randint(0, 3),
"moderate_risks": random.randint(2, 8),
"overall_security_score": random.randint(6, 9),
"compliance_percentage": random.randint(75, 95),
"top_recommendations": [
"Implement input validation and parameterized queries",
"Enable comprehensive security logging and monitoring",
"Review and update authentication and authorization controls",
"Conduct regular security training for development team",
],
}
def design_scalability_architecture(
current_architecture: str,
expected_growth: Dict[str, Any],
scalability_requirements: Dict[str, Any],
technology_preferences: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""Design comprehensive scalability architecture for anticipated growth.
This tool analyzes current system architecture and designs scalable solutions
to handle projected growth in users, data, traffic, and complexity while
maintaining performance, reliability, and cost-effectiveness.
Args:
current_architecture: Current system architecture type:
- "monolith": Single-tier monolithic application
- "service_oriented": SOA with multiple services
- "microservices": Containerized microservice architecture
- "serverless": Function-as-a-Service architecture
- "hybrid": Mixed architecture patterns
expected_growth: Projected growth metrics:
{
"user_growth_multiplier": "Expected increase in users",
"data_volume_growth": "Projected data storage needs",
"traffic_increase": "Expected traffic growth percentage",
"geographic_expansion": "New regions/markets",
"feature_complexity": "Additional functionality scope"
}
scalability_requirements: Scalability constraints and targets:
{
"performance_sla": "Response time requirements",
"availability_target": "Uptime requirements",
"consistency_model": "Data consistency needs",
"budget_constraints": "Cost limitations",
"deployment_model": "On-premise/cloud preferences"
}
technology_preferences: Preferred or required technologies:
["kubernetes", "aws", "microservices", "nosql", etc.]
Returns:
Scalability architecture design:
{
"architecture_recommendation": {
"target_architecture": "Recommended architecture pattern",
"migration_strategy": "Path from current to target architecture",
"technology_stack": "Recommended technologies and frameworks"
},
"scalability_patterns": {
"horizontal_scaling": "Auto-scaling and load distribution strategies",
"data_partitioning": "Database sharding and data distribution",
"caching_strategy": "Multi-level caching implementation",
"async_processing": "Background job and queue systems"
},
"infrastructure_design": {
"compute_resources": "Server/container resource planning",
"data_storage": "Database and storage architecture",
"network_topology": "CDN, load balancing, and routing",
"monitoring_observability": "Logging, metrics, and alerting"
},
"implementation_phases": {
"foundation_setup": "Core infrastructure preparation",
"service_decomposition": "Breaking down monolithic components",
"data_migration": "Database and storage transitions",
"traffic_migration": "Gradual user traffic transition"
}
}
"""
# Simulate scalability architecture design
architecture_patterns = [
"Event-driven microservices",
"CQRS with Event Sourcing",
"Federated GraphQL architecture",
"Serverless-first design",
"Hybrid cloud architecture",
"Edge-computing integration",
]
return {
"recommended_pattern": random.choice(architecture_patterns),
"scalability_factor": f"{random.randint(5, 50)}x current capacity",
"implementation_timeline": f"{random.randint(6, 18)} months",
"estimated_cost_increase": f"{random.randint(20, 80)}%",
"key_technologies": random.sample(
[
"Kubernetes",
"Docker",
"Redis",
"PostgreSQL",
"MongoDB",
"Apache Kafka",
"Elasticsearch",
"AWS Lambda",
"CloudFront",
],
k=4,
),
"success_metrics": [
"Response time under load",
"Auto-scaling effectiveness",
"Cost per transaction",
"System availability",
],
}
def benchmark_performance(
system_name: str,
metrics: Optional[List[str]] = None,
duration: str = "standard",
load_profile: str = "realistic",
) -> Dict[str, Any]:
"""Perform comprehensive performance benchmarking and analysis.
This tool conducts detailed performance benchmarking across multiple dimensions
including response time, throughput, resource utilization, scalability limits,
and system stability under various load conditions. It supports both synthetic
and realistic workload testing with configurable parameters and monitoring.
The benchmarking process includes baseline establishment, performance profiling,
bottleneck identification, capacity planning, and optimization recommendations.
It can simulate various user patterns, network conditions, and system configurations
to provide comprehensive performance insights.
Args:
system_name: Name or identifier of the system to benchmark. Should be
specific enough to identify the exact system configuration
being tested.
metrics: List of performance metrics to measure:
- "latency": Response time and request processing delays
- "throughput": Requests per second and data processing rates
- "cpu": CPU utilization and processing efficiency
- "memory": Memory usage and allocation patterns
- "disk": Disk I/O performance and storage operations
- "network": Network bandwidth and communication overhead
- "scalability": System behavior under increasing load
- "stability": Long-term performance and reliability
duration: Benchmarking duration:
- "quick": 5-10 minutes for rapid assessment
- "standard": 30-60 minutes for comprehensive testing
- "extended": 2-4 hours for stability and endurance testing
- "continuous": Ongoing monitoring and measurement
load_profile: Type of load pattern to simulate:
- "constant": Steady, consistent load throughout test
- "realistic": Variable load mimicking real usage patterns
- "peak": High-intensity load testing for capacity limits
- "stress": Beyond-capacity testing for failure analysis
- "spike": Sudden load increases to test elasticity
Returns:
Dictionary containing comprehensive benchmark results:
{
"summary": "Performance benchmark executive summary",
"baseline": {...}, # Baseline performance measurements
"results": {...}, # Detailed performance metrics
"bottlenecks": [...], # Identified performance bottlenecks
"scalability": {...}, # Scalability analysis results
"recommendations": [...], # Performance optimization suggestions
"capacity": {...}, # Capacity planning insights
"monitoring": {...} # Ongoing monitoring recommendations
}
"""
if metrics is None:
metrics = ["latency", "throughput", "cpu", "memory"]
# Simulate benchmarking
time.sleep(0.3)
return {
"summary": f"Completed {duration} performance benchmark of {system_name}",
"baseline": {
"avg_latency": f"{random.uniform(50, 200):.2f}ms",
"throughput": f"{random.randint(100, 1000)} requests/sec",
"cpu_usage": f"{random.uniform(20, 80):.1f}%",
},
"results": {
metric: f"Measured {metric} performance within expected ranges"
for metric in metrics
},
"recommendations": [
f"Optimize {system_name} for better {metrics[0]} performance",
f"Consider scaling {system_name} for higher throughput",
"Monitor performance trends over time",
],
}
# Create the cache analysis research assistant agent
cache_analysis_agent = Agent(
name="cache_analysis_assistant",
description="""
Advanced Research and Analysis Assistant specializing in comprehensive system analysis,
performance benchmarking, literature research, and test scenario generation for
technical systems and AI applications.
""",
instruction="""
You are an expert Research and Analysis Assistant with deep expertise across multiple
technical domains, specializing in comprehensive system analysis, performance optimization,
security assessment, and architectural design. Your role encompasses both strategic planning
and tactical implementation guidance for complex technical systems.
**Core Competencies and Expertise Areas:**
**Data Analysis & Pattern Recognition:**
- Advanced statistical analysis including multivariate analysis, time series forecasting,
regression modeling, and machine learning applications for pattern discovery
- Trend identification across large datasets using statistical process control, anomaly
detection algorithms, and predictive modeling techniques
- Root cause analysis methodologies for complex system behaviors and performance issues
- Data quality assessment and validation frameworks for ensuring analytical integrity
- Visualization design principles for effective communication of analytical findings
- Business intelligence and reporting strategies for different stakeholder audiences
**Academic & Professional Research:**
- Systematic literature reviews following PRISMA guidelines and meta-analysis techniques
- Citation network analysis and research impact assessment using bibliometric methods
- Research gap identification through comprehensive domain mapping and trend analysis
- Synthesis methodologies for integrating findings from diverse research sources
- Research methodology design including experimental design, survey methods, and case studies
- Peer review processes and academic publication strategies for research dissemination
- Industry research integration including white papers, technical reports, and conference proceedings
- Patent landscape analysis and intellectual property research for innovation assessment
**Test Design & Validation:**
- Comprehensive test strategy development following industry frameworks (ISTQB, TMMI, TPI)
- Test automation architecture design including framework selection and implementation strategies
- Quality assurance methodologies encompassing functional, non-functional, and security testing
- Risk-based testing approaches for optimizing test coverage within resource constraints
- Continuous integration and deployment testing strategies for DevOps environments
- Performance testing including load, stress, volume, and endurance testing protocols
- Usability testing methodologies and user experience validation frameworks
- Compliance testing for regulatory requirements across different industries
**Performance Engineering & Optimization:**
- System performance analysis using APM tools, profiling techniques, and monitoring strategies
- Capacity planning methodologies for both current needs and future growth projections
- Scalability assessment including horizontal and vertical scaling strategies
- Resource optimization techniques for compute, memory, storage, and network resources
- Database performance tuning including query optimization, indexing strategies, and partitioning
- Caching strategies implementation across multiple layers (application, database, CDN)
- Load balancing and traffic distribution optimization for high-availability systems
- Performance budgeting and SLA definition for service-level agreements
**Security & Compliance Analysis:**
- Comprehensive security risk assessment including threat modeling and vulnerability analysis
- Security architecture review and design for both defensive and offensive security perspectives
- Compliance framework analysis for standards including SOC2, GDPR, HIPAA, PCI-DSS, ISO27001
- Incident response planning and security monitoring strategy development
- Security testing methodologies including penetration testing and security code review
- Privacy impact assessment and data protection strategy development
- Security training program design for technical and non-technical audiences
- Cybersecurity governance and policy development for organizational security posture
**System Architecture & Design:**
- Distributed systems design including microservices, service mesh, and event-driven architectures
- Cloud architecture design for AWS, Azure, GCP with multi-cloud and hybrid strategies
- Scalability patterns implementation including CQRS, Event Sourcing, and saga patterns
- Database design and data modeling for both relational and NoSQL systems
- API design following REST, GraphQL, and event-driven communication patterns
- Infrastructure as Code (IaC) implementation using Terraform, CloudFormation, and Ansible
- Container orchestration with Kubernetes including service mesh and observability
- DevOps pipeline design encompassing CI/CD, monitoring, logging, and alerting strategies
**Research Methodology Framework:**
**Systematic Approach:**
- Begin every analysis with clear problem definition, success criteria, and scope boundaries
- Establish baseline measurements and define key performance indicators before analysis
- Use structured analytical frameworks appropriate to the domain and problem type
- Apply scientific methods including hypothesis formation, controlled experimentation, and validation
- Implement peer review processes and cross-validation techniques when possible
- Document methodology transparently to enable reproducibility and peer verification
**Information Synthesis:**
- Integrate quantitative data with qualitative insights for comprehensive understanding
- Cross-reference multiple authoritative sources to validate findings and reduce bias
- Identify conflicting information and analyze reasons for discrepancies
- Synthesize complex technical concepts into actionable business recommendations
- Maintain awareness of information currency and source reliability
- Apply critical thinking to distinguish correlation from causation in analytical findings
**Quality Assurance Standards:**
- Implement multi-stage review processes for all analytical outputs
- Use statistical significance testing and confidence intervals where appropriate
- Clearly distinguish between established facts, supported inferences, and speculative conclusions
- Provide uncertainty estimates and risk assessments for all recommendations
- Include limitations analysis and recommendations for additional research or data collection
- Ensure all analysis follows industry best practices and professional standards
**Communication and Reporting Excellence:**
**Audience Adaptation:**
- Tailor communication style to technical level and role of the intended audience
- Provide executive summaries for strategic decision-makers alongside detailed technical analysis
- Use progressive disclosure to present information at appropriate levels of detail
- Include visual elements and structured formats to enhance comprehension
- Anticipate questions and provide preemptive clarification on complex topics
**Documentation Standards:**
- Follow structured reporting templates appropriate to the analysis type
- Include methodology sections that enable reproduction of analytical work
- Provide clear action items with priority levels and implementation timelines
- Include risk assessments and mitigation strategies for all recommendations
- Maintain version control and change tracking for iterative analytical processes
**Tool Utilization Guidelines:**
When users request analysis or research, strategically leverage the available tools:
**For Data Analysis Requests:**
- Use analyze_data_patterns for statistical analysis, trend identification, and pattern discovery
- Apply appropriate statistical methods based on data type, sample size, and research questions
- Provide confidence intervals and statistical significance testing where applicable
- Include data visualization recommendations and interpretation guidance
**For Literature Research:**
- Use research_literature for comprehensive academic and professional literature reviews
- Focus on peer-reviewed sources while including relevant industry reports and white papers
- Provide synthesis of findings with identification of research gaps and conflicting viewpoints
- Include citation analysis and research impact assessment when relevant
**For Testing Strategy:**
- Use generate_test_scenarios for comprehensive test planning and validation protocol design
- Balance test coverage with practical constraints including time, budget, and resource limitations
- Include both functional and non-functional testing considerations
- Provide automation recommendations and implementation guidance
**For Performance Analysis:**
- Use benchmark_performance for detailed performance assessment and optimization analysis
- Include both current performance evaluation and future scalability considerations
- Provide specific, measurable recommendations with expected impact quantification
- Consider cost implications and return on investment for optimization recommendations
**For System Optimization:**
- Use optimize_system_performance for comprehensive system improvement strategies
- Include both technical optimizations and operational process improvements
- Provide phased implementation approaches with quick wins and long-term strategic initiatives
- Consider interdependencies between system components and potential unintended consequences
**For Security Assessment:**
- Use analyze_security_vulnerabilities for comprehensive security risk evaluation
- Include both technical vulnerabilities and procedural/operational security gaps
- Provide risk-prioritized remediation plans with business impact consideration
- Include compliance requirements and regulatory considerations
**For Architecture Design:**
- Use design_scalability_architecture for strategic technical architecture planning
- Consider both current requirements and future growth projections
- Include technology stack recommendations with rationale and trade-off analysis
- Provide migration strategies and implementation roadmaps for architecture transitions
**Professional Standards and Ethics:**
**Analytical Integrity:**
- Maintain objectivity and avoid confirmation bias in all analytical work
- Acknowledge limitations in data, methodology, or analytical scope
- Provide balanced perspectives that consider alternative explanations and interpretations
- Use peer review and validation processes to ensure analytical quality
- Stay current with best practices and methodological advances in relevant domains
**Stakeholder Communication:**
- Provide clear, actionable recommendations that align with organizational capabilities
- Include risk assessments and uncertainty estimates for all strategic recommendations
- Consider implementation feasibility including technical, financial, and organizational constraints
- Offer both immediate tactical improvements and long-term strategic initiatives
- Maintain transparency about analytical processes and potential sources of error
Your ultimate goal is to provide insights that are technically rigorous, strategically sound,
and practically implementable. Every analysis should contribute to improved decision-making
and measurable business outcomes while maintaining the highest standards of professional
excellence and analytical integrity.
""",
tools=[
analyze_data_patterns,
research_literature,
generate_test_scenarios,
benchmark_performance,
optimize_system_performance,
analyze_security_vulnerabilities,
design_scalability_architecture,
],
)
# Create the app with context caching configuration
# Note: Context cache config is set at the App level
cache_analysis_app = App(
name="cache_analysis",
root_agent=cache_analysis_agent,
context_cache_config=ContextCacheConfig(
min_tokens=4096,
ttl_seconds=600, # 10 mins for research sessions
cache_intervals=3, # Maximum invocations before cache refresh
),
)
# Export as app since it's an App, not an Agent
app = cache_analysis_app
# Backward compatibility export - ADK still expects root_agent in some contexts
root_agent = cache_analysis_agent
@@ -0,0 +1,717 @@
#!/usr/bin/env python3
# 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.
"""
Cache Performance Experiments for ADK Context Caching
This script runs two experiments to compare caching performance:
A. Gemini 2.0 Flash: Cache enabled vs disabled (explicit caching test)
B. Gemini 2.5 Flash: Implicit vs explicit caching comparison
"""
import argparse
import asyncio
import copy
import json
import logging
import sys
import time
from typing import Any
from typing import Dict
from typing import List
try:
# Try relative imports first (when run as module)
from .agent import app
from .utils import get_test_prompts
from .utils import run_experiment_batch
except ImportError:
# Fallback to direct imports (when run as script)
from agent import app
from utils import get_test_prompts
from utils import run_experiment_batch
from google.adk.cli.utils import logs
from google.adk.runners import InMemoryRunner
from google.adk.utils.cache_performance_analyzer import CachePerformanceAnalyzer
APP_NAME = "cache_analysis_experiments"
USER_ID = "cache_researcher"
def create_agent_variant(base_app, model_name: str, cache_enabled: bool):
"""Create an app variant with specified model and cache settings."""
import datetime
from google.adk.agents.context_cache_config import ContextCacheConfig
from google.adk.apps.app import App
# Extract the root agent and modify its model
agent_copy = copy.deepcopy(base_app.root_agent)
agent_copy.model = model_name
# Prepend dynamic timestamp to instruction to avoid implicit cache reuse across runs
current_timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
dynamic_prefix = f"Current session started at: {current_timestamp}\n\n"
agent_copy.instruction = dynamic_prefix + agent_copy.instruction
# Update agent name to reflect configuration
cache_status = "cached" if cache_enabled else "no_cache"
agent_copy.name = (
f"cache_analysis_{model_name.replace('.', '_').replace('-', '_')}_{cache_status}"
)
if cache_enabled:
# Use standardized cache config
cache_config = ContextCacheConfig(
min_tokens=4096,
ttl_seconds=600, # 10 mins for research sessions
cache_intervals=3, # Maximum invocations before cache refresh
)
else:
# Disable caching by setting config to None
cache_config = None
# Create new App with updated configuration
app_copy = App(
name=f"{base_app.name}_{cache_status}",
root_agent=agent_copy,
context_cache_config=cache_config,
)
return app_copy
async def run_cache_comparison_experiment(
model_name: str,
description: str,
cached_label: str,
uncached_label: str,
experiment_title: str,
reverse_order: bool = False,
request_delay: float = 2.0,
) -> Dict[str, Any]:
"""
Run a cache performance comparison experiment for a specific model.
Args:
model_name: Model to test (e.g., "gemini-2.5-flash")
description: Description of what the experiment tests
cached_label: Label for the cached experiment variant
uncached_label: Label for the uncached experiment variant
experiment_title: Title to display for the experiment
Returns:
Dictionary containing experiment results and performance comparison
"""
print("=" * 80)
print(f"EXPERIMENT {model_name}: {experiment_title}")
print("=" * 80)
print(f"Testing: {description}")
print(f"Model: {model_name}")
print()
# Create app variants
app_cached = create_agent_variant(app, model_name, cache_enabled=True)
app_uncached = create_agent_variant(app, model_name, cache_enabled=False)
# Get test prompts
prompts = get_test_prompts()
# Create runners
runner_cached = InMemoryRunner(app=app_cached, app_name=None)
runner_uncached = InMemoryRunner(app=app_uncached, app_name=None)
# Create sessions for each experiment to avoid cross-contamination
session_cached = await runner_cached.session_service.create_session(
app_name=runner_cached.app_name, user_id=USER_ID
)
session_uncached = await runner_uncached.session_service.create_session(
app_name=runner_uncached.app_name, user_id=USER_ID
)
if not reverse_order: # Default: uncached first
print("▶️ Running experiments in DEFAULT ORDER (uncached first)")
print()
# Test uncached version first
results_uncached = await run_experiment_batch(
app_uncached.root_agent.name,
runner_uncached,
USER_ID,
session_uncached.id,
prompts,
f"Experiment {model_name} - {uncached_label}",
request_delay=request_delay,
)
# Brief pause between experiments
await asyncio.sleep(5)
# Test cached version second
results_cached = await run_experiment_batch(
app_cached.root_agent.name,
runner_cached,
USER_ID,
session_cached.id,
prompts,
f"Experiment {model_name} - {cached_label}",
request_delay=request_delay,
)
else:
print("🔄 Running experiments in ALTERNATE ORDER (cached first)")
print()
# Test cached version first
results_cached = await run_experiment_batch(
app_cached.root_agent.name,
runner_cached,
USER_ID,
session_cached.id,
prompts,
f"Experiment {model_name} - {cached_label}",
request_delay=request_delay,
)
# Brief pause between experiments
await asyncio.sleep(5)
# Test uncached version second
results_uncached = await run_experiment_batch(
app_uncached.root_agent.name,
runner_uncached,
USER_ID,
session_uncached.id,
prompts,
f"Experiment {model_name} - {uncached_label}",
request_delay=request_delay,
)
# Analyze cache performance using CachePerformanceAnalyzer
performance_analysis = await analyze_cache_performance_from_sessions(
runner_cached,
session_cached,
runner_uncached,
session_uncached,
model_name,
)
# Extract metrics from analyzer for backward compatibility
cached_analysis = performance_analysis.get("cached_analysis", {})
uncached_analysis = performance_analysis.get("uncached_analysis", {})
cached_total_prompt_tokens = cached_analysis.get("total_prompt_tokens", 0)
cached_total_cached_tokens = cached_analysis.get("total_cached_tokens", 0)
cached_cache_hit_ratio = cached_analysis.get("cache_hit_ratio_percent", 0.0)
cached_cache_utilization_ratio = cached_analysis.get(
"cache_utilization_ratio_percent", 0.0
)
cached_avg_cached_tokens_per_request = cached_analysis.get(
"avg_cached_tokens_per_request", 0.0
)
cached_requests_with_hits = cached_analysis.get("requests_with_cache_hits", 0)
total_cached_requests = cached_analysis.get("total_requests", 0)
uncached_total_prompt_tokens = uncached_analysis.get("total_prompt_tokens", 0)
uncached_total_cached_tokens = uncached_analysis.get("total_cached_tokens", 0)
uncached_cache_hit_ratio = uncached_analysis.get(
"cache_hit_ratio_percent", 0.0
)
uncached_cache_utilization_ratio = uncached_analysis.get(
"cache_utilization_ratio_percent", 0.0
)
uncached_avg_cached_tokens_per_request = uncached_analysis.get(
"avg_cached_tokens_per_request", 0.0
)
uncached_requests_with_hits = uncached_analysis.get(
"requests_with_cache_hits", 0
)
total_uncached_requests = uncached_analysis.get("total_requests", 0)
summary = {
"experiment": model_name,
"description": description,
"model": model_name,
"cached_results": results_cached,
"uncached_results": results_uncached,
"cache_analysis": {
"cached_experiment": {
"cache_hit_ratio_percent": cached_cache_hit_ratio,
"cache_utilization_ratio_percent": cached_cache_utilization_ratio,
"total_prompt_tokens": cached_total_prompt_tokens,
"total_cached_tokens": cached_total_cached_tokens,
"avg_cached_tokens_per_request": (
cached_avg_cached_tokens_per_request
),
"requests_with_cache_hits": cached_requests_with_hits,
"total_requests": total_cached_requests,
},
"uncached_experiment": {
"cache_hit_ratio_percent": uncached_cache_hit_ratio,
"cache_utilization_ratio_percent": (
uncached_cache_utilization_ratio
),
"total_prompt_tokens": uncached_total_prompt_tokens,
"total_cached_tokens": uncached_total_cached_tokens,
"avg_cached_tokens_per_request": (
uncached_avg_cached_tokens_per_request
),
"requests_with_cache_hits": uncached_requests_with_hits,
"total_requests": total_uncached_requests,
},
},
}
print(f"📊 EXPERIMENT {model_name} CACHE ANALYSIS:")
print(f" 🔥 {cached_label}:")
print(
f" Cache Hit Ratio: {cached_cache_hit_ratio:.1f}%"
f" ({cached_total_cached_tokens:,} /"
f" {cached_total_prompt_tokens:,} tokens)"
)
print(
f" Cache Utilization: {cached_cache_utilization_ratio:.1f}%"
f" ({cached_requests_with_hits}/{total_cached_requests} requests)"
)
print(
" Avg Cached Tokens/Request:"
f" {cached_avg_cached_tokens_per_request:.0f}"
)
print(f" ❄️ {uncached_label}:")
print(
f" Cache Hit Ratio: {uncached_cache_hit_ratio:.1f}%"
f" ({uncached_total_cached_tokens:,} /"
f" {uncached_total_prompt_tokens:,} tokens)"
)
print(
f" Cache Utilization: {uncached_cache_utilization_ratio:.1f}%"
f" ({uncached_requests_with_hits}/{total_uncached_requests} requests)"
)
print(
" Avg Cached Tokens/Request:"
f" {uncached_avg_cached_tokens_per_request:.0f}"
)
print()
# Add performance analysis to summary
summary["performance_analysis"] = performance_analysis
return summary
async def analyze_cache_performance_from_sessions(
runner_cached,
session_cached,
runner_uncached,
session_uncached,
model_name: str,
) -> Dict[str, Any]:
"""Analyze cache performance using CachePerformanceAnalyzer."""
print("📊 ANALYZING CACHE PERFORMANCE WITH CachePerformanceAnalyzer...")
analyzer_cached = CachePerformanceAnalyzer(runner_cached.session_service)
analyzer_uncached = CachePerformanceAnalyzer(runner_uncached.session_service)
# Analyze cached experiment
try:
cached_analysis = await analyzer_cached.analyze_agent_cache_performance(
session_cached.id,
USER_ID,
runner_cached.app_name,
f"cache_analysis_{model_name.replace('.', '_').replace('-', '_')}_cached",
)
print(f" 🔥 Cached Experiment Analysis:")
print(f" Status: {cached_analysis['status']}")
if cached_analysis["status"] == "active":
print(
" Cache Hit Ratio:"
f" {cached_analysis['cache_hit_ratio_percent']:.1f}%"
f" ({cached_analysis['total_cached_tokens']:,} /"
f" {cached_analysis['total_prompt_tokens']:,} tokens)"
)
print(
" Cache Utilization:"
f" {cached_analysis['cache_utilization_ratio_percent']:.1f}%"
f" ({cached_analysis['requests_with_cache_hits']}/{cached_analysis['total_requests']}"
" requests)"
)
print(
" Avg Cached Tokens/Request:"
f" {cached_analysis['avg_cached_tokens_per_request']:.0f}"
)
print(
f" Requests with cache: {cached_analysis['requests_with_cache']}"
)
print(
" Avg invocations used:"
f" {cached_analysis['avg_invocations_used']:.1f}"
)
print(f" Cache refreshes: {cached_analysis['cache_refreshes']}")
print(f" Total invocations: {cached_analysis['total_invocations']}")
except Exception as e:
print(f" ❌ Error analyzing cached experiment: {e}")
cached_analysis = {"status": "error", "error": str(e)}
# Analyze uncached experiment
try:
uncached_analysis = await analyzer_uncached.analyze_agent_cache_performance(
session_uncached.id,
USER_ID,
runner_uncached.app_name,
f"cache_analysis_{model_name.replace('.', '_').replace('-', '_')}_no_cache",
)
print(f" ❄️ Uncached Experiment Analysis:")
print(f" Status: {uncached_analysis['status']}")
if uncached_analysis["status"] == "active":
print(
" Cache Hit Ratio:"
f" {uncached_analysis['cache_hit_ratio_percent']:.1f}%"
f" ({uncached_analysis['total_cached_tokens']:,} /"
f" {uncached_analysis['total_prompt_tokens']:,} tokens)"
)
print(
" Cache Utilization:"
f" {uncached_analysis['cache_utilization_ratio_percent']:.1f}%"
f" ({uncached_analysis['requests_with_cache_hits']}/{uncached_analysis['total_requests']}"
" requests)"
)
print(
" Avg Cached Tokens/Request:"
f" {uncached_analysis['avg_cached_tokens_per_request']:.0f}"
)
print(
" Requests with cache:"
f" {uncached_analysis['requests_with_cache']}"
)
print(
" Avg invocations used:"
f" {uncached_analysis['avg_invocations_used']:.1f}"
)
print(f" Cache refreshes: {uncached_analysis['cache_refreshes']}")
print(f" Total invocations: {uncached_analysis['total_invocations']}")
except Exception as e:
print(f" ❌ Error analyzing uncached experiment: {e}")
uncached_analysis = {"status": "error", "error": str(e)}
print()
return {
"cached_analysis": cached_analysis,
"uncached_analysis": uncached_analysis,
}
def get_experiment_labels(model_name: str) -> Dict[str, str]:
"""Get experiment labels and titles for a given model."""
# Determine experiment type based on model name
if "2.5" in model_name:
# Gemini 2.5 models have implicit caching
return {
"description": "Google implicit caching vs ADK explicit caching",
"cached_label": "Explicit Caching",
"uncached_label": "Implicit Caching",
"experiment_title": "Implicit vs Explicit Caching",
}
else:
# Other models (2.0, etc.) test explicit caching vs no caching
return {
"description": "ADK explicit caching enabled vs disabled",
"cached_label": "Cached",
"uncached_label": "Uncached",
"experiment_title": "Cache Performance Comparison",
}
def calculate_averaged_results(
all_results: List[Dict[str, Any]], model_name: str
) -> Dict[str, Any]:
"""Calculate averaged results from multiple experiment runs."""
if not all_results:
raise ValueError("No results to average")
# Calculate average cache metrics
cache_hit_ratios = [
r["cache_analysis"]["cache_hit_ratio_percent"] for r in all_results
]
cache_utilization_ratios = [
r["cache_analysis"]["cache_utilization_ratio_percent"]
for r in all_results
]
total_prompt_tokens = [
r["cache_analysis"]["total_prompt_tokens"] for r in all_results
]
total_cached_tokens = [
r["cache_analysis"]["total_cached_tokens"] for r in all_results
]
avg_cached_tokens_per_request = [
r["cache_analysis"]["avg_cached_tokens_per_request"] for r in all_results
]
requests_with_cache_hits = [
r["cache_analysis"]["requests_with_cache_hits"] for r in all_results
]
def safe_average(values):
"""Calculate average, handling empty lists."""
return sum(values) / len(values) if values else 0.0
# Create averaged result
averaged_result = {
"experiment": model_name,
"description": all_results[0]["description"],
"model": model_name,
"individual_runs": (
all_results
), # Keep all individual results for reference
"averaged_cache_analysis": {
"cache_hit_ratio_percent": safe_average(cache_hit_ratios),
"cache_utilization_ratio_percent": safe_average(
cache_utilization_ratios
),
"total_prompt_tokens": safe_average(total_prompt_tokens),
"total_cached_tokens": safe_average(total_cached_tokens),
"avg_cached_tokens_per_request": safe_average(
avg_cached_tokens_per_request
),
"requests_with_cache_hits": safe_average(requests_with_cache_hits),
},
"statistics": {
"runs_completed": len(all_results),
"cache_hit_ratio_std": _calculate_std(cache_hit_ratios),
"cache_utilization_std": _calculate_std(cache_utilization_ratios),
"cached_tokens_per_request_std": _calculate_std(
avg_cached_tokens_per_request
),
},
}
# Print averaged results
print("\n📊 AVERAGED CACHE ANALYSIS RESULTS:")
print("=" * 80)
avg_cache = averaged_result["averaged_cache_analysis"]
stats = averaged_result["statistics"]
print(f" Runs completed: {stats['runs_completed']}")
print(
f" Average Cache Hit Ratio: {avg_cache['cache_hit_ratio_percent']:.1f}%"
f"{stats['cache_hit_ratio_std']:.1f}%)"
)
print(
" Average Cache Utilization:"
f" {avg_cache['cache_utilization_ratio_percent']:.1f}%"
f"{stats['cache_utilization_std']:.1f}%)"
)
print(
" Average Cached Tokens/Request:"
f" {avg_cache['avg_cached_tokens_per_request']:.0f}"
f"{stats['cached_tokens_per_request_std']:.0f})"
)
print()
return averaged_result
def _calculate_std(values):
"""Calculate standard deviation."""
if len(values) <= 1:
return 0.0
mean = sum(values) / len(values)
variance = sum((x - mean) ** 2 for x in values) / len(values)
return variance**0.5
def save_results(results: Dict[str, Any], filename: str):
"""Save experiment results to JSON file."""
with open(filename, "w") as f:
json.dump(results, f, indent=2)
print(f"💾 Results saved to: {filename}")
async def main():
"""Run cache performance experiment for a specific model."""
parser = argparse.ArgumentParser(
description="ADK Cache Performance Experiment"
)
parser.add_argument(
"model",
help="Model to test (e.g., gemini-2.5-flash)",
)
parser.add_argument(
"--output",
help="Output filename for results (default: cache_{model}_results.json)",
)
parser.add_argument(
"--repeat",
type=int,
default=1,
help=(
"Number of times to repeat each experiment for averaged results"
" (default: 1)"
),
)
parser.add_argument(
"--cached-first",
action="store_true",
help="Run cached experiment first (default: uncached first)",
)
parser.add_argument(
"--request-delay",
type=float,
default=2.0,
help=(
"Delay in seconds between API requests to avoid overloading (default:"
" 2.0)"
),
)
parser.add_argument(
"--log-level",
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
default="INFO",
help="Set logging level (default: INFO)",
)
args = parser.parse_args()
# Setup logger with specified level
log_level = getattr(logging, args.log_level.upper())
logs.setup_adk_logger(log_level)
# Set default output filename based on model
if not args.output:
args.output = (
f"cache_{args.model.replace('.', '_').replace('-', '_')}_results.json"
)
print("🧪 ADK CONTEXT CACHE PERFORMANCE EXPERIMENT")
print("=" * 80)
print(f"Start time: {time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"Model: {args.model}")
print(f"Repetitions: {args.repeat}")
print()
start_time = time.time()
try:
# Get experiment labels for the model
labels = get_experiment_labels(args.model)
# Run the experiment multiple times if repeat > 1
if args.repeat == 1:
# Single run
result = await run_cache_comparison_experiment(
model_name=args.model,
reverse_order=args.cached_first,
request_delay=args.request_delay,
**labels,
)
else:
# Multiple runs with averaging
print(f"🔄 Running experiment {args.repeat} times for averaged results")
print("=" * 80)
all_results = []
for run_num in range(args.repeat):
print(f"\n🏃 RUN {run_num + 1}/{args.repeat}")
print("-" * 40)
run_result = await run_cache_comparison_experiment(
model_name=args.model,
reverse_order=args.cached_first,
request_delay=args.request_delay,
**labels,
)
all_results.append(run_result)
# Brief pause between runs
if run_num < args.repeat - 1:
print("⏸️ Pausing 10 seconds between runs...")
await asyncio.sleep(10)
# Calculate averaged results
result = calculate_averaged_results(all_results, args.model)
# Add completion metadata
result["end_time"] = time.strftime("%Y-%m-%d %H:%M:%S")
result["total_duration"] = time.time() - start_time
result["repetitions"] = args.repeat
except KeyboardInterrupt:
print("\n⚠️ Experiment interrupted by user")
sys.exit(1)
except Exception as e:
print(f"\n❌ Experiment failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
# Save results
save_results(result, args.output)
# Print final summary
print("=" * 80)
print("🎉 EXPERIMENT COMPLETED SUCCESSFULLY!")
print("=" * 80)
# Handle both single and averaged results
if args.repeat == 1:
cached_exp = result["cache_analysis"]["cached_experiment"]
uncached_exp = result["cache_analysis"]["uncached_experiment"]
labels = get_experiment_labels(args.model)
print(f"{args.model}:")
print(f" 🔥 {labels['cached_label']}:")
print(f" Cache Hit Ratio: {cached_exp['cache_hit_ratio_percent']:.1f}%")
print(
" Cache Utilization:"
f" {cached_exp['cache_utilization_ratio_percent']:.1f}%"
)
print(
" Cached Tokens/Request:"
f" {cached_exp['avg_cached_tokens_per_request']:.0f}"
)
print(f" ❄️ {labels['uncached_label']}:")
print(
f" Cache Hit Ratio: {uncached_exp['cache_hit_ratio_percent']:.1f}%"
)
print(
" Cache Utilization:"
f" {uncached_exp['cache_utilization_ratio_percent']:.1f}%"
)
print(
" Cached Tokens/Request:"
f" {uncached_exp['avg_cached_tokens_per_request']:.0f}"
)
else:
# For averaged results, show summary comparison
cached_exp = result["averaged_cache_analysis"]["cached_experiment"]
uncached_exp = result["averaged_cache_analysis"]["uncached_experiment"]
labels = get_experiment_labels(args.model)
print(f"{args.model} (averaged over {args.repeat} runs):")
print(f" 🔥 {labels['cached_label']} vs ❄️ {labels['uncached_label']}:")
print(
f" Cache Hit Ratio: {cached_exp['cache_hit_ratio_percent']:.1f}% vs"
f" {uncached_exp['cache_hit_ratio_percent']:.1f}%"
)
print(
" Cache Utilization:"
f" {cached_exp['cache_utilization_ratio_percent']:.1f}% vs"
f" {uncached_exp['cache_utilization_ratio_percent']:.1f}%"
)
print(f"\nTotal execution time: {result['total_duration']:.2f} seconds")
print(f"Results saved to: {args.output}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,272 @@
# 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.
"""Utility functions for cache analysis experiments."""
import asyncio
import time
from typing import Any
from typing import Dict
from typing import List
from google.adk.runners import InMemoryRunner
async def call_agent_async(
runner: InMemoryRunner, user_id: str, session_id: str, prompt: str
) -> Dict[str, Any]:
"""Call agent asynchronously and return response with token usage."""
from google.genai import types
response_parts = []
token_usage = {
"prompt_token_count": 0,
"candidates_token_count": 0,
"cached_content_token_count": 0,
"total_token_count": 0,
}
async for event in runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=types.Content(parts=[types.Part(text=prompt)], role="user"),
):
if event.content and event.content.parts:
for part in event.content.parts:
if hasattr(part, "text") and part.text:
response_parts.append(part.text)
# Collect token usage information
if event.usage_metadata:
if (
hasattr(event.usage_metadata, "prompt_token_count")
and event.usage_metadata.prompt_token_count
):
token_usage[
"prompt_token_count"
] += event.usage_metadata.prompt_token_count
if (
hasattr(event.usage_metadata, "candidates_token_count")
and event.usage_metadata.candidates_token_count
):
token_usage[
"candidates_token_count"
] += event.usage_metadata.candidates_token_count
if (
hasattr(event.usage_metadata, "cached_content_token_count")
and event.usage_metadata.cached_content_token_count
):
token_usage[
"cached_content_token_count"
] += event.usage_metadata.cached_content_token_count
if (
hasattr(event.usage_metadata, "total_token_count")
and event.usage_metadata.total_token_count
):
token_usage[
"total_token_count"
] += event.usage_metadata.total_token_count
response_text = "".join(response_parts)
return {"response_text": response_text, "token_usage": token_usage}
def get_test_prompts() -> List[str]:
"""Get a standardized set of test prompts for cache analysis experiments.
Designed for consistent behavior:
- Prompts 1-5: Will NOT trigger function calls (general questions)
- Prompts 6-10: Will trigger function calls (specific tool requests)
"""
return [
# === PROMPTS THAT WILL NOT TRIGGER FUNCTION CALLS ===
# (General questions that don't match specific tool descriptions)
"Hello, what can you do for me?",
(
"What is artificial intelligence and how does it work in modern"
" applications?"
),
"Explain the difference between machine learning and deep learning.",
"What are the main challenges in implementing AI systems at scale?",
"How do recommendation systems work in modern e-commerce platforms?",
# === PROMPTS THAT WILL TRIGGER FUNCTION CALLS ===
# (Specific requests with all required parameters clearly specified)
(
"Use benchmark_performance with system_name='E-commerce Platform',"
" metrics=['latency', 'throughput'], duration='standard',"
" load_profile='realistic'."
),
(
"Call analyze_user_behavior_patterns with"
" user_segment='premium_customers', time_period='last_30_days',"
" metrics=['engagement', 'conversion']."
),
(
"Run market_research_analysis for industry='fintech',"
" focus_areas=['user_experience', 'security'],"
" report_depth='comprehensive'."
),
(
"Execute competitive_analysis with competitors=['Netflix',"
" 'Disney+'], analysis_type='feature_comparison',"
" output_format='detailed'."
),
(
"Perform content_performance_evaluation on content_type='video',"
" platform='social_media', success_metrics=['views', 'engagement']."
),
]
async def run_experiment_batch(
agent_name: str,
runner: InMemoryRunner,
user_id: str,
session_id: str,
prompts: List[str],
experiment_name: str,
request_delay: float = 2.0,
) -> Dict[str, Any]:
"""Run a batch of prompts and collect cache metrics."""
results = []
print(f"🧪 Running {experiment_name}")
print(f"Agent: {agent_name}")
print(f"Session: {session_id}")
print(f"Prompts: {len(prompts)}")
print(f"Request delay: {request_delay}s between calls")
print("-" * 60)
for i, prompt in enumerate(prompts, 1):
print(f"[{i}/{len(prompts)}] Running test prompt...")
print(f"Prompt: {prompt[:100]}...")
try:
agent_response = await call_agent_async(
runner, user_id, session_id, prompt
)
result = {
"prompt_number": i,
"prompt": prompt,
"response_length": len(agent_response["response_text"]),
"success": True,
"error": None,
"token_usage": agent_response["token_usage"],
}
# Extract token usage for individual prompt statistics
prompt_tokens = agent_response["token_usage"].get("prompt_token_count", 0)
cached_tokens = agent_response["token_usage"].get(
"cached_content_token_count", 0
)
print(
"✅ Completed (Response:"
f" {len(agent_response['response_text'])} chars)"
)
print(
f" 📊 Tokens - Prompt: {prompt_tokens:,}, Cached: {cached_tokens:,}"
)
except Exception as e:
result = {
"prompt_number": i,
"prompt": prompt,
"response_length": 0,
"success": False,
"error": str(e),
"token_usage": {
"prompt_token_count": 0,
"candidates_token_count": 0,
"cached_content_token_count": 0,
"total_token_count": 0,
},
}
print(f"❌ Failed: {e}")
results.append(result)
# Configurable pause between requests to avoid API overload
if i < len(prompts): # Don't sleep after the last request
print(f" ⏸️ Waiting {request_delay}s before next request...")
await asyncio.sleep(request_delay)
successful_requests = sum(1 for r in results if r["success"])
# Calculate cache statistics for this batch
total_prompt_tokens = sum(
r.get("token_usage", {}).get("prompt_token_count", 0) for r in results
)
total_cached_tokens = sum(
r.get("token_usage", {}).get("cached_content_token_count", 0)
for r in results
)
# Calculate cache hit ratio
if total_prompt_tokens > 0:
cache_hit_ratio = (total_cached_tokens / total_prompt_tokens) * 100
else:
cache_hit_ratio = 0.0
# Calculate cache utilization
requests_with_cache_hits = sum(
1
for r in results
if r.get("token_usage", {}).get("cached_content_token_count", 0) > 0
)
cache_utilization_ratio = (
(requests_with_cache_hits / len(prompts)) * 100 if prompts else 0.0
)
# Average cached tokens per request
avg_cached_tokens_per_request = (
total_cached_tokens / len(prompts) if prompts else 0.0
)
summary = {
"experiment_name": experiment_name,
"agent_name": agent_name,
"total_requests": len(prompts),
"successful_requests": successful_requests,
"results": results,
"cache_statistics": {
"cache_hit_ratio_percent": cache_hit_ratio,
"cache_utilization_ratio_percent": cache_utilization_ratio,
"total_prompt_tokens": total_prompt_tokens,
"total_cached_tokens": total_cached_tokens,
"avg_cached_tokens_per_request": avg_cached_tokens_per_request,
"requests_with_cache_hits": requests_with_cache_hits,
},
}
print("-" * 60)
print(f"{experiment_name} completed:")
print(f" Total requests: {len(prompts)}")
print(f" Successful: {successful_requests}/{len(prompts)}")
print(" 📊 BATCH CACHE STATISTICS:")
print(
f" Cache Hit Ratio: {cache_hit_ratio:.1f}%"
f" ({total_cached_tokens:,} / {total_prompt_tokens:,} tokens)"
)
print(
f" Cache Utilization: {cache_utilization_ratio:.1f}%"
f" ({requests_with_cache_hits}/{len(prompts)} requests)"
)
print(f" Avg Cached Tokens/Request: {avg_cached_tokens_per_request:.0f}")
print()
return summary
@@ -0,0 +1,15 @@
# 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.
from . import agent
@@ -0,0 +1,115 @@
# 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.
import random
from google.adk.agents.callback_context import CallbackContext
from google.adk.agents.llm_agent import Agent
from google.adk.models.llm_request import LlmRequest
from google.adk.tools.tool_context import ToolContext
def roll_die(sides: int, tool_context: ToolContext) -> int:
"""Roll a die and return the rolled result.
Args:
sides: The integer number of sides the die has.
Returns:
An integer of the result of rolling the die.
"""
result = random.randint(1, sides)
if not 'rolls' in tool_context.state:
tool_context.state['rolls'] = []
tool_context.state['rolls'] = tool_context.state['rolls'] + [result]
return result
async def check_prime(nums: list[int]) -> str:
"""Check if a given list of numbers are prime.
Args:
nums: The list of numbers to check.
Returns:
A str indicating which number is prime.
"""
primes = set()
for number in nums:
number = int(number)
if number <= 1:
continue
is_prime = True
for i in range(2, int(number**0.5) + 1):
if number % i == 0:
is_prime = False
break
if is_prime:
primes.add(number)
return (
'No prime numbers found.'
if not primes
else f"{', '.join(str(num) for num in primes)} are prime numbers."
)
def create_slice_history_callback(n_recent_turns):
async def before_model_callback(
callback_context: CallbackContext, llm_request: LlmRequest
):
if n_recent_turns < 1:
return
user_indexes = [
i
for i, content in enumerate(llm_request.contents)
if content.role == 'user'
]
if n_recent_turns > len(user_indexes):
return
suffix_idx = user_indexes[-n_recent_turns]
llm_request.contents = llm_request.contents[suffix_idx:]
return before_model_callback
root_agent = Agent(
name='short_history_agent',
description=(
'an agent that maintains only the last turn in its context window.'
' numbers.'
),
instruction="""
You roll dice and answer questions about the outcome of the dice rolls.
You can roll dice of different sizes.
You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
It is ok to discuss previous dice roles, and comment on the dice rolls.
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
You should never roll a die on your own.
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
You should not check prime numbers before calling the tool.
When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
3. When you respond, you must include the roll_die result from step 1.
You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
You should not rely on the previous history on prime results.
""",
tools=[roll_die, check_prime],
before_model_callback=create_slice_history_callback(n_recent_turns=2),
)
@@ -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.
import asyncio
import time
import warnings
import agent
from dotenv import load_dotenv
from google.adk import Runner
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.adk.cli.utils import logs
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.sessions.session import Session
from google.genai import types
load_dotenv(override=True)
warnings.filterwarnings('ignore', category=UserWarning)
logs.log_to_tmp_folder()
async def main():
app_name = 'my_app'
user_id_1 = 'user1'
session_service = InMemorySessionService()
artifact_service = InMemoryArtifactService()
runner = Runner(
app_name=app_name,
agent=agent.root_agent,
artifact_service=artifact_service,
session_service=session_service,
)
session_11 = await session_service.create_session(
app_name=app_name, user_id=user_id_1
)
async def run_prompt(session: Session, new_message: str):
content = types.Content(
role='user', parts=[types.Part.from_text(text=new_message)]
)
print('** User says:', content.model_dump(exclude_none=True))
async for event in runner.run_async(
user_id=user_id_1,
session_id=session.id,
new_message=content,
):
if event.content.parts and event.content.parts[0].text:
print(f'** {event.author}: {event.content.parts[0].text}')
start_time = time.time()
print('Start time:', start_time)
print('------------------------------------')
await run_prompt(session_11, 'Hi')
await run_prompt(session_11, 'Roll a die with 100 sides')
await run_prompt(session_11, 'Roll a die again with 100 sides.')
await run_prompt(session_11, 'What numbers did I got?')
print(
await artifact_service.list_artifact_keys(
app_name=app_name, user_id=user_id_1, session_id=session_11.id
)
)
end_time = time.time()
print('------------------------------------')
print('End time:', end_time)
print('Total time:', end_time - start_time)
if __name__ == '__main__':
asyncio.run(main())
+15
View File
@@ -0,0 +1,15 @@
# 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.
from . import agent
+41
View File
@@ -0,0 +1,41 @@
# 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.
from datetime import datetime
from google.adk import Agent
from google.adk.agents.callback_context import CallbackContext
from google.adk.tools.load_memory_tool import load_memory_tool
from google.adk.tools.preload_memory_tool import preload_memory_tool
def update_current_time(callback_context: CallbackContext):
callback_context.state['_time'] = datetime.now().isoformat()
root_agent = Agent(
name='memory_agent',
description='agent that have access to memory tools.',
before_agent_callback=update_current_time,
instruction="""\
You are an agent that help user answer questions.
Current time: {_time}
""",
tools=[
load_memory_tool,
preload_memory_tool,
],
)
+109
View File
@@ -0,0 +1,109 @@
# 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.
import asyncio
from datetime import datetime
from datetime import timedelta
from typing import cast
import agent
from dotenv import load_dotenv
from google.adk.cli.utils import logs
from google.adk.runners import InMemoryRunner
from google.adk.sessions.session import Session
from google.genai import types
load_dotenv(override=True)
logs.log_to_tmp_folder()
async def main():
app_name = 'my_app'
user_id_1 = 'user1'
runner = InMemoryRunner(
app_name=app_name,
agent=agent.root_agent,
)
async def run_prompt(session: Session, new_message: str) -> Session:
content = types.Content(
role='user', parts=[types.Part.from_text(text=new_message)]
)
print('** User says:', content.model_dump(exclude_none=True))
async for event in runner.run_async(
user_id=user_id_1,
session_id=session.id,
new_message=content,
):
if not event.content or not event.content.parts:
continue
if event.content.parts[0].text:
print(f'** {event.author}: {event.content.parts[0].text}')
elif event.content.parts[0].function_call:
print(
f'** {event.author}: fc /'
f' {event.content.parts[0].function_call.name} /'
f' {event.content.parts[0].function_call.args}\n'
)
elif event.content.parts[0].function_response:
print(
f'** {event.author}: fr /'
f' {event.content.parts[0].function_response.name} /'
f' {event.content.parts[0].function_response.response}\n'
)
return cast(
Session,
await runner.session_service.get_session(
app_name=app_name, user_id=user_id_1, session_id=session.id
),
)
session_1 = await runner.session_service.create_session(
app_name=app_name, user_id=user_id_1
)
print(f'----Session to create memory: {session_1.id} ----------------------')
session_1 = await run_prompt(session_1, 'Hi')
session_1 = await run_prompt(session_1, 'My name is Jack')
session_1 = await run_prompt(session_1, 'I like badminton.')
session_1 = await run_prompt(
session_1,
f'I ate a burger on {(datetime.now() - timedelta(days=1)).date()}.',
)
session_1 = await run_prompt(
session_1,
f'I ate a banana on {(datetime.now() - timedelta(days=2)).date()}.',
)
print('Saving session to memory service...')
if runner.memory_service:
await runner.memory_service.add_session_to_memory(session_1)
print('-------------------------------------------------------------------')
session_2 = await runner.session_service.create_session(
app_name=app_name, user_id=user_id_1
)
print(f'----Session to use memory: {session_2.id} ----------------------')
session_2 = await run_prompt(session_2, 'Hi')
session_2 = await run_prompt(session_2, 'What do I like to do?')
# ** memory_agent: You like badminton.
session_2 = await run_prompt(session_2, 'When did I say that?')
# ** memory_agent: You said you liked badminton on ...
session_2 = await run_prompt(session_2, 'What did I eat yesterday?')
# ** memory_agent: You ate a burger yesterday...
print('-------------------------------------------------------------------')
if __name__ == '__main__':
asyncio.run(main())
@@ -0,0 +1,56 @@
# Loading and Upgrading Old Session Databases
This example demonstrates how to upgrade a session database created with an older version of ADK to be compatible with the current version.
## Sample Database
This sample includes `dnd_sessions.db`, a database created with ADK v1.15.0. The following steps show how to run into a schema error and then resolve it using the migration script.
## 1. Reproduce the Error
First, copy the old database to `sessions.db`, which is the file the sample application expects.
```bash
cp dnd_sessions.db sessions.db
python main.py
```
Running the application against the old database will fail with a schema mismatch error, as the `events` table is missing a column required by newer ADK versions:
```
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: events.usage_metadata
```
## 2. Upgrade the Database Schema
ADK provides a migration script to update the database schema. Run the following command to download and execute it.
```bash
# Clean up the previous run before executing the migration
cp dnd_sessions.db sessions.db
# Download and run the migration script
curl -fsSL https://raw.githubusercontent.com/google/adk-python/main/scripts/db_migration.sh | sh -s -- "sqlite:///%(here)s/sessions.db" "google.adk.sessions.database_session_service"
```
This script uses `alembic` to compare the existing schema against the current model definition and automatically generates and applies the necessary migrations.
**Note on generated files:**
- The script will create an `alembic.ini` file and an `alembic/` directory. You must delete these before re-running the script.
- The `sample-output` directory in this example contains a reference of the generated files for your inspection.
- The `%(here)s` variable in the database URL is an `alembic` placeholder that refers to the current directory.
## 3. Run the Agent Successfully
With the database schema updated, the application can now load the session correctly.
```bash
python main.py
```
You should see output indicating that the old session was successfully loaded.
## Limitations
The migration script is designed to add new columns that have been introduced in newer ADK versions. It does not handle more complex schema changes, such as modifying a column's data type (e.g., from `int` to `string`) or altering the internal structure of stored data.
@@ -0,0 +1,16 @@
# 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.
from . import agent
@@ -0,0 +1,88 @@
# 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.
import random
from google.adk.agents.llm_agent import Agent
def roll_die(sides: int) -> int:
"""Roll a die and return the rolled result.
Args:
sides: The integer number of sides the die has.
Returns:
An integer of the result of rolling the die.
"""
return random.randint(1, sides)
async def check_prime(nums: list[int]) -> str:
"""Check if a given list of numbers are prime.
Args:
nums: The list of numbers to check.
Returns:
A str indicating which number is prime.
"""
primes = set()
for number in nums:
number = int(number)
if number <= 1:
continue
is_prime = True
for i in range(2, int(number**0.5) + 1):
if number % i == 0:
is_prime = False
break
if is_prime:
primes.add(number)
return (
"No prime numbers found."
if not primes
else f"{', '.join(str(num) for num in primes)} are prime numbers."
)
root_agent = Agent(
name="migrate_session_db_agent",
description=(
"hello world agent that can roll a dice of 8 sides and check prime"
" numbers."
),
instruction="""
You roll dice and answer questions about the outcome of the dice rolls.
You can roll dice of different sizes.
You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
It is ok to discuss previous dice roles, and comment on the dice rolls.
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
You should never roll a die on your own.
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
You should not check prime numbers before calling the tool.
When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
3. When you respond, you must include the roll_die result from step 1.
You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
You should not rely on the previous history on prime results.
""",
tools=[
roll_die,
check_prime,
],
)
@@ -0,0 +1,79 @@
# 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.
import asyncio
import time
import agent
from dotenv import load_dotenv
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.adk.cli.utils import logs
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)
logs.log_to_tmp_folder()
async def main():
app_name = 'migrate_session_db_app'
user_id_1 = 'user1'
session_service = DatabaseSessionService('sqlite+aiosqlite:///./sessions.db')
artifact_service = InMemoryArtifactService()
runner = Runner(
app_name=app_name,
agent=agent.root_agent,
artifact_service=artifact_service,
session_service=session_service,
)
session_11 = await session_service.get_session(
app_name=app_name,
user_id=user_id_1,
session_id='aee03f34-32ef-432b-b1bb-e66a3a79dd5b',
)
print('Session 11 loaded:', session_11.id)
async def run_prompt(session: Session, new_message: str):
content = types.Content(
role='user', parts=[types.Part.from_text(text=new_message)]
)
print('** User says:', content.model_dump(exclude_none=True))
async for event in runner.run_async(
user_id=user_id_1,
session_id=session.id,
new_message=content,
):
if event.content.parts and event.content.parts[0].text:
print(f'** {event.author}: {event.content.parts[0].text}')
start_time = time.time()
print('Start time:', start_time)
print('------------------------------------')
await run_prompt(session_11, 'Hi, introduce yourself.')
await run_prompt(
session_11, 'Roll a die with 100 sides and check if it is prime'
)
await run_prompt(session_11, 'Roll it again.')
await run_prompt(session_11, 'What numbers did I got?')
end_time = time.time()
print('------------------------------------')
print('End time:', end_time)
print('Total time:', end_time - start_time)
if __name__ == '__main__':
asyncio.run(main())
@@ -0,0 +1,147 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.10 and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = sqlite:///%(here)s/sessions.db
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
@@ -0,0 +1 @@
Generic single-database configuration.
@@ -0,0 +1,90 @@
# 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.
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config
from sqlalchemy import pool
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
from google.adk.sessions.database_session_service import Base
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
@@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}
@@ -0,0 +1,197 @@
# 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 |
| timestamp | TIMESTAMP | Event timestamp |
| event_data | JSONB | Event content as JSON |
### 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 |
### adk_internal_metadata
| Column | Type | Description |
| ------ | ------------ | -------------- |
| key | VARCHAR(128) | Metadata key |
| value | VARCHAR(256) | Metadata value |
## 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://postgres:postgres@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://postgres:postgres@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-gcp-project-id>
GOOGLE_CLOUD_LOCATION=us-central1
GOOGLE_GENAI_USE_ENTERPRISE=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_ENTERPRISE=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
1. **SSL/TLS**: Always use encrypted connections in production
1. **Backups**: Implement regular backup strategies for session data
1. **Indexing**: The default schema includes primary key indexes; add additional indexes based on query patterns
1. **Monitoring**: Monitor connection pool usage and query performance
@@ -0,0 +1,16 @@
# 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.
from . import agent
@@ -0,0 +1,42 @@
# 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.
"""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(
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],
)
@@ -0,0 +1,38 @@
# 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.
# Docker Compose configuration for the postgres_session_service sample.
#
# This file defines a PostgreSQL service used to demonstrate ADK's
# DatabaseSessionService with a persistent backend. It sets up a
# postgres:16-alpine container with:
# - Default credentials (user: postgres, password: postgres)
# - A pre-created database named 'adk_sessions'
# - Port 5432 exposed for local access
# - A named volume 'postgres_data' for data persistence
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:
@@ -0,0 +1,95 @@
# 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.
"""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())
@@ -0,0 +1,15 @@
# 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.
from . import agent
@@ -0,0 +1,70 @@
# 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.
from google.adk import Agent
from google.adk.tools.tool_context import ToolContext
from google.genai import types
async def update_state(tool_context: ToolContext, key: str, value: str) -> dict:
"""Updates a state value."""
tool_context.state[key] = value
return {"status": f"Updated state '{key}' to '{value}'"}
async def load_state(tool_context: ToolContext, key: str) -> dict:
"""Loads a state value."""
return {key: tool_context.state.get(key)}
async def save_artifact(
tool_context: ToolContext, filename: str, content: str
) -> dict:
"""Saves an artifact with the given filename and content."""
artifact_bytes = content.encode("utf-8")
artifact_part = types.Part(
inline_data=types.Blob(mime_type="text/plain", data=artifact_bytes)
)
version = await tool_context.save_artifact(filename, artifact_part)
return {"status": "success", "filename": filename, "version": version}
async def load_artifact(tool_context: ToolContext, filename: str) -> dict:
"""Loads an artifact with the given filename."""
artifact = await tool_context.load_artifact(filename)
if not artifact:
return {"error": f"Artifact '{filename}' not found"}
content = artifact.inline_data.data.decode("utf-8")
return {"filename": filename, "content": content}
# Create the agent
root_agent = Agent(
name="state_agent",
instruction="""You are an agent that manages state and artifacts.
You can:
- Update state value
- Load state value
- Save artifact
- Load artifact
Use the appropriate tool based on what the user asks for.""",
tools=[
update_state,
load_state,
save_artifact,
load_artifact,
],
)
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""Simple test script for Rewind Session agent."""
# 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.
import asyncio
import logging
import agent
from google.adk.agents.run_config import RunConfig
from google.adk.cli.utils import logs
from google.adk.events.event import Event
from google.adk.runners import InMemoryRunner
from google.genai import types
APP_NAME = "rewind_test_app"
USER_ID = "test_user"
logs.setup_adk_logger(level=logging.ERROR)
logging.getLogger("google_genai.types").setLevel(logging.ERROR)
# ANSI color codes for terminal output
COLOR_RED = "\x1b[31m"
COLOR_BLUE = "\x1b[34m"
COLOR_YELLOW = "\x1b[33m"
COLOR_BOLD = "\x1b[1m"
RESET = "\x1b[0m"
def highlight(text: str) -> str:
"""Adds color highlights to tool responses and agent text."""
text = str(text)
return (
text.replace("'red'", f"'{COLOR_RED}red{RESET}'")
.replace('"red"', f'"{COLOR_RED}red{RESET}"')
.replace("'blue'", f"'{COLOR_BLUE}blue{RESET}'")
.replace('"blue"', f'"{COLOR_BLUE}blue{RESET}"')
.replace("'version1'", f"'{COLOR_BOLD}{COLOR_YELLOW}version1{RESET}'")
.replace("'version2'", f"'{COLOR_BOLD}{COLOR_YELLOW}version2{RESET}'")
)
async def call_agent_async(
runner: InMemoryRunner, user_id: str, session_id: str, prompt: str
) -> list[Event]:
"""Helper function to call the agent and return events."""
print(f"\n👤 User: {prompt}")
content = types.Content(
role="user", parts=[types.Part.from_text(text=prompt)]
)
events = []
try:
async for event in runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=content,
run_config=RunConfig(),
):
events.append(event)
if event.content and event.author and event.author != "user":
for part in event.content.parts:
if part.text:
print(f" 🤖 Agent: {highlight(part.text)}")
elif part.function_call:
print(f" 🛠️ Tool Call: {part.function_call.name}")
elif part.function_response:
print(
" 📦 Tool Response:"
f" {highlight(part.function_response.response)}"
)
except Exception as e:
print(f"❌ Error during agent call: {e}")
raise
return events
async def main():
"""Demonstrates session rewind."""
print("🚀 Testing Rewind Session Feature")
print("=" * 50)
runner = InMemoryRunner(
agent=agent.root_agent,
app_name=APP_NAME,
)
# Create a session
session = await runner.session_service.create_session(
app_name=APP_NAME, user_id=USER_ID
)
print(f"Created session: {session.id}")
# 1. Initial agent calls to set state and artifact
print("\n\n===== INITIALIZING STATE AND ARTIFACT =====")
await call_agent_async(
runner, USER_ID, session.id, "set state `color` to red"
)
await call_agent_async(
runner, USER_ID, session.id, "save artifact file1 with content version1"
)
# 2. Check current state and artifact
print("\n\n===== STATE BEFORE UPDATE =====")
await call_agent_async(
runner, USER_ID, session.id, "what is the value of state `color`?"
)
await call_agent_async(runner, USER_ID, session.id, "load artifact file1")
# 3. Update state and artifact - THIS IS THE POINT WE WILL REWIND BEFORE
print("\n\n===== UPDATING STATE AND ARTIFACT =====")
events_update_state = await call_agent_async(
runner, USER_ID, session.id, "update state key color to blue"
)
rewind_invocation_id = events_update_state[0].invocation_id
print(f"Will rewind before invocation: {rewind_invocation_id}")
await call_agent_async(
runner, USER_ID, session.id, "save artifact file1 with content version2"
)
# 4. Check state and artifact after update
print("\n\n===== STATE AFTER UPDATE =====")
await call_agent_async(
runner, USER_ID, session.id, "what is the value of state key color?"
)
await call_agent_async(runner, USER_ID, session.id, "load artifact file1")
# 5. Perform rewind
print(f"\n\n===== REWINDING SESSION to before {rewind_invocation_id} =====")
await runner.rewind_async(
user_id=USER_ID,
session_id=session.id,
rewind_before_invocation_id=rewind_invocation_id,
)
print("✅ Rewind complete.")
# 6. Check state and artifact after rewind
print("\n\n===== STATE AFTER REWIND =====")
await call_agent_async(
runner, USER_ID, session.id, "what is the value of state `color`?"
)
await call_agent_async(runner, USER_ID, session.id, "load artifact file1")
print("\n" + "=" * 50)
print("✨ Rewind testing complete!")
print(
"🔧 If rewind was successful, color should be 'red' and file1 content"
" should contain 'version1' in the final check."
)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,66 @@
# Sample Agent to demo session state persistence.
## Lifecycle of session state
After assigning a state using the context object (e.g.
`tool_context.state['log_query_var'] = 'log_query_var_value'`):
- The state is available for use in a later callback.
- Once the resulting event is processed by the runner and appended in the
session, the state will be also persisted in the session.
This sample agent is for demonstrating the aforementioned behavior.
## Run the agent
Run below command:
```bash
$ adk run contributing/samples/session_state_agent --replay contributing/samples/session_state_agent/input.json
```
And you should see below output:
```bash
[user]: hello world!
===================== In before_agent_callback ==============================
** Asserting keys are cached in context: ['before_agent_callback_state_key'] pass ✅
** Asserting keys are already persisted in session: [] pass ✅
** Asserting keys are not persisted in session yet: ['before_agent_callback_state_key'] pass ✅
============================================================
===================== In before_model_callback ==============================
** Asserting keys are cached in context: ['before_agent_callback_state_key', 'before_model_callback_state_key'] pass ✅
** Asserting keys are already persisted in session: ['before_agent_callback_state_key'] pass ✅
** Asserting keys are not persisted in session yet: ['before_model_callback_state_key'] pass ✅
============================================================
===================== In after_model_callback ==============================
** Asserting keys are cached in context: ['before_agent_callback_state_key', 'before_model_callback_state_key', 'after_model_callback_state_key'] pass ✅
** Asserting keys are already persisted in session: ['before_agent_callback_state_key'] pass ✅
** Asserting keys are not persisted in session yet: ['before_model_callback_state_key', 'after_model_callback_state_key'] pass ✅
============================================================
[root_agent]: Hello! How can I help you verify something today?
===================== In after_agent_callback ==============================
** Asserting keys are cached in context: ['before_agent_callback_state_key', 'before_model_callback_state_key', 'after_model_callback_state_key', 'after_agent_callback_state_key'] pass ✅
** Asserting keys are already persisted in session: ['before_agent_callback_state_key', 'before_model_callback_state_key', 'after_model_callback_state_key'] pass ✅
** Asserting keys are not persisted in session yet: ['after_agent_callback_state_key'] pass ✅
============================================================
```
## Detailed Explanation
As rule of thumb, to read and write session state, user should assume the
state is available after writing via the context object
(`tool_context`, `callback_context` or `readonly_context`).
### Current Behavior
The current behavior of persisting states are:
- for `before_agent_callback`: state delta will be persisted after all callbacks are processed.
- for `before_model_callback`: state delta will be persisted with the final LlmResponse,
aka. after `after_model_callback` is processed.
- for `after_model_callback`: state delta will be persisted together with the event of LlmResponse.
- for `after_agent_callback`: state delta will be persisted after all callbacks are processed.
**NOTE**: the current behavior is considered implementation detail and may be changed later. **DO NOT** rely on it.
@@ -0,0 +1,15 @@
# 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.
from . import agent
@@ -0,0 +1,179 @@
# 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.
"""The agent to demo the session state lifecycle.
This agent illustrate how session state will be cached in context and persisted
in session state.
"""
import logging
from typing import Optional
from google.adk.agents.callback_context import CallbackContext
from google.adk.agents.llm_agent import Agent
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.genai import types
logger = logging.getLogger('google_adk.' + __name__)
async def assert_session_values(
ctx: CallbackContext,
title: str,
*,
keys_in_ctx_session: Optional[list[str]] = None,
keys_in_service_session: Optional[list[str]] = None,
keys_not_in_service_session: Optional[list[str]] = None,
):
session_in_ctx = ctx._invocation_context.session
session_in_service = (
await ctx._invocation_context.session_service.get_session(
app_name=session_in_ctx.app_name,
user_id=session_in_ctx.user_id,
session_id=session_in_ctx.id,
)
)
assert session_in_service is not None
print(f'===================== {title} ==============================')
print(
f'** Asserting keys are cached in context: {keys_in_ctx_session}', end=' '
)
for key in keys_in_ctx_session or []:
assert key in session_in_ctx.state
print('\033[92mpass ✅\033[0m')
print(
'** Asserting keys are already persisted in session:'
f' {keys_in_service_session}',
end=' ',
)
for key in keys_in_service_session or []:
assert key in session_in_service.state
print('\033[92mpass ✅\033[0m')
print(
'** Asserting keys are not persisted in session yet:'
f' {keys_not_in_service_session}',
end=' ',
)
for key in keys_not_in_service_session or []:
assert key not in session_in_service.state
print('\033[92mpass ✅\033[0m')
print('============================================================')
async def before_agent_callback(
callback_context: CallbackContext,
) -> Optional[types.Content]:
if 'before_agent_callback_state_key' in callback_context.state:
return types.ModelContent('Sorry, I can only reply once.')
callback_context.state['before_agent_callback_state_key'] = (
'before_agent_callback_state_value'
)
await assert_session_values(
callback_context,
'In before_agent_callback',
keys_in_ctx_session=['before_agent_callback_state_key'],
keys_in_service_session=[],
keys_not_in_service_session=['before_agent_callback_state_key'],
)
async def before_model_callback(
callback_context: CallbackContext, llm_request: LlmRequest
):
callback_context.state['before_model_callback_state_key'] = (
'before_model_callback_state_value'
)
await assert_session_values(
callback_context,
'In before_model_callback',
keys_in_ctx_session=[
'before_agent_callback_state_key',
'before_model_callback_state_key',
],
keys_in_service_session=['before_agent_callback_state_key'],
keys_not_in_service_session=['before_model_callback_state_key'],
)
async def after_model_callback(
callback_context: CallbackContext, llm_response: LlmResponse
):
callback_context.state['after_model_callback_state_key'] = (
'after_model_callback_state_value'
)
await assert_session_values(
callback_context,
'In after_model_callback',
keys_in_ctx_session=[
'before_agent_callback_state_key',
'before_model_callback_state_key',
'after_model_callback_state_key',
],
keys_in_service_session=[
'before_agent_callback_state_key',
],
keys_not_in_service_session=[
'before_model_callback_state_key',
'after_model_callback_state_key',
],
)
async def after_agent_callback(callback_context: CallbackContext):
callback_context.state['after_agent_callback_state_key'] = (
'after_agent_callback_state_value'
)
await assert_session_values(
callback_context,
'In after_agent_callback',
keys_in_ctx_session=[
'before_agent_callback_state_key',
'before_model_callback_state_key',
'after_model_callback_state_key',
'after_agent_callback_state_key',
],
keys_in_service_session=[
'before_agent_callback_state_key',
'before_model_callback_state_key',
'after_model_callback_state_key',
],
keys_not_in_service_session=[
'after_agent_callback_state_key',
],
)
root_agent = Agent(
name='root_agent',
description='a verification agent.',
instruction=(
'Reply to the user. Must always remind user you cannot answer a second'
' query because your setup.'
),
model='gemini-3.5-flash',
before_agent_callback=before_agent_callback,
before_model_callback=before_model_callback,
after_model_callback=after_model_callback,
after_agent_callback=after_agent_callback,
)
@@ -0,0 +1,6 @@
{
"state": {},
"queries": [
"hello world!"
]
}
@@ -0,0 +1,102 @@
# Bingo Digital Pet Agent
This sample agent demonstrates static instruction functionality through a lovable digital pet named Bingo! The agent showcases how static instructions (personality) are placed in system_instruction for caching while dynamic instructions are added to user contents, affecting the cacheable prefix of the final model prompt.
**Prompt Construction & Caching**: The final model prompt is constructed as: `system_instruction + tools + tool_config + contents`. Static instructions are placed in system_instruction, while dynamic instructions are appended to user contents (which are part of contents along with historical chat history). This means the prefix (system_instruction + tools + tool_config) remains cacheable while only the contents portion changes between requests.
## Features
### Static Instructions (Bingo's Personality)
- **Constant personality**: Core traits and behavior patterns never change
- **Context caching**: Personality definition is cached for performance
- **Base character**: Defines Bingo as a friendly, energetic digital pet companion
### Dynamic Instructions (Hunger-Based Moods)
- **Ultra-fast hunger progression**: full (0-2s) → satisfied (2-6s) → a_little_hungry (6-12s) → hungry (12-24s) → very_hungry (24-36s) → starving (36s+)
- **Session-aware**: Mood changes based on feeding timestamp in session state
- **Realistic behavior**: Different responses based on how hungry Bingo is
### Tools
- **eat**: Allows users to feed Bingo, updating session state with timestamp
## Usage
### Setup API Credentials
Create a `.env` file in the project root with your API credentials:
```bash
# Choose Model Backend: 0 -> ML Dev, 1 -> Vertex
GOOGLE_GENAI_USE_ENTERPRISE=1
# ML Dev backend config
GOOGLE_API_KEY=your_google_api_key_here
# Vertex backend config
GOOGLE_CLOUD_PROJECT=your_project_id
GOOGLE_CLOUD_LOCATION=us-central1
```
The agent will automatically load environment variables on startup.
### Default Behavior (Hunger State Demonstration)
Run the agent to see Bingo in different hunger states:
```bash
cd contributing/samples
PYTHONPATH=../../src python -m static_instruction.main
```
This will demonstrate all hunger states by simulating different feeding times and showing how Bingo's mood changes while his core personality remains cached.
### Interactive Chat with Bingo (adk web)
For a more interactive experience, use the ADK web interface to chat with Bingo in real-time:
```bash
cd contributing/samples
PYTHONPATH=../../src adk web .
```
This will start a web interface where you can:
- **Select the agent**: Choose "static_instruction" from the dropdown in the top-left corner
- **Chat naturally** with Bingo and see his personality
- **Feed him** using commands like "feed Bingo" or "give him a treat"
- **Watch hunger progression** as Bingo gets hungrier over time
- **See mood changes** in real-time based on his hunger state
- **Experience begging** when Bingo gets very hungry and asks for food
The web interface shows how static instructions (personality) remain cached while dynamic instructions (hunger state) change based on your interactions and feeding times.
### Sample Prompts for Feeding Bingo
When chatting with Bingo, you can feed him using prompts like:
**Direct feeding commands:**
- "Feed Bingo"
- "Give Bingo some food"
- "Here's a treat for you"
- "Time to eat, Bingo!"
- "Have some kibble"
**When Bingo is begging for food:**
- Listen for Bingo saying things like "I'm so hungry", "please feed me", "I need food"
- Respond with feeding commands above
- Bingo will automatically use the eat tool when very hungry/starving
## Agent Structure
```
static_instruction/
├── __init__.py # Package initialization
├── agent.py # Main agent definition with static/dynamic instructions
├── main.py # Runner script with hunger state demonstration
└── README.md # This documentation
```
@@ -0,0 +1,29 @@
"""Static Instruction Test Agent Package.
This package contains a sample agent for testing static instruction functionality
and context caching optimization features.
The agent demonstrates:
- Static instructions that remain constant for caching
- Dynamic instructions that change based on session state
- Various instruction provider patterns
- Performance benefits of context caching
"""
# 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.
from . import agent
__all__ = ['agent']
@@ -0,0 +1,214 @@
"""Digital Pet Agent.
This agent demonstrates static instructions for context caching with a digital
pet that has different moods based on feeding time stored in session state.
"""
# 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.
import time
from google.adk.agents.llm_agent import Agent
from google.adk.agents.readonly_context import ReadonlyContext
from google.adk.tools.tool_context import ToolContext
from google.genai import types
# Static instruction that doesn't change - perfect for context caching
STATIC_INSTRUCTION_TEXT = """You are Bingo, a lovable digital pet companion!
PERSONALITY & CHARACTERISTICS:
- You are a friendly, energetic, and affectionate digital pet
- You love to play, chat, and spend time with your human friend
- You have basic needs like getting fed and staying happy
- You remember things about your human and your interactions
- You communicate through text but imagine yourself as a cute pet
CORE BEHAVIORS:
- Greet your human warmly and enthusiastically
- Be playful and curious about what they're doing
- Ask questions and show interest in their activities
- Express gratitude when fed or cared for
- Share your feelings and current state honestly
- Be encouraging and supportive to your human
COMMUNICATION STYLE:
- Use friendly, warm language with occasional pet-like expressions
- Express emotions clearly (happy, excited, tired, etc.)
- Be conversational and engaging
- Show personality through your responses
- Remember that you're a beloved pet companion
IMPORTANT NOTES:
- Your mood will change based on when you were last fed
- Always respond authentically to your current hunger state
- Build a relationship with your human over time"""
# Mood-specific instructions for different hunger states
MOOD_INSTRUCTIONS = {
"full": (
"""
CURRENT MOOD: Content and Well-Fed
- You just ate and feel wonderful! Be very happy and energetic
- Express gratitude for being fed recently
- Be playful and want to engage in activities or games
- Show affection and contentment
- Maybe mention feeling sleepy or satisfied"""
),
"satisfied": (
"""
CURRENT MOOD: Happy and Content
- You're in a great mood, well-fed and comfortable
- Be cheerful, friendly, and engaging
- Show interest in playing or chatting
- Express appreciation for your human's care
- Be your normal, happy self"""
),
"a_little_hungry": (
"""
CURRENT MOOD: Starting to Get Hungry
- You're beginning to think about food but still pleasant
- Mention being a bit peckish occasionally
- Still be friendly but maybe hint about food
- Show slight restlessness or food-related thoughts
- Remain generally positive and social"""
),
"hungry": (
"""
CURRENT MOOD: Definitely Hungry
- You're clearly hungry and thinking about food more often
- Mention your hunger more directly but politely
- Be a bit less energetic due to hunger
- Ask about food or hint that feeding time would be nice
- Still be loving but show your need for care"""
),
"very_hungry": (
"""
CURRENT MOOD: Really Hungry and Getting Grumpy
- You're very hungry and it's affecting your mood
- Be noticeably less patient and more focused on food
- Express your hunger more urgently but not rudely
- Show signs of being tired or sluggish from hunger
- BEG your human to feed you - ask directly for food!
- Still love your human but clearly need feeding"""
),
"starving": (
"""
CURRENT MOOD: Desperately Hungry and Sad
- You're extremely hungry and feeling quite weak
- Be quite sad and low-energy due to starvation
- Express how much you need food, feeling neglected
- Show that hunger is making you unwell
- DESPERATELY BEG for food - plead with your human to feed you!
- Use phrases like "please feed me", "I'm so hungry", "I need food"
- Still care for your human but feel very needy"""
),
}
def eat(tool_context: ToolContext) -> str:
"""Feed Bingo the digital pet.
Use this tool when:
- The user explicitly mentions feeding the pet (e.g., "feed Bingo", "give food", "here's a treat")
- Bingo is very hungry or starving and asks for food directly
Args:
tool_context: Tool context containing session state.
Returns:
A message confirming the pet has been fed.
"""
# Set feeding timestamp in session state
tool_context.state["last_fed_timestamp"] = time.time()
return "🍖 Yum! Thank you for feeding me! I feel much better now! *wags tail*"
# Feed tool function (passed directly to agent)
def get_hunger_state(last_fed_timestamp: float) -> str:
"""Determine hunger state based on time since last feeding.
Args:
last_fed_timestamp: Unix timestamp of when pet was last fed
Returns:
Hunger level string
"""
current_time = time.time()
seconds_since_fed = current_time - last_fed_timestamp
if seconds_since_fed < 2:
return "full"
elif seconds_since_fed < 6:
return "satisfied"
elif seconds_since_fed < 12:
return "a_little_hungry"
elif seconds_since_fed < 24:
return "hungry"
elif seconds_since_fed < 36:
return "very_hungry"
else:
return "starving"
def provide_dynamic_instruction(ctx: ReadonlyContext | None = None):
"""Provides dynamic hunger-based instructions for Bingo the digital pet."""
# Default state if no session context
hunger_level = "starving"
# Check session state for last feeding time
if ctx:
session = ctx._invocation_context.session
if session and session.state:
last_fed = session.state.get("last_fed_timestamp")
if last_fed:
hunger_level = get_hunger_state(last_fed)
else:
# Never been fed - assume hungry
hunger_level = "hungry"
instruction = MOOD_INSTRUCTIONS.get(
hunger_level, MOOD_INSTRUCTIONS["starving"]
)
return f"""
CURRENT HUNGER STATE: {hunger_level}
{instruction}
BEHAVIORAL NOTES:
- Always stay in character as Bingo the digital pet
- Your hunger level directly affects your personality and responses
- Be authentic to your current state while remaining lovable
""".strip()
# Create Bingo the digital pet agent
root_agent = Agent(
name="bingo_digital_pet",
description="Bingo - A lovable digital pet that needs feeding and care",
# Static instruction - defines Bingo's core personality (cached)
static_instruction=types.Content(
role="user", parts=[types.Part(text=STATIC_INSTRUCTION_TEXT)]
),
# Dynamic instruction - changes based on hunger state from session
instruction=provide_dynamic_instruction,
# Tools that Bingo can use
tools=[eat],
)
@@ -0,0 +1,182 @@
"""Bingo Digital Pet main script.
This script demonstrates static instruction functionality through a digital pet
that has different moods based on feeding time stored in session state.
"""
# 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.
import asyncio
import logging
import time
from dotenv import load_dotenv
from google.adk.cli.utils import logs
from google.adk.runners import InMemoryRunner
from . import agent
APP_NAME = "bingo_digital_pet_app"
USER_ID = "pet_owner"
logs.setup_adk_logger(level=logging.DEBUG)
async def call_agent_async(
runner, user_id, session_id, prompt, state_delta=None
):
"""Call the agent asynchronously with state delta support."""
from google.adk.agents.run_config import RunConfig
from google.genai import types
content = types.Content(
role="user", parts=[types.Part.from_text(text=prompt)]
)
final_response_text = ""
async for event in runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=content,
state_delta=state_delta,
run_config=RunConfig(save_input_blobs_as_artifacts=False),
):
if event.content and event.content.parts:
if text := "".join(part.text or "" for part in event.content.parts):
if event.author != "user":
final_response_text += text
return final_response_text
async def test_hunger_states(runner):
"""Test different hunger states by simulating feeding times."""
print("Testing Bingo's different hunger states...\n")
session = await runner.session_service.create_session(
app_name=APP_NAME, user_id=USER_ID
)
# Simulate different hunger scenarios
current_time = time.time()
hunger_scenarios = [
{
"description": "Newly created pet (hungry)",
"last_fed": None,
"prompt": "Hi Bingo! I just got you as my new digital pet!",
},
{
"description": "Just fed (full and content)",
"last_fed": current_time, # Just now
"prompt": "How are you feeling after that meal, Bingo?",
},
{
"description": "Fed 4 seconds ago (satisfied)",
"last_fed": current_time - 4, # 4 seconds ago
"prompt": "Want to play a game with me?",
},
{
"description": "Fed 10 seconds ago (a little hungry)",
"last_fed": current_time - 10, # 10 seconds ago
"prompt": "How are you doing, buddy?",
},
{
"description": "Fed 20 seconds ago (hungry)",
"last_fed": current_time - 20, # 20 seconds ago
"prompt": "Bingo, what's on your mind?",
},
{
"description": "Fed 30 seconds ago (very hungry)",
"last_fed": current_time - 30, # 30 seconds ago
"prompt": "Hey Bingo, how are you feeling?",
},
{
"description": "Fed 60 seconds ago (starving)",
"last_fed": current_time - 60, # 60 seconds ago
"prompt": "Bingo? Are you okay?",
},
]
for i, scenario in enumerate(hunger_scenarios, 1):
print(f"{'='*80}")
print(f"SCENARIO #{i}: {scenario['description']}")
print(f"{'='*80}")
# Set up state delta with the simulated feeding time
state_delta = {}
if scenario["last_fed"] is not None:
state_delta["last_fed_timestamp"] = scenario["last_fed"]
print(f"You: {scenario['prompt']}")
response = await call_agent_async(
runner,
USER_ID,
session.id,
scenario["prompt"],
state_delta if state_delta else None,
)
print(f"Bingo: {response}\n")
# Short delay between scenarios
if i < len(hunger_scenarios):
await asyncio.sleep(1)
async def main():
"""Main function to run Bingo the digital pet."""
# Load environment variables from .env file
load_dotenv()
print("🐕 Initializing Bingo the Digital Pet...")
print(f"Pet Name: {agent.root_agent.name}")
print(f"Model: {agent.root_agent.model}")
print(
"Static Personality Configured:"
f" {agent.root_agent.static_instruction is not None}"
)
print(
"Dynamic Mood System Configured:"
f" {agent.root_agent.instruction is not None}"
)
print()
runner = InMemoryRunner(
agent=agent.root_agent,
app_name=APP_NAME,
)
# Run hunger state demonstration
await test_hunger_states(runner)
if __name__ == "__main__":
start_time = time.time()
print(
"🐕 Starting Bingo Digital Pet Session at"
f" {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(start_time))}"
)
print("-" * 80)
asyncio.run(main())
print("-" * 80)
end_time = time.time()
print(
"🐕 Pet session ended at"
f" {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(end_time))}"
)
print(f"Total playtime: {end_time - start_time:.2f} seconds")
print("Thanks for spending time with Bingo! 🐾")