Skip to content

Commit b9fc737

Browse files
authored
Merge branch 'main' into fix/temp-state-output-key
2 parents 6d1a3f2 + 63f450e commit b9fc737

60 files changed

Lines changed: 5644 additions & 683 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

contributing/samples/agent_engine_code_execution/README

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ This sample data science agent uses Agent Engine Code Execution Sandbox to execu
77

88
## How to use
99

10-
* 1. Follow https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/code-execution/overview to create a code execution sandbox environment.
10+
* 1. Follow https://docs.cloud.google.com/agent-builder/agent-engine/code-execution/quickstart#create-an-agent-engine-instance to create an agent engine instance. Replace the AGENT_ENGINE_RESOURCE_NAME with the one you just created. A new sandbox environment under this agent engine instance will be created for each session with TTL of 1 year. But sandbox can only main its state for up to 14 days. This is the recommended usage for production environments.
1111

12-
* 2. Replace the SANDBOX_RESOURCE_NAME with the one you just created. If you dont want to create a new sandbox environment directly, the Agent Engine Code Execution Sandbox will create one for you by default using the AGENT_ENGINE_RESOURCE_NAME you specified, however, please ensure to clean up sandboxes after use; otherwise, it will consume quotas.
12+
* 2. For testing or protyping purposes, create a sandbox environment by following this guide: https://docs.cloud.google.com/agent-builder/agent-engine/code-execution/quickstart#create_a_sandbox. Replace the SANDBOX_RESOURCE_NAME with the one you just created. This will be used as the default sandbox environment for all the code executions throughout the lifetime of the agent. As the sandbox is re-used across sessions, all sessions will share the same Python environment and variable values."
1313

1414

1515
## Sample prompt

