feat: 优化 Agent 运行时与聊天设置体验

This commit is contained in:
qzl
2026-03-16 18:32:09 +08:00
parent 3f79cf0df7
commit 5a34616287
41 changed files with 2603 additions and 1263 deletions
+10 -3
View File
@@ -37,6 +37,13 @@ class AttachmentSignedUrlResponse(BaseModel):
url: str
class HistoryMessageAttachment(BaseModel):
model_config = ConfigDict(populate_by_name=True, serialize_by_alias=True)
mime_type: str = Field(alias="mimeType")
url: str
class HistoryMessage(BaseModel):
"""History message schema for /history endpoint response."""
@@ -46,9 +53,9 @@ class HistoryMessage(BaseModel):
seq: int = Field(description="Message sequence number")
role: str = Field(description="Message role: user | assistant | tool")
content: str = Field(description="Message text content")
url: str | None = Field(
default=None,
description="Temporary signed URL for user-attached images",
attachments: list[HistoryMessageAttachment] = Field(
default_factory=list,
description="Temporary signed URLs for user-attached images",
)
ui_schema: UiSchemaRenderer | None = Field(
default=None,
+38 -18
View File
@@ -19,7 +19,8 @@ from core.config.settings import config
from core.logging import get_logger
from schemas.messages.chat_message import (
AgentChatMessageMetadata,
UserMessageAttachments,
UserMessageAttachment,
extract_user_message_attachments,
)
from v1.agent.schemas import HistorySnapshotResponse
@@ -27,6 +28,7 @@ logger = get_logger(__name__)
_ALLOWED_ATTACHMENT_MIME_TYPES = {"image/png", "image/jpeg", "image/webp"}
_MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024
_MAX_TOTAL_ATTACHMENT_BYTES = 12 * 1024 * 1024
_MAX_ATTACHMENTS_PER_MESSAGE = 3
@dataclass(frozen=True)
@@ -230,7 +232,7 @@ class AgentService:
) -> tuple[str, AgentChatMessageMetadata | None]:
text, content_blocks = extract_latest_user_payload(run_input)
user_attachments: UserMessageAttachments | None = None
user_attachments: list[UserMessageAttachment] = []
for block in content_blocks:
if not isinstance(block, dict):
continue
@@ -257,12 +259,15 @@ class AgentService:
thread_id=run_input.thread_id,
current_user=current_user,
)
user_attachments = UserMessageAttachments(
bucket=bucket,
path=path,
mime_type=mime_type,
user_attachments.append(
UserMessageAttachment(
bucket=bucket,
path=path,
mime_type=mime_type,
)
)
break
if len(user_attachments) > _MAX_ATTACHMENTS_PER_MESSAGE:
raise HTTPException(status_code=422, detail="Too many attachments")
except HTTPException:
raise
except Exception as exc: # noqa: BLE001
@@ -270,7 +275,7 @@ class AgentService:
raise HTTPException(status_code=422, detail="Invalid signed image url")
metadata: AgentChatMessageMetadata | None = None
if user_attachments is not None:
if user_attachments:
metadata = AgentChatMessageMetadata(
run_id=run_input.run_id,
user_message_attachments=user_attachments,
@@ -438,23 +443,38 @@ class AgentService:
messages: list[HistoryMessage] = []
if day_payload:
raw_messages = day_payload.get("messages") or []
raw_messages_obj = day_payload.get("messages")
raw_messages = (
raw_messages_obj if isinstance(raw_messages_obj, list) else []
)
for msg_dict in raw_messages:
msg = AgentChatMessage.model_validate(msg_dict)
signed_url: str | None = None
if self._attachment_storage and msg.metadata:
att = msg.metadata.user_message_attachments
if att:
signed_urls: dict[str, str] = {}
attachments = extract_user_message_attachments(msg.metadata)
if self._attachment_storage and attachments:
expected_prefix = (
f"agent-inputs/{current_user.id}/{thread_id}/uploads/"
)
for attachment in attachments:
if not _is_safe_attachment_path(
attachment.path,
expected_prefix=expected_prefix,
):
continue
signed_url = await self._attachment_storage.create_signed_url(
bucket=att.bucket,
path=att.path,
bucket=attachment.bucket,
path=attachment.path,
expires_in_seconds=self._SIGNED_URL_EXPIRES_IN_SECONDS,
)
key = f"{attachment.bucket}/{attachment.path}"
signed_urls[key] = signed_url
converted = convert_message_to_history(msg, None)
if signed_url:
converted["url"] = signed_url
def _get_signed_url(payload: dict[str, str]) -> str:
key = f"{payload['bucket']}/{payload['path']}"
return signed_urls[key]
converted = convert_message_to_history(msg, _get_signed_url)
messages.append(HistoryMessage.model_validate(converted))
return HistorySnapshotResponse(
+28 -23
View File
@@ -11,7 +11,7 @@ from core.agentscope.runtime.ui_compiler import compile as compile_ui_hints
from schemas.messages.chat_message import (
AgentChatMessage,
AgentChatMessageMetadata,
UserMessageAttachments,
extract_user_message_attachments,
)
@@ -23,7 +23,7 @@ def convert_message_to_history(
将 AgentChatMessage 转换为 HistoryMessage 格式
转换规则:
- role=user: 读取 metadata.user_message_attachments将 bucket 转临时访问 url
- role=user: 读取 metadata.user_message_attachments转换为 attachments[]
- role=tool: 读取 content 和 metadata.tool_agent_output.ui_hints,编译成 ui_schema
- role=assistant: 读取 metadata.worker_agent_output.ui_hints,编译成 ui_schema
"""
@@ -31,11 +31,11 @@ def convert_message_to_history(
content = message.content
metadata = message.metadata
url: str | None = None
attachments: list[dict[str, str]] = []
ui_schema: dict[str, Any] | None = None
if role == "user":
url = _convert_user_attachments(metadata, get_signed_url_fn)
attachments = _convert_user_attachments(metadata, get_signed_url_fn)
elif role == "tool":
ui_schema = _compile_tool_ui_hints(metadata)
@@ -51,8 +51,8 @@ def convert_message_to_history(
"timestamp": message.timestamp.isoformat(),
}
if url:
result["url"] = url
if attachments:
result["attachments"] = attachments
if ui_schema:
result["ui_schema"] = ui_schema
@@ -63,28 +63,33 @@ def convert_message_to_history(
def _convert_user_attachments(
metadata: AgentChatMessageMetadata | dict[str, Any] | None,
get_signed_url_fn: Callable[[dict[str, str]], str] | None,
) -> str | None:
"""转换用户附件为临时访问 URL"""
if not metadata:
return None
) -> list[dict[str, str]]:
"""转换用户附件为临时访问 URL 列表"""
if not metadata or not get_signed_url_fn:
return []
if isinstance(metadata, AgentChatMessageMetadata):
attachments = metadata.user_message_attachments
resolved = extract_user_message_attachments(metadata)
elif isinstance(metadata, dict):
resolved = extract_user_message_attachments(metadata)
else:
attachments_data = metadata.get("user_message_attachments")
if not attachments_data:
return None
attachments = UserMessageAttachments.model_validate(attachments_data)
return []
if not attachments or not get_signed_url_fn:
return None
try:
return get_signed_url_fn(
{"bucket": attachments.bucket, "path": attachments.path}
signed_attachments: list[dict[str, str]] = []
for attachment in resolved:
try:
signed_url = get_signed_url_fn(
{"bucket": attachment.bucket, "path": attachment.path}
)
except Exception:
continue
signed_attachments.append(
{
"url": signed_url,
"mimeType": attachment.mime_type,
}
)
except Exception:
return None
return signed_attachments
def _compile_tool_ui_hints(