feat:提高工具调用成功率,移除冗余的描述中参数介绍,增加索引列表的描述,修改prompt,移除timing的wait打断

This commit is contained in:
SengokuCola
2026-04-10 00:45:32 +08:00
parent 0852c38e81
commit fee9341620
17 changed files with 828 additions and 450 deletions

View File

@@ -29,6 +29,6 @@ async def handle_tool(
tool_ctx.runtime._enter_stop_state()
return tool_ctx.build_success_result(
invocation.tool_name,
"当前对话循环已结束本轮思考,等待新的外部消息到来。",
"当前对话循环已结束本轮思考,等待新的消息到来。",
metadata={"pause_execution": True},
)

View File

@@ -29,6 +29,6 @@ async def handle_tool(
tool_ctx.runtime._enter_stop_state()
return tool_ctx.build_success_result(
invocation.tool_name,
"当前对话循环已暂停,等待新消息到来。",
"当前暂时停止思考,等待新消息到来。",
metadata={"pause_execution": True},
)

View File

@@ -91,10 +91,6 @@ async def handle_tool(
f"未找到要回复的目标消息msg_id={target_message_id}",
)
logger.info(
f"{tool_ctx.runtime.log_prefix} 已触发回复工具,"
f"目标消息编号={target_message_id} 引用回复={set_quote} 最新思考={latest_thought!r}"
)
try:
replyer = replyer_manager.get_replyer(
chat_stream=tool_ctx.runtime.chat_stream,

View File

@@ -408,46 +408,43 @@ class MaisakaChatLoopService:
if llm_message is not None:
messages.append(llm_message)
normalized_injected_messages: List[Message] = []
for injected_message in injected_user_messages or []:
normalized_message = str(injected_message or "").strip()
if not normalized_message:
continue
messages.append(
normalized_injected_messages.append(
MessageBuilder()
.set_role(RoleType.User)
.add_text_content(normalized_message)
.build()
)
if normalized_injected_messages:
insertion_index = self._resolve_injected_user_messages_insertion_index(messages)
messages[insertion_index:insertion_index] = normalized_injected_messages
return messages
@staticmethod
def _build_tool_names_log_text(tool_definitions: Sequence[ToolDefinitionInput]) -> str:
"""构造 planner 请求前的工具列表日志文本
def _resolve_injected_user_messages_insertion_index(messages: Sequence[Message]) -> int:
"""计算 injected meta user messages 在请求中的插入位置
Args:
tool_definitions: 本轮实际传给 planner 的工具定义列表。
Returns:
str: 适合直接写入日志的单行文本
规则与 deferred attachment 更接近:
- 从尾部向前寻找最近的 stopping point
- stopping point 为 assistant 消息或 tool 结果消息;
- 找到后插入到其后面;
- 若不存在 stopping point则退回到 system 消息之后
"""
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)
for index in range(len(messages) - 1, -1, -1):
message = messages[index]
if message.role in {RoleType.Assistant, RoleType.Tool}:
return index + 1
if not tool_names:
return "[无工具]"
return "".join(tool_names)
if messages and messages[0].role == RoleType.System:
return 1
return 0
async def chat_loop_step(
self,
@@ -517,11 +514,6 @@ 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"
@@ -536,13 +528,6 @@ class MaisakaChatLoopService:
tool_definitions=list(all_tools),
)
logger.info(
f"规划器请求开始(request_kind={request_kind}): "
f"已选上下文消息数={len(selected_history)} "
f"大模型消息数={len(built_messages)} "
f"工具数={len(all_tools)} "
f"启用打断={self._interrupt_flag is not None}"
)
generation_result = await self._llm_chat.generate_response_with_messages(
message_factory=message_factory,
options=LLMGenerationOptions(
@@ -609,7 +594,7 @@ class MaisakaChatLoopService:
*,
max_context_size: Optional[int] = None,
) -> tuple[List[LLMContextMessage], str]:
"""??????? LLM ???????"""
"""选择LLM上下文消息"""
effective_context_size = max(1, int(max_context_size or global_config.chat.max_context_size))
selected_indices: List[int] = []
@@ -627,7 +612,7 @@ class MaisakaChatLoopService:
break
if not selected_indices:
return [], f"???????? {effective_context_size} ? user/assistant??? 0 ??"
return [], f"没有选择到上下文消息,实际发送 {effective_context_size} user/assistant 消息"
selected_indices.reverse()
selected_history = [chat_history[index] for index in selected_indices]
@@ -644,47 +629,6 @@ class MaisakaChatLoopService:
selection_reason,
)
@staticmethod
def _select_llm_context_messages(chat_history: List[LLMContextMessage]) -> tuple[List[LLMContextMessage], str]:
"""选择真正发送给 LLM 的上下文消息。
Args:
chat_history: 当前全部对话历史。
Returns:
tuple[List[LLMContextMessage], str]: `(已选上下文, 选择说明)`。
"""
max_context_size = max(1, int(global_config.chat.max_context_size))
selected_indices: List[int] = []
counted_message_count = 0
for index in range(len(chat_history) - 1, -1, -1):
message = chat_history[index]
if message.to_llm_message() is None:
continue
selected_indices.append(index)
if message.count_in_context:
counted_message_count += 1
if counted_message_count >= max_context_size:
break
if not selected_indices:
return [], f"上下文判定:最近 {max_context_size} 条 user/assistant当前 0 条)"
selected_indices.reverse()
selected_history = [chat_history[index] for index in selected_indices]
selected_history, hidden_assistant_count = MaisakaChatLoopService._hide_early_assistant_messages(selected_history)
selected_history, _ = drop_orphan_tool_results(selected_history)
return (
selected_history,
(
f"上下文判定:最近 {max_context_size} 条 user/assistant"
f"展示并发送窗口内消息 {len(selected_history)}"
),
)
@staticmethod
def _hide_early_assistant_messages(
selected_history: List[LLMContextMessage],

View File

@@ -4,6 +4,7 @@ from typing import Any
_REQUEST_PANEL_STYLE_MAP: dict[str, tuple[str, str]] = {
"planner": ("\u004d\u0061\u0069\u0053\u0061\u006b\u0061 \u5927\u6a21\u578b\u8bf7\u6c42 - \u5bf9\u8bdd\u5355\u6b65", "green"),
"timing_gate": ("\u004d\u0061\u0069\u0053\u0061\u006b\u0061 \u5927\u6a21\u578b\u8bf7\u6c42 - Timing Gate \u5b50\u4ee3\u7406", "bright_magenta"),
"replyer": ("\u004d\u0061\u0069\u0053\u0061\u006b\u0061 \u56de\u590d\u5668 Prompt", "bright_yellow"),
"emotion": ("MaiSaka Emotion Tool Prompt", "bright_cyan"),

View File

@@ -269,7 +269,7 @@ class PromptCLIVisualizer:
)
return (
"<details class='tool-card'>"
"<details class='tool-card tool-call-card'>"
"<summary class='tool-card-summary'>"
f"<span class='tool-card-name'>{html.escape(tool_name)}</span>"
"</summary>"
@@ -638,6 +638,9 @@ class PromptCLIVisualizer:
border-radius: 14px;
overflow: hidden;
}}
.tool-call-card {{
border-color: #ff8700;
}}
.tool-card:first-of-type {{
margin-top: 0;
}}
@@ -672,6 +675,9 @@ class PromptCLIVisualizer:
padding: 12px 14px;
background: rgba(255, 255, 255, 0.52);
}}
.tool-call-card .tool-card-body {{
border-top-color: #ff8700;
}}
.tool-card-meta {{
margin-bottom: 10px;
color: #a21caf;

View File

@@ -118,36 +118,27 @@ class MaisakaReasoningEngine:
)
self._runtime._chat_loop_service.set_interrupt_flag(None)
async def _run_interruptible_sub_agent(
async def _run_timing_gate_sub_agent(
self,
*,
context_message_limit: int,
system_prompt: str,
tool_definitions: list[dict[str, Any]],
) -> Any:
"""运行一轮可被新消息打断的临时子代理请求。"""
"""运行一轮 Timing Gate 子代理请求。
interrupt_flag = asyncio.Event()
interrupted = False
self._runtime._bind_planner_interrupt_flag(interrupt_flag)
try:
return await self._runtime.run_sub_agent(
context_message_limit=context_message_limit,
system_prompt=system_prompt,
request_kind="timing_gate",
interrupt_flag=interrupt_flag,
max_tokens=TIMING_GATE_MAX_TOKENS,
temperature=0.1,
tool_definitions=tool_definitions,
)
except ReqAbortException:
interrupted = True
raise
finally:
self._runtime._unbind_planner_interrupt_flag(
interrupt_flag,
interrupted=interrupted,
)
Timing Gate 阶段不再响应新的 planner 打断,只有主 planner 阶段允许被打断。
"""
return await self._runtime.run_sub_agent(
context_message_limit=context_message_limit,
system_prompt=system_prompt,
request_kind="timing_gate",
interrupt_flag=None,
max_tokens=TIMING_GATE_MAX_TOKENS,
temperature=0.1,
tool_definitions=tool_definitions,
)
@staticmethod
def _build_timing_gate_fallback_prompt() -> str:
@@ -240,18 +231,19 @@ class MaisakaReasoningEngine:
async def _run_timing_gate(
self,
anchor_message: SessionMessage,
) -> tuple[Literal["continue", "no_reply", "wait"], Any, list[str]]:
) -> tuple[Literal["continue", "no_reply", "wait"], Any, list[str], list[dict[str, Any]]]:
"""运行 Timing Gate 子代理并返回控制决策。"""
if self._runtime._force_next_timing_continue:
return self._build_forced_continue_timing_result()
response = await self._run_interruptible_sub_agent(
response = await self._run_timing_gate_sub_agent(
context_message_limit=TIMING_GATE_CONTEXT_LIMIT,
system_prompt=self._build_timing_gate_system_prompt(),
tool_definitions=get_timing_tools(),
)
tool_result_summaries: list[str] = []
tool_monitor_results: list[dict[str, Any]] = []
selected_tool_call: Optional[ToolCall] = None
for tool_call in response.tool_calls:
if tool_call.func_name in TIMING_GATE_TOOL_NAMES:
@@ -260,11 +252,11 @@ class MaisakaReasoningEngine:
if selected_tool_call is None:
logger.warning(f"{self._runtime.log_prefix} Timing Gate 未返回有效控制工具,默认继续执行 Action Loop")
return "continue", response, tool_result_summaries
return "continue", response, tool_result_summaries, tool_monitor_results
append_history = selected_tool_call.func_name != "continue"
store_record = selected_tool_call.func_name != "continue"
_, result, _ = await self._invoke_tool_call(
invocation, result, tool_spec = await self._invoke_tool_call(
selected_tool_call,
response.content or "",
anchor_message,
@@ -272,16 +264,27 @@ class MaisakaReasoningEngine:
store_record=store_record,
)
tool_result_summaries.append(self._build_tool_result_summary(selected_tool_call, result))
tool_monitor_results.append(
self._build_tool_monitor_result(
selected_tool_call,
invocation,
result,
duration_ms=0.0,
tool_spec=tool_spec,
)
)
timing_action = str(result.metadata.get("timing_action") or selected_tool_call.func_name).strip()
if timing_action not in TIMING_GATE_TOOL_NAMES:
logger.warning(
f"{self._runtime.log_prefix} Timing Gate 返回未知动作 {timing_action!r},将按 continue 处理"
)
return "continue", response, tool_result_summaries
return timing_action, response, tool_result_summaries
return "continue", response, tool_result_summaries, tool_monitor_results
return timing_action, response, tool_result_summaries, tool_monitor_results
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], list[dict[str, Any]]]:
"""构造跳过 Timing Gate 时使用的伪 continue 结果。"""
reason = self._runtime._consume_force_next_timing_continue_reason() or "本轮直接跳过 Timing Gate 并视作 continue。"
@@ -309,6 +312,7 @@ class MaisakaReasoningEngine:
prompt_section=None,
),
[f"- continue [强制跳过]: {reason}"],
[],
)
@staticmethod
@@ -383,10 +387,14 @@ class MaisakaReasoningEngine:
planner_started_at = 0.0
planner_duration_ms = 0.0
timing_duration_ms = 0.0
current_stage_started_at = 0.0
timing_action: Optional[str] = None
timing_response: Optional[ChatResponse] = None
timing_tool_results: Optional[list[str]] = None
timing_tool_monitor_results: Optional[list[dict[str, Any]]] = None
response: Optional[ChatResponse] = None
action_tool_definitions: list[dict[str, Any]] = []
planner_extra_lines: list[str] = []
tool_result_summaries: list[str] = []
tool_monitor_results: list[dict[str, Any]] = []
try:
@@ -399,10 +407,14 @@ class MaisakaReasoningEngine:
)
if timing_gate_required:
current_stage_started_at = time.time()
timing_started_at = time.time()
timing_action, timing_response, timing_tool_results = await self._run_timing_gate(
anchor_message
)
(
timing_action,
timing_response,
timing_tool_results,
timing_tool_monitor_results,
) = await self._run_timing_gate(anchor_message)
timing_duration_ms = (time.time() - timing_started_at) * 1000
cycle_detail.time_records["timing_gate"] = timing_duration_ms / 1000
await emit_timing_gate_result(
@@ -430,6 +442,7 @@ class MaisakaReasoningEngine:
)
planner_started_at = time.time()
current_stage_started_at = planner_started_at
action_tool_definitions, deferred_tools_reminder = await self._build_action_tool_definitions()
logger.info(
f"{self._runtime.log_prefix} 规划器开始执行: "
@@ -472,14 +485,46 @@ class MaisakaReasoningEngine:
if not response.content:
break
except ReqAbortException:
except ReqAbortException as exc:
interrupted_at = time.time()
interrupted_stage_label = "Planner"
interrupted_text = (
"Planner 在流式响应阶段被新消息打断。"
"本轮未完成,因此这里展示的是中断说明而不是完整返回。"
)
interrupted_response = ChatResponse(
content=interrupted_text or None,
tool_calls=[],
request_messages=[],
raw_message=AssistantMessage(
content=interrupted_text,
timestamp=datetime.now(),
tool_calls=[],
source_kind="perception",
),
selected_history_count=len(self._runtime._chat_history),
tool_count=len(action_tool_definitions),
prompt_tokens=0,
built_message_count=0,
completion_tokens=0,
total_tokens=0,
prompt_section=None,
)
interrupted_extra_lines = [
"状态:已被新消息打断",
f"打断位置:{interrupted_stage_label} 请求流式响应阶段",
f"打断耗时:{interrupted_at - current_stage_started_at:.3f}",
f"打断原因:{str(exc) or '收到外部中断信号'}",
]
interrupted_extra_lines.append("展示内容:以下为 Maisaka 侧记录的中断说明")
response = interrupted_response
planner_extra_lines = interrupted_extra_lines
logger.info(
f"{self._runtime.log_prefix} 规划器打断成功: "
f"{self._runtime.log_prefix} {interrupted_stage_label} 打断成功: "
f"回合={round_index + 1} "
f"开始时间={planner_started_at:.3f} "
f"开始时间={current_stage_started_at:.3f} "
f"打断时间={interrupted_at:.3f} "
f"耗时={interrupted_at - planner_started_at:.3f}"
f"耗时={interrupted_at - current_stage_started_at:.3f}"
)
if not self._should_retry_planner_after_interrupt(
round_index=round_index,
@@ -506,6 +551,7 @@ class MaisakaReasoningEngine:
completed_cycle = self._end_cycle(cycle_detail)
self._runtime._render_context_usage_panel(
cycle_id=cycle_detail.cycle_id,
time_records=dict(completed_cycle.time_records),
timing_selected_history_count=(
timing_response.selected_history_count if timing_response is not None else None
),
@@ -516,6 +562,7 @@ class MaisakaReasoningEngine:
timing_response=timing_response.content or "" if timing_response is not None else "",
timing_tool_calls=timing_response.tool_calls if timing_response is not None else None,
timing_tool_results=timing_tool_results,
timing_tool_detail_results=timing_tool_monitor_results,
timing_prompt_section=(
timing_response.prompt_section if timing_response is not None else None
),
@@ -528,6 +575,7 @@ class MaisakaReasoningEngine:
planner_tool_results=tool_result_summaries,
planner_tool_detail_results=tool_monitor_results,
planner_prompt_section=response.prompt_section if response is not None else None,
planner_extra_lines=planner_extra_lines,
)
await emit_planner_finalized(
session_id=self._runtime.session_id,
@@ -1125,6 +1173,7 @@ class MaisakaReasoningEngine:
invocation: ToolInvocation,
result: ToolExecutionResult,
duration_ms: float,
tool_spec: Optional[ToolSpec] = None,
) -> dict[str, Any]:
"""构建 planner.finalized 中单个工具的监控结果。"""
@@ -1133,9 +1182,20 @@ class MaisakaReasoningEngine:
if monitor_detail is not None:
normalized_detail = self._normalize_tool_record_value(monitor_detail)
monitor_card = result.metadata.get("monitor_card")
normalized_card = None
if monitor_card is not None:
normalized_card = self._normalize_tool_record_value(monitor_card)
monitor_sub_cards = result.metadata.get("monitor_sub_cards")
normalized_sub_cards = None
if monitor_sub_cards is not None:
normalized_sub_cards = self._normalize_tool_record_value(monitor_sub_cards)
return {
"tool_call_id": tool_call.call_id,
"tool_name": tool_call.func_name,
"tool_title": tool_spec.title.strip() if tool_spec is not None and tool_spec.title.strip() else "",
"tool_args": self._normalize_tool_record_value(
invocation.arguments if isinstance(invocation.arguments, dict) else {}
),
@@ -1143,6 +1203,8 @@ class MaisakaReasoningEngine:
"duration_ms": round(duration_ms, 2),
"summary": self._build_tool_result_summary(tool_call, result),
"detail": normalized_detail,
"card": normalized_card,
"sub_cards": normalized_sub_cards,
}
async def _handle_tool_calls(
@@ -1178,7 +1240,7 @@ class MaisakaReasoningEngine:
self._append_tool_execution_result(tool_call, result)
tool_result_summaries.append(self._build_tool_result_summary(tool_call, result))
tool_monitor_results.append(
self._build_tool_monitor_result(tool_call, invocation, result, duration_ms=0.0)
self._build_tool_monitor_result(tool_call, invocation, result, duration_ms=0.0, tool_spec=None)
)
return False, tool_result_summaries, tool_monitor_results
@@ -1210,7 +1272,13 @@ class MaisakaReasoningEngine:
self._append_tool_execution_result(tool_call, result)
tool_result_summaries.append(self._build_tool_result_summary(tool_call, result))
tool_monitor_results.append(
self._build_tool_monitor_result(tool_call, invocation, result, tool_duration_ms)
self._build_tool_monitor_result(
tool_call,
invocation,
result,
tool_duration_ms,
tool_spec=tool_spec_map.get(invocation.tool_name),
)
)
if not result.success and tool_call.func_name == "reply":

View File

@@ -479,15 +479,23 @@ class MaisakaHeartFlowChatting:
def build_deferred_tools_reminder(self) -> str:
"""构造供 planner 使用的 deferred tools 提示消息。"""
undiscovered_tool_names = [
tool_name
for tool_name in self.deferred_tool_specs_by_name
undiscovered_tool_specs = [
tool_spec
for tool_name, tool_spec in self.deferred_tool_specs_by_name.items()
if tool_name not in self.discovered_tool_names
]
if not undiscovered_tool_names:
if not undiscovered_tool_specs:
return ""
tool_lines = [f"{index}. {tool_name}" for index, tool_name in enumerate(undiscovered_tool_names, start=1)]
tool_lines: list[str] = []
for index, tool_spec in enumerate(undiscovered_tool_specs, start=1):
tool_name = tool_spec.name.strip()
tool_description = tool_spec.brief_description.strip()
if tool_description:
tool_lines.append(f"{index}. {tool_name}: {tool_description}")
else:
tool_lines.append(f"{index}. {tool_name}")
reminder_lines = [
"<system-reminder>",
"以下工具当前未直接暴露给你,但可以通过 tool_search 工具发现并在后续轮次中使用:",
@@ -803,6 +811,7 @@ class MaisakaHeartFlowChatting:
self,
*,
cycle_id: Optional[int] = None,
time_records: Optional[dict[str, float]] = None,
timing_selected_history_count: Optional[int] = None,
timing_prompt_tokens: Optional[int] = None,
timing_action: str = "",
@@ -818,6 +827,7 @@ class MaisakaHeartFlowChatting:
planner_tool_results: Optional[list[str]] = None,
planner_tool_detail_results: Optional[list[dict[str, Any]]] = None,
planner_prompt_section: Optional[RenderableType] = None,
planner_extra_lines: Optional[list[str]] = None,
) -> None:
"""在终端展示当前聊天流本轮 cycle 的最终结果。"""
if not global_config.debug.show_maisaka_thinking:
@@ -830,6 +840,7 @@ class MaisakaHeartFlowChatting:
if cycle_id is not None:
body_lines.append(f"循环编号:{cycle_id}")
panel_subtitle = self._build_cycle_time_records_text(time_records or {})
renderables: list[RenderableType] = [Text("\n".join(body_lines))]
timing_panel = self._build_cycle_stage_panel(
title="Timing Gate",
@@ -837,33 +848,49 @@ class MaisakaHeartFlowChatting:
selected_history_count=timing_selected_history_count,
prompt_tokens=timing_prompt_tokens,
response_text=timing_response,
tool_calls=timing_tool_calls,
tool_results=timing_tool_results,
tool_detail_results=timing_tool_detail_results,
prompt_section=timing_prompt_section,
extra_lines=[f"门控动作:{timing_action}"] if timing_action.strip() else None,
)
if timing_panel is not None:
renderables.append(timing_panel)
timing_tool_cards = self._build_tool_activity_cards(
stage_title="Timing Tool",
tool_calls=timing_tool_calls,
tool_results=timing_tool_results,
tool_detail_results=timing_tool_detail_results,
planner_style=False,
)
if timing_tool_cards:
renderables.extend(timing_tool_cards)
planner_panel = self._build_cycle_stage_panel(
title="Planner",
border_style="green",
selected_history_count=planner_selected_history_count,
prompt_tokens=planner_prompt_tokens,
response_text=planner_response,
tool_calls=planner_tool_calls,
tool_results=planner_tool_results,
tool_detail_results=planner_tool_detail_results,
prompt_section=planner_prompt_section,
extra_lines=planner_extra_lines,
)
if planner_panel is not None:
renderables.append(planner_panel)
planner_tool_cards = self._build_tool_activity_cards(
stage_title="Planner Tool",
tool_calls=planner_tool_calls,
tool_results=planner_tool_results,
tool_detail_results=planner_tool_detail_results,
planner_style=True,
)
if planner_tool_cards:
renderables.extend(planner_tool_cards)
console.print(
Panel(
Group(*renderables),
title="MaiSaka 循环",
subtitle=panel_subtitle,
border_style="bright_blue",
padding=(0, 1),
)
@@ -877,9 +904,6 @@ class MaisakaHeartFlowChatting:
selected_history_count: Optional[int],
prompt_tokens: Optional[int],
response_text: str = "",
tool_calls: Optional[list[Any]] = None,
tool_results: Optional[list[str]] = None,
tool_detail_results: Optional[list[dict[str, Any]]] = None,
prompt_section: Optional[RenderableType] = None,
extra_lines: Optional[list[str]] = None,
) -> Optional[Panel]:
@@ -889,9 +913,6 @@ class MaisakaHeartFlowChatting:
selected_history_count is not None,
prompt_tokens is not None,
bool(response_text.strip()),
bool(tool_calls),
bool(tool_results),
bool(tool_detail_results),
prompt_section is not None,
bool(extra_lines),
])
@@ -918,40 +939,11 @@ class MaisakaHeartFlowChatting:
Panel(
Text(normalized_response),
title="Maisaka 返回",
border_style="green",
border_style=border_style,
padding=(0, 1),
)
)
normalized_tool_calls = build_tool_call_summary_lines(tool_calls or [])
if normalized_tool_calls:
renderables.append(
Panel(
Text("\n".join(normalized_tool_calls)),
title="工具调用",
border_style="magenta",
padding=(0, 1),
)
)
normalized_tool_results = self._filter_redundant_tool_results(
tool_results=tool_results or [],
tool_detail_results=tool_detail_results or [],
)
if normalized_tool_results:
renderables.append(
Panel(
Text("\n".join(normalized_tool_results)),
title="工具结果",
border_style="yellow",
padding=(0, 1),
)
)
detail_panels = self._build_tool_detail_panels(tool_detail_results or [])
if detail_panels:
renderables.extend(detail_panels)
return Panel(
Group(*renderables),
title=title,
@@ -959,6 +951,75 @@ class MaisakaHeartFlowChatting:
padding=(0, 1),
)
def _build_tool_activity_cards(
self,
*,
stage_title: str,
tool_calls: Optional[list[Any]] = None,
tool_results: Optional[list[str]] = None,
tool_detail_results: Optional[list[dict[str, Any]]] = None,
planner_style: bool = False,
) -> list[RenderableType]:
"""构建与阶段同级的工具执行卡片列表。"""
detail_results = tool_detail_results or []
cards = self._build_tool_detail_cards(
detail_results,
stage_title=stage_title,
planner_style=planner_style,
)
if cards:
return cards
# 兼容旧数据结构:若尚无 detail则降级为简单文本卡片。
fallback_lines = self._filter_redundant_tool_results(
tool_results=tool_results or [],
tool_detail_results=detail_results,
)
if not fallback_lines and tool_calls:
fallback_lines = build_tool_call_summary_lines(tool_calls)
if not fallback_lines:
return []
fallback_border_style = "blue" if planner_style else "magenta"
return [
Panel(
Text("\n".join(fallback_lines)),
title=stage_title,
border_style=fallback_border_style,
padding=(0, 1),
)
]
@staticmethod
def _build_cycle_time_records_text(time_records: dict[str, float]) -> str:
"""构建循环最外层面板展示的阶段耗时文本。"""
if not time_records:
return "流程耗时:无"
label_map = {
"timing_gate": "Timing Gate",
"planner": "Planner",
"tool_calls": "工具执行",
}
ordered_keys = ["timing_gate", "planner", "tool_calls"]
parts: list[str] = []
for key in ordered_keys:
duration = time_records.get(key)
if isinstance(duration, (int, float)):
parts.append(f"{label_map.get(key, key)} {float(duration):.2f} s")
for key, duration in time_records.items():
if key in ordered_keys or not isinstance(duration, (int, float)):
continue
parts.append(f"{label_map.get(key, key)} {float(duration):.2f} s")
if not parts:
return "流程耗时:无"
return "流程耗时:" + " | ".join(parts)
@staticmethod
def _filter_redundant_tool_results(
*,
@@ -1052,6 +1113,7 @@ class MaisakaHeartFlowChatting:
prompt_text: str,
request_messages: Optional[list[Any]] = None,
tool_call_id: str,
border_style: str = "bright_yellow",
) -> Panel:
"""将工具 prompt 渲染为可点击查看的预览入口。"""
@@ -1076,7 +1138,7 @@ class MaisakaHeartFlowChatting:
image_display_mode="path_link" if global_config.maisaka.show_image_path else "legacy",
),
title=labels["prompt_title"],
border_style="bright_yellow",
border_style=border_style,
padding=(0, 1),
)
@@ -1089,117 +1151,235 @@ class MaisakaHeartFlowChatting:
subtitle=subtitle,
),
title=labels["prompt_title"],
border_style="bright_yellow",
border_style=border_style,
padding=(0, 1),
)
def _build_tool_detail_panels(self, tool_detail_results: list[dict[str, Any]]) -> list[RenderableType]:
""" tool monitor detail 渲染为 CLI 详情卡片"""
def _normalize_tool_card_body_lines(self, body: Any) -> list[str]:
"""工具卡片正文规范化为行列表"""
if isinstance(body, str):
return [line for line in body.splitlines() if line.strip()]
if isinstance(body, list):
return [
str(item).strip()
for item in body
if str(item).strip()
]
return []
def _build_custom_tool_sub_cards(
self,
sub_cards: Any,
*,
default_border_style: str,
) -> list[RenderableType]:
"""构建工具自定义子卡片。"""
if not isinstance(sub_cards, list):
return []
renderables: list[RenderableType] = []
for sub_card in sub_cards:
if not isinstance(sub_card, dict):
continue
title = str(sub_card.get("title") or "").strip() or "附加信息"
border_style = str(sub_card.get("border_style") or "").strip() or default_border_style
body_lines = self._normalize_tool_card_body_lines(
sub_card.get("body_lines", sub_card.get("content", ""))
)
if not body_lines:
continue
renderables.append(
Panel(
Text("\n".join(body_lines)),
title=title,
border_style=border_style,
padding=(0, 1),
)
)
return renderables
def _build_default_tool_detail_parts(
self,
*,
tool_name: str,
tool_call_id: str,
tool_args: Any,
summary: str,
duration_ms: Any,
detail: dict[str, Any],
planner_style: bool,
) -> list[RenderableType]:
"""构建工具卡片默认内容块。"""
argument_border_style = "blue" if planner_style else "cyan"
metrics_border_style = "bright_blue" if planner_style else "bright_cyan"
prompt_border_style = "bright_blue" if planner_style else "bright_yellow"
reasoning_border_style = "blue" if planner_style else "magenta"
output_border_style = "bright_blue" if planner_style else "green"
extra_info_border_style = "cyan" if planner_style else "white"
detail_labels = self._get_tool_detail_labels(tool_name)
parts: list[RenderableType] = []
header_lines: list[str] = []
if summary:
header_lines.append(summary)
if tool_call_id:
header_lines.append(f"调用ID{tool_call_id}")
if isinstance(duration_ms, (int, float)):
header_lines.append(f"执行耗时:{round(float(duration_ms), 2)} ms")
if header_lines:
parts.append(Text("\n".join(header_lines)))
if isinstance(tool_args, dict) and tool_args:
parts.append(
Panel(
Pretty(tool_args, expand_all=True),
title="工具参数",
border_style=argument_border_style,
padding=(0, 1),
)
)
metrics = detail.get("metrics")
if isinstance(metrics, dict):
metrics_text = self._build_tool_metrics_text(metrics)
if metrics_text:
parts.append(
Panel(
Text(metrics_text),
title="执行指标",
border_style=metrics_border_style,
padding=(0, 1),
)
)
prompt_text = str(detail.get("prompt_text") or "").strip()
if prompt_text:
parts.append(
self._build_tool_prompt_access_panel(
tool_name=tool_name,
prompt_text=prompt_text,
request_messages=detail.get("request_messages") if isinstance(detail.get("request_messages"), list) else None,
tool_call_id=tool_call_id,
border_style=prompt_border_style,
)
)
reasoning_text = str(detail.get("reasoning_text") or "").strip()
if reasoning_text:
parts.append(
Panel(
Text(reasoning_text),
title=detail_labels["reasoning_title"],
border_style=reasoning_border_style,
padding=(0, 1),
)
)
output_text = str(detail.get("output_text") or "").strip()
if output_text:
parts.append(
Panel(
Text(output_text),
title=detail_labels["output_title"],
border_style=output_border_style,
padding=(0, 1),
)
)
extra_sections = detail.get("extra_sections")
if isinstance(extra_sections, list):
for section in extra_sections:
if not isinstance(section, dict):
continue
section_title = str(section.get("title") or "").strip() or "附加信息"
section_content = str(section.get("content") or "").strip()
if not section_content:
continue
parts.append(
Panel(
Text(section_content),
title=section_title,
border_style=extra_info_border_style,
padding=(0, 1),
)
)
return parts
def _build_tool_detail_cards(
self,
tool_detail_results: list[dict[str, Any]],
*,
stage_title: str,
planner_style: bool = False,
) -> list[RenderableType]:
"""将 tool monitor detail 渲染为与 Planner/Timing 平级的工具卡片。"""
detail_panel_border_style = "blue" if planner_style else "yellow"
sub_card_border_style = "cyan" if planner_style else "white"
panels: list[RenderableType] = []
for tool_result in tool_detail_results:
detail = tool_result.get("detail")
if not isinstance(detail, dict) or not detail:
continue
detail_dict = detail if isinstance(detail, dict) else {}
tool_name = str(tool_result.get("tool_name") or "unknown").strip() or "unknown"
detail_labels = self._get_tool_detail_labels(tool_name)
tool_title = str(tool_result.get("tool_title") or "").strip() or tool_name
tool_call_id = str(tool_result.get("tool_call_id") or "").strip()
tool_args = tool_result.get("tool_args")
summary = str(tool_result.get("summary") or "").strip()
duration_ms = tool_result.get("duration_ms")
custom_card = tool_result.get("card")
parts: list[RenderableType] = []
header_lines: list[str] = []
if summary:
header_lines.append(summary)
if tool_call_id:
header_lines.append(f"调用ID{tool_call_id}")
if isinstance(duration_ms, (int, float)):
header_lines.append(f"执行耗时:{round(float(duration_ms), 2)} ms")
if header_lines:
parts.append(Text("\n".join(header_lines)))
if isinstance(tool_args, dict) and tool_args:
parts.append(
Panel(
Pretty(tool_args, expand_all=True),
title="工具参数",
border_style="cyan",
padding=(0, 1),
)
custom_title = ""
card_border_style = detail_panel_border_style
replace_default_children = False
if isinstance(custom_card, dict):
custom_title = str(custom_card.get("title") or "").strip()
card_border_style = str(custom_card.get("border_style") or "").strip() or detail_panel_border_style
replace_default_children = bool(custom_card.get("replace_default_children", False))
custom_body_lines = self._normalize_tool_card_body_lines(
custom_card.get("body_lines", custom_card.get("content", ""))
)
if custom_body_lines:
parts.append(Text("\n".join(custom_body_lines)))
metrics = detail.get("metrics")
if isinstance(metrics, dict):
metrics_text = self._build_tool_metrics_text(metrics)
if metrics_text:
parts.append(
Panel(
Text(metrics_text),
title="执行指标",
border_style="bright_cyan",
padding=(0, 1),
)
)
prompt_text = str(detail.get("prompt_text") or "").strip()
if prompt_text:
parts.append(
self._build_tool_prompt_access_panel(
if not replace_default_children:
parts.extend(
self._build_default_tool_detail_parts(
tool_name=tool_name,
prompt_text=prompt_text,
request_messages=detail.get("request_messages") if isinstance(detail.get("request_messages"), list) else None,
tool_call_id=tool_call_id,
tool_args=tool_args,
summary=summary,
duration_ms=duration_ms,
detail=detail_dict,
planner_style=planner_style,
)
)
reasoning_text = str(detail.get("reasoning_text") or "").strip()
if reasoning_text:
parts.append(
Panel(
Text(reasoning_text),
title=detail_labels["reasoning_title"],
border_style="magenta",
padding=(0, 1),
if isinstance(custom_card, dict):
parts.extend(
self._build_custom_tool_sub_cards(
custom_card.get("sub_cards"),
default_border_style=sub_card_border_style,
)
)
output_text = str(detail.get("output_text") or "").strip()
if output_text:
parts.append(
Panel(
Text(output_text),
title=detail_labels["output_title"],
border_style="green",
padding=(0, 1),
)
parts.extend(
self._build_custom_tool_sub_cards(
tool_result.get("sub_cards"),
default_border_style=sub_card_border_style,
)
extra_sections = detail.get("extra_sections")
if isinstance(extra_sections, list):
for section in extra_sections:
if not isinstance(section, dict):
continue
section_title = str(section.get("title") or "").strip() or "附加信息"
section_content = str(section.get("content") or "").strip()
if not section_content:
continue
parts.append(
Panel(
Text(section_content),
title=section_title,
border_style="white",
padding=(0, 1),
)
)
)
if parts:
panels.append(
Panel(
Group(*parts),
title=f"{tool_name} 工具详情",
border_style="yellow",
title=custom_title or f"{stage_title} · {tool_title}",
border_style=card_border_style,
padding=(0, 1),
)
)