Skip to content
Merged
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
7 changes: 6 additions & 1 deletion backend/app/api/routes/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,12 @@ async def run_in_background():

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

estimated = 30 if request.evaluation_mode == "six_sommeliers" else 60
ETA_SECONDS = {
"six_sommeliers": 30,
"grand_tasting": 60,
"full_techniques": 600,
}
Comment on lines +263 to +267

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

ETA_SECONDS 딕셔너리는 애플리케이션의 설정을 나타내므로, 함수 내에 지역 변수로 두기보다는 모듈 최상단에 상수로 정의하는 것이 좋습니다. 이렇게 하면 코드의 다른 부분(예: create_public_evaluation 함수)에서도 이 값을 재사용할 수 있어 일관성과 유지보수성이 향상됩니다.

estimated = ETA_SECONDS.get(request.evaluation_mode, 60)
return EvaluateResponse(
evaluation_id=eval_id,
status="pending",
Expand Down
111 changes: 82 additions & 29 deletions backend/app/services/graph_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,68 @@
},
}

# Technique group configurations for full_techniques mode
TECHNIQUE_GROUPS = {
"structure": {"name": "Structure Analysis", "color": "#8B7355"},
"quality": {"name": "Quality Assessment", "color": "#C41E3A"},
"security": {"name": "Security Review", "color": "#2F4F4F"},
"innovation": {"name": "Innovation Scan", "color": "#DAA520"},
"implementation": {"name": "Implementation Check", "color": "#228B22"},
"documentation": {"name": "Documentation Review", "color": "#9370DB"},
"testing": {"name": "Testing Analysis", "color": "#FF6347"},
"performance": {"name": "Performance Audit", "color": "#20B2AA"},
TECHNIQUE_CATEGORIES = {
"aroma": {
"name": "Aroma",
"description": "Problem Analysis",
"color": "#9B59B6",
"total": 11,
"sommelier_origin": "marcel",
},
"palate": {
"name": "Palate",
"description": "Innovation",
"color": "#E74C3C",
"total": 13,
"sommelier_origin": "isabella",
},
"body": {
"name": "Body",
"description": "Risk Analysis",
"color": "#F39C12",
"total": 8,
"sommelier_origin": "heinrich",
},
"finish": {
"name": "Finish",
"description": "User-Centricity",
"color": "#1ABC9C",
"total": 12,
"sommelier_origin": "sofia",
},
"balance": {
"name": "Balance",
"description": "Feasibility",
"color": "#3498DB",
"total": 8,
"sommelier_origin": "laurent",
},
"vintage": {
"name": "Vintage",
"description": "Opportunity",
"color": "#27AE60",
"total": 10,
"sommelier_origin": "laurent",
},
"terroir": {
"name": "Terroir",
"description": "Presentation",
"color": "#E67E22",
"total": 5,
"sommelier_origin": "jeanpierre",
},
"cellar": {
"name": "Cellar",
"description": "Synthesis",
"color": "#34495E",
"total": 8,
"sommelier_origin": "jeanpierre",
},
}
Comment on lines +28 to 85

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

total 값을 각 카테고리에 하드코딩하면 TechniqueRegistry의 실제 기법 수와 동기화되지 않을 위험이 있습니다. graph_builder_3d.py에 구현된 _get_category_counts()와 유사한 함수를 만들어 레지스트리에서 동적으로 기법 수를 가져오는 것을 고려해 보세요. 이렇게 하면 데이터의 일관성을 보장하고 유지보수성을 높일 수 있습니다.


# Backward compatibility alias for tests
TECHNIQUE_GROUPS = TECHNIQUE_CATEGORIES


def build_six_sommeliers_topology() -> ReactFlowGraph:
"""Build ReactFlow graph for six_sommeliers mode.
Expand Down Expand Up @@ -172,59 +222,62 @@ def build_full_techniques_topology() -> ReactFlowGraph:
)
)

# Technique groups (8 groups in two rows, steps 1-8)
group_ids = list(TECHNIQUE_GROUPS.keys())
category_ids = list(TECHNIQUE_CATEGORIES.keys())
spacing_x = 120
start_x = 140

# First row (4 groups, steps 1-4)
for i, group_id in enumerate(group_ids[:4]):
config = TECHNIQUE_GROUPS[group_id]
for i, category_id in enumerate(category_ids[:4]):
config = TECHNIQUE_CATEGORIES[category_id]
x_pos = start_x + (i * spacing_x)
nodes.append(
ReactFlowNode(
id=group_id,
id=category_id,
type="technique_group",
position={"x": x_pos, "y": 100},
data={
"label": config["name"],
"description": config["description"],
"color": config["color"],
"group": group_id,
"category": category_id,
"total": config["total"],
"sommelier_origin": config["sommelier_origin"],
"step": i + 1,
},
)
)
edges.append(
ReactFlowEdge(
id=f"edge-start-{group_id}",
id=f"edge-start-{category_id}",
source="start",
target=group_id,
target=category_id,
animated=True,
)
)
Comment on lines +229 to 255
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Step 1: Check the ReactFlowNode definition in backend/app/models/graph.py
echo "=== ReactFlowNode type definition ==="
cat -n backend/app/models/graph.py | grep -A 10 -B 5 "class ReactFlowNode"

# Step 2: Search for technique_group in the entire codebase
echo -e "\n=== All occurrences of technique_group ==="
rg -rn "technique_group" --type-list

# Step 3: Search for technique_group more broadly
echo -e "\n=== Technique group search ==="
rg "technique_group"

Repository: Two-Weeks-Team/somm.dev

Length of output: 1839


🏁 Script executed:

#!/bin/bash
# Step 4: Check if TECHNIQUE_CATEGORIES is defined and how it's structured
echo "=== TECHNIQUE_CATEGORIES definition ==="
rg -rn "TECHNIQUE_CATEGORIES" -A 5 -B 2 | head -100

# Step 5: Look for nodeTypes or type mappings in frontend
echo -e "\n=== Frontend nodeTypes handling ==="
find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) | head -20

Repository: Two-Weeks-Team/somm.dev

Length of output: 2808


🏁 Script executed:

#!/bin/bash
# Get the full ReactFlowNode class definition
echo "=== Full ReactFlowNode class definition ==="
sed -n '55,90p' backend/app/models/graph.py

# Check if there's a Literal type constraint
echo -e "\n=== Search for Literal type imports and usage ==="
rg "Literal" backend/app/models/graph.py -A 2 -B 2

# Check the test files to see what they're actually testing
echo -e "\n=== Test file content checking technique_group ==="
rg -A 5 -B 5 "technique_group" backend/tests/test_graph_api.py

Repository: Two-Weeks-Team/somm.dev

Length of output: 2457


🏁 Script executed:

#!/bin/bash
# Search for frontend node type handlers
echo "=== Frontend node type component handling ==="
rg "technique_group|nodeTypes" frontend/src --type ts --type tsx -C 3

# Check if there's a CustomNode or similar component
echo -e "\n=== Frontend node components ==="
find frontend/src -name "*Node*" -o -name "*node*" | grep -E "\.(ts|tsx)$"

Repository: Two-Weeks-Team/somm.dev

Length of output: 744


🏁 Script executed:

#!/bin/bash
# Search for nodeTypes mapping in frontend
echo "=== Frontend nodeTypes or node type mapping ==="
rg "nodeTypes|nodeType:" frontend/src -A 5 -B 2

# Check the graph component files
echo -e "\n=== Graph component files ==="
find frontend/src -path "*graph*" -name "*.tsx" | head -20

Repository: Two-Weeks-Team/somm.dev

Length of output: 5255


🏁 Script executed:

#!/bin/bash
# Check main graph rendering component
echo "=== Search for ReactFlow node initialization ==="
rg "Handle|nodeTypes" frontend/src -A 3 -B 1 | head -100

# Check if TechniqueGroupNode exists
echo -e "\n=== Check for TechniqueGroupNode ==="
find frontend/src -name "*Technique*" -o -name "*technique*" | grep -i node

Repository: Two-Weeks-Team/somm.dev

Length of output: 7513


🏁 Script executed:

#!/bin/bash
# Check for any fallback or error handling in graph components
echo "=== Check InteractiveGraphView for fallback ==="
cat frontend/src/components/result/InteractiveGraphView.tsx | head -50

# Search for any TechniqueGroup component (with various naming)
echo -e "\n=== Search for TechniqueGroup component ==="
find frontend/src -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) | xargs grep -l -i "techniquegroup\|technique.group\|technique_group" 2>/dev/null

# Check the actual nodeTypes export more thoroughly
echo -e "\n=== Full nodeTypes export ==="
cat frontend/src/components/graph/nodes/index.ts

Repository: Two-Weeks-Team/somm.dev

Length of output: 2117


🏁 Script executed:

#!/bin/bash
# Check if there's a generic fallback node component
echo "=== Check for generic or default node components ==="
ls -la frontend/src/components/graph/nodes/

# Check if ReactFlow has configuration for unknown node types
echo -e "\n=== Check configuration for missing node types ==="
rg "nodeTypes|errorBoundary|fallback" frontend/src/components --type ts --type tsx -C 2 | grep -A 5 "InteractiveGraphView\|Graph2DTab"

Repository: Two-Weeks-Team/somm.dev

Length of output: 956


🏁 Script executed:

#!/bin/bash
# Check how the graph is actually displayed - look for the complete component
echo "=== Check where nodeTypes is used in InteractiveGraphView ==="
grep -n "nodeTypes\|technique_group" frontend/src/components/result/InteractiveGraphView.tsx

# Check the backend test to understand what the expected behavior is
echo -e "\n=== Backend test detail checking technique_group ==="
cat backend/tests/test_graph_api.py | grep -A 20 "test_full_techniques_has_expected_technique_groups"

# Check if build_full_techniques_topology uses technique_group
echo -e "\n=== Check topology building functions ==="
rg "def build_full_techniques_topology" backend/app/services/graph_builder.py -A 30

Repository: Two-Weeks-Team/somm.dev

Length of output: 1935


🏁 Script executed:

#!/bin/bash
# Let's check if ReactFlow has any warning/error handling for missing node types
echo "=== Check ReactFlow configuration in full graph setup ==="
cat frontend/src/components/result/InteractiveGraphView.tsx | tail -50

# Verify the actual structure returned from backend
echo -e "\n=== Check if technique_group appears in API responses ==="
rg "technique_group" backend/app --type py -B 2 -A 2

Repository: Two-Weeks-Team/somm.dev

Length of output: 2733


프론트엔드에서 "technique_group" 노드를 렌더링할 수 없습니다.

backend/app/services/graph_builder.py의 코드에서 생성하는 type="technique_group" 노드는 frontend/src/components/graph/nodes/index.tsnodeTypes 매핑에 존재하지 않습니다. ReactFlow는 매핑되지 않은 노드 타입을 렌더링할 수 없으므로 그래프 시각화에서 이 노드들이 표시되지 않을 것입니다.

ReactFlowNode.type의 문서화된 범위("start" | "end" | "agent" | "technique" | "synthesis" | "process")를 준수하거나, 백엔드에서 생성하는 노드 타입이 프론트엔드의 nodeTypes에 등록되어 있는지 확인하십시오.

🤖 Prompt for AI Agents
In `@backend/app/services/graph_builder.py` around lines 226 - 252, The backend is
emitting nodes with ReactFlowNode.type="technique_group" which isn't registered
in the frontend nodeTypes (frontend/src/components/graph/nodes/index.ts), so
ReactFlow can't render them; fix by changing the created node type in
backend/app/services/graph_builder.py to one of the supported types (e.g.,
"technique" or another type present in nodeTypes) or ensure the frontend
registers "technique_group" in nodeTypes; locate the node creation in the loop
that constructs ReactFlowNode (look for id=category_id, type="technique_group",
data=...) and update the type string to a supported value or coordinate with
frontend to add the "technique_group" mapping.


# Second row (4 groups, steps 5-8)
for i, group_id in enumerate(group_ids[4:]):
config = TECHNIQUE_GROUPS[group_id]
for i, category_id in enumerate(category_ids[4:]):
config = TECHNIQUE_CATEGORIES[category_id]
x_pos = start_x + (i * spacing_x)
nodes.append(
ReactFlowNode(
id=group_id,
id=category_id,
type="technique_group",
position={"x": x_pos, "y": 220},
data={
"label": config["name"],
"description": config["description"],
"color": config["color"],
"group": group_id,
"category": category_id,
"total": config["total"],
"sommelier_origin": config["sommelier_origin"],
"step": i + 5,
},
)
)
edges.append(
ReactFlowEdge(
id=f"edge-start-{group_id}",
id=f"edge-start-{category_id}",
source="start",
target=group_id,
target=category_id,
animated=True,
)
)
Comment on lines +229 to 283

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

두 개의 for 루프가 거의 동일한 로직을 반복하고 있습니다. 코드를 더 간결하고 유지보수하기 쉽게 만들기 위해 이 두 루프를 하나로 합치거나 헬퍼 함수로 추출하는 것을 고려해 보세요. 예를 들어, 행(row)의 설정(y 위치, 스텝 시작 번호 등)을 담은 리스트를 순회하는 방식으로 리팩토링할 수 있습니다.

    rows = [
        {"ids": category_ids[:4], "y": 100, "step_start": 1},
        {"ids": category_ids[4:], "y": 220, "step_start": 5},
    ]

    for row in rows:
        for i, category_id in enumerate(row["ids"]):
            config = TECHNIQUE_CATEGORIES[category_id]
            x_pos = start_x + (i * spacing_x)
            nodes.append(
                ReactFlowNode(
                    id=category_id,
                    type="technique_group",
                    position={"x": x_pos, "y": row["y"]},
                    data={
                        "label": config["name"],
                        "description": config["description"],
                        "color": config["color"],
                        "category": category_id,
                        "total": config["total"],
                        "sommelier_origin": config["sommelier_origin"],
                        "step": i + row["step_start"],
                    },
                )
            )
            edges.append(
                ReactFlowEdge(
                    id=f"edge-start-{category_id}",
                    source="start",
                    target=category_id,
                    animated=True,
                )
            )

Expand All @@ -244,11 +297,11 @@ def build_full_techniques_topology() -> ReactFlowGraph:
)
)

for group_id in group_ids:
for category_id in category_ids:
edges.append(
ReactFlowEdge(
id=f"edge-{group_id}-synthesis",
source=group_id,
id=f"edge-{category_id}-synthesis",
source=category_id,
target="synthesis",
animated=True,
)
Expand Down
148 changes: 141 additions & 7 deletions backend/app/services/graph_builder_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
ExcludedVisualization,
ExcludedTechnique,
)
from app.techniques.registry import get_registry


LAYER_START = 0
Expand All @@ -44,14 +45,34 @@
("laurent", "Laurent", "#228B22"),
]

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

ENRICHMENT_STEPS = [
("code_analysis", "Code Analysis", "#9370DB"),
("rag", "RAG Context", "#6C3483"),
("web_search", "Web Search", "#8E44AD"),
]

DEFAULT_TECHNIQUES: list[tuple[str, str, str]] = []
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check DEFAULT_TECHNIQUES previous value and build_3d_graph calls
echo "=== build_3d_graph 호출 확인 ==="
rg -n "build_3d_graph\b" --type py -C 3

echo ""
echo "=== DEFAULT_TECHNIQUES 변경 확인 ==="
git log --oneline -10

echo ""
echo "=== 최근 변경사항 (git diff) ==="
git diff HEAD~1 -- backend/app/services/graph_builder_3d.py | head -100

Repository: Two-Weeks-Team/somm.dev

Length of output: 14239


🏁 Script executed:

#!/bin/bash
# Read the relevant sections of graph_builder_3d.py
echo "=== 파일의 처음 100줄 (DEFAULT_TECHNIQUES 포함) ==="
head -100 backend/app/services/graph_builder_3d.py | cat -n

echo ""
echo "=== _build_agent_nodes 함수 (149-165 근처) ==="
sed -n '140,180p' backend/app/services/graph_builder_3d.py | cat -n

echo ""
echo "=== build_3d_graph 함수 서명 및 처음 부분 ==="
sed -n '372,420p' backend/app/services/graph_builder_3d.py | cat -n

echo ""
echo "=== six_sommeliers 또는 six_sommeliers 관련 참조 ==="
rg -n "six_sommeliers" --type py -C 2

Repository: Two-Weeks-Team/somm.dev

Length of output: 50379


🏁 Script executed:

#!/bin/bash
# Check the technique nodes test and techniques registry
echo "=== test_technique_nodes_under_agents 구현 ==="
sed -n '339,356p' backend/tests/test_graph_3d.py | cat -n

echo ""
echo "=== _get_category_counts 및 registry 활용 ==="
sed -n '68,75p' backend/app/services/graph_builder_3d.py | cat -n

echo ""
echo "=== 기술 레지스트리 확인 ==="
rg -n "get_registry\|DEFAULT_TECHNIQUES" --type py backend/app/services/graph_builder_3d.py -B 2 -A 2

echo ""
echo "=== build_3d_graph 호출 시 include_techniques 기본값 ==="
sed -n '372,390p' backend/app/services/graph_builder_3d.py | cat -n

Repository: Two-Weeks-Team/somm.dev

Length of output: 2052


🏁 Script executed:

#!/bin/bash
# Check what the technique test actually validates
echo "=== 기술 노드 테스트가 실제로 통과하는지 확인 ==="
# Check if test has any assertions about the count
sed -n '339,360p' backend/tests/test_graph_3d.py

echo ""
echo "=== DEFAULT_TECHNIQUES 이전 값이나 초기화 예상값 찾기 ==="
# Look for any comments or TODOs about DEFAULT_TECHNIQUES
rg -n "DEFAULT_TECHNIQUES\|technique.*list\|TASTING_NOTE" backend/app/services/graph_builder_3d.py | head -20

echo ""
echo "=== _build_agent_nodes 전체 구현 ==="
sed -n '130,180p' backend/app/services/graph_builder_3d.py | cat -n

Repository: Two-Weeks-Team/somm.dev

Length of output: 3164


🏁 Script executed:

#!/bin/bash
# Check if techniques should come from a registry or other source
echo "=== get_registry 사용처 확인 ==="
rg -n "get_registry\|registry" backend/app/services/graph_builder_3d.py

echo ""
echo "=== 테스트 실행 결과 - test_technique_nodes_under_agents 실제 동작 ==="
# The test is vacuous - it only asserts IF techniques exist, but doesn't assert they SHOULD exist
# Let's check if there are any other places where DEFAULT_TECHNIQUES is modified or populated
rg -n "DEFAULT_TECHNIQUES\s*=" --type py

echo ""
echo "=== include_techniques 파라미터 사용처 ==="
rg -n "include_techniques" --type py backend/app/services/graph_builder_3d.py

echo ""
echo "=== 그래프 API에서 build_3d_graph 호출 방식 ==="
sed -n '330,350p' backend/app/api/routes/graph.py | cat -n

Repository: Two-Weeks-Team/somm.dev

Length of output: 945


🏁 Script executed:

#!/bin/bash
# Check if techniques should be populated from the registry or mode-based selection
echo "=== 기술 선택 로직 (technique_router 또는 technique_mappings) ==="
rg -n "get_techniques_for_mode\|select_techniques" --type py backend/app | head -30

echo ""
echo "=== full_techniques 모드에서 기술 처리 방식 ==="
rg -n "full_techniques\|TASTING_NOTE_CATEGORIES" --type py backend/app/services/graph_builder_3d.py -B 2 -A 2

echo ""
echo "=== 커밋 메시지로부터 변경사항 확인 ==="
git show --stat 17498bc

echo ""
echo "=== 누가 DEFAULT_TECHNIQUES를 비우려고 했는지 확인 ==="
git show 17498bc -- backend/app/services/graph_builder_3d.py | head -150

Repository: Two-Weeks-Team/somm.dev

Length of output: 33651


🏁 Script executed:

#!/bin/bash
# Check mode-specific behavior in build_3d_graph
echo "=== build_3d_graph 전체 구현 - 모드별 처리 ==="
sed -n '372,450p' backend/app/services/graph_builder_3d.py | cat -n

echo ""
echo "=== 테스트에서 mode 별 include_techniques 설정 ==="
grep -n "include_techniques" backend/tests/test_graph_3d.py | head -20

echo ""
echo "=== six_sommeliers 관련 3D 그래프 테스트 ==="
rg -n "six_sommeliers.*build_3d_graph\|build_3d_graph.*six_sommeliers" --type py backend/tests/

Repository: Two-Weeks-Team/somm.dev

Length of output: 3246


🏁 Script executed:

#!/bin/bash
# Final verification - check if this is intentional design
echo "=== build_3d_graph 호출 시 모드별 처리 (API) ==="
sed -n '330,345p' backend/app/api/routes/graph.py | cat -n

echo ""
echo "=== 모드 결정 로직 ==="
sed -n '300,330p' backend/app/api/routes/graph.py | cat -n

echo ""
echo "=== 3D 그래프 모드별 구현 요약 ==="
grep -n "def build_3d_graph\|def build_3d_graph_full_techniques\|def _build_agent_nodes\|def _build_category_nodes" backend/app/services/graph_builder_3d.py

echo ""
echo "=== 기본 3D 그래프가 technique 노드를 원래 가져야 하는지 확인 ==="
# Check if there's any documentation or comments about expected technique nodes in basic mode
rg -n "technique.*node\|기법.*노드" backend/app/services/graph_builder_3d.py | head -10

Repository: Two-Weeks-Team/somm.dev

Length of output: 2151


DEFAULT_TECHNIQUES가 비어있으면 기본 3D 그래프에서 에이전트 기법 노드가 생성되지 않습니다.

_build_agent_nodesuse_techniques=True 경로(Line 150-151)에서 DEFAULT_TECHNIQUES를 순회하므로, 빈 리스트인 경우 기법 노드가 전혀 생성되지 않습니다. API에서 build_3d_graph를 호출할 때 include_techniques 파라미터를 명시하지 않아 기본값 True가 사용되지만, 빈 DEFAULT_TECHNIQUES 때문에 결과적으로 기법 노드가 없는 그래프가 반환됩니다.

기본 모드(six_sommeliers, grand_tasting)에서 기법 노드가 필요하다면 DEFAULT_TECHNIQUES를 채우거나, 필요하지 않다면 include_techniques 기본값을 False로 변경하는 것을 검토하세요.

🤖 Prompt for AI Agents
In `@backend/app/services/graph_builder_3d.py` at line 65, DEFAULT_TECHNIQUES is
currently an empty list which causes _build_agent_nodes (when called with
use_techniques=True) to never create technique nodes, so either populate
DEFAULT_TECHNIQUES with the default technique tuples used by your default modes
(e.g., the tuples expected by _build_agent_nodes for
six_sommeliers/grand_tasting) or change the default of the
build_3d_graph/include_techniques parameter to False; specifically, update the
DEFAULT_TECHNIQUES constant to a non-empty list of (technique_id, display_name,
category) tuples or modify build_3d_graph (and any callers) so
include_techniques=False by default, and ensure _build_agent_nodes still
respects that default.



def _get_category_counts() -> dict[str, int]:
"""Get technique counts per category from registry, with fallback to static values."""
try:
registry = get_registry()
return registry.count_by_category()
except Exception:
return {cat_id: total for cat_id, _, _, total in TASTING_NOTE_CATEGORIES}
Comment on lines +73 to +74

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

except Exception:은 너무 광범위한 예외를 처리하므로, 의도치 않은 다른 오류까지 숨길 수 있습니다. get_registry()registry.count_by_category()에서 발생할 수 있는 더 구체적인 예외(예: TechniqueValidationError)를 명시적으로 처리하는 것이 좋습니다. 또한, 예외 발생 시 로그를 남기면 디버깅에 도움이 됩니다.



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


def _build_rag_node(step_number: int = 1) -> Graph3DNode:
"""Build the RAG enrichment node at layer 100."""
"""Build the RAG enrichment node at layer 100 (legacy single node)."""
return Graph3DNode(
node_id="rag_enrich",
node_type="rag",
Expand All @@ -77,6 +98,28 @@ def _build_rag_node(step_number: int = 1) -> Graph3DNode:
)


def _build_enrichment_nodes(start_step: int = 1) -> list[Graph3DNode]:
"""Build 3 enrichment nodes (code_analysis, rag, web_search) at layer 100."""
nodes = []
num_steps = len(ENRICHMENT_STEPS)
spacing = 100
start_x = CENTER_X - (num_steps - 1) * spacing / 2

for i, (step_id, label, color) in enumerate(ENRICHMENT_STEPS):
x_pos = start_x + i * spacing
nodes.append(
Graph3DNode(
node_id=step_id,
node_type="enrichment",
label=label,
position=Position3D(x=x_pos, y=0, z=LAYER_RAG),
color=color,
step_number=start_step + i,
)
)
return nodes


def _build_agent_nodes(
start_step: int = 2, use_techniques: bool = True
) -> list[Graph3DNode]:
Expand Down Expand Up @@ -124,6 +167,33 @@ def _build_agent_nodes(
return nodes


def _build_category_nodes(start_step: int = 4) -> list[Graph3DNode]:
"""Build 8 tasting note category nodes at layer 200 for full_techniques mode."""
nodes = []
num_categories = len(TASTING_NOTE_CATEGORIES)
total_width = (num_categories - 1) * AGENT_SPACING
start_x = CENTER_X - total_width / 2

dynamic_counts = _get_category_counts()

for i, (cat_id, label, color, static_total) in enumerate(TASTING_NOTE_CATEGORIES):
x_pos = start_x + i * AGENT_SPACING
technique_count = dynamic_counts.get(cat_id, static_total)
nodes.append(
Graph3DNode(
node_id=cat_id,
node_type="category",
label=label,
position=Position3D(x=x_pos, y=0, z=LAYER_AGENTS),
color=color,
step_number=start_step + i,
category=cat_id,
metadata={"total_techniques": technique_count},
)
)
return nodes
Comment on lines +170 to +194
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find and inspect Graph3DNode class definition
echo "=== Finding Graph3DNode class definition ==="
find . -name "*.py" -path "*/models/*" | head -20

echo ""
echo "=== Searching for Graph3DNode class ==="
rg -n "class Graph3DNode" --type py

echo ""
echo "=== Inspecting graph.py for Graph3DNode and metadata/extra config ==="
rg -A 50 "class Graph3DNode" backend/app/models/graph.py

Repository: Two-Weeks-Team/somm.dev

Length of output: 2400


🏁 Script executed:

# Check for Pydantic configuration and metadata field
echo "=== Checking for metadata field in Graph3DNode ==="
rg -n "metadata" backend/app/models/graph.py

echo ""
echo "=== Checking for Pydantic extra configuration ==="
rg -n "extra.*allow|ConfigDict|model_config" backend/app/models/graph.py

Repository: Two-Weeks-Team/somm.dev

Length of output: 1073


🏁 Script executed:

# Verify the actual code at line 191
echo "=== Verifying code snippet at lines 170-194 ==="
sed -n '170,194p' backend/app/services/graph_builder_3d.py

Repository: Two-Weeks-Team/somm.dev

Length of output: 1097


Graph3DNodemetadata 필드가 없어 Pydantic 검증 오류 발생

Line 191에서 metadata={"total_techniques": technique_count}를 전달하고 있지만, Graph3DNode 모델에는 이 필드가 정의되어 있지 않습니다. Pydantic v2는 정의되지 않은 필드를 거부하므로 런타임에 ValidationError가 발생합니다.

item_count 필드를 사용하세요:

-                metadata={"total_techniques": technique_count},
+                item_count=technique_count,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _build_category_nodes(start_step: int = 4) -> list[Graph3DNode]:
"""Build 8 tasting note category nodes at layer 200 for full_techniques mode."""
nodes = []
num_categories = len(TASTING_NOTE_CATEGORIES)
total_width = (num_categories - 1) * AGENT_SPACING
start_x = CENTER_X - total_width / 2
dynamic_counts = _get_category_counts()
for i, (cat_id, label, color, static_total) in enumerate(TASTING_NOTE_CATEGORIES):
x_pos = start_x + i * AGENT_SPACING
technique_count = dynamic_counts.get(cat_id, static_total)
nodes.append(
Graph3DNode(
node_id=cat_id,
node_type="category",
label=label,
position=Position3D(x=x_pos, y=0, z=LAYER_AGENTS),
color=color,
step_number=start_step + i,
category=cat_id,
metadata={"total_techniques": technique_count},
)
)
return nodes
def _build_category_nodes(start_step: int = 4) -> list[Graph3DNode]:
"""Build 8 tasting note category nodes at layer 200 for full_techniques mode."""
nodes = []
num_categories = len(TASTING_NOTE_CATEGORIES)
total_width = (num_categories - 1) * AGENT_SPACING
start_x = CENTER_X - total_width / 2
dynamic_counts = _get_category_counts()
for i, (cat_id, label, color, static_total) in enumerate(TASTING_NOTE_CATEGORIES):
x_pos = start_x + i * AGENT_SPACING
technique_count = dynamic_counts.get(cat_id, static_total)
nodes.append(
Graph3DNode(
node_id=cat_id,
node_type="category",
label=label,
position=Position3D(x=x_pos, y=0, z=LAYER_AGENTS),
color=color,
step_number=start_step + i,
category=cat_id,
item_count=technique_count,
)
)
return nodes
🤖 Prompt for AI Agents
In `@backend/app/services/graph_builder_3d.py` around lines 170 - 194, In
_build_category_nodes replace the undefined Graph3DNode metadata kwarg with the
expected item_count field: remove metadata={"total_techniques": technique_count}
and set item_count=technique_count when constructing Graph3DNode so the Pydantic
model (Graph3DNode) validates; ensure the value uses the technique_count
variable (int) and no other unknown kwargs are passed to Graph3DNode.



def _build_synthesis_node(step_number: int = 7) -> Graph3DNode:
"""Build the synthesis node at layer 300."""
return Graph3DNode(
Expand Down Expand Up @@ -356,6 +426,70 @@ def build_3d_graph(
)


def build_3d_graph_full_techniques(
evaluation_id: str,
methodology_trace: list | None = None,
) -> Graph3DPayload:
"""Build 3D graph for full_techniques mode with 8 categories and 3 enrichment steps.

Layered layout (z-axis):
- Layer 0 (z=0): Start node
- Layer 1 (z=100): 3 enrichment nodes (code_analysis, rag, web_search)
- Layer 2 (z=200): 8 tasting note categories
- Layer 3 (z=300): Synthesis node
- Layer 4 (z=400): End node
"""
nodes: list[Graph3DNode] = []
edges: list[Graph3DEdge] = []

nodes.append(_build_start_node(step_number=0))

enrichment_nodes = _build_enrichment_nodes(start_step=1)
nodes.extend(enrichment_nodes)

category_nodes = _build_category_nodes(start_step=4)
nodes.extend(category_nodes)

nodes.append(_build_synthesis_node(step_number=12))
nodes.append(_build_end_node(step_number=13))

edge_id = 0
for enrich in enrichment_nodes:
edges.append(
_create_styled_edge(f"edge_{edge_id}", "start", enrich.node_id, "flow", 0)
)
edge_id += 1

last_enrich = enrichment_nodes[-1]
for cat in category_nodes:
edges.append(
_create_styled_edge(
f"edge_{edge_id}", last_enrich.node_id, cat.node_id, "parallel", 3
)
)
edge_id += 1
Comment on lines +456 to +470
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enrichment 노드 간 연결이 불완전합니다 — code_analysisrag 노드가 아웃바운드 엣지 없이 끊어집니다.

현재 구현:

  • start → 3개 enrichment 노드 (모두 연결 ✓)
  • last_enrich (web_search)만 → 8개 카테고리 노드

code_analysisrag 노드는 아웃바운드 엣지가 없어 그래프에서 데드엔드가 됩니다. enrichment가 순차적 파이프라인이라면 start → code_analysis → rag → web_search → categories로 연결해야 하고, 병렬이라면 3개 모두 카테고리에 연결해야 합니다.

🔧 순차적 파이프라인 방식 수정 제안
     edge_id = 0
-    for enrich in enrichment_nodes:
-        edges.append(
-            _create_styled_edge(f"edge_{edge_id}", "start", enrich.node_id, "flow", 0)
-        )
-        edge_id += 1
+    # start → first enrichment
+    edges.append(
+        _create_styled_edge(f"edge_{edge_id}", "start", enrichment_nodes[0].node_id, "flow", 0)
+    )
+    edge_id += 1
+
+    # chain enrichment nodes sequentially
+    for i in range(len(enrichment_nodes) - 1):
+        edges.append(
+            _create_styled_edge(
+                f"edge_{edge_id}",
+                enrichment_nodes[i].node_id,
+                enrichment_nodes[i + 1].node_id,
+                "flow",
+                enrichment_nodes[i].step_number,
+            )
+        )
+        edge_id += 1

     last_enrich = enrichment_nodes[-1]
     for cat in category_nodes:
🔧 또는 병렬 방식 수정 제안 (모든 enrichment → categories)
-    last_enrich = enrichment_nodes[-1]
-    for cat in category_nodes:
-        edges.append(
-            _create_styled_edge(
-                f"edge_{edge_id}", last_enrich.node_id, cat.node_id, "parallel", 3
+    for enrich in enrichment_nodes:
+        for cat in category_nodes:
+            edges.append(
+                _create_styled_edge(
+                    f"edge_{edge_id}", enrich.node_id, cat.node_id, "parallel", enrich.step_number
+                )
             )
-        )
-        edge_id += 1
+            edge_id += 1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
edge_id = 0
for enrich in enrichment_nodes:
edges.append(
_create_styled_edge(f"edge_{edge_id}", "start", enrich.node_id, "flow", 0)
)
edge_id += 1
last_enrich = enrichment_nodes[-1]
for cat in category_nodes:
edges.append(
_create_styled_edge(
f"edge_{edge_id}", last_enrich.node_id, cat.node_id, "parallel", 3
)
)
edge_id += 1
edge_id = 0
# start → first enrichment
edges.append(
_create_styled_edge(f"edge_{edge_id}", "start", enrichment_nodes[0].node_id, "flow", 0)
)
edge_id += 1
# chain enrichment nodes sequentially
for i in range(len(enrichment_nodes) - 1):
edges.append(
_create_styled_edge(
f"edge_{edge_id}",
enrichment_nodes[i].node_id,
enrichment_nodes[i + 1].node_id,
"flow",
enrichment_nodes[i].step_number,
)
)
edge_id += 1
last_enrich = enrichment_nodes[-1]
for cat in category_nodes:
edges.append(
_create_styled_edge(
f"edge_{edge_id}", last_enrich.node_id, cat.node_id, "parallel", 3
)
)
edge_id += 1
🤖 Prompt for AI Agents
In `@backend/app/services/graph_builder_3d.py` around lines 456 - 470, The
enrichment nodes currently get inbound edges from "start" but no outbound links
for intermediate nodes (e.g., code_analysis, rag), so add linking logic after
the start->enrichment loop: either (A) sequential pipeline: iterate
enrichment_nodes and append _create_styled_edge(f"edge_{edge_id}",
enrichment_nodes[i].node_id, enrichment_nodes[i+1].node_id, "flow", 0) for each
adjacent pair (incrementing edge_id), then keep the existing edge from the final
enrichment to each category; or (B) parallel mode: for each enrich in
enrichment_nodes append edges from enrich.node_id to each cat.node_id using
_create_styled_edge(f"edge_{edge_id}", enrich.node_id, cat.node_id, "parallel",
3) (incrementing edge_id) so code_analysis and rag are not dead ends—use the
existing variables enrichment_nodes, category_nodes, _create_styled_edge, and
edge_id when inserting the new loops.

Comment on lines +463 to +470

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

현재 로직에서는 마지막 enrichment 노드만 카테고리 노드에 연결됩니다. full_techniques_graph.py의 langgraph 정의를 보면 모든 enrichment 노드가 모든 카테고리 노드에 연결되어야 합니다. 3D 그래프 시각화가 실제 실행 흐름을 정확하게 반영하도록 모든 enrichment 노드에서 각 카테고리 노드로 에지를 생성하도록 수정해야 합니다.

Suggested change
last_enrich = enrichment_nodes[-1]
for cat in category_nodes:
edges.append(
_create_styled_edge(
f"edge_{edge_id}", last_enrich.node_id, cat.node_id, "parallel", 3
)
)
edge_id += 1
for enrich in enrichment_nodes:
for cat in category_nodes:
edges.append(
_create_styled_edge(
f"edge_{edge_id}", enrich.node_id, cat.node_id, "parallel", 3
)
)
edge_id += 1


for cat in category_nodes:
edges.append(
_create_styled_edge(
f"edge_{edge_id}", cat.node_id, "synthesis", "flow", cat.step_number
)
)
edge_id += 1

edges.append(_create_styled_edge(f"edge_{edge_id}", "synthesis", "end", "flow", 12))

if methodology_trace:
assign_step_numbers(nodes, edges, methodology_trace)
Comment on lines +480 to +483
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

assign_step_numbers가 새로운 노드 타입(enrichment, category)을 처리하지 않습니다.

assign_step_numbers (Line 316-369)는 "agent", "technique", "rag_enrich" 노드만 매핑합니다. methodology_trace가 전달되더라도 enrichment/category 노드의 step_number는 업데이트되지 않습니다. 현재 기본 step 값이 유지되므로 즉시 문제가 되지는 않지만, trace 기반 애니메이션에서 순서가 맞지 않을 수 있습니다.

🤖 Prompt for AI Agents
In `@backend/app/services/graph_builder_3d.py` around lines 480 - 483,
assign_step_numbers currently only maps "agent", "technique", and "rag_enrich"
node types to methodology_trace entries, so nodes of types "enrichment" and
"category" never get their step_number updated; update the assign_step_numbers
function to recognize node["type"] == "enrichment" and node["type"] ==
"category" (alongside existing types), map them to the appropriate
methodology_trace entries using the same lookup logic used for
"rag_enrich"/"technique" (so they receive the correct step index), and set
node["data"]["step_number"] accordingly so trace-driven animations honor the
intended ordering for enrichment and category nodes in the nodes/edges lists
referenced where assign_step_numbers is called.


return Graph3DPayload.create(
evaluation_id=evaluation_id,
mode="full_techniques",
nodes=nodes,
edges=edges,
)


# =============================================================================
# Phase G4: FDEB Edge Bundling and Graph3DBuilder
# =============================================================================
Expand Down
4 changes: 2 additions & 2 deletions backend/tests/test_graph_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ def test_without_rag(self):
assert len(rag_nodes) == 0

def test_without_techniques(self):
"""Graph without techniques must have fewer nodes."""
"""Graph without techniques has same or fewer nodes (currently DEFAULT_TECHNIQUES is empty)."""
graph_with = build_3d_graph(
evaluation_id="eval_123",
mode="basic",
Expand All @@ -434,7 +434,7 @@ def test_without_techniques(self):
include_techniques=False,
)

assert len(graph_without.nodes) < len(graph_with.nodes)
assert len(graph_without.nodes) <= len(graph_with.nodes)

def test_different_modes(self):
"""Graph must work with different modes."""
Expand Down
Loading