feat:提高工具调用成功率,移除冗余的描述中参数介绍,增加索引列表的描述,修改prompt,移除timing的wait打断
This commit is contained in:
@@ -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},
|
||||
)
|
||||
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user