feat: Introduce unified tooling system for plugins and MCP

- Added a new `tooling` module to define a unified model for tool declarations, invocations, and execution results, facilitating compatibility between plugins, legacy actions, and MCP tools.
- Implemented `ToolProvider` interface for various tool providers including built-in tools, MCP tools, and plugin runtime tools.
- Enhanced `MCPManager` and `MCPConnection` to support unified tool invocation and execution results.
- Updated `ComponentRegistry` and related classes to accommodate the new tool specifications and descriptions.
- Refactored existing components to utilize the new tooling system, ensuring backward compatibility with legacy actions.
- Improved error handling and logging for tool invocations across different providers.
This commit is contained in:
DrSmoothl
2026-03-30 23:11:56 +08:00
parent 898b693fe0
commit dc2bf02a42
35 changed files with 1663 additions and 6756 deletions

View File

@@ -1,124 +1,163 @@
"""
MaiSaka built-in tool definitions.
"""
"""Maisaka 内置工具声明。"""
from typing import List
from copy import deepcopy
from typing import Any, Dict, List
from src.llm_models.payload_content.tool_option import ToolOption, ToolParamType
from src.core.tooling import ToolSpec, build_tool_detailed_description
from src.llm_models.payload_content.tool_option import ToolDefinitionInput
def create_builtin_tools() -> List[ToolOption]:
"""Create built-in tools exposed to the main chat-loop model."""
from src.llm_models.payload_content.tool_option import ToolOptionBuilder
def _build_tool_spec(
name: str,
brief_description: str,
parameters_schema: Dict[str, Any] | None = None,
detailed_description: str = "",
) -> ToolSpec:
"""构建单个内置工具声明。
tools: List[ToolOption] = []
Args:
name: 工具名称。
brief_description: 简要描述。
parameters_schema: 参数 Schema。
detailed_description: 详细描述;为空时自动根据参数生成。
wait_builder = ToolOptionBuilder()
wait_builder.set_name("wait")
wait_builder.set_description("Pause speaking and wait for the user to provide more input.")
wait_builder.add_param(
name="seconds",
param_type=ToolParamType.INTEGER,
description="How many seconds to wait before timing out.",
required=True,
enum_values=None,
Returns:
ToolSpec: 构建完成的工具声明。
"""
normalized_schema = deepcopy(parameters_schema) if parameters_schema is not None else None
return ToolSpec(
name=name,
brief_description=brief_description,
detailed_description=(
detailed_description.strip()
or build_tool_detailed_description(normalized_schema)
),
parameters_schema=normalized_schema,
provider_name="maisaka_builtin",
provider_type="builtin",
)
tools.append(wait_builder.build())
reply_builder = ToolOptionBuilder()
reply_builder.set_name("reply")
reply_builder.set_description(
"Generate and emit a visible reply based on the current thought. "
"You must specify the target user msg_id to reply to."
)
reply_builder.add_param(
name="msg_id",
param_type=ToolParamType.STRING,
description="The msg_id of the specific user message that this reply should target.",
required=True,
enum_values=None,
)
reply_builder.add_param(
name="quote",
param_type=ToolParamType.BOOLEAN,
description="Whether the visible reply should be sent as a quoted reply to the target msg_id.",
required=False,
enum_values=None,
)
reply_builder.add_param(
name="unknown_words",
param_type=ToolParamType.ARRAY,
description="Optional list of words or phrases that may need jargon lookup before replying.",
required=False,
enum_values=None,
items_schema={"type": "string"},
)
tools.append(reply_builder.build())
query_jargon_builder = ToolOptionBuilder()
query_jargon_builder.set_name("query_jargon")
query_jargon_builder.set_description(
"Query the meanings of one or more jargon words in the current chat context."
)
query_jargon_builder.add_param(
name="words",
param_type=ToolParamType.ARRAY,
description="A list of words or phrases to query from the jargon store.",
required=True,
enum_values=None,
items_schema={"type": "string"},
)
tools.append(query_jargon_builder.build())
query_person_info_builder = ToolOptionBuilder()
query_person_info_builder.set_name("query_person_info")
query_person_info_builder.set_description(
"Query profile and memory information about a specific person by person name, nickname, or user ID."
)
query_person_info_builder.add_param(
name="person_name",
param_type=ToolParamType.STRING,
description="The person's name, nickname, or user ID to search for.",
required=True,
enum_values=None,
)
query_person_info_builder.add_param(
name="limit",
param_type=ToolParamType.INTEGER,
description="Maximum number of matched person records to return. Defaults to 3.",
required=False,
enum_values=None,
)
tools.append(query_person_info_builder.build())
no_reply_builder = ToolOptionBuilder()
no_reply_builder.set_name("no_reply")
no_reply_builder.set_description("Do not emit a visible reply this round and continue thinking.")
tools.append(no_reply_builder.build())
stop_builder = ToolOptionBuilder()
stop_builder.set_name("stop")
stop_builder.set_description("Stop the current inner loop and return control to the outer chat flow.")
tools.append(stop_builder.build())
send_emoji_builder = ToolOptionBuilder()
send_emoji_builder.set_name("send_emoji")
send_emoji_builder.set_description(
"Send an emoji sticker to help express emotions. "
"You should specify the emotion type to select an appropriate emoji."
)
send_emoji_builder.add_param(
name="emotion",
param_type=ToolParamType.STRING,
description="The emotion type for selecting an appropriate emoji (e.g., 'happy', 'sad', 'angry', 'surprised', etc.).",
required=False,
enum_values=None,
)
tools.append(send_emoji_builder.build())
return tools
def get_builtin_tools() -> List[ToolOption]:
"""Return built-in tools."""
return create_builtin_tools()
def create_builtin_tool_specs() -> List[ToolSpec]:
"""创建 Maisaka 内置工具声明列表。
Returns:
List[ToolSpec]: 内置工具声明列表。
"""
return [
_build_tool_spec(
name="wait",
brief_description="暂停当前对话并等待用户新的输入。",
parameters_schema={
"type": "object",
"properties": {
"seconds": {
"type": "integer",
"description": "等待的秒数。",
},
},
"required": ["seconds"],
},
),
_build_tool_spec(
name="reply",
brief_description="根据当前思考生成并发送一条可见回复。",
parameters_schema={
"type": "object",
"properties": {
"msg_id": {
"type": "string",
"description": "要回复的目标用户消息编号。",
},
"quote": {
"type": "boolean",
"description": "是否以引用回复的方式发送。",
"default": True,
},
"unknown_words": {
"type": "array",
"description": "回复前可能需要查询的黑话或词条列表。",
"items": {"type": "string"},
},
},
"required": ["msg_id"],
},
),
_build_tool_spec(
name="query_jargon",
brief_description="查询当前聊天上下文中的黑话或词条含义。",
parameters_schema={
"type": "object",
"properties": {
"words": {
"type": "array",
"description": "要查询的词条列表。",
"items": {"type": "string"},
},
},
"required": ["words"],
},
),
_build_tool_spec(
name="query_person_info",
brief_description="查询某个人的档案和相关记忆信息。",
parameters_schema={
"type": "object",
"properties": {
"person_name": {
"type": "string",
"description": "人物名称、昵称或用户 ID。",
},
"limit": {
"type": "integer",
"description": "最多返回多少条匹配记录。",
"default": 3,
},
},
"required": ["person_name"],
},
),
_build_tool_spec(
name="no_reply",
brief_description="本轮不发送可见回复,继续下一步思考。",
),
_build_tool_spec(
name="stop",
brief_description="暂停当前内部循环,等待新的外部消息。",
),
_build_tool_spec(
name="send_emoji",
brief_description="发送一个合适的表情包来辅助表达情绪。",
parameters_schema={
"type": "object",
"properties": {
"emotion": {
"type": "string",
"description": "希望表达的情绪,例如 happy、sad、angry 等。",
},
},
},
),
]
def get_builtin_tool_specs() -> List[ToolSpec]:
"""获取 Maisaka 内置工具声明。
Returns:
List[ToolSpec]: 内置工具声明列表。
"""
return create_builtin_tool_specs()
def get_builtin_tools() -> List[ToolDefinitionInput]:
"""获取兼容旧模型层的内置工具定义。
Returns:
List[ToolDefinitionInput]: 可直接传给模型层的工具定义。
"""
return [tool_spec.to_llm_definition() for tool_spec in create_builtin_tool_specs()]

