fix: chat functionality and extended thinking handling
Deploy Research Workbench / deploy (push) Failing after 14m1s
Deploy Research Workbench / deploy (push) Failing after 14m1s
- bundle/bundle.md: Add provider-anthropic with source URL and orchestrator config override to disable extended_thinking (foundation enables it by default, causes empty text responses for simple queries) - Dockerfile: Add amplifier-module-provider-anthropic pip install for container - backend/agui_adapter.py: Fix thinking block handling and empty text block rendering - Thinking blocks no longer fall through to text handler - Text blocks only emit START when actual content arrives - Handle amplifierd wire format (block_index/block_type, complete text in content_block:end) - frontend/src/hooks/useChat.ts: Reset assistantId on TEXT_MESSAGE_END to prevent stale message IDs across turns - tests/test_agui_adapter.py: Add extended thinking test coverage Chat functionality now works end-to-end. Verified: login -> create session -> send message -> receive AI response visible in chat UI. Generated with Amplifier
This commit is contained in:
+80
-20
@@ -40,13 +40,19 @@ class AmplifierdEventTranslator:
|
||||
"""Translate a single amplifierd SSE event into zero or more AG-UI events.
|
||||
|
||||
Args:
|
||||
event: Dict with 'event_type' and 'data' keys.
|
||||
event: Dict with 'event_type' and 'data' keys. The 'data' value is
|
||||
the raw SSE payload from amplifierd, which wraps the event-specific
|
||||
fields under a nested ``"data"`` key
|
||||
(e.g. ``{"event": "...", "data": {...}, "session_id": ..., ...}``).
|
||||
|
||||
Returns:
|
||||
List of ag_ui.core event objects (may be empty for unknown events).
|
||||
"""
|
||||
event_type = event.get("event_type", "")
|
||||
data = event.get("data", {})
|
||||
outer = event.get("data", {})
|
||||
# amplifierd wraps event-specific fields in a nested "data" key.
|
||||
# Fall back to the outer dict for tests that pass data directly.
|
||||
data = outer.get("data", outer)
|
||||
|
||||
if event_type == "content_block:start":
|
||||
return self._handle_block_start(data)
|
||||
@@ -62,9 +68,13 @@ class AmplifierdEventTranslator:
|
||||
return []
|
||||
|
||||
def _handle_block_start(self, data: dict[str, Any]) -> list[Any]:
|
||||
"""Handle content_block:start — emit TEXT_MESSAGE_START or TOOL_CALL_START."""
|
||||
index = data.get("index", 0)
|
||||
block_type = data.get("type", "text")
|
||||
"""Handle content_block:start — register block; defer TEXT_MESSAGE_START to first delta.
|
||||
|
||||
Supports both amplifierd wire format (``block_index`` / ``block_type``) and the
|
||||
legacy test format (``index`` / ``type``) via fallbacks.
|
||||
"""
|
||||
index = data.get("block_index", data.get("index", 0))
|
||||
block_type = data.get("block_type", data.get("type", "text"))
|
||||
|
||||
if block_type == "tool_use":
|
||||
tool_id = data.get("id", "")
|
||||
@@ -81,21 +91,27 @@ class AmplifierdEventTranslator:
|
||||
parent_message_id=self._current_message_id,
|
||||
)
|
||||
]
|
||||
else:
|
||||
# Text block
|
||||
elif block_type == "text":
|
||||
# Lazy start: don't emit TEXT_MESSAGE_START until we know there's actual content.
|
||||
message_id = str(uuid.uuid4())
|
||||
self._blocks[index] = {"type": "text", "message_id": message_id}
|
||||
self._blocks[index] = {"type": "text", "message_id": message_id, "started": False}
|
||||
self._current_message_id = message_id
|
||||
return [
|
||||
TextMessageStartEvent(
|
||||
message_id=message_id,
|
||||
role="assistant",
|
||||
)
|
||||
]
|
||||
return []
|
||||
else:
|
||||
# thinking, image, or any other block type — skip entirely
|
||||
self._blocks[index] = {"type": "skip"}
|
||||
return []
|
||||
|
||||
def _handle_block_delta(self, data: dict[str, Any]) -> list[Any]:
|
||||
"""Handle content_block:delta — emit TEXT_MESSAGE_CONTENT or TOOL_CALL_ARGS."""
|
||||
index = data.get("index", 0)
|
||||
"""Handle content_block:delta — emit TEXT_MESSAGE_CONTENT or TOOL_CALL_ARGS.
|
||||
|
||||
For text blocks, TEXT_MESSAGE_START is emitted lazily here on the first
|
||||
non-empty token so that thinking/empty text blocks produce zero AG-UI events.
|
||||
|
||||
Supports both amplifierd wire format (``block_index``) and legacy test format
|
||||
(``index``) via fallback.
|
||||
"""
|
||||
index = data.get("block_index", data.get("index", 0))
|
||||
delta = data.get("delta", {})
|
||||
delta_type = delta.get("type", "")
|
||||
|
||||
@@ -112,6 +128,9 @@ class AmplifierdEventTranslator:
|
||||
]
|
||||
return []
|
||||
|
||||
if block_info["type"] == "skip":
|
||||
return []
|
||||
|
||||
if block_info["type"] == "tool_use":
|
||||
return [
|
||||
ToolCallArgsEvent(
|
||||
@@ -120,10 +139,22 @@ class AmplifierdEventTranslator:
|
||||
)
|
||||
]
|
||||
else:
|
||||
# Text block
|
||||
# Text block — lazy start: emit TEXT_MESSAGE_START on the first real token
|
||||
token = delta.get("text", "")
|
||||
if not token:
|
||||
return []
|
||||
if not block_info.get("started"):
|
||||
block_info["started"] = True
|
||||
return [
|
||||
TextMessageStartEvent(
|
||||
message_id=block_info["message_id"],
|
||||
role="assistant",
|
||||
),
|
||||
TextMessageContentEvent(
|
||||
message_id=block_info["message_id"],
|
||||
delta=token,
|
||||
),
|
||||
]
|
||||
return [
|
||||
TextMessageContentEvent(
|
||||
message_id=block_info["message_id"],
|
||||
@@ -132,17 +163,46 @@ class AmplifierdEventTranslator:
|
||||
]
|
||||
|
||||
def _handle_block_end(self, data: dict[str, Any]) -> list[Any]:
|
||||
"""Handle content_block:end — emit TEXT_MESSAGE_END or TOOL_CALL_END."""
|
||||
index = data.get("index", 0)
|
||||
"""Handle content_block:end — emit TEXT_MESSAGE_END or TOOL_CALL_END.
|
||||
|
||||
Supports two modes:
|
||||
- **Streaming mode** (delta events were received): the block was already opened
|
||||
via ``_handle_block_delta``; just close it with TEXT_MESSAGE_END.
|
||||
- **Batch mode** (no delta events, full text in ``block.text``): emit
|
||||
TEXT_MESSAGE_START + TEXT_MESSAGE_CONTENT + TEXT_MESSAGE_END in one shot.
|
||||
This is the format used by amplifierd, which sends complete blocks at end time.
|
||||
|
||||
Text blocks with no content in either mode are silently dropped.
|
||||
|
||||
Supports both amplifierd wire format (``block_index``) and legacy test format
|
||||
(``index``) via fallback.
|
||||
"""
|
||||
index = data.get("block_index", data.get("index", 0))
|
||||
block_info = self._blocks.pop(index, None)
|
||||
|
||||
if block_info is None:
|
||||
return []
|
||||
|
||||
if block_info["type"] == "skip":
|
||||
return []
|
||||
|
||||
if block_info["type"] == "tool_use":
|
||||
return [ToolCallEndEvent(tool_call_id=block_info["tool_id"])]
|
||||
else:
|
||||
return [TextMessageEndEvent(message_id=block_info["message_id"])]
|
||||
message_id = block_info["message_id"]
|
||||
if block_info.get("started"):
|
||||
# Streaming mode: deltas already opened the message — just close it.
|
||||
return [TextMessageEndEvent(message_id=message_id)]
|
||||
# Batch mode: full text is in the "block" field (amplifierd wire format).
|
||||
block = data.get("block", {})
|
||||
text = block.get("text", "")
|
||||
if text:
|
||||
return [
|
||||
TextMessageStartEvent(message_id=message_id, role="assistant"),
|
||||
TextMessageContentEvent(message_id=message_id, delta=text),
|
||||
TextMessageEndEvent(message_id=message_id),
|
||||
]
|
||||
return []
|
||||
|
||||
def _handle_tool_result(self, data: dict[str, Any]) -> list[Any]:
|
||||
"""Handle tool:result — emit TOOL_CALL_RESULT with optional MCP App metadata."""
|
||||
|
||||
Reference in New Issue
Block a user