Files
graphify/tests/test_suggest_questions_ambiguous.py
T
JunoLee 97f0dc197e fix(cluster,analyze,report): isolate AMBIGUOUS edges from top-N and modularity
AMBIGUOUS-tagged edges (confidence ≈ 0.35, often `conceptually_related_to`
from LLM semantic extraction) could glue otherwise-independent clusters
together through Leiden/Louvain modularity, and the resulting bridge
questions crowded `Suggested Questions` with low-signal entries. The
`AMBIGUOUS` flag was already correct — only downstream consumers
treated it identically to EXTRACTED/INFERRED.

Changes:
- `cluster()` gains `ambiguous_scale: float = 0.5` (overridable via
  `GRAPHIFY_AMBIGUOUS_SCALE`). Multiplier is applied to AMBIGUOUS edge
  `weight` in a graph copy before partitioning so the raw graph (and
  `/graphify path`/`explain` output) stays untouched. 1.0 restores
  legacy behavior.
- `suggest_questions()` tags each ambiguous_edge entry with
  `low_confidence=True`. Interleave + top_n contract from #3849 is
  preserved; the tag is purely metadata.
- `report.py` splits tagged entries into a new "Low-confidence Hints"
  section below the main Suggested Questions.

Tests:
- `test_cluster_ambiguous_scale.py` (new, 7 tests): default scale
  demotes AMBIGUOUS weight, scale=0.0 drops it entirely, scale=1.0
  is no-op, env override + malformed env fail-soft, raw graph not
  mutated, no copy when no AMBIGUOUS edges present.
- `test_suggest_questions_ambiguous.py` (new, 4 tests): tag present
  on every ambiguous_edge entry, never on other types.
- `test_analyze.py::test_suggest_questions_diversity_preserves_limit_and_candidates`
  relaxed to allow the optional `low_confidence` key.

Fixes #4199

---
한국어 요약: AMBIGUOUS edge (LLM 추론, 신뢰도 ~0.35) 가 Leiden 모듈러리티로 독립
클러스터를 묶거나 Suggested Questions top-N 을 잠식하던 문제 수정. cluster() 에
ambiguous_scale 파라미터 (default 0.5, env override) 추가, 원본 그래프는 복사 후
수정하여 /graphify path/explain 영향 없음. suggest_questions 는 low_confidence
플래그만 추가하고 interleave contract (#3849) 유지. report.py 가 분리 렌더링.
regression test 11 개 신규 + 기존 1 개 relax.

(cherry picked from commit ff52a29266)
2026-10-08 14:27:06 +01:00

70 lines
3.0 KiB
Python

"""#4199 regression — AMBIGUOUS questions tagged ``low_confidence`` and
split out of top-N so a flood of noise cannot push real bridges off the
Suggested Questions list.
"""
from __future__ import annotations
import networkx as nx
from graphify.analyze import suggest_questions
def _graph_with_noise(num_ambiguous: int = 20) -> tuple[nx.Graph, dict, dict]:
"""A graph with ``num_ambiguous`` AMBIGUOUS edges and one real
bridge-like structure (a central node connecting two communities)."""
G = nx.Graph()
# Community A: 5-node clique (community 0)
for i in range(5):
G.add_node(f"A{i}", label=f"A{i}")
for i in range(5):
for j in range(i + 1, 5):
G.add_edge(f"A{i}", f"A{j}", confidence="EXTRACTED", weight=1.0)
# Community B: 5-node clique (community 1)
for i in range(5):
G.add_node(f"B{i}", label=f"B{i}")
for i in range(5):
for j in range(i + 1, 5):
G.add_edge(f"B{i}", f"B{j}", confidence="EXTRACTED", weight=1.0)
# Flood AMBIGUOUS edges between A and B — raw noise.
for k in range(num_ambiguous):
G.add_node(f"amb_{k}", label=f"amb_{k}")
G.add_edge(f"A0", f"amb_{k}", confidence="AMBIGUOUS", weight=0.3, relation="conceptually_related_to")
G.add_edge(f"B0", f"amb_{k}", confidence="AMBIGUOUS", weight=0.3, relation="conceptually_related_to")
communities = {
0: [f"A{i}" for i in range(5)],
1: [f"B{i}" for i in range(5)],
2: [f"amb_{k}" for k in range(num_ambiguous)],
}
labels = {0: "A clique", 1: "B clique", 2: "Noise"}
return G, communities, labels
def test_ambiguous_questions_carry_low_confidence_flag():
"""Every AMBIGUOUS-type question must be tagged so renderers can route
it to the low-confidence section."""
G, c, l = _graph_with_noise(num_ambiguous=5)
qs = suggest_questions(G, c, l, top_n=50)
ambiguous = [q for q in qs if q["type"] == "ambiguous_edge"]
assert ambiguous, "fixture must produce ambiguous questions"
assert all(q.get("low_confidence") is True for q in ambiguous)
def test_high_confidence_questions_never_tagged():
"""Non-AMBIGUOUS types must not accidentally pick up low_confidence."""
G, c, l = _graph_with_noise(num_ambiguous=3)
qs = suggest_questions(G, c, l, top_n=50)
for q in qs:
if q["type"] != "ambiguous_edge":
assert not q.get("low_confidence"), f"{q['type']} wrongly tagged low_confidence"
def test_ambiguous_tagged_regardless_of_rank():
"""`low_confidence=True` must appear on every ambiguous_edge entry even
when top_n includes multiple of them. The downstream renderer uses the
tag — not position — to split sections."""
G, c, l = _graph_with_noise(num_ambiguous=20)
qs = suggest_questions(G, c, l, top_n=10)
tagged_count = sum(1 for q in qs if q.get("low_confidence"))
ambiguous_count = sum(1 for q in qs if q["type"] == "ambiguous_edge")
assert tagged_count == ambiguous_count
assert ambiguous_count > 0 # fixture produces at least one