View File

@@ -1,5 +1,7 @@
from dataclasses import dataclass
"""Maisaka 对话循环服务。"""
from base64 import b64decode
from dataclasses import dataclass
from datetime import datetime
from io import BytesIO
from time import perf_counter
@@ -20,6 +22,7 @@ from src.common.data_models.message_component_data_model import MessageSequence,
from src.common.logger import get_logger
from src.common.prompt_i18n import load_prompt
from src.config.config import global_config
from src.core.tooling import ToolRegistry
from src.know_u.knowledge import extract_category_ids_from_result
from src.llm_models.model_client.base_client import BaseClient
from src.llm_models.payload_content.message import Message, MessageBuilder, RoleType
@@ -52,10 +55,19 @@ class MaisakaChatLoopService:
temperature: float = 0.5,
max_tokens: int = 2048,
) -> None:
"""初始化 Maisaka 对话循环服务。
Args:
chat_system_prompt: 可选的系统提示词。
temperature: 规划器温度参数。
max_tokens: 规划器最大输出长度。
"""
self._temperature = temperature
self._max_tokens = max_tokens
self._extra_tools: List[ToolOption] = []
self._interrupt_flag: asyncio.Event | None = None
self._tool_registry: ToolRegistry | None = None
self._prompts_loaded = False
self._prompt_load_lock = asyncio.Lock()
self._personality_prompt = self._build_personality_prompt()
@@ -67,9 +79,13 @@ class MaisakaChatLoopService:
@property
def personality_prompt(self) -> str:
"""返回当前人格提示词。"""
return self._personality_prompt
def _build_personality_prompt(self) -> str:
"""构造人格提示词。"""
try:
bot_name = global_config.bot.nickname
if global_config.bot.alias_names:
@@ -92,6 +108,12 @@ class MaisakaChatLoopService:
return "Your name is MaiMai; persona: lively and cute AI assistant."
async def ensure_chat_prompt_loaded(self, tools_section: str = "") -> None:
"""确保主聊天提示词已经加载完成。
Args:
tools_section: 额外注入到提示词中的工具说明片段。
"""
if self._prompts_loaded:
return
@@ -112,8 +134,23 @@ class MaisakaChatLoopService:
self._prompts_loaded = True
def set_extra_tools(self, tools: List[ToolDefinitionInput]) -> None:
"""设置额外工具定义。
Args:
tools: 兼容旧接口的额外工具定义列表。
"""
self._extra_tools = normalize_tool_options(tools) or []
def set_tool_registry(self, tool_registry: ToolRegistry | None) -> None:
"""设置统一工具注册表。
Args:
tool_registry: 统一工具注册表;传入 ``None`` 时退回旧工具列表模式。
"""
self._tool_registry = tool_registry
def set_interrupt_flag(self, interrupt_flag: asyncio.Event | None) -> None:
"""设置当前 planner 请求使用的中断标记。"""
self._interrupt_flag = interrupt_flag
@@ -329,6 +366,15 @@ class MaisakaChatLoopService:
)
async def chat_loop_step(self, chat_history: List[LLMContextMessage]) -> ChatResponse:
"""执行一轮 Maisaka 规划器请求。
Args:
chat_history: 当前对话历史。
Returns:
ChatResponse: 本轮规划器返回结果。
"""
await self.ensure_chat_prompt_loaded()
selected_history, selection_reason = self._select_llm_context_messages(chat_history)
@@ -336,7 +382,11 @@ class MaisakaChatLoopService:
del _client
return self._build_request_messages(selected_history)
all_tools: List[ToolDefinitionInput] = [*get_builtin_tools(), *self._extra_tools]
all_tools: List[ToolDefinitionInput]
if self._tool_registry is not None:
all_tools = await self._tool_registry.get_llm_definitions()
else:
all_tools = [*get_builtin_tools(), *self._extra_tools]
built_messages = self._build_request_messages(selected_history)
ordered_panels: List[Panel] = []

