Add attachment support for agui - #648
Conversation
0e012c9 to
2aa2e37
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates the core MultimodalPreHook to correctly interpret different attachment “source forms” (bare base64, base64 data: URIs, and remote references like http(s):// / s3://) so that only truly consumable attachments are described + stored, while remote/unsupported forms are preserved and passed through to downstream adapters.
Changes:
- Added source-form resolution logic to split base64
data:URIs into(payload, mime_type)and to detect remote references that must not be stored/described. - Updated request filtering so only attachments actually consumed by the hook are stripped; declined attachments remain in the request list.
- Added a comprehensive pytest suite covering consumable vs remote/declined attachment behaviors and edge cases (including scheme/header case folding).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| ak-py/src/agentkernel/core/multimodal/hooks.py | Adds attachment source parsing/classification and updates filtering to only remove attachments the hook consumes. |
| ak-py/tests/test_multimodal_source_forms.py | Adds regression tests for the supported attachment source forms and ensures declined attachments are retained. |
Suppressed comments (1)
ak-py/src/agentkernel/core/multimodal/hooks.py:330
_resolve_source()treatsdata:*;base64,(base64 marker present but empty payload) as non-consumable becausenot payloadis true, which makes the hook retain an attachment that has no bytes. This is inconsistent with the stated behavior for no-data attachments and can bubble invalid inputs into adapters. Consider recognizing an empty base64 payload as a consumable source with empty data so it can be dropped consistently upstream.
header, _, payload = source.partition(",")
if not payload or not header.lower().endswith(";base64"):
return source, declared_mime or default_mime, False
uri_mime = header[len("data:") :].split(";", 1)[0].lower()
return payload, uri_mime or declared_mime or default_mime, True
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
amithad
left a comment
There was a problem hiding this comment.
Reviewed against spec #523 §8 (docs/specs/523-ag-ui-support/ on the base branch), plan.md iteration 2, and the ak-dev architecture/code-quality/testing skills. Overall: this conforms to the spec and is well built.
Spec conformance (dimension checked requirement by requirement):
- §8a implemented:
data:URIs split into payload + their own mime type (the URI wins over the declared mime and theimage/jpegfallback), bare base64 unchanged,http/https/s3classified not consumable and neither described nor stored. - §8b implemented:
_process_attachmentsreturns the consumed set keyed byid(req), the filter loop retains declined attachments, andAgentRequestAttachmentRefis still always stripped. The retention tests (test_*_is_retained_*) cover exactly the half the spec flags as easy to miss. - Verified the spec's factual claims: retained URL/
data:requests are handled downstream by the OpenAI, ADK, and Pydantic AI adapters; LangGraph/CrewAI/smolagents ignore attachment requests entirely, which matches what those adapters already do withmultimodal.enabled: false. - File scope matches plan.md iteration 2 exactly (hooks.py + the new test file); docs are correctly deferred to PR 7 per the plan.
- Dropping the old
getattr(req, "name", ...)fallback is safe:nameis a required field on both request models.
Findings:
- 2 inline
[question]s on spec-vs-implementation deltas (see comments). [suggestion](not anchorable to this diff): the thread-enabled path still has the bug this PR fixes.ConversationThreadManager.store_attachments(integration/thread/manager.py:188-191) storesreq.image_data/req.file_dataverbatim with theimage/jpeg/application/octet-streamfallback, so with thread mode a URL ordata:URI is stored corrupted before the hook ever sees it, and the hook's ref path then describes those corrupted bytes. design.md's claim that "after this, all five forms do [work]" therefore holds only for the thread-off path. Not a regression from this PR and consistent with spec §8's stated scope, but worth a follow-up issue, and PR 7'sadvanced/multimodal.mdupdate should state which path each source form works on.[nit]PR hygiene: the PR description is the unfilled template, with no linked issue. Please fill it in and link #523 (per the repo PR guidelines), and consider a conventional-commits title, e.g.feat: support all attachment source forms in MultimodalPreHook (#523).[nit]optional cleanups, non-blocking: theif TYPE_CHECKING: passblock in hooks.py is dead and could be dropped while this PR is touching the imports; themultimodal_enabledfixture and_run_hookhelper are now duplicated verbatim withtest_thread_multimodal_hook.pyand could move to a shared conftest/helper.
No existing review feedback to dedupe against. CI is fully green.
Description
Type of Change
Related Issues
Fixes #
Relates to #
Changes Made
Testing
Checklist
Screenshots (if applicable)
Additional Notes