This commit is contained in:
Windpicker-owo
2025-10-19 22:49:14 +08:00
48 changed files with 1700 additions and 2279 deletions

View File

@@ -155,88 +155,22 @@ class ChatterPlanFilter:
identity_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}"
schedule_block = ""
# 优先检查是否被吵醒
from src.chat.message_manager.message_manager import message_manager
angry_prompt_addition = ""
try:
from src.plugins.built_in.sleep_system.api import get_wakeup_manager
wakeup_mgr = get_wakeup_manager()
except ImportError:
logger.debug("无法导入睡眠系统API将跳过相关检查。")
wakeup_mgr = None
if wakeup_mgr:
# 双重检查确保愤怒状态不会丢失
# 检查1: 直接从 wakeup_manager 获取
if wakeup_mgr.is_in_angry_state():
angry_prompt_addition = wakeup_mgr.get_angry_prompt_addition()
# 检查2: 如果上面没获取到,再从 mood_manager 确认
if not angry_prompt_addition:
chat_mood_for_check = mood_manager.get_mood_by_chat_id(plan.chat_id)
if chat_mood_for_check.is_angry_from_wakeup:
angry_prompt_addition = global_config.sleep_system.angry_prompt
if angry_prompt_addition:
schedule_block = angry_prompt_addition
elif global_config.planning_system.schedule_enable:
if global_config.planning_system.schedule_enable:
if activity_info := schedule_manager.get_current_activity():
activity = activity_info.get("activity", "未知活动")
schedule_block = f"你当前正在:{activity},但注意它与群聊的聊天无关。"
mood_block = ""
# 如果被吵醒,则心情也是愤怒的,不需要另外的情绪模块
if not angry_prompt_addition and global_config.mood.enable_mood:
# 需要情绪模块打开才能获得情绪,否则会引发报错
if global_config.mood.enable_mood:
chat_mood = mood_manager.get_mood_by_chat_id(plan.chat_id)
mood_block = f"你现在的心情是:{chat_mood.mood_state}"
if plan.mode == ChatMode.PROACTIVE:
long_term_memory_block = await self._get_long_term_memory_context()
chat_content_block, message_id_list = await build_readable_messages_with_id(
messages=[msg.flatten() for msg in plan.chat_history],
timestamp_mode="normal",
truncate=False,
show_actions=False,
)
prompt_template = await global_prompt_manager.get_prompt_async("proactive_planner_prompt")
actions_before_now = await get_actions_by_timestamp_with_chat(
chat_id=plan.chat_id,
timestamp_start=time.time() - 3600,
timestamp_end=time.time(),
limit=5,
)
actions_before_now_block = build_readable_actions(actions=actions_before_now)
actions_before_now_block = f"你刚刚选择并执行过的action是\n{actions_before_now_block}"
prompt = prompt_template.format(
time_block=time_block,
identity_block=identity_block,
schedule_block=schedule_block,
mood_block=mood_block,
long_term_memory_block=long_term_memory_block,
chat_content_block=chat_content_block or "最近没有聊天内容。",
actions_before_now_block=actions_before_now_block,
)
return prompt, message_id_list
# 构建已读/未读历史消息
read_history_block, unread_history_block, message_id_list = await self._build_read_unread_history_blocks(
plan
)
# 为了兼容性保留原有的chat_content_block
chat_content_block, _ = await build_readable_messages_with_id(
messages=[msg.flatten() for msg in plan.chat_history],
timestamp_mode="normal",
read_mark=self.last_obs_time_mark,
truncate=True,
show_actions=True,
)
actions_before_now = await get_actions_by_timestamp_with_chat(
chat_id=plan.chat_id,
timestamp_start=time.time() - 3600,
@@ -286,7 +220,7 @@ class ChatterPlanFilter:
is_group_chat = plan.chat_type == ChatType.GROUP
chat_context_description = "你现在正在一个群聊中"
if not is_group_chat and plan.target_info:
chat_target_name = plan.target_info.get("person_name") or plan.target_info.get("user_nickname") or "对方"
chat_target_name = plan.target_info.person_name or plan.target_info.user_nickname or "对方"
chat_context_description = f"你正在和 {chat_target_name} 私聊"
action_options_block = await self._build_action_options(plan.available_actions)

View File

@@ -9,7 +9,7 @@ from src.chat.utils.utils import get_chat_type_and_target_info
from src.common.data_models.database_data_model import DatabaseMessages
from src.common.data_models.info_data_model import Plan, TargetPersonInfo
from src.config.config import global_config
from src.plugin_system.base.component_types import ActionInfo, ChatMode, ChatType
from src.plugin_system.base.component_types import ActionInfo, ChatMode, ChatType, ComponentType
from src.plugin_system.core.component_registry import component_registry
@@ -55,6 +55,11 @@ class ChatterPlanGenerator:
try:
# 获取聊天类型和目标信息
chat_type, target_info = await get_chat_type_and_target_info(self.chat_id)
if chat_type:
chat_type = ChatType.GROUP
else:
#遇到未知类型也当私聊处理
chat_type = ChatType.PRIVATE
# 获取可用动作列表
available_actions = await self._get_available_actions(chat_type, mode)
@@ -62,12 +67,16 @@ class ChatterPlanGenerator:
# 获取聊天历史记录
recent_messages = await self._get_recent_messages()
# 构建计划对象
# 使用 target_info 字典创建 TargetPersonInfo 实例
target_person_info = TargetPersonInfo(**target_info) if target_info else TargetPersonInfo()
# 构建计划对象
plan = Plan(
chat_id=self.chat_id,
chat_type=chat_type,
mode=mode,
target_info=target_info,
target_info=target_person_info,
available_actions=available_actions,
chat_history=recent_messages,
)
@@ -77,6 +86,7 @@ class ChatterPlanGenerator:
except Exception:
# 如果生成失败,返回一个基本的空计划
return Plan(
chat_type = ChatType.PRIVATE,#空计划默认当成私聊
chat_id=self.chat_id,
mode=mode,
target_info=TargetPersonInfo(),
@@ -124,7 +134,7 @@ class ChatterPlanGenerator:
try:
# 获取最近的消息记录
raw_messages = await get_raw_msg_before_timestamp_with_chat(
chat_id=self.chat_id, timestamp=time.time(), limit=global_config.memory.short_memory_length
chat_id=self.chat_id, timestamp=time.time(), limit=global_config.chat.max_context_size
)
# 转换为 DatabaseMessages 对象

View File

@@ -70,6 +70,7 @@ class ChatterActionPlanner:
"replies_generated": 0,
"other_actions_executed": 0,
}
self._background_tasks: set[asyncio.Task] = set()
async def plan(self, context: "StreamContext | None" = None) -> tuple[list[dict[str, Any]], Any | None]:
"""
@@ -157,7 +158,9 @@ class ChatterActionPlanner:
)
if interest_updates:
asyncio.create_task(self._commit_interest_updates(interest_updates))
task = asyncio.create_task(self._commit_interest_updates(interest_updates))
self._background_tasks.add(task)
task.add_done_callback(self._handle_task_result)
# 检查兴趣度是否达到非回复动作阈值
non_reply_action_interest_threshold = global_config.affinity_flow.non_reply_action_interest_threshold
@@ -266,6 +269,17 @@ class ChatterActionPlanner:
return final_actions_dict, final_target_message_dict
def _handle_task_result(self, task: asyncio.Task) -> None:
"""处理后台任务的结果,记录异常。"""
try:
task.result()
except asyncio.CancelledError:
pass # 任务被取消是正常现象
except Exception as e:
logger.error(f"后台任务执行失败: {e}", exc_info=True)
finally:
self._background_tasks.discard(task)
def get_planner_stats(self) -> dict[str, Any]:
"""获取规划器统计"""
return self.planner_stats.copy()

View File

@@ -15,7 +15,7 @@ logger = get_logger(__name__)
@register_plugin
class ProactiveThinkerPlugin(BasePlugin):
"""一个主动思考的插件,但现在还只是个空壳子"""
"""一个主动思考的插件"""
plugin_name: str = "proactive_thinker"
enable_plugin: bool = True

View File

@@ -6,6 +6,7 @@ from datetime import datetime
from maim_message import UserInfo
from src.chat.message_manager.sleep_system.state_manager import SleepState, sleep_state_manager
from src.chat.message_receive.chat_stream import get_chat_manager
from src.common.logger import get_logger
from src.config.config import global_config
@@ -13,7 +14,6 @@ from src.manager.async_task_manager import AsyncTask, async_task_manager
from src.plugin_system import BaseEventHandler, EventType
from src.plugin_system.apis import chat_api, message_api, person_api
from src.plugin_system.base.base_event import HandlerResult
from src.chat.message_manager.sleep_system.state_manager import SleepState, sleep_state_manager
from .proactive_thinker_executor import ProactiveThinkerExecutor

View File

@@ -3,7 +3,7 @@ Base search engine interface
"""
from abc import ABC, abstractmethod
from typing import Any
from typing import Any, Optional
class BaseSearchEngine(ABC):
@@ -24,6 +24,12 @@ class BaseSearchEngine(ABC):
"""
pass
async def read_url(self, url: str) -> Optional[str]:
"""
读取URL内容如果引擎不支持则返回None
"""
return None
@abstractmethod
def is_available(self) -> bool:
"""

View File

@@ -0,0 +1,107 @@
"""
Metaso Search Engine (Chat Completions Mode)
"""
import json
from typing import Any, List
import httpx
from src.common.logger import get_logger
from src.plugin_system.apis import config_api
from ..utils.api_key_manager import create_api_key_manager_from_config
from .base import BaseSearchEngine
logger = get_logger(__name__)
class MetasoClient:
"""A client to interact with the Metaso API."""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://metaso.cn/api/v1"
self.headers = {
"Authorization": f"Bearer {self.api_key}",
"Accept": "application/json",
"Content-Type": "application/json",
}
async def search(self, query: str, **kwargs) -> List[dict[str, Any]]:
"""Perform a search using the Metaso Chat Completions API."""
payload = {"model": "fast", "stream": True, "messages": [{"role": "user", "content": query}]}
search_url = f"{self.base_url}/chat/completions"
full_response_content = ""
async with httpx.AsyncClient(timeout=90.0) as client:
try:
async with client.stream("POST", search_url, headers=self.headers, json=payload) as response:
response.raise_for_status()
async for line in response.aiter_lines():
if line.startswith("data:"):
data_str = line[len("data:") :].strip()
if data_str == "[DONE]":
break
try:
data = json.loads(data_str)
delta = data.get("choices", [{}])[0].get("delta", {})
content_chunk = delta.get("content")
if content_chunk:
full_response_content += content_chunk
except json.JSONDecodeError:
logger.warning(f"Metaso stream: could not decode JSON line: {data_str}")
continue
if not full_response_content:
logger.warning("Metaso search returned an empty stream.")
return []
return [
{
"title": query,
"url": "https://metaso.cn/",
"snippet": full_response_content,
"provider": "Metaso (Chat)",
}
]
except httpx.HTTPStatusError as e:
logger.error(f"HTTP error occurred while searching with Metaso Chat: {e.response.text}")
return []
except Exception as e:
logger.error(f"An error occurred while searching with Metaso Chat: {e}", exc_info=True)
return []
class MetasoSearchEngine(BaseSearchEngine):
"""Metaso Search Engine implementation."""
def __init__(self):
self._initialize_clients()
def _initialize_clients(self):
"""Initialize Metaso clients."""
metaso_api_keys = config_api.get_global_config("web_search.metaso_api_keys", None)
self.api_manager = create_api_key_manager_from_config(
metaso_api_keys, lambda key: MetasoClient(api_key=key), "Metaso"
)
def is_available(self) -> bool:
"""Check if the Metaso search engine is available."""
return self.api_manager.is_available()
async def search(self, args: dict[str, Any]) -> list[dict[str, Any]]:
"""Execute a Metaso search."""
if not self.is_available():
return []
query = args["query"]
try:
metaso_client = self.api_manager.get_next_client()
if not metaso_client:
logger.error("Could not get Metaso client.")
return []
return await metaso_client.search(query)
except Exception as e:
logger.error(f"Metaso search failed: {e}", exc_info=True)
return []

View File

@@ -22,6 +22,7 @@ class WEBSEARCHPLUGIN(BasePlugin):
提供网络搜索和URL解析功能支持多种搜索引擎
- Exa (需要API密钥)
- Tavily (需要API密钥)
- Metaso (需要API密钥)
- DuckDuckGo (免费)
- Bing (免费)
"""
@@ -43,6 +44,7 @@ class WEBSEARCHPLUGIN(BasePlugin):
from .engines.exa_engine import ExaSearchEngine
from .engines.searxng_engine import SearXNGSearchEngine
from .engines.tavily_engine import TavilySearchEngine
from .engines.metaso_engine import MetasoSearchEngine
# 实例化所有搜索引擎这会触发API密钥管理器的初始化
exa_engine = ExaSearchEngine()
@@ -50,14 +52,16 @@ class WEBSEARCHPLUGIN(BasePlugin):
ddg_engine = DDGSearchEngine()
bing_engine = BingSearchEngine()
searxng_engine = SearXNGSearchEngine()
# 报告每个引擎的状态
metaso_engine = MetasoSearchEngine()
# 报告每个引擎的状态
engines_status = {
"Exa": exa_engine.is_available(),
"Tavily": tavily_engine.is_available(),
"DuckDuckGo": ddg_engine.is_available(),
"Bing": bing_engine.is_available(),
"SearXNG": searxng_engine.is_available(),
"Metaso": metaso_engine.is_available(),
}
available_engines = [name for name, available in engines_status.items() if available]

View File

@@ -15,6 +15,7 @@ from ..engines.ddg_engine import DDGSearchEngine
from ..engines.exa_engine import ExaSearchEngine
from ..engines.searxng_engine import SearXNGSearchEngine
from ..engines.tavily_engine import TavilySearchEngine
from ..engines.metaso_engine import MetasoSearchEngine
from ..utils.formatters import deduplicate_results, format_search_results
logger = get_logger("web_search_tool")
@@ -51,6 +52,7 @@ class WebSurfingTool(BaseTool):
"ddg": DDGSearchEngine(),
"bing": BingSearchEngine(),
"searxng": SearXNGSearchEngine(),
"metaso": MetasoSearchEngine(),
}
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: