This commit is contained in:
Windpicker-owo
2025-11-12 13:38:12 +08:00
36 changed files with 934 additions and 626 deletions

View File

@@ -161,16 +161,16 @@ class EmbeddingStore:
# 限制 chunk_size 和 max_workers 在合理范围内
chunk_size = max(MIN_CHUNK_SIZE, min(chunk_size, MAX_CHUNK_SIZE))
max_workers = max(MIN_WORKERS, min(max_workers, MAX_WORKERS))
semaphore = asyncio.Semaphore(max_workers)
llm = LLMRequest(model_set=model_config.model_task_config.embedding, request_type="embedding")
results = {}
# 将字符串列表分成多个 chunk
chunks = []
for i in range(0, len(strs), chunk_size):
chunks.append(strs[i : i + chunk_size])
async def _process_chunk(chunk: list[str]):
"""处理一个 chunk 的字符串(批量获取 embedding"""
async with semaphore:
@@ -180,12 +180,12 @@ class EmbeddingStore:
embedding = await EmbeddingStore._get_embedding_async(llm, s)
embeddings.append(embedding)
results[s] = embedding
if progress_callback:
progress_callback(len(chunk))
return embeddings
# 并发处理所有 chunks
tasks = [_process_chunk(chunk) for chunk in chunks]
await asyncio.gather(*tasks)
@@ -418,22 +418,22 @@ class EmbeddingStore:
# 🔧 修复:检查所有 embedding 的维度是否一致
dimensions = [len(emb) for emb in array]
unique_dims = set(dimensions)
if len(unique_dims) > 1:
logger.error(f"检测到不一致的 embedding 维度: {unique_dims}")
logger.error(f"维度分布: {dict(zip(*np.unique(dimensions, return_counts=True)))}")
# 获取期望的维度(使用最常见的维度)
from collections import Counter
dim_counter = Counter(dimensions)
expected_dim = dim_counter.most_common(1)[0][0]
logger.warning(f"将使用最常见的维度: {expected_dim}")
# 过滤掉维度不匹配的 embedding
filtered_array = []
filtered_idx2hash = {}
skipped_count = 0
for i, emb in enumerate(array):
if len(emb) == expected_dim:
filtered_array.append(emb)
@@ -442,11 +442,11 @@ class EmbeddingStore:
skipped_count += 1
hash_key = self.idx2hash[str(i)]
logger.warning(f"跳过维度不匹配的 embedding: {hash_key}, 维度={len(emb)}, 期望={expected_dim}")
logger.warning(f"已过滤 {skipped_count} 个维度不匹配的 embedding")
array = filtered_array
self.idx2hash = filtered_idx2hash
if not array:
logger.error("过滤后没有可用的 embedding无法构建索引")
embedding_dim = expected_dim

View File

@@ -13,4 +13,4 @@ __all__ = [
"StreamLoopManager",
"message_manager",
"stream_loop_manager",
]
]

View File

@@ -82,7 +82,7 @@ class SingleStreamContextManager:
self.total_messages += 1
self.last_access_time = time.time()
# 如果使用了缓存系统,输出调试信息
if cache_enabled and self.context.is_cache_enabled:
if self.context.is_chatter_processing:

View File

@@ -111,9 +111,9 @@ class StreamLoopManager:
# 获取或创建该流的启动锁
if stream_id not in self._stream_start_locks:
self._stream_start_locks[stream_id] = asyncio.Lock()
lock = self._stream_start_locks[stream_id]
# 使用锁防止并发启动同一个流的多个循环任务
async with lock:
# 获取流上下文
@@ -148,7 +148,7 @@ class StreamLoopManager:
# 紧急取消
context.stream_loop_task.cancel()
await asyncio.sleep(0.1)
loop_task = asyncio.create_task(self._stream_loop_worker(stream_id), name=f"stream_loop_{stream_id}")
# 将任务记录到 StreamContext 中
@@ -252,7 +252,7 @@ class StreamLoopManager:
self.stats["total_process_cycles"] += 1
if success:
logger.info(f"✅ [流工作器] stream={stream_id[:8]}, 任务ID={task_id}, 处理成功")
# 🔒 处理成功后,等待一小段时间确保清理操作完成
# 这样可以避免在 chatter_manager 清除未读消息之前就进入下一轮循环
await asyncio.sleep(0.1)
@@ -382,7 +382,7 @@ class StreamLoopManager:
self.chatter_manager.process_stream_context(stream_id, context),
name=f"chatter_process_{stream_id}"
)
# 等待 chatter 任务完成
results = await chatter_task
success = results.get("success", False)
@@ -398,8 +398,8 @@ class StreamLoopManager:
else:
logger.warning(f"流处理失败: {stream_id} - {results.get('error_message', '未知错误')}")
return success
except asyncio.CancelledError:
return success
except asyncio.CancelledError:
if chatter_task and not chatter_task.done():
chatter_task.cancel()
raise
@@ -709,4 +709,4 @@ class StreamLoopManager:
# 全局流循环管理器实例
stream_loop_manager = StreamLoopManager()
stream_loop_manager = StreamLoopManager()

