feat: 实现 AgentScope ReAct Runner 两阶段执行并重构事件处理

This commit is contained in:
zl-q
2026-03-16 09:01:01 +08:00
parent 072c09d99d
commit dcceb48d84
51 changed files with 5015 additions and 5663 deletions
+6 -2
View File
@@ -14,7 +14,9 @@ from schemas.agent.runtime_models import (
from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig
from schemas.agent.ui_hints import (
UiHintAction,
UiHintBlock,
UiHintIntent,
UiHintSection,
UiHintStatus,
UiHintsPayload,
)
@@ -29,7 +31,9 @@ __all__ = [
"ToolStatus",
"UiMode",
"UiHintAction",
"UiHintBlock",
"UiHintIntent",
"UiHintSection",
"UiHintStatus",
"UiHintsPayload",
"WorkerAgentOutputLite",
"WorkerAgentOutputRich",
+183 -438
View File
@@ -1,10 +1,26 @@
"""
UiHints - 描述性 UI 提示
设计原则:
- 描述性而非渲染性: 告诉编译器“要展示什么”,而不是“如何渲染”
- 最小化 token: 保持字段简洁
- 可编译: 可机械转换为 UiSchemaRenderer
- 尽量无损: hints 中的主要内容字段应尽量被保留到 renderer 中
Version: 2.1
"""
from __future__ import annotations
from enum import Enum
from typing import Annotated, Any, Literal
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field
# ============================================================
# Enums
# ============================================================
class UiHintStatus(str, Enum):
INFO = "info"
@@ -14,6 +30,17 @@ class UiHintStatus(str, Enum):
PENDING = "pending"
class UiHintIntent(str, Enum):
"""主要展示意图(弱提示,不应决定字段生死)"""
MESSAGE = "message" # 普通消息/说明
DATA = "data" # 数据/结果摘要
LIST = "list" # 列表为主
STATUS = "status" # 状态结果为主
FORM = "form" # 结构化内容(当前不表示真实输入表单)
MIXED = "mixed" # 混合内容
class UiHintActionStyle(str, Enum):
PRIMARY = "primary"
SECONDARY = "secondary"
@@ -26,520 +53,238 @@ class UiHintTextFormat(str, Enum):
MARKDOWN = "markdown"
class UiHintContainerDirection(str, Enum):
VERTICAL = "vertical"
HORIZONTAL = "horizontal"
class UiHintActionType(str, Enum):
NAVIGATION = "navigation"
URL = "url"
EVENT = "event"
TOOL = "tool"
COPY = "copy"
PAYLOAD = "payload"
class UiHintKvLayout(str, Enum):
VERTICAL = "vertical"
HORIZONTAL = "horizontal"
GRID = "grid"
class UiHintIconSource(str, Enum):
ICON = "icon"
EMOJI = "emoji"
URL = "url"
class UiHintOperationType(str, Enum):
CREATE = "create"
UPDATE = "update"
DELETE = "delete"
EXECUTE = "execute"
# ============================================================
# Base Config
# ============================================================
class UiHintOperationResult(str, Enum):
SUCCESS = "success"
FAILURE = "failure"
PARTIAL = "partial"
class UiHintConfirm(BaseModel):
model_config = ConfigDict(extra="forbid")
title: str | None = Field(
default=None,
description="Optional confirmation dialog title.",
)
message: str | None = Field(
default=None,
description="Optional confirmation message shown before action execution.",
)
confirm_label: str | None = Field(
default=None,
alias="confirmLabel",
description="Optional confirm button label, e.g. 'Delete'.",
)
cancel_label: str | None = Field(
default=None,
alias="cancelLabel",
description="Optional cancel button label, e.g. 'Cancel'.",
class UiHintBaseModel(BaseModel):
model_config = ConfigDict(
extra="forbid",
populate_by_name=True,
)
class UiHintActionNavigation(BaseModel):
model_config = ConfigDict(extra="forbid")
# ============================================================
# Action Targets
# ============================================================
class UiHintActionNavigation(UiHintBaseModel):
type: Literal["navigation"]
path: str = Field(
...,
description="Internal route path to navigate to.",
)
params: dict[str, Any] | None = Field(
default=None,
description="Optional route params for internal navigation.",
)
path: str = Field(..., description="Internal route path.")
params: dict[str, Any] | None = Field(default=None, description="Route params.")
class UiHintActionUrl(BaseModel):
model_config = ConfigDict(extra="forbid")
class UiHintActionUrl(UiHintBaseModel):
type: Literal["url"]
url: str = Field(..., description="External URL to open.")
target: Literal["_self", "_blank"] | None = Field(
default=None,
description="Optional browser target for URL action.",
)
url: str = Field(..., description="External URL.")
target: Literal["_self", "_blank"] | None = Field(default=None)
class UiHintActionEvent(BaseModel):
model_config = ConfigDict(extra="forbid")
class UiHintActionEvent(UiHintBaseModel):
type: Literal["event"]
event: str = Field(
...,
description="Frontend domain event name, e.g. 'chat.retry'.",
)
payload: dict[str, Any] | None = Field(
default=None,
description="Optional event payload for frontend event handling.",
)
event: str = Field(..., description="Frontend event name.")
payload: dict[str, Any] | None = Field(default=None)
class UiHintActionTool(BaseModel):
model_config = ConfigDict(extra="forbid")
class UiHintActionTool(UiHintBaseModel):
type: Literal["tool"]
tool_id: str = Field(
alias="toolId",
description="Tool identifier used to trigger another tool execution.",
)
params: dict[str, Any] | None = Field(
default=None,
description="Optional parameters for tool re-execution.",
)
tool_id: str = Field(alias="toolId", description="Tool identifier.")
params: dict[str, Any] | None = Field(default=None)
class UiHintActionCopy(BaseModel):
model_config = ConfigDict(extra="forbid")
class UiHintActionCopy(UiHintBaseModel):
type: Literal["copy"]
content: str = Field(..., description="Text content to copy to clipboard.")
success_message: str | None = Field(
default=None,
alias="successMessage",
description="Optional user-facing success message after copy.",
)
content: str = Field(..., description="Content to copy.")
success_message: str | None = Field(alias="successMessage", default=None)
class UiHintActionPayload(BaseModel):
model_config = ConfigDict(extra="forbid")
class UiHintActionPayload(UiHintBaseModel):
type: Literal["payload"]
payload: dict[str, Any] = Field(
...,
description="Structured payload to submit to frontend or gateway.",
)
submit_to: str | None = Field(
default=None,
alias="submitTo",
description="Optional submit target path or endpoint key.",
)
payload: dict[str, Any] = Field(..., description="Structured payload.")
submit_to: str | None = Field(alias="submitTo", default=None)
UiHintActionTarget = Annotated[
(
UiHintActionNavigation
| UiHintActionUrl
| UiHintActionEvent
| UiHintActionTool
| UiHintActionCopy
| UiHintActionPayload
),
Field(discriminator="type"),
]
UiHintActionTarget = (
UiHintActionNavigation
| UiHintActionUrl
| UiHintActionEvent
| UiHintActionTool
| UiHintActionCopy
| UiHintActionPayload
)
class UiHintAction(BaseModel):
model_config = ConfigDict(
extra="forbid",
json_schema_extra={
"examples": [
{
"id": "action-open-calendar",
"label": "Open calendar",
"style": "primary",
"action": {"type": "navigation", "path": "/calendar"},
}
]
},
)
id: str | None = Field(
default=None,
description="Optional stable action id for tracking and targeting.",
)
label: str = Field(
...,
description="User-facing action label shown on button/link.",
)
style: UiHintActionStyle | None = Field(
default=None,
description="Optional semantic button style.",
)
disabled: bool = Field(
default=False,
description="Whether this action should be rendered as disabled.",
)
action: UiHintActionTarget = Field(
...,
description="Executable action target definition.",
)
confirm: UiHintConfirm | None = Field(
default=None,
description="Optional confirmation requirement before execution.",
)
class UiHintAction(UiHintBaseModel):
label: str = Field(..., description="Button label.")
style: UiHintActionStyle | None = Field(default=None, description="Button style.")
disabled: bool = Field(default=False, description="Disabled state.")
action: UiHintActionTarget = Field(..., description="Action to execute.")
class UiHintIcon(BaseModel):
model_config = ConfigDict(extra="forbid")
source: Literal["icon", "emoji", "url"] = Field(
...,
description="Icon source type.",
)
value: str = Field(
...,
description="Icon identifier, emoji text, or image URL based on source.",
)
color: str | None = Field(
default=None,
description="Optional semantic color hint. Do not encode pixel-level style rules.",
)
size: int | None = Field(
default=None,
description="Optional icon size hint in abstract UI units.",
)
# ============================================================
# Small Descriptive Models
# ============================================================
class UiHintBadge(BaseModel):
model_config = ConfigDict(extra="forbid")
label: str = Field(..., description="Badge text label.")
variant: Literal["default", "success", "warning", "error", "info"] = Field(
default="default",
description="Semantic badge variant.",
)
class UiHintIcon(UiHintBaseModel):
source: UiHintIconSource = Field(default=UiHintIconSource.ICON)
value: str = Field(..., description="Icon identifier / emoji / url.")
color: str | None = Field(default=None)
size: int | None = Field(default=None)
class UiHintKeyValuePair(BaseModel):
model_config = ConfigDict(extra="forbid")
key: str = Field(..., description="Stable key identifier for this pair.")
label: str | None = Field(
default=None,
description="Optional user-facing label. Fallback to key when missing.",
)
value: str | int | bool | None = Field(
default=None,
description="Scalar value for this key-value pair.",
)
copyable: bool = Field(
default=False,
description="Whether frontend may offer copy interaction for this value.",
)
class UiHintKvItem(UiHintBaseModel):
key: str = Field(..., description="Key identifier.")
label: str | None = Field(default=None, description="Display label.")
value: Any = Field(default=None, description="Value.")
copyable: bool = Field(default=False, description="Allow copy.")
class UiHintListItem(BaseModel):
model_config = ConfigDict(extra="forbid")
id: str | None = Field(
default=None,
description="Optional stable list item id.",
)
title: str = Field(..., description="Primary list item title.")
subtitle: str | None = Field(
default=None,
description="Optional short secondary text.",
)
description: str | None = Field(
default=None,
description="Optional detailed description for this item.",
)
icon: UiHintIcon | None = Field(
default=None,
description="Optional semantic icon metadata.",
)
badge: UiHintBadge | None = Field(
default=None,
description="Optional semantic badge metadata.",
)
metadata: dict[str, Any] = Field(
default_factory=dict,
description="Optional non-visual metadata for analytics or interactions.",
)
actions: list[UiHintAction] = Field(
default_factory=list,
description="Optional per-item actions, recommended up to 3.",
)
class UiHintListItem(UiHintBaseModel):
id: str | None = Field(default=None)
title: str = Field(..., description="Item title.")
subtitle: str | None = Field(default=None)
description: str | None = Field(default=None)
icon: UiHintIcon | None = Field(default=None)
status: UiHintStatus | None = Field(default=None)
actions: list[UiHintAction] = Field(default_factory=list)
class UiHintPagination(BaseModel):
model_config = ConfigDict(extra="forbid")
class UiHintSection(UiHintBaseModel):
title: str | None = Field(default=None, description="Section title.")
description: str | None = Field(default=None, description="Section description.")
icon: UiHintIcon | None = Field(default=None, description="Section icon.")
page: int = Field(..., description="Current page number starting from 1.")
page_size: int = Field(
alias="pageSize",
description="Page size used for this list page.",
)
total: int = Field(..., description="Total number of records.")
has_more: bool = Field(
alias="hasMore",
description="Whether there are more pages after current page.",
)
class UiHintBaseBlock(BaseModel):
model_config = ConfigDict(extra="forbid")
id: str | None = Field(
default=None,
description="Optional stable block id.",
)
title: str | None = Field(
default=None,
description="Optional block title.",
)
description: str | None = Field(
default=None,
description="Optional block description.",
)
status: UiHintStatus | None = Field(
default=None,
description="Optional semantic status for this block.",
)
actions: list[UiHintAction] = Field(
default_factory=list,
description="Optional block-level actions, recommended up to 3.",
)
class UiHintTextBlock(UiHintBaseBlock):
kind: Literal["text"]
content: str = Field(
...,
description="Main text content to present.",
)
format: UiHintTextFormat = Field(
content: str | None = Field(default=None, description="Main text content.")
content_format: UiHintTextFormat = Field(
default=UiHintTextFormat.PLAIN,
description="Text format: plain or markdown.",
alias="contentFormat",
description="Section content text format.",
)
class UiHintCardBlock(UiHintBaseBlock):
kind: Literal["card"]
children: list["UiHintBlock"] = Field(
items: list[UiHintKvItem] = Field(default_factory=list, description="KV items.")
list_items: list[UiHintListItem] = Field(
default_factory=list,
description="Nested child blocks grouped under this card.",
alias="listItems",
description="List items.",
)
actions: list[UiHintAction] = Field(default_factory=list, description="Actions.")
class UiHintKvBlock(UiHintBaseBlock):
kind: Literal["kv"]
pairs: list[UiHintKeyValuePair] = Field(
default_factory=list,
description="Key-value pairs to display.",
)
layout: UiHintKvLayout = Field(
default=UiHintKvLayout.VERTICAL,
description="Preferred semantic layout for key-value content.",
)
# ============================================================
# Root Payload
# ============================================================
class UiHintListBlock(UiHintBaseBlock):
kind: Literal["list"]
items: list[UiHintListItem] = Field(
default_factory=list,
description="List items to present.",
)
pagination: UiHintPagination | None = Field(
default=None,
description="Optional pagination metadata.",
)
empty_text: str | None = Field(
default=None,
alias="emptyText",
description="Optional message shown when list items are empty.",
)
class UiHintsPayload(UiHintBaseModel):
"""
描述性 UI 提示
设计目标:
- agent 输出尽可能短
- 不表达布局细节
- 编译器负责转换为完整 UiSchemaRenderer
"""
class UiHintOperationBlock(UiHintBaseBlock):
kind: Literal["operation"]
operation: UiHintOperationType = Field(
...,
description="Operation category: create/update/delete/execute.",
)
result: UiHintOperationResult = Field(
...,
description="Operation result: success/failure/partial.",
)
message: str | None = Field(
default=None,
description="Optional operation summary message.",
)
affected_count: int | None = Field(
default=None,
alias="affectedCount",
description="Optional affected record count.",
)
details: dict[str, Any] | None = Field(
default=None,
description="Optional machine-readable operation details.",
)
class UiHintErrorBlock(UiHintBaseBlock):
kind: Literal["error"]
error_code: str = Field(
alias="errorCode",
description="Stable error code for categorization.",
)
message: str = Field(
...,
description="Human-readable error message.",
)
retryable: bool = Field(
default=False,
description="Whether retry is likely to succeed.",
)
details: str | None = Field(
default=None,
description="Optional plain-text diagnostic details.",
)
suggestions: list[str] = Field(
default_factory=list,
description="Optional actionable suggestions, recommended up to 3.",
)
class UiHintContainerBlock(UiHintBaseBlock):
kind: Literal["container"]
direction: UiHintContainerDirection = Field(
default=UiHintContainerDirection.VERTICAL,
description="Child block layout direction.",
)
gap: int | None = Field(
default=None,
description="Optional semantic spacing hint between children.",
)
children: list["UiHintBlock"] = Field(
default_factory=list,
description="Nested child blocks in this container.",
)
class UiHintCustomBlock(UiHintBaseBlock):
kind: Literal["custom"]
renderer_key: str = Field(
alias="rendererKey",
description=(
"Custom semantic renderer key. Use only when standard block kinds "
"cannot represent the intent."
),
)
payload: dict[str, Any] = Field(
default_factory=dict,
description="Structured custom payload consumed by the renderer.",
)
UiHintBlock = Annotated[
(
UiHintTextBlock
| UiHintCardBlock
| UiHintKvBlock
| UiHintListBlock
| UiHintOperationBlock
| UiHintErrorBlock
| UiHintContainerBlock
| UiHintCustomBlock
),
Field(discriminator="kind"),
]
class UiHintsPayload(BaseModel):
model_config = ConfigDict(
extra="forbid",
populate_by_name=True,
json_schema_extra={
"examples": [
{
"version": "1.0",
"status": "info",
"title": "Schedule update",
"blocks": [
{
"kind": "text",
"content": "Your meeting is moved to 3:00 PM.",
"format": "plain",
},
{
"kind": "list",
"title": "Next steps",
"items": [
{"title": "Open calendar"},
{"title": "Notify attendees"},
],
},
"intent": "status",
"status": "success",
"title": "日程已创建",
"body": "本次创建已成功完成。",
"items": [
{"key": "title", "label": "主题", "value": "Q1 规划会议"},
{"key": "time", "label": "时间", "value": "2026-03-15 14:00"},
],
"actions": [
{
"label": "Open calendar",
"label": "查看详情",
"style": "primary",
"action": {"type": "navigation", "path": "/calendar"},
}
"action": {
"type": "navigation",
"path": "/calendar/evt_123",
},
},
{
"label": "删除",
"style": "danger",
"action": {
"type": "tool",
"toolId": "calendar.delete",
"params": {"eventId": "evt_123"},
},
},
],
"meta": {"source": "worker"},
}
]
},
)
version: str = Field(
default="1.0",
description="Ui hints payload version.",
version: str = Field(default="2.1")
intent: UiHintIntent = Field(
default=UiHintIntent.MESSAGE,
description="Primary display intent.",
)
status: UiHintStatus = Field(
default=UiHintStatus.INFO,
description="Overall semantic status for the full ui_hints payload.",
description="Overall status.",
)
title: str | None = Field(
default=None,
description="Optional top-level semantic title.",
title: str | None = Field(default=None, description="Top-level title.")
description: str | None = Field(default=None, description="Top-level description.")
body: str | None = Field(default=None, description="Top-level main body text.")
body_format: UiHintTextFormat = Field(
default=UiHintTextFormat.PLAIN,
alias="bodyFormat",
description="Body text format.",
)
description: str | None = Field(
default=None,
description="Optional top-level semantic description.",
)
blocks: list[UiHintBlock] = Field(
items: list[UiHintKvItem] = Field(
default_factory=list,
description="Main semantic content blocks.",
description="Top-level key-value items.",
)
list_items: list[UiHintListItem] = Field(
default_factory=list,
alias="listItems",
description="Top-level list items.",
)
sections: list[UiHintSection] = Field(
default_factory=list,
description="Grouped sections.",
)
actions: list[UiHintAction] = Field(
default_factory=list,
description="Optional top-level actions, recommended up to 3.",
description="Top-level actions.",
)
icon: UiHintIcon | None = Field(
default=None,
description="Top-level icon.",
)
meta: dict[str, Any] = Field(
default_factory=dict,
description="Optional non-visual metadata for tracing and integration.",
description="Extra meta, e.g. requestId/toolId/traceId/userId.",
)
UiHintCardBlock.model_rebuild()
UiHintContainerBlock.model_rebuild()
File diff suppressed because it is too large Load Diff
+12 -3
View File
@@ -1,12 +1,14 @@
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import ClassVar
from uuid import UUID
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel, ConfigDict, Field
from schemas.agent.runtime_models import RouterAgentOutput, WorkerAgentOutputRich
from ..agent import AgentType, ToolAgentOutput, WorkerAgentOutput
from ..agent import AgentType, ToolAgentOutput
class UserMessageAttachments(BaseModel):
@@ -22,8 +24,9 @@ class AgentChatMessageMetadata(BaseModel):
run_id: str
agent_type: AgentType | None = None
user_message_attachments: UserMessageAttachments | None = None
router_agent_output: RouterAgentOutput | None = None
tool_agent_output: ToolAgentOutput | None = None
worker_agent_output: WorkerAgentOutput | None = None
worker_agent_output: WorkerAgentOutputRich | None = None
class AgentChatMessage(BaseModel):
@@ -35,5 +38,11 @@ class AgentChatMessage(BaseModel):
seq: int
role: str
content: str
model_code: str | None = None
tool_name: str | None = None
input_tokens: int = Field(default=0, ge=0)
output_tokens: int = Field(default=0, ge=0)
cost: Decimal = Field(default=Decimal("0"))
latency_ms: int | None = Field(default=None, ge=0)
metadata: AgentChatMessageMetadata | dict[str, object] | None = None
timestamp: datetime