Skip to content

Commit 6a37224

Browse files
fix(adk): disable save_input_blobs_as_artifacts for multimodal support
ADK's runner was replacing inline_data parts with artifact text placeholders ("Uploaded file: artifact_xxx") before the model could see them, causing the model to report it cannot view images. Setting save_input_blobs_as_artifacts=False in RunConfig preserves inline binary data so the model receives actual image/audio/video content. Also adds a test_multimodal_live.py script for interactive testing of multimodal messaging against a running ADK server. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 23e62c6 commit 6a37224

3 files changed

Lines changed: 198 additions & 1 deletion

File tree

integrations/adk-middleware/python/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Legacy `BinaryInputContent` continues to work for backward compatibility
1717
- Adds E2E tests gated on `GOOGLE_API_KEY` covering inline images, document URLs (RFC 2549 via IETF), multi-image messages, and mixed text+image content
1818

19+
### Fixed
20+
21+
- **FIX**: Disable `save_input_blobs_as_artifacts` so inline images reach the model (#1405)
22+
- ADK's runner was converting `inline_data` parts to artifact references before the model could see them, replacing images with text like `"Uploaded file: artifact_xxx. It is saved into artifacts"`
23+
- Setting `save_input_blobs_as_artifacts=False` in `RunConfig` preserves inline binary data so the model receives the actual image/audio/video/document content
24+
1925
## [0.5.2] - 2026-03-26
2026

2127
### Changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#!/usr/bin/env python
2+
"""Interactive script to test multimodal messaging against a running ADK server.
3+
4+
Usage:
5+
python test_multimodal_live.py # text-only chat
6+
python test_multimodal_live.py --image photo.png # send an image
7+
python test_multimodal_live.py --image photo.jpg --text "What's in this image?"
8+
python test_multimodal_live.py --url https://example.com/doc.pdf --text "Summarize this"
9+
10+
Requires the ADK server to be running on http://localhost:8000 (or set --server).
11+
"""
12+
13+
import argparse
14+
import base64
15+
import json
16+
import mimetypes
17+
import sys
18+
import uuid
19+
from pathlib import Path
20+
21+
import httpx
22+
23+
24+
def build_message(text: str, image_path: str | None, url: str | None) -> dict:
25+
"""Build an AG-UI UserMessage with optional multimodal content."""
26+
content_parts = []
27+
28+
if text:
29+
content_parts.append({"type": "text", "text": text})
30+
31+
if image_path:
32+
path = Path(image_path)
33+
if not path.exists():
34+
print(f"Error: file not found: {image_path}", file=sys.stderr)
35+
sys.exit(1)
36+
37+
mime_type = mimetypes.guess_type(str(path))[0] or "application/octet-stream"
38+
data = base64.b64encode(path.read_bytes()).decode("ascii")
39+
content_parts.append({
40+
"type": "image",
41+
"source": {
42+
"type": "data",
43+
"value": data,
44+
"mimeType": mime_type,
45+
},
46+
})
47+
print(f" Attached image: {path.name} ({mime_type}, {len(data)} bytes base64)")
48+
49+
if url:
50+
# Guess mime type from URL extension
51+
mime_type = mimetypes.guess_type(url)[0]
52+
content_parts.append({
53+
"type": "document",
54+
"source": {
55+
"type": "url",
56+
"value": url,
57+
**({"mimeType": mime_type} if mime_type else {}),
58+
},
59+
})
60+
print(f" Attached URL: {url} ({mime_type or 'auto-detect'})")
61+
62+
# If only text, send as plain string; otherwise send content array
63+
if len(content_parts) == 1 and content_parts[0]["type"] == "text":
64+
msg_content = text
65+
elif not content_parts:
66+
msg_content = text or "Hello"
67+
else:
68+
msg_content = content_parts
69+
70+
return {
71+
"id": f"msg-{uuid.uuid4().hex[:8]}",
72+
"role": "user",
73+
"content": msg_content,
74+
}
75+
76+
77+
def send_message(server_url: str, message: dict, thread_id: str):
78+
"""Send a message to the ADK server and stream the response."""
79+
payload = {
80+
"threadId": thread_id,
81+
"runId": f"run-{uuid.uuid4().hex[:8]}",
82+
"messages": [message],
83+
"context": [],
84+
"state": {},
85+
"tools": [],
86+
"forwardedProps": {},
87+
}
88+
89+
print(f"\n--- Sending to {server_url} (thread: {thread_id}) ---\n")
90+
91+
with httpx.stream(
92+
"POST",
93+
server_url,
94+
json=payload,
95+
headers={"Accept": "text/event-stream"},
96+
timeout=60.0,
97+
) as response:
98+
if response.status_code != 200:
99+
print(f"Error: HTTP {response.status_code}")
100+
print(response.read().decode())
101+
return
102+
103+
full_text = []
104+
for line in response.iter_lines():
105+
if not line.strip():
106+
continue
107+
108+
# Parse SSE format
109+
if line.startswith("data: "):
110+
data_str = line[6:]
111+
try:
112+
event = json.loads(data_str)
113+
except json.JSONDecodeError:
114+
continue
115+
116+
event_type = event.get("type")
117+
118+
if event_type == "TEXT_MESSAGE_CONTENT":
119+
delta = event.get("delta", "")
120+
print(delta, end="", flush=True)
121+
full_text.append(delta)
122+
elif event_type == "RUN_STARTED":
123+
print("[Run started]")
124+
elif event_type == "RUN_FINISHED":
125+
print("\n[Run finished]")
126+
elif event_type == "RUN_ERROR":
127+
print(f"\n[ERROR] {event.get('message', 'Unknown error')}")
128+
elif event_type == "TEXT_MESSAGE_START":
129+
pass # beginning of message
130+
elif event_type == "TEXT_MESSAGE_END":
131+
pass # end of message
132+
133+
if full_text:
134+
print(f"\n\n--- Full response ({len(''.join(full_text))} chars) ---")
135+
136+
137+
def main():
138+
parser = argparse.ArgumentParser(description="Test multimodal messaging against ADK server")
139+
parser.add_argument("--server", default="http://localhost:8000/chat/", help="Server endpoint URL")
140+
parser.add_argument("--text", "-t", default=None, help="Text message to send")
141+
parser.add_argument("--image", "-i", default=None, help="Path to an image file to attach")
142+
parser.add_argument("--url", "-u", default=None, help="URL of a document to attach")
143+
parser.add_argument("--thread", default=None, help="Thread ID (default: random)")
144+
parser.add_argument("--interactive", action="store_true", help="Interactive chat mode")
145+
args = parser.parse_args()
146+
147+
thread_id = args.thread or f"thread-{uuid.uuid4().hex[:8]}"
148+
149+
if args.interactive:
150+
print("Interactive multimodal chat (type 'quit' to exit)")
151+
print(" Prefix with /image <path> to attach an image")
152+
print(" Prefix with /url <url> to attach a document URL")
153+
print()
154+
155+
while True:
156+
try:
157+
user_input = input("You: ").strip()
158+
except (EOFError, KeyboardInterrupt):
159+
print("\nBye!")
160+
break
161+
162+
if user_input.lower() in ("quit", "exit", "/quit"):
163+
break
164+
165+
image_path = None
166+
url = None
167+
text = user_input
168+
169+
if user_input.startswith("/image "):
170+
parts = user_input[7:].split(" ", 1)
171+
image_path = parts[0]
172+
text = parts[1] if len(parts) > 1 else "Describe this image."
173+
174+
elif user_input.startswith("/url "):
175+
parts = user_input[5:].split(" ", 1)
176+
url = parts[0]
177+
text = parts[1] if len(parts) > 1 else "What is this document about?"
178+
179+
message = build_message(text, image_path, url)
180+
send_message(args.server, message, thread_id)
181+
print()
182+
else:
183+
if not args.text and not args.image and not args.url:
184+
args.text = "Hello! What can you help me with?"
185+
186+
message = build_message(args.text or "", args.image, args.url)
187+
send_message(args.server, message, thread_id)
188+
189+
190+
if __name__ == "__main__":
191+
main()

integrations/adk-middleware/python/src/ag_ui_adk/adk_agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@ def _default_run_config(self, input: RunAgentInput) -> ADKRunConfig:
725725
"""
726726
config_kwargs = {
727727
'streaming_mode': StreamingMode.SSE,
728-
'save_input_blobs_as_artifacts': True,
728+
'save_input_blobs_as_artifacts': False,
729729
}
730730

731731
# For ADK 1.22.0+, also include context in custom_metadata

0 commit comments

Comments
 (0)