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
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5e6f5853c | |||
| 758d337c76 | |||
| 3cf43e3842 | |||
| 4aa475145f | |||
| 7110336788 | |||
| 0abf4cd2c7 | |||
| 40c15d0595 | |||
| 2010569010 | |||
| 7af1858f46 | |||
| e6da417292 | |||
| 6ee5126d1c | |||
| a88e864755 | |||
| 25ec2c6b61 | |||
| 2f0fe97729 | |||
| f5702d70bf | |||
| 59e88972ae | |||
| de79bf12b5 | |||
| 80ff067c6b | |||
| e0b9712a49 | |||
| 7c7d25a4a6 | |||
| ec660ed4f0 | |||
| 19b607684f | |||
| 8d0279251c | |||
| f50847460f | |||
| 32ee07df01 | |||
| 43c437e38b | |||
| d2dba27134 | |||
| 663cb75b32 | |||
| 64a44c2897 | |||
| 0758f877b1 | |||
| fd8a9e3962 | |||
| 6bc70a6bab | |||
| a2e43aaf19 | |||
| e25227da5e | |||
| c7362100eb | |||
| 483c5bab94 | |||
| 0b9cbd2d42 | |||
| 781f605a1e | |||
| d9e8e9cf32 |
@@ -24,7 +24,7 @@ jobs:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Load adk-bot SSH Private Key
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.ADK_BOT_SSH_PRIVATE_KEY }}
|
||||
|
||||
|
||||
@@ -15,17 +15,17 @@ jobs:
|
||||
python-version: ['3.10', '3.11', '3.12', '3.13',]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Generate Baseline (Main)
|
||||
run: |
|
||||
|
||||
@@ -14,13 +14,13 @@ jobs:
|
||||
python-version: ['3.10', '3.11', '3.12', '3.13',]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v1
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
@@ -119,6 +119,39 @@ type.
|
||||
|
||||
1. Set `CREDENTIALS_TYPE=AuthCredentialTypes.OAUTH2` in `agent.py` and run the agent
|
||||
|
||||
### With Agent Engine and Gemini Enterprise
|
||||
|
||||
This mode is useful when you deploy the agent to Vertex AI Agent Engine and
|
||||
want to make it available in Gemini Enterprise, allowing the agent to access
|
||||
BigQuery on behalf of the end-user. This setup uses OAuth 2.0 managed by
|
||||
Gemini Enterprise.
|
||||
|
||||
1. Create an Authorization resource in Gemini Enterprise by following the guide at
|
||||
[Register and manage ADK agents hosted on Vertex AI Agent Engine](https://docs.cloud.google.com/gemini/enterprise/docs/register-and-manage-an-adk-agent) to:
|
||||
* Create OAuth 2.0 credentials in your Google Cloud project.
|
||||
* Create an Authorization resource in Gemini Enterprise, linking it to your
|
||||
OAuth 2.0 credentials. When creating this resource, you will define a
|
||||
unique identifier (`AUTH_ID`).
|
||||
|
||||
2. Prepare the sample agent for consuming the access token provided by Gemini
|
||||
Enterprise and deploy to Vertex AI Agent Engine.
|
||||
* Set `CREDENTIALS_TYPE=AuthCredentialTypes.HTTP` in `agent.py`. This
|
||||
configures the agent to use access tokens provided by Gemini Enterprise and
|
||||
provided by Agent Engine via the tool context.
|
||||
* Replace `AUTH_ID` in `agent.py` with your authorization resource identifier
|
||||
from step 1.
|
||||
* [Deploy your agent to Vertex AI Agent Engine](https://google.github.io/adk-docs/deploy/agent-engine/).
|
||||
|
||||
3. [Register your deployed agent with Gemini Enterprise](https://docs.cloud.google.com/gemini/enterprise/docs/register-and-manage-an-adk-agent#register-an-adk-agent), attaching the
|
||||
Authorization resource `AUTH_ID`. When this agent is invoked through Gemini
|
||||
Enterprise, an access token obtained using these OAuth credentials will be
|
||||
passed to the agent and made available in the ADK `tool_context` under the key
|
||||
`AUTH_ID`, which `agent.py` is configured to use.
|
||||
|
||||
Once registered, users interacting with your agent via Gemini Enterprise will
|
||||
go through an OAuth consent flow, and Agent Engine will provide the agent with
|
||||
the necessary access tokens to call BigQuery APIs on their behalf.
|
||||
|
||||
## Sample prompts
|
||||
|
||||
* which weather datasets exist in bigquery public data?
|
||||
|
||||
@@ -56,6 +56,17 @@ elif CREDENTIALS_TYPE == AuthCredentialTypes.SERVICE_ACCOUNT:
|
||||
# https://cloud.google.com/iam/docs/service-account-creds#user-managed-keys
|
||||
creds, _ = google.auth.load_credentials_from_file("service_account_key.json")
|
||||
credentials_config = BigQueryCredentialsConfig(credentials=creds)
|
||||
elif CREDENTIALS_TYPE == AuthCredentialTypes.HTTP:
|
||||
# Initialize the tools to use the externally provided access token. One such
|
||||
# use case is creating an authorization resource `AUTH_ID` in Gemini
|
||||
# Enterprise and using it to register an ADK agent deployed to Vertex AI
|
||||
# Agent Engine with Gemini Enterprise. See for more details:
|
||||
# https://docs.cloud.google.com/gemini/enterprise/docs/register-and-manage-an-adk-agent.
|
||||
# This access token will be passed to the agent via the tool context, with
|
||||
# the key `AUTH_ID`.
|
||||
credentials_config = BigQueryCredentialsConfig(
|
||||
external_access_token_key="AUTH_ID"
|
||||
)
|
||||
else:
|
||||
# Initialize the tools to use the application default credentials.
|
||||
# https://cloud.google.com/docs/authentication/provide-credentials-adc
|
||||
|
||||
@@ -159,7 +159,7 @@ def _adk_agent(
|
||||
|
||||
|
||||
class _UserAgent(base_agent.BaseAgent):
|
||||
"""An agent that wraps the provided environment and simulates an user."""
|
||||
"""An agent that wraps the provided environment and simulates a user."""
|
||||
|
||||
env: Env
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ class _ADKAgent(tool_calling_agent.ToolCallingAgent):
|
||||
max_num_steps: The maximum number of steps to run the agent.
|
||||
|
||||
Returns:
|
||||
The result of the solve.
|
||||
The result of the solve function.
|
||||
|
||||
Raises:
|
||||
- ValueError: If the LLM inference failed.
|
||||
|
||||
@@ -52,6 +52,7 @@ Allowed directory: {_allowed_path}
|
||||
'get_file_info',
|
||||
'list_allowed_directories',
|
||||
],
|
||||
use_mcp_resources=True,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
@@ -45,6 +46,24 @@ def get_cwd() -> str:
|
||||
return str(Path.cwd())
|
||||
|
||||
|
||||
# Add a resource for testing with JSON data
|
||||
@mcp.resource(
|
||||
name="sample_data",
|
||||
uri="file:///sample_data.json",
|
||||
mime_type="application/json",
|
||||
)
|
||||
def sample_data() -> str:
|
||||
data = {
|
||||
"users": [
|
||||
{"id": 1, "name": "Alice", "role": "admin"},
|
||||
{"id": 2, "name": "Bob", "role": "user"},
|
||||
{"id": 3, "name": "Charlie", "role": "user"},
|
||||
],
|
||||
"settings": {"theme": "dark", "notifications": True},
|
||||
}
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
|
||||
# Graceful shutdown handler
|
||||
async def shutdown(signal, loop):
|
||||
"""Cleanup tasks tied to the service's shutdown."""
|
||||
|
||||
@@ -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,54 @@
|
||||
# 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 agent demonstrating the use of SkillToolset."""
|
||||
|
||||
import inspect
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.skills import models
|
||||
from google.adk.tools import skill_toolset
|
||||
|
||||
greeting_skill = models.Skill(
|
||||
frontmatter=models.Frontmatter(
|
||||
name="greeting-skill",
|
||||
description=(
|
||||
"A friendly greeting skill that can say hello to a specific person."
|
||||
),
|
||||
),
|
||||
instructions=(
|
||||
"Step 1: Read the 'references/hello_world.txt' file to understand how"
|
||||
" to greet the user. Step 2: Return a greeting based on the reference."
|
||||
),
|
||||
resources=models.Resources(
|
||||
references={
|
||||
"hello_world.txt": "Hello! 👋👋👋 So glad to have you here! ✨✨✨",
|
||||
"example.md": "This is an example reference.",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
my_skill_toolset = skill_toolset.SkillToolset(skills=[greeting_skill])
|
||||
|
||||
root_agent = Agent(
|
||||
model="gemini-2.5-flash",
|
||||
name="skill_user_agent",
|
||||
description="An agent that can use specialized skills.",
|
||||
instruction=(
|
||||
"You are a helpful assistant that can leverage skills to perform tasks."
|
||||
),
|
||||
tools=[
|
||||
my_skill_toolset,
|
||||
],
|
||||
)
|
||||
+1
-1
@@ -143,7 +143,7 @@ docs = [
|
||||
"autodoc_pydantic",
|
||||
"furo",
|
||||
"myst-parser",
|
||||
"sphinx",
|
||||
"sphinx<9.0.0",
|
||||
"sphinx-autodoc-typehints",
|
||||
"sphinx-rtd-theme",
|
||||
]
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -28,6 +30,7 @@ if TYPE_CHECKING:
|
||||
from ..artifacts.base_artifact_service import ArtifactVersion
|
||||
from ..auth.auth_credential import AuthCredential
|
||||
from ..auth.auth_tool import AuthConfig
|
||||
from ..events.event import Event
|
||||
from ..events.event_actions import EventActions
|
||||
from ..sessions.state import State
|
||||
from .invocation_context import InvocationContext
|
||||
@@ -219,3 +222,32 @@ class CallbackContext(ReadonlyContext):
|
||||
await self._invocation_context.memory_service.add_session_to_memory(
|
||||
self._invocation_context.session
|
||||
)
|
||||
|
||||
async def add_events_to_memory(
|
||||
self,
|
||||
*,
|
||||
events: Sequence[Event],
|
||||
custom_metadata: Mapping[str, object] | None = None,
|
||||
) -> None:
|
||||
"""Adds an explicit list of events to the memory service.
|
||||
|
||||
Uses this callback's current session identifiers as memory scope.
|
||||
|
||||
Args:
|
||||
events: Explicit events to add to memory.
|
||||
custom_metadata: Optional standard metadata for memory generation.
|
||||
|
||||
Raises:
|
||||
ValueError: If memory service is not available.
|
||||
"""
|
||||
if self._invocation_context.memory_service is None:
|
||||
raise ValueError(
|
||||
"Cannot add events to memory: memory service is not available."
|
||||
)
|
||||
await self._invocation_context.memory_service.add_events_to_memory(
|
||||
app_name=self._invocation_context.session.app_name,
|
||||
user_id=self._invocation_context.session.user_id,
|
||||
session_id=self._invocation_context.session.id,
|
||||
events=events,
|
||||
custom_metadata=custom_metadata,
|
||||
)
|
||||
|
||||
@@ -469,7 +469,7 @@ class LlmAgent(BaseAgent):
|
||||
self.__maybe_save_output_to_state(event)
|
||||
yield event
|
||||
if ctx.should_pause_invocation(event):
|
||||
# Do not pause immediately, wait until the long running tool call is
|
||||
# Do not pause immediately, wait until the long-running tool call is
|
||||
# executed.
|
||||
should_pause = True
|
||||
if should_pause:
|
||||
@@ -479,7 +479,7 @@ class LlmAgent(BaseAgent):
|
||||
events = ctx._get_events(current_invocation=True, current_branch=True)
|
||||
if events and any(ctx.should_pause_invocation(e) for e in events[-2:]):
|
||||
return
|
||||
# Only yield an end state if the last event is no longer a long running
|
||||
# Only yield an end state if the last event is no longer a long-running
|
||||
# tool call.
|
||||
ctx.set_agent_state(self.name, end_of_agent=True)
|
||||
yield self._create_agent_state_event(ctx)
|
||||
|
||||
@@ -80,6 +80,33 @@ class EventsCompactionConfig(BaseModel):
|
||||
end of the last compacted range. This creates an overlap between consecutive
|
||||
compacted summaries, maintaining context."""
|
||||
|
||||
token_threshold: Optional[int] = Field(
|
||||
default=None,
|
||||
gt=0,
|
||||
)
|
||||
"""Post-invocation token threshold trigger.
|
||||
|
||||
If set, ADK will attempt a post-invocation compaction when the most recently
|
||||
observed prompt token count meets or exceeds this threshold.
|
||||
"""
|
||||
|
||||
event_retention_size: Optional[int] = Field(default=None, ge=0)
|
||||
"""Post-invocation raw event retention size.
|
||||
|
||||
If token-based post-invocation compaction is triggered, this keeps the last N
|
||||
raw events un-compacted.
|
||||
"""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_token_params(self) -> EventsCompactionConfig:
|
||||
token_threshold_set = self.token_threshold is not None
|
||||
retention_size_set = self.event_retention_size is not None
|
||||
if token_threshold_set != retention_size_set:
|
||||
raise ValueError(
|
||||
"token_threshold and event_retention_size must be set together."
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class App(BaseModel):
|
||||
"""Represents an LLM-backed agentic application.
|
||||
|
||||
@@ -16,14 +16,236 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from google.adk.apps.app import App
|
||||
from google.adk.apps.llm_event_summarizer import LlmEventSummarizer
|
||||
from google.adk.sessions.base_session_service import BaseSessionService
|
||||
from google.adk.sessions.session import Session
|
||||
from ..events.event import Event
|
||||
from ..sessions.base_session_service import BaseSessionService
|
||||
from ..sessions.session import Session
|
||||
from .app import App
|
||||
from .llm_event_summarizer import LlmEventSummarizer
|
||||
|
||||
logger = logging.getLogger('google_adk.' + __name__)
|
||||
|
||||
|
||||
def _count_text_chars_in_event(event: Event) -> int:
|
||||
"""Returns the number of text characters in an event's content."""
|
||||
total_chars = 0
|
||||
if event.content and event.content.parts:
|
||||
for part in event.content.parts:
|
||||
if part.text:
|
||||
total_chars += len(part.text)
|
||||
return total_chars
|
||||
|
||||
|
||||
def _is_compaction_subsumed(
|
||||
*,
|
||||
start_timestamp: float,
|
||||
end_timestamp: float,
|
||||
event_index: int,
|
||||
compactions: list[tuple[int, float, float, Event]],
|
||||
) -> bool:
|
||||
"""Returns True if a compaction range is fully contained by another.
|
||||
|
||||
If two compactions have identical ranges, the earlier event is treated as
|
||||
subsumed by the later event.
|
||||
"""
|
||||
for other_index, other_start, other_end, _ in compactions:
|
||||
if other_index == event_index:
|
||||
continue
|
||||
if other_start <= start_timestamp and other_end >= end_timestamp:
|
||||
if (
|
||||
other_start < start_timestamp
|
||||
or other_end > end_timestamp
|
||||
or other_index > event_index
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _estimate_prompt_token_count(events: list[Event]) -> int | None:
|
||||
"""Returns an approximate prompt token count from session events.
|
||||
|
||||
This estimate is compaction-aware: it counts compaction summaries and only
|
||||
counts raw events that would remain visible after applying compaction ranges.
|
||||
"""
|
||||
compactions: list[tuple[int, float, float, Event]] = []
|
||||
for i, event in enumerate(events):
|
||||
if not (event.actions and event.actions.compaction):
|
||||
continue
|
||||
compaction = event.actions.compaction
|
||||
if (
|
||||
compaction.start_timestamp is None
|
||||
or compaction.end_timestamp is None
|
||||
or compaction.compacted_content is None
|
||||
):
|
||||
continue
|
||||
compactions.append((
|
||||
i,
|
||||
compaction.start_timestamp,
|
||||
compaction.end_timestamp,
|
||||
Event(
|
||||
timestamp=compaction.end_timestamp,
|
||||
author='model',
|
||||
content=compaction.compacted_content,
|
||||
branch=event.branch,
|
||||
invocation_id=event.invocation_id,
|
||||
actions=event.actions,
|
||||
),
|
||||
))
|
||||
|
||||
effective_compactions = [
|
||||
(i, start, end, summary_event)
|
||||
for i, start, end, summary_event in compactions
|
||||
if not _is_compaction_subsumed(
|
||||
start_timestamp=start,
|
||||
end_timestamp=end,
|
||||
event_index=i,
|
||||
compactions=compactions,
|
||||
)
|
||||
]
|
||||
compaction_ranges = [
|
||||
(start, end) for _, start, end, _ in effective_compactions
|
||||
]
|
||||
|
||||
def _is_timestamp_compacted(ts: float) -> bool:
|
||||
for start_ts, end_ts in compaction_ranges:
|
||||
if start_ts <= ts <= end_ts:
|
||||
return True
|
||||
return False
|
||||
|
||||
total_chars = 0
|
||||
for _, _, _, summary_event in effective_compactions:
|
||||
total_chars += _count_text_chars_in_event(summary_event)
|
||||
|
||||
for event in events:
|
||||
if event.actions and event.actions.compaction:
|
||||
continue
|
||||
if _is_timestamp_compacted(event.timestamp):
|
||||
continue
|
||||
total_chars += _count_text_chars_in_event(event)
|
||||
|
||||
if total_chars <= 0:
|
||||
return None
|
||||
|
||||
# Rough estimate: 4 characters per token.
|
||||
return total_chars // 4
|
||||
|
||||
|
||||
def _latest_prompt_token_count(events: list[Event]) -> int | None:
|
||||
"""Returns the most recently observed prompt token count, if available."""
|
||||
for event in reversed(events):
|
||||
if (
|
||||
event.usage_metadata
|
||||
and event.usage_metadata.prompt_token_count is not None
|
||||
):
|
||||
return event.usage_metadata.prompt_token_count
|
||||
return _estimate_prompt_token_count(events)
|
||||
|
||||
|
||||
def _latest_compaction_event(events: list[Event]) -> Event | None:
|
||||
"""Returns the compaction event with the greatest covered end timestamp."""
|
||||
latest_event = None
|
||||
latest_end = 0.0
|
||||
for event in events:
|
||||
if (
|
||||
event.actions
|
||||
and event.actions.compaction
|
||||
and event.actions.compaction.end_timestamp is not None
|
||||
):
|
||||
end_ts = event.actions.compaction.end_timestamp
|
||||
if end_ts is not None and end_ts >= latest_end:
|
||||
latest_end = end_ts
|
||||
latest_event = event
|
||||
return latest_event
|
||||
|
||||
|
||||
def _latest_compaction_end_timestamp(events: list[Event]) -> float:
|
||||
"""Returns the end timestamp of the most recent compaction event."""
|
||||
latest_event = _latest_compaction_event(events)
|
||||
if not latest_event or not latest_event.actions.compaction:
|
||||
return 0.0
|
||||
if latest_event.actions.compaction.end_timestamp is None:
|
||||
return 0.0
|
||||
return latest_event.actions.compaction.end_timestamp
|
||||
|
||||
|
||||
async def _run_compaction_for_token_threshold(
|
||||
app: App, session: Session, session_service: BaseSessionService
|
||||
):
|
||||
"""Runs post-invocation compaction based on a token threshold.
|
||||
|
||||
If triggered, this compacts older raw events and keeps the last
|
||||
`event_retention_size` raw events un-compacted.
|
||||
"""
|
||||
config = app.events_compaction_config
|
||||
if not config:
|
||||
return False
|
||||
if config.token_threshold is None or config.event_retention_size is None:
|
||||
return False
|
||||
|
||||
prompt_token_count = _latest_prompt_token_count(session.events)
|
||||
if prompt_token_count is None or prompt_token_count < config.token_threshold:
|
||||
return False
|
||||
|
||||
latest_compaction_event = _latest_compaction_event(session.events)
|
||||
last_compacted_end_timestamp = 0.0
|
||||
if (
|
||||
latest_compaction_event
|
||||
and latest_compaction_event.actions
|
||||
and latest_compaction_event.actions.compaction
|
||||
and latest_compaction_event.actions.compaction.end_timestamp is not None
|
||||
):
|
||||
last_compacted_end_timestamp = (
|
||||
latest_compaction_event.actions.compaction.end_timestamp
|
||||
)
|
||||
candidate_events = [
|
||||
e
|
||||
for e in session.events
|
||||
if not (e.actions and e.actions.compaction)
|
||||
and e.timestamp > last_compacted_end_timestamp
|
||||
]
|
||||
|
||||
if len(candidate_events) <= config.event_retention_size:
|
||||
return False
|
||||
|
||||
if config.event_retention_size == 0:
|
||||
events_to_compact = candidate_events
|
||||
else:
|
||||
events_to_compact = candidate_events[: -config.event_retention_size]
|
||||
if not events_to_compact:
|
||||
return False
|
||||
|
||||
# Rolling summary: if a previous compaction exists, seed the next summary with
|
||||
# the previous compaction summary content so new compactions can subsume older
|
||||
# ones while still keeping `event_retention_size` raw events visible.
|
||||
if (
|
||||
latest_compaction_event
|
||||
and latest_compaction_event.actions
|
||||
and latest_compaction_event.actions.compaction
|
||||
and latest_compaction_event.actions.compaction.start_timestamp is not None
|
||||
and latest_compaction_event.actions.compaction.compacted_content
|
||||
is not None
|
||||
):
|
||||
seed_event = Event(
|
||||
timestamp=latest_compaction_event.actions.compaction.start_timestamp,
|
||||
author='model',
|
||||
content=latest_compaction_event.actions.compaction.compacted_content,
|
||||
branch=latest_compaction_event.branch,
|
||||
invocation_id=Event.new_id(),
|
||||
)
|
||||
events_to_compact = [seed_event] + events_to_compact
|
||||
|
||||
if not config.summarizer:
|
||||
config.summarizer = LlmEventSummarizer(llm=app.root_agent.canonical_model)
|
||||
|
||||
compaction_event = await config.summarizer.maybe_summarize_events(
|
||||
events=events_to_compact
|
||||
)
|
||||
if compaction_event:
|
||||
await session_service.append_event(session=session, event=compaction_event)
|
||||
logger.debug('Token-threshold event compactor finished.')
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def _run_compaction_for_sliding_window(
|
||||
app: App, session: Session, session_service: BaseSessionService
|
||||
):
|
||||
@@ -109,6 +331,18 @@ async def _run_compaction_for_sliding_window(
|
||||
events = session.events
|
||||
if not events:
|
||||
return None
|
||||
|
||||
# Prefer token-threshold compaction if configured and triggered.
|
||||
if (
|
||||
app.events_compaction_config
|
||||
and app.events_compaction_config.token_threshold is not None
|
||||
):
|
||||
token_compacted = await _run_compaction_for_token_threshold(
|
||||
app, session, session_service
|
||||
)
|
||||
if token_compacted:
|
||||
return None
|
||||
|
||||
# Find the last compaction event and its range.
|
||||
last_compacted_end_timestamp = 0.0
|
||||
for event in reversed(events):
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import AsyncGenerator
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
@@ -30,9 +29,6 @@ from .auth_handler import AuthHandler
|
||||
from .auth_tool import AuthConfig
|
||||
from .auth_tool import AuthToolArguments
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..agents.llm_agent import LlmAgent
|
||||
|
||||
# Prefix used by toolset auth credential IDs.
|
||||
# Auth requests with this prefix are for toolset authentication (before tool
|
||||
# listing) and don't require resuming a function call.
|
||||
@@ -46,10 +42,8 @@ class _AuthLlmRequestProcessor(BaseLlmRequestProcessor):
|
||||
async def run_async(
|
||||
self, invocation_context: InvocationContext, llm_request: LlmRequest
|
||||
) -> AsyncGenerator[Event, None]:
|
||||
from ..agents.llm_agent import LlmAgent
|
||||
|
||||
agent = invocation_context.agent
|
||||
if not isinstance(agent, LlmAgent):
|
||||
if not hasattr(agent, 'canonical_tools'):
|
||||
return
|
||||
events = invocation_context.session.events
|
||||
if not events:
|
||||
|
||||
@@ -20,6 +20,7 @@ import importlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import typing
|
||||
@@ -88,6 +89,7 @@ from ..runners import Runner
|
||||
from ..sessions.base_session_service import BaseSessionService
|
||||
from ..sessions.session import Session
|
||||
from ..utils.context_utils import Aclosing
|
||||
from ..version import __version__
|
||||
from .cli_eval import EVAL_SESSION_ID_PREFIX
|
||||
from .utils import cleanup
|
||||
from .utils import common
|
||||
@@ -492,6 +494,7 @@ class AdkWebServer:
|
||||
logo_text: Optional[str] = None,
|
||||
logo_image_url: Optional[str] = None,
|
||||
url_prefix: Optional[str] = None,
|
||||
auto_create_session: bool = False,
|
||||
):
|
||||
self.agent_loader = agent_loader
|
||||
self.session_service = session_service
|
||||
@@ -509,6 +512,7 @@ class AdkWebServer:
|
||||
self.current_app_name_ref: SharedValue[str] = SharedValue(value="")
|
||||
self.runner_dict = {}
|
||||
self.url_prefix = url_prefix
|
||||
self.auto_create_session = auto_create_session
|
||||
|
||||
async def get_runner_async(self, app_name: str) -> Runner:
|
||||
"""Returns the cached runner for the given app."""
|
||||
@@ -558,6 +562,7 @@ class AdkWebServer:
|
||||
session_service=self.session_service,
|
||||
memory_service=self.memory_service,
|
||||
credential_service=self.credential_service,
|
||||
auto_create_session=self.auto_create_session,
|
||||
)
|
||||
|
||||
def _instantiate_extra_plugins(self) -> list[BasePlugin]:
|
||||
@@ -757,6 +762,18 @@ class AdkWebServer:
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.get("/version")
|
||||
async def version() -> dict[str, str]:
|
||||
return {
|
||||
"version": __version__,
|
||||
"language": "python",
|
||||
"language_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||
}
|
||||
|
||||
@app.get("/list-apps")
|
||||
async def list_apps(
|
||||
detailed: bool = Query(
|
||||
@@ -1347,6 +1364,24 @@ class AdkWebServer:
|
||||
raise HTTPException(status_code=404, detail="Artifact not found")
|
||||
return artifact
|
||||
|
||||
@app.get(
|
||||
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}/versions/metadata",
|
||||
response_model=list[ArtifactVersion],
|
||||
response_model_exclude_none=True,
|
||||
)
|
||||
async def list_artifact_versions_metadata(
|
||||
app_name: str,
|
||||
user_id: str,
|
||||
session_id: str,
|
||||
artifact_name: str,
|
||||
) -> list[ArtifactVersion]:
|
||||
return await self.artifact_service.list_artifact_versions(
|
||||
app_name=app_name,
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
filename=artifact_name,
|
||||
)
|
||||
|
||||
@app.get(
|
||||
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}/versions/{version_id}",
|
||||
response_model_exclude_none=True,
|
||||
@@ -1416,6 +1451,31 @@ class AdkWebServer:
|
||||
)
|
||||
return artifact_version
|
||||
|
||||
@app.get(
|
||||
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{artifact_name}/versions/{version_id}/metadata",
|
||||
response_model=ArtifactVersion,
|
||||
response_model_exclude_none=True,
|
||||
)
|
||||
async def get_artifact_version_metadata(
|
||||
app_name: str,
|
||||
user_id: str,
|
||||
session_id: str,
|
||||
artifact_name: str,
|
||||
version_id: int,
|
||||
) -> ArtifactVersion:
|
||||
artifact_version = await self.artifact_service.get_artifact_version(
|
||||
app_name=app_name,
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
filename=artifact_name,
|
||||
version=version_id,
|
||||
)
|
||||
if not artifact_version:
|
||||
raise HTTPException(
|
||||
status_code=404, detail="Artifact version not found"
|
||||
)
|
||||
return artifact_version
|
||||
|
||||
@app.get(
|
||||
"/apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts",
|
||||
response_model_exclude_none=True,
|
||||
@@ -1503,6 +1563,7 @@ class AdkWebServer:
|
||||
session_id=req.session_id,
|
||||
new_message=req.new_message,
|
||||
state_delta=req.state_delta,
|
||||
invocation_id=req.invocation_id,
|
||||
)
|
||||
) as agen:
|
||||
events = [event async for event in agen]
|
||||
|
||||
@@ -294,11 +294,28 @@ def cli_conformance_record(
|
||||
" runs evaluation-based verification."
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--generate_report",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Optional. Whether to generate a Markdown report of the test results.",
|
||||
)
|
||||
@click.option(
|
||||
"--report_dir",
|
||||
type=click.Path(file_okay=False, dir_okay=True, resolve_path=True),
|
||||
help=(
|
||||
"Optional. Directory to store the generated report. Defaults to current"
|
||||
" directory."
|
||||
),
|
||||
)
|
||||
@click.pass_context
|
||||
def cli_conformance_test(
|
||||
ctx,
|
||||
paths: tuple[str, ...],
|
||||
mode: str,
|
||||
generate_report: bool,
|
||||
report_dir: Optional[str] = None,
|
||||
):
|
||||
"""Run conformance tests to verify agent behavior consistency.
|
||||
|
||||
@@ -309,7 +326,7 @@ def cli_conformance_test(
|
||||
- Contain a spec.yaml file directly (single test case)
|
||||
- Contain subdirectories with spec.yaml files (multiple test cases)
|
||||
|
||||
If no paths are provided, defaults to searching the 'tests' folder.
|
||||
If no paths are provided, defaults to searching for the 'tests' folder.
|
||||
|
||||
TEST MODES:
|
||||
|
||||
@@ -329,6 +346,11 @@ def cli_conformance_test(
|
||||
generated-recordings.yaml # Recorded interactions (replay mode)
|
||||
generated-session.yaml # Session data (replay mode)
|
||||
|
||||
REPORT GENERATION:
|
||||
|
||||
Use --generate_report to create a Markdown report of test results.
|
||||
Use --report_dir to specify where the report should be saved.
|
||||
|
||||
EXAMPLES:
|
||||
|
||||
\b
|
||||
@@ -346,6 +368,14 @@ def cli_conformance_test(
|
||||
\b
|
||||
# Run in live mode (when available)
|
||||
adk conformance test --mode=live tests/core
|
||||
|
||||
\b
|
||||
# Generate a test report
|
||||
adk conformance test --generate_report
|
||||
|
||||
\b
|
||||
# Generate a test report in a specific directory
|
||||
adk conformance test --generate_report --report_dir=reports
|
||||
"""
|
||||
|
||||
try:
|
||||
@@ -363,10 +393,18 @@ def cli_conformance_test(
|
||||
)
|
||||
ctx.exit(1)
|
||||
|
||||
# Convert to Path objects, use default if empty (paths are already resolved by Click)
|
||||
# Convert to Path objects, use default if empty (paths are already resolved
|
||||
# by Click)
|
||||
test_paths = [Path(p) for p in paths] if paths else [Path("tests").resolve()]
|
||||
|
||||
asyncio.run(run_conformance_test(test_paths=test_paths, mode=mode.lower()))
|
||||
asyncio.run(
|
||||
run_conformance_test(
|
||||
test_paths=test_paths,
|
||||
mode=mode.lower(),
|
||||
generate_report=generate_report,
|
||||
report_dir=report_dir,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@main.command("create", cls=HelpfulCommand)
|
||||
@@ -1381,6 +1419,14 @@ def cli_web(
|
||||
@fast_api_common_options()
|
||||
@adk_services_options(default_use_local_storage=True)
|
||||
@deprecated_adk_services_options()
|
||||
@click.option(
|
||||
"--auto_create_session",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help=(
|
||||
"Automatically create a session if it doesn't exist when calling /run."
|
||||
),
|
||||
)
|
||||
def cli_api_server(
|
||||
agents_dir: str,
|
||||
eval_storage_uri: Optional[str] = None,
|
||||
@@ -1401,6 +1447,7 @@ def cli_api_server(
|
||||
a2a: bool = False,
|
||||
reload_agents: bool = False,
|
||||
extra_plugins: Optional[list[str]] = None,
|
||||
auto_create_session: bool = False,
|
||||
):
|
||||
"""Starts a FastAPI server for agents.
|
||||
|
||||
@@ -1433,6 +1480,7 @@ def cli_api_server(
|
||||
url_prefix=url_prefix,
|
||||
reload_agents=reload_agents,
|
||||
extra_plugins=extra_plugins,
|
||||
auto_create_session=auto_create_session,
|
||||
),
|
||||
host=host,
|
||||
port=port,
|
||||
@@ -1494,14 +1542,20 @@ def cli_api_server(
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Optional. Whether to enable Cloud Trace for cloud run.",
|
||||
help=(
|
||||
"Optional. Whether to enable Cloud Trace export for Cloud Run"
|
||||
" deployments."
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--otel_to_cloud",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Optional. Whether to enable OpenTelemetry for Agent Engine.",
|
||||
help=(
|
||||
"Optional. Whether to enable OpenTelemetry export to GCP for Cloud Run"
|
||||
" deployments."
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--with_ui",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user