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:
@@ -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()]
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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,
|
||||
"发送表情包失败。",
|
||||
)
|
||||
|
||||
@@ -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()}"
|
||||
)
|
||||
|
||||
|
||||
64
src/maisaka/tool_provider.py
Normal file
64
src/maisaka/tool_provider.py
Normal 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 无需释放额外资源。
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user