feat: 优化 Agent 运行时与聊天设置体验
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -25,13 +25,14 @@ class AppVersionInfo(BaseModel):
|
||||
router = APIRouter(prefix="/app", tags=["app"])
|
||||
|
||||
|
||||
def _parse_version(filename: str) -> tuple[str, int] | None:
|
||||
def _parse_version(filename: str) -> tuple[str, int, tuple[int, ...]] | None:
|
||||
pattern = r"app[-_]v?(\d+\.\d+\.\d+)\+(\d+)\.(?:apk|ipa)"
|
||||
match = re.search(pattern, filename, re.IGNORECASE)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
build = int(match.group(2))
|
||||
return (version, build)
|
||||
version_tuple = tuple(int(x) for x in version.split("."))
|
||||
return (version, build, version_tuple)
|
||||
return None
|
||||
|
||||
|
||||
@@ -44,21 +45,32 @@ def _get_latest_release(
|
||||
if not base_path.exists():
|
||||
return None
|
||||
|
||||
ext = "ipa" if platform == "ios" else "apk"
|
||||
target_ext = "ipa" if platform == "ios" else "apk"
|
||||
candidates = []
|
||||
|
||||
MIN_APK_SIZE = 1024 * 1024 # 1MB
|
||||
MIN_IPA_SIZE = 1024 * 1024 # 1MB
|
||||
|
||||
for f in base_path.iterdir():
|
||||
if f.is_file() and f.suffix.lstrip(".").lower() == ext.lower():
|
||||
parsed = _parse_version(f.name)
|
||||
if parsed:
|
||||
version, build = parsed
|
||||
candidates.append((version, build, f.name))
|
||||
if not f.is_file():
|
||||
continue
|
||||
ext = f.suffix.lstrip(".").lower()
|
||||
if ext != target_ext:
|
||||
continue
|
||||
# 简单校验文件大小,排除伪装文件
|
||||
if f.stat().st_size < (MIN_APK_SIZE if ext == "apk" else MIN_IPA_SIZE):
|
||||
continue
|
||||
parsed = _parse_version(f.name)
|
||||
if parsed:
|
||||
version, build, version_tuple = parsed
|
||||
candidates.append((version_tuple, build, f.name))
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
|
||||
return candidates[0][0], candidates[0][1], candidates[0][2]
|
||||
result = candidates[0]
|
||||
return result[2].replace("+", "."), result[1], result[2]
|
||||
|
||||
|
||||
def _compare_versions(
|
||||
|
||||
Reference in New Issue
Block a user