Skip to content

Commit 0418956

Browse files
authored
Merge branch 'main' into fix/list-agents-filter-non-agents-dirs
2 parents 1066eae + 63f450e commit 0418956

18 files changed

Lines changed: 957 additions & 101 deletions

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
)

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

src/google/adk/artifacts/file_artifact_service.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import shutil
2323
from typing import Any
2424
from typing import Optional
25+
from typing import Union
2526
from urllib.parse import unquote
2627
from urllib.parse import urlparse
2728

@@ -35,6 +36,7 @@
3536
from ..errors.input_validation_error import InputValidationError
3637
from .base_artifact_service import ArtifactVersion
3738
from .base_artifact_service import BaseArtifactService
39+
from .base_artifact_service import ensure_part
3840

3941
logger = logging.getLogger("google_adk." + __name__)
4042

@@ -314,7 +316,7 @@ async def save_artifact(
314316
app_name: str,
315317
user_id: str,
316318
filename: str,
317-
artifact: types.Part,
319+
artifact: Union[types.Part, dict[str, Any]],
318320
session_id: Optional[str] = None,
319321
custom_metadata: Optional[dict[str, Any]] = None,
320322
) -> int:
@@ -339,11 +341,12 @@ def _save_artifact_sync(
339341
self,
340342
user_id: str,
341343
filename: str,
342-
artifact: types.Part,
344+
artifact: Union[types.Part, dict[str, Any]],
343345
session_id: Optional[str],
344346
custom_metadata: Optional[dict[str, Any]],
345347
) -> int:
346348
"""Saves an artifact to disk and returns its version."""
349+
artifact = ensure_part(artifact)
347350
artifact_dir = self._artifact_dir(
348351
user_id=user_id,
349352
session_id=session_id,

src/google/adk/artifacts/gcs_artifact_service.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@
2727
import logging
2828
from typing import Any
2929
from typing import Optional
30+
from typing import Union
3031

3132
from google.genai import types
3233
from typing_extensions import override
3334

3435
from ..errors.input_validation_error import InputValidationError
3536
from .base_artifact_service import ArtifactVersion
3637
from .base_artifact_service import BaseArtifactService
38+
from .base_artifact_service import ensure_part
3739

3840
logger = logging.getLogger("google_adk." + __name__)
3941

@@ -61,7 +63,7 @@ async def save_artifact(
6163
app_name: str,
6264
user_id: str,
6365
filename: str,
64-
artifact: types.Part,
66+
artifact: Union[types.Part, dict[str, Any]],
6567
session_id: Optional[str] = None,
6668
custom_metadata: Optional[dict[str, Any]] = None,
6769
) -> int:
@@ -198,9 +200,10 @@ def _save_artifact(
198200
user_id: str,
199201
session_id: Optional[str],
200202
filename: str,
201-
artifact: types.Part,
203+
artifact: Union[types.Part, dict[str, Any]],
202204
custom_metadata: Optional[dict[str, Any]] = None,
203205
) -> int:
206+
artifact = ensure_part(artifact)
204207
versions = self._list_versions(
205208
app_name=app_name,
206209
user_id=user_id,

src/google/adk/artifacts/in_memory_artifact_service.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import logging
1818
from typing import Any
1919
from typing import Optional
20+
from typing import Union
2021

2122
from google.genai import types
2223
from pydantic import BaseModel
@@ -27,6 +28,7 @@
2728
from ..errors.input_validation_error import InputValidationError
2829
from .base_artifact_service import ArtifactVersion
2930
from .base_artifact_service import BaseArtifactService
31+
from .base_artifact_service import ensure_part
3032

3133
logger = logging.getLogger("google_adk." + __name__)
3234

@@ -99,10 +101,11 @@ async def save_artifact(
99101
app_name: str,
100102
user_id: str,
101103
filename: str,
102-
artifact: types.Part,
104+
artifact: Union[types.Part, dict[str, Any]],
103105
session_id: Optional[str] = None,
104106
custom_metadata: Optional[dict[str, Any]] = None,
105107
) -> int:
108+
artifact = ensure_part(artifact)
106109
path = self._artifact_path(app_name, user_id, filename, session_id)
107110
if path not in self.artifacts:
108111
self.artifacts[path] = []

src/google/adk/flows/llm_flows/_output_schema_processor.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,12 @@ def get_structured_model_response(function_response_event: Event) -> str | None:
110110

111111
for func_response in function_response_event.get_function_responses():
112112
if func_response.name == 'set_model_response':
113-
# Convert dict to JSON string
114-
return json.dumps(func_response.response, ensure_ascii=False)
113+
# Extract the actual result from the wrapped response.
114+
# Tool results are wrapped as {'result': ...} when not already a dict.
115+
response = func_response.response
116+
if isinstance(response, dict) and 'result' in response:
117+
response = response['result']
118+
return json.dumps(response, ensure_ascii=False)
115119

116120
return None
117121

src/google/adk/models/llm_request.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from ..agents.context_cache_config import ContextCacheConfig
2727
from ..tools.base_tool import BaseTool
28+
from ..utils._schema_utils import SchemaType
2829
from .cache_metadata import CacheMetadata
2930

3031

@@ -273,12 +274,27 @@ def append_tools(self, tools: list[BaseTool]) -> None:
273274
# No existing tool with function_declarations, create new one
274275
self.config.tools.append(types.Tool(function_declarations=declarations))
275276

276-
def set_output_schema(self, base_model: type[BaseModel]) -> None:
277+
def set_output_schema(
278+
self,
279+
output_schema: Optional[SchemaType] = None,
280+
*,
281+
base_model: Optional[SchemaType] = None,
282+
) -> None:
277283
"""Sets the output schema for the request.
278284
279285
Args:
280-
base_model: The pydantic base model to set the output schema to.
286+
output_schema: The output schema to set. Supports all types from
287+
SchemaUnion:
288+
- type[BaseModel]: A pydantic model class (e.g., MySchema)
289+
- list[type[BaseModel]]: A generic list type (e.g., list[MySchema])
290+
- list[primitive]: e.g., list[str], list[int]
291+
- dict: Raw dict schemas
292+
- Schema: Google's Schema type
293+
base_model: Deprecated alias for output_schema. Use output_schema instead.
281294
"""
295+
schema = output_schema or base_model
296+
if schema is None:
297+
raise ValueError("Either output_schema or base_model must be provided.")
282298

283-
self.config.response_schema = base_model
299+
self.config.response_schema = schema
284300
self.config.response_mime_type = "application/json"

0 commit comments

Comments
 (0)