contributing/samples/agent_engine_code_execution/agent.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,10 @@ def base_system_instruction():
8585
8686
""",
8787
code_executor=AgentEngineSandboxCodeExecutor(
88-
# Replace with your sandbox resource name if you already have one.
89-
sandbox_resource_name="SANDBOX_RESOURCE_NAME",
88+
# Replace with your sandbox resource name if you already have one. Only use it for testing or prototyping purposes, because this will use the same sandbox for all requests.
9089
# "projects/vertex-agent-loadtest/locations/us-central1/reasoningEngines/6842889780301135872/sandboxEnvironments/6545148628569161728",
91-
# Replace with agent engine resource name used for creating sandbox if
92-
# sandbox_resource_name is not set.
90+
sandbox_resource_name=None,
91+
# Replace with agent engine resource name used for creating sandbox environment.
9392
agent_engine_resource_name="AGENT_ENGINE_RESOURCE_NAME",
9493
),
9594
)

contributing/samples/authn-adk-all-in-one/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
google-adk==1.12
2-
Flask==3.1.1
2+
Flask==3.1.3
33
flask-cors==6.0.1
44
python-dotenv==1.1.1
55
PyJWT[crypto]==2.10.1

contributing/samples/fields_output_schema/agent.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,20 @@ class WeatherData(BaseModel):
2222
wind_speed: str
2323

2424

25+
def get_current_year() -> str:
26+
"""Get the current year.
27+
28+
Returns:
29+
The current year as a string
30+
"""
31+
from datetime import datetime
32+
33+
return str(datetime.now().year)
34+
35+
2536
root_agent = Agent(
2637
name='root_agent',
27-
model='gemini-2.0-flash',
38+
model='gemini-2.5-flash',
2839
instruction="""\
2940
Answer user's questions based on the data you have.
3041
@@ -43,6 +54,7 @@ class WeatherData(BaseModel):
4354
* wind_speed: 13 mph
4455
4556
""",
46-
output_schema=WeatherData,
57+
output_schema=list[WeatherData],
4758
output_key='weather_data',
59+
tools=[get_current_year],
4860
)

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ dependencies = [
5656
"opentelemetry-resourcedetector-gcp>=1.9.0a0, <2.0.0",
5757
"opentelemetry-sdk>=1.36.0, <1.39.0",
5858
"pyarrow>=14.0.0",
59-
"pydantic>=2.7.0, <3.0.0", # For data validation/models
59+
"pydantic>=2.12.0, <3.0.0", # For data validation/models
6060
"python-dateutil>=2.9.0.post0, <3.0.0", # For Vertext AI Session Service
6161
"python-dotenv>=1.0.0, <2.0.0", # To manage environment variables
6262
"requests>=2.32.4, <3.0.0",
@@ -109,6 +109,7 @@ community = [
109109
eval = [
110110
# go/keep-sorted start
111111
"Jinja2>=3.1.4,<4.0.0", # For eval template rendering
112+
"gepa>=0.1.0",
112113
"google-cloud-aiplatform[evaluation]>=1.100.0",
113114
"pandas>=2.2.3",
114115
"rouge-score>=0.1.2",
@@ -155,6 +156,7 @@ extensions = [
155156
"crewai[tools];python_version>='3.11' and python_version<'3.12'", # For CrewaiTool; chromadb/pypika fail on 3.12+
156157
"docker>=7.0.0", # For ContainerCodeExecutor
157158
"kubernetes>=29.0.0", # For GkeCodeExecutor
159+
"k8s-agent-sandbox>=0.1.1.post2", # For GkeCodeExecutor sandbox mode
158160
"langgraph>=0.2.60, <0.4.8", # For LangGraphAgent
159161
"litellm>=1.75.5, <2.0.0", # For LiteLlm class. Currently has OpenAI limitations. TODO: once LiteLlm fix it
160162
"llama-index-readers-file>=0.4.0", # For retrieval using LlamaIndex.

src/google/adk/a2a/converters/event_converter.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ def convert_a2a_message_to_event(
370370
@a2a_experimental
371371
def convert_event_to_a2a_message(
372372
event: Event,
373-
invocation_context: InvocationContext,
373+
invocation_context: InvocationContext | None = None,
374374
role: Role = Role.agent,
375375
part_converter: GenAIPartToA2APartConverter = convert_genai_part_to_a2a_part,
376376
) -> Optional[Message]:
@@ -390,8 +390,6 @@ def convert_event_to_a2a_message(
390390
"""
391391
if not event:
392392
raise ValueError("Event cannot be None")
393-
if not invocation_context:
394-
raise ValueError("Invocation context cannot be None")
395393

396394
if not event.content or not event.content.parts:
397395
return None

src/google/adk/a2a/converters/part_converter.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ def convert_a2a_part_to_genai_part(
7070
if isinstance(part.file, a2a_types.FileWithUri):
7171
return genai_types.Part(
7272
file_data=genai_types.FileData(
73-
file_uri=part.file.uri, mime_type=part.file.mime_type
73+
file_uri=part.file.uri,
74+
mime_type=part.file.mime_type,
75+
display_name=part.file.name,
7476
)
7577
)
7678

@@ -79,6 +81,7 @@ def convert_a2a_part_to_genai_part(
7981
inline_data=genai_types.Blob(
8082
data=base64.b64decode(part.file.bytes),
8183
mime_type=part.file.mime_type,
84+
display_name=part.file.name,
8285
)
8386
)
8487
else:
@@ -104,10 +107,25 @@ def convert_a2a_part_to_genai_part(
104107
part.metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
105108
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
106109
):
110+
# Restore thought_signature if present
111+
thought_signature = None
112+
thought_sig_key = _get_adk_metadata_key('thought_signature')
113+
if thought_sig_key in part.metadata:
114+
sig_value = part.metadata[thought_sig_key]
115+
if isinstance(sig_value, bytes):
116+
thought_signature = sig_value
117+
elif isinstance(sig_value, str):
118+
try:
119+
thought_signature = base64.b64decode(sig_value)
120+
except Exception:
121+
logger.warning(
122+
'Failed to decode thought_signature: %s', sig_value
123+
)
107124
return genai_types.Part(
108125
function_call=genai_types.FunctionCall.model_validate(
109126
part.data, by_alias=True
110-
)
127+
),
128+
thought_signature=thought_signature,
111129
)
112130
if (
113131
part.metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
@@ -173,6 +191,7 @@ def convert_genai_part_to_a2a_part(
173191
file=a2a_types.FileWithUri(
174192
uri=part.file_data.file_uri,
175193
mime_type=part.file_data.mime_type,
194+
name=part.file_data.display_name,
176195
)
177196
)
178197
)
@@ -196,6 +215,7 @@ def convert_genai_part_to_a2a_part(
196215
file=a2a_types.FileWithBytes(
197216
bytes=base64.b64encode(part.inline_data.data).decode('utf-8'),
198217
mime_type=part.inline_data.mime_type,
218+
name=part.inline_data.display_name,
199219
)
200220
)
201221

