添加测试、添加总结、全量、rag(todo)3种历史记录保存方式,流式实现

This commit is contained in:
2026-05-05 03:01:20 +08:00
parent 2050a30a52
commit adb59da06d
46 changed files with 4484 additions and 882 deletions

View File

@@ -123,28 +123,7 @@ class ChatWorkflowService:
print(f"[TableCondition] 解析条件失败: {condition}, 错误: {e}")
return False
def _check_keyword_trigger(self, keywords: List[str], text: str) -> bool:
"""
检查关键词触发
Args:
keywords: 关键词列表(支持正则)
text: 要检查的文本
Returns:
bool: 是否匹配
"""
for keyword in keywords:
try:
# 尝试作为正则表达式匹配
if re.search(keyword, text, re.IGNORECASE):
return True
except re.error:
# 如果不是合法正则,则进行普通字符串匹配
if keyword.lower() in text.lower():
return True
return False
async def process_chat_request(
self,
request_data: Dict[str, Any]
@@ -315,15 +294,25 @@ class ChatWorkflowService:
"""
normalized = entry_data.copy()
# ✅ content 必须是字符串(如果是字典或列表,转换为 JSON 字符串)
if 'content' in normalized:
content = normalized['content']
if not isinstance(content, str):
import json
normalized['content'] = json.dumps(content, ensure_ascii=False)
# uid 必须是字符串
if 'uid' in normalized and not isinstance(normalized['uid'], str):
normalized['uid'] = str(normalized['uid'])
# position 必须是字符串或 None
# position 必须是整数或 None
if 'position' in normalized:
pos = normalized['position']
if pos is not None and not isinstance(pos, str):
normalized['position'] = str(pos)
if pos is not None:
try:
normalized['position'] = int(pos)
except (ValueError, TypeError):
normalized['position'] = 0
# group 必须是列表或 None
if 'group' in normalized:
@@ -340,6 +329,68 @@ class ChatWorkflowService:
return normalized
def _get_trigger_type(self, entry_data: Dict[str, Any]) -> str:
"""
获取条目的触发类型
Args:
entry_data: 世界书条目数据
Returns:
str: 触发类型 ('constant', 'keyword', 'condition', 'rag', 'unknown')
"""
trigger_config = entry_data.get("trigger_config", {})
triggers = trigger_config.get("triggers", {})
# 检查常驻触发
constant_trigger = triggers.get("constant", [False, None])
if constant_trigger[0]:
return "constant"
# 检查关键词触发
keyword_trigger = triggers.get("keyword", [False, None])
if keyword_trigger[0]:
return "keyword"
# 检查条件触发
condition_trigger = triggers.get("condition", [False, None])
if condition_trigger[0]:
return "condition"
# 检查RAG触发
rag_trigger = triggers.get("rag", [False, None])
if rag_trigger[0]:
return "rag"
return "unknown"
def _get_position_label(self, position) -> str:
"""
获取位置的中文标签
Args:
position: 位置编号 (0-7)
Returns:
str: 位置标签
"""
position_labels = {
0: '角色定义之后',
1: '角色定义之前',
2: '示例对话之前',
3: '示例对话之后',
4: '系统提示/作者注释',
5: '作为系统消息',
6: '深度插入',
7: '宏替换'
}
try:
pos_int = int(position) if position is not None else 0
return position_labels.get(pos_int, f'未知位置({position})')
except (ValueError, TypeError):
return f'未知位置({position})'
async def _collect_and_activate_worldbooks(
self,
request_data: Dict[str, Any],
@@ -359,7 +410,7 @@ class ChatWorkflowService:
character: 角色卡对象
Returns:
List[WorldInfoEntry]: 激活的条目列表
List[WorldInfoEntry]: 激活的条目列表(已按位置和顺序排序)
"""
try:
from backend.models.internal import WorldInfoEntry
@@ -376,16 +427,31 @@ class ChatWorkflowService:
world_book_data = request_data.get("worldBookData", {})
if not world_book_data:
print(f"[WorldBook] ⚠️ 未收到 worldBookData")
return active_entries
# ✅ 详细日志:检查接收到的世界书数据
global_books = world_book_data.get("globalBooks", [])
character_book_id = world_book_data.get("characterBookId") or (character.worldInfoId if character else None)
print(f"[WorldBook] 📋 接收到的世界书数据:")
print(f" - 全局世界书数量: {len(global_books)}")
for wb in global_books:
print(f" * {wb.get('name')}")
print(f" - 角色绑定世界书: {character_book_id}")
if not global_books and not character_book_id:
print(f"[WorldBook] ⚠️ 既没有全局世界书,也没有角色绑定世界书")
return active_entries
# === 1. 加载全局世界书 ===
global_books = world_book_data.get("globalBooks", [])
for wb in global_books:
try:
wb_name = wb.get("name")
if wb_name:
wb_data = self.worldbook_service._load_worldbook(wb_name)
if wb_data and "entries" in wb_data:
print(f"[WorldBook] 📖 加载全局世界书 '{wb_name}',共 {len(wb_data['entries'])} 个条目")
for entry_data in wb_data["entries"]:
# 检查是否禁用
if entry_data.get("disable", False):
@@ -399,18 +465,21 @@ class ChatWorkflowService:
try:
entry = WorldInfoEntry(**normalized_entry)
active_entries.append(entry)
print(f"[WorldBook] 全局世界书 '{wb_name}' 条目 {entry_data.get('uid')} 已激活")
trigger_type = self._get_trigger_type(normalized_entry)
position_label = self._get_position_label(normalized_entry.get("position", 0))
print(f"[WorldBook] ✅ 全局世界书 '{wb_name}' 条目激活 | UID: {entry_data.get('uid')} | 触发: {trigger_type} | 位置: {position_label}")
except Exception as validation_error:
print(f"[WorldBook] ⚠️ 条目验证失败: {entry_data.get('uid')}, 错误: {validation_error}")
except Exception as e:
print(f"[WorldBook] 加载全局世界书失败: {wb.get('name')}, 错误: {e}")
print(f"[WorldBook] 加载全局世界书失败: {wb.get('name')}, 错误: {e}")
# === 2. 加载角色绑定的世界书 ===
character_book_id = character.worldInfoId
if character_book_id:
print(f"[WorldBook] 📖 开始加载角色绑定世界书: {character_book_id}")
try:
wb_data = self.worldbook_service._load_worldbook(character_book_id)
if wb_data and "entries" in wb_data:
print(f"[WorldBook] ✅ 成功加载世界书 '{character_book_id}',共 {len(wb_data['entries'])} 个条目")
for entry_data in wb_data["entries"]:
if entry_data.get("disable", False):
continue
@@ -422,16 +491,42 @@ class ChatWorkflowService:
try:
entry = WorldInfoEntry(**normalized_entry)
active_entries.append(entry)
print(f"[WorldBook] 角色世界书 '{character_book_id}' 条目 {entry_data.get('uid')} 已激活")
trigger_type = self._get_trigger_type(normalized_entry)
position_label = self._get_position_label(normalized_entry.get("position", 0))
print(f"[WorldBook] ✅ 角色世界书 '{character_book_id}' 条目激活 | UID: {entry_data.get('uid')} | 触发: {trigger_type} | 位置: {position_label}")
except Exception as validation_error:
print(f"[WorldBook] ⚠️ 条目验证失败: {entry_data.get('uid')}, 错误: {validation_error}")
else:
print(f"[WorldBook] ⚠️ 世界书 '{character_book_id}' 不存在或无条目")
except Exception as e:
print(f"[WorldBook] 加载角色世界书失败: {character_book_id}, 错误: {e}")
print(f"[WorldBook] 加载角色世界书失败: {character_book_id}, 错误: {e}")
else:
print(f"[WorldBook] 角色未绑定世界书")
# === 3. 按位置和顺序排序 ===
active_entries.sort(key=lambda x: (x.position or 0, x.order or 0))
print(f"[WorldBook] 总共激活 {len(active_entries)} 个条目")
# === 4. 统计激活条目的分布情况 ===
position_stats = {}
trigger_stats = {}
for entry in active_entries:
pos = entry.position or 0
position_stats[pos] = position_stats.get(pos, 0) + 1
trigger_type = self._get_trigger_type(entry.model_dump())
trigger_stats[trigger_type] = trigger_stats.get(trigger_type, 0) + 1
print(f"\n[WorldBook] 📊 激活统计:")
print(f" - 总激活条目数: {len(active_entries)}")
print(f" - 按位置分布:")
for pos, count in sorted(position_stats.items()):
pos_label = self._get_position_label(pos)
print(f" * {pos_label} (pos={pos}): {count}")
print(f" - 按触发类型分布:")
for trigger_type, count in sorted(trigger_stats.items()):
print(f" * {trigger_type}: {count}")
print()
return active_entries
def _check_entry_activation(
@@ -456,23 +551,28 @@ class ChatWorkflowService:
trigger_config = entry_data.get("trigger_config", {})
triggers = trigger_config.get("triggers", {})
# 1. 常驻触发 (constant)
# 1. 常驻触发 (constant) - 总是激活
constant_trigger = triggers.get("constant", [False, None])
if constant_trigger[0]: # [true, null]
print(f"[WorldBook-Check] 📌 常驻触发 | UID: {entry_data.get('uid')}")
return True
# 2. 关键词触发 (keyword)
# 2. 关键词触发 (keyword) - 检查用户输入和聊天历史
keyword_trigger = triggers.get("keyword", [False, None])
if keyword_trigger[0] and keyword_trigger[1]:
keywords = keyword_trigger[1].get("keys", [])
if keywords:
keyword_config = keyword_trigger[1]
keys = keyword_config.get("key", []) or keyword_config.get("keys", [])
if keys:
# 检查用户输入
if self._check_keyword_trigger(keywords, user_message):
if self._check_keyword_trigger_with_config(keys, user_message, keyword_config):
print(f"[WorldBook-Check] 🔑 关键词触发(用户输入) | UID: {entry_data.get('uid')} | 匹配关键词: {keys}")
return True
# 检查聊天历史
for history_text in chat_history:
if self._check_keyword_trigger(keywords, history_text):
for i, history_text in enumerate(chat_history):
if self._check_keyword_trigger_with_config(keys, history_text, keyword_config):
print(f"[WorldBook-Check] 🔑 关键词触发(历史消息#{i}) | UID: {entry_data.get('uid')} | 匹配关键词: {keys}")
return True
# 3. 条件触发 (condition) - 支持动态表格语法
@@ -481,19 +581,68 @@ class ChatWorkflowService:
conditions = condition_trigger[1].get("conditions", [])
if conditions:
# 所有条件都必须满足 (AND逻辑)
all_conditions_met = True
for condition in conditions:
cond_text = condition.get("text", "")
if cond_text:
# 解析条件,如 (tb.力量 > 10)
if not self._parse_table_condition(cond_text, table_data):
return False
return True
all_conditions_met = False
break
if all_conditions_met:
print(f"[WorldBook-Check] ⚙️ 条件触发 | UID: {entry_data.get('uid')} | 条件数: {len(conditions)}")
return True
# 4. RAG触发 (暂不实现)
# rag_trigger = triggers.get("rag", [False, None])
return False
def _check_keyword_trigger_with_config(self, keywords: List[str], text: str, config: Dict[str, Any] = None) -> bool:
"""
检查文本中是否包含关键词(带配置)
Args:
keywords: 关键词列表
text: 要检查的文本
config: 关键词配置(可选)
Returns:
bool: 是否匹配
"""
if not keywords or not text:
return False
config = config or {}
case_sensitive = config.get("caseSensitive", False)
match_whole_words = config.get("matchWholeWords", False)
# 处理大小写
if not case_sensitive:
text_lower = text.lower()
keywords_lower = [kw.lower() for kw in keywords]
else:
text_lower = text
keywords_lower = keywords
# 检查每个关键词
for keyword in keywords_lower:
if not keyword:
continue
if match_whole_words:
# 全词匹配:使用正则表达式
pattern = r'\b' + re.escape(keyword) + r'\b'
if re.search(pattern, text_lower):
return True
else:
# 部分匹配:直接检查子串
if keyword in text_lower:
return True
return False
async def _load_chat_history(
self,
role_name: str,
@@ -851,7 +1000,7 @@ class ChatWorkflowService:
)
print(f"\n[ChatWorkflow-Stream] 📚 世界书激活结果: {len(active_entries)} 个条目")
for i, entry in enumerate(active_entries, 1):
print(f" {i}. {entry.get('name', 'N/A')} (UID: {entry.get('uid', 'N/A')})")
print(f" {i}. {getattr(entry, 'name', 'N/A')} (UID: {getattr(entry, 'uid', 'N/A')})")
# === 第4步加载聊天历史 ===
chat_history = await self._load_chat_history(current_role, current_chat)
@@ -900,7 +1049,7 @@ class ChatWorkflowService:
print(f"[ChatWorkflow-Stream] 📡 正在调用 llm_client.stream_chat()...")
try:
async for chunk in self.llm_client.stream_chat(
async for chunk_dict in self.llm_client.stream_chat(
messages=prompt_messages,
api_url=api_config.get("api_url", ""),
api_key=api_config.get("api_key", ""),
@@ -909,7 +1058,22 @@ class ChatWorkflowService:
max_tokens=preset_config.get("parameters", {}).get("max_tokens", 30000),
request_timeout=preset_config.get("parameters", {}).get("request_timeout", 60) # ✅ 传递超时时间
):
generated_content += chunk
# ✅ 处理 LLM 返回的字典格式数据
if isinstance(chunk_dict, dict):
if chunk_dict.get("type") == "chunk":
chunk_content = chunk_dict.get("content", "")
elif chunk_dict.get("type") == "usage":
# 跳过 usage 信息,不拼接到内容中
continue
else:
# 兼容旧格式或未知类型,尝试直接获取 content
chunk_content = chunk_dict.get("content", str(chunk_dict))
else:
# 如果直接返回字符串(兼容情况)
chunk_content = str(chunk_dict)
# ✅ 拼接纯文本内容
generated_content += chunk_content
chunk_count += 1
# 第一个 chunk 到达时记录
@@ -922,8 +1086,8 @@ class ChatWorkflowService:
elapsed = time.time() - start_time
print(f"[ChatWorkflow-Stream] 📊 已接收 {chunk_count} 个 chunks, 当前长度: {len(generated_content)}, 耗时: {elapsed:.2f}s")
# 调用回调函数发送 chunk
await on_chunk(chunk)
# 调用回调函数发送纯文本 chunk
await on_chunk(chunk_content)
except Exception as stream_error:
elapsed = time.time() - start_time