feat:修复门控多重result问题,新增at动作,插件现在运行chat_id指定或chat_type指定

This commit is contained in:
SengokuCola
2026-04-22 00:11:14 +08:00
parent 363c0a77b7
commit f1563ede65
12 changed files with 833 additions and 122 deletions

View File

@@ -1,12 +1,16 @@
"""Maisaka 内置工具聚合入口。"""
from collections.abc import Awaitable, Callable
from typing import Dict, List, Optional
from copy import deepcopy
from dataclasses import dataclass
from typing import Dict, List, Literal, Optional
from src.config.config import global_config
from src.core.tooling import ToolExecutionContext, ToolExecutionResult, ToolInvocation, ToolSpec
from src.core.tooling import ToolAvailabilityContext, ToolExecutionContext, ToolExecutionResult, ToolInvocation, ToolSpec
from src.llm_models.payload_content.tool_option import ToolDefinitionInput
from .at import get_tool_spec as get_at_tool_spec
from .at import handle_tool as handle_at_tool
from .context import BuiltinToolRuntimeContext
from .continue_tool import get_tool_spec as get_continue_tool_spec
from .continue_tool import handle_tool as handle_continue_tool
@@ -32,93 +36,152 @@ from .wait import get_tool_spec as get_wait_tool_spec
from .wait import handle_tool as handle_wait_tool
BuiltinToolHandler = Callable[[ToolInvocation, Optional[ToolExecutionContext]], Awaitable[ToolExecutionResult]]
BuiltinToolRawHandler = Callable[
[BuiltinToolRuntimeContext, ToolInvocation, Optional[ToolExecutionContext]],
Awaitable[ToolExecutionResult],
]
BuiltinToolStage = Literal["timing", "action"]
BuiltinToolVisibility = Literal["visible", "deferred", "hidden"]
BuiltinToolChatScope = Literal["all", "group", "private"]
def get_timing_tool_specs() -> List[ToolSpec]:
"""获取 Timing Gate 阶段可用的内置工具声明。"""
@dataclass(frozen=True)
class BuiltinToolEntry:
"""内置工具目录项,集中声明工具所属阶段与默认可见性。"""
return [
get_wait_tool_spec(),
get_no_reply_tool_spec(),
get_continue_tool_spec(),
]
name: str
get_spec: Callable[[], ToolSpec]
handle_tool: BuiltinToolRawHandler
stage: BuiltinToolStage
visibility: BuiltinToolVisibility = "visible"
chat_scope: BuiltinToolChatScope = "all"
def build_spec(self) -> ToolSpec:
"""生成带统一可见性元数据的工具声明。"""
tool_spec = deepcopy(self.get_spec())
tool_spec.metadata["builtin_stage"] = self.stage
tool_spec.metadata["visibility"] = self.visibility
return tool_spec
def get_action_tool_specs() -> List[ToolSpec]:
"""获取 Action Loop 阶段可用的内置工具声明。"""
def _get_query_memory_tool_spec() -> ToolSpec:
"""根据配置生成 query_memory 工具声明。"""
return [
get_finish_tool_spec(),
get_reply_tool_spec(),
get_view_complex_message_tool_spec(),
get_query_jargon_tool_spec(),
get_query_memory_tool_spec(enabled=bool(global_config.memory.enable_memory_query_tool)),
get_send_emoji_tool_spec(),
get_tool_search_tool_spec(),
]
return get_query_memory_tool_spec(enabled=bool(global_config.memory.enable_memory_query_tool))
def get_builtin_tool_specs() -> List[ToolSpec]:
"""获取默认暴露的 Maisaka 内置工具声明。"""
return get_action_tool_specs()
BUILTIN_TOOL_ENTRIES: List[BuiltinToolEntry] = [
BuiltinToolEntry("wait", get_wait_tool_spec, handle_wait_tool, stage="timing"),
BuiltinToolEntry("no_reply", get_no_reply_tool_spec, handle_no_reply_tool, stage="timing"),
BuiltinToolEntry("continue", get_continue_tool_spec, handle_continue_tool, stage="timing"),
BuiltinToolEntry("finish", get_finish_tool_spec, handle_finish_tool, stage="action"),
BuiltinToolEntry("reply", get_reply_tool_spec, handle_reply_tool, stage="action"),
BuiltinToolEntry(
"view_complex_message",
get_view_complex_message_tool_spec,
handle_view_complex_message_tool,
stage="action",
),
BuiltinToolEntry("query_jargon", get_query_jargon_tool_spec, handle_query_jargon_tool, stage="action"),
BuiltinToolEntry("query_memory", _get_query_memory_tool_spec, handle_query_memory_tool, stage="action"),
BuiltinToolEntry(
"query_person_info",
get_query_person_info_tool_spec,
handle_query_person_info_tool,
stage="action",
visibility="hidden",
),
BuiltinToolEntry("send_emoji", get_send_emoji_tool_spec, handle_send_emoji_tool, stage="action"),
BuiltinToolEntry(
"at",
get_at_tool_spec,
handle_at_tool,
stage="action",
visibility="deferred",
chat_scope="group",
),
BuiltinToolEntry("tool_search", get_tool_search_tool_spec, handle_tool_search_tool, stage="action"),
]
def get_all_builtin_tool_specs() -> List[ToolSpec]:
def _get_builtin_tool_entries(
*,
stage: Optional[BuiltinToolStage] = None,
visibility: Optional[BuiltinToolVisibility] = None,
context: Optional[ToolAvailabilityContext] = None,
) -> List[BuiltinToolEntry]:
"""按阶段与可见性筛选内置工具目录项。"""
entries = BUILTIN_TOOL_ENTRIES
if stage is not None:
entries = [entry for entry in entries if entry.stage == stage]
if visibility is not None:
entries = [entry for entry in entries if entry.visibility == visibility]
if context is not None:
entries = [entry for entry in entries if _is_builtin_tool_available(entry, context)]
return entries
def _is_builtin_tool_available(entry: BuiltinToolEntry, context: ToolAvailabilityContext) -> bool:
"""判断内置工具是否适用于当前聊天。"""
if entry.chat_scope == "all":
return True
if entry.chat_scope == "group":
return context.is_group_chat is True
if entry.chat_scope == "private":
return context.is_group_chat is False
return True
def get_builtin_tool_visibility(tool_spec: ToolSpec) -> BuiltinToolVisibility:
"""读取工具声明里的可见性。"""
raw_visibility = str(tool_spec.metadata.get("visibility") or "").strip()
if raw_visibility == "deferred":
return "deferred"
if raw_visibility == "hidden":
return "hidden"
return "visible"
def is_builtin_tool_in_action_stage(tool_spec: ToolSpec) -> bool:
"""判断内置工具是否属于 Action Loop 阶段。"""
return str(tool_spec.metadata.get("builtin_stage") or "").strip() == "action"
def get_all_builtin_tool_specs(context: Optional[ToolAvailabilityContext] = None) -> List[ToolSpec]:
"""获取全部内置工具声明。"""
return [
*get_timing_tool_specs(),
get_finish_tool_spec(),
get_reply_tool_spec(),
get_view_complex_message_tool_spec(),
get_query_jargon_tool_spec(),
get_query_memory_tool_spec(enabled=True),
get_query_person_info_tool_spec(),
get_send_emoji_tool_spec(),
get_tool_search_tool_spec(),
]
return [entry.build_spec() for entry in _get_builtin_tool_entries(context=context)]
def get_timing_tools() -> List[ToolDefinitionInput]:
"""获取 Timing Gate 阶段的兼容工具定义。"""
return [tool_spec.to_llm_definition() for tool_spec in get_timing_tool_specs()]
def get_action_tools() -> List[ToolDefinitionInput]:
"""获取 Action Loop 阶段的兼容工具定义。"""
return [tool_spec.to_llm_definition() for tool_spec in get_action_tool_specs()]
tool_specs = [
entry.build_spec()
for entry in _get_builtin_tool_entries(stage="timing", visibility="visible")
]
return [tool_spec.to_llm_definition() for tool_spec in tool_specs if tool_spec.enabled]
def get_builtin_tools() -> List[ToolDefinitionInput]:
"""获取默认暴露给模型层的内置工具定义。"""
return get_action_tools()
tool_specs = [
entry.build_spec()
for entry in _get_builtin_tool_entries(stage="action", visibility="visible")
]
return [tool_spec.to_llm_definition() for tool_spec in tool_specs if tool_spec.enabled]
def build_builtin_tool_handlers(tool_ctx: BuiltinToolRuntimeContext) -> Dict[str, BuiltinToolHandler]:
"""构建内置工具处理器映射。"""
return {
"continue": lambda invocation, context=None: handle_continue_tool(tool_ctx, invocation, context),
"finish": lambda invocation, context=None: handle_finish_tool(tool_ctx, invocation, context),
"reply": lambda invocation, context=None: handle_reply_tool(tool_ctx, invocation, context),
"no_reply": lambda invocation, context=None: handle_no_reply_tool(tool_ctx, invocation, context),
"query_jargon": lambda invocation, context=None: handle_query_jargon_tool(tool_ctx, invocation, context),
"query_memory": lambda invocation, context=None: handle_query_memory_tool(tool_ctx, invocation, context),
"query_person_info": lambda invocation, context=None: handle_query_person_info_tool(
tool_ctx,
invocation,
context,
),
"wait": lambda invocation, context=None: handle_wait_tool(tool_ctx, invocation, context),
"send_emoji": lambda invocation, context=None: handle_send_emoji_tool(tool_ctx, invocation, context),
"tool_search": lambda invocation, context=None: handle_tool_search_tool(tool_ctx, invocation, context),
"view_complex_message": lambda invocation, context=None: handle_view_complex_message_tool(
tool_ctx,
invocation,
context,
),
entry.name: lambda invocation, context=None, entry=entry: entry.handle_tool(tool_ctx, invocation, context)
for entry in BUILTIN_TOOL_ENTRIES
}

