Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions graphify/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,13 +326,36 @@ def _score_nodes(G: nx.Graph, terms: list[str]) -> list[tuple[float, str]]:
return scored


def _pick_seeds(scored: list[tuple[float, str]], max_k: int = 3, gap_ratio: float = 0.2) -> list[str]:
def _pick_seeds(
scored: list[tuple[float, str]],
max_k: int = 3,
gap_ratio: float = 0.2,
*,
G: "nx.Graph | None" = None,
terms: list[str] | None = None,
) -> list[str]:
"""Select BFS seed nodes, stopping when score drops too far below the top.

Prevents high-frequency noise terms (error, exception) from stealing seed
slots from a dominant identifier match. When FooBarService scores 1000 and
error nodes score 1.0, only FooBarService is seeded — the score gap is 99.9%
which is well above the 20% threshold that would allow additional seeds.

That same gap_ratio cutoff has a failure mode on multi-term natural-language
queries: if one term happens to hit an EXACT label match on a node that is
otherwise unrelated to the query's intent (e.g. a common word that is also
used as an unrelated identifier or field name elsewhere in the corpus), it
can outscore every SUBSTRING match on the query's other, actually-relevant
terms by ~1000x (see `_EXACT_MATCH_BONUS` vs. `_SUBSTRING_MATCH_BONUS`).
The 20%-gap cutoff then silently discards all of those substring-tier
seeds, so the BFS traversal only ever explores the neighborhood of the one
unrelated exact match — see #1445.

When `G` and `terms` are supplied, this guarantees at least one seed per
distinct query term that has any match at all, so one term's incidental
collision cannot starve out the others. Ties within a term are broken by
graph degree (structural centrality), so an isolated incidental match
doesn't out-rank a real, well-connected hub for that term.
"""
if not scored:
return []
Expand All @@ -342,6 +365,18 @@ def _pick_seeds(scored: list[tuple[float, str]], max_k: int = 3, gap_ratio: floa
if seeds and score < top_score * gap_ratio:
break
seeds.append(nid)

if G is not None and terms:
norm_terms = sorted({tok for t in terms for tok in _search_tokens(t)})
for term in norm_terms:
term_scored = _score_nodes(G, [term])
if not term_scored:
continue
best_score = term_scored[0][0]
tied = [nid for s, nid in term_scored if s == best_score]
best_nid = max(tied, key=lambda n: G.degree(n)) if len(tied) > 1 else term_scored[0][1]
if best_nid not in seeds:
seeds.append(best_nid)
return seeds


Expand Down Expand Up @@ -580,7 +615,7 @@ def _query_graph_text(
) -> str:
terms = _query_terms(question)
scored = _score_nodes(G, terms)
start_nodes = _pick_seeds(scored)
start_nodes = _pick_seeds(scored, G=G, terms=terms)
if not start_nodes:
return "No matching nodes found."
resolved_filters, filter_source = _resolve_context_filters(question, context_filters)
Expand Down
36 changes: 36 additions & 0 deletions tests/test_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,42 @@ def test_pick_seeds_respects_max_k():
assert len(seeds) == 3


def test_pick_seeds_without_diversity_args_is_unchanged():
"""G/terms are optional and default to None: existing callers see identical
behavior to before this change."""
scored = [(1000.0, "fbs"), (1.0, "err1"), (0.9, "err2")]
assert _pick_seeds(scored) == ["fbs"]


def test_pick_seeds_diversity_recovers_starved_term(monkeypatch):
"""Reproduces #1445: a vague natural-language query where one term's
incidental EXACT match on an unrelated node (e.g. a common word also used
as an unrelated field/identifier) outscores every SUBSTRING match on the
query's other, actually-relevant terms by ~1000x. Without G/terms, the
20%-gap cutoff discards the relevant candidate entirely; with them, it is
recovered as a guaranteed per-term seed.
"""
G = nx.DiGraph()
# "unrelated" is an exact label match for the query term "unrelated" and
# has no connection to the actually-relevant "target" node.
G.add_node("noise", label="unrelated", source_file="design_tokens.json")
# "target" only substring-matches the query term "widget" via its label.
G.add_node("target", label="rate_limit_widget", source_file="src/widget.py")
G.add_node("other", label="something_else", source_file="src/other.py")
G.add_edge("other", "target")

terms = ["unrelated", "widget"]
scored = _score_nodes(G, terms)

# Sanity check the premise: without diversity, only the exact match survives.
seeds_before = _pick_seeds(scored)
assert seeds_before == ["noise"]

seeds_after = _pick_seeds(scored, G=G, terms=terms)
assert "noise" in seeds_after
assert "target" in seeds_after


# --- actionable truncation hint (#897) ---

def test_subgraph_to_text_truncation_hint_is_actionable():
Expand Down