mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-25 06:55:43 +00:00
merge PR #573: cross-language edge context filters in MCP query tool
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+20
-10
@@ -1016,6 +1016,7 @@ def main() -> None:
|
||||
print(" --no-viz skip graph.html generation (useful for >5000 node graphs / CI)")
|
||||
print(" query \"<question>\" BFS traversal of graph.json for a question")
|
||||
print(" --dfs use depth-first instead of breadth-first")
|
||||
print(" --context C explicit edge-context filter (repeatable)")
|
||||
print(" --budget N cap output at N tokens (default 2000)")
|
||||
print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
|
||||
print(" save-result save a Q&A result to graphify-out/memory/ for graph feedback loop")
|
||||
@@ -1209,15 +1210,16 @@ def main() -> None:
|
||||
sys.exit(1)
|
||||
elif cmd == "query":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: graphify query \"<question>\" [--dfs] [--budget N] [--graph path]", file=sys.stderr)
|
||||
print("Usage: graphify query \"<question>\" [--dfs] [--context C] [--budget N] [--graph path]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
from graphify.serve import _score_nodes, _bfs, _dfs, _subgraph_to_text
|
||||
from graphify.serve import _query_graph_text
|
||||
from graphify.security import sanitize_label
|
||||
from networkx.readwrite import json_graph
|
||||
question = sys.argv[2]
|
||||
use_dfs = "--dfs" in sys.argv
|
||||
budget = 2000
|
||||
graph_path = "graphify-out/graph.json"
|
||||
context_filters: list[str] = []
|
||||
args = sys.argv[3:]
|
||||
i = 0
|
||||
while i < len(args):
|
||||
@@ -1235,6 +1237,12 @@ def main() -> None:
|
||||
print(f"error: --budget must be an integer", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
i += 1
|
||||
elif args[i] == "--context" and i + 1 < len(args):
|
||||
context_filters.append(args[i + 1])
|
||||
i += 2
|
||||
elif args[i].startswith("--context="):
|
||||
context_filters.append(args[i].split("=", 1)[1])
|
||||
i += 1
|
||||
elif args[i] == "--graph" and i + 1 < len(args):
|
||||
graph_path = args[i + 1]; i += 2
|
||||
else:
|
||||
@@ -1257,14 +1265,16 @@ def main() -> None:
|
||||
except Exception as exc:
|
||||
print(f"error: could not load graph: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
terms = [t.lower() for t in question.split() if len(t) > 2]
|
||||
scored = _score_nodes(G, terms)
|
||||
if not scored:
|
||||
print("No matching nodes found.")
|
||||
sys.exit(0)
|
||||
start = [nid for _, nid in scored[:5]]
|
||||
nodes, edges = (_dfs if use_dfs else _bfs)(G, start, depth=2)
|
||||
print(_subgraph_to_text(G, nodes, edges, token_budget=budget))
|
||||
print(
|
||||
_query_graph_text(
|
||||
G,
|
||||
question,
|
||||
mode="dfs" if use_dfs else "bfs",
|
||||
depth=2,
|
||||
token_budget=budget,
|
||||
context_filters=context_filters,
|
||||
)
|
||||
)
|
||||
elif cmd == "save-result":
|
||||
# graphify save-result --question Q --answer A --type T [--nodes N1 N2 ...]
|
||||
import argparse as _ap
|
||||
|
||||
+137
-41
@@ -150,6 +150,7 @@ def _import_python(node, source: bytes, file_nid: str, stem: str, edges: list, s
|
||||
"source": file_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports",
|
||||
"context": "import",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
@@ -174,6 +175,7 @@ def _import_python(node, source: bytes, file_nid: str, stem: str, edges: list, s
|
||||
"source": file_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports_from",
|
||||
"context": "import",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
@@ -221,6 +223,7 @@ def _import_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_p
|
||||
"source": file_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports_from",
|
||||
"context": "import",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
@@ -285,6 +288,7 @@ def _import_java(node, source: bytes, file_nid: str, stem: str, edges: list, str
|
||||
"source": file_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports",
|
||||
"context": "import",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
@@ -304,6 +308,7 @@ def _import_c(node, source: bytes, file_nid: str, stem: str, edges: list, str_pa
|
||||
"source": file_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports",
|
||||
"context": "import",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
@@ -323,6 +328,7 @@ def _import_csharp(node, source: bytes, file_nid: str, stem: str, edges: list, s
|
||||
"source": file_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports",
|
||||
"context": "import",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
@@ -342,6 +348,7 @@ def _import_kotlin(node, source: bytes, file_nid: str, stem: str, edges: list, s
|
||||
"source": file_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports",
|
||||
"context": "import",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
@@ -357,6 +364,7 @@ def _import_kotlin(node, source: bytes, file_nid: str, stem: str, edges: list, s
|
||||
"source": file_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports",
|
||||
"context": "import",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
@@ -376,6 +384,7 @@ def _import_scala(node, source: bytes, file_nid: str, stem: str, edges: list, st
|
||||
"source": file_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports",
|
||||
"context": "import",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
@@ -395,6 +404,7 @@ def _import_php(node, source: bytes, file_nid: str, stem: str, edges: list, str_
|
||||
"source": file_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports",
|
||||
"context": "import",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
@@ -696,6 +706,7 @@ def _import_lua(node, source: bytes, file_nid: str, stem: str, edges: list, str_
|
||||
"source": file_nid,
|
||||
"target": module_name,
|
||||
"relation": "imports",
|
||||
"context": "import",
|
||||
"confidence": "EXTRACTED",
|
||||
"confidence_score": 1.0,
|
||||
"source_file": str_path,
|
||||
@@ -730,6 +741,7 @@ def _import_swift(node, source: bytes, file_nid: str, stem: str, edges: list, st
|
||||
"source": file_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "imports",
|
||||
"context": "import",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{node.start_point[0] + 1}",
|
||||
@@ -756,6 +768,27 @@ def _import_vbnet(node, source: bytes, file_nid: str, stem: str, edges: list, st
|
||||
})
|
||||
|
||||
|
||||
def _read_csharp_type_name(node, source: bytes) -> str | None:
|
||||
"""Resolve a readable C# type name from a field/type node."""
|
||||
if node is None:
|
||||
return None
|
||||
if node.type in ("identifier", "predefined_type"):
|
||||
return _read_text(node, source)
|
||||
if node.type == "qualified_name":
|
||||
return _read_text(node, source).split(".")[-1]
|
||||
if node.type == "generic_name":
|
||||
name_node = node.child_by_field_name("name")
|
||||
if name_node is not None:
|
||||
return _read_text(name_node, source)
|
||||
for child in node.children:
|
||||
if not child.is_named:
|
||||
continue
|
||||
name = _read_csharp_type_name(child, source)
|
||||
if name:
|
||||
return name
|
||||
return None
|
||||
|
||||
|
||||
_SWIFT_CONFIG = LanguageConfig(
|
||||
ts_module="tree_sitter_swift",
|
||||
class_types=frozenset({"class_declaration", "protocol_declaration"}),
|
||||
@@ -833,8 +866,9 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
|
||||
})
|
||||
|
||||
def add_edge(src: str, tgt: str, relation: str, line: int,
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
||||
edges.append({
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0,
|
||||
context: str | None = None) -> None:
|
||||
edge = {
|
||||
"source": src,
|
||||
"target": tgt,
|
||||
"relation": relation,
|
||||
@@ -842,7 +876,19 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{line}",
|
||||
"weight": weight,
|
||||
})
|
||||
}
|
||||
if context:
|
||||
edge["context"] = context
|
||||
edges.append(edge)
|
||||
|
||||
def ensure_named_node(name: str, line: int) -> str:
|
||||
nid = _make_id(stem, name)
|
||||
if nid in seen_ids:
|
||||
return nid
|
||||
nid = _make_id(name)
|
||||
if nid not in seen_ids:
|
||||
add_node(nid, name, line)
|
||||
return nid
|
||||
|
||||
file_nid = _make_id(str(path))
|
||||
add_node(file_nid, path.name, 1)
|
||||
@@ -1078,6 +1124,23 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
|
||||
break
|
||||
return
|
||||
|
||||
if (config.ts_module == "tree_sitter_c_sharp"
|
||||
and t == "field_declaration"
|
||||
and parent_class_nid):
|
||||
type_node = node.child_by_field_name("type")
|
||||
if type_node is None:
|
||||
for child in node.children:
|
||||
if child.type == "variable_declaration":
|
||||
type_node = child.child_by_field_name("type")
|
||||
if type_node is not None:
|
||||
break
|
||||
type_name = _read_csharp_type_name(type_node, source)
|
||||
if type_name:
|
||||
line = node.start_point[0] + 1
|
||||
add_edge(parent_class_nid, ensure_named_node(type_name, line),
|
||||
"references", line, context="field")
|
||||
return
|
||||
|
||||
# Function types
|
||||
if t in config.function_types:
|
||||
# Swift deinit/subscript have no name field — resolve before generic fallback
|
||||
@@ -1304,6 +1367,7 @@ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
|
||||
"source": caller_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "calls",
|
||||
"context": "call",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{line}",
|
||||
@@ -2032,8 +2096,9 @@ def extract_julia(path: Path) -> dict:
|
||||
})
|
||||
|
||||
def add_edge(src: str, tgt: str, relation: str, line: int,
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
||||
edges.append({
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0,
|
||||
context: str | None = None) -> None:
|
||||
edge = {
|
||||
"source": src,
|
||||
"target": tgt,
|
||||
"relation": relation,
|
||||
@@ -2041,7 +2106,10 @@ def extract_julia(path: Path) -> dict:
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{line}",
|
||||
"weight": weight,
|
||||
})
|
||||
}
|
||||
if context:
|
||||
edge["context"] = context
|
||||
edges.append(edge)
|
||||
|
||||
file_nid = _make_id(str(path))
|
||||
add_node(file_nid, path.name, 1)
|
||||
@@ -2068,14 +2136,14 @@ def extract_julia(path: Path) -> dict:
|
||||
callee_name = _read_text(callee, source)
|
||||
target_nid = _make_id(stem, callee_name)
|
||||
add_edge(func_nid, target_nid, "calls", body_node.start_point[0] + 1,
|
||||
confidence="EXTRACTED")
|
||||
confidence="EXTRACTED", context="call")
|
||||
# Method call: obj.method(...)
|
||||
elif callee.type == "field_expression" and len(callee.children) >= 3:
|
||||
method_node = callee.children[-1]
|
||||
method_name = _read_text(method_node, source)
|
||||
target_nid = _make_id(stem, method_name)
|
||||
add_edge(func_nid, target_nid, "calls", body_node.start_point[0] + 1,
|
||||
confidence="EXTRACTED")
|
||||
confidence="EXTRACTED", context="call")
|
||||
for child in body_node.children:
|
||||
walk_calls(child, func_nid)
|
||||
|
||||
@@ -2176,14 +2244,14 @@ def extract_julia(path: Path) -> dict:
|
||||
mod_name = _read_text(child, source)
|
||||
imp_nid = _make_id(mod_name)
|
||||
add_node(imp_nid, mod_name, line)
|
||||
add_edge(scope_nid, imp_nid, "imports", line)
|
||||
add_edge(scope_nid, imp_nid, "imports", line, context="import")
|
||||
elif child.type == "selected_import":
|
||||
identifiers = [c for c in child.children if c.type == "identifier"]
|
||||
if identifiers:
|
||||
pkg_name = _read_text(identifiers[0], source)
|
||||
pkg_nid = _make_id(pkg_name)
|
||||
add_node(pkg_nid, pkg_name, line)
|
||||
add_edge(scope_nid, pkg_nid, "imports", line)
|
||||
add_edge(scope_nid, pkg_nid, "imports", line, context="import")
|
||||
return
|
||||
|
||||
for child in node.children:
|
||||
@@ -2248,8 +2316,9 @@ def extract_go(path: Path) -> dict:
|
||||
})
|
||||
|
||||
def add_edge(src: str, tgt: str, relation: str, line: int,
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
||||
edges.append({
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0,
|
||||
context: str | None = None) -> None:
|
||||
edge = {
|
||||
"source": src,
|
||||
"target": tgt,
|
||||
"relation": relation,
|
||||
@@ -2257,7 +2326,10 @@ def extract_go(path: Path) -> dict:
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{line}",
|
||||
"weight": weight,
|
||||
})
|
||||
}
|
||||
if context:
|
||||
edge["context"] = context
|
||||
edges.append(edge)
|
||||
|
||||
file_nid = _make_id(str(path))
|
||||
add_node(file_nid, path.name, 1)
|
||||
@@ -2331,7 +2403,7 @@ def extract_go(path: Path) -> dict:
|
||||
# Prefix with go_pkg_ so stdlib names (e.g. "context")
|
||||
# don't collide with local files of the same basename.
|
||||
tgt_nid = _make_id("go", "pkg", raw)
|
||||
add_edge(file_nid, tgt_nid, "imports_from", spec.start_point[0] + 1)
|
||||
add_edge(file_nid, tgt_nid, "imports_from", spec.start_point[0] + 1, context="import")
|
||||
# Track local name (alias or last path segment)
|
||||
alias = spec.child_by_field_name("name")
|
||||
local_name = _read_text(alias, source) if alias else raw.split("/")[-1]
|
||||
@@ -2342,7 +2414,7 @@ def extract_go(path: Path) -> dict:
|
||||
if path_node:
|
||||
raw = _read_text(path_node, source).strip('"')
|
||||
tgt_nid = _make_id("go", "pkg", raw)
|
||||
add_edge(file_nid, tgt_nid, "imports_from", child.start_point[0] + 1)
|
||||
add_edge(file_nid, tgt_nid, "imports_from", child.start_point[0] + 1, context="import")
|
||||
alias = child.child_by_field_name("name")
|
||||
local_name = _read_text(alias, source) if alias else raw.split("/")[-1]
|
||||
if local_name and local_name != "_" and local_name != ".":
|
||||
@@ -2393,6 +2465,7 @@ def extract_go(path: Path) -> dict:
|
||||
"source": caller_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "calls",
|
||||
"context": "call",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{line}",
|
||||
@@ -2460,8 +2533,9 @@ def extract_rust(path: Path) -> dict:
|
||||
})
|
||||
|
||||
def add_edge(src: str, tgt: str, relation: str, line: int,
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
||||
edges.append({
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0,
|
||||
context: str | None = None) -> None:
|
||||
edge = {
|
||||
"source": src,
|
||||
"target": tgt,
|
||||
"relation": relation,
|
||||
@@ -2469,7 +2543,10 @@ def extract_rust(path: Path) -> dict:
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{line}",
|
||||
"weight": weight,
|
||||
})
|
||||
}
|
||||
if context:
|
||||
edge["context"] = context
|
||||
edges.append(edge)
|
||||
|
||||
file_nid = _make_id(str(path))
|
||||
add_node(file_nid, path.name, 1)
|
||||
@@ -2526,7 +2603,7 @@ def extract_rust(path: Path) -> dict:
|
||||
module_name = clean.split("::")[-1].strip()
|
||||
if module_name:
|
||||
tgt_nid = _make_id(module_name)
|
||||
add_edge(file_nid, tgt_nid, "imports_from", node.start_point[0] + 1)
|
||||
add_edge(file_nid, tgt_nid, "imports_from", node.start_point[0] + 1, context="import")
|
||||
return
|
||||
|
||||
for child in node.children:
|
||||
@@ -2573,6 +2650,7 @@ def extract_rust(path: Path) -> dict:
|
||||
"source": caller_nid,
|
||||
"target": tgt_nid,
|
||||
"relation": "calls",
|
||||
"context": "call",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": str_path,
|
||||
"source_location": f"L{line}",
|
||||
@@ -2635,10 +2713,14 @@ def extract_zig(path: Path) -> dict:
|
||||
"source_file": str_path, "source_location": f"L{line}"})
|
||||
|
||||
def add_edge(src: str, tgt: str, relation: str, line: int,
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
||||
edges.append({"source": src, "target": tgt, "relation": relation,
|
||||
"confidence": confidence, "source_file": str_path,
|
||||
"source_location": f"L{line}", "weight": weight})
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0,
|
||||
context: str | None = None) -> None:
|
||||
edge = {"source": src, "target": tgt, "relation": relation,
|
||||
"confidence": confidence, "source_file": str_path,
|
||||
"source_location": f"L{line}", "weight": weight}
|
||||
if context:
|
||||
edge["context"] = context
|
||||
edges.append(edge)
|
||||
|
||||
file_nid = _make_id(str(path))
|
||||
add_node(file_nid, path.name, 1)
|
||||
@@ -2801,10 +2883,14 @@ def extract_powershell(path: Path) -> dict:
|
||||
"source_file": str_path, "source_location": f"L{line}"})
|
||||
|
||||
def add_edge(src: str, tgt: str, relation: str, line: int,
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
||||
edges.append({"source": src, "target": tgt, "relation": relation,
|
||||
"confidence": confidence, "source_file": str_path,
|
||||
"source_location": f"L{line}", "weight": weight})
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0,
|
||||
context: str | None = None) -> None:
|
||||
edge = {"source": src, "target": tgt, "relation": relation,
|
||||
"confidence": confidence, "source_file": str_path,
|
||||
"source_location": f"L{line}", "weight": weight}
|
||||
if context:
|
||||
edge["context"] = context
|
||||
edges.append(edge)
|
||||
|
||||
file_nid = _make_id(str(path))
|
||||
add_node(file_nid, path.name, 1)
|
||||
@@ -3196,10 +3282,14 @@ def extract_objc(path: Path) -> dict:
|
||||
"source_file": str_path, "source_location": f"L{line}"})
|
||||
|
||||
def add_edge(src: str, tgt: str, relation: str, line: int,
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
||||
edges.append({"source": src, "target": tgt, "relation": relation,
|
||||
"confidence": confidence, "source_file": str_path,
|
||||
"source_location": f"L{line}", "weight": weight})
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0,
|
||||
context: str | None = None) -> None:
|
||||
edge = {"source": src, "target": tgt, "relation": relation,
|
||||
"confidence": confidence, "source_file": str_path,
|
||||
"source_location": f"L{line}", "weight": weight}
|
||||
if context:
|
||||
edge["context"] = context
|
||||
edges.append(edge)
|
||||
|
||||
file_nid = _make_id(str(path))
|
||||
add_node(file_nid, path.name, 1)
|
||||
@@ -3223,7 +3313,7 @@ def extract_objc(path: Path) -> dict:
|
||||
module = raw.split("/")[-1].replace(".h", "")
|
||||
if module:
|
||||
tgt_nid = _make_id(module)
|
||||
add_edge(file_nid, tgt_nid, "imports", line)
|
||||
add_edge(file_nid, tgt_nid, "imports", line, context="import")
|
||||
elif child.type == "string_literal":
|
||||
# recurse into string_literal to find string_content
|
||||
for sub in child.children:
|
||||
@@ -3232,7 +3322,7 @@ def extract_objc(path: Path) -> dict:
|
||||
module = raw.split("/")[-1].replace(".h", "")
|
||||
if module:
|
||||
tgt_nid = _make_id(module)
|
||||
add_edge(file_nid, tgt_nid, "imports", line)
|
||||
add_edge(file_nid, tgt_nid, "imports", line, context="import")
|
||||
return
|
||||
|
||||
if t == "class_interface":
|
||||
@@ -3263,7 +3353,7 @@ def extract_objc(path: Path) -> dict:
|
||||
for s in sub.children:
|
||||
if s.type == "type_identifier":
|
||||
proto_nid = _make_id(_read(s))
|
||||
add_edge(cls_nid, proto_nid, "imports", line)
|
||||
add_edge(cls_nid, proto_nid, "imports", line, context="import")
|
||||
elif child.type == "method_declaration":
|
||||
walk(child, cls_nid)
|
||||
return
|
||||
@@ -3355,7 +3445,7 @@ def extract_objc(path: Path) -> dict:
|
||||
if pair not in seen_calls and caller_nid != candidate:
|
||||
seen_calls.add(pair)
|
||||
add_edge(caller_nid, candidate, "calls", body_node.start_point[0] + 1,
|
||||
confidence="EXTRACTED", weight=1.0)
|
||||
confidence="EXTRACTED", weight=1.0, context="call")
|
||||
for child in n.children:
|
||||
walk_calls(child)
|
||||
walk_calls(body_node)
|
||||
@@ -3394,10 +3484,14 @@ def extract_elixir(path: Path) -> dict:
|
||||
"source_file": str_path, "source_location": f"L{line}"})
|
||||
|
||||
def add_edge(src: str, tgt: str, relation: str, line: int,
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
|
||||
edges.append({"source": src, "target": tgt, "relation": relation,
|
||||
"confidence": confidence, "source_file": str_path,
|
||||
"source_location": f"L{line}", "weight": weight})
|
||||
confidence: str = "EXTRACTED", weight: float = 1.0,
|
||||
context: str | None = None) -> None:
|
||||
edge = {"source": src, "target": tgt, "relation": relation,
|
||||
"confidence": confidence, "source_file": str_path,
|
||||
"source_location": f"L{line}", "weight": weight}
|
||||
if context:
|
||||
edge["context"] = context
|
||||
edges.append(edge)
|
||||
|
||||
file_nid = _make_id(str(path))
|
||||
add_node(file_nid, path.name, 1)
|
||||
@@ -3476,7 +3570,7 @@ def extract_elixir(path: Path) -> dict:
|
||||
module_name = _get_alias_text(arguments_node)
|
||||
if module_name:
|
||||
tgt_nid = _make_id(module_name)
|
||||
add_edge(file_nid, tgt_nid, "imports", line)
|
||||
add_edge(file_nid, tgt_nid, "imports", line, context="import")
|
||||
return
|
||||
|
||||
for child in node.children:
|
||||
@@ -3531,7 +3625,8 @@ def extract_elixir(path: Path) -> dict:
|
||||
if pair not in seen_call_pairs:
|
||||
seen_call_pairs.add(pair)
|
||||
add_edge(caller_nid, tgt_nid, "calls",
|
||||
node.start_point[0] + 1, confidence="EXTRACTED", weight=1.0)
|
||||
node.start_point[0] + 1, confidence="EXTRACTED", weight=1.0,
|
||||
context="call")
|
||||
else:
|
||||
raw_calls.append({
|
||||
"caller_nid": caller_nid,
|
||||
@@ -3763,6 +3858,7 @@ def extract(paths: list[Path], cache_root: Path | None = None) -> dict:
|
||||
"source": caller,
|
||||
"target": tgt,
|
||||
"relation": "calls",
|
||||
"context": "call",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.8,
|
||||
"source_file": rc.get("source_file", ""),
|
||||
|
||||
+111
-9
@@ -63,6 +63,68 @@ def _score_nodes(G: nx.Graph, terms: list[str]) -> list[tuple[float, str]]:
|
||||
return sorted(scored, reverse=True)
|
||||
|
||||
|
||||
_CONTEXT_HINTS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("call", ("call", "calls", "called", "invoke", "invokes", "invoked")),
|
||||
("import", ("import", "imports", "imported", "module", "modules")),
|
||||
("field", ("field", "fields", "member", "members", "property", "properties")),
|
||||
("parameter_type", ("parameter", "parameters", "param", "params", "argument", "arguments")),
|
||||
("return_type", ("return", "returns", "returned")),
|
||||
("generic_arg", ("generic", "generics", "template", "templates")),
|
||||
)
|
||||
|
||||
|
||||
def _normalize_context_filters(filters: list[str] | None) -> list[str]:
|
||||
if not filters:
|
||||
return []
|
||||
normalized: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for value in filters:
|
||||
key = _strip_diacritics(str(value)).strip().lower()
|
||||
if key and key not in seen:
|
||||
seen.add(key)
|
||||
normalized.append(key)
|
||||
return normalized
|
||||
|
||||
|
||||
def _infer_context_filters(question: str) -> list[str]:
|
||||
lowered = {
|
||||
_strip_diacritics(token).lower()
|
||||
for token in question.replace("?", " ").replace(",", " ").split()
|
||||
}
|
||||
inferred: list[str] = []
|
||||
for context, hints in _CONTEXT_HINTS:
|
||||
if any(hint in lowered for hint in hints):
|
||||
inferred.append(context)
|
||||
return inferred
|
||||
|
||||
|
||||
def _resolve_context_filters(question: str, explicit_filters: list[str] | None = None) -> tuple[list[str], str | None]:
|
||||
normalized = _normalize_context_filters(explicit_filters)
|
||||
if normalized:
|
||||
return normalized, "explicit"
|
||||
inferred = _infer_context_filters(question)
|
||||
if inferred:
|
||||
return inferred, "heuristic"
|
||||
return [], None
|
||||
|
||||
|
||||
def _filter_graph_by_context(G: nx.Graph, context_filters: list[str] | None) -> nx.Graph:
|
||||
filters = set(_normalize_context_filters(context_filters))
|
||||
if not filters:
|
||||
return G
|
||||
H = G.__class__()
|
||||
H.add_nodes_from(G.nodes(data=True))
|
||||
if isinstance(G, (nx.MultiGraph, nx.MultiDiGraph)):
|
||||
for u, v, key, data in G.edges(keys=True, data=True):
|
||||
if data.get("context") in filters:
|
||||
H.add_edge(u, v, key=key, **data)
|
||||
else:
|
||||
for u, v, data in G.edges(data=True):
|
||||
if data.get("context") in filters:
|
||||
H.add_edge(u, v, **data)
|
||||
return H
|
||||
|
||||
|
||||
def _bfs(G: nx.Graph, start_nodes: list[str], depth: int) -> tuple[set[str], list[tuple]]:
|
||||
visited: set[str] = set(start_nodes)
|
||||
frontier = set(start_nodes)
|
||||
@@ -114,7 +176,13 @@ def _subgraph_to_text(G: nx.Graph, nodes: set[str], edges: list[tuple], token_bu
|
||||
if u in nodes and v in nodes:
|
||||
raw = G[u][v]
|
||||
d = next(iter(raw.values()), {}) if isinstance(G, (nx.MultiGraph, nx.MultiDiGraph)) else raw
|
||||
line = f"EDGE {sanitize_label(G.nodes[u].get('label', u))} --{d.get('relation', '')} [{d.get('confidence', '')}]--> {sanitize_label(G.nodes[v].get('label', v))}"
|
||||
context = d.get("context")
|
||||
context_suffix = f" context={context}" if context else ""
|
||||
line = (
|
||||
f"EDGE {sanitize_label(G.nodes[u].get('label', u))} "
|
||||
f"--{d.get('relation', '')} [{d.get('confidence', '')}{context_suffix}]--> "
|
||||
f"{sanitize_label(G.nodes[v].get('label', v))}"
|
||||
)
|
||||
lines.append(line)
|
||||
output = "\n".join(lines)
|
||||
if len(output) > char_budget:
|
||||
@@ -122,6 +190,34 @@ def _subgraph_to_text(G: nx.Graph, nodes: set[str], edges: list[tuple], token_bu
|
||||
return output
|
||||
|
||||
|
||||
def _query_graph_text(
|
||||
G: nx.Graph,
|
||||
question: str,
|
||||
*,
|
||||
mode: str = "bfs",
|
||||
depth: int = 3,
|
||||
token_budget: int = 2000,
|
||||
context_filters: list[str] | None = None,
|
||||
) -> str:
|
||||
terms = [t.lower() for t in question.split() if len(t) > 2]
|
||||
scored = _score_nodes(G, terms)
|
||||
start_nodes = [nid for _, nid in scored[:3]]
|
||||
if not start_nodes:
|
||||
return "No matching nodes found."
|
||||
resolved_filters, filter_source = _resolve_context_filters(question, context_filters)
|
||||
traversal_graph = _filter_graph_by_context(G, resolved_filters)
|
||||
nodes, edges = _dfs(traversal_graph, start_nodes, depth) if mode == "dfs" else _bfs(traversal_graph, start_nodes, depth)
|
||||
header_parts = [
|
||||
f"Traversal: {mode.upper()} depth={depth}",
|
||||
f"Start: {[G.nodes[n].get('label', n) for n in start_nodes]}",
|
||||
]
|
||||
if resolved_filters:
|
||||
header_parts.append(f"Context: {', '.join(resolved_filters)} ({filter_source})")
|
||||
header_parts.append(f"{len(nodes)} nodes found")
|
||||
header = " | ".join(header_parts) + "\n\n"
|
||||
return header + _subgraph_to_text(traversal_graph, nodes, edges, token_budget)
|
||||
|
||||
|
||||
def _find_node(G: nx.Graph, label: str) -> list[str]:
|
||||
"""Return node IDs whose label or ID matches the search term (diacritic-insensitive)."""
|
||||
term = _strip_diacritics(label).lower()
|
||||
@@ -188,6 +284,11 @@ def serve(graph_path: str = "graphify-out/graph.json") -> None:
|
||||
"description": "bfs=broad context, dfs=trace a specific path"},
|
||||
"depth": {"type": "integer", "default": 3, "description": "Traversal depth (1-6)"},
|
||||
"token_budget": {"type": "integer", "default": 2000, "description": "Max output tokens"},
|
||||
"context_filter": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Optional explicit edge-context filter, e.g. ['call', 'field']",
|
||||
},
|
||||
},
|
||||
"required": ["question"],
|
||||
},
|
||||
@@ -252,14 +353,15 @@ def serve(graph_path: str = "graphify-out/graph.json") -> None:
|
||||
mode = arguments.get("mode", "bfs")
|
||||
depth = min(int(arguments.get("depth", 3)), 6)
|
||||
budget = int(arguments.get("token_budget", 2000))
|
||||
terms = [t.lower() for t in question.split() if len(t) > 2]
|
||||
scored = _score_nodes(G, terms)
|
||||
start_nodes = [nid for _, nid in scored[:3]]
|
||||
if not start_nodes:
|
||||
return "No matching nodes found."
|
||||
nodes, edges = _dfs(G, start_nodes, depth) if mode == "dfs" else _bfs(G, start_nodes, depth)
|
||||
header = f"Traversal: {mode.upper()} depth={depth} | Start: {[G.nodes[n].get('label', n) for n in start_nodes]} | {len(nodes)} nodes found\n\n"
|
||||
return header + _subgraph_to_text(G, nodes, edges, budget, seeds=start_nodes)
|
||||
context_filter = arguments.get("context_filter")
|
||||
return _query_graph_text(
|
||||
G,
|
||||
question,
|
||||
mode=mode,
|
||||
depth=depth,
|
||||
token_budget=budget,
|
||||
context_filters=context_filter,
|
||||
)
|
||||
|
||||
def _tool_get_node(arguments: dict) -> str:
|
||||
label = arguments["label"].lower()
|
||||
|
||||
Reference in New Issue
Block a user