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
2 changes: 2 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class Settings(BaseSettings):
POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str = "postgres"
POSTGRES_DB: str = "ai_vibe_coding_test"
# Spring BE / prod: public | 로컬 init-db.sql: ai_vibe_coding_test (.env에서만 오버라이드)
POSTGRES_SEARCH_PATH: str = "public"

# DB에 참가자 테이블이 `users` 인 경우(Core/구 스키마) `users` 로 설정.
# init-db.sql 기본은 `participants`.
Expand Down
17 changes: 13 additions & 4 deletions app/domain/langgraph/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@
from app.domain.langgraph.nodes.eval.n6_holistic_flow import eval_static_analysis
from app.domain.langgraph.nodes.eval.n7_aggregate_turn_scores import \
eval_code_agent
from app.domain.langgraph.nodes.eval.n8_code_execution import \
holistic_debate_flow
from app.domain.langgraph.nodes.eval.n8_code_execution import (
holistic_debate_flow,
holistic_debate_skipped_flow,
)
import logging
from app.domain.langgraph.nodes.eval.n9_final_scores import \
aggregate_final_scores
Expand Down Expand Up @@ -214,6 +216,12 @@ def create_main_graph(checkpointer: Optional[MemorySaver] = None) -> StateGraph:
"holistic_debate", # N8
wrap_eval_node_tracking("holistic_debate", holistic_debate_flow),
)
builder.add_node(
"holistic_debate_skipped", # N8 스킵 (0점·플레이스홀더)
wrap_eval_node_tracking(
"holistic_debate_skipped", holistic_debate_skipped_flow
),
)
builder.add_node(
"aggregate_final_scores", # N9
wrap_eval_node_tracking("aggregate_final_scores", aggregate_final_scores),
Expand Down Expand Up @@ -273,18 +281,19 @@ def create_main_graph(checkpointer: Optional[MemorySaver] = None) -> StateGraph:

builder.add_edge("summarize_memory", "handle_request")

# 제출 평가 파이프라인: N5 → N6 → N7 → (N4 turn_scores 있으면 N8) → N9 → END
# 제출 평가 파이프라인: N5 → N6 → N7 → (비가드레일 턴 있으면 N8, 없으면 N8 스킵) → N9 → END
builder.add_edge("eval_code_execution", "eval_static_analysis")
builder.add_edge("eval_static_analysis", "eval_code_agent")
builder.add_conditional_edges(
"eval_code_agent",
holistic_debate_router,
{
"holistic_debate": "holistic_debate",
"aggregate_final_scores": "aggregate_final_scores",
"holistic_debate_skipped": "holistic_debate_skipped",
},
)
builder.add_edge("holistic_debate", "aggregate_final_scores")
builder.add_edge("holistic_debate_skipped", "aggregate_final_scores")
builder.add_edge("aggregate_final_scores", END)

# 그래프 컴파일
Expand Down
7 changes: 5 additions & 2 deletions app/domain/langgraph/nodes/eval/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from app.domain.langgraph.nodes.eval.n6_holistic_flow import eval_static_analysis
from app.domain.langgraph.nodes.eval.n7_aggregate_turn_scores import \
eval_code_agent
from app.domain.langgraph.nodes.eval.n8_code_execution import \
holistic_debate_flow
from app.domain.langgraph.nodes.eval.n8_code_execution import (
holistic_debate_flow,
holistic_debate_skipped_flow,
)
from app.domain.langgraph.nodes.eval.n9_final_scores import \
aggregate_final_scores

Expand All @@ -16,5 +18,6 @@
"eval_static_analysis",
"eval_code_agent",
"holistic_debate_flow",
"holistic_debate_skipped_flow",
"aggregate_final_scores",
]
52 changes: 46 additions & 6 deletions app/domain/langgraph/nodes/eval/eval_turn_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from __future__ import annotations

from typing import Any, List, Mapping
from typing import Any, List, Mapping, Optional

from app.domain.langgraph.utils.guardrail_turns import get_guardrail_flag_turns


def eval_target_turn_numbers(current_turn: Any) -> List[int]:
Expand All @@ -18,12 +20,28 @@ def eval_target_turn_numbers(current_turn: Any) -> List[int]:
return list(range(1, ct))


def _turn_key_to_int(key: Any) -> Optional[int]:
try:
return int(key)
except (TypeError, ValueError):
return None