View File

@@ -20,6 +20,7 @@ from src.common.database.database import get_db_session
from src.common.database.database_model import PersonInfo
from src.common.logger import get_logger
from src.config.config import global_config
from src.core.tooling import ToolExecutionContext, ToolExecutionResult, ToolInvocation
from src.know_u.knowledge_store import get_knowledge_store
from src.learners.jargon_explainer import search_jargon
from src.llm_models.exceptions import ReqAbortException
@@ -37,13 +38,10 @@ from .message_adapter import (
clone_message_sequence,
format_speaker_content,
)
from .tool_handlers import (
handle_mcp_tool,
handle_unknown_tool,
)
if TYPE_CHECKING:
from .runtime import MaisakaHeartFlowChatting
from .tool_provider import BuiltinToolHandler
logger = get_logger("maisaka_reasoning_engine")
@@ -55,6 +53,23 @@ class MaisakaReasoningEngine:
self._runtime = runtime
self._last_reasoning_content: str = ""
def build_builtin_tool_handlers(self) -> dict[str, "BuiltinToolHandler"]:
"""构造 Maisaka 内置工具处理器映射。
Returns:
dict[str, BuiltinToolHandler]: 工具名到处理器的映射。
"""
return {
"reply": self._invoke_reply_tool,
"no_reply": self._invoke_no_reply_tool,
"query_jargon": self._invoke_query_jargon_tool,
"query_person_info": self._invoke_query_person_info_tool,
"wait": self._invoke_wait_tool,
"stop": self._invoke_stop_tool,
"send_emoji": self._invoke_send_emoji_tool,
}
async def run_loop(self) -> None:
"""独立消费消息批次,并执行对应的内部思考轮次。"""
try:
@@ -360,79 +375,287 @@ class MaisakaReasoningEngine:
return processed_segments
return [reply_text.strip()]
def _build_tool_invocation(self, tool_call: ToolCall, latest_thought: str) -> ToolInvocation:
"""将模型输出的工具调用转换为统一调用对象。
Args:
tool_call: 模型返回的工具调用。
latest_thought: 当前轮的最新思考文本。
Returns:
ToolInvocation: 统一工具调用对象。
"""
return ToolInvocation(
tool_name=tool_call.func_name,
arguments=dict(tool_call.args or {}),
call_id=tool_call.call_id,
session_id=self._runtime.session_id,
stream_id=self._runtime.session_id,
reasoning=latest_thought,
)
def _build_tool_execution_context(
self,
latest_thought: str,
anchor_message: SessionMessage,
) -> ToolExecutionContext:
"""构造统一工具执行上下文。
Args:
latest_thought: 当前轮的最新思考文本。
anchor_message: 当前轮的锚点消息。
Returns:
ToolExecutionContext: 统一工具执行上下文。
"""
return ToolExecutionContext(
session_id=self._runtime.session_id,
stream_id=self._runtime.session_id,
reasoning=latest_thought,
metadata={"anchor_message": anchor_message},
)
def _append_tool_execution_result(self, tool_call: ToolCall, result: ToolExecutionResult) -> None:
"""将统一工具执行结果写回 Maisaka 历史。
Args:
tool_call: 原始工具调用对象。
result: 统一工具执行结果。
"""
history_content = result.get_history_content()
if not history_content:
history_content = "工具执行成功。" if result.success else f"工具 {tool_call.func_name} 执行失败。"
self._runtime._chat_history.append(
ToolResultMessage(
content=history_content,
timestamp=datetime.now(),
tool_call_id=tool_call.call_id,
tool_name=tool_call.func_name,
)
)
@staticmethod
def _build_tool_call_from_invocation(invocation: ToolInvocation) -> ToolCall:
"""将统一工具调用对象恢复为 `ToolCall` 兼容对象。
Args:
invocation: 统一工具调用对象。
Returns:
ToolCall: 兼容旧内部逻辑的工具调用对象。
"""
return ToolCall(
call_id=invocation.call_id or f"{invocation.tool_name}_call",
func_name=invocation.tool_name,
args=dict(invocation.arguments),
)
@staticmethod
def _build_tool_success_result(
tool_name: str,
content: str = "",
structured_content: Any = None,
metadata: Optional[dict[str, Any]] = None,
) -> ToolExecutionResult:
"""构造统一工具成功结果。
Args:
tool_name: 工具名称。
content: 结果文本。
structured_content: 结构化结果。
metadata: 附加元数据。
Returns:
ToolExecutionResult: 统一工具成功结果。
"""
return ToolExecutionResult(
tool_name=tool_name,
success=True,
content=content,
structured_content=structured_content,
metadata=dict(metadata or {}),
)
@staticmethod
def _build_tool_failure_result(
tool_name: str,
error_message: str,
structured_content: Any = None,
metadata: Optional[dict[str, Any]] = None,
) -> ToolExecutionResult:
"""构造统一工具失败结果。
Args:
tool_name: 工具名称。
error_message: 错误信息。
structured_content: 结构化结果。
metadata: 附加元数据。
Returns:
ToolExecutionResult: 统一工具失败结果。
"""
return ToolExecutionResult(
tool_name=tool_name,
success=False,
error_message=error_message,
structured_content=structured_content,
metadata=dict(metadata or {}),
)
async def _invoke_reply_tool(
self,
invocation: ToolInvocation,
context: Optional[ToolExecutionContext] = None,
) -> ToolExecutionResult:
"""执行 reply 内置工具。"""
latest_thought = context.reasoning if context is not None else invocation.reasoning
return await self._handle_reply(self._build_tool_call_from_invocation(invocation), latest_thought)
async def _invoke_no_reply_tool(
self,
invocation: ToolInvocation,
context: Optional[ToolExecutionContext] = None,
) -> ToolExecutionResult:
"""执行 no_reply 内置工具。"""
del context
return self._build_tool_success_result(invocation.tool_name, "本轮未发送可见回复。")
async def _invoke_query_jargon_tool(
self,
invocation: ToolInvocation,
context: Optional[ToolExecutionContext] = None,
) -> ToolExecutionResult:
"""执行 query_jargon 内置工具。"""
del context
return await self._handle_query_jargon(self._build_tool_call_from_invocation(invocation))
async def _invoke_query_person_info_tool(
self,
invocation: ToolInvocation,
context: Optional[ToolExecutionContext] = None,
) -> ToolExecutionResult:
"""执行 query_person_info 内置工具。"""
del context
return await self._handle_query_person_info(self._build_tool_call_from_invocation(invocation))
async def _invoke_wait_tool(
self,
invocation: ToolInvocation,
context: Optional[ToolExecutionContext] = None,
) -> ToolExecutionResult:
"""执行 wait 内置工具。"""
del context
seconds = invocation.arguments.get("seconds", 30)
try:
wait_seconds = int(seconds)
except (TypeError, ValueError):
wait_seconds = 30
wait_seconds = max(0, wait_seconds)
self._runtime._enter_wait_state(seconds=wait_seconds, tool_call_id=invocation.call_id)
return self._build_tool_success_result(
invocation.tool_name,
f"当前对话循环进入等待状态,最长等待 {wait_seconds} 秒。",
metadata={"pause_execution": True},
)
async def _invoke_stop_tool(
self,
invocation: ToolInvocation,
context: Optional[ToolExecutionContext] = None,
) -> ToolExecutionResult:
"""执行 stop 内置工具。"""
del context
self._runtime._enter_stop_state()
return self._build_tool_success_result(
invocation.tool_name,
"当前对话循环已暂停,等待新消息到来。",
metadata={"pause_execution": True},
)
async def _invoke_send_emoji_tool(
self,
invocation: ToolInvocation,
context: Optional[ToolExecutionContext] = None,
) -> ToolExecutionResult:
"""执行 send_emoji 内置工具。"""
del context
return await self._handle_send_emoji(self._build_tool_call_from_invocation(invocation))
async def _handle_tool_calls(
self,
tool_calls: list[ToolCall],
latest_thought: str,
anchor_message: SessionMessage,
) -> bool:
"""执行一批统一工具调用。
Args:
tool_calls: 模型返回的工具调用列表。
latest_thought: 当前轮的最新思考文本。
anchor_message: 当前轮的锚点消息。
Returns:
bool: 是否需要暂停当前思考循环。
"""
if self._runtime._tool_registry is None:
for tool_call in tool_calls:
self._append_tool_execution_result(
tool_call,
ToolExecutionResult(
tool_name=tool_call.func_name,
success=False,
error_message="统一工具注册表尚未初始化。",
),
)
return False
execution_context = self._build_tool_execution_context(latest_thought, anchor_message)
for tool_call in tool_calls:
if tool_call.func_name == "reply":
reply_sent = await self._handle_reply(tool_call, latest_thought, anchor_message)
if not reply_sent:
logger.warning(
f"{self._runtime.log_prefix} 回复工具未生成可见消息,将继续下一轮循环"
)
continue
invocation = self._build_tool_invocation(tool_call, latest_thought)
result = await self._runtime._tool_registry.invoke(invocation, execution_context)
self._append_tool_execution_result(tool_call, result)
if tool_call.func_name == "no_reply":
self._runtime._chat_history.append(
self._build_tool_message(
tool_call,
"本轮未发送可见回复。",
)
)
continue
if not result.success and tool_call.func_name == "reply":
logger.warning(f"{self._runtime.log_prefix} 回复工具未生成可见消息,将继续下一轮循环")
if tool_call.func_name == "query_jargon":
await self._handle_query_jargon(tool_call)
continue
if tool_call.func_name == "query_person_info":
await self._handle_query_person_info(tool_call)
continue
if tool_call.func_name == "wait":
seconds = (tool_call.args or {}).get("seconds", 30)
try:
wait_seconds = int(seconds)
except (TypeError, ValueError):
wait_seconds = 30
wait_seconds = max(0, wait_seconds)
self._runtime._enter_wait_state(seconds=wait_seconds, tool_call_id=tool_call.call_id)
if bool(result.metadata.get("pause_execution", False)):
return True
if tool_call.func_name == "stop":
self._runtime._chat_history.append(
self._build_tool_message(
tool_call,
"当前对话循环已暂停,等待新消息到来。",
)
)
self._runtime._enter_stop_state()
return True
if tool_call.func_name == "send_emoji":
await self._handle_send_emoji(tool_call, anchor_message)
continue
if self._runtime._mcp_manager and self._runtime._mcp_manager.is_mcp_tool(tool_call.func_name):
await handle_mcp_tool(tool_call, self._runtime._chat_history, self._runtime._mcp_manager)
continue
await handle_unknown_tool(tool_call, self._runtime._chat_history)
return False
async def _handle_query_jargon(self, tool_call: ToolCall) -> None:
async def _handle_query_jargon(self, tool_call: ToolCall) -> ToolExecutionResult:
"""查询黑话解释并返回统一工具结果。
Args:
tool_call: 当前工具调用。
Returns:
ToolExecutionResult: 统一工具执行结果。
"""
tool_args = tool_call.args or {}
raw_words = tool_args.get("words")
if not isinstance(raw_words, list):
self._runtime._chat_history.append(
self._build_tool_message(tool_call, "查询黑话工具需要提供 `words` 数组参数。")
return self._build_tool_failure_result(
tool_call.func_name,
"查询黑话工具需要提供 `words` 数组参数。",
)
return
words: list[str] = []
seen_words: set[str] = set()
@@ -446,10 +669,10 @@ class MaisakaReasoningEngine:
words.append(word)
if not words:
self._runtime._chat_history.append(
self._build_tool_message(tool_call, "查询黑话工具至少需要一个非空词条。")
return self._build_tool_failure_result(
tool_call.func_name,
"查询黑话工具至少需要一个非空词条。",
)
return
logger.info(f"{self._runtime.log_prefix} 已触发黑话查询: 词条={words!r}")
@@ -479,31 +702,38 @@ class MaisakaReasoningEngine:
)
logger.info(f"{self._runtime.log_prefix} 黑话查询完成: 结果={results!r}")
self._runtime._chat_history.append(
self._build_tool_message(
tool_call,
json.dumps({"results": results}, ensure_ascii=False),
)
return self._build_tool_success_result(
tool_call.func_name,
json.dumps({"results": results}, ensure_ascii=False),
structured_content={"results": results},
)
async def _handle_query_person_info(self, tool_call: ToolCall) -> None:
"""查询指定人物的档案和相关知识。"""
async def _handle_query_person_info(self, tool_call: ToolCall) -> ToolExecutionResult:
"""查询指定人物的档案和相关知识。
Args:
tool_call: 当前工具调用。
Returns:
ToolExecutionResult: 统一工具执行结果。
"""
tool_args = tool_call.args or {}
raw_person_name = tool_args.get("person_name")
raw_limit = tool_args.get("limit", 3)
if not isinstance(raw_person_name, str):
self._runtime._chat_history.append(
self._build_tool_message(tool_call, "查询人物信息工具需要提供字符串类型的 `person_name` 参数。")
return self._build_tool_failure_result(
tool_call.func_name,
"查询人物信息工具需要提供字符串类型的 `person_name` 参数。",
)
return
person_name = raw_person_name.strip()
if not person_name:
self._runtime._chat_history.append(
self._build_tool_message(tool_call, "查询人物信息工具需要提供非空的 `person_name` 参数。")
return self._build_tool_failure_result(
tool_call.func_name,
"查询人物信息工具需要提供非空的 `person_name` 参数。",
)
return
try:
limit = max(1, min(int(raw_limit), 10))
@@ -526,11 +756,10 @@ class MaisakaReasoningEngine:
f"{self._runtime.log_prefix} 人物信息查询完成: "
f"人物记录数={len(result['persons'])} 相关知识数={len(result['related_knowledge'])}"
)
self._runtime._chat_history.append(
self._build_tool_message(
tool_call,
json.dumps(result, ensure_ascii=False),
)
return self._build_tool_success_result(
tool_call.func_name,
json.dumps(result, ensure_ascii=False),
structured_content=result,
)
def _query_person_records(self, person_name: str, limit: int) -> list[dict[str, Any]]:
@@ -632,25 +861,34 @@ class MaisakaReasoningEngine:
self,
tool_call: ToolCall,
latest_thought: str,
anchor_message: SessionMessage,
) -> bool:
) -> ToolExecutionResult:
"""执行 reply 工具并生成可见回复。
Args:
tool_call: 当前工具调用。
latest_thought: 当前轮的最新思考文本。
Returns:
ToolExecutionResult: 统一工具执行结果。
"""
tool_args = tool_call.args or {}
target_message_id = str(tool_args.get("msg_id") or "").strip()
quote_reply = bool(tool_args.get("quote", True))
raw_unknown_words = tool_args.get("unknown_words")
unknown_words = raw_unknown_words if isinstance(raw_unknown_words, list) else None
if not target_message_id:
self._runtime._chat_history.append(
self._build_tool_message(tool_call, "回复工具需要提供有效的 `msg_id` 参数。")
return self._build_tool_failure_result(
tool_call.func_name,
"回复工具需要提供有效的 `msg_id` 参数。",
)
return False
target_message = self._runtime._source_messages_by_id.get(target_message_id)
if target_message is None:
self._runtime._chat_history.append(
self._build_tool_message(tool_call, f"未找到要回复的目标消息msg_id={target_message_id}")
return self._build_tool_failure_result(
tool_call.func_name,
f"未找到要回复的目标消息msg_id={target_message_id}",
)
return False
logger.info(
f"{self._runtime.log_prefix} 已触发回复工具: "
@@ -668,17 +906,17 @@ class MaisakaReasoningEngine:
f"{self._runtime.log_prefix} 获取回复生成器时发生异常: "
f"目标消息编号={target_message_id}"
)
self._runtime._chat_history.append(
self._build_tool_message(tool_call, "获取 Maisaka 回复生成器时发生异常。")
return self._build_tool_failure_result(
tool_call.func_name,
"获取 Maisaka 回复生成器时发生异常。",
)
return False
if replyer is None:
logger.error(f"{self._runtime.log_prefix} 获取 Maisaka 回复生成器失败")
self._runtime._chat_history.append(
self._build_tool_message(tool_call, "Maisaka 回复生成器当前不可用。")
return self._build_tool_failure_result(
tool_call.func_name,
"Maisaka 回复生成器当前不可用。",
)
return False
from src.chat.replyer.maisaka_generator import MaisakaReplyGenerator
@@ -701,10 +939,10 @@ class MaisakaReasoningEngine:
f"{self._runtime.log_prefix} 回复生成器执行异常: 目标消息编号={target_message_id} "
f"异常类型={type(exc).__name__} 异常信息={str(exc)}\n{traceback.format_exc()}"
)
self._runtime._chat_history.append(
self._build_tool_message(tool_call, "生成可见回复时发生异常。")
return self._build_tool_failure_result(
tool_call.func_name,
"生成可见回复时发生异常。",
)
return False
logger.info(
f"{self._runtime.log_prefix} 回复生成完成: "
@@ -717,10 +955,10 @@ class MaisakaReasoningEngine:
f"{self._runtime.log_prefix} 回复生成器返回空文本: "
f"目标消息编号={target_message_id} 错误信息={reply_result.error_message!r}"
)
self._runtime._chat_history.append(
self._build_tool_message(tool_call, "生成可见回复失败。")
return self._build_tool_failure_result(
tool_call.func_name,
"生成可见回复失败。",
)
return False
reply_segments = self._post_process_reply_text(reply_text)
combined_reply_text = "".join(reply_segments)
@@ -751,19 +989,25 @@ class MaisakaReasoningEngine:
logger.exception(
f"{self._runtime.log_prefix} 发送文字消息时发生异常,目标消息编号={target_message_id}"
)
self._runtime._chat_history.append(
self._build_tool_message(tool_call, "发送可见回复时发生异常。")
return self._build_tool_failure_result(
tool_call.func_name,
"发送可见回复时发生异常。",
)
return False
logger.info(
f"{self._runtime.log_prefix} 引导回复发送结果: "
f"目标消息编号={target_message_id} 发送成功={sent}"
)
tool_result = "可见回复已生成并发送。" if sent else "可见回复生成成功,但发送失败。"
self._runtime._chat_history.append(self._build_tool_message(tool_call, tool_result))
if not sent:
return False
return self._build_tool_failure_result(
tool_call.func_name,
"可见回复生成成功,但发送失败。",
structured_content={
"msg_id": target_message_id,
"quote": quote_reply,
"reply_segments": reply_segments,
},
)
target_user_info = target_message.message_info.user_info
target_user_name = (
@@ -807,14 +1051,26 @@ class MaisakaReasoningEngine:
)
history_message.visible_text = visible_reply_text
self._runtime._chat_history.append(history_message)
return True
return self._build_tool_success_result(
tool_call.func_name,
"可见回复已生成并发送。",
structured_content={
"msg_id": target_message_id,
"quote": quote_reply,
"reply_text": combined_reply_text,
"reply_segments": reply_segments,
"target_user_name": target_user_name,
},
)
async def _handle_send_emoji(self, tool_call: ToolCall, anchor_message: SessionMessage) -> None:
async def _handle_send_emoji(self, tool_call: ToolCall) -> ToolExecutionResult:
"""处理发送表情包的工具调用。
Args:
tool_call: 工具调用对象
anchor_message: 锚点消息
tool_call: 工具调用对象
Returns:
ToolExecutionResult: 统一工具执行结果。
"""
from src.chat.emoji_system.emoji_manager import emoji_manager
from src.common.utils.utils_image import ImageUtils
@@ -827,10 +1083,10 @@ class MaisakaReasoningEngine:
# 获取表情包列表
if not emoji_manager.emojis:
self._runtime._chat_history.append(
self._build_tool_message(tool_call, "当前表情包库中没有可用表情。")
return self._build_tool_failure_result(
tool_call.func_name,
"当前表情包库中没有可用表情。",
)
return
# 根据情感选择表情包
selected_emoji = None
@@ -867,10 +1123,10 @@ class MaisakaReasoningEngine:
logger.error(
f"{self._runtime.log_prefix} 表情图片转换为 base64 失败: {exc}"
)
self._runtime._chat_history.append(
self._build_tool_message(tool_call, f"发送表情包失败:{exc}")
return self._build_tool_failure_result(
tool_call.func_name,
f"发送表情包失败:{exc}",
)
return
# 发送表情包
try:
@@ -885,32 +1141,26 @@ class MaisakaReasoningEngine:
logger.exception(
f"{self._runtime.log_prefix} 发送表情包时发生异常: {exc}"
)
self._runtime._chat_history.append(
self._build_tool_message(tool_call, f"发送表情包时发生异常:{exc}")
return self._build_tool_failure_result(
tool_call.func_name,
f"发送表情包时发生异常:{exc}",
)
return
if sent:
logger.info(
f"{self._runtime.log_prefix} 表情包发送成功: "
f"描述={selected_emoji.description!r} 情绪标签={selected_emoji.emotion}"
)
self._runtime._chat_history.append(
self._build_tool_message(
tool_call,
f"已发送表情包:{selected_emoji.description}(情绪:{', '.join(selected_emoji.emotion)}"
)
return self._build_tool_success_result(
tool_call.func_name,
f"已发送表情包:{selected_emoji.description}(情绪:{', '.join(selected_emoji.emotion)}",
structured_content={
"description": selected_emoji.description,
"emotion": list(selected_emoji.emotion),
},
)
else:
logger.warning(f"{self._runtime.log_prefix} 表情包发送失败")
self._runtime._chat_history.append(
self._build_tool_message(tool_call, "发送表情包失败。")
)
def _build_tool_message(self, tool_call: ToolCall, content: str) -> ToolResultMessage:
return ToolResultMessage(
content=content,
timestamp=datetime.now(),
tool_call_id=tool_call.call_id,
tool_name=tool_call.func_name,
logger.warning(f"{self._runtime.log_prefix} 表情包发送失败")
return self._build_tool_failure_result(
tool_call.func_name,
"发送表情包失败。",
)

View File

@@ -1,7 +1,7 @@
"""Maisaka runtime for non-CLI integrations."""
"""Maisaka 非 CLI 运行时。"""
from pathlib import Path
from typing import Literal, Optional, cast
from typing import Literal, Optional
import asyncio
import time
@@ -13,21 +13,24 @@ from src.common.data_models.mai_message_data_model import GroupInfo, UserInfo
from src.common.logger import get_logger
from src.common.utils.utils_config import ExpressionConfigUtils
from src.config.config import global_config
from src.core.tooling import ToolRegistry
from src.know_u.knowledge import KnowledgeLearner
from src.learners.expression_learner import ExpressionLearner
from src.learners.jargon_miner import JargonMiner
from src.llm_models.payload_content.tool_option import ToolDefinitionInput
from src.mcp_module import MCPManager
from src.mcp_module.provider import MCPToolProvider
from src.plugin_runtime.tool_provider import PluginToolProvider
from .chat_loop_service import MaisakaChatLoopService
from .context_messages import LLMContextMessage
from .reasoning_engine import MaisakaReasoningEngine
from .tool_provider import MaisakaBuiltinToolProvider
logger = get_logger("maisaka_runtime")
class MaisakaHeartFlowChatting:
"""Session-scoped Maisaka runtime."""
"""会话级别的 Maisaka 运行时。"""
_STATE_RUNNING: Literal["running"] = "running"
_STATE_WAIT: Literal["wait"] = "wait"
@@ -79,9 +82,11 @@ class MaisakaHeartFlowChatting:
self._knowledge_learner = KnowledgeLearner(session_id)
self._reasoning_engine = MaisakaReasoningEngine(self)
self._tool_registry = ToolRegistry()
self._register_tool_providers()
async def start(self) -> None:
"""Start the runtime loop."""
"""启动运行时主循环。"""
if self._running:
self._ensure_background_tasks_running()
return
@@ -94,7 +99,7 @@ class MaisakaHeartFlowChatting:
logger.info(f"{self.log_prefix} Maisaka 运行时已启动")
async def stop(self) -> None:
"""Stop the runtime loop."""
"""停止运行时主循环。"""
if not self._running:
return
@@ -121,18 +126,17 @@ class MaisakaHeartFlowChatting:
finally:
self._internal_loop_task = None
if self._mcp_manager is not None:
await self._mcp_manager.close()
self._mcp_manager = None
await self._tool_registry.close()
self._mcp_manager = None
logger.info(f"{self.log_prefix} Maisaka 运行时已停止")
def adjust_talk_frequency(self, frequency: float) -> None:
"""Compatibility shim for the existing manager API."""
"""兼容现有管理器接口的占位方法。"""
_ = frequency
async def register_message(self, message: SessionMessage) -> None:
"""Cache a new message and wake the main loop."""
"""缓存一条新消息并唤醒主循环。"""
if self._running:
self._ensure_background_tasks_running()
self.message_cache.append(message)
@@ -175,6 +179,15 @@ class MaisakaHeartFlowChatting:
self._loop_task = asyncio.create_task(self._main_loop())
logger.warning(f"{self.log_prefix} 已重新拉起 Maisaka 主循环任务")
def _register_tool_providers(self) -> None:
"""注册 Maisaka 运行时默认启用的工具 Provider。"""
self._tool_registry.register_provider(
MaisakaBuiltinToolProvider(self._reasoning_engine.build_builtin_tool_handlers())
)
self._tool_registry.register_provider(PluginToolProvider())
self._chat_loop_service.set_tool_registry(self._tool_registry)
async def _main_loop(self) -> None:
try:
while self._running:
@@ -215,7 +228,7 @@ class MaisakaHeartFlowChatting:
return self._last_processed_index < len(self.message_cache)
def _collect_pending_messages(self) -> list[SessionMessage]:
"""Collect one batch of unprocessed messages from message_cache."""
"""从消息缓存中收集一批尚未处理的消息。"""
start_index = self._last_processed_index
pending_messages = self.message_cache[start_index:]
if not pending_messages:
@@ -264,13 +277,13 @@ class MaisakaHeartFlowChatting:
return "timeout"
def _enter_wait_state(self, seconds: Optional[float] = None, tool_call_id: Optional[str] = None) -> None:
"""Enter wait state."""
"""切换到等待状态。"""
self._agent_state = self._STATE_WAIT
self._wait_until = None if seconds is None else time.time() + seconds
self._pending_wait_tool_call_id = tool_call_id
def _enter_stop_state(self) -> None:
"""Enter stop state."""
"""切换到停止状态。"""
self._agent_state = self._STATE_STOP
self._wait_until = None
self._pending_wait_tool_call_id = None
@@ -288,7 +301,7 @@ class MaisakaHeartFlowChatting:
logger.error(f"{self.log_prefix} 知识学习任务异常退出: {knowledge_result}")
async def _trigger_expression_learning(self, messages: list[SessionMessage]) -> None:
"""Trigger expression learning from the newly collected batch."""
"""基于新收集的一批消息触发表达学习。"""
self._expression_learner.add_messages(messages)
if not self._enable_expression_learning:
@@ -331,7 +344,7 @@ class MaisakaHeartFlowChatting:
logger.exception(f"{self.log_prefix} 表达学习失败")
async def _trigger_knowledge_learning(self, messages: list[SessionMessage]) -> None:
"""Trigger knowledge learning from the newly collected batch."""
"""基于新收集的一批消息触发知识学习。"""
self._knowledge_learner.add_messages(messages)
if not global_config.maisaka.enable_knowledge_module:
@@ -372,22 +385,21 @@ class MaisakaHeartFlowChatting:
logger.exception(f"{self.log_prefix} 知识学习失败")
async def _init_mcp(self) -> None:
"""Initialize MCP tools and inject them into the planner."""
"""初始化 MCP 工具并注册到统一工具层。"""
config_path = Path(__file__).resolve().parents[2] / "config" / "mcp_config.json"
self._mcp_manager = await MCPManager.from_config(str(config_path))
if self._mcp_manager is None:
logger.info(f"{self.log_prefix} MCP 管理器不可用")
return
mcp_tools = self._mcp_manager.get_openai_tools()
if not mcp_tools:
mcp_tool_specs = self._mcp_manager.get_tool_specs()
if not mcp_tool_specs:
logger.info(f"{self.log_prefix} 没有可供 Maisaka 使用的 MCP 工具")
return
mcp_tool_definitions = [cast(ToolDefinitionInput, tool) for tool in mcp_tools]
self._chat_loop_service.set_extra_tools(mcp_tool_definitions)
self._tool_registry.register_provider(MCPToolProvider(self._mcp_manager))
logger.info(
f"{self.log_prefix} 已向 Maisaka 加载 {len(mcp_tools)} 个 MCP 工具:\n"
f"{self.log_prefix} 已向 Maisaka 加载 {len(mcp_tool_specs)} 个 MCP 工具:\n"
f"{self._mcp_manager.get_tool_summary()}"
)

View File

@@ -0,0 +1,64 @@
"""Maisaka 内置工具 Provider。"""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from typing import Dict, Optional
from src.core.tooling import ToolExecutionContext, ToolExecutionResult, ToolInvocation, ToolProvider, ToolSpec
from .builtin_tools import get_builtin_tool_specs
BuiltinToolHandler = Callable[[ToolInvocation, Optional[ToolExecutionContext]], Awaitable[ToolExecutionResult]]
class MaisakaBuiltinToolProvider(ToolProvider):
"""Maisaka 内置工具提供者。"""
provider_name = "maisaka_builtin"
provider_type = "builtin"
def __init__(self, handlers: Optional[Dict[str, BuiltinToolHandler]] = None) -> None:
"""初始化内置工具 Provider。
Args:
handlers: 工具名到异步处理器的映射。
"""
self._handlers = dict(handlers or {})
async def list_tools(self) -> list[ToolSpec]:
"""列出全部内置工具。"""
return list(get_builtin_tool_specs())
async def invoke(
self,
invocation: ToolInvocation,
context: Optional[ToolExecutionContext] = None,
) -> ToolExecutionResult:
"""执行指定内置工具。
Args:
invocation: 工具调用请求。
context: 执行上下文。
Returns:
ToolExecutionResult: 工具执行结果。
"""
handler = self._handlers.get(invocation.tool_name)
if handler is None:
return ToolExecutionResult(
tool_name=invocation.tool_name,
success=False,
error_message=f"未找到内置工具处理器:{invocation.tool_name}",
)
return await handler(invocation, context)
async def close(self) -> None:
"""关闭 Provider。
内置 Provider 无需释放额外资源。
"""