@@ -214,16 +234,22 @@ def convert_genai_part_to_a2a_part(
214234
# TODO once A2A defined how to service such information, migrate below
215235
# logic accordingly
216236
if part.function_call:
237+
fc_metadata = {
238+
_get_adk_metadata_key(
239+
A2A_DATA_PART_METADATA_TYPE_KEY
240+
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
241+
}
242+
# Preserve thought_signature if present
243+
if part.thought_signature is not None:
244+
fc_metadata[_get_adk_metadata_key('thought_signature')] = (
245+
base64.b64encode(part.thought_signature).decode('utf-8')
246+
)
217247
return a2a_types.Part(
218248
root=a2a_types.DataPart(
219249
data=part.function_call.model_dump(
220250
by_alias=True, exclude_none=True
221251
),
222-
metadata={
223-
_get_adk_metadata_key(
224-
A2A_DATA_PART_METADATA_TYPE_KEY
225-
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
226-
},
252+
metadata=fc_metadata,
227253
)
228254
)
229255

src/google/adk/a2a/experimental.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
default_message=(
2424
"ADK Implementation for A2A support (A2aAgentExecutor, RemoteA2aAgent "
2525
"and corresponding supporting components etc.) is in experimental mode "
26-
"and is subjected to breaking changes. A2A protocol and SDK are"
26+
"and is subject to breaking changes. A2A protocol and SDK are "
2727
"themselves not experimental. Once it's stable enough the experimental "
2828
"mode will be removed. Your feedback is welcome."
2929
),

src/google/adk/agents/llm_agent.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
from ..tools.function_tool import FunctionTool
5757
from ..tools.tool_configs import ToolConfig
5858
from ..tools.tool_context import ToolContext
59+
from ..utils._schema_utils import SchemaType
60+
from ..utils._schema_utils import validate_schema
5961
from ..utils.context_utils import Aclosing
6062
from .base_agent import BaseAgent
6163
from .base_agent import BaseAgentState
@@ -318,9 +320,16 @@ class LlmAgent(BaseAgent):
318320
# Controlled input/output configurations - Start
319321
input_schema: Optional[type[BaseModel]] = None
320322
"""The input schema when agent is used as a tool."""
321-
output_schema: Optional[type[BaseModel]] = None
323+
output_schema: Optional[SchemaType] = None
322324
"""The output schema when agent replies.
323325
326+
Supports all schema types that the underlying Google GenAI API supports:
327+
- type[BaseModel]: e.g., MySchema
328+
- list[type[BaseModel]]: e.g., list[MySchema]
329+
- list[primitive]: e.g., list[str], list[int]
330+
- dict: Raw dict schemas
331+
- Schema: Google's Schema type
332+
324333
NOTE:
325334
When this is set, agent can ONLY reply and CANNOT use any tools, such as
326335
function tools, RAGs, agent transfer, etc.
@@ -820,12 +829,12 @@ def __maybe_save_output_to_state(self, event: Event):
820829
event.author,
821830
)
822831
return
823-
if (
824-
self.output_key
825-
and event.is_final_response()
826-
and event.content
827-
and event.content.parts
828-
):
832+
833+
if not self.output_key:
834+
return
835+
836+
# Handle text responses
837+
if event.is_final_response() and event.content and event.content.parts:
829838

830839
result = ''.join(
831840
part.text
@@ -838,9 +847,7 @@ def __maybe_save_output_to_state(self, event: Event):
838847
# Do not attempt to parse it as JSON.
839848
if not result.strip():
840849
return
841-
result = self.output_schema.model_validate_json(result).model_dump(
842-
exclude_none=True
843-
)
850+
result = validate_schema(self.output_schema, result)
844851
event.actions.state_delta[self.output_key] = result
845852

846853
@model_validator(mode='after')

src/google/adk/artifacts/base_artifact_service.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@
1616
from abc import ABC
1717
from abc import abstractmethod
1818
from datetime import datetime
19+
import logging
1920
from typing import Any
2021
from typing import Optional
22+
from typing import Union
2123

2224
from google.genai import types
2325
from pydantic import alias_generators
2426
from pydantic import BaseModel
2527
from pydantic import ConfigDict
2628
from pydantic import Field
2729

30+
logger = logging.getLogger("google_adk." + __name__)
31+
2832

2933
class ArtifactVersion(BaseModel):
3034
"""Metadata describing a specific version of an artifact."""
@@ -60,6 +64,26 @@ class ArtifactVersion(BaseModel):
6064
)
6165

6266

67+
def ensure_part(artifact: Union[types.Part, dict[str, Any]]) -> types.Part:
68+
"""Normalizes an artifact to a ``types.Part`` instance.
69+
70+
External callers may provide artifacts as
71+
plain dictionaries with camelCase keys (``inlineData``) instead of properly
72+
deserialized ``types.Part`` objects. ``model_validate`` handles both
73+
camelCase and snake_case dictionaries transparently via Pydantic aliases.
74+
75+
Args:
76+
artifact: A ``types.Part`` instance or a dictionary representation.
77+
78+
Returns:
79+
A validated ``types.Part`` instance.
80+
"""
81+
if isinstance(artifact, dict):
82+
logger.debug("Normalizing artifact dict to types.Part: %s", list(artifact))
83+
return types.Part.model_validate(artifact)
84+
return artifact
85+
86+
6387
class BaseArtifactService(ABC):
6488
"""Abstract base class for artifact services."""
6589

@@ -70,7 +94,7 @@ async def save_artifact(
7094
app_name: str,
7195
user_id: str,
7296
filename: str,
73-
artifact: types.Part,
97+
artifact: Union[types.Part, dict[str, Any]],
7498
session_id: Optional[str] = None,
7599
custom_metadata: Optional[dict[str, Any]] = None,
76100
) -> int:
@@ -84,10 +108,12 @@ async def save_artifact(
84108
app_name: The app name.
85109
user_id: The user ID.
86110
filename: The filename of the artifact.
87-
artifact: The artifact to save. If the artifact consists of `file_data`,
88-
the artifact service assumes its content has been uploaded separately,
89-
and this method will associate the `file_data` with the artifact if
90-
necessary.
111+
artifact: The artifact to save. Accepts a ``types.Part`` instance or a
112+
plain dictionary (camelCase or snake_case keys) which will be
113+
normalized via ``ensure_part``. If the artifact consists of
114+
``file_data``, the artifact service assumes its content has been
115+
uploaded separately, and this method will associate the ``file_data``
116+
with the artifact if necessary.
91117
session_id: The session ID. If `None`, the artifact is user-scoped.
92118
custom_metadata: custom metadata to associate with the artifact.
93119

0 commit comments

Comments
 (0)