adk-python icon indicating copy to clipboard operation
adk-python copied to clipboard

Bug: AttributeError: 'dict' object has no attribute 'inline_data' when uploading a file to a custom ADK Agent via AgentSpace

Open Yash-rai-29 opened this issue 1 month ago • 6 comments

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 version

Expected behavior

  • Uploading a file to AgentSpace should create/save an artifact and continue the agent run without crashing.
  • save_artifact should be resilient to the type of artifact provided by the upstream call.
  • If artifact is 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 version

The 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_artifact implementation inspects the artifact to infer mime_type:

    • artifact.inline_data.mime_type
    • artifact.text
    • artifact.file_data.mime_type
  • This only works if artifact is an object with those attributes (e.g. a types.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, so artifact.inline_data raises AttributeError.

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 version

Why this helps:

  • getattr(...) won’t throw if artifact is a dict or any object without that attribute.
  • We preserve the newer behavior (versioning, canonical URI, setting mime_type) when the artifact is a proper types.Part.
  • We gracefully fall back to "application/octet-stream" when the artifact is not a types.Part, instead of crashing.

Yash-rai-29 avatar Nov 11 '25 12:11 Yash-rai-29

@xuanyang15 Please have a look into it

klateefa avatar Nov 19 '25 06:11 klateefa

@DeanChensj Could you please take a look when you get a chance?

xuanyang15 avatar Nov 19 '25 07:11 xuanyang15

Hi eng team from Gemini Enterprise is taking a look at this issue

DeanChensj avatar Nov 19 '25 21:11 DeanChensj

Is this resolved or not yet?

YoussephAhmed7 avatar Nov 20 '25 14:11 YoussephAhmed7

Very interested as well. When using the BuiltInCodeExecutor, when it generates an image, any follow up question instantly crashes the agent in Gemini Enterprise with Error during async stream generation: 'dict' object has no attribute 'inline_data'.

Using ADK 1.19.0.

svelezdevilla avatar Nov 27 '25 15:11 svelezdevilla

same here

guanerdan avatar Nov 30 '25 15:11 guanerdan