fix: chat functionality and extended thinking handling
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:
Ken
2026-05-27 01:42:52 +00:00
parent a21329bb04
commit 11b3bc2bbf
5 changed files with 219 additions and 39 deletions
+80 -20
View File
@@ -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."""