218 lines
7.7 KiB
Python
218 lines
7.7 KiB
Python
"""
|
||
统一历史对话管理器
|
||
整合所有分散的 parse_history / _parse_history / build_history_str / filter_image_urls 逻辑
|
||
"""
|
||
|
||
import json
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
|
||
class HistoryManager:
|
||
"""统一的历史对话管理器,提供解析、截断、格式化、过滤等功能"""
|
||
|
||
def __init__(
|
||
self,
|
||
max_messages: int = 8,
|
||
max_assistant_chars: int = 300,
|
||
):
|
||
"""
|
||
Args:
|
||
max_messages: 保留的最近消息条数
|
||
max_assistant_chars: assistant 角色内容截断字符数
|
||
"""
|
||
self.max_messages = max_messages
|
||
self.max_assistant_chars = max_assistant_chars
|
||
|
||
# ==================== 解析 ====================
|
||
|
||
def parse(self, history_message: Any) -> List[Dict[str, Any]]:
|
||
"""
|
||
解析历史消息为列表,保留最近 max_messages 条。
|
||
兼容字符串和列表输入,替代所有 parse_history / _parse_history。
|
||
"""
|
||
if not history_message:
|
||
return []
|
||
try:
|
||
data = json.loads(history_message) if isinstance(history_message, str) else history_message
|
||
return data[-self.max_messages:] if isinstance(data, list) else []
|
||
except Exception:
|
||
return []
|
||
|
||
# ==================== 截断 ====================
|
||
|
||
def truncate(self, messages: List[Dict[str, Any]], keep_recent: Optional[int] = None) -> List[Dict[str, Any]]:
|
||
"""按数量截断,保留最近 keep_recent 条"""
|
||
if not messages:
|
||
return []
|
||
n = keep_recent if keep_recent is not None else self.max_messages
|
||
return messages[-n:]
|
||
|
||
# ==================== 格式化 ====================
|
||
|
||
def to_prompt_str(
|
||
self,
|
||
messages: List[Dict[str, Any]],
|
||
recent_count: Optional[int] = None,
|
||
assistant_truncate: Optional[int] = None,
|
||
) -> str:
|
||
"""
|
||
格式化为 "用户: xxx\\n助手: xxx" 纯文本字符串,用于 prompt 注入。
|
||
替代 build_history_str。
|
||
"""
|
||
if not messages:
|
||
return ""
|
||
n = recent_count if recent_count is not None else self.max_messages
|
||
trunc = assistant_truncate if assistant_truncate is not None else self.max_assistant_chars
|
||
recent = messages[-n:]
|
||
parts = []
|
||
for m in recent:
|
||
if isinstance(m, dict):
|
||
role = m.get("role", "")
|
||
content = (m.get("content") or "").strip()
|
||
if role == "user":
|
||
parts.append(f"用户: {content}")
|
||
elif role == "assistant":
|
||
parts.append(f"助手: {content[:trunc]}")
|
||
return "\n".join(parts) if parts else ""
|
||
|
||
def to_openai_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||
"""
|
||
格式化为 OpenAI messages 列表,直接传入 LLM。
|
||
用于百科问答等需要完整消息格式的场景。
|
||
"""
|
||
if not messages:
|
||
return []
|
||
result = []
|
||
for m in messages:
|
||
if isinstance(m, dict) and m.get("role") and m.get("content"):
|
||
result.append({"role": m["role"], "content": m["content"]})
|
||
return result
|
||
|
||
# ==================== 请求预处理 ====================
|
||
|
||
def preprocess_from_request(self, history_message: Any, exclude_last: bool = True) -> str:
|
||
"""
|
||
处理前端传入的历史消息:解析、移除当前用户消息、序列化。
|
||
对应 app.py 中 history_message 预处理逻辑。
|
||
|
||
Args:
|
||
history_message: 前端传入的历史消息,支持 str 或 list
|
||
exclude_last: 是否移除最后一条(当前用户消息),默认 True
|
||
|
||
Returns:
|
||
处理后的 JSON 字符串
|
||
"""
|
||
if not history_message:
|
||
return ""
|
||
try:
|
||
data = json.loads(history_message) if isinstance(history_message, str) else history_message
|
||
except (json.JSONDecodeError, TypeError):
|
||
return ""
|
||
if not isinstance(data, list):
|
||
return ""
|
||
if exclude_last and data:
|
||
data = data[:-1]
|
||
return json.dumps(data, ensure_ascii=False)
|
||
|
||
# ==================== 状态初始化 ====================
|
||
|
||
def init_history(self, history_message: Any) -> Dict[str, Any]:
|
||
"""
|
||
一步完成历史消息解析,返回 history_message(str) + history_messages(list) 双字段。
|
||
用于各工作流入口初始化 state,消除重复的 parse_history 调用。
|
||
|
||
Args:
|
||
history_message: 历史消息,支持 str 或 list
|
||
|
||
Returns:
|
||
{"history_message": str, "history_messages": list}
|
||
"""
|
||
parsed = self.parse(history_message)
|
||
raw = json.dumps(parsed, ensure_ascii=False) if parsed else ""
|
||
return {"history_message": raw, "history_messages": parsed}
|
||
|
||
# ==================== 过滤 ====================
|
||
|
||
@staticmethod
|
||
def filter_image_urls(history_message: Any) -> Any:
|
||
"""
|
||
过滤历史消息中的图片URL,将多模态内容转为纯文本。
|
||
替代 main_agent.filter_image_urls。
|
||
"""
|
||
if not history_message:
|
||
return history_message
|
||
|
||
try:
|
||
history_messages = json.loads(history_message) if isinstance(history_message, str) else history_message
|
||
except (json.JSONDecodeError, TypeError):
|
||
return history_message
|
||
|
||
if not isinstance(history_messages, list):
|
||
return history_messages
|
||
|
||
filtered_messages = []
|
||
for msg in history_messages:
|
||
if not isinstance(msg, dict):
|
||
filtered_messages.append(msg)
|
||
continue
|
||
|
||
new_msg = msg.copy()
|
||
content = msg.get("content")
|
||
|
||
if isinstance(content, list):
|
||
text_parts = []
|
||
for content_item in content:
|
||
if isinstance(content_item, dict):
|
||
if content_item.get("type") == "text":
|
||
text_value = content_item.get("text", "")
|
||
if text_value:
|
||
text_parts.append(text_value)
|
||
elif isinstance(content_item, str):
|
||
text_parts.append(content_item)
|
||
|
||
if text_parts:
|
||
new_msg["content"] = "".join(text_parts)
|
||
filtered_messages.append(new_msg)
|
||
else:
|
||
filtered_messages.append(new_msg)
|
||
|
||
return filtered_messages
|
||
|
||
|
||
# ==================== 模块级便捷实例 ====================
|
||
# 提供与旧函数签名兼容的模块级函数,方便渐进式迁移
|
||
|
||
_default_manager = HistoryManager()
|
||
|
||
|
||
def parse_history(history_message: Any) -> List[Dict[str, Any]]:
|
||
"""兼容旧调用的模块级函数"""
|
||
return _default_manager.parse(history_message)
|
||
|
||
|
||
def build_history_str(
|
||
history_messages: List[Dict[str, Any]],
|
||
recent_count: int = 4,
|
||
assistant_truncate: int = 300,
|
||
) -> str:
|
||
"""兼容旧调用的模块级函数"""
|
||
mgr = HistoryManager(max_messages=recent_count, max_assistant_chars=assistant_truncate)
|
||
return mgr.to_prompt_str(history_messages, recent_count=recent_count, assistant_truncate=assistant_truncate)
|
||
|
||
|
||
def filter_image_urls(history_message: Any) -> Any:
|
||
"""兼容旧调用的模块级函数"""
|
||
return HistoryManager.filter_image_urls(history_message)
|
||
|
||
|
||
def preprocess_from_request(history_message: Any, exclude_last: bool = True) -> str:
|
||
"""兼容旧调用的模块级函数"""
|
||
return _default_manager.preprocess_from_request(history_message, exclude_last=exclude_last)
|
||
|
||
|
||
def init_history(history_message: Any) -> Dict[str, Any]:
|
||
"""兼容旧调用的模块级函数"""
|
||
return _default_manager.init_history(history_message)
|
||
|
||
|