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
@@ -54,10 +54,7 @@ def _is_agui_event(event: dict[str, Any]) -> bool:
def _sanitize_agui_event(event: dict[str, Any]) -> dict[str, Any]:
payload = dict(event)
event_type = str(payload.get("type", "")).strip().upper()
if event_type in {
EventType.TEXT_MESSAGE_END.value,
EventType.TOOL_CALL_RESULT.value,
}:
if event_type == EventType.TEXT_MESSAGE_END.value:
ui_hints = payload.get("ui_hints")
if ui_hints is not None:
try:
@@ -67,7 +64,6 @@ def _sanitize_agui_event(event: dict[str, Any]) -> dict[str, Any]:
except Exception:
pass
payload.pop("ui_hints", None)
if event_type == EventType.TEXT_MESSAGE_END.value:
for key in (
"inputTokens",
"outputTokens",
@@ -76,6 +72,9 @@ def _sanitize_agui_event(event: dict[str, Any]) -> dict[str, Any]:
"model",
):
payload.pop(key, None)
if event_type == EventType.TOOL_CALL_RESULT.value:
payload.pop("ui_hints", None)
payload.pop("ui_schema", None)
return payload
@@ -130,10 +129,13 @@ def _build_text_end(event: dict[str, Any]) -> TextMessageEndEvent:
def _build_tool_result(event: dict[str, Any]) -> ToolCallResultEvent:
data = event.get("data", {})
content = data.get("result")
if not isinstance(content, str):
content = data.get("toolAgentOutput", "")
return ToolCallResultEvent(
message_id=data.get("messageId", ""),
tool_call_id=data.get("toolCallId", ""),
content=data.get("toolAgentOutput", ""),
content=content,
role="tool",
)
@@ -191,7 +193,7 @@ def to_agui_wire_event(event: dict[str, Any] | BaseEvent) -> dict[str, Any]:
tool_result_payload["threadId"] = thread_id
if isinstance(run_id, str) and run_id:
tool_result_payload["runId"] = run_id
reserved = {"type", "threadId", "runId"}
reserved = {"type", "threadId", "runId", "ui_hints", "ui_schema"}
tool_result_payload.update({k: v for k, v in data.items() if k not in reserved})
return tool_result_payload
+2 -3
View File
@@ -213,9 +213,8 @@ class SqlAlchemyEventStore:
"tool_call_id": self._event_value(event, "tool_call_id"),
"tool_call_args": self._event_value(event, "tool_call_args"),
"status": self._event_value(event, "status"),
"result_summary": self._event_value(event, "result_summary"),
"result": self._event_value(event, "result"),
"error": self._event_value(event, "error"),
"ui_hints": self._event_value(event, "ui_hints"),
}
try:
@@ -231,7 +230,7 @@ class SqlAlchemyEventStore:
)
return
content = tool_output.result_summary
content = tool_output.result
locked_session = await session_repo.lock_session_for_update(
session_id=session_id
@@ -112,13 +112,8 @@ class PipelineStageEmitter:
"tool_call_id": tool_output.tool_call_id,
"tool_call_args": tool_output.tool_call_args,
"status": tool_output.status.value,
"result_summary": tool_output.result_summary,
"result": tool_output.result,
}
ui_hints = tool_output.model_dump(mode="json", exclude_none=True).get(
"ui_hints"
)
if ui_hints is not None:
payload["ui_hints"] = ui_hints
if tool_output.error:
payload["error"] = tool_output.error.model_dump(mode="json")
@@ -16,13 +16,9 @@ from core.agentscope.tools.utils.calendar_domain import (
)
from core.agentscope.tools.utils.calendar_ui import (
calendar_error_output,
calendar_read_hints,
calendar_share_hints,
calendar_write_hints,
dump_tool_output,
)
from schemas.agent.runtime_models import ErrorInfo, ToolAgentOutput, ToolStatus
from schemas.agent.ui_hints import UiHintListItem, UiHintStatus
from v1.schedule_items.schemas import (
ScheduleItemCreateRequest,
ScheduleItemShareRequest,
@@ -73,6 +69,18 @@ def _validate_runtime_context(
return None
def _format_event_brief(event_items: list[dict[str, Any]], limit: int = 3) -> str:
briefs: list[str] = []
for item in event_items[:limit]:
event_id = str(item.get("id") or "")
title = str(item.get("title") or "")
start_at = str(item.get("startAt") or "")
if not event_id:
continue
briefs.append(f"{{id={event_id},title={title},startAt={start_at}}}")
return ",".join(briefs)
async def calendar_read(
query: Annotated[
str | None,
@@ -114,24 +122,28 @@ async def calendar_read(
service = create_schedule_service(
cast(AsyncSession, session), cast(UUID, owner_id)
)
items, total = await service.list_paginated(page=page, page_size=page_size)
total_pages = max(1, (total + page_size - 1) // page_size) if total else 0
items, total = await service.list_paginated(
page=page,
page_size=page_size,
query=query,
)
total_pages = (total + page_size - 1) // page_size if total else 0
event_items = [schedule_event_to_dict(item) for item in items]
query_value = (query or "").strip() or "*"
event_brief = _format_event_brief(event_items)
summary = (
f"status=success query={query_value} total={total} page={page}/"
f"{total_pages or 1} returned={len(event_items)}"
)
if event_brief:
summary = f"{summary} items=[{event_brief}]"
return dump_tool_output(
ToolAgentOutput(
tool_name=tool_name,
tool_call_id=f"{tool_name}-call",
tool_call_args=tool_call_args,
status=ToolStatus.SUCCESS,
result_summary=f"已获取日程列表,共 {total}",
ui_hints=calendar_read_hints(
total=total,
page=page,
page_size=page_size,
total_pages=total_pages,
events=event_items,
),
result=summary,
)
)
except Exception as exc:
@@ -297,6 +309,7 @@ async def calendar_write(
success_count = 0
failed_count = 0
success_event_ids: list[str] = []
result_items: list[dict[str, Any]] = []
for idx, operation in enumerate(operations):
@@ -353,6 +366,7 @@ async def calendar_write(
"message": f"日程「{created.title}」已创建",
}
)
success_event_ids.append(str(created.id))
continue
if operation == "update":
@@ -397,6 +411,7 @@ async def calendar_write(
"message": f"日程「{updated.title}」已更新",
}
)
success_event_ids.append(str(updated.id))
continue
if operation == "delete":
@@ -413,6 +428,7 @@ async def calendar_write(
"message": f"日程 {event_id} 已删除",
}
)
success_event_ids.append(event_id)
continue
except Exception as exc:
code, message, _ = map_calendar_exception(exc)
@@ -430,16 +446,22 @@ async def calendar_write(
if failed_count == 0:
final_status = ToolStatus.SUCCESS
ui_status = UiHintStatus.SUCCESS
summary = f"日程批量操作完成,共 {batch_size} 条,成功 {success_count} "
summary = (
f"status=success batch={batch_size} success={success_count} "
f"failed={failed_count} ids=[{','.join(success_event_ids)}]"
)
elif success_count == 0:
final_status = ToolStatus.FAILURE
ui_status = UiHintStatus.ERROR
summary = f"日程批量操作失败,共 {batch_size} 条,失败 {failed_count} "
summary = (
f"status=failure batch={batch_size} success={success_count} "
f"failed={failed_count}"
)
else:
final_status = ToolStatus.PARTIAL
ui_status = UiHintStatus.WARNING
summary = f"日程批量操作部分成功,共 {batch_size} 条,成功 {success_count} 条,失败 {failed_count}"
summary = (
f"status=partial batch={batch_size} success={success_count} "
f"failed={failed_count} ids=[{','.join(success_event_ids)}]"
)
error_info: ErrorInfo | None = None
if final_status == ToolStatus.FAILURE:
@@ -459,30 +481,10 @@ async def calendar_write(
retryable=False,
details={"results": result_items},
)
result_list_items = [
UiHintListItem(
id=(
str(item.get("eventId"))
if isinstance(item, dict) and item.get("eventId") is not None
else None
),
title=(
f"#{int(item.get('index', 0)) + 1} {str(item.get('operation', 'unknown'))}"
if isinstance(item, dict)
else "unknown"
),
subtitle=(
"成功"
if isinstance(item, dict) and item.get("status") == "success"
else "失败"
),
description=(
str(item.get("message") or "") if isinstance(item, dict) else ""
),
summary = (
f"{summary} first_error_code={error_info.code} "
f"first_error_message={error_info.message}"
)
for item in result_items
]
return dump_tool_output(
ToolAgentOutput(
@@ -490,24 +492,8 @@ async def calendar_write(
tool_call_id=f"{tool_name}-call",
tool_call_args=tool_call_args,
status=final_status,
result_summary=summary,
result=summary,
error=error_info,
ui_hints=calendar_write_hints(
operation="batch",
message=summary,
event=None,
event_id=None,
status=ui_status,
).model_copy(
update={
"list_items": result_list_items,
"meta": {
"total": batch_size,
"success": success_count,
"failed": failed_count,
},
}
),
)
)
@@ -611,19 +597,14 @@ async def calendar_share(
retryable=False,
)
summary = f"日程已分享,已邀请 {len(invited)}"
summary = f"status=success event_id={event_id} invited_count={len(invited)}"
return dump_tool_output(
ToolAgentOutput(
tool_name=tool_name,
tool_call_id=f"{tool_name}-call",
tool_call_args=tool_call_args,
status=ToolStatus.SUCCESS,
result_summary=summary,
ui_hints=calendar_share_hints(
event_id=event_id,
invited=invited,
permission={"per_user": True},
),
result=summary,
)
)
except Exception as exc:
@@ -17,15 +17,6 @@ from core.agentscope.tools.utils.tool_response_builder import (
)
from models.profile import Profile
from schemas.agent.runtime_models import ToolAgentOutput, ToolStatus
from schemas.agent.ui_hints import (
UiHintAction,
UiHintActionCopy,
UiHintActionStyle,
UiHintIntent,
UiHintKvItem,
UiHintStatus,
UiHintsPayload,
)
from v1.auth.gateway import SupabaseAuthGateway
@@ -47,51 +38,10 @@ def _lookup_error_output(
message=message,
retryable=retryable,
)
output = output.model_copy(
update={
"tool_call_args": tool_call_args,
"ui_hints": UiHintsPayload(
intent=UiHintIntent.STATUS,
status=UiHintStatus.ERROR,
title="用户查找失败",
body=message,
),
}
)
output = output.model_copy(update={"tool_call_args": tool_call_args})
return _dump_tool_output(output)
def _lookup_success_hints(resolved: dict[str, Any]) -> UiHintsPayload:
user_id = str(resolved.get("userId") or "")
email = str(resolved.get("email") or "")
username = str(resolved.get("username") or "")
matched_by = str(resolved.get("matchedBy") or "")
return UiHintsPayload(
intent=UiHintIntent.DATA,
status=UiHintStatus.SUCCESS,
title="用户信息",
description=f"匹配方式: {matched_by}",
items=[
UiHintKvItem(key="user_id", label="用户ID", value=user_id, copyable=True),
UiHintKvItem(key="email", label="邮箱", value=email, copyable=True),
UiHintKvItem(key="username", label="用户名", value=username or "-"),
UiHintKvItem(key="matched_by", label="匹配方式", value=matched_by),
],
actions=[
UiHintAction(
label="复制用户ID",
style=UiHintActionStyle.SECONDARY,
action=UiHintActionCopy(
type="copy",
content=user_id,
successMessage="用户ID已复制",
),
disabled=not bool(user_id),
)
],
)
async def _resolve_identity(
*,
session: AsyncSession,
@@ -189,15 +139,19 @@ async def user_lookup(
username = str(resolved.get("username") or "")
email = str(resolved.get("email") or "")
summary = f"已找到用户: {username or email}"
user_id = str(resolved.get("userId") or "")
matched_by = str(resolved.get("matchedBy") or "")
summary = (
f"status=success matched_by={matched_by} user_id={user_id} "
f"username={username} has_email={str(bool(email)).lower()}"
)
return _dump_tool_output(
ToolAgentOutput(
tool_name="user_lookup",
tool_call_id="user_lookup-call",
tool_call_args=tool_call_args,
status=ToolStatus.SUCCESS,
result_summary=summary,
ui_hints=_lookup_success_hints(resolved),
result=summary,
)
)
except HTTPException as exc:
@@ -8,16 +8,6 @@ from core.agentscope.tools.utils.tool_response_builder import (
build_tool_response,
)
from schemas.agent.runtime_models import ToolAgentOutput
from schemas.agent.ui_hints import (
UiHintAction,
UiHintActionNavigation,
UiHintActionStyle,
UiHintIntent,
UiHintKvItem,
UiHintListItem,
UiHintStatus,
UiHintsPayload,
)
def dump_tool_output(output: ToolAgentOutput) -> ToolResponse:
@@ -32,12 +22,6 @@ def calendar_error_output(
message: str,
retryable: bool,
) -> ToolResponse:
ui_hints = UiHintsPayload(
intent=UiHintIntent.STATUS,
status=UiHintStatus.ERROR,
title="日历操作失败",
body=message,
)
output = build_error_output(
tool_name=tool_name,
tool_call_id=f"{tool_name}-call",
@@ -45,120 +29,5 @@ def calendar_error_output(
message=message,
retryable=retryable,
)
output = output.model_copy(
update={"tool_call_args": tool_call_args, "ui_hints": ui_hints}
)
output = output.model_copy(update={"tool_call_args": tool_call_args})
return dump_tool_output(output)
def calendar_read_hints(
*,
total: int,
page: int,
page_size: int,
total_pages: int,
events: list[dict[str, Any]],
) -> UiHintsPayload:
event_items = [
UiHintListItem(
id=event.get("id"),
title=str(event.get("title") or "未命名日程"),
subtitle=str(event.get("startAt") or ""),
description=str(event.get("location") or "") or None,
)
for event in events
]
return UiHintsPayload(
intent=UiHintIntent.LIST,
status=UiHintStatus.SUCCESS,
title="日程列表",
description=f"{total} 个日程",
items=[
UiHintKvItem(key="total", label="总数", value=total),
UiHintKvItem(key="page", label="当前页", value=page),
UiHintKvItem(key="page_size", label="每页", value=page_size),
UiHintKvItem(key="total_pages", label="总页数", value=total_pages),
],
listItems=event_items,
actions=[
UiHintAction(
label="打开日历",
style=UiHintActionStyle.PRIMARY,
action=UiHintActionNavigation(type="navigation", path="/calendar"),
)
],
meta={"total": total, "page": page, "page_size": page_size},
)
def calendar_write_hints(
*,
operation: str,
message: str,
event: dict[str, Any] | None,
event_id: str | None,
status: UiHintStatus = UiHintStatus.SUCCESS,
) -> UiHintsPayload:
kv_items: list[UiHintKvItem] = []
if event:
kv_items = [
UiHintKvItem(
key="event_id",
label="日程ID",
value=str(event.get("id") or ""),
copyable=True,
),
UiHintKvItem(
key="title",
label="标题",
value=str(event.get("title") or ""),
copyable=True,
),
UiHintKvItem(
key="start_at",
label="开始时间",
value=str(event.get("startAt") or ""),
copyable=True,
),
]
elif event_id:
message = f"目标日程 ID: {event_id}\n{message}"
return UiHintsPayload(
intent=UiHintIntent.STATUS,
status=status,
title="日历操作完成",
body=message,
items=kv_items,
actions=[
UiHintAction(
label="查看日历",
style=UiHintActionStyle.PRIMARY,
action=UiHintActionNavigation(type="navigation", path="/calendar"),
)
],
)
def calendar_share_hints(
*,
event_id: str,
invited: list[str],
permission: dict[str, Any],
) -> UiHintsPayload:
permission_text = (
", ".join([k for k, v in permission.items() if v is True]) or "按邀请人单独设置"
)
return UiHintsPayload(
intent=UiHintIntent.STATUS,
status=UiHintStatus.SUCCESS,
title="日程已分享",
description=f"已邀请 {len(invited)}",
items=[
UiHintKvItem(key="event_id", label="日程ID", value=event_id, copyable=True),
UiHintKvItem(key="permission", label="权限", value=permission_text),
],
listItems=[UiHintListItem(title=email) for email in invited] if invited else [],
)
@@ -34,7 +34,7 @@ def build_error_output(
tool_name=tool_name,
tool_call_id=tool_call_id,
status=ToolStatus.FAILURE,
result_summary=message,
result=f"status=failure code={code} message={message}",
error=ErrorInfo(
code=code,
message=message,