You've already forked adk-python
mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e172811bc7 | |||
| da6f1d3653 | |||
| 2c752934a8 | |||
| b2b80e7fa0 | |||
| dd1ffad394 | |||
| 8b081751ed | |||
| b5a65fb4f4 | |||
| 839d2e43bb | |||
| 1589fcdd86 | |||
| e7528aebd4 | |||
| cbb6e4945a | |||
| c6b6b6f3c6 | |||
| c8c6cd70a4 |
@@ -1,5 +1,60 @@
|
||||
# Changelog
|
||||
|
||||
## [1.15.0](https://github.com/google/adk-python/compare/v1.14.1...v1.15.0) (2025-09-24)
|
||||
|
||||
### Features
|
||||
|
||||
* [Core]
|
||||
* Adding the ContextFilterPlugin ([a06bf27](https://github.com/google/adk-python/commit/a06bf278cbc89f521c187ed51b032d82ffdafe2d))
|
||||
* Adds plugin to save artifacts for issue [#2176](https://github.com/google/adk-python/issues/2176) ([657369c](https://github.com/google/adk-python/commit/657369cffe142ef3745cd5950d0d24a49f42f7fd))
|
||||
* Expose log probs of candidates in LlmResponse ([f7bd3c1](https://github.com/google/adk-python/commit/f7bd3c111c211e880d7c1954dd4508b952704c68))
|
||||
* [Context Caching]
|
||||
* Support context caching ([c66245a](https://github.com/google/adk-python/commit/c66245a3b80192c16cb67ee3194f82c9a7c901e5))
|
||||
- Support explicit context caching auto creation and lifecycle management.
|
||||
|
||||
Usage: `App(root_agent=..., plugins=..., context_cache_config=...)`
|
||||
* Support non-text content in static instruction ([61213ce](https://github.com/google/adk-python/commit/61213ce4d4c10f7ecaf6ddb521672059cee27942))
|
||||
* Support static instructions ([9be9cc2](https://github.com/google/adk-python/commit/9be9cc2feee92241fd2fbf9dea3a42de5a78e9ce))
|
||||
- Support static instruction that won't change, put at the beginning of
|
||||
the instruction.
|
||||
Static instruction support inline_data and file_data as contents.
|
||||
Dynamic instruction moved to the end of LlmRequest, increasing prefix
|
||||
caching matching size.
|
||||
|
||||
Usage:
|
||||
`LlmAgent(model=...,static_instruction =types.Content(parts=...), ... )`
|
||||
* [Telemetry]
|
||||
* Add --otel_to_cloud experimental support ([1ae0b82](https://github.com/google/adk-python/commit/1ae0b82f5602a57ad1ca975ca0b7c85003d1a28a), [b131268](https://github.com/google/adk-python/commit/b1312680f4ea9f21c3246a1d24392619643d71f5), [7870480](https://github.com/google/adk-python/commit/7870480c63bb4fc08cfb3cabc0e1f0458f0e85bd))
|
||||
* Add GenAI Instrumentation if --otel_to_cloud is enabled ([cee365a](https://github.com/google/adk-python/commit/cee365a13d0d1b1f2be046c1cc29e24a8d1fdbcc))
|
||||
* Support standard OTel env variables for exporter endpoints ([f157b2e](https://github.com/google/adk-python/commit/f157b2ee4caf4055e78f4657254e45913895f5de))
|
||||
* Temporarily disable Cloud Monitoring integration in --otel_to_cloud ([3b80337](https://github.com/google/adk-python/commit/3b80337faf427460e4743e25dbb92578f823513f))
|
||||
* [Services]
|
||||
* Add endpoint to generate memory from session ([2595824](https://github.com/google/adk-python/commit/25958242db890b4d2aac8612f7f7cfbb561727fa))
|
||||
* [Tools]
|
||||
* Add Google Maps Grounding Tool to ADK ([6b49391](https://github.com/google/adk-python/commit/6b493915469ecb42068e24818ab547b0856e4709))
|
||||
* **MCP:** Initialize tool_name_prefix in MCPToolse ([86dea5b](https://github.com/google/adk-python/commit/86dea5b53ac305367283b7e353b60d0f4515be3b))
|
||||
* [Evals]
|
||||
* Data model for storing App Details and data model for steps ([01923a9](https://github.com/google/adk-python/commit/01923a9227895906ca8ae32712d65b178e2cd7d5))
|
||||
* Adds Rubric based final response evaluator ([5a485b0](https://github.com/google/adk-python/commit/5a485b01cd64cb49735e13ebd5e7fa3da02cd85f))
|
||||
* Populate AppDetails to each Invocation ([d486795](https://github.com/google/adk-python/commit/d48679582de91050ca9c5106402319be9a8ae7e8))
|
||||
* [Samples]
|
||||
* Make the bigquery sample agent run with ADC out-of-the-box ([10cf377](https://github.com/google/adk-python/commit/10cf37749417856e394e62896231e41b13420f18))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Close runners after running eval ([86ee6e3](https://github.com/google/adk-python/commit/86ee6e3fa3690148d60358fc3dacb0e0ab40942b))
|
||||
* Filter out thought parts when saving agent output to state ([632bf8b](https://github.com/google/adk-python/commit/632bf8b0bcf18ff4e4505e4e5f4c626510f366a2))
|
||||
* Ignore empty function chunk in LiteLlm streaming response ([8a92fd1](https://github.com/google/adk-python/commit/8a92fd18b600da596c22fd80c6148511a136dfd0))
|
||||
* Introduces a `raw_mcp_tool` method in `McpTool` to provide direct access to the underlying MCP tool ([6158075](https://github.com/google/adk-python/commit/6158075a657f8fe0835679e509face6191905403))
|
||||
* Make a copy of the `columns` instead of modifying it in place ([aef1ee9](https://github.com/google/adk-python/commit/aef1ee97a55a310f3959d475b8d7d6bc3915ae48))
|
||||
* Prevent escaping of Latin characters in LLM response ([c9ea80a](https://github.com/google/adk-python/commit/c9ea80af28e586c9cc1f643b365cdba82f80c700))
|
||||
* Retain the consumers and transport registry when recreating the ClientFactory in remote_a2a_agent.py ([6bd33e1](https://github.com/google/adk-python/commit/6bd33e1be36f741a6ed0514197550f9f336262ed))
|
||||
* Remove unsupported 'type': 'unknown' in test_common.py for fastapi 0.117.1 ([3745221](https://github.com/google/adk-python/commit/374522197fa6843f786bfd12d17ce0fc20461dfd))
|
||||
|
||||
### Documentation
|
||||
|
||||
* Correct the documentation of `after_agent_callback` ([b9735b2](https://github.com/google/adk-python/commit/b9735b2193267645781b268231d63c23c6fec654))
|
||||
|
||||
## [1.14.1](https://github.com/google/adk-python/compare/v1.14.0...v1.14.1) (2025-09-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -25,6 +25,7 @@ import argparse
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
@@ -42,6 +43,7 @@ except ImportError:
|
||||
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
|
||||
|
||||
@@ -570,9 +572,19 @@ async def main():
|
||||
" 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 = (
|
||||
|
||||
@@ -9,7 +9,7 @@ This sample demonstrates ADK's static instruction feature with non-text content
|
||||
- **Gemini Files API integration**: Demonstrates uploading documents and using file_data
|
||||
- **Mixed content types**: inline_data for images, file_data for documents
|
||||
- **API variant detection**: Different behavior for Gemini API vs Vertex AI
|
||||
- **GCS file references**: Additional GCS file support when using Vertex AI
|
||||
- **GCS file references**: Support for both GCS URI and HTTPS URL access methods in Vertex AI
|
||||
|
||||
## Static Instruction Content
|
||||
|
||||
@@ -23,7 +23,7 @@ The agent includes:
|
||||
|
||||
**Vertex AI:**
|
||||
3. **Research paper**: Gemma research paper from Google Cloud Storage via GCS file reference
|
||||
4. **Contributing guide**: Gemini Cookbook contributing guide from GitHub via HTTPS file reference
|
||||
4. **AI research paper**: Same research paper accessed via HTTPS URL for comparison
|
||||
|
||||
## Content Used
|
||||
|
||||
@@ -37,14 +37,14 @@ The agent includes:
|
||||
- Files are automatically cleaned up after 48 hours by the Gemini API
|
||||
|
||||
**Vertex AI:**
|
||||
- **Research Paper**: Gemma research paper (GCS file reference as `file_data`)
|
||||
- Public GCS URI: `gs://cloud-samples-data/generative-ai/pdf/2403.05530.pdf`
|
||||
- Demonstrates GCS file access in Vertex AI
|
||||
- PDF format with technical AI research content
|
||||
- **Contributing Guide**: Gemini Cookbook contributing guide (HTTPS file reference as `file_data`)
|
||||
- Public GitHub URL: `https://raw.githubusercontent.com/google-gemini/cookbook/main/CONTRIBUTING.md`
|
||||
- **Gemma Research Paper**: Research paper accessed via GCS URI (as `file_data`)
|
||||
- GCS URI: `gs://cloud-samples-data/generative-ai/pdf/2403.05530.pdf`
|
||||
- Demonstrates native GCS file access in Vertex AI
|
||||
- PDF format with technical AI research content about Gemini 1.5
|
||||
- **AI Research Paper**: Same research paper accessed via HTTPS URL (as `file_data`)
|
||||
- HTTPS URL: `https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/2403.05530.pdf`
|
||||
- Demonstrates HTTPS file access in Vertex AI
|
||||
- Markdown format with development guidelines
|
||||
- Agent can discover these are the same document and compare access methods
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -73,7 +73,9 @@ The agent will automatically load environment variables on startup.
|
||||
cd contributing/samples
|
||||
python -m static_non_text_content.main
|
||||
```
|
||||
This runs 4 test prompts that specifically demonstrate the static content features.
|
||||
This runs test prompts that demonstrate the static content features:
|
||||
- **Gemini Developer API**: 4 prompts testing inline_data + Files API upload
|
||||
- **Vertex AI**: 5 prompts testing inline_data + GCS/HTTPS file access comparison
|
||||
|
||||
### Interactive Mode
|
||||
```bash
|
||||
@@ -101,13 +103,17 @@ The sample automatically runs test prompts when no `--prompt` is specified:
|
||||
**All API variants:**
|
||||
1. "What reference materials do you have access to?"
|
||||
2. "Can you describe the sample chart that was provided to you?"
|
||||
3. "What does the contributing guide document say about best practices?"
|
||||
4. "How do the inline image and file references in your instructions help you answer questions?"
|
||||
3. "How do the inline image and file references in your instructions help you answer questions?"
|
||||
|
||||
**Vertex AI only (additional prompt):**
|
||||
**Gemini Developer API only:**
|
||||
4. "What does the contributing guide document say about best practices?"
|
||||
|
||||
**Vertex AI only (additional prompts):**
|
||||
5. "What is the Gemma research paper about and what are its key contributions?"
|
||||
6. "Can you compare the research papers you have access to? Are they related or different?"
|
||||
|
||||
These prompts test `inline_data`, Files API `file_data` (Gemini API), and GCS/HTTPS `file_data` (Vertex AI).
|
||||
**Gemini Developer API** tests: `inline_data` (image) + Files API `file_data` (uploaded document)
|
||||
**Vertex AI** tests: `inline_data` (image) + GCS URI `file_data` + HTTPS URL `file_data` (same document via different access methods)
|
||||
|
||||
## How It Works
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ def create_static_instruction_with_file_upload():
|
||||
file_data_parts = []
|
||||
|
||||
if api_variant == GoogleLLMVariant.VERTEX_AI:
|
||||
print("Using Vertex AI - adding GCS and GitHub file references")
|
||||
print("Using Vertex AI - adding GCS URI and HTTPS URL references")
|
||||
|
||||
# Add GCS file reference
|
||||
file_data_parts.append(
|
||||
@@ -90,20 +90,20 @@ def create_static_instruction_with_file_upload():
|
||||
)
|
||||
)
|
||||
|
||||
# Add GitHub public file reference
|
||||
# Add the same document via HTTPS URL to demonstrate both access methods
|
||||
file_data_parts.append(
|
||||
types.Part(
|
||||
file_data=types.FileData(
|
||||
file_uri="https://raw.githubusercontent.com/google-gemini/cookbook/main/CONTRIBUTING.md",
|
||||
mime_type="text/markdown",
|
||||
display_name="Gemini Cookbook Contributing Guide",
|
||||
file_uri="https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/2403.05530.pdf",
|
||||
mime_type="application/pdf",
|
||||
display_name="AI Research Paper (HTTPS)",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
additional_text = (
|
||||
" You also have access to the Gemma research paper from Google Cloud"
|
||||
" Storage and the Gemini Cookbook contributing guide from GitHub."
|
||||
" You also have access to a Gemma research paper from GCS"
|
||||
" and an AI research paper from HTTPS URL."
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -187,8 +187,8 @@ def create_static_instruction_with_file_upload():
|
||||
instruction_text = """
|
||||
When users ask questions, you should:
|
||||
1. Use the reference chart above to provide context when discussing visual data or charts
|
||||
2. Reference the Gemma research paper when discussing AI research, model architectures, or technical details
|
||||
3. Reference the Gemini Cookbook contributing guide when explaining best practices and guidelines
|
||||
2. Reference the Gemma research paper (from GCS) when discussing AI research, model architectures, or technical details
|
||||
3. Reference the AI research paper (from HTTPS) when discussing research topics
|
||||
4. Be helpful and informative in your responses
|
||||
5. Explain how the provided reference materials relate to their questions"""
|
||||
else:
|
||||
|
||||
@@ -115,22 +115,33 @@ async def run_default_test_prompts(runner):
|
||||
app_name=APP_NAME, user_id=USER_ID
|
||||
)
|
||||
|
||||
# Test prompts that specifically exercise the static content features
|
||||
# Common test prompts for all API variants
|
||||
test_prompts = [
|
||||
"What reference materials do you have access to?",
|
||||
"Can you describe the sample chart that was provided to you?",
|
||||
"What does the contributing guide document say about best practices?",
|
||||
(
|
||||
"How do the inline image and file references in your instructions "
|
||||
"help you answer questions?"
|
||||
),
|
||||
]
|
||||
|
||||
# Add Vertex AI specific prompt to test GCS file reference
|
||||
# Add API-specific prompts
|
||||
if api_variant == GoogleLLMVariant.VERTEX_AI:
|
||||
# Vertex AI has research papers instead of contributing guide
|
||||
test_prompts.extend([
|
||||
(
|
||||
"What is the Gemma research paper about and what are its key "
|
||||
"contributions?"
|
||||
),
|
||||
(
|
||||
"Can you compare the research papers you have access to? Are they "
|
||||
"related or different?"
|
||||
),
|
||||
])
|
||||
else:
|
||||
# Gemini Developer API has contributing guide document
|
||||
test_prompts.append(
|
||||
"What is the Gemma research paper about and what are its key "
|
||||
"contributions?"
|
||||
"What does the contributing guide document say about best practices?"
|
||||
)
|
||||
|
||||
for i, prompt in enumerate(test_prompts, 1):
|
||||
|
||||
+3
-3
@@ -38,16 +38,16 @@ dependencies = [
|
||||
"google-cloud-spanner>=3.56.0, <4.0.0", # For Spanner database
|
||||
"google-cloud-speech>=2.30.0, <3.0.0", # For Audio Transcription
|
||||
"google-cloud-storage>=2.18.0, <3.0.0", # For GCS Artifact service
|
||||
"google-genai>=1.21.1, <2.0.0", # Google GenAI SDK
|
||||
"google-genai>=1.21.1, <=1.40.0, !=1.37.0, !=1.38.0, !=1.39.0", # Google GenAI SDK
|
||||
"graphviz>=0.20.2, <1.0.0", # Graphviz for graph rendering
|
||||
"mcp>=1.8.0, <2.0.0;python_version>='3.10'", # For MCP Toolset
|
||||
"opentelemetry-api>=1.31.0, <=1.37.0", # OpenTelemetry - limit upper version for sdk and api to not risk breaking changes from unstable _logs package.
|
||||
"opentelemetry-api>=1.37.0, <=1.37.0", # OpenTelemetry - limit upper version for sdk and api to not risk breaking changes from unstable _logs package.
|
||||
"opentelemetry-exporter-gcp-logging>=1.9.0a0, <2.0.0",
|
||||
"opentelemetry-exporter-gcp-monitoring>=1.9.0a0, <2.0.0",
|
||||
"opentelemetry-exporter-gcp-trace>=1.9.0, <2.0.0",
|
||||
"opentelemetry-exporter-otlp-proto-http>=1.36.0",
|
||||
"opentelemetry-resourcedetector-gcp>=1.9.0a0, <2.0.0",
|
||||
"opentelemetry-sdk>=1.31.0, <=1.37.0",
|
||||
"opentelemetry-sdk>=1.37.0, <=1.37.0",
|
||||
"pydantic>=2.0, <3.0.0", # For data validation/models
|
||||
"python-dateutil>=2.9.0.post0, <3.0.0", # For Vertext AI Session Service
|
||||
"python-dotenv>=1.0.0, <2.0.0", # To manage environment variables
|
||||
|
||||
@@ -30,7 +30,6 @@ from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
from google.genai import types
|
||||
from opentelemetry import trace
|
||||
from pydantic import BaseModel
|
||||
from pydantic import ConfigDict
|
||||
from pydantic import Field
|
||||
@@ -39,17 +38,17 @@ from typing_extensions import override
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from ..events.event import Event
|
||||
from ..events.event_actions import EventActions
|
||||
from ..telemetry import tracing
|
||||
from ..telemetry.tracing import tracer
|
||||
from ..utils.context_utils import Aclosing
|
||||
from ..utils.feature_decorator import experimental
|
||||
from .base_agent_config import BaseAgentConfig
|
||||
from .callback_context import CallbackContext
|
||||
from .common_configs import AgentRefConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .invocation_context import InvocationContext
|
||||
|
||||
tracer = trace.get_tracer('gcp.vertex.agent')
|
||||
|
||||
_SingleAgentCallback: TypeAlias = Callable[
|
||||
[CallbackContext],
|
||||
Union[Awaitable[Optional[types.Content]], Optional[types.Content]],
|
||||
@@ -68,6 +67,18 @@ AfterAgentCallback: TypeAlias = Union[
|
||||
SelfAgent = TypeVar('SelfAgent', bound='BaseAgent')
|
||||
|
||||
|
||||
@experimental
|
||||
class BaseAgentState(BaseModel):
|
||||
"""Base class for all agent states."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
extra='forbid',
|
||||
)
|
||||
|
||||
|
||||
AgentState = TypeVar('AgentState', bound=BaseAgentState)
|
||||
|
||||
|
||||
class BaseAgent(BaseModel):
|
||||
"""Base class for all agents in Agent Development Kit."""
|
||||
|
||||
@@ -148,6 +159,57 @@ class BaseAgent(BaseModel):
|
||||
response and appended to event history as agent response.
|
||||
"""
|
||||
|
||||
def _load_agent_state(
|
||||
self,
|
||||
ctx: InvocationContext,
|
||||
state_type: Type[AgentState],
|
||||
default_state: AgentState,
|
||||
) -> tuple[AgentState, bool]:
|
||||
"""Loads the agent state from the invocation context, handling resumption.
|
||||
|
||||
Args:
|
||||
ctx: The invocation context.
|
||||
state_type: The type of the agent state.
|
||||
default_state: The default state to use if not resuming.
|
||||
|
||||
Returns:
|
||||
tuple[AgentState, bool]: The current state and a boolean indicating if
|
||||
resuming.
|
||||
"""
|
||||
if self.name not in ctx.agent_states:
|
||||
return default_state, False
|
||||
else:
|
||||
return state_type.model_validate(ctx.agent_states.get(self.name)), True
|
||||
|
||||
def _create_agent_state_event(
|
||||
self,
|
||||
ctx: InvocationContext,
|
||||
*,
|
||||
state: Optional[BaseAgentState] = None,
|
||||
end_of_agent: bool = False,
|
||||
) -> Event:
|
||||
"""Creates an event for agent state.
|
||||
|
||||
Args:
|
||||
ctx: The invocation context.
|
||||
state: The agent state to checkpoint.
|
||||
end_of_agent: Whether the agent is finished running.
|
||||
|
||||
Returns:
|
||||
An Event object representing the checkpoint.
|
||||
"""
|
||||
event_actions = EventActions()
|
||||
if state:
|
||||
event_actions.agent_state = state.model_dump(mode='json')
|
||||
if end_of_agent:
|
||||
event_actions.end_of_agent = True
|
||||
return Event(
|
||||
invocation_id=ctx.invocation_id,
|
||||
author=self.name,
|
||||
branch=ctx.branch,
|
||||
actions=event_actions,
|
||||
)
|
||||
|
||||
def clone(
|
||||
self: SelfAgent, update: Mapping[str, Any] | None = None
|
||||
) -> SelfAgent:
|
||||
@@ -226,9 +288,9 @@ class BaseAgent(BaseModel):
|
||||
"""
|
||||
|
||||
async def _run_with_trace() -> AsyncGenerator[Event, None]:
|
||||
with tracer.start_as_current_span(f'agent_run [{self.name}]'):
|
||||
with tracer.start_as_current_span(f'invoke_agent {self.name}') as span:
|
||||
ctx = self._create_invocation_context(parent_context)
|
||||
|
||||
tracing.trace_agent_invocation(span, self, ctx)
|
||||
if event := await self.__handle_before_agent_callback(ctx):
|
||||
yield event
|
||||
if ctx.end_invocation:
|
||||
@@ -264,9 +326,9 @@ class BaseAgent(BaseModel):
|
||||
"""
|
||||
|
||||
async def _run_with_trace() -> AsyncGenerator[Event, None]:
|
||||
with tracer.start_as_current_span(f'agent_run [{self.name}]'):
|
||||
with tracer.start_as_current_span(f'invoke_agent {self.name}') as span:
|
||||
ctx = self._create_invocation_context(parent_context)
|
||||
|
||||
tracing.trace_agent_invocation(span, self, ctx)
|
||||
if event := await self.__handle_before_agent_callback(ctx):
|
||||
yield event
|
||||
if ctx.end_invocation:
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
import uuid
|
||||
|
||||
from google.genai import types
|
||||
@@ -24,6 +24,7 @@ from pydantic import ConfigDict
|
||||
from pydantic import Field
|
||||
from pydantic import PrivateAttr
|
||||
|
||||
from ..apps.app import ResumabilityConfig
|
||||
from ..artifacts.base_artifact_service import BaseArtifactService
|
||||
from ..auth.credential_service.base_credential_service import BaseCredentialService
|
||||
from ..events.event import Event
|
||||
@@ -31,7 +32,6 @@ from ..memory.base_memory_service import BaseMemoryService
|
||||
from ..plugins.plugin_manager import PluginManager
|
||||
from ..sessions.base_session_service import BaseSessionService
|
||||
from ..sessions.session import Session
|
||||
from ..utils.feature_decorator import working_in_progress
|
||||
from .active_streaming_tool import ActiveStreamingTool
|
||||
from .base_agent import BaseAgent
|
||||
from .context_cache_config import ContextCacheConfig
|
||||
@@ -163,6 +163,12 @@ class InvocationContext(BaseModel):
|
||||
session: Session
|
||||
"""The current session of this invocation context. Readonly."""
|
||||
|
||||
agent_states: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
||||
"""The state of the agent for this invocation."""
|
||||
|
||||
end_of_agents: dict[str, bool] = Field(default_factory=dict)
|
||||
"""The end of agent status for each agent in this invocation."""
|
||||
|
||||
end_invocation: bool = False
|
||||
"""Whether to end this invocation.
|
||||
|
||||
@@ -189,6 +195,9 @@ class InvocationContext(BaseModel):
|
||||
run_config: Optional[RunConfig] = None
|
||||
"""Configurations for live agents under this invocation."""
|
||||
|
||||
resumability_config: Optional[ResumabilityConfig] = None
|
||||
"""The resumability config that applies to all agents under this invocation."""
|
||||
|
||||
plugin_manager: PluginManager = Field(default_factory=PluginManager)
|
||||
"""The manager for keeping track of plugins in this invocation."""
|
||||
|
||||
@@ -199,6 +208,11 @@ class InvocationContext(BaseModel):
|
||||
of this invocation.
|
||||
"""
|
||||
|
||||
def reset_agent_state(self, agent_name: str) -> None:
|
||||
"""Resets the state of an agent, allowing it to be re-run."""
|
||||
self.agent_states.pop(agent_name, None)
|
||||
self.end_of_agents.pop(agent_name, None)
|
||||
|
||||
def increment_llm_call_count(
|
||||
self,
|
||||
):
|
||||
@@ -220,7 +234,6 @@ class InvocationContext(BaseModel):
|
||||
def user_id(self) -> str:
|
||||
return self.session.user_id
|
||||
|
||||
@working_in_progress("incomplete feature, don't use yet")
|
||||
def get_events(
|
||||
self,
|
||||
current_invocation: bool = False,
|
||||
@@ -247,6 +260,45 @@ class InvocationContext(BaseModel):
|
||||
results = [event for event in results if event.branch == self.branch]
|
||||
return results
|
||||
|
||||
def should_pause_invocation(self, event: Event) -> bool:
|
||||
"""Returns whether to pause the invocation right after this event.
|
||||
|
||||
"Pausing" an invocation is different from "ending" an invocation. A paused
|
||||
invocation can be resumed later, while an ended invocation cannot.
|
||||
|
||||
Pausing the current agent's run will also pause all the agents that
|
||||
depend on its execution, i.e. the subsequent agents in a workflow, and the
|
||||
current agent's ancestors, etc.
|
||||
|
||||
Note that parallel sibling agents won't be affected, but their common
|
||||
ancestors will be paused after all the non-blocking sub-agents finished
|
||||
running.
|
||||
|
||||
Should meet all following conditions to pause an invocation:
|
||||
1. The app is resumable.
|
||||
2. The current event has a long running function call.
|
||||
|
||||
Args:
|
||||
event: The current event.
|
||||
|
||||
Returns:
|
||||
Whether to pause the invocation right after this event.
|
||||
"""
|
||||
if (
|
||||
not self.resumability_config
|
||||
or not self.resumability_config.is_resumable
|
||||
):
|
||||
return False
|
||||
|
||||
if not event.long_running_tool_ids or not event.get_function_calls():
|
||||
return False
|
||||
|
||||
for fc in event.get_function_calls():
|
||||
if fc.id in event.long_running_tool_ids:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def new_invocation_context_id() -> str:
|
||||
return "e-" + str(uuid.uuid4())
|
||||
|
||||
@@ -341,6 +341,8 @@ class LlmAgent(BaseAgent):
|
||||
async for event in agen:
|
||||
self.__maybe_save_output_to_state(event)
|
||||
yield event
|
||||
if ctx.should_pause_invocation(event):
|
||||
return
|
||||
|
||||
@override
|
||||
async def _run_live_impl(
|
||||
|
||||
@@ -21,7 +21,6 @@ from typing import AsyncGenerator
|
||||
from typing import ClassVar
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
@@ -30,10 +29,22 @@ from ..events.event import Event
|
||||
from ..utils.context_utils import Aclosing
|
||||
from ..utils.feature_decorator import experimental
|
||||
from .base_agent import BaseAgent
|
||||
from .base_agent import BaseAgentState
|
||||
from .base_agent_config import BaseAgentConfig
|
||||
from .loop_agent_config import LoopAgentConfig
|
||||
|
||||
|
||||
@experimental
|
||||
class LoopAgentState(BaseAgentState):
|
||||
"""State for LoopAgent."""
|
||||
|
||||
current_sub_agent: str = ''
|
||||
"""The name of the current sub-agent to run in the loop."""
|
||||
|
||||
times_looped: int = 0
|
||||
"""The number of times the loop agent has looped."""
|
||||
|
||||
|
||||
class LoopAgent(BaseAgent):
|
||||
"""A shell agent that run its sub-agents in a loop.
|
||||
|
||||
@@ -55,19 +66,33 @@ class LoopAgent(BaseAgent):
|
||||
async def _run_async_impl(
|
||||
self, ctx: InvocationContext
|
||||
) -> AsyncGenerator[Event, None]:
|
||||
if not self.sub_agents:
|
||||
return
|
||||
|
||||
times_looped = 0
|
||||
while not self.max_iterations or times_looped < self.max_iterations:
|
||||
for sub_agent in self.sub_agents:
|
||||
should_exit = False
|
||||
pause_invocation = False
|
||||
|
||||
async with Aclosing(sub_agent.run_async(ctx)) as agen:
|
||||
async for event in agen:
|
||||
yield event
|
||||
if event.actions.escalate:
|
||||
should_exit = True
|
||||
if ctx.should_pause_invocation(event):
|
||||
pause_invocation = True
|
||||
|
||||
# Indicates that the loop agent should exist after running this
|
||||
# sub-agent.
|
||||
if should_exit:
|
||||
return
|
||||
|
||||
# Indicates that the invocation should be paused after running this
|
||||
# sub-agent.
|
||||
if pause_invocation:
|
||||
return
|
||||
|
||||
times_looped += 1
|
||||
return
|
||||
|
||||
|
||||
@@ -175,22 +175,35 @@ class ParallelAgent(BaseAgent):
|
||||
async def _run_async_impl(
|
||||
self, ctx: InvocationContext
|
||||
) -> AsyncGenerator[Event, None]:
|
||||
if not self.sub_agents:
|
||||
return
|
||||
|
||||
agent_runs = [
|
||||
sub_agent.run_async(
|
||||
_create_branch_ctx_for_sub_agent(self, sub_agent, ctx)
|
||||
)
|
||||
for sub_agent in self.sub_agents
|
||||
]
|
||||
|
||||
pause_invocation = False
|
||||
try:
|
||||
# TODO remove if once Python <3.11 is no longer supported.
|
||||
if sys.version_info >= (3, 11):
|
||||
async with Aclosing(_merge_agent_run(agent_runs)) as agen:
|
||||
async for event in agen:
|
||||
yield event
|
||||
if ctx.should_pause_invocation(event):
|
||||
pause_invocation = True
|
||||
else:
|
||||
async with Aclosing(_merge_agent_run_pre_3_11(agent_runs)) as agen:
|
||||
async for event in agen:
|
||||
yield event
|
||||
if ctx.should_pause_invocation(event):
|
||||
pause_invocation = True
|
||||
|
||||
if pause_invocation:
|
||||
return
|
||||
|
||||
finally:
|
||||
for sub_agent_run in agent_runs:
|
||||
await sub_agent_run.aclose()
|
||||
|
||||
@@ -24,13 +24,23 @@ from typing_extensions import override
|
||||
|
||||
from ..events.event import Event
|
||||
from ..utils.context_utils import Aclosing
|
||||
from ..utils.feature_decorator import experimental
|
||||
from .base_agent import BaseAgent
|
||||
from .base_agent import BaseAgentConfig
|
||||
from .base_agent import BaseAgentState
|
||||
from .base_agent_config import BaseAgentConfig
|
||||
from .invocation_context import InvocationContext
|
||||
from .llm_agent import LlmAgent
|
||||
from .sequential_agent_config import SequentialAgentConfig
|
||||
|
||||
|
||||
@experimental
|
||||
class SequentialAgentState(BaseAgentState):
|
||||
"""State for SequentialAgent."""
|
||||
|
||||
current_sub_agent: str = ''
|
||||
"""The name of the current sub-agent to run."""
|
||||
|
||||
|
||||
class SequentialAgent(BaseAgent):
|
||||
"""A shell agent that runs its sub-agents in sequence."""
|
||||
|
||||
@@ -41,10 +51,23 @@ class SequentialAgent(BaseAgent):
|
||||
async def _run_async_impl(
|
||||
self, ctx: InvocationContext
|
||||
) -> AsyncGenerator[Event, None]:
|
||||
# Skip if there is no sub-agent.
|
||||
if not self.sub_agents:
|
||||
return
|
||||
|
||||
for sub_agent in self.sub_agents:
|
||||
pause_invocation = False
|
||||
|
||||
async with Aclosing(sub_agent.run_async(ctx)) as agen:
|
||||
async for event in agen:
|
||||
yield event
|
||||
if ctx.should_pause_invocation(event):
|
||||
pause_invocation = True
|
||||
|
||||
# Indicates the invocation should pause when receiving signal from
|
||||
# the current sub_agent.
|
||||
if pause_invocation:
|
||||
return
|
||||
|
||||
@override
|
||||
async def _run_live_impl(
|
||||
@@ -61,6 +84,9 @@ class SequentialAgent(BaseAgent):
|
||||
Args:
|
||||
ctx: The invocation context of the agent.
|
||||
"""
|
||||
if not self.sub_agents:
|
||||
return
|
||||
|
||||
# There is no way to know if it's using live during init phase so we have to init it here
|
||||
for sub_agent in self.sub_agents:
|
||||
# add tool
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
# limitations under the License.
|
||||
|
||||
from .app import App
|
||||
from .app import ResumabilityConfig
|
||||
|
||||
__all__ = [
|
||||
'App',
|
||||
'ResumabilityConfig',
|
||||
]
|
||||
|
||||
@@ -26,6 +26,27 @@ from ..plugins.base_plugin import BasePlugin
|
||||
from ..utils.feature_decorator import experimental
|
||||
|
||||
|
||||
@experimental
|
||||
class ResumabilityConfig(BaseModel):
|
||||
"""The config of the resumability for an application.
|
||||
|
||||
The "resumability" in ADK refers to the ability to:
|
||||
1. pause an invocation upon a long running function call.
|
||||
2. resume an invocation from the last event, if it's paused or failed midway
|
||||
through.
|
||||
|
||||
Note: ADK resumes the invocation in a best-effort manner:
|
||||
1. Tool call to resume needs to be idempotent because we only guarantee
|
||||
an at-least-once behavior once resumed.
|
||||
2. Any temporary / in-memory state will be lost upon resumption.
|
||||
"""
|
||||
|
||||
is_resumable: bool = False
|
||||
"""Whether the app supports agent resumption.
|
||||
If enabled, the feature will be enabled for all agents in the app.
|
||||
"""
|
||||
|
||||
|
||||
@experimental
|
||||
class App(BaseModel):
|
||||
"""Represents an LLM-backed agentic application.
|
||||
@@ -57,3 +78,9 @@ class App(BaseModel):
|
||||
|
||||
context_cache_config: Optional[ContextCacheConfig] = None
|
||||
"""Context cache configuration that applies to all LLM agents in the app."""
|
||||
|
||||
resumability_config: Optional[ResumabilityConfig] = None
|
||||
"""
|
||||
The config of the resumability for the application.
|
||||
If configured, will be applied to all agents in the app.
|
||||
"""
|
||||
|
||||
@@ -12,17 +12,22 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
from fastapi.openapi.models import OAuth2
|
||||
from fastapi.openapi.models import OAuthFlows
|
||||
from fastapi.openapi.models import SecurityBase
|
||||
from fastapi.openapi.models import SecurityScheme
|
||||
from fastapi.openapi.models import SecuritySchemeType
|
||||
from pydantic import Field
|
||||
|
||||
from ..utils.feature_decorator import experimental
|
||||
|
||||
|
||||
class OpenIdConnectWithConfig(SecurityBase):
|
||||
type_: SecuritySchemeType = Field(
|
||||
@@ -65,3 +70,10 @@ class OAuthGrantType(str, Enum):
|
||||
|
||||
# AuthSchemeType re-exports SecuritySchemeType from OpenAPI 3.0.
|
||||
AuthSchemeType = SecuritySchemeType
|
||||
|
||||
|
||||
@experimental
|
||||
class ExtendedOAuth2(OAuth2):
|
||||
"""OAuth2 scheme that incorporates auto-discovery for endpoints."""
|
||||
|
||||
issuer_url: Optional[str] = None # Used for endpoint-discovery
|
||||
|
||||
@@ -14,19 +14,26 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.openapi.models import OAuth2
|
||||
|
||||
from ..agents.callback_context import CallbackContext
|
||||
from ..utils.feature_decorator import experimental
|
||||
from .auth_credential import AuthCredential
|
||||
from .auth_credential import AuthCredentialTypes
|
||||
from .auth_schemes import AuthSchemeType
|
||||
from .auth_schemes import ExtendedOAuth2
|
||||
from .auth_tool import AuthConfig
|
||||
from .exchanger.base_credential_exchanger import BaseCredentialExchanger
|
||||
from .exchanger.credential_exchanger_registry import CredentialExchangerRegistry
|
||||
from .oauth2_discovery import OAuth2DiscoveryManager
|
||||
from .refresher.base_credential_refresher import BaseCredentialRefresher
|
||||
from .refresher.credential_refresher_registry import CredentialRefresherRegistry
|
||||
|
||||
logger = logging.getLogger("google_adk." + __name__)
|
||||
|
||||
|
||||
@experimental
|
||||
class CredentialManager:
|
||||
@@ -74,6 +81,7 @@ class CredentialManager:
|
||||
self._auth_config = auth_config
|
||||
self._exchanger_registry = CredentialExchangerRegistry()
|
||||
self._refresher_registry = CredentialRefresherRegistry()
|
||||
self._discovery_manager = OAuth2DiscoveryManager()
|
||||
|
||||
# Register default exchangers and refreshers
|
||||
# TODO: support service account credential exchanger
|
||||
@@ -247,7 +255,14 @@ class CredentialManager:
|
||||
"auth_config.raw_credential.oauth2 required for credential type "
|
||||
f"{raw_credential.auth_type}"
|
||||
)
|
||||
# Additional validation can be added here
|
||||
|
||||
if self._missing_oauth_info() and not await self._populate_auth_scheme():
|
||||
raise ValueError(
|
||||
"OAuth scheme info is missing, and auto-discovery has failed to fill"
|
||||
" them in."
|
||||
)
|
||||
|
||||
# Additional validation can be added here
|
||||
|
||||
async def _save_credential(
|
||||
self, callback_context: CallbackContext, credential: AuthCredential
|
||||
@@ -259,3 +274,57 @@ class CredentialManager:
|
||||
credential_service = callback_context._invocation_context.credential_service
|
||||
if credential_service:
|
||||
await callback_context.save_credential(self._auth_config)
|
||||
|
||||
async def _populate_auth_scheme(self) -> bool:
|
||||
"""Auto-discover server metadata and populate missing auth scheme info.
|
||||
|
||||
Returns:
|
||||
True if auto-discovery was successful, False otherwise.
|
||||
"""
|
||||
auth_scheme = self._auth_config.auth_scheme
|
||||
if (
|
||||
not isinstance(auth_scheme, ExtendedOAuth2)
|
||||
or not auth_scheme.issuer_url
|
||||
):
|
||||
logger.warning("No issuer_url was provided for auto-discovery.")
|
||||
return False
|
||||
|
||||
metadata = await self._discovery_manager.discover_auth_server_metadata(
|
||||
auth_scheme.issuer_url
|
||||
)
|
||||
if not metadata:
|
||||
logger.warning("Auto-discovery has failed to populate OAuth scheme info.")
|
||||
return False
|
||||
|
||||
flows = auth_scheme.flows
|
||||
|
||||
if flows.implicit and not flows.implicit.authorizationUrl:
|
||||
flows.implicit.authorizationUrl = metadata.authorization_endpoint
|
||||
if flows.password and not flows.password.tokenUrl:
|
||||
flows.password.tokenUrl = metadata.token_endpoint
|
||||
if flows.clientCredentials and not flows.clientCredentials.tokenUrl:
|
||||
flows.clientCredentials.tokenUrl = metadata.token_endpoint
|
||||
if flows.authorizationCode and not flows.authorizationCode.authorizationUrl:
|
||||
flows.authorizationCode.authorizationUrl = metadata.authorization_endpoint
|
||||
if flows.authorizationCode and not flows.authorizationCode.tokenUrl:
|
||||
flows.authorizationCode.tokenUrl = metadata.token_endpoint
|
||||
return True
|
||||
|
||||
def _missing_oauth_info(self) -> bool:
|
||||
"""Checks if we are missing auth/token URLs needed for OAuth."""
|
||||
auth_scheme = self._auth_config.auth_scheme
|
||||
if isinstance(auth_scheme, OAuth2):
|
||||
flows = auth_scheme.flows
|
||||
return (
|
||||
flows.implicit
|
||||
and not flows.implicit.authorizationUrl
|
||||
or flows.password
|
||||
and not flows.password.tokenUrl
|
||||
or flows.clientCredentials
|
||||
and not flows.clientCredentials.tokenUrl
|
||||
or flows.authorizationCode
|
||||
and not flows.authorizationCode.authorizationUrl
|
||||
or flows.authorizationCode
|
||||
and not flows.authorizationCode.tokenUrl
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
|
||||
from google.genai.types import Content
|
||||
@@ -95,3 +96,11 @@ class EventActions(BaseModel):
|
||||
|
||||
compaction: Optional[EventCompaction] = None
|
||||
"""The compaction of the events."""
|
||||
|
||||
end_of_agent: Optional[bool] = None
|
||||
"""If true, the current agent has finished its current run. Note that there
|
||||
can be multiple events with end_of_agent=True for the same agent within one
|
||||
invocation when there is a loop."""
|
||||
|
||||
agent_state: Optional[dict[str, Any]] = None
|
||||
"""The agent state at the current event."""
|
||||
|
||||
@@ -36,6 +36,7 @@ from .agents.live_request_queue import LiveRequestQueue
|
||||
from .agents.llm_agent import LlmAgent
|
||||
from .agents.run_config import RunConfig
|
||||
from .apps.app import App
|
||||
from .apps.app import ResumabilityConfig
|
||||
from .artifacts.base_artifact_service import BaseArtifactService
|
||||
from .artifacts.in_memory_artifact_service import InMemoryArtifactService
|
||||
from .auth.credential_service.base_credential_service import BaseCredentialService
|
||||
@@ -74,6 +75,8 @@ class Runner:
|
||||
session_service: The session service for the runner.
|
||||
memory_service: The memory service for the runner.
|
||||
credential_service: The credential service for the runner.
|
||||
context_cache_config: The context cache config for the runner.
|
||||
resumability_config: The resumability config for the application.
|
||||
"""
|
||||
|
||||
app_name: str
|
||||
@@ -90,6 +93,10 @@ class Runner:
|
||||
"""The memory service for the runner."""
|
||||
credential_service: Optional[BaseCredentialService] = None
|
||||
"""The credential service for the runner."""
|
||||
context_cache_config: Optional[ContextCacheConfig] = None
|
||||
"""The context cache config for the runner."""
|
||||
resumability_config: Optional[ResumabilityConfig] = None
|
||||
"""The resumability config for the application."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -110,11 +117,11 @@ class Runner:
|
||||
`ValueError`. Providing `app` is the recommended way to create a runner.
|
||||
|
||||
Args:
|
||||
app: An optional `App` instance. If provided, `app_name` and `agent`
|
||||
should not be specified.
|
||||
app_name: The application name of the runner. Required if `app` is not
|
||||
provided.
|
||||
agent: The root agent to run. Required if `app` is not provided.
|
||||
app: An optional `App` instance. If provided, `app_name` and `agent`
|
||||
should not be specified.
|
||||
plugins: Deprecated. A list of plugins for the runner. Please use the
|
||||
`app` argument to provide plugins instead.
|
||||
artifact_service: The artifact service for the runner.
|
||||
@@ -126,9 +133,13 @@ class Runner:
|
||||
ValueError: If `app` is provided along with `app_name` or `plugins`, or
|
||||
if `app` is not provided but either `app_name` or `agent` is missing.
|
||||
"""
|
||||
self.app_name, self.agent, self.context_cache_config, plugins = (
|
||||
self._validate_runner_params(app, app_name, agent, plugins)
|
||||
)
|
||||
(
|
||||
self.app_name,
|
||||
self.agent,
|
||||
self.context_cache_config,
|
||||
self.resumability_config,
|
||||
plugins,
|
||||
) = self._validate_runner_params(app, app_name, agent, plugins)
|
||||
self.artifact_service = artifact_service
|
||||
self.session_service = session_service
|
||||
self.memory_service = memory_service
|
||||
@@ -142,7 +153,11 @@ class Runner:
|
||||
agent: Optional[BaseAgent],
|
||||
plugins: Optional[List[BasePlugin]],
|
||||
) -> tuple[
|
||||
str, BaseAgent, Optional[ContextCacheConfig], Optional[List[BasePlugin]]
|
||||
str,
|
||||
BaseAgent,
|
||||
Optional[ContextCacheConfig],
|
||||
Optional[ResumabilityConfig],
|
||||
Optional[List[BasePlugin]],
|
||||
]:
|
||||
"""Validates and extracts runner parameters.
|
||||
|
||||
@@ -153,7 +168,8 @@ class Runner:
|
||||
plugins: A list of plugins for the runner.
|
||||
|
||||
Returns:
|
||||
A tuple containing (app_name, agent, context_cache_config, plugins).
|
||||
A tuple containing (app_name, agent, context_cache_config,
|
||||
resumability_config, plugins).
|
||||
|
||||
Raises:
|
||||
ValueError: If parameters are invalid.
|
||||
@@ -174,12 +190,14 @@ class Runner:
|
||||
agent = app.root_agent
|
||||
plugins = app.plugins
|
||||
context_cache_config = app.context_cache_config
|
||||
resumability_config = app.resumability_config
|
||||
elif not app_name or not agent:
|
||||
raise ValueError(
|
||||
'Either app or both app_name and agent must be provided.'
|
||||
)
|
||||
else:
|
||||
context_cache_config = None
|
||||
resumability_config = None
|
||||
|
||||
if plugins:
|
||||
warnings.warn(
|
||||
@@ -187,7 +205,7 @@ class Runner:
|
||||
' to provide plugins instead.',
|
||||
DeprecationWarning,
|
||||
)
|
||||
return app_name, agent, context_cache_config, plugins
|
||||
return app_name, agent, context_cache_config, resumability_config, plugins
|
||||
|
||||
def run(
|
||||
self,
|
||||
@@ -264,6 +282,7 @@ class Runner:
|
||||
user_id: The user ID of the session.
|
||||
session_id: The session ID of the session.
|
||||
new_message: A new message to append to the session.
|
||||
state_delta: Optional state changes to apply to the session.
|
||||
run_config: The run config for the agent.
|
||||
|
||||
Yields:
|
||||
@@ -687,6 +706,7 @@ class Runner:
|
||||
user_content=new_message,
|
||||
live_request_queue=live_request_queue,
|
||||
run_config=run_config,
|
||||
resumability_config=self.resumability_config,
|
||||
)
|
||||
|
||||
def _new_invocation_context_for_live(
|
||||
|
||||
@@ -371,15 +371,15 @@ class VertexAiSessionService(BaseSessionService):
|
||||
) -> Optional[genai.types.HttpOptions]:
|
||||
return None
|
||||
|
||||
def _get_api_client(self):
|
||||
def _get_api_client(self) -> genai.client.BaseApiClient:
|
||||
"""Instantiates an API client for the given project and location.
|
||||
|
||||
It needs to be instantiated inside each request so that the event loop
|
||||
management can be properly propagated.
|
||||
"""
|
||||
api_client = genai.Client(
|
||||
api_client = genai.client.BaseApiClient(
|
||||
vertexai=True, project=self._project, location=self._location
|
||||
)._api_client
|
||||
)
|
||||
|
||||
if new_options := self._api_client_http_options_override():
|
||||
api_client._http_options = new_options
|
||||
|
||||
@@ -25,17 +25,38 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from google.genai import types
|
||||
from opentelemetry import trace
|
||||
|
||||
from ..agents.invocation_context import InvocationContext
|
||||
from .. import version
|
||||
from ..events.event import Event
|
||||
from ..models.llm_request import LlmRequest
|
||||
from ..models.llm_response import LlmResponse
|
||||
from ..tools.base_tool import BaseTool
|
||||
|
||||
tracer = trace.get_tracer('gcp.vertex.agent')
|
||||
# TODO: Replace with constant from opentelemetry.semconv when it reaches version 1.37 in g3.
|
||||
GEN_AI_AGENT_DESCRIPTION = 'gen_ai.agent.description'
|
||||
GEN_AI_AGENT_NAME = 'gen_ai.agent.name'
|
||||
GEN_AI_CONVERSATION_ID = 'gen_ai.conversation.id'
|
||||
GEN_AI_OPERATION_NAME = 'gen_ai.operation.name'
|
||||
GEN_AI_TOOL_CALL_ID = 'gen_ai.tool.call.id'
|
||||
GEN_AI_TOOL_DESCRIPTION = 'gen_ai.tool.description'
|
||||
GEN_AI_TOOL_NAME = 'gen_ai.tool.name'
|
||||
GEN_AI_TOOL_TYPE = 'gen_ai.tool.type'
|
||||
|
||||
# Needed to avoid circular imports
|
||||
if TYPE_CHECKING:
|
||||
from ..agents.base_agent import BaseAgent
|
||||
from ..agents.invocation_context import InvocationContext
|
||||
from ..models.llm_request import LlmRequest
|
||||
from ..models.llm_response import LlmResponse
|
||||
from ..tools.base_tool import BaseTool
|
||||
|
||||
tracer = trace.get_tracer(
|
||||
instrumenting_module_name='gcp.vertex.agent',
|
||||
instrumenting_library_version=version.__version__,
|
||||
# TODO: Replace with constant from opentelemetry.semconv when it reaches version 1.37 in g3.
|
||||
schema_url='https://opentelemetry.io/schemas/1.37.0',
|
||||
)
|
||||
|
||||
|
||||
def _safe_json_serialize(obj) -> str:
|
||||
@@ -57,6 +78,39 @@ def _safe_json_serialize(obj) -> str:
|
||||
return '<not serializable>'
|
||||
|
||||
|
||||
def trace_agent_invocation(
|
||||
span: trace.Span, agent: BaseAgent, ctx: InvocationContext
|
||||
) -> None:
|
||||
"""Sets span attributes immedietely available on agent invocation according to OTEL semconv version 1.37.
|
||||
|
||||
Args:
|
||||
span: Span on which attributes are set.
|
||||
agent: Agent from which attributes are gathered.
|
||||
ctx: InvocationContext from which attrbiutes are gathered.
|
||||
|
||||
Inference related fields are not set, due to their planned removal from invoke_agent span:
|
||||
https://github.com/open-telemetry/semantic-conventions/issues/2632
|
||||
|
||||
`gen_ai.agent.id` is not set because currently it's unclear what attributes this field should have, specifically:
|
||||
- In which scope should it be unique (globally, given project, given agentic flow, given deployment).
|
||||
- Should it be unchanging between deployments, and how this should this be achieved.
|
||||
|
||||
`gen_ai.data_source.id` is not set because it's not available.
|
||||
Closest type which could contain this information is types.GroundingMetadata, which does not have an ID.
|
||||
|
||||
`server.*` attributes are not set pending confirmation from aabmass.
|
||||
"""
|
||||
|
||||
# Required
|
||||
span.set_attribute(GEN_AI_OPERATION_NAME, 'invoke_agent')
|
||||
|
||||
# Conditionally Required
|
||||
span.set_attribute(GEN_AI_AGENT_DESCRIPTION, agent.description)
|
||||
|
||||
span.set_attribute(GEN_AI_AGENT_NAME, agent.name)
|
||||
span.set_attribute(GEN_AI_CONVERSATION_ID, ctx.session.id)
|
||||
|
||||
|
||||
def trace_tool_call(
|
||||
tool: BaseTool,
|
||||
args: dict[str, Any],
|
||||
@@ -70,40 +124,49 @@ def trace_tool_call(
|
||||
function_response_event: The event with the function response details.
|
||||
"""
|
||||
span = trace.get_current_span()
|
||||
span.set_attribute('gen_ai.system', 'gcp.vertex.agent')
|
||||
span.set_attribute('gen_ai.operation.name', 'execute_tool')
|
||||
span.set_attribute('gen_ai.tool.name', tool.name)
|
||||
span.set_attribute('gen_ai.tool.description', tool.description)
|
||||
tool_call_id = '<not specified>'
|
||||
tool_response = '<not specified>'
|
||||
if function_response_event.content.parts:
|
||||
function_response = function_response_event.content.parts[
|
||||
0
|
||||
].function_response
|
||||
if function_response is not None:
|
||||
tool_call_id = function_response.id
|
||||
tool_response = function_response.response
|
||||
|
||||
span.set_attribute('gen_ai.tool.call.id', tool_call_id)
|
||||
span.set_attribute(GEN_AI_OPERATION_NAME, 'execute_tool')
|
||||
|
||||
span.set_attribute(GEN_AI_TOOL_DESCRIPTION, tool.description)
|
||||
span.set_attribute(GEN_AI_TOOL_NAME, tool.name)
|
||||
|
||||
# e.g. FunctionTool
|
||||
span.set_attribute(GEN_AI_TOOL_TYPE, tool.__class__.__name__)
|
||||
|
||||
# Setting empty llm request and response (as UI expect these) while not
|
||||
# applicable for tool_response.
|
||||
span.set_attribute('gcp.vertex.agent.llm_request', '{}')
|
||||
span.set_attribute('gcp.vertex.agent.llm_response', '{}')
|
||||
|
||||
if not isinstance(tool_response, dict):
|
||||
tool_response = {'result': tool_response}
|
||||
span.set_attribute(
|
||||
'gcp.vertex.agent.tool_call_args',
|
||||
_safe_json_serialize(args),
|
||||
)
|
||||
|
||||
# Tracing tool response
|
||||
tool_call_id = '<not specified>'
|
||||
tool_response = '<not specified>'
|
||||
if (
|
||||
function_response_event.content is not None
|
||||
and function_response_event.content.parts
|
||||
):
|
||||
response_parts = function_response_event.content.parts
|
||||
function_response = response_parts[0].function_response
|
||||
if function_response is not None:
|
||||
if function_response.id is not None:
|
||||
tool_call_id = function_response.id
|
||||
if function_response.response is not None:
|
||||
tool_response = function_response.response
|
||||
|
||||
span.set_attribute(GEN_AI_TOOL_CALL_ID, tool_call_id)
|
||||
|
||||
if not isinstance(tool_response, dict):
|
||||
tool_response = {'result': tool_response}
|
||||
span.set_attribute('gcp.vertex.agent.event_id', function_response_event.id)
|
||||
span.set_attribute(
|
||||
'gcp.vertex.agent.tool_response',
|
||||
_safe_json_serialize(tool_response),
|
||||
)
|
||||
# Setting empty llm request and response (as UI expect these) while not
|
||||
# applicable for tool_response.
|
||||
span.set_attribute('gcp.vertex.agent.llm_request', '{}')
|
||||
span.set_attribute(
|
||||
'gcp.vertex.agent.llm_response',
|
||||
'{}',
|
||||
)
|
||||
|
||||
|
||||
def trace_merged_tool_calls(
|
||||
@@ -121,12 +184,13 @@ def trace_merged_tool_calls(
|
||||
"""
|
||||
|
||||
span = trace.get_current_span()
|
||||
span.set_attribute('gen_ai.system', 'gcp.vertex.agent')
|
||||
span.set_attribute('gen_ai.operation.name', 'execute_tool')
|
||||
span.set_attribute('gen_ai.tool.name', '(merged tools)')
|
||||
span.set_attribute('gen_ai.tool.description', '(merged tools)')
|
||||
span.set_attribute('gen_ai.tool.call.id', response_event_id)
|
||||
|
||||
span.set_attribute(GEN_AI_OPERATION_NAME, 'execute_tool')
|
||||
span.set_attribute(GEN_AI_TOOL_NAME, '(merged tools)')
|
||||
span.set_attribute(GEN_AI_TOOL_DESCRIPTION, '(merged tools)')
|
||||
span.set_attribute(GEN_AI_TOOL_CALL_ID, response_event_id)
|
||||
|
||||
# TODO(b/441461932): See if these are still necessary
|
||||
span.set_attribute('gcp.vertex.agent.tool_call_args', 'N/A')
|
||||
span.set_attribute('gcp.vertex.agent.event_id', response_event_id)
|
||||
try:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user