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:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user