Skip to content

Commit c51f268

Browse files
authored
Merge branch 'main' into fix/langgraph-tool-name-none-normal-branch
2 parents f0f819a + 0947b43 commit c51f268

23 files changed

Lines changed: 1422 additions & 257 deletions

integrations/adk-middleware/python/CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- **BREAKING**: Migrate from deprecated `THINKING_*` events to `REASONING_*` events (#1406)
13+
- `THINKING_START` / `THINKING_END``REASONING_START` / `REASONING_END`
14+
- `THINKING_TEXT_MESSAGE_START` / `CONTENT` / `END``REASONING_MESSAGE_START` / `CONTENT` / `END`
15+
- All reasoning events now carry a `message_id` for client-side correlation and `role="reasoning"` on message start
16+
- Internal state variables renamed accordingly (`_is_thinking``_is_reasoning`, etc.)
17+
- Aligns the ADK middleware with the Claude Agent SDK and LangGraph integrations, which already use `REASONING_*` events
18+
19+
### Added
20+
21+
- **NEW**: `REASONING_ENCRYPTED_VALUE` support for Gemini thought signatures (#1406)
22+
- Extracts `thought_signature` (opaque bytes) from Google GenAI SDK `Part` objects when present
23+
- Emits `REASONING_ENCRYPTED_VALUE` events with `subtype="message"` and base64-encoded signature
24+
- Enables encrypted reasoning / zero-data-retention workflows with Gemini models
25+
26+
- **NEW**: Reasoning chat example (`examples/server/api/agentic_chat_reasoning.py`)
27+
- Demonstrates `REASONING_*` event emission using Gemini 2.5 Flash with `include_thoughts=True`
28+
- Registered at `/adk-reasoning-chat` in the example server
29+
30+
- **NEW**: Support for multimodal input types (`ImageInputContent`, `AudioInputContent`, `VideoInputContent`, `DocumentInputContent`) (#1405)
31+
- Replaces reliance on the deprecated `BinaryInputContent` with the newer modality-specific types defined in the AG-UI protocol
32+
- `InputContentDataSource` (inline base64) converts to `types.Part(inline_data=types.Blob(...))`, same as before
33+
- `InputContentUrlSource` (HTTPS/GCS URLs) converts to `types.Part(file_data=types.FileData(file_uri=...))`, leveraging ADK's native URI support
34+
- Legacy `BinaryInputContent` continues to work for backward compatibility
35+
- 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
36+
37+
### Fixed
38+
39+
- **FIX**: Disable `save_input_blobs_as_artifacts` so inline images reach the model (#1405)
40+
- 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"`
41+
- Setting `save_input_blobs_as_artifacts=False` in `RunConfig` preserves inline binary data so the model receives the actual image/audio/video/document content
42+
1043
## [0.5.2] - 2026-03-26
1144

1245
### Changed

integrations/adk-middleware/python/examples/server/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from .api import (
2121
agentic_chat_app,
22+
agentic_chat_reasoning_app,
2223
agentic_generative_ui_app,
2324
tool_based_generative_ui_app,
2425
human_in_the_loop_app,
@@ -37,6 +38,7 @@
3738
app.include_router(shared_state_app.router, prefix='/adk-shared-state-agent', tags=['Shared State'])
3839
app.include_router(backend_tool_rendering_app.router, prefix='/backend_tool_rendering', tags=['Backend Tool Rendering'])
3940
app.include_router(predictive_state_updates_app.router, prefix='/adk-predictive-state-agent', tags=['Predictive State Updates'])
41+
app.include_router(agentic_chat_reasoning_app.router, prefix='/adk-reasoning-chat', tags=['Agentic Chat Reasoning'])
4042

4143

4244
@app.get("/")
@@ -51,6 +53,7 @@ async def root():
5153
"shared_state": "/adk-shared-state-agent",
5254
"backend_tool_rendering": "/backend_tool_rendering",
5355
"predictive_state_updates": "/adk-predictive-state-agent",
56+
"agentic_chat_reasoning": "/adk-reasoning-chat",
5457
"docs": "/docs"
5558
}
5659
}
@@ -91,6 +94,7 @@ def main():
9194
print(f" • Human in the Loop: http://localhost:{port}/adk-human-in-loop-agent")
9295
print(f" • Shared State: http://localhost:{port}/adk-shared-state-agent")
9396
print(f" • Predictive State Updates: http://localhost:{port}/adk-predictive-state-agent")
97+
print(f" • Agentic Chat Reasoning: http://localhost:{port}/adk-reasoning-chat")
9498
print(f" • API docs: http://localhost:{port}/docs")
9599
uvicorn.run(app, host="0.0.0.0", port=port)
96100

integrations/adk-middleware/python/examples/server/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
from .shared_state import app as shared_state_app
88
from .predictive_state_updates import app as predictive_state_updates_app
99
from .backend_tool_rendering import app as backend_tool_rendering_app
10+
from .agentic_chat_reasoning import app as agentic_chat_reasoning_app
1011

1112
__all__ = [
1213
"agentic_chat_app",
14+
"agentic_chat_reasoning_app",
1315
"agentic_generative_ui_app",
1416
"tool_based_generative_ui_app",
1517
"human_in_the_loop_app",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Agentic Chat with Reasoning (Thinking) feature.
2+
3+
Demonstrates REASONING_* events emitted when Gemini's include_thoughts
4+
is enabled, including encrypted thought signatures.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from fastapi import FastAPI
10+
from ag_ui_adk import ADKAgent, AGUIToolset, add_adk_fastapi_endpoint
11+
from google.adk.agents import LlmAgent
12+
from google.adk.planners import BuiltInPlanner
13+
from google.genai import types
14+
15+
# Create a reasoning-enabled ADK agent using Gemini 2.5 Flash
16+
reasoning_agent = LlmAgent(
17+
name="reasoning_assistant",
18+
model="gemini-2.5-flash",
19+
instruction="""You are a helpful assistant that thinks carefully before responding.
20+
Work through problems step by step in your reasoning.
21+
""",
22+
planner=BuiltInPlanner(
23+
thinking_config=types.ThinkingConfig(
24+
include_thoughts=True
25+
)
26+
),
27+
tools=[
28+
AGUIToolset(),
29+
],
30+
)
31+
32+
# Create ADK middleware agent instance
33+
chat_agent = ADKAgent(
34+
adk_agent=reasoning_agent,
35+
app_name="demo_app",
36+
user_id="demo_user",
37+
session_timeout_seconds=3600,
38+
use_in_memory_services=True,
39+
)
40+
41+
# Create FastAPI app
42+
app = FastAPI(title="ADK Middleware Reasoning Chat")
43+
44+
# Add the ADK endpoint
45+
add_adk_fastapi_endpoint(app, chat_agent, path="/")
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/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ authors = [
88
]
99
requires-python = ">=3.10, <3.15"
1010
dependencies = [
11-
"ag-ui-protocol>=0.1.11",
11+
"ag-ui-protocol>=0.1.15",
1212
"aiohttp>=3.12.0",
1313
"asyncio>=3.4.3",
1414
"fastapi>=0.115.2",

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)