message / plugins /json /openai_parser.py
hunian
refactor(plugins): 插件短名并统一 MCP tool 为 {plugin}-{tool}
cc826a1
Raw
History Blame Contribute Delete
10.3 kB
"""
OpenAI 对话格式解析器
支持 OpenAI messages、content parts、tool calls/results 的结构化解析。
"""
import json
import logging
import uuid
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger(__name__)
class OpenAIMessageParser:
"""OpenAI 对话格式解析器"""
def parse_json(self, json_data: Any) -> Dict[str, Any]:
"""
解析 JSON 数据为对话视图模型
Args:
json_data: JSON 数据(dict 或 list)
Returns:
对话视图模型 {
"conversation_id": str,
"messages": List[dict],
"raw_warnings": List[str],
}
"""
conversation_id = str(uuid.uuid4())[:8]
messages = []
raw_warnings = []
# 处理不同的输入格式
if isinstance(json_data, dict):
# 检查是否有 messages 字段
if "messages" in json_data:
conversation_id = json_data.get("conversation_id", conversation_id)
raw_messages = json_data["messages"]
if isinstance(raw_messages, list):
messages, raw_warnings = self._parse_messages(raw_messages)
else:
raw_warnings.append("messages 字段不是数组")
else:
# 尝试将整个 dict 作为单条消息解析
msg, warnings = self._parse_single_message(json_data, 0)
if msg:
messages.append(msg)
raw_warnings.extend(warnings)
elif isinstance(json_data, list):
# 直接作为消息列表解析
messages, raw_warnings = self._parse_messages(json_data)
else:
raw_warnings.append(f"不支持的 JSON 类型: {type(json_data).__name__}")
return {
"conversation_id": conversation_id,
"messages": messages,
"raw_warnings": raw_warnings,
}
def parse_jsonl(self, text: str) -> Dict[str, Any]:
"""
解析 JSONL 格式
Args:
text: JSONL 文本
Returns:
对话视图模型
"""
conversation_id = str(uuid.uuid4())[:8]
raw_messages = []
raw_warnings = []
for line_num, line in enumerate(text.strip().split("\n"), 1):
line = line.strip()
if not line:
continue
try:
data = json.loads(line)
raw_messages.append(data)
except json.JSONDecodeError as e:
raw_warnings.append(f"第 {line_num} 行 JSON 解析失败: {e}")
messages, warnings = self._parse_messages(raw_messages)
raw_warnings.extend(warnings)
return {
"conversation_id": conversation_id,
"messages": messages,
"raw_warnings": raw_warnings,
}
def _parse_messages(self, raw_messages: List[Any]) -> Tuple[List[dict], List[str]]:
"""
解析消息列表
Args:
raw_messages: 原始消息列表
Returns:
(解析后的消息列表, 警告列表)
"""
messages = []
warnings = []
for idx, raw_msg in enumerate(raw_messages):
if not isinstance(raw_msg, dict):
warnings.append(f"消息 {idx} 不是对象,跳过")
continue
msg, msg_warnings = self._parse_single_message(raw_msg, idx)
if msg:
messages.append(msg)
warnings.extend(msg_warnings)
return messages, warnings
def _parse_single_message(self, raw_msg: dict, index: int) -> Tuple[Optional[dict], List[str]]:
"""
解析单条消息
Args:
raw_msg: 原始消息
index: 消息索引
Returns:
(解析后的消息, 警告列表)
"""
warnings = []
# 提取基本字段
role = raw_msg.get("role", "unknown")
msg_id = raw_msg.get("id", f"msg_{index}")
# 解析 content
content_parts, content_warnings = self._parse_content(raw_msg.get("content"))
warnings.extend(content_warnings)
# 解析 tool_calls
tool_calls = self._parse_tool_calls(raw_msg.get("tool_calls"))
# 解析 tool results (从 tool_call_id 字段判断)
tool_results = self._parse_tool_results(raw_msg)
# 提取 metadata
metadata = {}
for key in ["name", "function_call", "tool_call_id", "finish_reason", "usage"]:
if key in raw_msg:
metadata[key] = raw_msg[key]
return {
"id": msg_id,
"role": role,
"content_parts": content_parts,
"tool_calls": tool_calls,
"tool_results": tool_results,
"metadata": metadata,
"raw_index": index,
}, warnings
def _parse_content(self, content: Any) -> Tuple[List[dict], List[str]]:
"""
解析 content 字段
Args:
content: content 字段值
Returns:
(content parts 列表, 警告列表)
"""
parts = []
warnings = []
if content is None:
return parts, warnings
if isinstance(content, str):
# 纯文本
if content.strip():
parts.append({
"type": "text",
"text": content.strip(),
})
elif isinstance(content, list):
# 数组格式(OpenAI 多模态)
for item in content:
if isinstance(item, dict):
part, item_warnings = self._parse_content_part(item)
if part:
parts.append(part)
warnings.extend(item_warnings)
elif isinstance(item, str):
if item.strip():
parts.append({
"type": "text",
"text": item.strip(),
})
else:
warnings.append(f"不支持的 content 类型: {type(content).__name__}")
return parts, warnings
def _parse_content_part(self, item: dict) -> Tuple[Optional[dict], List[str]]:
"""
解析单个 content part
Args:
item: content part 对象
Returns:
(解析后的 part, 警告列表)
"""
warnings = []
item_type = item.get("type", "")
if item_type == "text":
text = item.get("text", "")
if text.strip():
return {
"type": "text",
"text": text.strip(),
}, warnings
elif item_type == "thinking":
thinking = item.get("thinking", "")
if thinking.strip():
return {
"type": "thinking",
"text": thinking.strip(),
}, warnings
elif item_type == "image_url":
image_url = item.get("image_url", {})
url = image_url.get("url", "") if isinstance(image_url, dict) else ""
return {
"type": "image",
"url": url,
}, warnings
elif item_type == "tool_use":
# Anthropic 格式的 tool_use
return {
"type": "tool_use",
"tool_name": item.get("name", "未知工具"),
"tool_input": item.get("input", {}),
"tool_use_id": item.get("id", ""),
}, warnings
elif item_type == "tool_result":
# Anthropic 格式的 tool_result
result_content = item.get("content", "")
is_error = item.get("is_error", False)
return {
"type": "tool_result",
"tool_use_id": item.get("tool_use_id", ""),
"content": result_content,
"is_error": is_error,
}, warnings
else:
warnings.append(f"不支持的 content part 类型: {item_type}")
return None, warnings
def _parse_tool_calls(self, tool_calls: Any) -> List[dict]:
"""
解析 tool_calls 字段(OpenAI 格式)
Args:
tool_calls: tool_calls 字段值
Returns:
解析后的 tool calls 列表
"""
if not tool_calls or not isinstance(tool_calls, list):
return []
result = []
for tc in tool_calls:
if not isinstance(tc, dict):
continue
tc_id = tc.get("id", "")
tc_type = tc.get("type", "function")
function = tc.get("function", {})
if isinstance(function, dict):
func_name = function.get("name", "")
func_args = function.get("arguments", "")
# 尝试解析 arguments JSON
if isinstance(func_args, str):
try:
func_args = json.loads(func_args)
except json.JSONDecodeError:
pass
result.append({
"id": tc_id,
"type": tc_type,
"function_name": func_name,
"function_arguments": func_args,
})
return result
def _parse_tool_results(self, raw_msg: dict) -> List[dict]:
"""
解析 tool results(从消息中提取)
Args:
raw_msg: 原始消息
Returns:
tool results 列表
"""
results = []
# OpenAI 格式:tool_call_id 字段
if "tool_call_id" in raw_msg:
results.append({
"tool_call_id": raw_msg["tool_call_id"],
"content": raw_msg.get("content", ""),
"is_error": False,
})
return results
# 全局单例
_parser: Optional[OpenAIMessageParser] = None
def get_openai_parser() -> OpenAIMessageParser:
"""获取 OpenAI 对话解析器全局单例"""
global _parser
if _parser is None:
_parser = OpenAIMessageParser()
return _parser