View File

@@ -417,7 +417,7 @@ class MessageManager:
return
# 记录详细信息
msg_previews = [f"{str(msg.message_id)[:8] if msg.message_id else 'unknown'}:{msg.processed_plain_text[:20] if msg.processed_plain_text else '(空)'}"
msg_previews = [f"{str(msg.message_id)[:8] if msg.message_id else 'unknown'}:{msg.processed_plain_text[:20] if msg.processed_plain_text else '(空)'}"
for msg in unread_messages[:3]] # 只显示前3条
logger.info(f"🧹 [清除未读] stream={stream_id[:8]}, 开始清除 {len(unread_messages)} 条未读消息, 示例: {msg_previews}")
@@ -446,15 +446,15 @@ class MessageManager:
context = chat_stream.context_manager.context
if hasattr(context, "unread_messages") and context.unread_messages:
unread_count = len(context.unread_messages)
# 如果还有未读消息,说明 action_manager 可能遗漏了,标记它们
if unread_count > 0:
if unread_count > 0:
# 获取所有未读消息的 ID
message_ids = [msg.message_id for msg in context.unread_messages]
# 标记为已读(会移到历史消息)
success = chat_stream.context_manager.mark_messages_as_read(message_ids)
if success:
logger.debug(f"✅ stream={stream_id[:8]}, 成功标记 {unread_count} 条消息为已读")
else:
@@ -481,7 +481,7 @@ class MessageManager:
try:
chat_manager = get_chat_manager()
chat_stream = await chat_manager.get_stream(stream_id)
if chat_stream and hasattr(chat_stream.context_manager.context, 'is_chatter_processing'):
if chat_stream and hasattr(chat_stream.context_manager.context, "is_chatter_processing"):
chat_stream.context_manager.context.is_chatter_processing = is_processing
logger.debug(f"设置StreamContext处理状态: stream={stream_id}, processing={is_processing}")
except Exception as e:
@@ -517,7 +517,7 @@ class MessageManager:
try:
chat_manager = get_chat_manager()
chat_stream = await chat_manager.get_stream(stream_id)
if chat_stream and hasattr(chat_stream.context_manager.context, 'is_chatter_processing'):
if chat_stream and hasattr(chat_stream.context_manager.context, "is_chatter_processing"):
return chat_stream.context_manager.context.is_chatter_processing
except Exception:
pass
@@ -677,4 +677,4 @@ class MessageManager:
# 创建全局消息管理器实例
message_manager = MessageManager()
message_manager = MessageManager()

View File

@@ -248,16 +248,16 @@ class ChatterActionManager:
try:
# 根据动作类型确定提示词模式
prompt_mode = "s4u" if action_name == "reply" else "normal"
# 将prompt_mode传递给generate_reply
action_data_with_mode = (action_data or {}).copy()
action_data_with_mode["prompt_mode"] = prompt_mode
# 只传递当前正在执行的动作,而不是所有可用动作
# 这样可以让LLM明确知道"已决定执行X动作",而不是"有这些动作可用"
current_action_info = self._using_actions.get(action_name)
current_actions: dict[str, Any] = {action_name: current_action_info} if current_action_info else {}
# 附加目标消息信息(如果存在)
if target_message:
# 提取目标消息的关键信息
@@ -268,7 +268,7 @@ class ChatterActionManager:
"time": getattr(target_message, "time", 0),
}
current_actions["_target_message"] = target_msg_info
success, response_set, _ = await generator_api.generate_reply(
chat_stream=chat_stream,
reply_message=target_message,
@@ -295,12 +295,12 @@ class ChatterActionManager:
should_quote_reply = None
if action_data and isinstance(action_data, dict):
should_quote_reply = action_data.get("should_quote_reply", None)
# respond动作默认不引用回复保持对话流畅
if action_name == "respond" and should_quote_reply is None:
should_quote_reply = False
async def _after_reply():
async def _after_reply():
# 发送并存储回复
loop_info, reply_text, cycle_timers_reply = await self._send_and_store_reply(
chat_stream,

View File

@@ -365,7 +365,7 @@ class DefaultReplyer:
# 确保类型安全
if isinstance(mode, str):
prompt_mode_value = mode
# 构建 Prompt
with Timer("构建Prompt", {}): # 内部计时器,可选保留
prompt = await self.build_prompt_reply_context(
@@ -1171,16 +1171,16 @@ class DefaultReplyer:
from src.plugin_system.apis.chat_api import get_chat_manager
chat_manager = get_chat_manager()
chat_stream_obj = await chat_manager.get_stream(chat_id)
if chat_stream_obj:
unread_messages = chat_stream_obj.context_manager.get_unread_messages()
if unread_messages:
# 使用最后一条未读消息作为参考
last_msg = unread_messages[-1]
platform = last_msg.chat_info.platform if hasattr(last_msg, 'chat_info') else chat_stream.platform
user_id = last_msg.user_info.user_id if hasattr(last_msg, 'user_info') else ""
user_nickname = last_msg.user_info.user_nickname if hasattr(last_msg, 'user_info') else ""
user_cardname = last_msg.user_info.user_cardname if hasattr(last_msg, 'user_info') else ""
platform = last_msg.chat_info.platform if hasattr(last_msg, "chat_info") else chat_stream.platform
user_id = last_msg.user_info.user_id if hasattr(last_msg, "user_info") else ""
user_nickname = last_msg.user_info.user_nickname if hasattr(last_msg, "user_info") else ""
user_cardname = last_msg.user_info.user_cardname if hasattr(last_msg, "user_info") else ""
processed_plain_text = last_msg.processed_plain_text or ""
else:
# 没有未读消息,使用默认值
@@ -1263,19 +1263,19 @@ class DefaultReplyer:
if available_actions:
# 过滤掉特殊键以_开头
action_items = {k: v for k, v in available_actions.items() if not k.startswith("_")}
# 提取目标消息信息(如果存在)
target_msg_info = available_actions.get("_target_message") # type: ignore
if action_items:
if len(action_items) == 1:
# 单个动作
action_name, action_info = list(action_items.items())[0]
action_desc = action_info.description
# 构建基础决策信息
action_descriptions = f"## 决策信息\n\n你已经决定要执行 **{action_name}** 动作({action_desc})。\n\n"
# 只有需要目标消息的动作才显示目标消息详情
# respond 动作是统一回应所有未读消息,不应该显示特定目标消息
if action_name not in ["respond"] and target_msg_info and isinstance(target_msg_info, dict):
@@ -1284,7 +1284,7 @@ class DefaultReplyer:
content = target_msg_info.get("content", "")
msg_time = target_msg_info.get("time", 0)
time_str = time_module.strftime("%H:%M:%S", time_module.localtime(msg_time)) if msg_time else "未知时间"
action_descriptions += f"**目标消息**: {time_str} {sender} 说: {content}\n\n"
else:
# 多个动作
@@ -2137,7 +2137,7 @@ class DefaultReplyer:
except Exception as e:
logger.error(f"存储聊天记忆失败: {e}")
def weighted_sample_no_replacement(items, weights, k) -> list:
"""

View File

@@ -5,12 +5,12 @@
插件可以通过实现这些接口来扩展安全功能。
"""
from .interfaces import SecurityCheckResult, SecurityChecker
from .interfaces import SecurityChecker, SecurityCheckResult
from .manager import SecurityManager, get_security_manager
__all__ = [
"SecurityChecker",
"SecurityCheckResult",
"SecurityChecker",
"SecurityManager",
"get_security_manager",
]

View File

@@ -10,7 +10,7 @@ from typing import Any
from src.common.logger import get_logger
from .interfaces import SecurityAction, SecurityCheckResult, SecurityChecker, SecurityLevel
from .interfaces import SecurityAction, SecurityChecker, SecurityCheckResult, SecurityLevel
logger = get_logger("security.manager")

View File

@@ -1,5 +1,7 @@
import asyncio
import copy
import re
from collections.abc import Awaitable, Callable
from src.chat.utils.prompt_params import PromptParameters
from src.common.logger import get_logger
@@ -12,122 +14,205 @@ logger = get_logger("prompt_component_manager")
class PromptComponentManager:
"""
管理所有 `BasePrompt` 组件的单例类
一个统一的、动态的、可观测的提示词组件管理中心
该管理器负责:
1. 从 `component_registry` 中查询 `BasePrompt` 子类。
2. 根据注入点目标Prompt名称对它们进行筛选
3. 提供一个接口以便在构建核心Prompt时能够获取并执行所有相关的组件。
该管理器是整个提示词动态注入系统的核心,它负责:
1. **规则加载**: 在系统启动时,自动扫描所有已注册的 `BasePrompt` 组件,
并将其静态定义的 `injection_rules` 加载为默认的动态规则
2. **动态管理**: 提供线程安全的 API允许在运行时动态地添加、更新或移除注入规则
使得提示词的结构可以被实时调整。
3. **状态观测**: 提供丰富的查询 API用于观测系统当前完整的注入状态
例如查询所有注入到特定目标的规则、或查询某个组件定义的所有规则。
4. **注入应用**: 在构建核心 Prompt 时,根据统一的、按优先级排序的规则集,
动态地修改和装配提示词模板,实现灵活的提示词组合。
"""
def _get_rules_for(self, target_prompt_name: str) -> list[tuple[InjectionRule, type[BasePrompt]]]:
"""
获取指定目标Prompt的所有注入规则及其关联的组件类
def __init__(self):
"""初始化管理器实例。"""
# _dynamic_rules 是管理器的核心状态,存储所有注入规则
# 结构: {
# "target_prompt_name": {
# "prompt_component_name": (InjectionRule, content_provider, source)
# }
# }
# content_provider 是一个异步函数,用于在应用规则时动态生成注入内容。
# source 记录了规则的来源(例如 "static_default" 或 "runtime")。
self._dynamic_rules: dict[str, dict[str, tuple[InjectionRule, Callable[..., Awaitable[str]], str]]] = {}
self._lock = asyncio.Lock() # 使用异步锁确保对 _dynamic_rules 的并发访问安全。
self._initialized = False # 标记静态规则是否已加载,防止重复加载。
Args:
target_prompt_name (str): 目标 Prompt 的名称。
# --- 核心生命周期与初始化 ---
Returns:
list[tuple[InjectionRule, Type[BasePrompt]]]: 一个元组列表,
每个元组包含一个注入规则和其对应的 Prompt 组件类,并已根据优先级排序。
def load_static_rules(self):
"""
# 从注册表中获取所有已启用的 PROMPT 类型的组件
在系统启动时加载所有静态注入规则。
该方法会扫描所有已在 `component_registry` 中注册并启用的 Prompt 组件,
将其类变量 `injection_rules` 转换为管理器的动态规则。
这确保了所有插件定义的默认注入行为在系统启动时就能生效。
此操作是幂等的,一旦初始化完成就不会重复执行。
"""
if self._initialized:
return
logger.info("正在加载静态 Prompt 注入规则...")
# 从组件注册表中获取所有已启用的 Prompt 组件
enabled_prompts = component_registry.get_enabled_components_by_type(ComponentType.PROMPT)
matching_rules = []
# 遍历所有启用的 Prompt 组件,查找与目标 Prompt 相关的注入规则
for prompt_name, prompt_info in enabled_prompts.items():
if not isinstance(prompt_info, PromptInfo):
continue
# prompt_info.injection_rules 已经经过了后向兼容处理,确保总是列表
for rule in prompt_info.injection_rules:
# 如果规则的目标是当前指定的 Prompt
if rule.target_prompt == target_prompt_name:
# 获取该规则对应的组件类
component_class = component_registry.get_component_class(prompt_name, ComponentType.PROMPT)
# 确保获取到的确实是一个 BasePrompt 的子类
if component_class and issubclass(component_class, BasePrompt):
matching_rules.append((rule, component_class))
component_class = component_registry.get_component_class(prompt_name, ComponentType.PROMPT)
if not (component_class and issubclass(component_class, BasePrompt)):
logger.warning(f"无法为 '{prompt_name}' 加载静态规则,因为它不是一个有效的 Prompt 组件。")
continue
# 根据规则的优先级进行排序,数字越小,优先级越高,越先应用
matching_rules.sort(key=lambda x: x[0].priority)
return matching_rules
def create_provider(cls: type[BasePrompt]) -> Callable[[PromptParameters], Awaitable[str]]:
"""
为静态组件创建一个内容提供者闭包 (Content Provider Closure)。
这个闭包捕获了组件的类 `cls`,并返回一个标准的 `content_provider` 异步函数。
当 `apply_injections` 需要内容时,它会调用这个函数。
函数内部会实例化组件,并执行其 `execute` 方法来获取注入内容。
Args:
cls (type[BasePrompt]): 需要为其创建提供者的 Prompt 组件类。
Returns:
Callable[[PromptParameters], Awaitable[str]]: 一个符合管理器标准的异步内容提供者。
"""
async def content_provider(params: PromptParameters) -> str:
"""实际执行内容生成的异步函数。"""
try:
# 从注册表获取最新的组件信息,包括插件配置
p_info = component_registry.get_component_info(cls.prompt_name, ComponentType.PROMPT)
plugin_config = {}
if isinstance(p_info, PromptInfo):
plugin_config = component_registry.get_plugin_config(p_info.plugin_name)
# 实例化组件并执行
instance = cls(params=params, plugin_config=plugin_config)
result = await instance.execute()
return str(result) if result is not None else ""
except Exception as e:
logger.error(f"执行静态规则提供者 '{cls.prompt_name}' 时出错: {e}", exc_info=True)
return "" # 出错时返回空字符串,避免影响主流程
return content_provider
# 为该组件的每条静态注入规则创建并注册一个动态规则
for rule in prompt_info.injection_rules:
provider = create_provider(component_class)
target_rules = self._dynamic_rules.setdefault(rule.target_prompt, {})
target_rules[prompt_name] = (rule, provider, "static_default")
self._initialized = True
logger.info(f"静态 Prompt 注入规则加载完成,共处理 {len(enabled_prompts)} 个组件。")
# --- 运行时规则管理 API ---
async def add_injection_rule(
self,
prompt_name: str,
rule: InjectionRule,
content_provider: Callable[..., Awaitable[str]],
source: str = "runtime",
) -> bool:
"""
动态添加或更新一条注入规则。
此方法允许在系统运行时,由外部逻辑(如插件、命令)向管理器中添加新的注入行为。
如果已存在同名组件针对同一目标的规则,此方法会覆盖旧规则。
Args:
prompt_name (str): 动态注入组件的唯一名称。
rule (InjectionRule): 描述注入行为的规则对象。
content_provider (Callable[..., Awaitable[str]]):
一个异步函数,用于在应用注入时动态生成内容。
函数签名应为: `async def provider(params: "PromptParameters") -> str`
source (str, optional): 规则的来源标识,默认为 "runtime"
Returns:
bool: 如果成功添加或更新,则返回 True。
"""
async with self._lock:
target_rules = self._dynamic_rules.setdefault(rule.target_prompt, {})
target_rules[prompt_name] = (rule, content_provider, source)
logger.info(f"成功添加/更新注入规则: '{prompt_name}' -> '{rule.target_prompt}' (来源: {source})")
return True
async def remove_injection_rule(self, prompt_name: str, target_prompt: str) -> bool:
"""
移除一条动态注入规则。
Args:
prompt_name (str): 要移除的注入组件的名称。
target_prompt (str): 该组件注入的目标核心提示词名称。
Returns:
bool: 如果成功移除,则返回 True如果规则不存在则返回 False。
"""
async with self._lock:
if target_prompt in self._dynamic_rules and prompt_name in self._dynamic_rules[target_prompt]:
del self._dynamic_rules[target_prompt][prompt_name]
# 如果目标下已无任何规则,则清理掉这个键
if not self._dynamic_rules[target_prompt]:
del self._dynamic_rules[target_prompt]
logger.info(f"成功移除注入规则: '{prompt_name}' from '{target_prompt}'")
return True
logger.warning(f"尝试移除注入规则失败: 未找到 '{prompt_name}' on '{target_prompt}'")
return False
# --- 核心注入逻辑 ---
async def apply_injections(
self, target_prompt_name: str, original_template: str, params: PromptParameters
) -> str:
"""
获取、实例化并执行所有相关组件,然后根据注入规则修改原始模板。
【核心方法】根据目标名称,应用所有匹配的注入规则,返回修改后的模板。
这是一个三步走的过程
1. 实例化所有需要执行的组件
2. 并行执行它们的 `execute` 方法以获取注入内容
3. 按照优先级顺序,将内容注入到原始模板中
这是提示词构建流程中的关键步骤。它会执行以下操作
1. 检查并确保静态规则已加载
2. 获取所有注入到 `target_prompt_name` 的规则
3. 按照规则的 `priority` 属性进行升序排序,优先级数字越小越先应用
4. 依次执行每个规则的 `content_provider` 来异步获取注入内容。
5. 根据规则的 `injection_type` (如 PREPEND, APPEND, REPLACE 等) 将内容应用到模板上。
Args:
target_prompt_name (str): 目标 Prompt 的名称。
original_template (str): 原始的、未经修改的 Prompt 模板字符串
params (PromptParameters): 传递给 Prompt 组件实例的参数
target_prompt_name (str): 目标核心提示词的名称。
original_template (str): 未经修改的原始提示词模板
params (PromptParameters): 当前请求的参数,会传递给 `content_provider`
Returns:
str: 应用了所有注入规则后,修改过的 Prompt 模板字符串。
str: 应用了所有注入规则后,最终生成的提示词模板字符串。
"""
rules_with_classes = self._get_rules_for(target_prompt_name)
# 如果没有找到任何匹配的规则,就直接返回原始模板,啥也不干
if not rules_with_classes:
if not self._initialized:
self.load_static_rules()
# 步骤 1: 获取所有指向当前目标的规则
# 使用 .values() 获取 (rule, provider, source) 元组列表
rules_for_target = list(self._dynamic_rules.get(target_prompt_name, {}).values())
if not rules_for_target:
return original_template
# --- 第一步: 实例化所有需要执行的组件 ---
instance_map = {} # 存储组件实例,虽然目前没直接用,但留着总没错
tasks = [] # 存放所有需要并行执行的 execute 异步任务
components_to_execute = [] # 存放需要执行的组件类,用于后续结果映射
# 步骤 2: 按优先级排序,数字越小越优先
rules_for_target.sort(key=lambda x: x[0].priority)
for rule, component_class in rules_with_classes:
# 如果注入类型是 REMOVE那就不需要执行组件了因为它不产生内容
# 步骤 3: 依次执行内容提供者并根据注入类型修改模板
modified_template = original_template
for rule, provider, source in rules_for_target:
content = ""
# 对于非 REMOVE 类型的注入,需要先获取内容
if rule.injection_type != InjectionType.REMOVE:
try:
# 获取组件的元信息,主要是为了拿到插件名称来读取插件配置
prompt_info = component_registry.get_component_info(
component_class.prompt_name, ComponentType.PROMPT
)
if not isinstance(prompt_info, PromptInfo):
plugin_config = {}
else:
# 从注册表获取该组件所属插件的配置
plugin_config = component_registry.get_plugin_config(prompt_info.plugin_name)
# 实例化组件,并传入参数和插件配置
instance = component_class(params=params, plugin_config=plugin_config)
instance_map[component_class.prompt_name] = instance
# 将组件的 execute 方法作为一个任务添加到列表中
tasks.append(instance.execute())
components_to_execute.append(component_class)
content = await provider(params)
except Exception as e:
logger.error(f"实例化 Prompt 组件 '{component_class.prompt_name}' 失败: {e}")
# 即使失败,也添加一个立即完成的空任务,以保持与其他任务的索引同步
tasks.append(asyncio.create_task(asyncio.sleep(0, result=e))) # type: ignore
# --- 第二步: 并行执行所有组件的 execute 方法 ---
# 使用 asyncio.gather 来同时运行所有任务,提高效率
results = await asyncio.gather(*tasks, return_exceptions=True)
# 创建一个从组件名到执行结果的映射,方便后续查找
result_map = {
components_to_execute[i].prompt_name: res
for i, res in enumerate(results)
if not isinstance(res, Exception) # 只包含成功的结果
}
# 单独处理并记录执行失败的组件
for i, res in enumerate(results):
if isinstance(res, Exception):
logger.error(f"执行 Prompt 组件 '{components_to_execute[i].prompt_name}' 失败: {res}")
# --- 第三步: 按优先级顺序应用注入规则 ---
modified_template = original_template
for rule, component_class in rules_with_classes:
# 从结果映射中获取该组件生成的内容
content = result_map.get(component_class.prompt_name)
logger.error(f"执行规则 '{rule}' (来源: {source}) 的内容提供者时失败: {e}", exc_info=True)
continue # 跳过失败的 provider不中断整个流程
# 应用注入逻辑
try:
if rule.injection_type == InjectionType.PREPEND:
if content:
@@ -136,28 +221,178 @@ class PromptComponentManager:
if content:
modified_template = f"{modified_template}\n{content}"
elif rule.injection_type == InjectionType.REPLACE:
# 使用正则表达式替换目标内容
if content and rule.target_content:
# 只有在 content 不为 None 且 target_content 有效时才执行替换
if content is not None and rule.target_content:
modified_template = re.sub(rule.target_content, str(content), modified_template)
elif rule.injection_type == InjectionType.INSERT_AFTER:
# 在匹配到的内容后面插入
if content and rule.target_content:
# re.sub a little trick: \g<0> represents the entire matched string
# 使用 `\g<0>` 在正则匹配的整个内容后添加新内容
replacement = f"\\g<0>\n{content}"
modified_template = re.sub(rule.target_content, replacement, modified_template)
elif rule.injection_type == InjectionType.REMOVE:
# 使用正则表达式移除目标内容
if rule.target_content:
modified_template = re.sub(rule.target_content, "", modified_template)
except re.error as e:
logger.error(
f"在为 '{component_class.prompt_name}' 应用规则时发生正则错误: {e} (pattern: '{rule.target_content}')"
)
logger.error(f"应用规则时发生正则错误: {e} (pattern: '{rule.target_content}')")
except Exception as e:
logger.error(f"应用 Prompt 注入规则 '{rule}' 失败: {e}")
logger.error(f"应用注入规则 '{rule}' (来源: {source}) 失败: {e}", exc_info=True)
return modified_template
async def preview_prompt_injections(
self, target_prompt_name: str, params: PromptParameters
) -> str:
"""
【预览功能】模拟应用所有注入规则,返回最终生成的模板字符串,而不实际修改任何状态。
# 创建全局单例
这个方法对于调试和测试非常有用,可以查看在特定参数下,
一个核心提示词经过所有注入规则处理后会变成什么样子。
Args:
target_prompt_name (str): 希望预览的目标核心提示词名称。
params (PromptParameters): 模拟的请求参数。
Returns:
str: 模拟生成的最终提示词模板字符串。如果找不到模板,则返回错误信息。
"""
try:
# 从全局提示词管理器获取最原始的模板内容
from src.chat.utils.prompt import global_prompt_manager
original_prompt = global_prompt_manager._prompts.get(target_prompt_name)
if not original_prompt:
logger.warning(f"无法预览 '{target_prompt_name}',因为找不到这个核心 Prompt。")
return f"Error: Prompt '{target_prompt_name}' not found."
original_template = original_prompt.template
except KeyError:
logger.warning(f"无法预览 '{target_prompt_name}',因为找不到这个核心 Prompt。")
return f"Error: Prompt '{target_prompt_name}' not found."
# 直接调用核心注入逻辑来模拟结果
return await self.apply_injections(target_prompt_name, original_template, params)
# --- 状态观测与查询 API ---
def get_core_prompts(self) -> list[str]:
"""获取所有已注册的核心提示词模板名称列表(即所有可注入的目标)。"""
from src.chat.utils.prompt import global_prompt_manager
return list(global_prompt_manager._prompts.keys())
def get_core_prompt_contents(self) -> dict[str, str]:
"""获取所有核心提示词模板的原始内容。"""
from src.chat.utils.prompt import global_prompt_manager
return {name: prompt.template for name, prompt in global_prompt_manager._prompts.items()}
def get_registered_prompt_component_info(self) -> list[PromptInfo]:
"""获取所有在 ComponentRegistry 中注册的 Prompt 组件信息。"""
components = component_registry.get_components_by_type(ComponentType.PROMPT).values()
return [info for info in components if isinstance(info, PromptInfo)]
async def get_full_injection_map(self) -> dict[str, list[dict]]:
"""
获取当前完整的注入映射图。
此方法提供了一个系统全局的注入视图展示了每个核心提示词target
被哪些注入组件source以何种优先级注入。
Returns:
dict[str, list[dict]]: 一个字典,键是目标提示词名称,
值是按优先级排序的注入信息列表。
`[{"name": str, "priority": int, "source": str}]`
"""
injection_map = {}
async with self._lock:
# 合并所有动态规则的目标和所有核心提示词,确保所有潜在目标都被包含
all_targets = set(self._dynamic_rules.keys()) | set(self.get_core_prompts())
for target in sorted(all_targets):
rules = self._dynamic_rules.get(target, {})
if not rules:
injection_map[target] = []
continue
info_list = []
for prompt_name, (rule, _, source) in rules.items():
info_list.append({"name": prompt_name, "priority": rule.priority, "source": source})
# 按优先级排序后存入 map
info_list.sort(key=lambda x: x["priority"])
injection_map[target] = info_list
return injection_map
async def get_injections_for_prompt(self, target_prompt_name: str) -> list[dict]:
"""
获取指定核心提示词模板的所有注入信息(包含详细规则)。
Args:
target_prompt_name (str): 目标核心提示词的名称。
Returns:
list[dict]: 一个包含注入规则详细信息的列表,已按优先级排序。
"""
rules_for_target = self._dynamic_rules.get(target_prompt_name, {})
if not rules_for_target:
return []
info_list = []
for prompt_name, (rule, _, source) in rules_for_target.items():
info_list.append(
{
"name": prompt_name,
"priority": rule.priority,
"source": source,
"injection_type": rule.injection_type.value,
"target_content": rule.target_content,
}
)
info_list.sort(key=lambda x: x["priority"])
return info_list
def get_all_dynamic_rules(self) -> dict[str, dict[str, "InjectionRule"]]:
"""
获取所有当前的动态注入规则,以 InjectionRule 对象形式返回。
此方法返回一个深拷贝的规则副本,隐藏了 `content_provider` 等内部实现细节。
适合用于展示或序列化当前的规则配置。
"""
rules_copy = {}
for target, rules in self._dynamic_rules.items():
target_copy = {name: rule for name, (rule, _, _) in rules.items()}
rules_copy[target] = target_copy
return copy.deepcopy(rules_copy)
def get_rules_for_target(self, target_prompt: str) -> dict[str, InjectionRule]:
"""
获取所有注入到指定核心提示词的动态规则。
Args:
target_prompt (str): 目标核心提示词的名称。
Returns:
dict[str, InjectionRule]: 一个字典,键是注入组件的名称,值是 `InjectionRule` 对象。
如果找不到任何注入到该目标的规则,则返回一个空字典。
"""
target_rules = self._dynamic_rules.get(target_prompt, {})
return {name: copy.deepcopy(rule_info[0]) for name, rule_info in target_rules.items()}
def get_rules_by_component(self, component_name: str) -> dict[str, InjectionRule]:
"""
获取由指定的单个注入组件定义的所有动态规则。
Args:
component_name (str): 注入组件的名称。
Returns:
dict[str, InjectionRule]: 一个字典,键是目标核心提示词的名称,值是 `InjectionRule` 对象。
如果该组件没有定义任何注入规则,则返回一个空字典。
"""
found_rules = {}
for target, rules in self._dynamic_rules.items():
if component_name in rules:
rule_info = rules[component_name]
found_rules[target] = copy.deepcopy(rule_info[0])
return found_rules
# 创建全局单例 (Singleton)
# 在整个应用程序中,应该只使用这一个 `prompt_component_manager` 实例,
# 以确保所有部分都共享和操作同一份动态规则集。
prompt_component_manager = PromptComponentManager()