Skip to content

Commit 2152ecc

Browse files
authored
Merge branch 'main' into fix/gke-deploy-gcloud-windows
2 parents fe69e2f + cca8c56 commit 2152ecc

27 files changed

Lines changed: 1003 additions & 296 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/telemetry/_instrumentation.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ def record_llm_response(
125125
def _record_agent_metrics(
126126
agent_name: str,
127127
elapsed_s: float,
128-
events: object,
129128
caught_error: Exception | None,
130129
) -> None:
131130
try:
@@ -134,7 +133,6 @@ def _record_agent_metrics(
134133
elapsed_s,
135134
caught_error,
136135
)
137-
_metrics.record_agent_workflow_steps(agent_name, events)
138136
except Exception: # pylint: disable=broad-exception-caught
139137
logger.exception("Failed to record agent metrics for agent %s", agent_name)
140138

@@ -196,7 +194,6 @@ async def record_agent_invocation(
196194
_record_agent_metrics(
197195
agent.name,
198196
_metrics.get_elapsed_s(span, start_time),
199-
getattr(getattr(ctx, "session", None), "events", []),
200197
caught_error,
201198
)
202199
_flush_invoke_agent_metrics(tel_ctx, agent.name)

src/google/adk/telemetry/_metrics.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
from opentelemetry.semconv.attributes import error_attributes
2828

2929
if TYPE_CHECKING:
30-
from google.adk.events.event import Event
3130
from google.adk.models.llm_request import LlmRequest
3231
from google.adk.models.llm_response import LlmResponse
3332
from opentelemetry.trace import Span
@@ -91,24 +90,6 @@
9190
81.92,
9291
],
9392
)
94-
_agent_workflow_steps = meter.create_histogram(
95-
"gen_ai.agent.workflow.steps",
96-
unit="1",
97-
description="Length of agentic workflow (# of events).",
98-
explicit_bucket_boundaries_advisory=[
99-
1,
100-
2,
101-
4,
102-
8,
103-
16,
104-
32,
105-
64,
106-
128,
107-
256,
108-
512,
109-
1024,
110-
],
111-
)
11293
_client_operation_duration = (
11394
gen_ai_metrics.create_gen_ai_client_operation_duration(meter)
11495
)
@@ -198,13 +179,6 @@ def record_invoke_agent_tool_calls(agent_name: str, count: int) -> None:
198179
_invoke_agent_tool_calls.record(count, attributes=attrs)
199180

200181

201-
def record_agent_workflow_steps(agent_name: str, events: list[Event]):
202-
"""Records the number of steps in the agent workflow by counting the number of events."""
203-
attrs = {gen_ai_attributes.GEN_AI_AGENT_NAME: agent_name}
204-
count = sum(1 for event in events if event.author == agent_name)
205-
_agent_workflow_steps.record(count, attributes=attrs)
206-
207-
208182
def record_tool_execution_duration(
209183
tool_name: str,
210184
tool_type: str,

src/google/adk/telemetry/node_tracing.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,6 @@ def _use_invoke_workflow_span(
186186
# The flag rides along the otel_context propagated to child nodes, so nested
187187
# workflows see it set.
188188
nested = bool(context_api.get_value(_ENTRYPOINT_WORKFLOW_KEY, otel_context))
189-
if not nested:
190-
otel_context = context_api.set_value(
191-
_ENTRYPOINT_WORKFLOW_KEY, True, otel_context
192-
)
193189
attributes: dict[str, AttributeValue] = {
194190
GEN_AI_OPERATION_NAME: "invoke_workflow",
195191
GEN_AI_CONVERSATION_ID: conversation_id,
@@ -207,11 +203,14 @@ def _use_invoke_workflow_span(
207203
start_s = time.monotonic()
208204
workflow_span: Span | None = None
209205
try:
210-
with tracer.start_as_current_span(
211-
name=span_name,
212-
attributes=attributes,
213-
context=otel_context,
214-
) as span:
206+
with (
207+
tracer.start_as_current_span(
208+
name=span_name,
209+
attributes=attributes,
210+
context=otel_context,
211+
) as span,
212+
_mark_nested_workflows(),
213+
):
215214
workflow_span = span
216215
yield span
217216
finally:
@@ -221,3 +220,14 @@ def _use_invoke_workflow_span(
221220
nested=nested,
222221
error=sys.exc_info()[1],
223222
)
223+
224+
225+
@contextmanager
226+
def _mark_nested_workflows() -> Iterator[None]:
227+
token = context_api.attach(
228+
context_api.set_value(_ENTRYPOINT_WORKFLOW_KEY, True)
229+
)
230+
try:
231+
yield
232+
finally:
233+
context_api.detach(token)

0 commit comments

Comments
 (0)