def _is_guardrail_turn_score(
turn: int, entry: Any, state: Mapping[str, Any]
) -> bool:
if turn in set(get_guardrail_flag_turns(state)):
return True
if isinstance(entry, dict) and entry.get("is_guardrail_failed"):
return True
return False


def has_prompt_turn_evaluations(state: Mapping[str, Any]) -> bool:
"""
N4가 실제로 저장한 턴 프롬프트 평가가 1건 이상인지.
N4가 저장한 turn_scores가 1건 이상인지 (가드레일 0점 포함).

turn_scores는 Redis turn_logs의 prompt_evaluation_details.score 기준으로
N4 종료 시 채워진다 (SAVE 턴·메시지 추출 실패 턴은 포함되지 않음).
N8 실행 여부는 has_non_guardrail_prompt_evaluations 를 사용한다.
"""
turn_scores = state.get("turn_scores")
if not isinstance(turn_scores, dict) or not turn_scores:
Expand All @@ -36,6 +54,28 @@ def has_prompt_turn_evaluations(state: Mapping[str, Any]) -> bool:
return False


def has_non_guardrail_prompt_evaluations(state: Mapping[str, Any]) -> bool:
"""
가드레일 턴을 제외한 프롬프트 턴 평가가 1건 이상인지.

N8(홀리스틱 토론)은 이 조건을 만족할 때만 LLM 토론을 실행한다.
"""
turn_scores = state.get("turn_scores")
if not isinstance(turn_scores, dict) or not turn_scores:
return False
for key, entry in turn_scores.items():
turn = _turn_key_to_int(key)
if turn is None:
continue
if _is_guardrail_turn_score(turn, entry, state):
continue
if isinstance(entry, dict) and entry.get("turn_score") is not None:
return True
if isinstance(entry, (int, float)):
return True
return False


def should_run_holistic_debate(state: Mapping[str, Any]) -> bool:
"""프롬프트 턴 평가가 있을 때만 N8(다중 에이전트 토론) 실행."""
return has_prompt_turn_evaluations(state)
"""비가드레일 프롬프트 턴이 1건 이상일 때만 N8 LLM 토론 실행."""
return has_non_guardrail_prompt_evaluations(state)
62 changes: 62 additions & 0 deletions app/domain/langgraph/nodes/eval/holistic_debate_skip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""N8 스킵 시 FE·rubric_json용 플레이스홀더 (LLM 토론 없음)."""

from __future__ import annotations

from typing import Any, Dict, List

HOLISTIC_DEBATE_SKIP_MESSAGE = "프롬프트 미제출로 평가 스킵"


def _skip_opinion(agent: str, round_num: int) -> Dict[str, Any]:
msg = HOLISTIC_DEBATE_SKIP_MESSAGE
return {
"agent": agent,
"round": round_num,
"stance": msg,
"key_points": [msg],
"suggested_score": 0.0,
"code_quality_assessment": msg,
"prompt_quality_assessment": msg,
}


def build_skipped_holistic_debate_result() -> Dict[str, Any]:
"""
가드레일만 있거나 평가 가능한 프롬프트 턴이 없을 때 N8 대체 출력.

holistic_flow_score·R4는 0, debate_log는 strict/advocate/neutral + verdict 형태.
"""
initial: List[Dict[str, Any]] = [
_skip_opinion("strict", 1),
_skip_opinion("advocate", 1),
_skip_opinion("neutral", 1),
]
rebuttals: List[Dict[str, Any]] = [
_skip_opinion("strict", 2),
_skip_opinion("advocate", 2),
_skip_opinion("neutral", 2),
]
msg = HOLISTIC_DEBATE_SKIP_MESSAGE
verdict = {
"agent": "verdict",
"round": 0,
"holistic_flow_score": 0.0,
"r4_context_maintenance_score": 0.0,
"grade": "F",
"consensus_summary": msg,
}
debate_log = initial + rebuttals + [verdict]
holistic_flow_analysis = (
f"[합의 요약] {msg}\n\n"
f"[종합 분석] {msg}\n\n"
f"[점수 근거] {msg}\n\n"
f"[R4 맥락 유지] 0.0/100 (turn_scores 궤적 기반 수석 심사관 산정)"
)
return {
"holistic_flow_score": 0.0,
"r4_context_maintenance_score": 0.0,
"holistic_flow_analysis": holistic_flow_analysis,
"debate_log": debate_log,
"debate_initial_opinions": initial,
"debate_rebuttals": rebuttals,
}
Loading
Loading