Skip to content

Commit cdb9819

Browse files
authored
Merge branch 'main' into feat/loop-agent-iteration-metadata
2 parents 25f4c43 + cca8c56 commit cdb9819

38 files changed

Lines changed: 1637 additions & 781 deletions

contributing/samples/context_management/session_state_agent/agent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ async def after_agent_callback(callback_context: CallbackContext):
168168
name='root_agent',
169169
description='a verification agent.',
170170
instruction=(
171-
'Log all users query with `log_query` tool. Must always remind user you'
172-
' cannot answer second query because your setup.'
171+
'Reply to the user. Must always remind user you cannot answer a second'
172+
' query because your setup.'
173173
),
174174
model='gemini-3.5-flash',
175175
before_agent_callback=before_agent_callback,

src/google/adk/artifacts/artifact_util.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
from google.genai import types
2222

23+
from ..errors import input_validation_error
24+
2325

2426
class ParsedArtifactUri(NamedTuple):
2527
"""The result of parsing an artifact URI."""
@@ -113,3 +115,22 @@ def is_artifact_ref(artifact: types.Part) -> bool:
113115
and artifact.file_data.file_uri
114116
and artifact.file_data.file_uri.startswith("artifact://")
115117
)
118+
119+
120+
def validate_artifact_reference_scope(
121+
*,
122+
app_name: str,
123+
user_id: str,
124+
session_id: str | None,
125+
parsed_uri: ParsedArtifactUri,
126+
) -> None:
127+
"""Ensures artifact references cannot escape the caller's scope."""
128+
if parsed_uri.app_name != app_name or parsed_uri.user_id != user_id:
129+
raise input_validation_error.InputValidationError(
130+
"Artifact references must stay within the same app and user scope."
131+
)
132+
if parsed_uri.session_id is not None and parsed_uri.session_id != session_id:
133+
raise input_validation_error.InputValidationError(
134+
"Session-scoped artifact references must stay within the same"
135+
" session scope."
136+
)

src/google/adk/artifacts/file_artifact_service.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from typing import Union
2626
from urllib.parse import unquote
2727
from urllib.parse import urlparse
28+
from urllib.request import url2pathname
2829

2930
from google.genai import types
3031
from pydantic import alias_generators
@@ -59,7 +60,10 @@ def _file_uri_to_path(uri: str) -> Optional[Path]:
5960
parsed = urlparse(uri)
6061
if parsed.scheme != "file":
6162
return None
62-
return Path(unquote(parsed.path))
63+
path_str = unquote(parsed.path)
64+
if os.name == "nt":
65+
path_str = url2pathname(path_str)
66+
return Path(path_str)
6367

6468

6569
_USER_NAMESPACE_PREFIX = "user:"

