Spaces:
Paused
Paused
| """OpenAI 兼容的请求/响应模型。""" | |
| from typing import Any | |
| from pydantic import BaseModel, Field | |
| from core.api.conv_parser import strip_session_id_suffix | |
| class OpenAIContentPart(BaseModel): | |
| type: str | |
| text: str | None = None | |
| image_url: dict[str, Any] | str | None = None | |
| class InputAttachment(BaseModel): | |
| filename: str | |
| mime_type: str | |
| data: bytes | |
| class OpenAIMessage(BaseModel): | |
| role: str = Field(..., description="system | user | assistant | tool") | |
| content: str | list[OpenAIContentPart] | None = "" | |
| tool_calls: list[dict[str, Any]] | None = Field( | |
| default=None, description="assistant 发起的工具调用" | |
| ) | |
| tool_call_id: str | None = Field( | |
| default=None, description="tool 消息对应的 call id" | |
| ) | |
| model_config = {"extra": "allow"} | |
| class OpenAIChatRequest(BaseModel): | |
| """OpenAI Chat Completions API 兼容请求体。""" | |
| model: str = Field(default="", description="模型名,可忽略") | |
| messages: list[OpenAIMessage] = Field(..., description="对话列表") | |
| stream: bool = Field(default=False, description="是否流式返回") | |
| tools: list[dict] | None = Field( | |
| default=None, | |
| description='工具列表,每项为 {"type":"function","function":{name,description,parameters,strict?}}', | |
| ) | |
| tool_choice: str | dict | None = Field( | |
| default=None, | |
| description='工具选择: "auto"|"required"|"none" 或 {"type":"function","name":"xxx"}', | |
| ) | |
| parallel_tool_calls: bool | None = Field( | |
| default=None, | |
| description="是否允许单次响应中并行多个 tool_call,false 时仅 0 或 1 个", | |
| ) | |
| resume_session_id: str | None = Field(default=None, exclude=True) | |
| upstream_model: str | None = Field(default=None, exclude=True) | |
| attachment_files: list[InputAttachment] = Field( | |
| default_factory=list, | |
| exclude=True, | |
| description="本次实际要发送给站点的附件,由 ChatHandler 根据 full_history 选择来源填充。", | |
| ) | |
| # 仅供内部调度使用:最后一条 user 消息里的附件 & 所有 user 消息里的附件 | |
| attachment_files_last_user: list[InputAttachment] = Field( | |
| default_factory=list, exclude=True | |
| ) | |
| attachment_files_all_users: list[InputAttachment] = Field( | |
| default_factory=list, exclude=True | |
| ) | |
| def _norm_content(c: str | list[OpenAIContentPart] | None) -> str: | |
| """将 content 转为单段字符串。仅支持官方格式:字符串或 type=text 的 content part(取 text 字段)。""" | |
| if c is None: | |
| return "" | |
| if isinstance(c, str): | |
| return strip_session_id_suffix(c) | |
| if not isinstance(c, list): | |
| return "" | |
| return strip_session_id_suffix( | |
| " ".join( | |
| p.text or "" | |
| for p in c | |
| if isinstance(p, OpenAIContentPart) and p.type == "text" and p.text | |
| ) | |
| ) | |
| REACT_STRICT_SUFFIX = ( | |
| "(严格 ReAct 执行模式;禁止输出「无法执行工具所以直接给方案」等解释或替代内容)" | |
| ) | |
| def extract_user_content( | |
| messages: list[OpenAIMessage], | |
| *, | |
| has_tools: bool = False, | |
| react_prompt_prefix: str = "", | |
| full_history: bool = False, | |
| ) -> str: | |
| """ | |
| 从 messages 中提取对话,拼成发给模型的 prompt。 | |
| 网页/会话侧已有完整历史,只取尾部:最后一条为 user 时,从后向前找到最后一个 assistant(不包含), | |
| 取该 assistant 之后到末尾;最后一条为 tool 时,从后向前找到最后一个 user(不包含),取该 user 之后到末尾。 | |
| 支持 user、assistant、tool 角色;assistant 的 tool_calls 与 tool 结果会拼回。 | |
| ReAct 模式:完整 ReAct Prompt 仅第一次对话传入(按完整 messages 判断 is_first_turn);后续只传尾部内容。 | |
| """ | |
| if not messages: | |
| return "" | |
| parts: list[str] = [] | |
| # 重建会话时会把完整历史重新回放给站点,因此 tools 指令也需要重新注入。 | |
| is_first_turn = not any(m.role in ("assistant", "tool") for m in messages) | |
| if has_tools and react_prompt_prefix and (full_history or is_first_turn): | |
| parts.append(react_prompt_prefix) | |
| if full_history: | |
| tail = messages | |
| else: | |
| last = messages[-1] | |
| if last.role == "user": | |
| i = len(messages) - 1 | |
| while i >= 0 and messages[i].role != "assistant": | |
| i -= 1 | |
| tail = messages[i + 1 :] | |
| elif last.role == "tool": | |
| i = len(messages) - 1 | |
| while i >= 0 and messages[i].role != "user": | |
| i -= 1 | |
| tail = messages[i + 1 :] | |
| else: | |
| tail = messages[-2:] | |
| for m in tail: | |
| if m.role == "system": | |
| txt = _norm_content(m.content) | |
| if txt: | |
| parts.append(f"System:{txt}") | |
| elif m.role == "user": | |
| txt = _norm_content(m.content) | |
| if txt: | |
| if has_tools: | |
| parts.append(f"**User**: {txt} {REACT_STRICT_SUFFIX}") | |
| else: | |
| parts.append(f"User:{txt}") | |
| elif m.role == "assistant": | |
| tool_calls_list = list(m.tool_calls or []) | |
| if tool_calls_list: | |
| for tc in tool_calls_list: | |
| fn = tc.get("function") or {} | |
| call_id = tc.get("id", "") | |
| name = fn.get("name", "") | |
| args = fn.get("arguments", "{}") | |
| parts.append( | |
| f"**Assistant**:\n\n```\nAction: {name}\nAction Input: {args}\nCall ID: {call_id}\n```" | |
| ) | |
| else: | |
| txt = _norm_content(m.content) | |
| if txt: | |
| if has_tools: | |
| parts.append(f"**Assistant**:\n\n{txt}") | |
| else: | |
| parts.append(f"Assistant:{txt}") | |
| elif m.role == "tool": | |
| txt = _norm_content(m.content) | |
| call_id = m.tool_call_id or "" | |
| parts.append( | |
| f"**Observation(Call ID: {call_id})**: {txt}\n\n请根据以上观察结果继续。如需调用工具,输出 Thought / Action / Action Input;若任务已完成,输出 Final Answer。" | |
| ) | |
| return "\n".join(parts) | |