refactor: 重构 Tool Result 契约,移除 ui_hints 统一使用 result 字段

- ToolAgentOutput 移除 result_summary 和 ui_hints,统一使用 result 字段
- 日历/用户查找工具移除 ui_hints 输出,改为机器可读的结构化结果
- Agent History 移除 tool 消息的 ui_hints 处理逻辑
- App 版本检查改为 manifest.json 方式,支持多渠道发布
- 更新 settings 配置和测试用例适配新结构
This commit is contained in:
qzl
2026-03-17 12:18:09 +08:00
parent c26cdbbc27
commit aa30fe0ce6
44 changed files with 984 additions and 655 deletions
@@ -38,8 +38,7 @@ def test_tool_result_wire_event_with_bare_fields() -> None:
"tool_call_id": "call-1",
"tool_call_args": {"start_date": "2024-01-01"},
"status": "success",
"result_summary": "summary",
"ui_schema": {"version": "2.0"},
"result": "summary",
},
}
@@ -50,8 +49,7 @@ def test_tool_result_wire_event_with_bare_fields() -> None:
assert result["tool_name"] == "calendar_write"
assert result["tool_call_id"] == "call-1"
assert result["status"] == "success"
assert result["result_summary"] == "summary"
assert result["ui_schema"] == {"version": "2.0"}
assert result["result"] == "summary"
def test_text_end_event_with_bare_fields() -> None:
@@ -124,7 +122,7 @@ def test_text_message_end_agui_event_strips_internal_usage_fields() -> None:
assert "model" not in result
def test_tool_call_result_agui_event_compiles_ui_hints_to_ui_schema() -> None:
def test_tool_call_result_agui_event_strips_tool_ui_fields() -> None:
event = {
"type": "TOOL_CALL_RESULT",
"threadId": "thread-1",
@@ -136,19 +134,20 @@ def test_tool_call_result_agui_event_compiles_ui_hints_to_ui_schema() -> None:
"tool_call_id": "call-1",
"tool_call_args": {"page": 1},
"status": "success",
"result_summary": "ok",
"result": "ok",
"ui_hints": {
"intent": "status",
"status": "success",
"title": "Done",
},
"ui_schema": {"version": "2.0"},
}
result = to_agui_wire_event(event)
assert result["type"] == "TOOL_CALL_RESULT"
assert "ui_hints" not in result
assert isinstance(result.get("ui_schema"), dict)
assert "ui_schema" not in result
def test_text_message_end_agui_event_compiles_ui_hints_to_ui_schema() -> None:
@@ -125,19 +125,19 @@ async def test_store_persists_tool_output_with_summary_as_content(
"tool_call_id": "call-1",
"tool_call_args": {"title": "A"},
"status": "success",
"result_summary": "已创建日程 A",
"ui_hints": {
"intent": "status",
"status": "success",
"sections": [],
},
"result": "status=success batch=1 success=1 failed=0 ids=[event-1]",
}
)
append_kwargs = cast(dict[str, Any], captured["append_kwargs"])
assert getattr(append_kwargs["role"], "value", None) == "tool"
assert append_kwargs["content"] == "已创建日程 A"
assert (
append_kwargs["content"]
== "status=success batch=1 success=1 failed=0 ids=[event-1]"
)
metadata = cast(dict[str, Any], append_kwargs["metadata"])
assert sorted(metadata.keys()) == ["run_id", "tool_agent_output"]
assert metadata["tool_agent_output"]["result_summary"] == "已创建日程 A"
assert metadata["tool_agent_output"]["ui_hints"]["intent"] == "status"
assert (
metadata["tool_agent_output"]["result"]
== "status=success batch=1 success=1 failed=0 ids=[event-1]"
)
@@ -1,73 +0,0 @@
from __future__ import annotations
from core.agentscope.events.tool_result_summary import build_tool_content_summary
def test_summary_prioritizes_error() -> None:
text = build_tool_content_summary(
tool_name="calendar_write",
args={"title": "A"},
result={"message": "ignored"},
error={"message": "denied"},
)
assert text == "calendar_write 执行失败:denied"
def test_summary_for_calendar_write() -> None:
text = build_tool_content_summary(
tool_name="calendar_write",
args={"title": "项目评审"},
result={"startAt": "明天 10:00"},
error=None,
)
assert text == "已创建日程:项目评审(明天 10:00)"
def test_summary_for_calendar_read() -> None:
text = build_tool_content_summary(
tool_name="calendar_read",
args={"query": "今天"},
result={"data": {"total": 3}},
error=None,
)
assert text == "查询到 3 条日程(今天)"
def test_summary_falls_back_to_result_content() -> None:
text = build_tool_content_summary(
tool_name="unknown_tool",
args=None,
result={"content": "这是非常长的说明" * 20},
error=None,
)
assert text.startswith("这是非常长的说明")
assert len(text) <= 80
def test_summary_default_done() -> None:
text = build_tool_content_summary(
tool_name="unknown_tool",
args=None,
result=None,
error=None,
)
assert text == "unknown_tool 执行完成"
def test_summary_marks_business_failure_when_ok_false() -> None:
text = build_tool_content_summary(
tool_name="calendar_write",
args={"title": "上学"},
result={
"type": "calendar_operation.v1",
"data": {
"ok": False,
"code": "UNAUTHORIZED",
"message": "calendar.write requires validated user token",
},
},
error=None,
)
assert (
text == "calendar_write 执行失败:calendar.write requires validated user token"
)
@@ -1,11 +1,11 @@
from __future__ import annotations
import json
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Any
from uuid import uuid4
from uuid import UUID, uuid4
import pytest
from agentscope.tool import ToolResponse
@@ -25,11 +25,30 @@ def _decode_tool_response(response: ToolResponse) -> dict[str, Any]:
@dataclass
class _FakeService:
created_request: Any = None
created_id: str = field(default_factory=lambda: str(uuid4()))
list_calls: list[dict[str, Any]] = field(default_factory=list)
async def list_paginated(
self, *, page: int, page_size: int, query: str | None = None
):
self.list_calls.append({"page": page, "page_size": page_size, "query": query})
item = SimpleNamespace(
id=UUID(self.created_id),
title="会议",
description="今天下午五点的会议",
start_at=datetime(2026, 3, 17, 9, 0, tzinfo=timezone.utc),
end_at=datetime(2026, 3, 17, 9, 30, tzinfo=timezone.utc),
timezone="Asia/Shanghai",
metadata=SimpleNamespace(
location=None, color="#4F46E5", reminder_minutes=15
),
)
return [item], 1
async def create_agent_generated(self, request):
self.created_request = request
return SimpleNamespace(
id=uuid4(),
id=UUID(self.created_id),
title=request.title,
description=request.description,
start_at=request.start_at,
@@ -136,6 +155,8 @@ async def test_calendar_write_create_normalizes_to_utc(
payload = _decode_tool_response(result)
assert payload["status"] == "success"
assert payload["result"].startswith("status=success")
assert fake_service.created_id in payload["result"]
assert fake_service.created_request is not None
request = fake_service.created_request
assert request.timezone == "Asia/Shanghai"
@@ -164,3 +185,28 @@ async def test_calendar_write_rejects_misaligned_batch_lists(
assert payload["status"] == "failure"
assert payload["error"]["code"] == "INVALID_ARGUMENT"
assert "长度必须与 operations 一致" in payload["error"]["message"]
@pytest.mark.asyncio
async def test_calendar_read_returns_structured_result_with_ids(
monkeypatch: pytest.MonkeyPatch,
) -> None:
fake_service = _FakeService()
monkeypatch.setattr(
calendar_module, "create_schedule_service", lambda *_: fake_service
)
result = await calendar_module.calendar_read(
query="会议",
page=1,
page_size=20,
session=SimpleNamespace(),
owner_id=uuid4(),
)
payload = _decode_tool_response(result)
assert payload["status"] == "success"
assert payload["result"].startswith("status=success")
assert "query=会议" in payload["result"]
assert fake_service.created_id in payload["result"]
assert fake_service.list_calls == [{"page": 1, "page_size": 20, "query": "会议"}]