mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 03:09:50 +08:00
144 lines
4.6 KiB
Python
Executable File
144 lines
4.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Render the declared action tree as a box-drawing summary.md.
|
|
|
|
Called by finalize.py (cold path) and cache_check.py (hit path when the
|
|
cached summary is missing). Mirrors the old agent Phase 7 render_tree()
|
|
logic verbatim.
|
|
|
|
Usage:
|
|
python3 summarize_tree.py <tree_json_path> <out_md_path> <built_at_utc>
|
|
|
|
Inputs:
|
|
argv[1] absolute path to declared_action_tree.json (or similar)
|
|
argv[2] output .summary.md path (overwritten atomically)
|
|
argv[3] ISO-8601 UTC built-at timestamp (e.g. 2026-04-25T14:00:00Z)
|
|
|
|
Outputs:
|
|
argv[2] rendered markdown
|
|
exit 0 success
|
|
exit 1 missing args, read failure, write failure
|
|
"""
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import sys
|
|
|
|
KC_ORDER = [
|
|
"BOT_DEFINITION", "TOPIC", "GEN_AI_FUNCTION",
|
|
"FLOW", "APEX", "PROMPT_TEMPLATE",
|
|
"STANDARD_ACTION", "UNKNOWN",
|
|
]
|
|
|
|
|
|
def render_tree(node, prefix="", is_last=True, lines=None, agent_=None):
|
|
if lines is None:
|
|
lines = []
|
|
connector = "└── " if is_last else "├── "
|
|
kind = node.get("kind", "?")
|
|
api = node.get("api_name", "?")
|
|
elem = node.get("element_name")
|
|
if kind == "BOT_DEFINITION":
|
|
v = (agent_ or {}).get("version") or ""
|
|
label = f"[{kind}] {api}" + (f" ({v})" if v else "")
|
|
elif kind == "GEN_AI_FUNCTION":
|
|
unwraps = node.get("unwraps_to") or {}
|
|
uk = unwraps.get("kind")
|
|
un = unwraps.get("api_name")
|
|
arrow = f" → {uk}:{un}" if uk and un else ""
|
|
label = f"[{kind}] {api}{arrow}"
|
|
elif kind in ("STANDARD_ACTION", "UNKNOWN"):
|
|
# Canonical key is `invocation_type` (schema 3.1+); fall back to
|
|
# the two legacy keys so caches built by an older parse_wave still
|
|
# render the qualifier.
|
|
raw = (
|
|
node.get("invocation_type")
|
|
or node.get("raw_invocation_type")
|
|
or node.get("raw_action_type")
|
|
or ""
|
|
)
|
|
label = f"[{kind}] {api}" + (f" ({raw})" if raw else "")
|
|
else:
|
|
label = f"[{kind}] {api}"
|
|
if elem and kind != "BOT_DEFINITION":
|
|
label += f" — element:{elem}"
|
|
lines.append(prefix + connector + label)
|
|
children = node.get("children") or []
|
|
child_prefix = prefix + (" " if is_last else "│ ")
|
|
for i, c in enumerate(children):
|
|
render_tree(c, child_prefix, i == len(children) - 1, lines, agent_)
|
|
return lines
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) != 4:
|
|
sys.stderr.write(
|
|
"summarize_tree.py: usage: python3 summarize_tree.py <tree_json_path> "
|
|
"<out_md_path> <built_at_utc>\n"
|
|
)
|
|
return 1
|
|
|
|
tree_path = pathlib.Path(sys.argv[1])
|
|
out_path = pathlib.Path(sys.argv[2])
|
|
built_at = sys.argv[3]
|
|
|
|
try:
|
|
tree = json.loads(tree_path.read_text())
|
|
except (OSError, json.JSONDecodeError) as e:
|
|
sys.stderr.write(f"summarize_tree.py: cannot read {tree_path}: {e}\n")
|
|
return 1
|
|
|
|
a = tree.get("agent", {}) or {}
|
|
kc = tree.get("_kind_counts", {}) or {}
|
|
|
|
lines = [
|
|
f"# {a.get('api_name', '?')} {a.get('version', '?')} — declared action tree",
|
|
"",
|
|
f"- **Bot ID:** `{a.get('bot_id', '?')}`",
|
|
f"- **Master label:** {a.get('master_label', '?')}",
|
|
f"- **Generation:** {a.get('generation', 'unknown')}",
|
|
f"- **Planner:** `{a.get('planner_name', '?')}` ({a.get('planner_type', '?')})",
|
|
f"- **Version auto-picked:** {a.get('_version_auto_picked', False)}",
|
|
f"- **Built at (UTC):** {built_at}",
|
|
f"- **Node count:** {tree.get('node_count', 0)}",
|
|
f"- **Depth:** {tree.get('depth', 0)}",
|
|
f"- **Partial:** {tree.get('_partial', False)}",
|
|
"",
|
|
"## Kind counts",
|
|
"",
|
|
]
|
|
for k in KC_ORDER:
|
|
lines.append(f"- `{k}`: {kc.get(k, 0)}")
|
|
|
|
lines += [
|
|
"",
|
|
"## Declared action tree",
|
|
"",
|
|
"```",
|
|
]
|
|
root = tree.get("root") or {"kind": "BOT_DEFINITION", "api_name": a.get("api_name", "?"), "children": []}
|
|
lines.extend(render_tree(root, "", True, None, a))
|
|
lines.append("```")
|
|
|
|
if tree.get("_unresolved"):
|
|
lines += ["", "## Unresolved", ""]
|
|
for u in tree["_unresolved"]:
|
|
lines.append(
|
|
f"- `{u.get('kind')}`/`{u.get('api_name')}` — {u.get('reason')}"
|
|
)
|
|
|
|
content = "\n".join(lines) + "\n"
|
|
|
|
try:
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = out_path.with_suffix(out_path.suffix + ".tmp")
|
|
tmp.write_text(content)
|
|
os.replace(tmp, out_path)
|
|
except OSError as e:
|
|
sys.stderr.write(f"summarize_tree.py: write failed: {e}\n")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|