mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
feat: expose artifact URLs to the model when available
SaveFilesAsArtifactsPlugin now resolves the canonical URI for each saved artifact. When the URI is model-accessible (`gs://`, `https://`, `http://`), we add a `Part(file_data=...)` so the LLM can fetch the filedirectly while still emitting the placeholder. If no model-accessible URI exists, we retain the original inline blob alongside the placeholder to preserve access to the uploaded bytes. Close #2016 Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 828786519
This commit is contained in:
committed by
Copybara-Service
parent
52db15f133
commit
e3caf79139
@@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
import copy
|
||||
import logging
|
||||
from typing import Optional
|
||||
import urllib.parse
|
||||
|
||||
from google.genai import types
|
||||
|
||||
@@ -25,6 +26,11 @@ from .base_plugin import BasePlugin
|
||||
|
||||
logger = logging.getLogger('google_adk.' + __name__)
|
||||
|
||||
# Schemes supported by our current LLM connectors. Vertex exposes `gs://` while
|
||||
# hosted endpoints use HTTPS. Expand this list when BaseLlm surfaces provider
|
||||
# capabilities.
|
||||
_MODEL_ACCESSIBLE_URI_SCHEMES = {'gs', 'https', 'http'}
|
||||
|
||||
|
||||
class SaveFilesAsArtifactsPlugin(BasePlugin):
|
||||
"""A plugin that saves files embedded in user messages as artifacts.
|
||||
@@ -75,8 +81,9 @@ class SaveFilesAsArtifactsPlugin(BasePlugin):
|
||||
continue
|
||||
|
||||
try:
|
||||
# Use display_name if available; otherwise, generate a filename
|
||||
file_name = part.inline_data.display_name
|
||||
# Use display_name if available, otherwise generate a filename
|
||||
inline_data = part.inline_data
|
||||
file_name = inline_data.display_name
|
||||
if not file_name:
|
||||
file_name = f'artifact_{invocation_context.invocation_id}_{i}'
|
||||
logger.info(
|
||||
@@ -87,7 +94,7 @@ class SaveFilesAsArtifactsPlugin(BasePlugin):
|
||||
display_name = file_name
|
||||
|
||||
# Create a copy to stop mutation of the saved artifact if the original part is modified
|
||||
await invocation_context.artifact_service.save_artifact(
|
||||
version = await invocation_context.artifact_service.save_artifact(
|
||||
app_name=invocation_context.app_name,
|
||||
user_id=invocation_context.user_id,
|
||||
session_id=invocation_context.session.id,
|
||||
@@ -95,10 +102,28 @@ class SaveFilesAsArtifactsPlugin(BasePlugin):
|
||||
artifact=copy.copy(part),
|
||||
)
|
||||
|
||||
# Replace the inline data with a placeholder text (using the clean name)
|
||||
new_parts.append(
|
||||
types.Part(text=f'[Uploaded Artifact: "{display_name}"]')
|
||||
placeholder_part = types.Part(
|
||||
text=f'[Uploaded Artifact: "{display_name}"]'
|
||||
)
|
||||
new_parts.append(placeholder_part)
|
||||
|
||||
file_part = await self._build_file_reference_part(
|
||||
invocation_context=invocation_context,
|
||||
filename=file_name,
|
||||
version=version,
|
||||
mime_type=inline_data.mime_type,
|
||||
display_name=display_name,
|
||||
)
|
||||
if file_part:
|
||||
new_parts.append(file_part)
|
||||
else:
|
||||
logger.debug(
|
||||
'Artifact %s is not exposed via a model-accessible URI; keeping'
|
||||
' inline data in user message.',
|
||||
file_name,
|
||||
)
|
||||
new_parts.append(part)
|
||||
|
||||
modified = True
|
||||
logger.info(f'Successfully saved artifact: {file_name}')
|
||||
|
||||
@@ -112,3 +137,58 @@ class SaveFilesAsArtifactsPlugin(BasePlugin):
|
||||
return types.Content(role=user_message.role, parts=new_parts)
|
||||
else:
|
||||
return None
|
||||
|
||||
async def _build_file_reference_part(
|
||||
self,
|
||||
*,
|
||||
invocation_context: InvocationContext,
|
||||
filename: str,
|
||||
version: int,
|
||||
mime_type: Optional[str],
|
||||
display_name: str,
|
||||
) -> Optional[types.Part]:
|
||||
"""Constructs a file reference part if the artifact URI is model-accessible."""
|
||||
|
||||
artifact_service = invocation_context.artifact_service
|
||||
if not artifact_service:
|
||||
return None
|
||||
|
||||
try:
|
||||
artifact_version = await artifact_service.get_artifact_version(
|
||||
app_name=invocation_context.app_name,
|
||||
user_id=invocation_context.user_id,
|
||||
session_id=invocation_context.session.id,
|
||||
filename=filename,
|
||||
version=version,
|
||||
)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
logger.warning(
|
||||
'Failed to resolve artifact version for %s: %s', filename, exc
|
||||
)
|
||||
return None
|
||||
|
||||
if (
|
||||
not artifact_version
|
||||
or not artifact_version.canonical_uri
|
||||
or not _is_model_accessible_uri(artifact_version.canonical_uri)
|
||||
):
|
||||
return None
|
||||
|
||||
file_data = types.FileData(
|
||||
file_uri=artifact_version.canonical_uri,
|
||||
mime_type=mime_type or artifact_version.mime_type,
|
||||
display_name=display_name,
|
||||
)
|
||||
return types.Part(file_data=file_data)
|
||||
|
||||
|
||||
def _is_model_accessible_uri(uri: str) -> bool:
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(uri)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
if not parsed.scheme:
|
||||
return False
|
||||
|
||||
return parsed.scheme.lower() in _MODEL_ACCESSIBLE_URI_SCHEMES
|
||||
|
||||
Reference in New Issue
Block a user