src/google/adk/artifacts/gcs_artifact_service.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,17 @@ def _save_artifact(
252252
if not file_uri:
253253
raise InputValidationError("Artifact file_data must have a file_uri.")
254254
if artifact_util.is_artifact_ref(artifact):
255-
if not artifact_util.parse_artifact_uri(file_uri):
255+
parsed_uri = artifact_util.parse_artifact_uri(file_uri)
256+
if not parsed_uri:
256257
raise InputValidationError(
257258
f"Invalid artifact reference URI: {file_uri}"
258259
)
260+
artifact_util.validate_artifact_reference_scope(
261+
app_name=app_name,
262+
user_id=user_id,
263+
session_id=session_id,
264+
parsed_uri=parsed_uri,
265+
)
259266
# Store the URI and mime_type (if any) as blob metadata; no content to upload.
260267
metadata = {
261268
**(blob.metadata or {}),
@@ -315,6 +322,12 @@ def _load_artifact(
315322
raise InputValidationError(
316323
f"Invalid artifact reference URI: {file_uri}"
317324
)
325+
artifact_util.validate_artifact_reference_scope(
326+
app_name=app_name,
327+
user_id=user_id,
328+
session_id=session_id,
329+
parsed_uri=parsed_uri,
330+
)
318331
return self._load_artifact(
319332
app_name=parsed_uri.app_name,
320333
user_id=parsed_uri.user_id,

src/google/adk/artifacts/in_memory_artifact_service.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,19 @@ async def save_artifact(
128128
artifact_version.mime_type = "text/plain"
129129
elif artifact.file_data is not None:
130130
if artifact_util.is_artifact_ref(artifact):
131-
if not artifact_util.parse_artifact_uri(artifact.file_data.file_uri):
131+
parsed_uri = artifact_util.parse_artifact_uri(
132+
artifact.file_data.file_uri
133+
)
134+
if not parsed_uri:
132135
raise InputValidationError(
133136
f"Invalid artifact reference URI: {artifact.file_data.file_uri}"
134137
)
138+
artifact_util.validate_artifact_reference_scope(
139+
app_name=app_name,
140+
user_id=user_id,
141+
session_id=session_id,
142+
parsed_uri=parsed_uri,
143+
)
135144
# If it's a valid artifact URI, we store the artifact part as-is.
136145
# And we don't know the mime type until we load it.
137146
else:
@@ -180,6 +189,12 @@ async def load_artifact(
180189
"Invalid artifact reference URI:"
181190
f" {artifact_data.file_data.file_uri}"
182191
)
192+
artifact_util.validate_artifact_reference_scope(
193+
app_name=app_name,
194+
user_id=user_id,
195+
session_id=session_id,
196+
parsed_uri=parsed_uri,
197+
)
183198
return await self.load_artifact(
184199
app_name=parsed_uri.app_name,
185200
user_id=parsed_uri.user_id,

src/google/adk/cli/service_registry.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def my_session_factory(uri: str, **kwargs):
7272
from typing import Protocol
7373
from urllib.parse import unquote
7474
from urllib.parse import urlparse
75+
from urllib.request import url2pathname
7576

7677
from ..artifacts.base_artifact_service import BaseArtifactService
7778
from ..memory.base_memory_service import BaseMemoryService
@@ -310,7 +311,12 @@ def file_artifact_factory(uri: str, **_):
310311
)
311312
if not parsed_uri.path:
312313
raise ValueError("file:// artifact URIs must include a path component.")
313-
artifact_path = Path(unquote(parsed_uri.path))
314+
315+
artifact_path_str = unquote(parsed_uri.path)
316+
if os.name == "nt":
317+
artifact_path_str = url2pathname(artifact_path_str)
318+
319+
artifact_path = Path(artifact_path_str)
314320
return FileArtifactService(root_dir=artifact_path)
315321

316322
registry.register_artifact_service("memory", memory_artifact_factory)

src/google/adk/events/event.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def _accept_convenience_kwargs(cls, data: Any) -> Any:
171171
if not isinstance(data, dict):
172172
return data
173173

174+
data = dict(data)
174175
field_names: set[str] = set(cls.model_fields.keys())
175176
for f in cls.model_fields.values():
176177
if f.alias:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ async def handle_function_calls_live(
668668
invocation_context: InvocationContext,
669669
function_call_event: Event,
670670
tools_dict: dict[str, BaseTool],
671-
) -> Event:
671+
) -> Event | None:
672672
"""Calls the functions and returns the function response event."""
673673
from ...agents.llm_agent import LlmAgent
674674

src/google/adk/integrations/agent_identity/_iam_connector_credentials_provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ async def get_auth_credential(
238238
logger.debug("Auth credential obtained immediately.")
239239
return _construct_auth_credential(response)
240240

241-
if metadata and metadata.consent_pending:
241+
if metadata is not None and "consent_pending" in metadata:
242242
# Get 2-legged OAuth token. Allow enough time for token exchange.
243243
try:
244244
operation = await self._poll_credentials(

src/google/adk/integrations/bigquery/data_insights_tool.py

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,13 @@
1313
# limitations under the License.
1414
from __future__ import annotations
1515

16-
import json
1716
from typing import Any
1817
from typing import Dict
1918
from typing import List
2019

2120
from google.adk.tools import _gda_stream_util
2221
from google.auth.credentials import Credentials
23-
from google.cloud import bigquery
24-
import requests
2522

26-
from . import client
2723
from .config import BigQueryToolConfig
2824

2925
_GDA_CLIENT_ID = "GOOGLE_ADK"
@@ -117,48 +113,39 @@ def ask_data_insights(
117113
"""
118114
try:
119115
location = "global"
120-
if not credentials.token:
121-
error_message = (
122-
"Error: The provided credentials object does not have a valid access"
123-
" token.\n\nThis is often because the credentials need to be"
124-
" refreshed or require specific API scopes. Please ensure the"
125-
" credentials are prepared correctly before calling this"
126-
" function.\n\nThere may be other underlying causes as well."
127-
)
128-
return {
129-
"status": "ERROR",
130-
"error_details": "ask_data_insights requires a valid access token.",
116+
session, endpoint = _gda_stream_util.get_gda_session(credentials)
117+
with session:
118+
headers = {
119+
"Content-Type": "application/json",
120+
"X-Goog-API-Client": _GDA_CLIENT_ID,
131121
}
132-
headers = {
133-
"Authorization": f"Bearer {credentials.token}",
134-
"Content-Type": "application/json",
135-
"X-Goog-API-Client": _GDA_CLIENT_ID,
136-
}
137-
ca_url = f"https://geminidataanalytics.googleapis.com/v1beta/projects/{project_id}/locations/{location}:chat"
122+
ca_url = (
123+
f"{endpoint}/v1beta/projects/{project_id}/locations/{location}:chat"
124+
)
138125

139-
instructions = """**INSTRUCTIONS - FOLLOW THESE RULES:**
126+
instructions = """**INSTRUCTIONS - FOLLOW THESE RULES:**
140127
1. **CONTENT:** Your answer should present the supporting data and then provide a conclusion based on that data, including relevant details and observations where possible.
141128
2. **ANALYSIS DEPTH:** Your analysis must go beyond surface-level observations. Crucially, you must prioritize metrics that measure impact or outcomes over metrics that simply measure volume or raw counts. For open-ended questions, explore the topic from multiple perspectives to provide a holistic view.
142129
3. **OUTPUT FORMAT:** Your entire response MUST be in plain text format ONLY.
143130
4. **NO CHARTS:** You are STRICTLY FORBIDDEN from generating any charts, graphs, images, or any other form of visualization.
144131
"""
145132

146-
ca_payload = {
147-
"project": f"projects/{project_id}",
148-
"messages": [{"userMessage": {"text": user_query_with_context}}],
149-
"inlineContext": {
150-
"datasourceReferences": {
151-
"bq": {"tableReferences": table_references}
152-
},
153-
"systemInstruction": instructions,
154-
"options": {"chart": {"image": {"noImage": {}}}},
155-
},
156-
"clientIdEnum": _GDA_CLIENT_ID,
157-
}
133+
ca_payload = {
134+
"project": f"projects/{project_id}",
135+
"messages": [{"userMessage": {"text": user_query_with_context}}],
136+
"inlineContext": {
137+
"datasourceReferences": {
138+
"bq": {"tableReferences": table_references}
139+
},
140+
"systemInstruction": instructions,
141+
"options": {"chart": {"image": {"noImage": {}}}},
142+
},
143+
"clientIdEnum": _GDA_CLIENT_ID,
144+
}
158145

159-
resp = _gda_stream_util.get_stream(
160-
ca_url, ca_payload, headers, settings.max_query_result_rows
161-
)
146+
resp = _gda_stream_util.get_stream(
147+
session, ca_url, ca_payload, headers, settings.max_query_result_rows
148+
)
162149
except Exception as ex: # pylint: disable=broad-except
163150
return {
164151
"status": "ERROR",

0 commit comments

Comments
 (0)