View File

@@ -0,0 +1,186 @@
"""Maisaka 内置 at 工具。"""
from typing import Any, Optional, TYPE_CHECKING
from src.cli.maisaka_cli_sender import CLI_PLATFORM_NAME, render_cli_message
from src.common.data_models.message_component_data_model import AtComponent, MessageSequence, TextComponent
from src.common.logger import get_logger
from src.core.tooling import ToolExecutionContext, ToolExecutionResult, ToolInvocation, ToolSpec
from src.services import send_service
if TYPE_CHECKING:
from .context import BuiltinToolRuntimeContext
logger = get_logger("maisaka_builtin_at")
def get_tool_spec() -> ToolSpec:
"""获取 at 工具声明。"""
return ToolSpec(
name="at",
brief_description="根据一条已知 msg_id 找到发言用户,并发送一条 @ 该用户的消息。",
detailed_description=(
"参数说明:\n"
"- msg_idstring必填。要 @ 的目标用户发过的消息编号。\n"
"- textstring可选。@ 后追加发送的短文本;只想单独 @ 人时留空。\n"
"请优先从上下文里选择一条明确属于目标用户的 msg_id不要凭昵称或印象猜测用户。"
),
parameters_schema={
"type": "object",
"properties": {
"msg_id": {
"type": "string",
"description": "要 @ 的目标用户发过的消息编号。",
},
"text": {
"type": "string",
"description": "@ 后追加发送的短文本;只想单独 @ 人时留空。",
"default": "",
},
},
"required": ["msg_id"],
},
provider_name="maisaka_builtin",
provider_type="builtin",
)
def _get_target_user_info(target_message: Any) -> tuple[str, str, str]:
"""从目标消息中提取可用于构造 at 组件的用户信息。"""
message_info = getattr(target_message, "message_info", None)
user_info = getattr(message_info, "user_info", None)
target_user_id = str(getattr(user_info, "user_id", "") or "").strip()
target_user_nickname = str(getattr(user_info, "user_nickname", "") or "").strip()
target_user_cardname = str(getattr(user_info, "user_cardname", "") or "").strip()
return target_user_id, target_user_nickname, target_user_cardname
def _build_at_message_sequence(
*,
target_user_id: str,
target_user_nickname: str = "",
target_user_cardname: str = "",
text: str = "",
) -> MessageSequence:
"""构造 @ 用户的消息组件序列。"""
components = [
AtComponent(
target_user_id=target_user_id,
target_user_nickname=target_user_nickname or None,
target_user_cardname=target_user_cardname or None,
)
]
normalized_text = text.strip()
if normalized_text:
components.append(TextComponent(f" {normalized_text}"))
return MessageSequence(components=components)
async def handle_tool(
tool_ctx: "BuiltinToolRuntimeContext",
invocation: ToolInvocation,
context: Optional[ToolExecutionContext] = None,
) -> ToolExecutionResult:
"""执行 at 内置工具。"""
del context
target_message_id = str(invocation.arguments.get("msg_id") or "").strip()
text = str(invocation.arguments.get("text") or "").strip()
if not target_message_id:
return tool_ctx.build_failure_result(
invocation.tool_name,
"at 工具需要提供有效的 `msg_id` 参数。",
)
if not str(getattr(tool_ctx.runtime.chat_stream, "group_id", "") or "").strip():
return tool_ctx.build_failure_result(
invocation.tool_name,
"at 工具只能在群聊中使用。",
structured_content={"msg_id": target_message_id},
)
target_message = tool_ctx.runtime._source_messages_by_id.get(target_message_id)
if target_message is None:
return tool_ctx.build_failure_result(
invocation.tool_name,
f"未找到要 @ 的目标消息msg_id={target_message_id}",
structured_content={"msg_id": target_message_id},
)
target_user_id, target_user_nickname, target_user_cardname = _get_target_user_info(target_message)
if not target_user_id:
return tool_ctx.build_failure_result(
invocation.tool_name,
f"目标消息缺少有效用户 IDmsg_id={target_message_id}",
structured_content={"msg_id": target_message_id},
)
target_user_name = target_user_cardname or target_user_nickname or target_user_id
message_sequence = _build_at_message_sequence(
target_user_id=target_user_id,
target_user_nickname=target_user_nickname,
target_user_cardname=target_user_cardname,
text=text,
)
display_message = f"@{target_user_name}" + (f" {text}" if text else "")
try:
if tool_ctx.runtime.chat_stream.platform == CLI_PLATFORM_NAME:
render_cli_message(display_message)
tool_ctx.append_guided_reply_to_chat_history(display_message)
sent_message = None
sent = True
else:
sent_message = await send_service._send_to_target_with_message(
message_sequence=message_sequence,
stream_id=tool_ctx.runtime.session_id,
display_message=display_message,
typing=False,
storage_message=True,
show_log=True,
sync_to_maisaka_history=True,
maisaka_source_kind="guided_reply",
)
sent = sent_message is not None
except Exception as exc:
logger.exception(
f"{tool_ctx.runtime.log_prefix} 发送 at 消息时发生异常: msg_id={target_message_id} user_id={target_user_id}"
)
return tool_ctx.build_failure_result(
invocation.tool_name,
f"发送 at 消息时发生异常:{exc}",
structured_content={
"msg_id": target_message_id,
"target_user_id": target_user_id,
"target_user_name": target_user_name,
},
)
if not sent:
return tool_ctx.build_failure_result(
invocation.tool_name,
"at 消息发送失败。",
structured_content={
"msg_id": target_message_id,
"target_user_id": target_user_id,
"target_user_name": target_user_name,
},
)
sent_message_id = str(getattr(sent_message, "message_id", "") or "").strip() if sent_message is not None else ""
tool_ctx.runtime._record_reply_sent()
return tool_ctx.build_success_result(
invocation.tool_name,
f"已 @ {target_user_name}",
structured_content={
"msg_id": target_message_id,
"target_user_id": target_user_id,
"target_user_name": target_user_name,
"text": text,
"sent_message_id": sent_message_id,
},
)

