-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Description
Summary
When using AgentSpace and uploading a file, the system crashes during session initialization with:
AttributeError: 'dict' object has no attribute 'inline_data'
This happens in the newer save_artifact implementation of InMemoryArtifactService. The method assumes that the artifact argument is always a types.Part-like object with attributes such as .inline_data, .text, and .file_data, but in practice (during file upload) artifact can be a plain dict.
Because the code accesses artifact.inline_data directly, this raises an AttributeError and breaks the agent flow.
This regression does not happen with the older/previous save_artifact implementation, which simply stored the artifact without inspecting its fields.
Affected code
Current implementation (failing):
@override
async def save_artifact(
self,
*,
app_name: str,
user_id: str,
filename: str,
artifact: types.Part,
session_id: Optional[str] = None,
custom_metadata: Optional[dict[str, Any]] = None,
) -> int:
path = self._artifact_path(app_name, user_id, filename, session_id)
if path not in self.artifacts:
self.artifacts[path] = []
version = len(self.artifacts[path])
if self._file_has_user_namespace(filename):
canonical_uri = (
f"memory://apps/{app_name}/users/{user_id}/artifacts/{filename}/versions/{version}"
)
else:
canonical_uri = (
f"memory://apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{filename}/versions/{version}"
)
artifact_version = ArtifactVersion(
version=version,
canonical_uri=canonical_uri,
)
if custom_metadata:
artifact_version.custom_metadata = custom_metadata
# ── Problem starts here ──
if artifact.inline_data is not None:
artifact_version.mime_type = artifact.inline_data.mime_type
elif artifact.text is not None:
artifact_version.mime_type = "text/plain"
elif artifact.file_data is not None:
if artifact_util.is_artifact_ref(artifact):
if not artifact_util.parse_artifact_uri(artifact.file_data.file_uri):
raise ValueError(
f"Invalid artifact reference URI: {artifact.file_data.file_uri}"
)
# If it's a valid artifact URI, we store the artifact part as-is.
# And we don't know the mime type until we load it.
else:
artifact_version.mime_type = artifact.file_data.mime_type
else:
raise ValueError("Not supported artifact type.")
# ── End problematic block ──
self.artifacts[path].append(
_ArtifactEntry(data=artifact, artifact_version=artifact_version)
)
return versionExpected behavior
- Uploading a file to AgentSpace should create/save an artifact and continue the agent run without crashing.
save_artifactshould be resilient to the type ofartifactprovided by the upstream call.- If
artifactis represented as a dict, it should still be stored and versioned.
This is how the previous implementation behaved:
@override
async def save_artifact(
self,
*,
app_name: str,
user_id: str,
session_id: str,
filename: str,
artifact: types.Part,
) -> int:
path = self._artifact_path(app_name, user_id, session_id, filename)
if path not in self.artifacts:
self.artifacts[path] = []
version = len(self.artifacts[path])
self.artifacts[path].append(artifact)
return versionThe previous version did not assume anything about artifact internals.
Actual behavior
When a file is uploaded and an agent session is started, we hit this traceback:
File ".../vertexai/agent_engines/templates/adk.py", line 1106, in streaming_agent_run_with_events
session = await self._init_session(
File ".../vertexai/agent_engines/templates/adk.py", line 636, in _init_session
saved_version = await artifact_service.save_artifact(
File ".../google/adk/artifacts/in_memory_artifact_service.py", line 121, in save_artifact
if artifact.inline_data is not None:
AttributeError: 'dict' object has no attribute 'inline_data'
So artifact is a plain dict, but save_artifact unconditionally dereferences artifact.inline_data.
Root cause
-
The new
save_artifactimplementation inspects the artifact to infermime_type:artifact.inline_data.mime_typeartifact.textartifact.file_data.mime_type
-
This only works if
artifactis an object with those attributes (e.g. atypes.Part/ protocol buffer style object). -
During AgentSpace upload, the artifact is passed in as a plain Python
dict. Dicts do not expose attributes like.inline_data, soartifact.inline_dataraisesAttributeError.
In short: the function assumes a stricter artifact shape than what the caller actually sends.
Impact
- AgentSpace cannot accept uploaded files without crashing.
- This blocks basic “upload context” / “upload document to session” flows.
Proposed fix
Safely access inline_data, text, and file_data using getattr(...) so that dict-like artifacts don’t immediately cause an AttributeError. Fallback to best-effort MIME type inference.
Example patch:
@override
async def save_artifact(
self,
*,
app_name: str,
user_id: str,
filename: str,
artifact: types.Part,
session_id: Optional[str] = None,
custom_metadata: Optional[dict[str, Any]] = None,
) -> int:
path = self._artifact_path(app_name, user_id, filename, session_id)
if path not in self.artifacts:
self.artifacts[path] = []
version = len(self.artifacts[path])
if self._file_has_user_namespace(filename):
canonical_uri = (
f"memory://apps/{app_name}/users/{user_id}/artifacts/{filename}/versions/{version}"
)
else:
canonical_uri = (
f"memory://apps/{app_name}/users/{user_id}/sessions/{session_id}/artifacts/{filename}/versions/{version}"
)
artifact_version = ArtifactVersion(
version=version,
canonical_uri=canonical_uri,
)
if custom_metadata:
artifact_version.custom_metadata = custom_metadata
# --- Safer field extraction ---
inline_data = getattr(artifact, "inline_data", None)
text = getattr(artifact, "text", None)
file_data = getattr(artifact, "file_data", None)
if inline_data is not None:
artifact_version.mime_type = inline_data.mime_type
elif text is not None:
artifact_version.mime_type = "text/plain"
elif file_data is not None:
if artifact_util.is_artifact_ref(artifact):
if not artifact_util.parse_artifact_uri(file_data.file_uri):
raise ValueError(
f"Invalid artifact reference URI: {file_data.file_uri}"
)
# valid artifact ref: keep as-is, mime type may be resolved later
else:
artifact_version.mime_type = file_data.mime_type
else:
# Fallback: dict payloads / unknown structure
# We still want to save the artifact for backward compatibility
artifact_version.mime_type = "application/octet-stream"
self.artifacts[path].append(
_ArtifactEntry(data=artifact, artifact_version=artifact_version)
)
return versionWhy this helps:
getattr(...)won’t throw ifartifactis a dict or any object without that attribute.- We preserve the newer behavior (versioning, canonical URI, setting
mime_type) when the artifact is a propertypes.Part. - We gracefully fall back to
"application/octet-stream"when the artifact is not atypes.Part, instead of crashing.