|
|
@@ -2,7 +2,7 @@
|
|
|
"""将 run_log 文本渲染为可折叠 HTML 页面。
|
|
|
|
|
|
直接在脚本内修改 INPUT_LOG_PATH / OUTPUT_HTML_PATH 后运行:
|
|
|
- python examples/piaoquan_needs/render_log_html.py
|
|
|
+ python examples/piaoquan_demand/render_log_html.py
|
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
@@ -56,13 +56,13 @@ DEFAULT_COLLAPSE_KEYWORDS = ["调用参数", "返回内容"]
|
|
|
TOOL_DESCRIPTION_MAP: dict[str, str] = {
|
|
|
"think_and_plan": "系统化记录思考、计划与下一步行动,不查询数据也不修改数据。",
|
|
|
"get_weight_score_topn": "按层级和维度查询权重分 TopN,快速定位高权重元素或分类。",
|
|
|
- "get_weight_score_by_name": "按名称精确查询指定元素或分类的权重分,返回匹配明细。",
|
|
|
+ "get_weight_score_by_name": "按名称列表批量查询元素或分类的权重分,每项返回匹配明细。",
|
|
|
"get_category_tree": "获取分类树快照,快速查看实质/形式/意图三维结构全貌。",
|
|
|
"get_frequent_itemsets": "查询高频共现的分类组合,按维度模式和深度分组返回。",
|
|
|
"get_itemset_detail": "获取频繁项集完整详情,包括项集结构和匹配帖子列表。",
|
|
|
"get_post_elements": "按帖子查看结构化元素内容,支持点类型及三维元素下钻。",
|
|
|
"search_elements": "按关键词搜索元素,返回分类归属、出现频次与帖子覆盖。",
|
|
|
- "get_element_category_chain": "从元素名称反查所属分类链,查看从根到叶的路径。",
|
|
|
+ "get_element_category_chain": "从元素名称列表批量反查所属分类链,每项查看从根到叶的路径。",
|
|
|
"get_category_detail": "查看分类节点上下文,含祖先、子节点、兄弟节点与元素。",
|
|
|
"search_categories": "按关键词搜索分类节点,返回分类 ID 与路径等导航信息。",
|
|
|
"get_category_elements": "获取指定分类下的元素列表及出现统计,便于落地选题。",
|
|
|
@@ -73,7 +73,7 @@ TOOL_DESCRIPTION_MAP: dict[str, str] = {
|
|
|
# =========================
|
|
|
# 运行配置(直接改变量即可)
|
|
|
# =========================
|
|
|
-INPUT_LOG_PATH = "examples/piaoquan_needs/output/run_log_17_20260324_204533.txt"
|
|
|
+INPUT_LOG_PATH = "examples/piaoquan_demand/output/element/run_log_17_20260326_141309.txt"
|
|
|
# 设为 None 则默认生成到输入文件同名 .html
|
|
|
OUTPUT_HTML_PATH: str | None = None
|
|
|
# 是否默认折叠所有 [FOLD] 块
|
|
|
@@ -81,6 +81,14 @@ COLLAPSE_ALL_FOLDS = False
|
|
|
# 命中这些前缀/关键词的折叠块默认收起
|
|
|
COLLAPSE_PREFIXES = DEFAULT_COLLAPSE_PREFIXES
|
|
|
COLLAPSE_KEYWORDS = DEFAULT_COLLAPSE_KEYWORDS
|
|
|
+# 仅针对特定结构做“折叠内容复制”
|
|
|
+# 规则(HTML 渲染层面):
|
|
|
+# - 只对 `FOLD:🔧 think_and_plan` 进行处理
|
|
|
+# - 只复制其内部 `FOLD:📤 返回内容` 包裹的“内容文本”
|
|
|
+# - 将复制内容直接插入到 `think_and_plan` 对应的 `<details>` 之外,
|
|
|
+# 从而无需点开工具调用就能在最外层看到
|
|
|
+THINK_AND_PLAN_TOOL_TITLE = "🔧 think_and_plan"
|
|
|
+RETURN_CONTENT_FOLD_TITLE = "📤 返回内容"
|
|
|
|
|
|
|
|
|
def resolve_config_path(path_str: str) -> Path:
|
|
|
@@ -147,6 +155,18 @@ def render_text_block(lines: list[str]) -> str:
|
|
|
return f'<pre class="log-text">{escaped}</pre>'
|
|
|
|
|
|
|
|
|
+def flatten_entries_to_lines(entries: list[str | Node]) -> list[str]:
|
|
|
+ """把节点树(可能包含 fold)拍平成纯文本行,用于“无需点开折叠块”展示。"""
|
|
|
+ out: list[str] = []
|
|
|
+ for entry in entries:
|
|
|
+ if isinstance(entry, str):
|
|
|
+ out.append(entry)
|
|
|
+ else:
|
|
|
+ # 只取其内部文本,忽略 fold 标题本身
|
|
|
+ out.extend(flatten_entries_to_lines(entry.entries))
|
|
|
+ return out
|
|
|
+
|
|
|
+
|
|
|
def enrich_fold_title(title: str) -> str:
|
|
|
"""为工具调用标题附加工具功能描述。"""
|
|
|
tool_prefix = "🔧 "
|
|
|
@@ -165,7 +185,115 @@ def render_node(
|
|
|
collapse_prefixes: list[str],
|
|
|
collapse_keywords: list[str],
|
|
|
collapse_all: bool,
|
|
|
+ # 预留参数:历史上用于判断折叠上下文;当前逻辑已不需要该状态。
|
|
|
+ in_think_and_plan: bool = False,
|
|
|
+ # 是否在渲染 `🔧 think_and_plan` 时,把其 `📤 返回内容` 复制为“details 外的纯文本”。
|
|
|
+ # root 的“工具调用收集块”会负责把复制文本放到块外,因此在该场景下需要关闭。
|
|
|
+ emit_think_plan_return_copy: bool = True,
|
|
|
) -> str:
|
|
|
+ # root(最外层)只做一件事:
|
|
|
+ # 把“连续出现的工具调用”打包到一个大折叠块里(文本之间的工具调用收集在一起)。
|
|
|
+ if node.title is None:
|
|
|
+ parts: list[str] = []
|
|
|
+ text_buffer: list[str] = []
|
|
|
+ tool_group_parts: list[str] = []
|
|
|
+
|
|
|
+ def flush_text_buffer() -> None:
|
|
|
+ if text_buffer:
|
|
|
+ parts.append(render_text_block(text_buffer))
|
|
|
+ text_buffer.clear()
|
|
|
+
|
|
|
+ def flush_tool_group() -> None:
|
|
|
+ if tool_group_parts:
|
|
|
+ parts.append(
|
|
|
+ '<details class="fold tool-fold tool-call-group">'
|
|
|
+ '<summary>工具调用</summary>'
|
|
|
+ + "".join(tool_group_parts)
|
|
|
+ + "</details>"
|
|
|
+ )
|
|
|
+ tool_group_parts.clear()
|
|
|
+
|
|
|
+ def render_fold_details(fold_node: Node) -> str:
|
|
|
+ """把一个 fold 节点整体包装为 <details>(不仅渲染它的子内容)。"""
|
|
|
+ title = fold_node.title or ""
|
|
|
+ is_collapsed = should_collapse(
|
|
|
+ title=title,
|
|
|
+ collapse_prefixes=collapse_prefixes,
|
|
|
+ collapse_keywords=collapse_keywords,
|
|
|
+ collapse_all=collapse_all,
|
|
|
+ )
|
|
|
+ folded_class = "fold tool-fold" if is_collapsed else "fold normal-fold"
|
|
|
+ open_attr = "" if is_collapsed else " open"
|
|
|
+ display_title = enrich_fold_title(title)
|
|
|
+ inner = render_node(
|
|
|
+ fold_node,
|
|
|
+ collapse_prefixes=collapse_prefixes,
|
|
|
+ collapse_keywords=collapse_keywords,
|
|
|
+ collapse_all=collapse_all,
|
|
|
+ emit_think_plan_return_copy=False,
|
|
|
+ )
|
|
|
+ rendered = (
|
|
|
+ f'<details class="{folded_class}"{open_attr}>'
|
|
|
+ f"<summary>{html.escape(display_title)}</summary>"
|
|
|
+ f"{inner}"
|
|
|
+ "</details>"
|
|
|
+ )
|
|
|
+
|
|
|
+ return rendered
|
|
|
+
|
|
|
+ for entry in node.entries:
|
|
|
+ if isinstance(entry, str):
|
|
|
+ # 空白行不算“文本”,不打断连续工具调用收集。
|
|
|
+ if entry.strip() == "":
|
|
|
+ if tool_group_parts:
|
|
|
+ # 工具块之间的空白忽略
|
|
|
+ continue
|
|
|
+ text_buffer.append(entry)
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 真正的文本出现:结束当前工具调用收集,并输出文本
|
|
|
+ flush_tool_group()
|
|
|
+ text_buffer.append(entry)
|
|
|
+ continue
|
|
|
+
|
|
|
+ child = entry
|
|
|
+ child_title = child.title or ""
|
|
|
+ is_tool_call = child.is_fold and child_title.startswith("🔧 ")
|
|
|
+
|
|
|
+ if is_tool_call:
|
|
|
+ # 工具调用开始:先把前面的文本输出,再收集到工具组里
|
|
|
+ flush_text_buffer()
|
|
|
+ tool_group_parts.append(render_fold_details(child))
|
|
|
+
|
|
|
+ # 关键:`🔧 think_and_plan` 复制出来的 `📤 返回内容` 也算文本,
|
|
|
+ # 需要放在工具调用收集块之外,从而打断相邻工具调用的收集分组。
|
|
|
+ if child_title == THINK_AND_PLAN_TOOL_TITLE:
|
|
|
+ return_nodes = [
|
|
|
+ e
|
|
|
+ for e in child.entries
|
|
|
+ if isinstance(e, Node) and e.is_fold and (e.title or "") == RETURN_CONTENT_FOLD_TITLE
|
|
|
+ ]
|
|
|
+ if return_nodes:
|
|
|
+ flattened_lines = flatten_entries_to_lines(return_nodes[0].entries)
|
|
|
+ flush_tool_group()
|
|
|
+ parts.append(render_text_block(flattened_lines))
|
|
|
+ else:
|
|
|
+ # 遇到非工具调用(或非工具 fold):结束当前工具组,再按原样输出
|
|
|
+ flush_tool_group()
|
|
|
+ flush_text_buffer()
|
|
|
+ parts.append(
|
|
|
+ render_node(
|
|
|
+ child,
|
|
|
+ collapse_prefixes=collapse_prefixes,
|
|
|
+ collapse_keywords=collapse_keywords,
|
|
|
+ collapse_all=collapse_all,
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ flush_tool_group()
|
|
|
+ flush_text_buffer()
|
|
|
+ return "".join(parts)
|
|
|
+
|
|
|
parts: list[str] = []
|
|
|
text_buffer: list[str] = []
|
|
|
|
|
|
@@ -204,6 +332,19 @@ def render_node(
|
|
|
f"{inner}"
|
|
|
"</details>"
|
|
|
)
|
|
|
+ # 关键点:只把 `📤 返回内容` 复制到 `think_and_plan` 的 <details> 之外
|
|
|
+ # 但当 root 工具调用收集块要负责“details 外展示”时,需要关闭此复制。
|
|
|
+ if emit_think_plan_return_copy and title == THINK_AND_PLAN_TOOL_TITLE:
|
|
|
+ return_nodes = [
|
|
|
+ e
|
|
|
+ for e in child.entries
|
|
|
+ if isinstance(e, Node) and e.is_fold and (e.title or "") == RETURN_CONTENT_FOLD_TITLE
|
|
|
+ ]
|
|
|
+ if return_nodes:
|
|
|
+ # 一般只会有一个 `📤 返回内容`,取第一个
|
|
|
+ return_node = return_nodes[0]
|
|
|
+ flattened_lines = flatten_entries_to_lines(return_node.entries)
|
|
|
+ parts.append(render_text_block(flattened_lines))
|
|
|
|
|
|
flush_text_buffer()
|
|
|
|
|
|
@@ -315,6 +456,14 @@ def build_html(body: str, source_name: str) -> str:
|
|
|
font-size: 13px;
|
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
|
}}
|
|
|
+ .fold-duplicate {{
|
|
|
+ margin: 6px 0 12px 0;
|
|
|
+ padding-left: 10px;
|
|
|
+ border-left: 2px solid rgba(110, 168, 254, 0.6);
|
|
|
+ }}
|
|
|
+ .tool-call-group {{
|
|
|
+ margin-top: 10px;
|
|
|
+ }}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
@@ -335,6 +484,13 @@ def build_html(body: str, source_name: str) -> str:
|
|
|
document.getElementById("collapse-tools").addEventListener("click", () => {{
|
|
|
toolFolds.forEach((el) => (el.open = false));
|
|
|
}});
|
|
|
+
|
|
|
+ // 每个“工具调用”大折叠块:打开外层时,里面的子工具调用保持收起(不自动展开)
|
|
|
+ Array.from(document.querySelectorAll("details.tool-call-group")).forEach((group) => {{
|
|
|
+ group.addEventListener("toggle", () => {{
|
|
|
+ group.querySelectorAll("details.tool-fold").forEach((el) => (el.open = false));
|
|
|
+ }});
|
|
|
+ }});
|
|
|
</script>
|
|
|
</body>
|
|
|
</html>
|