View File

@@ -12,7 +12,7 @@ from src.common.logger import get_logger
from src.common.prompt_i18n import load_prompt
from src.common.utils.utils_session import SessionUtils
from src.config.config import global_config
from src.core.tooling import ToolRegistry
from src.core.tooling import ToolAvailabilityContext, ToolRegistry
from src.llm_models.model_client.base_client import BaseClient
from src.llm_models.payload_content.message import Message, MessageBuilder, RoleType
from src.llm_models.payload_content.resp_format import RespFormat
@@ -502,7 +502,13 @@ class MaisakaChatLoopService:
if tool_definitions is not None:
all_tools = list(tool_definitions)
elif self._tool_registry is not None:
tool_specs = await self._tool_registry.list_tools()
tool_specs = await self._tool_registry.list_tools(
ToolAvailabilityContext(
session_id=self._session_id,
stream_id=self._session_id,
is_group_chat=self._is_group_chat,
)
)
all_tools = [tool_spec.to_llm_definition() for tool_spec in tool_specs]
else:
all_tools = [*get_builtin_tools(), *self._extra_tools]

View File

@@ -14,14 +14,14 @@ from src.chat.message_receive.message import SessionMessage
from src.common.data_models.message_component_data_model import EmojiComponent, ImageComponent, MessageSequence
from src.common.logger import get_logger
from src.common.prompt_i18n import load_prompt
from src.core.tooling import ToolExecutionContext, ToolExecutionResult, ToolInvocation, ToolSpec
from src.core.tooling import ToolAvailabilityContext, ToolExecutionContext, ToolExecutionResult, ToolInvocation, ToolSpec
from src.llm_models.exceptions import ReqAbortException
from src.llm_models.payload_content.tool_option import ToolCall
from src.services import database_service as database_api
from src.services.memory_service import memory_service
from .builtin_tool import get_action_tool_specs
from .builtin_tool import build_builtin_tool_handlers as build_split_builtin_tool_handlers
from .builtin_tool import get_builtin_tool_visibility, is_builtin_tool_in_action_stage
from .builtin_tool import get_timing_tools
from .chat_loop_service import ChatResponse
from .chat_history_visual_refresher import refresh_chat_history_visual_placeholders
@@ -54,8 +54,6 @@ logger = get_logger("maisaka_reasoning_engine")
TIMING_GATE_CONTEXT_LIMIT = 24
TIMING_GATE_MAX_TOKENS = 384
TIMING_GATE_TOOL_NAMES = {"continue", "no_reply", "wait"}
ACTION_HIDDEN_TOOL_NAMES = {"continue", "no_reply"}
ACTION_BUILTIN_TOOL_NAMES = {tool_spec.name for tool_spec in get_action_tool_specs()}
class MaisakaReasoningEngine:
@@ -175,15 +173,19 @@ class MaisakaReasoningEngine:
self._runtime.set_current_action_tool_names([])
return [], ""
tool_specs = await self._runtime._tool_registry.list_tools()
availability_context = self._build_tool_availability_context()
tool_specs = await self._runtime._tool_registry.list_tools(availability_context)
visible_builtin_tool_specs: list[ToolSpec] = []
deferred_tool_specs: list[ToolSpec] = []
for tool_spec in tool_specs:
if tool_spec.name in ACTION_HIDDEN_TOOL_NAMES:
continue
if tool_spec.provider_name == "maisaka_builtin":
if tool_spec.name in ACTION_BUILTIN_TOOL_NAMES:
if not is_builtin_tool_in_action_stage(tool_spec):
continue
visibility = get_builtin_tool_visibility(tool_spec)
if visibility == "visible":
visible_builtin_tool_specs.append(tool_spec)
elif visibility == "deferred":
deferred_tool_specs.append(tool_spec)
continue
deferred_tool_specs.append(tool_spec)
@@ -877,6 +879,19 @@ class MaisakaReasoningEngine:
reasoning=latest_thought,
)
def _build_tool_availability_context(self) -> ToolAvailabilityContext:
"""构造当前聊天的工具暴露上下文。"""
chat_stream = self._runtime.chat_stream
return ToolAvailabilityContext(
session_id=self._runtime.session_id,
stream_id=self._runtime.session_id,
is_group_chat=chat_stream.is_group_session,
group_id=str(getattr(chat_stream, "group_id", "") or "").strip(),
user_id=str(getattr(chat_stream, "user_id", "") or "").strip(),
platform=str(getattr(chat_stream, "platform", "") or "").strip(),
)
def _build_tool_execution_context(
self,
latest_thought: str,
@@ -1197,6 +1212,8 @@ class MaisakaReasoningEngine:
source_kind="timing_gate",
)
)
if tool_call.func_name == "wait":
return
self._append_tool_execution_result(tool_call, result)
def _build_tool_result_summary(self, tool_call: ToolCall, result: ToolExecutionResult) -> str:
@@ -1290,9 +1307,10 @@ class MaisakaReasoningEngine:
return False, tool_result_summaries, tool_monitor_results
execution_context = self._build_tool_execution_context(latest_thought, anchor_message)
availability_context = self._build_tool_availability_context()
tool_spec_map = {
tool_spec.name: tool_spec
for tool_spec in await self._runtime._tool_registry.list_tools()
for tool_spec in await self._runtime._tool_registry.list_tools(availability_context)
}
total_tool_count = len(tool_calls)
for tool_index, tool_call in enumerate(tool_calls, start=1):

View File

@@ -5,7 +5,14 @@ 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 src.core.tooling import (
ToolAvailabilityContext,
ToolExecutionContext,
ToolExecutionResult,
ToolInvocation,
ToolProvider,
ToolSpec,
)
from .builtin_tool import get_all_builtin_tool_specs
@@ -27,10 +34,13 @@ class MaisakaBuiltinToolProvider(ToolProvider):
self._handlers = dict(handlers or {})
async def list_tools(self) -> list[ToolSpec]:
async def list_tools(
self,
context: Optional[ToolAvailabilityContext] = None,
) -> list[ToolSpec]:
"""列出全部内置工具。"""
return list(get_all_builtin_tool_specs())
return list(get_all_builtin_tool_specs(context))
async def invoke(
self,