feat:展示更详细的工具信息,修改wait定义
This commit is contained in:
@@ -37,4 +37,4 @@
|
||||
|
||||
{group_chat_attention_block}
|
||||
|
||||
现在,请你输出你对{bot_name}发言的分析,你必须先输出文本内容的分析,然后再进行工具调用,输出json形式的function call:
|
||||
现在,请你输出你对{bot_name}发言的分析,你必须先输出文本内容的分析,然后再进行工具调用,:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
在当前场景中,不同的人正在互动({bot_name} 也是一位参与的用户),用户也可能正在连续发送消息或彼此互动。
|
||||
你的任务不是生成对别人可见的发言,也不是直接使用查询类工具,而是判断当前是否应该:
|
||||
- continue:立刻进入下一轮完整思考、搜集信息、回复与其他工具执行
|
||||
- wait:再等待一段时间,然后重新判断,可选几秒的等待,也可等待数分钟
|
||||
- wait:固定再等待一段时间,时间到后再重新判断;等待期间即使收到新消息也不会提前打断,只会暂存到超时后统一处理
|
||||
- no_reply:本轮不继续,直接等待新的外部消息
|
||||
|
||||
节奏控制规则:
|
||||
|
||||
@@ -21,6 +21,7 @@ dependencies = [
|
||||
"maim-message>=0.6.2",
|
||||
"maibot-dashboard==1.0.0.dev2026040439",
|
||||
"maibot-plugin-sdk>=2.3.0",
|
||||
"matplotlib>=3.10.5",
|
||||
"mcp",
|
||||
"msgpack>=1.1.2",
|
||||
"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.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:
|
||||
|
||||
@@ -346,6 +346,32 @@ def _convert_assistant_tool_calls(tool_calls: List[ToolCall]) -> List[ChatComple
|
||||
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]:
|
||||
"""将内部消息列表转换为 OpenAI 兼容消息列表。
|
||||
|
||||
@@ -965,7 +991,12 @@ class OpenaiClient(AdapterClient[AsyncStream[ChatCompletionChunk], ChatCompletio
|
||||
model_info = request.model_info
|
||||
|
||||
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 = (
|
||||
_convert_tool_options(request.tool_options) if request.tool_options else None
|
||||
)
|
||||
|
||||
@@ -397,8 +397,6 @@ class LLMOrchestrator:
|
||||
start_time = time.time()
|
||||
|
||||
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(
|
||||
request_type=RequestType.RESPONSE,
|
||||
|
||||
@@ -12,8 +12,8 @@ def get_tool_spec() -> ToolSpec:
|
||||
|
||||
return ToolSpec(
|
||||
name="wait",
|
||||
brief_description="暂停当前对话并等待用户新的输入。",
|
||||
detailed_description="参数说明:\n- seconds:integer,必填。等待的秒数。",
|
||||
brief_description="暂停当前对话并固定等待一段时间,期间不因新消息提前恢复。",
|
||||
detailed_description="参数说明:\n- seconds:integer,必填。等待的秒数。等待期间收到的新消息只会暂存,直到超时后再继续处理。",
|
||||
parameters_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -46,6 +46,6 @@ async def handle_tool(
|
||||
tool_ctx.runtime._enter_wait_state(seconds=wait_seconds, tool_call_id=invocation.call_id)
|
||||
return tool_ctx.build_success_result(
|
||||
invocation.tool_name,
|
||||
f"当前对话循环进入等待状态,最长等待 {wait_seconds} 秒。",
|
||||
f"当前对话循环进入等待状态,将固定等待 {wait_seconds} 秒;期间收到的新消息不会提前打断本次等待。",
|
||||
metadata={"pause_execution": True},
|
||||
)
|
||||
|
||||
@@ -475,6 +475,44 @@ class MaisakaChatLoopService:
|
||||
return normalized_text
|
||||
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(
|
||||
self,
|
||||
selected_history: List[LLMContextMessage],
|
||||
@@ -620,7 +658,8 @@ class MaisakaChatLoopService:
|
||||
f"总工具数={len(tool_specs)} "
|
||||
f"内置工具数={len(builtin_tool_specs)} "
|
||||
f"候选工具数={len(candidate_tool_specs)} "
|
||||
f"最多保留候选数={max_keep}"
|
||||
f"最多保留候选数={max_keep} "
|
||||
f"过滤前全部工具名={self._build_tool_spec_names_log_text(tool_specs)}"
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -660,6 +699,7 @@ class MaisakaChatLoopService:
|
||||
"工具预筛选完成: "
|
||||
f"筛选前总数={len(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]}"
|
||||
)
|
||||
return filtered_tool_specs
|
||||
@@ -729,6 +769,11 @@ class MaisakaChatLoopService:
|
||||
if isinstance(raw_tool_definitions, list):
|
||||
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
|
||||
if global_config.debug.show_maisaka_thinking:
|
||||
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,
|
||||
image_display_mode=image_display_mode,
|
||||
folded=global_config.debug.fold_maisaka_thinking,
|
||||
tool_definitions=list(all_tools),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"规划器请求开始: "
|
||||
f"规划器请求开始(request_kind={request_kind}): "
|
||||
f"已选上下文消息数={len(selected_history)} "
|
||||
f"大模型消息数={len(built_messages)} "
|
||||
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_tool_calls = list(generation_result.tool_calls or [])
|
||||
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]:
|
||||
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
|
||||
def _render_tool_call_panel(cls, tool_call: Any, index: int, parent_index: int) -> Panel:
|
||||
title = Text.assemble(
|
||||
@@ -291,6 +370,20 @@ class PromptCLIVisualizer:
|
||||
|
||||
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
|
||||
def _render_message_content_html(cls, content: Any) -> str:
|
||||
if isinstance(content, str):
|
||||
@@ -356,6 +449,7 @@ class PromptCLIVisualizer:
|
||||
*,
|
||||
request_kind: str,
|
||||
selection_reason: str,
|
||||
tool_definitions: list[dict[str, Any]] | None = None,
|
||||
) -> str:
|
||||
panel_title, _ = cls.get_request_panel_style(request_kind)
|
||||
message_cards: List[str] = []
|
||||
@@ -378,16 +472,12 @@ class PromptCLIVisualizer:
|
||||
tool_panels = ""
|
||||
raw_tool_calls = message.get("tool_calls") or []
|
||||
if isinstance(raw_tool_calls, list) and raw_tool_calls:
|
||||
tool_items = []
|
||||
for tool_call_index, tool_call in enumerate(raw_tool_calls, start=1):
|
||||
normalized_tool_call = cls.format_tool_call_for_display(tool_call)
|
||||
tool_items.append(
|
||||
"<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>"
|
||||
)
|
||||
tool_panels = "".join(tool_items)
|
||||
tool_panels = (
|
||||
"<div class='tool-list'>"
|
||||
"<div class='tool-list-title'>工具调用</div>"
|
||||
f"{''.join(cls._build_tool_call_html(tool_call) for tool_call in raw_tool_calls)}"
|
||||
"</div>"
|
||||
)
|
||||
|
||||
message_cards.append(
|
||||
"<section class='message-card'>"
|
||||
@@ -405,6 +495,21 @@ class PromptCLIVisualizer:
|
||||
if selection_reason.strip():
|
||||
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>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
@@ -491,7 +596,7 @@ class PromptCLIVisualizer:
|
||||
font-weight: 600;
|
||||
}}
|
||||
.message-content pre,
|
||||
.tool-panel pre {{
|
||||
.tool-card pre {{
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
@@ -517,18 +622,75 @@ class PromptCLIVisualizer:
|
||||
border-radius: 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;
|
||||
background: #fcf4ff;
|
||||
border: 1px solid #f0d7fb;
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
overflow: hidden;
|
||||
}}
|
||||
.tool-panel-title {{
|
||||
color: #a21caf;
|
||||
.tool-card:first-of-type {{
|
||||
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-weight: 800;
|
||||
}}
|
||||
.tool-card-summary::-webkit-details-marker {{
|
||||
display: none;
|
||||
}}
|
||||
.tool-card-summary::after {{
|
||||
content: "展开";
|
||||
color: #a21caf;
|
||||
font-size: 12px;
|
||||
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 {{
|
||||
background: #f8fafc;
|
||||
@@ -564,6 +726,7 @@ class PromptCLIVisualizer:
|
||||
{subtitle_html}
|
||||
</header>
|
||||
{''.join(message_cards)}
|
||||
{tool_definition_section_html}
|
||||
</main>
|
||||
</body>
|
||||
</html>"""
|
||||
@@ -578,6 +741,7 @@ class PromptCLIVisualizer:
|
||||
request_kind: str,
|
||||
selection_reason: str,
|
||||
image_display_mode: Literal["legacy", "path_link"],
|
||||
tool_definitions: list[dict[str, Any]] | None = None,
|
||||
) -> RenderableType:
|
||||
"""构建用于查看完整 prompt 的折叠入口内容。"""
|
||||
|
||||
@@ -603,10 +767,14 @@ class PromptCLIVisualizer:
|
||||
viewer_messages.append(normalized_message)
|
||||
|
||||
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_messages,
|
||||
request_kind=request_kind,
|
||||
selection_reason=selection_reason,
|
||||
tool_definitions=tool_definitions,
|
||||
)
|
||||
saved_paths = PromptPreviewLogger.save_preview_files(
|
||||
chat_id,
|
||||
@@ -623,12 +791,12 @@ class PromptCLIVisualizer:
|
||||
|
||||
body = Group(
|
||||
Text.from_markup(
|
||||
f"[bold green]富文本预览:{viewer_html_path}[/bold green] "
|
||||
f"[link={viewer_uri}]点击在浏览器打开富文本 Prompt 视图[/link]"
|
||||
f"[bold green]html预览:{viewer_html_path}[/bold green] "
|
||||
f"[link={viewer_uri}]在浏览器打开 Prompt [/link]"
|
||||
),
|
||||
Text.from_markup(
|
||||
f"[magenta]原始文本备份:{prompt_dump_path}[/magenta] "
|
||||
f"[cyan][link={dump_uri}]点击直接打开 Prompt 文本[/link][/cyan]"
|
||||
f"[magenta]原始文本:{prompt_dump_path}[/magenta] "
|
||||
f"[cyan][link={dump_uri}]点击打开 Prompt 文本[/link][/cyan]"
|
||||
),
|
||||
)
|
||||
return body
|
||||
@@ -644,6 +812,7 @@ class PromptCLIVisualizer:
|
||||
selection_reason: str,
|
||||
image_display_mode: Literal["legacy", "path_link"],
|
||||
folded: bool,
|
||||
tool_definitions: list[dict[str, Any]] | None = None,
|
||||
) -> Panel:
|
||||
"""构建用于嵌入结果面板中的 Prompt 区块。"""
|
||||
|
||||
@@ -656,6 +825,7 @@ class PromptCLIVisualizer:
|
||||
request_kind=request_kind,
|
||||
selection_reason=selection_reason,
|
||||
image_display_mode=image_display_mode,
|
||||
tool_definitions=tool_definitions,
|
||||
)
|
||||
else:
|
||||
ordered_panels = cls.build_prompt_panels(
|
||||
|
||||
@@ -229,7 +229,7 @@ class MaisakaReasoningEngine:
|
||||
) -> tuple[Literal["continue", "no_reply", "wait"], Any, list[str]]:
|
||||
"""运行 Timing Gate 子代理并返回控制决策。"""
|
||||
|
||||
if self._runtime._force_continue_until_reply:
|
||||
if self._runtime._force_next_timing_continue:
|
||||
return self._build_forced_continue_timing_result()
|
||||
|
||||
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]]:
|
||||
"""构造跳过 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}")
|
||||
return (
|
||||
"continue",
|
||||
@@ -334,7 +334,10 @@ class MaisakaReasoningEngine:
|
||||
self._runtime._agent_state = self._runtime._STATE_RUNNING
|
||||
if 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)
|
||||
anchor_message = cached_messages[-1]
|
||||
else:
|
||||
@@ -346,7 +349,9 @@ class MaisakaReasoningEngine:
|
||||
continue
|
||||
logger.info(f"{self._runtime.log_prefix} 等待超时后开始新一轮思考")
|
||||
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()
|
||||
|
||||
try:
|
||||
@@ -399,7 +404,7 @@ class MaisakaReasoningEngine:
|
||||
)
|
||||
timing_gate_required = self._mark_timing_gate_completed(timing_action)
|
||||
if timing_action != "continue":
|
||||
logger.info(
|
||||
logger.debug(
|
||||
f"{self._runtime.log_prefix} Timing Gate 结束当前回合: "
|
||||
f"回合={round_index + 1} 动作={timing_action}"
|
||||
)
|
||||
@@ -475,7 +480,6 @@ class MaisakaReasoningEngine:
|
||||
break
|
||||
|
||||
asyncio.create_task(self._runtime._trigger_batch_learning(interrupted_messages))
|
||||
self._append_wait_interrupted_message_if_needed()
|
||||
await self._ingest_messages(interrupted_messages)
|
||||
anchor_message = interrupted_messages[-1]
|
||||
logger.info(
|
||||
@@ -588,33 +592,22 @@ class MaisakaReasoningEngine:
|
||||
return self._runtime.message_cache[-1]
|
||||
return None
|
||||
|
||||
def _build_wait_timeout_message(self) -> ToolResultMessage:
|
||||
"""构造 wait 超时后的工具结果消息。"""
|
||||
def _build_wait_completed_message(self, *, has_new_messages: bool) -> ToolResultMessage:
|
||||
"""构造 wait 完成后的工具结果消息。"""
|
||||
tool_call_id = self._runtime._pending_wait_tool_call_id or "wait_timeout"
|
||||
self._runtime._pending_wait_tool_call_id = None
|
||||
content = (
|
||||
"等待已结束,期间收到了新的用户输入。请结合这些新消息继续下一轮思考。"
|
||||
if has_new_messages
|
||||
else "等待已超时,期间没有收到新的用户输入。请基于现有上下文继续下一轮思考。"
|
||||
)
|
||||
return ToolResultMessage(
|
||||
content="等待已超时,期间没有收到新的用户输入。请基于现有上下文继续下一轮思考。",
|
||||
content=content,
|
||||
timestamp=datetime.now(),
|
||||
tool_call_id=tool_call_id,
|
||||
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:
|
||||
"""处理传入消息列表,将其转换为历史消息并加入聊天历史缓存。"""
|
||||
for message in messages:
|
||||
|
||||
@@ -93,11 +93,10 @@ class MaisakaHeartFlowChatting:
|
||||
self._max_internal_rounds = MAX_INTERNAL_ROUNDS
|
||||
self._max_context_size = max(1, int(global_config.chat.max_context_size))
|
||||
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._force_continue_until_reply = False
|
||||
self._force_continue_trigger_message_id = ""
|
||||
self._force_continue_trigger_reason = ""
|
||||
self._force_next_timing_continue = False
|
||||
self._force_next_timing_message_id = ""
|
||||
self._force_next_timing_reason = ""
|
||||
self._planner_interrupt_flag: Optional[asyncio.Event] = None
|
||||
self._planner_interrupt_requested = False
|
||||
self._planner_interrupt_consecutive_count = 0
|
||||
@@ -176,9 +175,6 @@ class MaisakaHeartFlowChatting:
|
||||
self.message_cache.append(message)
|
||||
self._message_received_at_by_id[message.message_id] = received_at
|
||||
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:
|
||||
self._message_debounce_required = True
|
||||
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:
|
||||
"""在成功发送 reply 后记录本轮消息回复时长。"""
|
||||
self._clear_force_continue_until_reply()
|
||||
if self._reply_latency_measurement_started_at is None:
|
||||
return
|
||||
|
||||
@@ -309,26 +304,26 @@ class MaisakaHeartFlowChatting:
|
||||
if not message.is_at and not message.is_mentioned:
|
||||
return
|
||||
|
||||
self._arm_force_continue_until_reply(
|
||||
self._arm_force_next_timing_continue(
|
||||
message,
|
||||
is_at=message.is_at,
|
||||
is_mentioned=message.is_mentioned,
|
||||
)
|
||||
|
||||
def _arm_force_continue_until_reply(
|
||||
def _arm_force_next_timing_continue(
|
||||
self,
|
||||
message: SessionMessage,
|
||||
*,
|
||||
is_at: bool,
|
||||
is_mentioned: bool,
|
||||
) -> None:
|
||||
"""在检测到 @ 或提及时,要求后续轮次跳过 Timing Gate 直到成功 reply。"""
|
||||
"""在检测到 @ 或提及时,要求下一次 Timing Gate 直接 continue。"""
|
||||
|
||||
trigger_reason = "@消息" if is_at else "提及消息" if is_mentioned else "触发消息"
|
||||
was_armed = self._force_continue_until_reply
|
||||
self._force_continue_until_reply = True
|
||||
self._force_continue_trigger_message_id = message.message_id
|
||||
self._force_continue_trigger_reason = trigger_reason
|
||||
was_armed = self._force_next_timing_continue
|
||||
self._force_next_timing_continue = True
|
||||
self._force_next_timing_message_id = message.message_id
|
||||
self._force_next_timing_reason = trigger_reason
|
||||
|
||||
if was_armed:
|
||||
logger.info(
|
||||
@@ -338,34 +333,31 @@ class MaisakaHeartFlowChatting:
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"{self.log_prefix} 检测到{trigger_reason},将跳过 Timing Gate 直到成功发送一条 reply;"
|
||||
f"{self.log_prefix} 检测到{trigger_reason},下一次 Timing Gate 将直接视作 continue;"
|
||||
f"消息编号={message.message_id}"
|
||||
)
|
||||
|
||||
def _clear_force_continue_until_reply(self) -> None:
|
||||
"""在成功发送 reply 后清理强制 continue 状态。"""
|
||||
def _consume_force_next_timing_continue_reason(self) -> str | None:
|
||||
"""消费一次性 Timing Gate continue 状态,并返回原因描述。"""
|
||||
|
||||
if not self._force_continue_until_reply:
|
||||
return
|
||||
if not self._force_next_timing_continue:
|
||||
return None
|
||||
|
||||
logger.info(
|
||||
f"{self.log_prefix} 已成功发送 reply,恢复 Timing Gate;"
|
||||
f"触发原因={self._force_continue_trigger_reason or '未知'} "
|
||||
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 (
|
||||
trigger_reason = self._force_next_timing_reason or "@/提及消息"
|
||||
trigger_message_id = self._force_next_timing_message_id or "unknown"
|
||||
reason = (
|
||||
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:
|
||||
"""绑定当前可打断请求使用的中断标记。"""
|
||||
@@ -453,6 +445,9 @@ class MaisakaHeartFlowChatting:
|
||||
|
||||
def _schedule_message_turn(self) -> None:
|
||||
"""为当前待处理消息安排一次内部 turn。"""
|
||||
if self._agent_state == self._STATE_WAIT:
|
||||
return
|
||||
|
||||
if not self._has_pending_messages() or self._message_turn_scheduled:
|
||||
return
|
||||
|
||||
@@ -532,8 +527,9 @@ class MaisakaHeartFlowChatting:
|
||||
def _enter_wait_state(self, seconds: Optional[float] = None, tool_call_id: Optional[str] = None) -> None:
|
||||
"""切换到等待状态。"""
|
||||
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._message_turn_scheduled = False
|
||||
self._cancel_deferred_message_turn_task()
|
||||
self._cancel_wait_timeout_task()
|
||||
if seconds is not None:
|
||||
self._wait_timeout_task = asyncio.create_task(
|
||||
@@ -543,7 +539,6 @@ class MaisakaHeartFlowChatting:
|
||||
def _enter_stop_state(self) -> None:
|
||||
"""切换到停止状态。"""
|
||||
self._agent_state = self._STATE_STOP
|
||||
self._wait_until = None
|
||||
self._pending_wait_tool_call_id = None
|
||||
self._cancel_wait_timeout_task()
|
||||
|
||||
@@ -568,7 +563,6 @@ class MaisakaHeartFlowChatting:
|
||||
|
||||
logger.info(f"{self.log_prefix} Maisaka 等待已超时")
|
||||
self._agent_state = self._STATE_RUNNING
|
||||
self._wait_until = None
|
||||
await self._internal_turn_queue.put("timeout")
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
|
||||
@@ -672,6 +672,32 @@ class ComponentQueryService:
|
||||
collected_specs[entry.name] = self._build_tool_spec(entry) # type: ignore[arg-type]
|
||||
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
|
||||
def _build_tool_invocation_payload(
|
||||
entry: "ToolEntry",
|
||||
@@ -690,16 +716,27 @@ class ComponentQueryService:
|
||||
"""
|
||||
|
||||
payload = dict(invocation.arguments)
|
||||
context_payload = ComponentQueryService._build_tool_context_payload(context)
|
||||
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
|
||||
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,
|
||||
"chat_id": stream_id,
|
||||
"reasoning": reasoning,
|
||||
"action_data": dict(invocation.arguments),
|
||||
}
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user