mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-18 01:49:22 +00:00
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)
70 lines
3.0 KiB
Python
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
|