Files
agent-desktop/src/pineagents/schemas.py
T
Pine 96659759b0 feat: 智能体后端运行时(src/pineagents)
- Agent OS:agents 决策循环、runtime 编排、providers、governance/security/sandbox
- app 服务层(认证转发到 server-core)、channels 19 路频道、memory、plugins
2026-08-23 22:41:48 +08:00

296 lines
8.3 KiB
Python

# -*- coding: utf-8 -*-
"""QwenPaw streaming envelope schema.
Defines the ``Message`` / ``Content`` / ``AgentRequest`` / ``AgentResponse``
types that ``stream_query`` produces and all channels consume. These are
qwenpaw's own envelope protocol — independent of agentscope's internal
event types.
"""
from __future__ import annotations
from enum import Enum
from typing import Any, Dict, List, Optional, Union
from uuid import uuid4
from pydantic import BaseModel, ConfigDict, Field, field_validator
# ---------------------------------------------------------------------------
# Enums.
# ---------------------------------------------------------------------------
class Role(str, Enum):
USER = "user"
ASSISTANT = "assistant"
SYSTEM = "system"
TOOL = "tool"
class MessageType(str, Enum):
MESSAGE = "message"
REASONING = "reasoning"
PLUGIN_CALL = "plugin_call"
PLUGIN_CALL_OUTPUT = "plugin_call_output"
FUNCTION_CALL = "function_call"
FUNCTION_CALL_OUTPUT = "function_call_output"
MCP_TOOL_CALL = "mcp_tool_call"
MCP_TOOL_CALL_OUTPUT = "mcp_tool_call_output"
PROGRESS = "progress"
RESULT = "result"
class RunStatus(str, Enum):
Created = "created"
InProgress = "in_progress"
Completed = "completed"
Failed = "failed"
Cancelled = "cancelled"
class ContentType(str, Enum):
TEXT = "text"
IMAGE = "image"
AUDIO = "audio"
VIDEO = "video"
DATA = "data"
FILE = "file"
REFUSAL = "refusal"
# ---------------------------------------------------------------------------
# Content blocks.
# Each variant carries its modality-specific fields plus the streaming
# bookkeeping (``delta`` / ``index``) the runtime emitted.
# ---------------------------------------------------------------------------
class _ContentBase(BaseModel):
model_config = ConfigDict(extra="allow")
type: ContentType
delta: bool = False
index: Optional[int] = None
status: Optional[RunStatus] = None
object: str = "content"
msg_id: Optional[str] = None
def in_progress(self) -> "_ContentBase":
self.status = RunStatus.InProgress
return self
def completed(self) -> "_ContentBase":
self.status = RunStatus.Completed
return self
class TextContent(_ContentBase):
type: ContentType = ContentType.TEXT
text: str = ""
class ImageContent(_ContentBase):
type: ContentType = ContentType.IMAGE
image_url: Optional[str] = None
class AudioContent(_ContentBase):
type: ContentType = ContentType.AUDIO
data: Optional[str] = None
format: Optional[str] = None
class VideoContent(_ContentBase):
type: ContentType = ContentType.VIDEO
video_url: Optional[str] = None
class FileContent(_ContentBase):
type: ContentType = ContentType.FILE
filename: Optional[str] = None
file_url: Optional[str] = None
class DataContent(_ContentBase):
type: ContentType = ContentType.DATA
data: Any = None
class RefusalContent(_ContentBase):
type: ContentType = ContentType.REFUSAL
refusal: str = ""
Content = Union[
TextContent,
ImageContent,
AudioContent,
VideoContent,
FileContent,
DataContent,
RefusalContent,
]
# ---------------------------------------------------------------------------
# Function / tool call payloads (embedded inside ``DataContent.data``).
# ---------------------------------------------------------------------------
class FunctionCall(BaseModel):
model_config = ConfigDict(extra="allow")
call_id: Optional[str] = None
name: Optional[str] = None
arguments: Optional[str] = None
class FunctionCallOutput(BaseModel):
model_config = ConfigDict(extra="allow")
call_id: Optional[str] = None
name: Optional[str] = None
output: Optional[str] = None
# ---------------------------------------------------------------------------
# Message envelope.
# qwenpaw's runner constructs an empty ``Message`` then calls
# ``add_content(new_content=...)`` repeatedly and ``completed()`` once the
# stream ends. The original carried streaming bookkeeping (status,
# in-progress flag); we keep the same shape so callers that serialize the
# whole envelope still see familiar fields.
# ---------------------------------------------------------------------------
_CONTENT_TYPE_REGISTRY: Dict[str, type] = {
ContentType.TEXT.value: TextContent,
ContentType.IMAGE.value: ImageContent,
ContentType.AUDIO.value: AudioContent,
ContentType.VIDEO.value: VideoContent,
ContentType.FILE.value: FileContent,
ContentType.DATA.value: DataContent,
ContentType.REFUSAL.value: RefusalContent,
}
def _coerce_content_item(item: Any) -> Any:
"""Coerce a raw dict content item to its typed ``*Content`` subclass.
Channels access content via ``getattr(c, "type")`` / ``getattr(c,
"text")``;
raw JSON dicts arriving through the HTTP layer would silently fail those
checks (e.g. ``_apply_no_text_debounce`` would buffer the message and never
process it) because dict attribute access returns ``None``. Coerce here so
every downstream caller sees a pydantic model with real attributes.
"""
if not isinstance(item, dict):
return item
type_value = item.get("type")
if hasattr(type_value, "value"):
type_value = type_value.value
cls = _CONTENT_TYPE_REGISTRY.get(type_value)
if cls is None:
# Unknown type — leave the dict so the downstream layer can decide.
return item
try:
return cls(**item)
except Exception: # pragma: no cover - keep coercion best-effort
return item
class Message(BaseModel):
model_config = ConfigDict(extra="allow")
id: str = Field(default_factory=lambda: uuid4().hex)
type: MessageType = MessageType.MESSAGE
role: Optional[Role] = None
content: List[Any] = Field(default_factory=list)
status: RunStatus = RunStatus.InProgress
metadata: Optional[Dict[str, Any]] = None
@field_validator("content", mode="before")
@classmethod
def _coerce_content(cls, value: Any) -> Any:
if not isinstance(value, list):
return value
return [_coerce_content_item(item) for item in value]
def add_content(self, *, new_content: Any) -> "Message":
"""Append a content block, mirroring the 1.x runtime contract."""
self.content.append(new_content)
return self
def completed(self) -> "Message":
"""Mark the message as finished and return self (chain-friendly)."""
self.status = RunStatus.Completed
return self
def in_progress(self) -> "Message":
self.status = RunStatus.InProgress
return self
# ---------------------------------------------------------------------------
# Top-level event / request / response envelopes.
# These are constructed by qwenpaw rather than parsed from external input
# in the console mainline; the shapes below cover the constructor kwargs
# observed in src/.
# ---------------------------------------------------------------------------
class Event(BaseModel):
model_config = ConfigDict(extra="allow")
object: Optional[str] = None
status: Optional[RunStatus] = None
data: Optional[Dict[str, Any]] = None
class AgentRequest(BaseModel):
"""Incoming request envelope.
The original surface mirrored OpenAI's chat-completion request (input
list of ``Message``, stream flag, tools, sampling params). We keep
``extra="allow"`` so unknown fields from existing callers and on-disk
fixtures don't raise.
"""
model_config = ConfigDict(extra="allow")
input: List[Message] = Field(default_factory=list)
session_id: Optional[str] = None
user_id: Optional[str] = None
stream: bool = True
metadata: Optional[Dict[str, Any]] = None
class AgentResponse(BaseModel):
model_config = ConfigDict(extra="allow")
id: Optional[str] = None
output: List[Message] = Field(default_factory=list)
status: RunStatus = RunStatus.Completed
created_at: Optional[str] = None
completed_at: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
__all__ = [
"AgentRequest",
"AgentResponse",
"AudioContent",
"Content",
"ContentType",
"DataContent",
"Event",
"FileContent",
"FunctionCall",
"FunctionCallOutput",
"ImageContent",
"Message",
"MessageType",
"RefusalContent",
"Role",
"RunStatus",
"TextContent",
"VideoContent",
]