feat:展示更详细的工具信息,修改wait定义
This commit is contained in:
@@ -37,4 +37,4 @@
|
|||||||
|
|
||||||
{group_chat_attention_block}
|
{group_chat_attention_block}
|
||||||
|
|
||||||
现在,请你输出你对{bot_name}发言的分析,你必须先输出文本内容的分析,然后再进行工具调用,输出json形式的function call:
|
现在,请你输出你对{bot_name}发言的分析,你必须先输出文本内容的分析,然后再进行工具调用,:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
在当前场景中,不同的人正在互动({bot_name} 也是一位参与的用户),用户也可能正在连续发送消息或彼此互动。
|
在当前场景中,不同的人正在互动({bot_name} 也是一位参与的用户),用户也可能正在连续发送消息或彼此互动。
|
||||||
你的任务不是生成对别人可见的发言,也不是直接使用查询类工具,而是判断当前是否应该:
|
你的任务不是生成对别人可见的发言,也不是直接使用查询类工具,而是判断当前是否应该:
|
||||||
- continue:立刻进入下一轮完整思考、搜集信息、回复与其他工具执行
|
- continue:立刻进入下一轮完整思考、搜集信息、回复与其他工具执行
|
||||||
- wait:再等待一段时间,然后重新判断,可选几秒的等待,也可等待数分钟
|
- wait:固定再等待一段时间,时间到后再重新判断;等待期间即使收到新消息也不会提前打断,只会暂存到超时后统一处理
|
||||||
- no_reply:本轮不继续,直接等待新的外部消息
|
- no_reply:本轮不继续,直接等待新的外部消息
|
||||||
|
|
||||||
节奏控制规则:
|
节奏控制规则:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ dependencies = [
|
|||||||
"maim-message>=0.6.2",
|
"maim-message>=0.6.2",
|
||||||
"maibot-dashboard==1.0.0.dev2026040439",
|
"maibot-dashboard==1.0.0.dev2026040439",
|
||||||
"maibot-plugin-sdk>=2.3.0",
|
"maibot-plugin-sdk>=2.3.0",
|
||||||
|
"matplotlib>=3.10.5",
|
||||||
"mcp",
|
"mcp",
|
||||||
"msgpack>=1.1.2",
|
"msgpack>=1.1.2",
|
||||||
"numpy>=2.2.6",
|
"numpy>=2.2.6",
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
"""测试表达方式自动检查任务的数据库读取行为。"""
|
|
||||||
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from typing import Generator
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from sqlalchemy.pool import StaticPool
|
|
||||||
from sqlmodel import Session, SQLModel, create_engine
|
|
||||||
|
|
||||||
from src.bw_learner.expression_auto_check_task import ExpressionAutoCheckTask
|
|
||||||
from src.common.database.database_model import Expression
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="expression_auto_check_engine")
|
|
||||||
def expression_auto_check_engine_fixture() -> Generator:
|
|
||||||
"""创建用于表达方式自动检查任务测试的内存数据库引擎。
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
Generator: 供测试使用的 SQLite 内存引擎。
|
|
||||||
"""
|
|
||||||
|
|
||||||
engine = create_engine(
|
|
||||||
"sqlite://",
|
|
||||||
connect_args={"check_same_thread": False},
|
|
||||||
poolclass=StaticPool,
|
|
||||||
)
|
|
||||||
SQLModel.metadata.create_all(engine)
|
|
||||||
yield engine
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_select_expressions_uses_read_only_session(
|
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
|
||||||
expression_auto_check_engine,
|
|
||||||
) -> None:
|
|
||||||
"""选择表达方式时应使用只读会话,并在离开会话后安全读取 ORM 字段。"""
|
|
||||||
|
|
||||||
import src.bw_learner.expression_auto_check_task as expression_auto_check_task_module
|
|
||||||
|
|
||||||
with Session(expression_auto_check_engine) as session:
|
|
||||||
session.add(
|
|
||||||
Expression(
|
|
||||||
situation="表达情绪高涨或生理反应",
|
|
||||||
style="发送💦表情符号",
|
|
||||||
content_list='["表达情绪高涨或生理反应"]',
|
|
||||||
count=1,
|
|
||||||
session_id="session-a",
|
|
||||||
checked=False,
|
|
||||||
rejected=False,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
auto_commit_calls: list[bool] = []
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def fake_get_db_session(auto_commit: bool = True) -> Generator[Session, None, None]:
|
|
||||||
"""构造带自动提交语义的测试会话工厂。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
auto_commit: 退出上下文时是否自动提交。
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
Generator[Session, None, None]: SQLModel 会话对象。
|
|
||||||
"""
|
|
||||||
|
|
||||||
auto_commit_calls.append(auto_commit)
|
|
||||||
session = Session(expression_auto_check_engine)
|
|
||||||
try:
|
|
||||||
yield session
|
|
||||||
if auto_commit:
|
|
||||||
session.commit()
|
|
||||||
except Exception:
|
|
||||||
session.rollback()
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
monkeypatch.setattr(expression_auto_check_task_module, "get_db_session", fake_get_db_session)
|
|
||||||
monkeypatch.setattr(expression_auto_check_task_module.random, "sample", lambda entries, _count: list(entries))
|
|
||||||
|
|
||||||
task = ExpressionAutoCheckTask()
|
|
||||||
expressions = await task._select_expressions(1)
|
|
||||||
|
|
||||||
assert auto_commit_calls == [False]
|
|
||||||
assert len(expressions) == 1
|
|
||||||
assert expressions[0].id is not None
|
|
||||||
assert expressions[0].situation == "表达情绪高涨或生理反应"
|
|
||||||
assert expressions[0].style == "发送💦表情符号"
|
|
||||||
23
pytests/test_maisaka_tool_logging.py
Normal file
23
pytests/test_maisaka_tool_logging.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from src.maisaka.chat_loop_service import MaisakaChatLoopService
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_tool_names_log_text_supports_openai_function_schema() -> None:
|
||||||
|
tool_definitions = [
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "mute_user",
|
||||||
|
"description": "禁言指定用户",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reply",
|
||||||
|
"description": "发送回复",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
assert MaisakaChatLoopService._build_tool_names_log_text(tool_definitions) == "mute_user、reply"
|
||||||
160
pytests/test_mute_plugin_sdk.py
Normal file
160
pytests/test_mute_plugin_sdk.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
"""MutePlugin SDK 迁移回归测试。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from maibot_sdk.context import PluginContext
|
||||||
|
from maibot_sdk.plugin import MaiBotPlugin
|
||||||
|
|
||||||
|
from plugins.MutePlugin.plugin import create_plugin
|
||||||
|
from src.core.tooling import ToolExecutionContext, ToolInvocation
|
||||||
|
from src.plugin_runtime.component_query import ComponentQueryService
|
||||||
|
from src.plugin_runtime.runner.manifest_validator import ManifestValidator
|
||||||
|
|
||||||
|
|
||||||
|
def _build_plugin() -> MaiBotPlugin:
|
||||||
|
"""构造已注入默认配置的插件实例。"""
|
||||||
|
|
||||||
|
plugin = create_plugin()
|
||||||
|
plugin.set_plugin_config(plugin.get_default_config())
|
||||||
|
return plugin
|
||||||
|
|
||||||
|
|
||||||
|
def test_mute_plugin_manifest_is_valid_v2() -> None:
|
||||||
|
"""MutePlugin 的 manifest 应符合当前运行时要求。"""
|
||||||
|
|
||||||
|
validator = ManifestValidator(host_version="1.0.0", sdk_version="2.3.0")
|
||||||
|
manifest = validator.load_from_plugin_path(Path("plugins/MutePlugin"))
|
||||||
|
|
||||||
|
assert manifest is not None
|
||||||
|
assert manifest.id == "sengokucola.mute-plugin"
|
||||||
|
assert manifest.manifest_version == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_plugin_returns_sdk_plugin() -> None:
|
||||||
|
"""插件入口应返回 SDK 插件实例。"""
|
||||||
|
|
||||||
|
plugin = create_plugin()
|
||||||
|
|
||||||
|
assert isinstance(plugin, MaiBotPlugin)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mute_command_calls_napcat_group_ban_api() -> None:
|
||||||
|
"""手动禁言命令应通过 NapCat Adapter 新 API 执行。"""
|
||||||
|
|
||||||
|
plugin = _build_plugin()
|
||||||
|
plugin.set_plugin_config(
|
||||||
|
{
|
||||||
|
**plugin.get_default_config(),
|
||||||
|
"components": {
|
||||||
|
"enable_smart_mute": True,
|
||||||
|
"enable_mute_command": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
capability_calls: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
async def fake_rpc_call(method: str, plugin_id: str = "", payload: Dict[str, Any] | None = None) -> Dict[str, Any]:
|
||||||
|
assert method == "cap.call"
|
||||||
|
assert payload is not None
|
||||||
|
capability_calls.append(payload)
|
||||||
|
|
||||||
|
capability = payload["capability"]
|
||||||
|
if capability == "person.get_id_by_name":
|
||||||
|
return {"success": True, "person_id": "person-1"}
|
||||||
|
if capability == "person.get_value":
|
||||||
|
return {"success": True, "value": "123456"}
|
||||||
|
if capability == "api.call":
|
||||||
|
return {"success": True, "result": {"status": "ok", "retcode": 0}}
|
||||||
|
if capability == "send.text":
|
||||||
|
return {"success": True}
|
||||||
|
raise AssertionError(f"unexpected capability: {capability}")
|
||||||
|
|
||||||
|
plugin._set_context(PluginContext(plugin_id="mute", rpc_call=fake_rpc_call))
|
||||||
|
|
||||||
|
success, message, intercept = await plugin.handle_mute_command(
|
||||||
|
stream_id="group-10001",
|
||||||
|
group_id="10001",
|
||||||
|
user_id="42",
|
||||||
|
matched_groups={
|
||||||
|
"target": "张三",
|
||||||
|
"duration": "120",
|
||||||
|
"reason": "刷屏",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert success is True
|
||||||
|
assert message == "成功禁言 张三"
|
||||||
|
assert intercept is True
|
||||||
|
|
||||||
|
api_call = next(call for call in capability_calls if call["capability"] == "api.call")
|
||||||
|
assert api_call["args"]["api_name"] == "adapter.napcat.group.set_group_ban"
|
||||||
|
assert api_call["args"]["version"] == "1"
|
||||||
|
assert api_call["args"]["args"] == {
|
||||||
|
"group_id": "10001",
|
||||||
|
"user_id": "123456",
|
||||||
|
"duration": 120,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mute_tool_requires_target_person_name() -> None:
|
||||||
|
"""禁言工具在缺少目标时应直接失败并提示。"""
|
||||||
|
|
||||||
|
plugin = _build_plugin()
|
||||||
|
capability_calls: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
async def fake_rpc_call(method: str, plugin_id: str = "", payload: Dict[str, Any] | None = None) -> Dict[str, Any]:
|
||||||
|
assert method == "cap.call"
|
||||||
|
assert payload is not None
|
||||||
|
capability_calls.append(payload)
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
|
plugin._set_context(PluginContext(plugin_id="mute", rpc_call=fake_rpc_call))
|
||||||
|
|
||||||
|
success, message = await plugin.handle_mute_tool(
|
||||||
|
stream_id="group-10001",
|
||||||
|
group_id="10001",
|
||||||
|
target="",
|
||||||
|
duration="60",
|
||||||
|
reason="测试",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert success is False
|
||||||
|
assert message == "禁言目标不能为空"
|
||||||
|
assert capability_calls[-1]["capability"] == "send.text"
|
||||||
|
assert capability_calls[-1]["args"]["text"] == "没有指定禁言对象哦"
|
||||||
|
|
||||||
|
|
||||||
|
def test_tool_invocation_payload_injects_group_and_user_context() -> None:
|
||||||
|
"""插件工具执行时应自动补齐群聊上下文字段。"""
|
||||||
|
|
||||||
|
entry = SimpleNamespace(invoke_method="plugin.invoke_tool")
|
||||||
|
anchor_message = SimpleNamespace(
|
||||||
|
message_info=SimpleNamespace(
|
||||||
|
group_info=SimpleNamespace(group_id="10001"),
|
||||||
|
user_info=SimpleNamespace(user_id="20002"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
invocation = ToolInvocation(tool_name="mute", arguments={"target": "张三"}, stream_id="session-1")
|
||||||
|
context = ToolExecutionContext(
|
||||||
|
session_id="session-1",
|
||||||
|
stream_id="session-1",
|
||||||
|
reasoning="test",
|
||||||
|
metadata={"anchor_message": anchor_message},
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = ComponentQueryService._build_tool_invocation_payload(entry, invocation, context)
|
||||||
|
|
||||||
|
assert payload["target"] == "张三"
|
||||||
|
assert payload["stream_id"] == "session-1"
|
||||||
|
assert payload["chat_id"] == "session-1"
|
||||||
|
assert payload["group_id"] == "10001"
|
||||||
|
assert payload["user_id"] == "20002"
|
||||||
27
pytests/test_openai_client_toolless_request.py
Normal file
27
pytests/test_openai_client_toolless_request.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from src.llm_models.model_client.openai_client import _sanitize_messages_for_toolless_request
|
||||||
|
from src.llm_models.payload_content.message import Message, RoleType, TextMessagePart
|
||||||
|
from src.llm_models.payload_content.tool_option import ToolCall
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanitize_messages_for_toolless_request_drops_assistant_tool_call_without_parts() -> None:
|
||||||
|
messages = [
|
||||||
|
Message(
|
||||||
|
role=RoleType.Assistant,
|
||||||
|
tool_calls=[
|
||||||
|
ToolCall(
|
||||||
|
call_id="call_1",
|
||||||
|
func_name="mute_user",
|
||||||
|
args={"target": "alice"},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Message(
|
||||||
|
role=RoleType.User,
|
||||||
|
parts=[TextMessagePart(text="继续")],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
sanitized_messages = _sanitize_messages_for_toolless_request(messages)
|
||||||
|
|
||||||
|
assert len(sanitized_messages) == 1
|
||||||
|
assert sanitized_messages[0].role == RoleType.User
|
||||||
@@ -24,7 +24,7 @@ from src.common.data_models.llm_service_data_models import LLMGenerationOptions
|
|||||||
from src.services.llm_service import LLMServiceClient
|
from src.services.llm_service import LLMServiceClient
|
||||||
from src.manager.async_task_manager import AsyncTask
|
from src.manager.async_task_manager import AsyncTask
|
||||||
|
|
||||||
logger = get_logger("expression_auto_check_task")
|
logger = get_logger("expressor")
|
||||||
|
|
||||||
|
|
||||||
def create_evaluation_prompt(situation: str, style: str) -> str:
|
def create_evaluation_prompt(situation: str, style: str) -> str:
|
||||||
|
|||||||
@@ -346,6 +346,32 @@ def _convert_assistant_tool_calls(tool_calls: List[ToolCall]) -> List[ChatComple
|
|||||||
return converted_tool_calls
|
return converted_tool_calls
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_messages_for_toolless_request(messages: List[Message]) -> List[Message]:
|
||||||
|
"""在无工具请求时清洗历史工具调用链,避免兼容接口拒收消息。"""
|
||||||
|
sanitized_messages: List[Message] = []
|
||||||
|
|
||||||
|
for message in messages:
|
||||||
|
if message.role == RoleType.Tool:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if message.role == RoleType.Assistant and message.tool_calls:
|
||||||
|
if not message.parts:
|
||||||
|
continue
|
||||||
|
assistant_message = Message(
|
||||||
|
role=message.role,
|
||||||
|
parts=list(message.parts),
|
||||||
|
tool_call_id=message.tool_call_id,
|
||||||
|
tool_name=message.tool_name,
|
||||||
|
tool_calls=None,
|
||||||
|
)
|
||||||
|
sanitized_messages.append(assistant_message)
|
||||||
|
continue
|
||||||
|
|
||||||
|
sanitized_messages.append(message)
|
||||||
|
|
||||||
|
return sanitized_messages
|
||||||
|
|
||||||
|
|
||||||
def _convert_messages(messages: List[Message]) -> List[ChatCompletionMessageParam]:
|
def _convert_messages(messages: List[Message]) -> List[ChatCompletionMessageParam]:
|
||||||
"""将内部消息列表转换为 OpenAI 兼容消息列表。
|
"""将内部消息列表转换为 OpenAI 兼容消息列表。
|
||||||
|
|
||||||
@@ -965,7 +991,12 @@ class OpenaiClient(AdapterClient[AsyncStream[ChatCompletionChunk], ChatCompletio
|
|||||||
model_info = request.model_info
|
model_info = request.model_info
|
||||||
|
|
||||||
try:
|
try:
|
||||||
messages_payload: List[ChatCompletionMessageParam] = _convert_messages(request.message_list)
|
request_messages = (
|
||||||
|
list(request.message_list)
|
||||||
|
if request.tool_options
|
||||||
|
else _sanitize_messages_for_toolless_request(request.message_list)
|
||||||
|
)
|
||||||
|
messages_payload: List[ChatCompletionMessageParam] = _convert_messages(request_messages)
|
||||||
tools_payload: List[ChatCompletionToolParam] | None = (
|
tools_payload: List[ChatCompletionToolParam] | None = (
|
||||||
_convert_tool_options(request.tool_options) if request.tool_options else None
|
_convert_tool_options(request.tool_options) if request.tool_options else None
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -397,8 +397,6 @@ class LLMOrchestrator:
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
tool_built = self._build_tool_options(tools)
|
tool_built = self._build_tool_options(tools)
|
||||||
if self.request_type.startswith("maisaka_"):
|
|
||||||
logger.info(f"LLMOrchestrator[{self.request_type}] 已构建 {len(tool_built or [])} 个内部工具选项")
|
|
||||||
|
|
||||||
execution_result = await self._execute_request(
|
execution_result = await self._execute_request(
|
||||||
request_type=RequestType.RESPONSE,
|
request_type=RequestType.RESPONSE,
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ def get_tool_spec() -> ToolSpec:
|
|||||||
|
|
||||||
return ToolSpec(
|
return ToolSpec(
|
||||||
name="wait",
|
name="wait",
|
||||||
brief_description="暂停当前对话并等待用户新的输入。",
|
brief_description="暂停当前对话并固定等待一段时间,期间不因新消息提前恢复。",
|
||||||
detailed_description="参数说明:\n- seconds:integer,必填。等待的秒数。",
|
detailed_description="参数说明:\n- seconds:integer,必填。等待的秒数。等待期间收到的新消息只会暂存,直到超时后再继续处理。",
|
||||||
parameters_schema={
|
parameters_schema={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -46,6 +46,6 @@ async def handle_tool(
|
|||||||
tool_ctx.runtime._enter_wait_state(seconds=wait_seconds, tool_call_id=invocation.call_id)
|
tool_ctx.runtime._enter_wait_state(seconds=wait_seconds, tool_call_id=invocation.call_id)
|
||||||
return tool_ctx.build_success_result(
|
return tool_ctx.build_success_result(
|
||||||
invocation.tool_name,
|
invocation.tool_name,
|
||||||
f"当前对话循环进入等待状态,最长等待 {wait_seconds} 秒。",
|
f"当前对话循环进入等待状态,将固定等待 {wait_seconds} 秒;期间收到的新消息不会提前打断本次等待。",
|
||||||
metadata={"pause_execution": True},
|
metadata={"pause_execution": True},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -475,6 +475,44 @@ class MaisakaChatLoopService:
|
|||||||
return normalized_text
|
return normalized_text
|
||||||
return f"{normalized_text[: max_length - 1]}…"
|
return f"{normalized_text[: max_length - 1]}…"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_tool_names_log_text(tool_definitions: Sequence[ToolDefinitionInput]) -> str:
|
||||||
|
"""构造 planner 请求前的工具列表日志文本。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool_definitions: 本轮实际传给 planner 的工具定义列表。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 适合直接写入日志的单行文本。
|
||||||
|
"""
|
||||||
|
|
||||||
|
tool_names: List[str] = []
|
||||||
|
for tool_definition in tool_definitions:
|
||||||
|
if not isinstance(tool_definition, dict):
|
||||||
|
continue
|
||||||
|
normalized_name = str(tool_definition.get("name") or "").strip()
|
||||||
|
if not normalized_name:
|
||||||
|
function_definition = tool_definition.get("function")
|
||||||
|
if isinstance(function_definition, dict):
|
||||||
|
normalized_name = str(function_definition.get("name") or "").strip()
|
||||||
|
if normalized_name:
|
||||||
|
tool_names.append(normalized_name)
|
||||||
|
|
||||||
|
if not tool_names:
|
||||||
|
return "[无工具]"
|
||||||
|
|
||||||
|
return "、".join(tool_names)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_tool_spec_names_log_text(tool_specs: Sequence[ToolSpec]) -> str:
|
||||||
|
"""构造 ToolSpec 列表的工具名日志文本。"""
|
||||||
|
|
||||||
|
tool_names = [tool_spec.name for tool_spec in tool_specs if tool_spec.name]
|
||||||
|
if not tool_names:
|
||||||
|
return "[无工具]"
|
||||||
|
|
||||||
|
return "、".join(tool_names)
|
||||||
|
|
||||||
def _build_tool_filter_prompt(
|
def _build_tool_filter_prompt(
|
||||||
self,
|
self,
|
||||||
selected_history: List[LLMContextMessage],
|
selected_history: List[LLMContextMessage],
|
||||||
@@ -620,7 +658,8 @@ class MaisakaChatLoopService:
|
|||||||
f"总工具数={len(tool_specs)} "
|
f"总工具数={len(tool_specs)} "
|
||||||
f"内置工具数={len(builtin_tool_specs)} "
|
f"内置工具数={len(builtin_tool_specs)} "
|
||||||
f"候选工具数={len(candidate_tool_specs)} "
|
f"候选工具数={len(candidate_tool_specs)} "
|
||||||
f"最多保留候选数={max_keep}"
|
f"最多保留候选数={max_keep} "
|
||||||
|
f"过滤前全部工具名={self._build_tool_spec_names_log_text(tool_specs)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -660,6 +699,7 @@ class MaisakaChatLoopService:
|
|||||||
"工具预筛选完成: "
|
"工具预筛选完成: "
|
||||||
f"筛选前总数={len(tool_specs)} "
|
f"筛选前总数={len(tool_specs)} "
|
||||||
f"筛选后总数={len(filtered_tool_specs)} "
|
f"筛选后总数={len(filtered_tool_specs)} "
|
||||||
|
f"过滤后全部工具名={self._build_tool_spec_names_log_text(filtered_tool_specs)} "
|
||||||
f"保留候选工具={[tool_spec.name for tool_spec in filtered_candidate_tool_specs]}"
|
f"保留候选工具={[tool_spec.name for tool_spec in filtered_candidate_tool_specs]}"
|
||||||
)
|
)
|
||||||
return filtered_tool_specs
|
return filtered_tool_specs
|
||||||
@@ -729,6 +769,11 @@ class MaisakaChatLoopService:
|
|||||||
if isinstance(raw_tool_definitions, list):
|
if isinstance(raw_tool_definitions, list):
|
||||||
all_tools = [item for item in raw_tool_definitions if isinstance(item, dict)]
|
all_tools = [item for item in raw_tool_definitions if isinstance(item, dict)]
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"规划器工具列表(request_kind={request_kind}): "
|
||||||
|
f"共 {len(all_tools)} 个 -> {self._build_tool_names_log_text(all_tools)}"
|
||||||
|
)
|
||||||
|
|
||||||
prompt_section: RenderableType | None = None
|
prompt_section: RenderableType | None = None
|
||||||
if global_config.debug.show_maisaka_thinking:
|
if global_config.debug.show_maisaka_thinking:
|
||||||
image_display_mode: str = "path_link" if global_config.maisaka.show_image_path else "legacy"
|
image_display_mode: str = "path_link" if global_config.maisaka.show_image_path else "legacy"
|
||||||
@@ -740,10 +785,11 @@ class MaisakaChatLoopService:
|
|||||||
selection_reason=selection_reason,
|
selection_reason=selection_reason,
|
||||||
image_display_mode=image_display_mode,
|
image_display_mode=image_display_mode,
|
||||||
folded=global_config.debug.fold_maisaka_thinking,
|
folded=global_config.debug.fold_maisaka_thinking,
|
||||||
|
tool_definitions=list(all_tools),
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"规划器请求开始: "
|
f"规划器请求开始(request_kind={request_kind}): "
|
||||||
f"已选上下文消息数={len(selected_history)} "
|
f"已选上下文消息数={len(selected_history)} "
|
||||||
f"大模型消息数={len(built_messages)} "
|
f"大模型消息数={len(built_messages)} "
|
||||||
f"工具数={len(all_tools)} "
|
f"工具数={len(all_tools)} "
|
||||||
@@ -760,15 +806,6 @@ class MaisakaChatLoopService:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt_stats_text = PromptCLIVisualizer.build_prompt_stats_text(
|
|
||||||
selected_history_count=len(selected_history),
|
|
||||||
built_message_count=len(built_messages),
|
|
||||||
prompt_tokens=generation_result.prompt_tokens,
|
|
||||||
completion_tokens=generation_result.completion_tokens,
|
|
||||||
total_tokens=generation_result.total_tokens,
|
|
||||||
)
|
|
||||||
logger.info(f"本轮Prompt统计: {prompt_stats_text}")
|
|
||||||
|
|
||||||
final_response = generation_result.response or ""
|
final_response = generation_result.response or ""
|
||||||
final_tool_calls = list(generation_result.tool_calls or [])
|
final_tool_calls = list(generation_result.tool_calls or [])
|
||||||
after_response_result = await self._get_runtime_manager().invoke_hook(
|
after_response_result = await self._get_runtime_manager().invoke_hook(
|
||||||
|
|||||||
@@ -242,6 +242,85 @@ class PromptCLIVisualizer:
|
|||||||
def format_tool_call_for_display(cls, tool_call: Any) -> Dict[str, Any]:
|
def format_tool_call_for_display(cls, tool_call: Any) -> Dict[str, Any]:
|
||||||
return normalize_tool_call_for_display(tool_call)
|
return normalize_tool_call_for_display(tool_call)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_tool_card_title(cls, tool_call: Any) -> str:
|
||||||
|
"""构建 HTML 中工具卡片的折叠标题。"""
|
||||||
|
|
||||||
|
normalized_tool_call = cls.format_tool_call_for_display(tool_call)
|
||||||
|
tool_name = str(normalized_tool_call.get("name") or "").strip()
|
||||||
|
return tool_name or "unknown"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_tool_call_html(cls, tool_call: Any) -> str:
|
||||||
|
"""将单个工具调用渲染为默认折叠的 HTML 卡片。"""
|
||||||
|
|
||||||
|
normalized_tool_call = cls.format_tool_call_for_display(tool_call)
|
||||||
|
tool_name = cls._build_tool_card_title(tool_call)
|
||||||
|
tool_call_id = str(normalized_tool_call.get("id") or "").strip()
|
||||||
|
tool_arguments = normalized_tool_call.get("arguments")
|
||||||
|
|
||||||
|
tool_meta_html = ""
|
||||||
|
if tool_call_id:
|
||||||
|
tool_meta_html = (
|
||||||
|
"<div class='tool-card-meta'>"
|
||||||
|
"<span class='tool-card-meta-label'>调用 ID</span>"
|
||||||
|
f"<code>{html.escape(tool_call_id)}</code>"
|
||||||
|
"</div>"
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
"<details class='tool-card'>"
|
||||||
|
"<summary class='tool-card-summary'>"
|
||||||
|
f"<span class='tool-card-name'>{html.escape(tool_name)}</span>"
|
||||||
|
"</summary>"
|
||||||
|
"<div class='tool-card-body'>"
|
||||||
|
f"{tool_meta_html}"
|
||||||
|
f"<pre>{html.escape(json.dumps(tool_arguments, ensure_ascii=False, indent=2, default=str))}</pre>"
|
||||||
|
"</div>"
|
||||||
|
"</details>"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _extract_tool_definition_fields(cls, tool_definition: dict[str, Any]) -> tuple[str, str, Any]:
|
||||||
|
"""提取工具定义中的名称、描述和详情内容。"""
|
||||||
|
|
||||||
|
function_info = tool_definition.get("function")
|
||||||
|
if isinstance(function_info, dict):
|
||||||
|
tool_name = str(function_info.get("name") or "").strip() or "unknown"
|
||||||
|
description = str(function_info.get("description") or "").strip()
|
||||||
|
detail_payload = function_info
|
||||||
|
else:
|
||||||
|
tool_name = str(tool_definition.get("name") or "").strip() or "unknown"
|
||||||
|
description = str(tool_definition.get("description") or "").strip()
|
||||||
|
detail_payload = tool_definition
|
||||||
|
return tool_name, description, detail_payload
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_tool_definition_html(cls, tool_definition: dict[str, Any]) -> str:
|
||||||
|
"""将单个传入工具定义渲染为默认折叠的 HTML 卡片。"""
|
||||||
|
|
||||||
|
tool_name, description, detail_payload = cls._extract_tool_definition_fields(tool_definition)
|
||||||
|
description_html = ""
|
||||||
|
if description:
|
||||||
|
description_html = (
|
||||||
|
"<div class='tool-card-meta'>"
|
||||||
|
"<span class='tool-card-meta-label'>说明</span>"
|
||||||
|
f"<span>{html.escape(description)}</span>"
|
||||||
|
"</div>"
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
"<details class='tool-card tool-definition-card'>"
|
||||||
|
"<summary class='tool-card-summary'>"
|
||||||
|
f"<span class='tool-card-name'>{html.escape(tool_name)}</span>"
|
||||||
|
"</summary>"
|
||||||
|
"<div class='tool-card-body'>"
|
||||||
|
f"{description_html}"
|
||||||
|
f"<pre>{html.escape(json.dumps(detail_payload, ensure_ascii=False, indent=2, default=str))}</pre>"
|
||||||
|
"</div>"
|
||||||
|
"</details>"
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _render_tool_call_panel(cls, tool_call: Any, index: int, parent_index: int) -> Panel:
|
def _render_tool_call_panel(cls, tool_call: Any, index: int, parent_index: int) -> Panel:
|
||||||
title = Text.assemble(
|
title = Text.assemble(
|
||||||
@@ -291,6 +370,20 @@ class PromptCLIVisualizer:
|
|||||||
|
|
||||||
return "\n\n" + ("\n\n" + ("=" * 80) + "\n\n").join(sections) if sections else "[空 Prompt]"
|
return "\n\n" + ("\n\n" + ("=" * 80) + "\n\n").join(sections) if sections else "[空 Prompt]"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_tool_definition_dump_text(cls, tool_definitions: list[dict[str, Any]] | None) -> str:
|
||||||
|
"""构建传入工具定义的文本备份内容。"""
|
||||||
|
|
||||||
|
if not tool_definitions:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
sections: List[str] = ["[tool_definitions]"]
|
||||||
|
for index, tool_definition in enumerate(tool_definitions, start=1):
|
||||||
|
tool_name, _, detail_payload = cls._extract_tool_definition_fields(tool_definition)
|
||||||
|
sections.append(f"[{index}] name={tool_name}")
|
||||||
|
sections.append(json.dumps(detail_payload, ensure_ascii=False, indent=2, default=str))
|
||||||
|
return "\n\n".join(sections).strip()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _render_message_content_html(cls, content: Any) -> str:
|
def _render_message_content_html(cls, content: Any) -> str:
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
@@ -356,6 +449,7 @@ class PromptCLIVisualizer:
|
|||||||
*,
|
*,
|
||||||
request_kind: str,
|
request_kind: str,
|
||||||
selection_reason: str,
|
selection_reason: str,
|
||||||
|
tool_definitions: list[dict[str, Any]] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
panel_title, _ = cls.get_request_panel_style(request_kind)
|
panel_title, _ = cls.get_request_panel_style(request_kind)
|
||||||
message_cards: List[str] = []
|
message_cards: List[str] = []
|
||||||
@@ -378,16 +472,12 @@ class PromptCLIVisualizer:
|
|||||||
tool_panels = ""
|
tool_panels = ""
|
||||||
raw_tool_calls = message.get("tool_calls") or []
|
raw_tool_calls = message.get("tool_calls") or []
|
||||||
if isinstance(raw_tool_calls, list) and raw_tool_calls:
|
if isinstance(raw_tool_calls, list) and raw_tool_calls:
|
||||||
tool_items = []
|
tool_panels = (
|
||||||
for tool_call_index, tool_call in enumerate(raw_tool_calls, start=1):
|
"<div class='tool-list'>"
|
||||||
normalized_tool_call = cls.format_tool_call_for_display(tool_call)
|
"<div class='tool-list-title'>工具调用</div>"
|
||||||
tool_items.append(
|
f"{''.join(cls._build_tool_call_html(tool_call) for tool_call in raw_tool_calls)}"
|
||||||
"<div class='tool-panel'>"
|
|
||||||
f"<div class='tool-panel-title'>工具调用 #{index}.{tool_call_index}</div>"
|
|
||||||
f"<pre>{html.escape(json.dumps(normalized_tool_call, ensure_ascii=False, indent=2, default=str))}</pre>"
|
|
||||||
"</div>"
|
"</div>"
|
||||||
)
|
)
|
||||||
tool_panels = "".join(tool_items)
|
|
||||||
|
|
||||||
message_cards.append(
|
message_cards.append(
|
||||||
"<section class='message-card'>"
|
"<section class='message-card'>"
|
||||||
@@ -405,6 +495,21 @@ class PromptCLIVisualizer:
|
|||||||
if selection_reason.strip():
|
if selection_reason.strip():
|
||||||
subtitle_html = f"<div class='subtitle'>{html.escape(selection_reason)}</div>"
|
subtitle_html = f"<div class='subtitle'>{html.escape(selection_reason)}</div>"
|
||||||
|
|
||||||
|
tool_definition_section_html = ""
|
||||||
|
if tool_definitions:
|
||||||
|
tool_definition_section_html = (
|
||||||
|
"<section class='message-card tool-definition-section'>"
|
||||||
|
"<div class='message-head'>"
|
||||||
|
"<span class='role-badge tool'>全部工具</span>"
|
||||||
|
f"<span class='message-index'>{len(tool_definitions)} 个</span>"
|
||||||
|
"</div>"
|
||||||
|
"<div class='tool-list'>"
|
||||||
|
"<div class='tool-list-title'>本次送入模型的工具定义</div>"
|
||||||
|
f"{''.join(cls._build_tool_definition_html(tool_definition) for tool_definition in tool_definitions)}"
|
||||||
|
"</div>"
|
||||||
|
"</section>"
|
||||||
|
)
|
||||||
|
|
||||||
return f"""<!DOCTYPE html>
|
return f"""<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
@@ -491,7 +596,7 @@ class PromptCLIVisualizer:
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}}
|
}}
|
||||||
.message-content pre,
|
.message-content pre,
|
||||||
.tool-panel pre {{
|
.tool-card pre {{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -517,18 +622,75 @@ class PromptCLIVisualizer:
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
}}
|
}}
|
||||||
.tool-panel {{
|
.tool-list {{
|
||||||
|
margin-top: 14px;
|
||||||
|
}}
|
||||||
|
.tool-list-title {{
|
||||||
|
color: #86198f;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}}
|
||||||
|
.tool-card {{
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
background: #fcf4ff;
|
background: #fcf4ff;
|
||||||
border: 1px solid #f0d7fb;
|
border: 1px solid #f0d7fb;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 12px 14px;
|
overflow: hidden;
|
||||||
}}
|
}}
|
||||||
.tool-panel-title {{
|
.tool-card:first-of-type {{
|
||||||
color: #a21caf;
|
margin-top: 0;
|
||||||
|
}}
|
||||||
|
.tool-card-summary {{
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 14px;
|
||||||
|
color: #86198f;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
}}
|
||||||
|
.tool-card-summary::-webkit-details-marker {{
|
||||||
|
display: none;
|
||||||
|
}}
|
||||||
|
.tool-card-summary::after {{
|
||||||
|
content: "展开";
|
||||||
|
color: #a21caf;
|
||||||
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 8px;
|
}}
|
||||||
|
.tool-card[open] .tool-card-summary::after {{
|
||||||
|
content: "收起";
|
||||||
|
}}
|
||||||
|
.tool-card-name {{
|
||||||
|
word-break: break-word;
|
||||||
|
}}
|
||||||
|
.tool-card-body {{
|
||||||
|
border-top: 1px solid #f0d7fb;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.52);
|
||||||
|
}}
|
||||||
|
.tool-card-meta {{
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #a21caf;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}}
|
||||||
|
.tool-card-meta-label {{
|
||||||
|
font-weight: 700;
|
||||||
|
}}
|
||||||
|
.tool-card-meta code {{
|
||||||
|
background: #faf5ff;
|
||||||
|
border: 1px solid #e9d5ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
}}
|
||||||
|
.tool-card pre {{
|
||||||
|
color: #3b0764;
|
||||||
}}
|
}}
|
||||||
.image-card {{
|
.image-card {{
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
@@ -564,6 +726,7 @@ class PromptCLIVisualizer:
|
|||||||
{subtitle_html}
|
{subtitle_html}
|
||||||
</header>
|
</header>
|
||||||
{''.join(message_cards)}
|
{''.join(message_cards)}
|
||||||
|
{tool_definition_section_html}
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>"""
|
||||||
@@ -578,6 +741,7 @@ class PromptCLIVisualizer:
|
|||||||
request_kind: str,
|
request_kind: str,
|
||||||
selection_reason: str,
|
selection_reason: str,
|
||||||
image_display_mode: Literal["legacy", "path_link"],
|
image_display_mode: Literal["legacy", "path_link"],
|
||||||
|
tool_definitions: list[dict[str, Any]] | None = None,
|
||||||
) -> RenderableType:
|
) -> RenderableType:
|
||||||
"""构建用于查看完整 prompt 的折叠入口内容。"""
|
"""构建用于查看完整 prompt 的折叠入口内容。"""
|
||||||
|
|
||||||
@@ -603,10 +767,14 @@ class PromptCLIVisualizer:
|
|||||||
viewer_messages.append(normalized_message)
|
viewer_messages.append(normalized_message)
|
||||||
|
|
||||||
prompt_dump_text = cls._build_prompt_dump_text(messages)
|
prompt_dump_text = cls._build_prompt_dump_text(messages)
|
||||||
|
tool_definition_dump_text = cls._build_tool_definition_dump_text(tool_definitions)
|
||||||
|
if tool_definition_dump_text:
|
||||||
|
prompt_dump_text = f"{prompt_dump_text}\n\n{'=' * 80}\n\n{tool_definition_dump_text}"
|
||||||
viewer_html_text = cls._build_prompt_viewer_html(
|
viewer_html_text = cls._build_prompt_viewer_html(
|
||||||
viewer_messages,
|
viewer_messages,
|
||||||
request_kind=request_kind,
|
request_kind=request_kind,
|
||||||
selection_reason=selection_reason,
|
selection_reason=selection_reason,
|
||||||
|
tool_definitions=tool_definitions,
|
||||||
)
|
)
|
||||||
saved_paths = PromptPreviewLogger.save_preview_files(
|
saved_paths = PromptPreviewLogger.save_preview_files(
|
||||||
chat_id,
|
chat_id,
|
||||||
@@ -623,12 +791,12 @@ class PromptCLIVisualizer:
|
|||||||
|
|
||||||
body = Group(
|
body = Group(
|
||||||
Text.from_markup(
|
Text.from_markup(
|
||||||
f"[bold green]富文本预览:{viewer_html_path}[/bold green] "
|
f"[bold green]html预览:{viewer_html_path}[/bold green] "
|
||||||
f"[link={viewer_uri}]点击在浏览器打开富文本 Prompt 视图[/link]"
|
f"[link={viewer_uri}]在浏览器打开 Prompt [/link]"
|
||||||
),
|
),
|
||||||
Text.from_markup(
|
Text.from_markup(
|
||||||
f"[magenta]原始文本备份:{prompt_dump_path}[/magenta] "
|
f"[magenta]原始文本:{prompt_dump_path}[/magenta] "
|
||||||
f"[cyan][link={dump_uri}]点击直接打开 Prompt 文本[/link][/cyan]"
|
f"[cyan][link={dump_uri}]点击打开 Prompt 文本[/link][/cyan]"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return body
|
return body
|
||||||
@@ -644,6 +812,7 @@ class PromptCLIVisualizer:
|
|||||||
selection_reason: str,
|
selection_reason: str,
|
||||||
image_display_mode: Literal["legacy", "path_link"],
|
image_display_mode: Literal["legacy", "path_link"],
|
||||||
folded: bool,
|
folded: bool,
|
||||||
|
tool_definitions: list[dict[str, Any]] | None = None,
|
||||||
) -> Panel:
|
) -> Panel:
|
||||||
"""构建用于嵌入结果面板中的 Prompt 区块。"""
|
"""构建用于嵌入结果面板中的 Prompt 区块。"""
|
||||||
|
|
||||||
@@ -656,6 +825,7 @@ class PromptCLIVisualizer:
|
|||||||
request_kind=request_kind,
|
request_kind=request_kind,
|
||||||
selection_reason=selection_reason,
|
selection_reason=selection_reason,
|
||||||
image_display_mode=image_display_mode,
|
image_display_mode=image_display_mode,
|
||||||
|
tool_definitions=tool_definitions,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ordered_panels = cls.build_prompt_panels(
|
ordered_panels = cls.build_prompt_panels(
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ class MaisakaReasoningEngine:
|
|||||||
) -> tuple[Literal["continue", "no_reply", "wait"], Any, list[str]]:
|
) -> tuple[Literal["continue", "no_reply", "wait"], Any, list[str]]:
|
||||||
"""运行 Timing Gate 子代理并返回控制决策。"""
|
"""运行 Timing Gate 子代理并返回控制决策。"""
|
||||||
|
|
||||||
if self._runtime._force_continue_until_reply:
|
if self._runtime._force_next_timing_continue:
|
||||||
return self._build_forced_continue_timing_result()
|
return self._build_forced_continue_timing_result()
|
||||||
|
|
||||||
response = await self._run_interruptible_sub_agent(
|
response = await self._run_interruptible_sub_agent(
|
||||||
@@ -270,7 +270,7 @@ class MaisakaReasoningEngine:
|
|||||||
def _build_forced_continue_timing_result(self) -> tuple[Literal["continue"], ChatResponse, list[str]]:
|
def _build_forced_continue_timing_result(self) -> tuple[Literal["continue"], ChatResponse, list[str]]:
|
||||||
"""构造跳过 Timing Gate 时使用的伪 continue 结果。"""
|
"""构造跳过 Timing Gate 时使用的伪 continue 结果。"""
|
||||||
|
|
||||||
reason = self._runtime._build_force_continue_timing_reason()
|
reason = self._runtime._consume_force_next_timing_continue_reason() or "本轮直接跳过 Timing Gate 并视作 continue。"
|
||||||
logger.info(f"{self._runtime.log_prefix} {reason}")
|
logger.info(f"{self._runtime.log_prefix} {reason}")
|
||||||
return (
|
return (
|
||||||
"continue",
|
"continue",
|
||||||
@@ -334,7 +334,10 @@ class MaisakaReasoningEngine:
|
|||||||
self._runtime._agent_state = self._runtime._STATE_RUNNING
|
self._runtime._agent_state = self._runtime._STATE_RUNNING
|
||||||
if cached_messages:
|
if cached_messages:
|
||||||
asyncio.create_task(self._runtime._trigger_batch_learning(cached_messages))
|
asyncio.create_task(self._runtime._trigger_batch_learning(cached_messages))
|
||||||
self._append_wait_interrupted_message_if_needed()
|
if timeout_triggered:
|
||||||
|
self._runtime._chat_history.append(
|
||||||
|
self._build_wait_completed_message(has_new_messages=True)
|
||||||
|
)
|
||||||
await self._ingest_messages(cached_messages)
|
await self._ingest_messages(cached_messages)
|
||||||
anchor_message = cached_messages[-1]
|
anchor_message = cached_messages[-1]
|
||||||
else:
|
else:
|
||||||
@@ -346,7 +349,9 @@ class MaisakaReasoningEngine:
|
|||||||
continue
|
continue
|
||||||
logger.info(f"{self._runtime.log_prefix} 等待超时后开始新一轮思考")
|
logger.info(f"{self._runtime.log_prefix} 等待超时后开始新一轮思考")
|
||||||
if self._runtime._pending_wait_tool_call_id:
|
if self._runtime._pending_wait_tool_call_id:
|
||||||
self._runtime._chat_history.append(self._build_wait_timeout_message())
|
self._runtime._chat_history.append(
|
||||||
|
self._build_wait_completed_message(has_new_messages=False)
|
||||||
|
)
|
||||||
self._trim_chat_history()
|
self._trim_chat_history()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -399,7 +404,7 @@ class MaisakaReasoningEngine:
|
|||||||
)
|
)
|
||||||
timing_gate_required = self._mark_timing_gate_completed(timing_action)
|
timing_gate_required = self._mark_timing_gate_completed(timing_action)
|
||||||
if timing_action != "continue":
|
if timing_action != "continue":
|
||||||
logger.info(
|
logger.debug(
|
||||||
f"{self._runtime.log_prefix} Timing Gate 结束当前回合: "
|
f"{self._runtime.log_prefix} Timing Gate 结束当前回合: "
|
||||||
f"回合={round_index + 1} 动作={timing_action}"
|
f"回合={round_index + 1} 动作={timing_action}"
|
||||||
)
|
)
|
||||||
@@ -475,7 +480,6 @@ class MaisakaReasoningEngine:
|
|||||||
break
|
break
|
||||||
|
|
||||||
asyncio.create_task(self._runtime._trigger_batch_learning(interrupted_messages))
|
asyncio.create_task(self._runtime._trigger_batch_learning(interrupted_messages))
|
||||||
self._append_wait_interrupted_message_if_needed()
|
|
||||||
await self._ingest_messages(interrupted_messages)
|
await self._ingest_messages(interrupted_messages)
|
||||||
anchor_message = interrupted_messages[-1]
|
anchor_message = interrupted_messages[-1]
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -588,33 +592,22 @@ class MaisakaReasoningEngine:
|
|||||||
return self._runtime.message_cache[-1]
|
return self._runtime.message_cache[-1]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _build_wait_timeout_message(self) -> ToolResultMessage:
|
def _build_wait_completed_message(self, *, has_new_messages: bool) -> ToolResultMessage:
|
||||||
"""构造 wait 超时后的工具结果消息。"""
|
"""构造 wait 完成后的工具结果消息。"""
|
||||||
tool_call_id = self._runtime._pending_wait_tool_call_id or "wait_timeout"
|
tool_call_id = self._runtime._pending_wait_tool_call_id or "wait_timeout"
|
||||||
self._runtime._pending_wait_tool_call_id = None
|
self._runtime._pending_wait_tool_call_id = None
|
||||||
|
content = (
|
||||||
|
"等待已结束,期间收到了新的用户输入。请结合这些新消息继续下一轮思考。"
|
||||||
|
if has_new_messages
|
||||||
|
else "等待已超时,期间没有收到新的用户输入。请基于现有上下文继续下一轮思考。"
|
||||||
|
)
|
||||||
return ToolResultMessage(
|
return ToolResultMessage(
|
||||||
content="等待已超时,期间没有收到新的用户输入。请基于现有上下文继续下一轮思考。",
|
content=content,
|
||||||
timestamp=datetime.now(),
|
timestamp=datetime.now(),
|
||||||
tool_call_id=tool_call_id,
|
tool_call_id=tool_call_id,
|
||||||
tool_name="wait",
|
tool_name="wait",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _append_wait_interrupted_message_if_needed(self) -> None:
|
|
||||||
"""如果 wait 被新消息打断,则补一条对应的工具结果消息。"""
|
|
||||||
tool_call_id = self._runtime._pending_wait_tool_call_id
|
|
||||||
if not tool_call_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._runtime._pending_wait_tool_call_id = None
|
|
||||||
self._runtime._chat_history.append(
|
|
||||||
ToolResultMessage(
|
|
||||||
content="等待过程被新的用户输入打断,已继续处理最新消息。",
|
|
||||||
timestamp=datetime.now(),
|
|
||||||
tool_call_id=tool_call_id,
|
|
||||||
tool_name="wait",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _ingest_messages(self, messages: list[SessionMessage]) -> None:
|
async def _ingest_messages(self, messages: list[SessionMessage]) -> None:
|
||||||
"""处理传入消息列表,将其转换为历史消息并加入聊天历史缓存。"""
|
"""处理传入消息列表,将其转换为历史消息并加入聊天历史缓存。"""
|
||||||
for message in messages:
|
for message in messages:
|
||||||
|
|||||||
@@ -93,11 +93,10 @@ class MaisakaHeartFlowChatting:
|
|||||||
self._max_internal_rounds = MAX_INTERNAL_ROUNDS
|
self._max_internal_rounds = MAX_INTERNAL_ROUNDS
|
||||||
self._max_context_size = max(1, int(global_config.chat.max_context_size))
|
self._max_context_size = max(1, int(global_config.chat.max_context_size))
|
||||||
self._agent_state: Literal["running", "wait", "stop"] = self._STATE_STOP
|
self._agent_state: Literal["running", "wait", "stop"] = self._STATE_STOP
|
||||||
self._wait_until: Optional[float] = None
|
|
||||||
self._pending_wait_tool_call_id: Optional[str] = None
|
self._pending_wait_tool_call_id: Optional[str] = None
|
||||||
self._force_continue_until_reply = False
|
self._force_next_timing_continue = False
|
||||||
self._force_continue_trigger_message_id = ""
|
self._force_next_timing_message_id = ""
|
||||||
self._force_continue_trigger_reason = ""
|
self._force_next_timing_reason = ""
|
||||||
self._planner_interrupt_flag: Optional[asyncio.Event] = None
|
self._planner_interrupt_flag: Optional[asyncio.Event] = None
|
||||||
self._planner_interrupt_requested = False
|
self._planner_interrupt_requested = False
|
||||||
self._planner_interrupt_consecutive_count = 0
|
self._planner_interrupt_consecutive_count = 0
|
||||||
@@ -176,9 +175,6 @@ class MaisakaHeartFlowChatting:
|
|||||||
self.message_cache.append(message)
|
self.message_cache.append(message)
|
||||||
self._message_received_at_by_id[message.message_id] = received_at
|
self._message_received_at_by_id[message.message_id] = received_at
|
||||||
self._source_messages_by_id[message.message_id] = message
|
self._source_messages_by_id[message.message_id] = message
|
||||||
if self._agent_state == self._STATE_WAIT:
|
|
||||||
self._cancel_wait_timeout_task()
|
|
||||||
self._wait_until = None
|
|
||||||
if self._agent_state == self._STATE_RUNNING:
|
if self._agent_state == self._STATE_RUNNING:
|
||||||
self._message_debounce_required = True
|
self._message_debounce_required = True
|
||||||
if self._agent_state == self._STATE_RUNNING and self._planner_interrupt_flag is not None:
|
if self._agent_state == self._STATE_RUNNING and self._planner_interrupt_flag is not None:
|
||||||
@@ -249,7 +245,6 @@ class MaisakaHeartFlowChatting:
|
|||||||
|
|
||||||
def _record_reply_sent(self) -> None:
|
def _record_reply_sent(self) -> None:
|
||||||
"""在成功发送 reply 后记录本轮消息回复时长。"""
|
"""在成功发送 reply 后记录本轮消息回复时长。"""
|
||||||
self._clear_force_continue_until_reply()
|
|
||||||
if self._reply_latency_measurement_started_at is None:
|
if self._reply_latency_measurement_started_at is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -309,26 +304,26 @@ class MaisakaHeartFlowChatting:
|
|||||||
if not message.is_at and not message.is_mentioned:
|
if not message.is_at and not message.is_mentioned:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._arm_force_continue_until_reply(
|
self._arm_force_next_timing_continue(
|
||||||
message,
|
message,
|
||||||
is_at=message.is_at,
|
is_at=message.is_at,
|
||||||
is_mentioned=message.is_mentioned,
|
is_mentioned=message.is_mentioned,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _arm_force_continue_until_reply(
|
def _arm_force_next_timing_continue(
|
||||||
self,
|
self,
|
||||||
message: SessionMessage,
|
message: SessionMessage,
|
||||||
*,
|
*,
|
||||||
is_at: bool,
|
is_at: bool,
|
||||||
is_mentioned: bool,
|
is_mentioned: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""在检测到 @ 或提及时,要求后续轮次跳过 Timing Gate 直到成功 reply。"""
|
"""在检测到 @ 或提及时,要求下一次 Timing Gate 直接 continue。"""
|
||||||
|
|
||||||
trigger_reason = "@消息" if is_at else "提及消息" if is_mentioned else "触发消息"
|
trigger_reason = "@消息" if is_at else "提及消息" if is_mentioned else "触发消息"
|
||||||
was_armed = self._force_continue_until_reply
|
was_armed = self._force_next_timing_continue
|
||||||
self._force_continue_until_reply = True
|
self._force_next_timing_continue = True
|
||||||
self._force_continue_trigger_message_id = message.message_id
|
self._force_next_timing_message_id = message.message_id
|
||||||
self._force_continue_trigger_reason = trigger_reason
|
self._force_next_timing_reason = trigger_reason
|
||||||
|
|
||||||
if was_armed:
|
if was_armed:
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -338,34 +333,31 @@ class MaisakaHeartFlowChatting:
|
|||||||
return
|
return
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"{self.log_prefix} 检测到{trigger_reason},将跳过 Timing Gate 直到成功发送一条 reply;"
|
f"{self.log_prefix} 检测到{trigger_reason},下一次 Timing Gate 将直接视作 continue;"
|
||||||
f"消息编号={message.message_id}"
|
f"消息编号={message.message_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _clear_force_continue_until_reply(self) -> None:
|
def _consume_force_next_timing_continue_reason(self) -> str | None:
|
||||||
"""在成功发送 reply 后清理强制 continue 状态。"""
|
"""消费一次性 Timing Gate continue 状态,并返回原因描述。"""
|
||||||
|
|
||||||
if not self._force_continue_until_reply:
|
if not self._force_next_timing_continue:
|
||||||
return
|
return None
|
||||||
|
|
||||||
logger.info(
|
trigger_reason = self._force_next_timing_reason or "@/提及消息"
|
||||||
f"{self.log_prefix} 已成功发送 reply,恢复 Timing Gate;"
|
trigger_message_id = self._force_next_timing_message_id or "unknown"
|
||||||
f"触发原因={self._force_continue_trigger_reason or '未知'} "
|
reason = (
|
||||||
f"触发消息编号={self._force_continue_trigger_message_id or 'unknown'}"
|
|
||||||
)
|
|
||||||
self._force_continue_until_reply = False
|
|
||||||
self._force_continue_trigger_message_id = ""
|
|
||||||
self._force_continue_trigger_reason = ""
|
|
||||||
|
|
||||||
def _build_force_continue_timing_reason(self) -> str:
|
|
||||||
"""返回当前强制跳过 Timing Gate 的原因描述。"""
|
|
||||||
|
|
||||||
trigger_reason = self._force_continue_trigger_reason or "@/提及消息"
|
|
||||||
trigger_message_id = self._force_continue_trigger_message_id or "unknown"
|
|
||||||
return (
|
|
||||||
f"检测到新的{trigger_reason}(消息编号={trigger_message_id}),"
|
f"检测到新的{trigger_reason}(消息编号={trigger_message_id}),"
|
||||||
"本轮直接跳过 Timing Gate 并视作 continue,直到成功发送一条 reply。"
|
"本轮直接跳过 Timing Gate 并视作 continue。"
|
||||||
)
|
)
|
||||||
|
logger.info(
|
||||||
|
f"{self.log_prefix} 已结束本次强制 continue,恢复 Timing Gate;"
|
||||||
|
f"触发原因={trigger_reason} "
|
||||||
|
f"触发消息编号={trigger_message_id}"
|
||||||
|
)
|
||||||
|
self._force_next_timing_continue = False
|
||||||
|
self._force_next_timing_message_id = ""
|
||||||
|
self._force_next_timing_reason = ""
|
||||||
|
return reason
|
||||||
|
|
||||||
def _bind_planner_interrupt_flag(self, interrupt_flag: asyncio.Event) -> None:
|
def _bind_planner_interrupt_flag(self, interrupt_flag: asyncio.Event) -> None:
|
||||||
"""绑定当前可打断请求使用的中断标记。"""
|
"""绑定当前可打断请求使用的中断标记。"""
|
||||||
@@ -453,6 +445,9 @@ class MaisakaHeartFlowChatting:
|
|||||||
|
|
||||||
def _schedule_message_turn(self) -> None:
|
def _schedule_message_turn(self) -> None:
|
||||||
"""为当前待处理消息安排一次内部 turn。"""
|
"""为当前待处理消息安排一次内部 turn。"""
|
||||||
|
if self._agent_state == self._STATE_WAIT:
|
||||||
|
return
|
||||||
|
|
||||||
if not self._has_pending_messages() or self._message_turn_scheduled:
|
if not self._has_pending_messages() or self._message_turn_scheduled:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -532,8 +527,9 @@ class MaisakaHeartFlowChatting:
|
|||||||
def _enter_wait_state(self, seconds: Optional[float] = None, tool_call_id: Optional[str] = None) -> None:
|
def _enter_wait_state(self, seconds: Optional[float] = None, tool_call_id: Optional[str] = None) -> None:
|
||||||
"""切换到等待状态。"""
|
"""切换到等待状态。"""
|
||||||
self._agent_state = self._STATE_WAIT
|
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
|
self._pending_wait_tool_call_id = tool_call_id
|
||||||
|
self._message_turn_scheduled = False
|
||||||
|
self._cancel_deferred_message_turn_task()
|
||||||
self._cancel_wait_timeout_task()
|
self._cancel_wait_timeout_task()
|
||||||
if seconds is not None:
|
if seconds is not None:
|
||||||
self._wait_timeout_task = asyncio.create_task(
|
self._wait_timeout_task = asyncio.create_task(
|
||||||
@@ -543,7 +539,6 @@ class MaisakaHeartFlowChatting:
|
|||||||
def _enter_stop_state(self) -> None:
|
def _enter_stop_state(self) -> None:
|
||||||
"""切换到停止状态。"""
|
"""切换到停止状态。"""
|
||||||
self._agent_state = self._STATE_STOP
|
self._agent_state = self._STATE_STOP
|
||||||
self._wait_until = None
|
|
||||||
self._pending_wait_tool_call_id = None
|
self._pending_wait_tool_call_id = None
|
||||||
self._cancel_wait_timeout_task()
|
self._cancel_wait_timeout_task()
|
||||||
|
|
||||||
@@ -568,7 +563,6 @@ class MaisakaHeartFlowChatting:
|
|||||||
|
|
||||||
logger.info(f"{self.log_prefix} Maisaka 等待已超时")
|
logger.info(f"{self.log_prefix} Maisaka 等待已超时")
|
||||||
self._agent_state = self._STATE_RUNNING
|
self._agent_state = self._STATE_RUNNING
|
||||||
self._wait_until = None
|
|
||||||
await self._internal_turn_queue.put("timeout")
|
await self._internal_turn_queue.put("timeout")
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -672,6 +672,32 @@ class ComponentQueryService:
|
|||||||
collected_specs[entry.name] = self._build_tool_spec(entry) # type: ignore[arg-type]
|
collected_specs[entry.name] = self._build_tool_spec(entry) # type: ignore[arg-type]
|
||||||
return collected_specs
|
return collected_specs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_tool_context_payload(context: Optional[ToolExecutionContext]) -> Dict[str, Any]:
|
||||||
|
"""提取插件工具可复用的会话上下文字段。"""
|
||||||
|
|
||||||
|
if context is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
payload: Dict[str, Any] = {}
|
||||||
|
stream_id = str(context.stream_id or context.session_id or "").strip()
|
||||||
|
if stream_id:
|
||||||
|
payload["stream_id"] = stream_id
|
||||||
|
payload["chat_id"] = stream_id
|
||||||
|
|
||||||
|
anchor_message = context.metadata.get("anchor_message")
|
||||||
|
message_info = getattr(anchor_message, "message_info", None)
|
||||||
|
group_info = getattr(message_info, "group_info", None)
|
||||||
|
user_info = getattr(message_info, "user_info", None)
|
||||||
|
|
||||||
|
group_id = str(getattr(group_info, "group_id", "") or "").strip()
|
||||||
|
user_id = str(getattr(user_info, "user_id", "") or "").strip()
|
||||||
|
if group_id:
|
||||||
|
payload["group_id"] = group_id
|
||||||
|
if user_id:
|
||||||
|
payload["user_id"] = user_id
|
||||||
|
return payload
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_tool_invocation_payload(
|
def _build_tool_invocation_payload(
|
||||||
entry: "ToolEntry",
|
entry: "ToolEntry",
|
||||||
@@ -690,11 +716,17 @@ class ComponentQueryService:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
payload = dict(invocation.arguments)
|
payload = dict(invocation.arguments)
|
||||||
|
context_payload = ComponentQueryService._build_tool_context_payload(context)
|
||||||
if entry.invoke_method == "plugin.invoke_action":
|
if entry.invoke_method == "plugin.invoke_action":
|
||||||
stream_id = context.stream_id if context is not None else invocation.stream_id
|
stream_id = str(
|
||||||
|
context_payload.get("stream_id")
|
||||||
|
or (context.stream_id if context is not None else invocation.stream_id)
|
||||||
|
or invocation.stream_id
|
||||||
|
).strip()
|
||||||
reasoning = context.reasoning if context is not None else invocation.reasoning
|
reasoning = context.reasoning if context is not None else invocation.reasoning
|
||||||
payload = {
|
payload = {
|
||||||
**payload,
|
**payload,
|
||||||
|
**{key: value for key, value in context_payload.items() if key not in payload or not payload.get(key)},
|
||||||
"stream_id": stream_id,
|
"stream_id": stream_id,
|
||||||
"chat_id": stream_id,
|
"chat_id": stream_id,
|
||||||
"reasoning": reasoning,
|
"reasoning": reasoning,
|
||||||
@@ -702,6 +734,11 @@ class ComponentQueryService:
|
|||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
for key, value in context_payload.items():
|
||||||
|
if key not in payload or not payload.get(key):
|
||||||
|
payload[key] = value
|
||||||
|
return payload
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_tool_invoke_result(
|
def _parse_tool_invoke_result(
|
||||||
entry: "ToolEntry",
|
entry: "ToolEntry",
|
||||||
|
|||||||
Reference in New Issue
Block a user