177 lines
6.0 KiB
Python
177 lines
6.0 KiB
Python
"""Tools 10, 11: list_flows, get_flow."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from ..flows import get_flow_by_id, get_flows
|
|
from ..hints import generate_hints, get_session
|
|
from ._common import _get_store
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tool 10: list_flows [EXPLORE]
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def list_flows(
|
|
repo_root: str | None = None,
|
|
sort_by: str = "criticality",
|
|
limit: int = 50,
|
|
kind: str | None = None,
|
|
detail_level: str = "standard",
|
|
) -> dict[str, Any]:
|
|
"""List execution flows in the codebase, sorted by criticality.
|
|
|
|
[EXPLORE] Retrieves stored execution flows from the knowledge graph.
|
|
Each flow represents a call chain starting from an entry point
|
|
(e.g. HTTP handler, CLI command, test function).
|
|
|
|
Args:
|
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
sort_by: Sort column: criticality, depth, node_count, file_count,
|
|
or name.
|
|
limit: Maximum flows to return (default: 50).
|
|
kind: Optional filter by entry point kind (e.g. "Test", "Function").
|
|
detail_level: "standard" (default) returns full flow data;
|
|
"minimal" returns only name, criticality, and
|
|
node_count per flow.
|
|
|
|
Returns:
|
|
List of flows with criticality scores.
|
|
"""
|
|
store, root = _get_store(repo_root)
|
|
try:
|
|
fetch_limit = (
|
|
limit if not kind else limit * 10
|
|
) # fetch more when filtering
|
|
flows = get_flows(store, sort_by=sort_by, limit=fetch_limit)
|
|
|
|
if kind:
|
|
filtered = []
|
|
for f in flows:
|
|
ep_id = f.get("entry_point_id")
|
|
if ep_id is not None:
|
|
node_kind = store.get_node_kind_by_id(ep_id)
|
|
if node_kind == kind:
|
|
filtered.append(f)
|
|
flows = filtered[:limit]
|
|
|
|
if detail_level == "minimal":
|
|
flows = [
|
|
{
|
|
"name": f["name"],
|
|
"criticality": f["criticality"],
|
|
"node_count": f["node_count"],
|
|
}
|
|
for f in flows
|
|
]
|
|
|
|
result: dict[str, object] = {
|
|
"status": "ok",
|
|
"summary": f"Found {len(flows)} execution flow(s)",
|
|
"flows": flows,
|
|
}
|
|
result["_hints"] = generate_hints(
|
|
"list_flows", result, get_session()
|
|
)
|
|
return result
|
|
except Exception as exc:
|
|
return {"status": "error", "error": str(exc)}
|
|
finally:
|
|
store.close()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tool 11: get_flow [EXPLORE]
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def get_flow(
|
|
flow_id: int | None = None,
|
|
flow_name: str | None = None,
|
|
include_source: bool = False,
|
|
repo_root: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Get details of a single execution flow.
|
|
|
|
[EXPLORE] Retrieves full path details for a flow, including each step's
|
|
function name, file, and line numbers. Optionally includes source
|
|
snippets for every step in the path.
|
|
|
|
Args:
|
|
flow_id: Database ID of the flow (from list_flows).
|
|
flow_name: Name to search for (partial match). Ignored if flow_id
|
|
given.
|
|
include_source: If True, include source code snippets for each step.
|
|
repo_root: Repository root path. Auto-detected if omitted.
|
|
|
|
Returns:
|
|
Flow details with steps, or not_found status.
|
|
"""
|
|
store, root = _get_store(repo_root)
|
|
try:
|
|
flow: dict | None = None
|
|
|
|
if flow_id is not None:
|
|
flow = get_flow_by_id(store, flow_id)
|
|
elif flow_name is not None:
|
|
# Search flows by name match
|
|
all_flows = get_flows(
|
|
store, sort_by="criticality", limit=500
|
|
)
|
|
for f in all_flows:
|
|
if flow_name.lower() in f["name"].lower():
|
|
flow = get_flow_by_id(store, f["id"])
|
|
break
|
|
|
|
if flow is None:
|
|
return {
|
|
"status": "not_found",
|
|
"summary": "No flow found matching the given criteria.",
|
|
}
|
|
|
|
# Optionally include source snippets for each step
|
|
if include_source and "steps" in flow:
|
|
for step in flow["steps"]:
|
|
fp = Path(step["file"]) if step.get("file") else None
|
|
if fp is not None and not fp.is_absolute():
|
|
fp = root / fp
|
|
file_path = fp
|
|
if file_path and file_path.is_file():
|
|
try:
|
|
lines = file_path.read_text(
|
|
errors="replace"
|
|
).splitlines()
|
|
start = max(
|
|
0, (step.get("line_start") or 1) - 1
|
|
)
|
|
end = min(
|
|
len(lines),
|
|
step.get("line_end") or len(lines),
|
|
)
|
|
step["source"] = "\n".join(
|
|
f"{i + 1}: {lines[i]}"
|
|
for i in range(start, end)
|
|
)
|
|
except (OSError, UnicodeDecodeError):
|
|
step["source"] = "(could not read file)"
|
|
|
|
result = {
|
|
"status": "ok",
|
|
"summary": (
|
|
f"Flow '{flow['name']}': {flow['node_count']} nodes, "
|
|
f"depth {flow['depth']}, "
|
|
f"criticality {flow['criticality']:.4f}"
|
|
),
|
|
"flow": flow,
|
|
}
|
|
result["_hints"] = generate_hints(
|
|
"get_flow", result, get_session()
|
|
)
|
|
return result
|
|
except Exception as exc:
|
|
return {"status": "error", "error": str(exc)}
|
|
finally:
|
|
store.close()
|