c3bf08ac8d
K8s Workspace Integration Tests / k8s-workspace-tests (push) Waiting to run
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Python Unittest Coverage / test (macos-15, 3.11) (push) Waiting to run
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Waiting to run
Python Unittest Coverage / test (windows-latest, 3.11) (push) Waiting to run
Web UI / check (push) Waiting to run
717 lines
24 KiB
Python
717 lines
24 KiB
Python
# -*- coding: utf-8 -*-
|
|
# pylint: disable=protected-access
|
|
"""Test cases for AGUI protocol middleware."""
|
|
|
|
import json
|
|
from typing import AsyncGenerator
|
|
from unittest.async_case import IsolatedAsyncioTestCase
|
|
from unittest.mock import MagicMock
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.responses import StreamingResponse
|
|
from fastapi.testclient import TestClient
|
|
|
|
from agentscope.app.middleware import AGUIProtocolMiddleware
|
|
from agentscope.event import (
|
|
ConfirmResult,
|
|
DataBlockDeltaEvent,
|
|
DataBlockEndEvent,
|
|
DataBlockStartEvent,
|
|
ExceedMaxItersEvent,
|
|
ExternalExecutionResultEvent,
|
|
ModelCallEndEvent,
|
|
ModelCallStartEvent,
|
|
ReplyEndEvent,
|
|
ReplyStartEvent,
|
|
RequireExternalExecutionEvent,
|
|
RequireUserConfirmEvent,
|
|
TextBlockDeltaEvent,
|
|
TextBlockEndEvent,
|
|
TextBlockStartEvent,
|
|
ThinkingBlockDeltaEvent,
|
|
ThinkingBlockEndEvent,
|
|
ThinkingBlockStartEvent,
|
|
ToolCallDeltaEvent,
|
|
ToolCallEndEvent,
|
|
ToolCallStartEvent,
|
|
ToolResultDataDeltaEvent,
|
|
ToolResultEndEvent,
|
|
ToolResultStartEvent,
|
|
ToolResultTextDeltaEvent,
|
|
UserConfirmResultEvent,
|
|
ReplyEndReason,
|
|
)
|
|
from agentscope.message import ToolCallBlock, ToolResultBlock, ToolResultState
|
|
from agentscope.model import FinishedReason
|
|
|
|
|
|
async def _collect_stream(
|
|
mw: AGUIProtocolMiddleware,
|
|
chunks: list[str],
|
|
) -> str:
|
|
"""Collect converted stream chunks as text."""
|
|
|
|
async def _stream() -> AsyncGenerator[str, None]:
|
|
"""Yield the provided chunks."""
|
|
for chunk in chunks:
|
|
yield chunk
|
|
|
|
out: list[str] = []
|
|
async for item in mw._convert_stream(_stream()):
|
|
out.append(item.decode("utf-8"))
|
|
return "".join(out)
|
|
|
|
|
|
class AGUIProtocolStreamTest(IsolatedAsyncioTestCase):
|
|
"""Test stream-level conversion behavior."""
|
|
|
|
async def asyncSetUp(self) -> None:
|
|
"""The async setup method."""
|
|
self.mw = AGUIProtocolMiddleware(app=MagicMock())
|
|
|
|
async def test_raw_json_stream_is_converted(self) -> None:
|
|
"""Test raw AgentEvent JSON stream conversion."""
|
|
event = ReplyStartEvent(
|
|
session_id="sess_1",
|
|
reply_id="reply_1",
|
|
name="agent",
|
|
)
|
|
|
|
body = await _collect_stream(self.mw, [event.model_dump_json()])
|
|
data = json.loads(body)
|
|
|
|
self.assertEqual(data["type"], "RUN_STARTED")
|
|
self.assertEqual(data["threadId"], "sess_1")
|
|
self.assertEqual(data["runId"], "reply_1")
|
|
|
|
async def test_sse_data_frame_is_converted(self) -> None:
|
|
"""Test AgentEvent JSON inside an SSE data frame is converted."""
|
|
event = ReplyStartEvent(
|
|
session_id="sess_1",
|
|
reply_id="reply_1",
|
|
name="agent",
|
|
)
|
|
|
|
body = await _collect_stream(
|
|
self.mw,
|
|
[f"data: {event.model_dump_json()}\n\n"],
|
|
)
|
|
self.assertTrue(body.startswith("data: "))
|
|
|
|
data = json.loads(body.removeprefix("data: ").strip())
|
|
self.assertEqual(data["type"], "RUN_STARTED")
|
|
self.assertEqual(data["threadId"], "sess_1")
|
|
self.assertEqual(data["runId"], "reply_1")
|
|
self.assertNotIn("session_id", data)
|
|
|
|
async def test_sse_heartbeat_is_passed_through(self) -> None:
|
|
"""Test SSE heartbeat frames are not modified."""
|
|
self.assertEqual(
|
|
await _collect_stream(self.mw, [":\n\n"]),
|
|
":\n\n",
|
|
)
|
|
|
|
async def test_sse_data_frame_with_crlf_is_converted(self) -> None:
|
|
"""Test AgentEvent JSON inside a CRLF SSE frame is converted."""
|
|
event = ReplyStartEvent(
|
|
session_id="sess_1",
|
|
reply_id="reply_1",
|
|
name="agent",
|
|
)
|
|
|
|
body = await _collect_stream(
|
|
self.mw,
|
|
[f"data: {event.model_dump_json()}\r\n\r\n"],
|
|
)
|
|
self.assertTrue(body.startswith("data: "))
|
|
self.assertTrue(body.endswith("\r\n\r\n"))
|
|
|
|
data = json.loads(body.removeprefix("data: ").strip())
|
|
self.assertEqual(data["type"], "RUN_STARTED")
|
|
self.assertEqual(data["threadId"], "sess_1")
|
|
self.assertEqual(data["runId"], "reply_1")
|
|
|
|
async def test_fastapi_sse_response_is_converted(self) -> None:
|
|
"""Test middleware converts a real FastAPI SSE response."""
|
|
app = FastAPI()
|
|
app.add_middleware(AGUIProtocolMiddleware)
|
|
|
|
@app.get("/sessions/sess_1/stream")
|
|
async def stream() -> StreamingResponse:
|
|
event = ReplyStartEvent(
|
|
session_id="sess_1",
|
|
reply_id="reply_1",
|
|
name="agent",
|
|
)
|
|
|
|
async def gen() -> AsyncGenerator[str, None]:
|
|
yield f"data: {event.model_dump_json()}\n\n"
|
|
|
|
return StreamingResponse(gen(), media_type="text/event-stream")
|
|
|
|
client = TestClient(app)
|
|
with client.stream("GET", "/sessions/sess_1/stream") as response:
|
|
body = "".join(response.iter_text())
|
|
|
|
self.assertTrue(body.startswith("data: "))
|
|
data = json.loads(body.removeprefix("data: ").strip())
|
|
self.assertEqual(data["type"], "RUN_STARTED")
|
|
self.assertEqual(data["threadId"], "sess_1")
|
|
self.assertEqual(data["runId"], "reply_1")
|
|
self.assertNotIn("session_id", data)
|
|
|
|
async def test_fastapi_json_response_is_not_converted(self) -> None:
|
|
"""Test non-SSE responses are outside protocol conversion scope."""
|
|
app = FastAPI()
|
|
app.add_middleware(AGUIProtocolMiddleware)
|
|
|
|
@app.get("/event")
|
|
def event() -> JSONResponse:
|
|
agent_event = ReplyStartEvent(
|
|
session_id="sess_1",
|
|
reply_id="reply_1",
|
|
name="agent",
|
|
)
|
|
return JSONResponse(agent_event.model_dump(mode="json"))
|
|
|
|
client = TestClient(app)
|
|
response = client.get("/event")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
data = response.json()
|
|
self.assertEqual(data["type"], "REPLY_START")
|
|
self.assertEqual(data["session_id"], "sess_1")
|
|
self.assertNotIn("RUN_STARTED", response.text)
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
"""The async teardown method."""
|
|
|
|
|
|
class AGUIProtocolLifecycleTest(IsolatedAsyncioTestCase):
|
|
"""Test lifecycle event conversions."""
|
|
|
|
async def asyncSetUp(self) -> None:
|
|
"""The async setup method."""
|
|
self.mw = AGUIProtocolMiddleware(app=MagicMock())
|
|
|
|
async def test_reply_start_to_run_started(self) -> None:
|
|
"""Test ReplyStartEvent -> RUN_STARTED."""
|
|
event = ReplyStartEvent(
|
|
session_id="sess_1",
|
|
reply_id="reply_1",
|
|
name="agent",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "RUN_STARTED")
|
|
self.assertEqual(result["threadId"], "sess_1")
|
|
self.assertEqual(result["runId"], "reply_1")
|
|
self.assertNotIn("name", result)
|
|
|
|
async def test_reply_end_to_run_finished(self) -> None:
|
|
"""Test ReplyEndEvent -> RUN_FINISHED."""
|
|
event = ReplyEndEvent(
|
|
session_id="sess_1",
|
|
reply_id="reply_1",
|
|
finished_reason=ReplyEndReason.COMPLETED,
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "RUN_FINISHED")
|
|
self.assertEqual(result["threadId"], "sess_1")
|
|
self.assertEqual(result["runId"], "reply_1")
|
|
|
|
async def test_exceed_max_iters_to_run_error(self) -> None:
|
|
"""Test ExceedMaxItersEvent -> RUN_ERROR."""
|
|
event = ExceedMaxItersEvent(
|
|
reply_id="reply_1",
|
|
name="my_agent",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "RUN_ERROR")
|
|
self.assertIn("my_agent", result["message"])
|
|
self.assertEqual(result["code"], "exceed_max_iters")
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
"""The async teardown method."""
|
|
|
|
|
|
class AGUIProtocolStepTest(IsolatedAsyncioTestCase):
|
|
"""Test model call -> step event conversions."""
|
|
|
|
async def asyncSetUp(self) -> None:
|
|
"""The async setup method."""
|
|
self.mw = AGUIProtocolMiddleware(app=MagicMock())
|
|
|
|
async def test_model_call_start_to_step_started(self) -> None:
|
|
"""Test ModelCallStartEvent -> STEP_STARTED."""
|
|
event = ModelCallStartEvent(
|
|
reply_id="reply_1",
|
|
model_name="gpt-4",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "STEP_STARTED")
|
|
self.assertEqual(result["stepName"], "gpt-4")
|
|
|
|
async def test_model_call_end_to_step_finished(self) -> None:
|
|
"""Test ModelCallEndEvent -> STEP_FINISHED with matching step_name."""
|
|
start_event = ModelCallStartEvent(
|
|
reply_id="reply_1",
|
|
model_name="gpt-4",
|
|
)
|
|
self.mw._convert_to_protocol(start_event)
|
|
|
|
end_event = ModelCallEndEvent(
|
|
reply_id="reply_1",
|
|
input_tokens=100,
|
|
output_tokens=50,
|
|
finished_reason=FinishedReason.COMPLETED,
|
|
)
|
|
result = self.mw._convert_to_protocol(end_event)
|
|
|
|
self.assertEqual(result["type"], "STEP_FINISHED")
|
|
self.assertEqual(result["stepName"], "gpt-4")
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
"""The async teardown method."""
|
|
|
|
|
|
class AGUIProtocolTextMessageTest(IsolatedAsyncioTestCase):
|
|
"""Test text block -> text message event conversions."""
|
|
|
|
async def asyncSetUp(self) -> None:
|
|
"""The async setup method."""
|
|
self.mw = AGUIProtocolMiddleware(app=MagicMock())
|
|
|
|
async def test_text_block_start(self) -> None:
|
|
"""Test TextBlockStartEvent -> TEXT_MESSAGE_START."""
|
|
event = TextBlockStartEvent(
|
|
reply_id="reply_1",
|
|
block_id="block_1",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "TEXT_MESSAGE_START")
|
|
self.assertEqual(result["messageId"], "block_1")
|
|
|
|
async def test_text_block_delta(self) -> None:
|
|
"""Test TextBlockDeltaEvent -> TEXT_MESSAGE_CONTENT."""
|
|
event = TextBlockDeltaEvent(
|
|
reply_id="reply_1",
|
|
block_id="block_1",
|
|
delta="Hello, ",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "TEXT_MESSAGE_CONTENT")
|
|
self.assertEqual(result["messageId"], "block_1")
|
|
self.assertEqual(result["delta"], "Hello, ")
|
|
|
|
async def test_text_block_end(self) -> None:
|
|
"""Test TextBlockEndEvent -> TEXT_MESSAGE_END."""
|
|
event = TextBlockEndEvent(
|
|
reply_id="reply_1",
|
|
block_id="block_1",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "TEXT_MESSAGE_END")
|
|
self.assertEqual(result["messageId"], "block_1")
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
"""The async teardown method."""
|
|
|
|
|
|
class AGUIProtocolReasoningTest(IsolatedAsyncioTestCase):
|
|
"""Test thinking block -> reasoning message event conversions."""
|
|
|
|
async def asyncSetUp(self) -> None:
|
|
"""The async setup method."""
|
|
self.mw = AGUIProtocolMiddleware(app=MagicMock())
|
|
|
|
async def test_thinking_block_start(self) -> None:
|
|
"""Test ThinkingBlockStartEvent -> REASONING_MESSAGE_START."""
|
|
event = ThinkingBlockStartEvent(
|
|
reply_id="reply_1",
|
|
block_id="think_1",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "REASONING_MESSAGE_START")
|
|
self.assertEqual(result["messageId"], "think_1")
|
|
self.assertEqual(result["role"], "reasoning")
|
|
|
|
async def test_thinking_block_delta(self) -> None:
|
|
"""Test ThinkingBlockDeltaEvent -> REASONING_MESSAGE_CONTENT."""
|
|
event = ThinkingBlockDeltaEvent(
|
|
reply_id="reply_1",
|
|
block_id="think_1",
|
|
delta="Let me think...",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "REASONING_MESSAGE_CONTENT")
|
|
self.assertEqual(result["messageId"], "think_1")
|
|
self.assertEqual(result["delta"], "Let me think...")
|
|
|
|
async def test_thinking_block_end(self) -> None:
|
|
"""Test ThinkingBlockEndEvent -> REASONING_MESSAGE_END."""
|
|
event = ThinkingBlockEndEvent(
|
|
reply_id="reply_1",
|
|
block_id="think_1",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "REASONING_MESSAGE_END")
|
|
self.assertEqual(result["messageId"], "think_1")
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
"""The async teardown method."""
|
|
|
|
|
|
class AGUIProtocolToolCallTest(IsolatedAsyncioTestCase):
|
|
"""Test tool call event conversions."""
|
|
|
|
async def asyncSetUp(self) -> None:
|
|
"""The async setup method."""
|
|
self.mw = AGUIProtocolMiddleware(app=MagicMock())
|
|
|
|
async def test_tool_call_start(self) -> None:
|
|
"""Test ToolCallStartEvent -> TOOL_CALL_START."""
|
|
event = ToolCallStartEvent(
|
|
reply_id="reply_1",
|
|
tool_call_id="tc_1",
|
|
tool_call_name="search",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "TOOL_CALL_START")
|
|
self.assertEqual(result["toolCallId"], "tc_1")
|
|
self.assertEqual(result["toolCallName"], "search")
|
|
self.assertEqual(result["parentMessageId"], "reply_1")
|
|
|
|
async def test_tool_call_delta(self) -> None:
|
|
"""Test ToolCallDeltaEvent -> TOOL_CALL_ARGS."""
|
|
event = ToolCallDeltaEvent(
|
|
reply_id="reply_1",
|
|
tool_call_id="tc_1",
|
|
delta='{"query": "hello"}',
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "TOOL_CALL_ARGS")
|
|
self.assertEqual(result["toolCallId"], "tc_1")
|
|
self.assertEqual(result["delta"], '{"query": "hello"}')
|
|
|
|
async def test_tool_call_end(self) -> None:
|
|
"""Test ToolCallEndEvent -> TOOL_CALL_END."""
|
|
event = ToolCallEndEvent(
|
|
reply_id="reply_1",
|
|
tool_call_id="tc_1",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "TOOL_CALL_END")
|
|
self.assertEqual(result["toolCallId"], "tc_1")
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
"""The async teardown method."""
|
|
|
|
|
|
class AGUIProtocolToolResultTest(IsolatedAsyncioTestCase):
|
|
"""Test tool result event conversions."""
|
|
|
|
async def asyncSetUp(self) -> None:
|
|
"""The async setup method."""
|
|
self.mw = AGUIProtocolMiddleware(app=MagicMock())
|
|
|
|
async def test_tool_result_end_with_buffered_content(self) -> None:
|
|
"""Test that ToolResultEndEvent carries accumulated text content."""
|
|
self.mw._convert_to_protocol(
|
|
ToolResultTextDeltaEvent(
|
|
reply_id="reply_1",
|
|
tool_call_id="tc_1",
|
|
delta="partial ",
|
|
),
|
|
)
|
|
self.mw._convert_to_protocol(
|
|
ToolResultTextDeltaEvent(
|
|
reply_id="reply_1",
|
|
tool_call_id="tc_1",
|
|
delta="result",
|
|
),
|
|
)
|
|
|
|
result = self.mw._convert_to_protocol(
|
|
ToolResultEndEvent(
|
|
reply_id="reply_1",
|
|
tool_call_id="tc_1",
|
|
state=ToolResultState.SUCCESS,
|
|
),
|
|
)
|
|
|
|
self.assertEqual(result["type"], "TOOL_CALL_RESULT")
|
|
self.assertEqual(result["toolCallId"], "tc_1")
|
|
self.assertEqual(result["messageId"], "reply_1")
|
|
self.assertEqual(result["content"], "partial result")
|
|
|
|
async def test_tool_result_end_fallback_to_state(self) -> None:
|
|
"""Test that ToolResultEndEvent falls back to state when no buffer."""
|
|
result = self.mw._convert_to_protocol(
|
|
ToolResultEndEvent(
|
|
reply_id="reply_1",
|
|
tool_call_id="tc_1",
|
|
state=ToolResultState.ERROR,
|
|
),
|
|
)
|
|
|
|
self.assertEqual(result["type"], "TOOL_CALL_RESULT")
|
|
self.assertEqual(result["content"], "error")
|
|
|
|
async def test_tool_result_start_to_custom(self) -> None:
|
|
"""Test ToolResultStartEvent -> CUSTOM."""
|
|
event = ToolResultStartEvent(
|
|
reply_id="reply_1",
|
|
tool_call_id="tc_1",
|
|
tool_call_name="search",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "CUSTOM")
|
|
self.assertEqual(result["name"], "tool_result_start")
|
|
self.assertIsInstance(result["value"], dict)
|
|
|
|
async def test_tool_result_text_delta_to_custom(self) -> None:
|
|
"""Test ToolResultTextDeltaEvent -> CUSTOM."""
|
|
event = ToolResultTextDeltaEvent(
|
|
reply_id="reply_1",
|
|
tool_call_id="tc_1",
|
|
delta="partial result",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "CUSTOM")
|
|
self.assertEqual(result["name"], "tool_result_text_delta")
|
|
self.assertEqual(result["value"]["delta"], "partial result")
|
|
|
|
async def test_tool_result_data_delta_to_custom(self) -> None:
|
|
"""Test ToolResultDataDeltaEvent -> CUSTOM."""
|
|
event = ToolResultDataDeltaEvent(
|
|
reply_id="reply_1",
|
|
tool_call_id="tc_1",
|
|
media_type="image/png",
|
|
data="base64data",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "CUSTOM")
|
|
self.assertEqual(result["name"], "tool_result_data_delta")
|
|
self.assertEqual(result["value"]["media_type"], "image/png")
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
"""The async teardown method."""
|
|
|
|
|
|
class AGUIProtocolDataBlockTest(IsolatedAsyncioTestCase):
|
|
"""Test data block -> custom event conversions."""
|
|
|
|
async def asyncSetUp(self) -> None:
|
|
"""The async setup method."""
|
|
self.mw = AGUIProtocolMiddleware(app=MagicMock())
|
|
|
|
async def test_data_block_start(self) -> None:
|
|
"""Test DataBlockStartEvent -> CUSTOM."""
|
|
event = DataBlockStartEvent(
|
|
reply_id="reply_1",
|
|
block_id="db_1",
|
|
media_type="image/png",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "CUSTOM")
|
|
self.assertEqual(result["name"], "data_block_start")
|
|
self.assertEqual(result["value"]["media_type"], "image/png")
|
|
|
|
async def test_data_block_delta(self) -> None:
|
|
"""Test DataBlockDeltaEvent -> CUSTOM."""
|
|
event = DataBlockDeltaEvent(
|
|
reply_id="reply_1",
|
|
block_id="db_1",
|
|
data="base64chunk",
|
|
media_type="image/png",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "CUSTOM")
|
|
self.assertEqual(result["name"], "data_block_delta")
|
|
self.assertEqual(result["value"]["data"], "base64chunk")
|
|
|
|
async def test_data_block_end(self) -> None:
|
|
"""Test DataBlockEndEvent -> CUSTOM."""
|
|
event = DataBlockEndEvent(
|
|
reply_id="reply_1",
|
|
block_id="db_1",
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "CUSTOM")
|
|
self.assertEqual(result["name"], "data_block_end")
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
"""The async teardown method."""
|
|
|
|
|
|
class AGUIProtocolPermissionTest(IsolatedAsyncioTestCase):
|
|
"""Test permission-related event conversions."""
|
|
|
|
async def asyncSetUp(self) -> None:
|
|
"""The async setup method."""
|
|
self.mw = AGUIProtocolMiddleware(app=MagicMock())
|
|
|
|
async def test_require_user_confirm(self) -> None:
|
|
"""Test RequireUserConfirmEvent -> CUSTOM."""
|
|
event = RequireUserConfirmEvent(
|
|
reply_id="reply_1",
|
|
tool_calls=[
|
|
ToolCallBlock(
|
|
id="tc_1",
|
|
name="bash",
|
|
input='{"command": "ls"}',
|
|
),
|
|
],
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "CUSTOM")
|
|
self.assertEqual(result["name"], "require_user_confirm")
|
|
self.assertIn("tool_calls", result["value"])
|
|
|
|
async def test_require_external_execution(self) -> None:
|
|
"""Test RequireExternalExecutionEvent -> CUSTOM."""
|
|
event = RequireExternalExecutionEvent(
|
|
reply_id="reply_1",
|
|
tool_calls=[
|
|
ToolCallBlock(
|
|
id="tc_2",
|
|
name="external_api",
|
|
input="{}",
|
|
),
|
|
],
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "CUSTOM")
|
|
self.assertEqual(result["name"], "require_external_execution")
|
|
|
|
async def test_user_confirm_result(self) -> None:
|
|
"""Test UserConfirmResultEvent -> CUSTOM."""
|
|
event = UserConfirmResultEvent(
|
|
reply_id="reply_1",
|
|
confirm_results=[
|
|
ConfirmResult(
|
|
confirmed=True,
|
|
tool_call=ToolCallBlock(
|
|
id="tc_1",
|
|
name="bash",
|
|
input='{"command": "ls"}',
|
|
),
|
|
),
|
|
],
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "CUSTOM")
|
|
self.assertEqual(result["name"], "user_confirm_result")
|
|
|
|
async def test_external_execution_result(self) -> None:
|
|
"""Test ExternalExecutionResultEvent -> CUSTOM."""
|
|
event = ExternalExecutionResultEvent(
|
|
reply_id="reply_1",
|
|
execution_results=[
|
|
ToolResultBlock(
|
|
id="tc_2",
|
|
name="external_api",
|
|
output="result data",
|
|
),
|
|
],
|
|
)
|
|
result = self.mw._convert_to_protocol(event)
|
|
|
|
self.assertEqual(result["type"], "CUSTOM")
|
|
self.assertEqual(result["name"], "external_execution_result")
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
"""The async teardown method."""
|
|
|
|
|
|
class AGUIProtocolCamelCaseTest(IsolatedAsyncioTestCase):
|
|
"""Verify that all output dicts use camelCase keys as AGUI requires."""
|
|
|
|
async def asyncSetUp(self) -> None:
|
|
"""The async setup method."""
|
|
self.mw = AGUIProtocolMiddleware(app=MagicMock())
|
|
|
|
def _assert_no_snake_case_keys(self, d: dict, context: str) -> None:
|
|
"""Assert none of the top-level keys contain underscores."""
|
|
for key in d:
|
|
if key == "value":
|
|
continue
|
|
self.assertNotIn(
|
|
"_",
|
|
key,
|
|
f"Key '{key}' in {context} should be camelCase",
|
|
)
|
|
|
|
async def test_all_standard_events_produce_camel_case(self) -> None:
|
|
"""Test that all directly-mapped events produce camelCase keys."""
|
|
events = [
|
|
ReplyStartEvent(
|
|
session_id="s",
|
|
reply_id="r",
|
|
name="a",
|
|
),
|
|
ReplyEndEvent(
|
|
session_id="s",
|
|
reply_id="r",
|
|
finished_reason=ReplyEndReason.COMPLETED,
|
|
),
|
|
ModelCallStartEvent(reply_id="r", model_name="m"),
|
|
ModelCallEndEvent(
|
|
reply_id="r",
|
|
input_tokens=1,
|
|
output_tokens=1,
|
|
finished_reason=FinishedReason.COMPLETED,
|
|
),
|
|
TextBlockStartEvent(reply_id="r", block_id="b"),
|
|
TextBlockDeltaEvent(reply_id="r", block_id="b", delta="x"),
|
|
TextBlockEndEvent(reply_id="r", block_id="b"),
|
|
ThinkingBlockStartEvent(reply_id="r", block_id="b"),
|
|
ThinkingBlockDeltaEvent(reply_id="r", block_id="b", delta="x"),
|
|
ThinkingBlockEndEvent(reply_id="r", block_id="b"),
|
|
ToolCallStartEvent(
|
|
reply_id="r",
|
|
tool_call_id="t",
|
|
tool_call_name="n",
|
|
),
|
|
ToolCallDeltaEvent(
|
|
reply_id="r",
|
|
tool_call_id="t",
|
|
delta="x",
|
|
),
|
|
ToolCallEndEvent(reply_id="r", tool_call_id="t"),
|
|
ExceedMaxItersEvent(reply_id="r", name="a"),
|
|
]
|
|
|
|
for event in events:
|
|
result = self.mw._convert_to_protocol(event)
|
|
self._assert_no_snake_case_keys(
|
|
result,
|
|
type(event).__name__,
|
|
)
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
"""The async teardown method."""
|