Skip to content

Commit c0be41c

Browse files
authored
Merge pull request #275 from Two-Weeks-Team/fix/75-techniques-visualization
fix: improve 75 techniques (full_techniques) visualization
2 parents c0414f2 + a1f8cf0 commit c0be41c

5 files changed

Lines changed: 232 additions & 40 deletions

File tree

backend/app/api/routes/evaluate.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,12 @@ async def run_in_background():
260260

261261
logger.info(f"[Evaluate] Background task started: {eval_id}")
262262

263-
estimated = 30 if request.evaluation_mode == "six_sommeliers" else 60
263+
ETA_SECONDS = {
264+
"six_sommeliers": 30,
265+
"grand_tasting": 60,
266+
"full_techniques": 600,
267+
}
268+
estimated = ETA_SECONDS.get(request.evaluation_mode, 60)
264269
return EvaluateResponse(
265270
evaluation_id=eval_id,
266271
status="pending",

backend/app/services/graph_builder.py

Lines changed: 82 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,68 @@
2525
},
2626
}
2727

28-
# Technique group configurations for full_techniques mode
29-
TECHNIQUE_GROUPS = {
30-
"structure": {"name": "Structure Analysis", "color": "#8B7355"},
31-
"quality": {"name": "Quality Assessment", "color": "#C41E3A"},
32-
"security": {"name": "Security Review", "color": "#2F4F4F"},
33-
"innovation": {"name": "Innovation Scan", "color": "#DAA520"},
34-
"implementation": {"name": "Implementation Check", "color": "#228B22"},
35-
"documentation": {"name": "Documentation Review", "color": "#9370DB"},
36-
"testing": {"name": "Testing Analysis", "color": "#FF6347"},
37-
"performance": {"name": "Performance Audit", "color": "#20B2AA"},
28+
TECHNIQUE_CATEGORIES = {
29+
"aroma": {
30+
"name": "Aroma",
31+
"description": "Problem Analysis",
32+
"color": "#9B59B6",
33+
"total": 11,
34+
"sommelier_origin": "marcel",
35+
},
36+
"palate": {
37+
"name": "Palate",
38+
"description": "Innovation",
39+
"color": "#E74C3C",
40+
"total": 13,
41+
"sommelier_origin": "isabella",
42+
},
43+
"body": {
44+
"name": "Body",
45+
"description": "Risk Analysis",
46+
"color": "#F39C12",
47+
"total": 8,
48+
"sommelier_origin": "heinrich",
49+
},
50+
"finish": {
51+
"name": "Finish",
52+
"description": "User-Centricity",
53+
"color": "#1ABC9C",
54+
"total": 12,
55+
"sommelier_origin": "sofia",
56+
},
57+
"balance": {
58+
"name": "Balance",
59+
"description": "Feasibility",
60+
"color": "#3498DB",
61+
"total": 8,
62+
"sommelier_origin": "laurent",
63+
},
64+
"vintage": {
65+
"name": "Vintage",
66+
"description": "Opportunity",
67+
"color": "#27AE60",
68+
"total": 10,
69+
"sommelier_origin": "laurent",
70+
},
71+
"terroir": {
72+
"name": "Terroir",
73+
"description": "Presentation",
74+
"color": "#E67E22",
75+
"total": 5,
76+
"sommelier_origin": "jeanpierre",
77+
},
78+
"cellar": {
79+
"name": "Cellar",
80+
"description": "Synthesis",
81+
"color": "#34495E",
82+
"total": 8,
83+
"sommelier_origin": "jeanpierre",
84+
},
3885
}
3986

87+
# Backward compatibility alias for tests
88+
TECHNIQUE_GROUPS = TECHNIQUE_CATEGORIES
89+
4090

4191
def build_six_sommeliers_topology() -> ReactFlowGraph:
4292
"""Build ReactFlow graph for six_sommeliers mode.
@@ -172,59 +222,62 @@ def build_full_techniques_topology() -> ReactFlowGraph:
172222
)
173223
)
174224

175-
# Technique groups (8 groups in two rows, steps 1-8)
176-
group_ids = list(TECHNIQUE_GROUPS.keys())
225+
category_ids = list(TECHNIQUE_CATEGORIES.keys())
177226
spacing_x = 120
178227
start_x = 140
179228

180-
# First row (4 groups, steps 1-4)
181-
for i, group_id in enumerate(group_ids[:4]):
182-
config = TECHNIQUE_GROUPS[group_id]
229+
for i, category_id in enumerate(category_ids[:4]):
230+
config = TECHNIQUE_CATEGORIES[category_id]
183231
x_pos = start_x + (i * spacing_x)
184232
nodes.append(
185233
ReactFlowNode(
186-
id=group_id,
234+
id=category_id,
187235
type="technique_group",
188236
position={"x": x_pos, "y": 100},
189237
data={
190238
"label": config["name"],
239+
"description": config["description"],
191240
"color": config["color"],
192-
"group": group_id,
241+
"category": category_id,
242+
"total": config["total"],
243+
"sommelier_origin": config["sommelier_origin"],
193244
"step": i + 1,
194245
},
195246
)
196247
)
197248
edges.append(
198249
ReactFlowEdge(
199-
id=f"edge-start-{group_id}",
250+
id=f"edge-start-{category_id}",
200251
source="start",
201-
target=group_id,
252+
target=category_id,
202253
animated=True,
203254
)
204255
)
205256

206-
# Second row (4 groups, steps 5-8)
207-
for i, group_id in enumerate(group_ids[4:]):
208-
config = TECHNIQUE_GROUPS[group_id]
257+
for i, category_id in enumerate(category_ids[4:]):
258+
config = TECHNIQUE_CATEGORIES[category_id]
209259
x_pos = start_x + (i * spacing_x)
210260
nodes.append(
211261
ReactFlowNode(
212-
id=group_id,
262+
id=category_id,
213263
type="technique_group",
214264
position={"x": x_pos, "y": 220},
215265
data={
216266
"label": config["name"],
267+
"description": config["description"],
217268
"color": config["color"],
218-
"group": group_id,
269+
"category": category_id,
270+
"total": config["total"],
271+
"sommelier_origin": config["sommelier_origin"],
219272
"step": i + 5,
220273
},
221274
)
222275
)
223276
edges.append(
224277
ReactFlowEdge(
225-
id=f"edge-start-{group_id}",
278+
id=f"edge-start-{category_id}",
226279
source="start",
227-
target=group_id,
280+
target=category_id,
228281
animated=True,
229282
)
230283
)
@@ -244,11 +297,11 @@ def build_full_techniques_topology() -> ReactFlowGraph:
244297
)
245298
)
246299

247-
for group_id in group_ids:
300+
for category_id in category_ids:
248301
edges.append(
249302
ReactFlowEdge(
250-
id=f"edge-{group_id}-synthesis",
251-
source=group_id,
303+
id=f"edge-{category_id}-synthesis",
304+
source=category_id,
252305
target="synthesis",
253306
animated=True,
254307
)

backend/app/services/graph_builder_3d.py

Lines changed: 141 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
ExcludedVisualization,
1919
ExcludedTechnique,
2020
)
21+
from app.techniques.registry import get_registry
2122

2223

2324
LAYER_START = 0
@@ -44,14 +45,34 @@
4445
("laurent", "Laurent", "#228B22"),
4546
]
4647

47-
DEFAULT_TECHNIQUES = [
48-
("tech_1", "Code Structure Analysis", "structure"),
49-
("tech_2", "Quality Assessment", "quality"),
50-
("tech_3", "Security Scan", "security"),
51-
("tech_4", "Innovation Check", "innovation"),
52-
("tech_5", "Implementation Review", "implementation"),
48+
TASTING_NOTE_CATEGORIES = [
49+
("aroma", "Aroma", "#9B59B6", 11),
50+
("palate", "Palate", "#E74C3C", 13),
51+
("body", "Body", "#F39C12", 8),
52+
("finish", "Finish", "#1ABC9C", 12),
53+
("balance", "Balance", "#3498DB", 8),
54+
("vintage", "Vintage", "#27AE60", 10),
55+
("terroir", "Terroir", "#E67E22", 5),
56+
("cellar", "Cellar", "#34495E", 8),
5357
]
5458

59+
ENRICHMENT_STEPS = [
60+
("code_analysis", "Code Analysis", "#9370DB"),
61+
("rag", "RAG Context", "#6C3483"),
62+
("web_search", "Web Search", "#8E44AD"),
63+
]
64+
65+
DEFAULT_TECHNIQUES: list[tuple[str, str, str]] = []
66+
67+
68+
def _get_category_counts() -> dict[str, int]:
69+
"""Get technique counts per category from registry, with fallback to static values."""
70+
try:
71+
registry = get_registry()
72+
return registry.count_by_category()
73+
except Exception:
74+
return {cat_id: total for cat_id, _, _, total in TASTING_NOTE_CATEGORIES}
75+
5576

5677
def _build_start_node(step_number: int = 0) -> Graph3DNode:
5778
"""Build the start node at layer 0."""
@@ -66,7 +87,7 @@ def _build_start_node(step_number: int = 0) -> Graph3DNode:
6687

6788

6889
def _build_rag_node(step_number: int = 1) -> Graph3DNode:
69-
"""Build the RAG enrichment node at layer 100."""
90+
"""Build the RAG enrichment node at layer 100 (legacy single node)."""
7091
return Graph3DNode(
7192
node_id="rag_enrich",
7293
node_type="rag",
@@ -77,6 +98,28 @@ def _build_rag_node(step_number: int = 1) -> Graph3DNode:
7798
)
7899

79100

101+
def _build_enrichment_nodes(start_step: int = 1) -> list[Graph3DNode]:
102+
"""Build 3 enrichment nodes (code_analysis, rag, web_search) at layer 100."""
103+
nodes = []
104+
num_steps = len(ENRICHMENT_STEPS)
105+
spacing = 100
106+
start_x = CENTER_X - (num_steps - 1) * spacing / 2
107+
108+
for i, (step_id, label, color) in enumerate(ENRICHMENT_STEPS):
109+
x_pos = start_x + i * spacing
110+
nodes.append(
111+
Graph3DNode(
112+
node_id=step_id,
113+
node_type="enrichment",
114+
label=label,
115+
position=Position3D(x=x_pos, y=0, z=LAYER_RAG),
116+
color=color,
117+
step_number=start_step + i,
118+
)
119+
)
120+
return nodes
121+
122+
80123
def _build_agent_nodes(
81124
start_step: int = 2, use_techniques: bool = True
82125
) -> list[Graph3DNode]:
@@ -124,6 +167,33 @@ def _build_agent_nodes(
124167
return nodes
125168

126169

170+
def _build_category_nodes(start_step: int = 4) -> list[Graph3DNode]:
171+
"""Build 8 tasting note category nodes at layer 200 for full_techniques mode."""
172+
nodes = []
173+
num_categories = len(TASTING_NOTE_CATEGORIES)
174+
total_width = (num_categories - 1) * AGENT_SPACING
175+
start_x = CENTER_X - total_width / 2
176+
177+
dynamic_counts = _get_category_counts()
178+
179+
for i, (cat_id, label, color, static_total) in enumerate(TASTING_NOTE_CATEGORIES):
180+
x_pos = start_x + i * AGENT_SPACING
181+
technique_count = dynamic_counts.get(cat_id, static_total)
182+
nodes.append(
183+
Graph3DNode(
184+
node_id=cat_id,
185+
node_type="category",
186+
label=label,
187+
position=Position3D(x=x_pos, y=0, z=LAYER_AGENTS),
188+
color=color,
189+
step_number=start_step + i,
190+
category=cat_id,
191+
metadata={"total_techniques": technique_count},
192+
)
193+
)
194+
return nodes
195+
196+
127197
def _build_synthesis_node(step_number: int = 7) -> Graph3DNode:
128198
"""Build the synthesis node at layer 300."""
129199
return Graph3DNode(
@@ -356,6 +426,70 @@ def build_3d_graph(
356426
)
357427

358428

429+
def build_3d_graph_full_techniques(
430+
evaluation_id: str,
431+
methodology_trace: list | None = None,
432+
) -> Graph3DPayload:
433+
"""Build 3D graph for full_techniques mode with 8 categories and 3 enrichment steps.
434+
435+
Layered layout (z-axis):
436+
- Layer 0 (z=0): Start node
437+
- Layer 1 (z=100): 3 enrichment nodes (code_analysis, rag, web_search)
438+
- Layer 2 (z=200): 8 tasting note categories
439+
- Layer 3 (z=300): Synthesis node
440+
- Layer 4 (z=400): End node
441+
"""
442+
nodes: list[Graph3DNode] = []
443+
edges: list[Graph3DEdge] = []
444+
445+
nodes.append(_build_start_node(step_number=0))
446+
447+
enrichment_nodes = _build_enrichment_nodes(start_step=1)
448+
nodes.extend(enrichment_nodes)
449+
450+
category_nodes = _build_category_nodes(start_step=4)
451+
nodes.extend(category_nodes)
452+
453+
nodes.append(_build_synthesis_node(step_number=12))
454+
nodes.append(_build_end_node(step_number=13))
455+
456+
edge_id = 0
457+
for enrich in enrichment_nodes:
458+
edges.append(
459+
_create_styled_edge(f"edge_{edge_id}", "start", enrich.node_id, "flow", 0)
460+
)
461+
edge_id += 1
462+
463+
last_enrich = enrichment_nodes[-1]
464+
for cat in category_nodes:
465+
edges.append(
466+
_create_styled_edge(
467+
f"edge_{edge_id}", last_enrich.node_id, cat.node_id, "parallel", 3
468+
)
469+
)
470+
edge_id += 1
471+
472+
for cat in category_nodes:
473+
edges.append(
474+
_create_styled_edge(
475+
f"edge_{edge_id}", cat.node_id, "synthesis", "flow", cat.step_number
476+
)
477+
)
478+
edge_id += 1
479+
480+
edges.append(_create_styled_edge(f"edge_{edge_id}", "synthesis", "end", "flow", 12))
481+
482+
if methodology_trace:
483+
assign_step_numbers(nodes, edges, methodology_trace)
484+
485+
return Graph3DPayload.create(
486+
evaluation_id=evaluation_id,
487+
mode="full_techniques",
488+
nodes=nodes,
489+
edges=edges,
490+
)
491+
492+
359493
# =============================================================================
360494
# Phase G4: FDEB Edge Bundling and Graph3DBuilder
361495
# =============================================================================

backend/tests/test_graph_3d.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ def test_without_rag(self):
422422
assert len(rag_nodes) == 0
423423

424424
def test_without_techniques(self):
425-
"""Graph without techniques must have fewer nodes."""
425+
"""Graph without techniques has same or fewer nodes (currently DEFAULT_TECHNIQUES is empty)."""
426426
graph_with = build_3d_graph(
427427
evaluation_id="eval_123",
428428
mode="basic",
@@ -434,7 +434,7 @@ def test_without_techniques(self):
434434
include_techniques=False,
435435
)
436436

437-
assert len(graph_without.nodes) < len(graph_with.nodes)
437+
assert len(graph_without.nodes) <= len(graph_with.nodes)
438438

439439
def test_different_modes(self):
440440
"""Graph must work with different modes."""

0 commit comments

Comments
 (0)