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
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:
@@ -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())
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
],
|
||||
)
|
||||
Binary file not shown.
@@ -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
|
||||
+1
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
+90
@@ -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()
|
||||
+28
@@ -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"}
|
||||
Binary file not shown.
@@ -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! 🐾")
|
||||
Reference in New Issue
Block a user