gwdoc/workflows/history_manager.py.bak

218 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
统一历史对话管理器
整合所有分散的 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 = 10,
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 = 10,
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)