Skip to content

Feature/router unified routing#34

Merged
moonhwoo merged 8 commits into
developfrom
feature/router-unified-routing
Mar 13, 2026
Merged

Feature/router unified routing#34
moonhwoo merged 8 commits into
developfrom
feature/router-unified-routing

Conversation

@moonhwoo

@moonhwoo moonhwoo commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

Release Notes

  • Refactor

    • Intent 분류 시스템을 단순 선택에서 순위 기반 다중 의도 지원으로 개선했습니다.
    • 향상된 폴백 라우팅으로 더 나은 오류 처리 및 대체 경로 제공합니다.
  • Bug Fixes

    • 재시도 정책 최대 횟수를 2회에서 1회로 조정했습니다.
  • Chores

    • 처리 흐름 추적을 위한 내부 계측을 추가했습니다.

@moonhwoo moonhwoo closed this Mar 13, 2026
@moonhwoo moonhwoo reopened this Mar 13, 2026
@hwangtae123

Copy link
Copy Markdown
Contributor

황태토끼가 리뷰하러 찾아왔어요!
잠시만 기다려 주세요!

@moonhwoo

Copy link
Copy Markdown
Contributor Author

@coderabbitai review 리뷰해줘봐

@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown

@moonhwoo 네, 바로 PR을 리뷰하겠습니다! 잠시만 기다려 주세요 🐇

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown

개요

그래프 빌더의 모듈화 및 의도 기반 라우팅 시스템 재설계. 핵심 노드/엣지 등록 헬퍼 도입, 규칙 기반 의도 분류를 LLM 기반 랭킹으로 변경, 폴백 디스패치 라우터 추가, 및 각 노드의 실행 흐름 추적 메커니즘 강화.

변경 사항

코호트 / 파일 요약
그래프 빌더 모듈화
app/graph/graph_builder.py
핵심 노드/엣지 등록을 위한 헬퍼 함수(_register_core_nodes, _register_core_edges) 도입, 새로운 build_text_graph 공개 함수 추가. 기존 build_graph를 모듈화된 등록 로직으로 리팩토링.
의도 분류 및 라우팅 시스템 전면 재설계
app/graph/nodes/classify/intent_gate.py, app/graph/routers/intent_router.py, app/graph/routers/fallback_router.py
규칙 기반 의도 분류를 LLM 기반 다중 의도 랭킹으로 변경. 의도 상태를 단일값에서 intent_ranking 리스트와 current_intent_index로 변경. 새로운 fallback_dispatch_nodefallback_router 추가하여 의도 소진 시 폴백 처리.
상태 모델 업데이트
app/graph/state.py
intent, info_type 필드 제거. intent_ranking (List[str]), current_intent_index (int), fallback_next (str) 필드 추가.
실행 흐름 추적 메커니즘
app/graph/nodes/input/normalize_node.py, app/graph/nodes/input/stt_node.py, app/graph/nodes/rag/retrieve_node.py, app/graph/nodes/rag/retrieve_node_v2.py, app/graph/nodes/rag/translate_node.py, app/graph/nodes/rag/context_pack.py, app/graph/nodes/answer/answer_generate.py, app/graph/nodes/answer/answer_check.py, app/graph/nodes/answer/clarify_node.py, app/graph/nodes/output/tts_builder.py
각 노드에 trace["_flow"] 기반 실행 흐름 추적 추가. 기존 기능은 유지하되 흐름 메타데이터만 확대.
응답 생성 노드 간소화
app/graph/nodes/answer/answer_compose.py (삭제), app/graph/nodes/answer/direct_llm_node.py (삭제)
LLM 기반 자연어 응답 생성 노드 2개 제거. 이들의 기능은 다른 경로(smalltalk, struct_db, RAG)로 통합.
분류 및 쿼리 노드 제거
app/graph/nodes/classify/infotype_gate.py (삭제), app/graph/nodes/rag/query_rewrite.py (삭제)
정보 타입 기반 라우팅 및 쿼리 재작성 노드 제거. 의도 랭킹 시스템으로 통합.
구조화된 데이터베이스 및 이벤트 노드 변경
app/graph/nodes/tool/struct_db_node.py, app/graph/nodes/tool/event_node.py (신규), app/graph/nodes/tool/map_tool_node.py (삭제)
struct_db_node 응답 구조 변경 (stub db_result 제거, answer_text/check_result/trace 반환). 새로운 event_node 추가. 기존 map_tool_node 제거.
라우터 업데이트
app/graph/routers/answer_check_router.py, app/graph/routers/infotype_router.py (삭제)
answer_check_router가 "good"/"bad"를 모두 "fallback_dispatch"로 라우팅하도록 변경. infotype_router 제거.
소형토크 노드 확장
app/graph/nodes/answer/smalltalk_node.py
예외 처리 로직 변경하여 check_result 필드 추가 및 반환 구조 확대.

시퀀스 다이어그램

sequenceDiagram
    actor User
    participant intent_gate as intent_gate<br/>(LLM Ranking)
    participant fallback_dispatch as fallback_dispatch<br/>(Intent Processor)
    participant rag_path as RAG Path<br/>(retrieve/translate_ko)
    participant other_intents as Other Intents<br/>(smalltalk/event/struct_db)
    participant clarify as clarify<br/>(Final Fallback)

    User->>intent_gate: normalized_text
    intent_gate-->>intent_gate: LLM rank intents<br/>[rag, struct_db, ...]
    intent_gate->>fallback_dispatch: intent_ranking, index=0

    fallback_dispatch->>fallback_dispatch: check_result?
    
    alt check_result == "good"
        fallback_dispatch-->>User: fallback_next="done"<br/>(Return answer)
    else check_result != "good" & has next intent
        fallback_dispatch->>fallback_dispatch: advance to next intent<br/>(index+1)
        fallback_dispatch->>fallback_dispatch: determine next_intent<br/>from ranking[index]
        
        alt next_intent == "rag"
            fallback_dispatch->>rag_path: fallback_next=retrieve<br/>or translate_ko
            rag_path-->>fallback_dispatch: RAG result + trace
        else next_intent in {smalltalk, event, struct_db}
            fallback_dispatch->>other_intents: fallback_next=next_intent
            other_intents-->>fallback_dispatch: result + trace
        end
        
        fallback_dispatch->>fallback_dispatch: append to<br/>fallback_history
        fallback_dispatch-->>fallback_dispatch: next iteration
    else all intents exhausted
        fallback_dispatch->>clarify: fallback_next="clarify"
        clarify-->>User: clarification prompt
    end
Loading

예상 코드 리뷰 시간

🎯 4 (복잡) | ⏱️ ~45분

시 🐰

의도를 랭킹하고, 폴백으로 되돌아와,
흐름을 추적하며 진군해,
낡은 규칙은 버리고 LLM의 지혜로,
노드는 더 가볍게, 라우터는 똑똑하게,
우리의 그래프가 한 계단 올랐네! 🚀✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/router unified routing' accurately reflects the main objective of the PR, which consolidates routing logic through unified intent ranking and fallback dispatch mechanisms.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/router-unified-routing
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@moonhwoo

Copy link
Copy Markdown
Contributor Author

황태토끼가 리뷰하러 찾아왔어요! 잠시만 기다려 주세요!

일해주세요

@hwangtae123

hwangtae123 commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

리뷰 의견

  1. trace와 실제 ranking 불일치
    84-88행의 trace["intent_gate"]에는 슬라이싱 전 4개 전체 ranking이 기록되지만, 실제 반환되는 intent_ranking은 2개입니다. 디버깅 시 혼동될 수 있으므로, trace에도 잘린 결과를 기록하거나 tried_ranking 같은 필드를 별도로 남기는 게 좋겠습니다.

제안: trace에 실제 사용되는 ranking도 기록
trace["intent_gate"] = {
"text": text,
"ranking_full": ranking, # 전체 4개
"ranking_tried": ranking[:_MAX_INTENTS], # 실제 시도할 2개
"method": method,
}
ranking = ranking[:_MAX_INTENTS]
2. 매직넘버 → 상수화는 잘 됨
_MAX_INTENTS = 2로 상수화한 것은 좋습니다. 나중에 조정이 필요할 때 한 곳만 수정하면 됩니다.

  1. 주석의 범위 표기
    상위 N개 의도만 시도 (1~4) — 실제로 0도 가능하지만 의미가 없으므로, 런타임 안전을 위해 _MAX_INTENTS가 1 미만일 때의 방어 코드는 없습니다. 현재는 상수로 관리하니 큰 문제는 아니지만 참고 사항입니다.

  2. fallback 경로에서의 동작
    LLM 실패 시 _DEFAULT_RANKING이 그대로 사용되고 이후 슬라이싱으로 ["rag", "struct_db"]만 남게 됩니다. fallback에서도 상위 2개만 시도하는 것이 의도된 동작인지 확인이 필요합니다.

@hwangtae123

hwangtae123 commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

총평

작고 명확한 변경이며, 불필요한 하위 의도 시도를 줄여 응답 속도를 개선하려는 의도가 잘 드러납니다. trace 불일치 부분만 보완하면 좋겠습니다.

@hwangtae123

Copy link
Copy Markdown
Contributor

추가 발견 사항

1. fallback 기회가 4회 → 2회로 줄어들면서 clarify 도달이 너무 빨라짐

fallback_router.py:40에서 next_index >= len(ranking)이면 clarify로 빠집니다. 이전에는 4개 의도를 모두 시도한 후 clarify였지만, 이제 2번만 실패하면 바로 clarify입니다. 사용자 입장에서 답변을 못 받는 케이스가 늘어날 수 있습니다.

2. LLM이 순위를 잘못 매기면 올바른 의도가 아예 시도되지 않음

예를 들어 LLM이 ["smalltalk", "event", "rag", "struct_db"]를 반환하면, 슬라이싱 후 ["smalltalk", "event"]만 남습니다. 실제 정답이 rag나 struct_db였다면 시도조차 하지 않고 clarify로 빠지게 됩니다. 4개를 모두 시도할 때는 순위가 틀려도 결국 정답에 도달했지만, 2개 제한에서는 LLM 분류 정확도에 대한 의존도가 크게 높아집니다.

3. docstring과 실제 동작 불일치

intent_gate.py:16-17의 docstring:

4개 의도(rag, smalltalk, event, struct_db)를 순위로 반환.

실제로는 2개만 반환합니다. 또한 state.py:22의 주석도 4개를 가정하고 있습니다:

intent_ranking: List[str] # ["rag", "event", "struct_db", "smalltalk"] 순위

4. 유효성 검증 로직과의 비효율

intent_gate.py:70-73에서 누락된 의도를 보충하여 4개를 맞추는 로직이 있는데, 어차피 뒤에서 2개로 잘리므로 3번째·4번째에 추가된 의도는 의미 없는 연산입니다. 치명적이진 않지만, 코드 의도가 모호해집니다.

5. _MAX_INTENTS를 환경변수/설정으로 빼지 않은 점

현재 하드코딩 상수라 값을 바꾸려면 코드 수정 + 재배포가 필요합니다. 이 값은 정확도-속도 트레이드오프에 해당하므로, 튜닝 시 환경변수나 설정 파일로 관리하면 실험이 훨씬 수월합니다.

가장 중요한 것은 1번과 2번입니다. 의도 분류 LLM의 Top-2 정확도가 충분히 높은지 검증 없이 적용하면, 기존에 잘 동작하던 케이스에서 clarify가 많이 발생할 수 있습니다.

@moonhwoo

Copy link
Copy Markdown
Contributor Author
  1. 혼동될 수 있으므로, trace에도 잘린 결과를 기록하거나 tried_ranking 같은 필드를 별도로 남기는 게 좋겠습니

1번은 의도적으로 변경가능하게 한거야
4번도 의도적인 top-2만 타는거야

@hwangtae123

Copy link
Copy Markdown
Contributor

의도하신 내용이였군요! 확인해주셔서 감사합니다!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/graph/nodes/answer/answer_generate.py (1)

108-126: ⚠️ Potential issue | 🟠 Major

LLM 예외가 성공 응답처럼 종료될 수 있습니다.

Line 110에서 llm.chat()이 실패해도 여기서는 일반 오류 문구만 answer_text에 넣고 끝납니다. 그러면 뒤의 answer_check_node가 내부 생성 실패를 구분하지 못해 "good"으로 종료할 수 있으니, 명시적 실패 플래그를 state/trace에 남기고 bad로 처리되게 연결하는 편이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/nodes/answer/answer_generate.py` around lines 108 - 126, When
llm.chat(...) in answer_generate fails the code currently returns an error
string as answer_text which downstream nodes (like answer_check_node) may treat
as a successful response; modify the except block in answer_generate so it sets
a clear failure flag in the trace/state (e.g.,
trace["answer_generate"]["failed"] = True or state["answer_generate_failed"] =
True) and still populate answer_text with the error message or empty string,
then return that trace; ensure the failure flag is added to the trace dict built
just before return so downstream logic can detect the failure and mark the flow
as "bad".
🧹 Nitpick comments (4)
app/graph/nodes/rag/translate_node.py (1)

46-49: Flow 추적 이름과 trace 메타데이터 키의 일관성 검토.

_flow에는 "translate_ko"를 추가하고 있지만, trace 메타데이터 키는 "translate"를 사용하고 있습니다. 다른 노드들은 동일한 이름을 사용합니다 (예: "retrieve"trace["retrieve"]). 일관성을 위해 둘 중 하나로 통일하는 것을 고려해 주세요.

♻️ 일관성 개선 제안
     flow = list(trace.get("_flow") or [])
     flow.append("translate_ko")
     trace["_flow"] = flow
-    trace["translate"] = {
+    trace["translate_ko"] = {
         "original": text,
         "user_language": user_language,
         "retrieval_query_ko": retrieval_query_ko,
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/nodes/rag/translate_node.py` around lines 46 - 49, The flow name
added to trace["_flow"] ("translate_ko") is inconsistent with the trace metadata
key trace["translate"]; update either the flow entry or the metadata key so they
match (e.g., change trace["_flow"].append("translate_ko") to append("translate")
or rename trace["translate"] to trace["translate_ko"]) in translate_node.py so
the flow trace and the node metadata key are consistent; adjust any downstream
usages of trace["translate"] or the flow name accordingly.
app/graph/nodes/rag/retrieve_node_v2.py (1)

20-55: MMR 로직 중복에 대한 선택적 리팩토링 고려.

_cosine_sim_mmr_select 함수가 retrieve_node.py와 동일하게 중복 구현되어 있습니다. 향후 유지보수 시 두 파일을 동시에 수정해야 할 수 있습니다. 공통 유틸리티 모듈로 추출하는 것을 고려해 볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/nodes/rag/retrieve_node_v2.py` around lines 20 - 55, The
_cosine_sim and _mmr_select implementations are duplicated; extract them into a
shared utility (e.g., a new module like emb_utils or graph.utils) and replace
the local definitions in retrieve_node_v2.py and retrieve_node.py with imports
from that module; ensure the shared functions keep the same signatures
(_cosine_sim(query_emb, doc_emb) and _mmr_select(query_emb, doc_embs, chunks,
top_k, lambda_param=0.5)), update any references, run/adjust tests and linters,
and remove the duplicate local implementations to avoid divergence.
app/graph/nodes/answer/smalltalk_node.py (1)

50-52: 예외 발생 시 로깅 추가를 권장합니다.

현재 Exception을 catch하고 있지만 로깅 없이 조용히 실패 처리됩니다. 디버깅 및 운영 모니터링을 위해 예외 정보를 trace에 기록하거나 로깅하는 것이 좋습니다.

♻️ 예외 로깅 추가 제안
+        import logging
+        logger = logging.getLogger(__name__)
+
         check_result = "good"
         try:
             answer = llm.chat(messages, max_tokens=100)
-        except Exception:
+        except Exception as e:
+            logger.warning("smalltalk LLM call failed: %s", e)
             answer = ""
             check_result = "bad"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/nodes/answer/smalltalk_node.py` around lines 50 - 52, The except
block in smalltalk_node.py currently swallows all exceptions (except Exception:)
and sets answer/ check_result without logging; modify the exception handler in
the function containing that block to log the exception details (use the module
logger.exception(...) or logging.exception(...) to capture message and stack
trace) before setting answer = "" and check_result = "bad", so failures are
recorded for debugging and monitoring.
app/graph/nodes/tool/event_node.py (1)

5-5: TODO 항목에 대해 도움을 드릴 수 있습니다.

이벤트 DB 테이블 구현 시 이 노드의 내부 로직을 교체해야 합니다. 이벤트 조회 및 LLM 기반 답변 생성 로직 구현에 도움이 필요하시면 말씀해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/nodes/tool/event_node.py` at line 5, The TODO comment indicates
this node is a placeholder and must be replaced with real event DB access and
LLM response generation; update the EventNode implementation to query the new
events table instead of the placeholder logic, implementing a function/method
like get_events (or fetch_events) that uses your DB client to retrieve events by
id/time/filters, and replace the placeholder response generation in methods such
as handle_request or generate_answer to call the LLM with the fetched event data
and return structured results; ensure you wire in the existing DB
client/connection, add error handling and tests for EventNode.get_events and
EventNode.generate_answer, and remove the TODO comment once the real logic is in
place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/graph/graph_builder.py`:
- Around line 73-75: The event_node and struct_db_node currently return
check_result="bad" in their stub implementations; update the actual
data-fetching logic inside the event_node and struct_db_node handlers so that on
successful lookup/validation they emit or return check_result="good" (preserving
existing error/bad paths), ensuring compatibility with fallback_dispatch_node
which routes to "done" only when check_result="good"; locate the implementations
referenced as event_node and struct_db_node in graph_builder.py and change their
success branch to set check_result="good" while leaving failure branches as
"bad".

In `@app/graph/nodes/classify/intent_gate.py`:
- Around line 71-75: Normalize LLM-provided ranking items with strip().lower()
and remove duplicates while preserving order before filtering against
_ALLOWED_INTENTS: transform the incoming ranking list into a normalized_dedup
list (e.g., [s.strip().lower() for s in ranking] then dedupe preserving first
occurrence), then compute valid = [r for r in normalized_dedup if r in
_ALLOWED_INTENTS], missing = [i for i in _DEFAULT_RANKING if i not in valid],
and set ranking = valid + missing; keep the existing method = "llm_partial_fix"
assignment when len(valid) < len(_ALLOWED_INTENTS). Apply the same normalization
+ dedupe step to the other place where ranking is truncated/used (the later
block that slices or selects top intents) so duplicates/case/whitespace variants
cannot cause repeated intents to be tried.

In `@app/graph/nodes/tool/struct_db_node.py`:
- Around line 19-30: The current stub in struct_db_node.py always returns
check_result="bad", which prevents the struct_db intent from ever winning in
intent_router/graph_builder; update the stub behavior so it doesn't permanently
fail: either set check_result to a neutral value (e.g., "skip" or "defer") and
add trace["struct_db"]["note"]="stub - excluded from ranking" so intent_router
can ignore it, or return check_result="good" with a user-facing placeholder
answer_text explaining DB is not available (e.g., "Temporary: DB lookup not
available") and keep trace entries as-is; modify the return object in the
struct_db block accordingly and ensure trace["_flow"] still appends "struct_db"
so routing logic can see the decision.

---

Outside diff comments:
In `@app/graph/nodes/answer/answer_generate.py`:
- Around line 108-126: When llm.chat(...) in answer_generate fails the code
currently returns an error string as answer_text which downstream nodes (like
answer_check_node) may treat as a successful response; modify the except block
in answer_generate so it sets a clear failure flag in the trace/state (e.g.,
trace["answer_generate"]["failed"] = True or state["answer_generate_failed"] =
True) and still populate answer_text with the error message or empty string,
then return that trace; ensure the failure flag is added to the trace dict built
just before return so downstream logic can detect the failure and mark the flow
as "bad".

---

Nitpick comments:
In `@app/graph/nodes/answer/smalltalk_node.py`:
- Around line 50-52: The except block in smalltalk_node.py currently swallows
all exceptions (except Exception:) and sets answer/ check_result without
logging; modify the exception handler in the function containing that block to
log the exception details (use the module logger.exception(...) or
logging.exception(...) to capture message and stack trace) before setting answer
= "" and check_result = "bad", so failures are recorded for debugging and
monitoring.

In `@app/graph/nodes/rag/retrieve_node_v2.py`:
- Around line 20-55: The _cosine_sim and _mmr_select implementations are
duplicated; extract them into a shared utility (e.g., a new module like
emb_utils or graph.utils) and replace the local definitions in
retrieve_node_v2.py and retrieve_node.py with imports from that module; ensure
the shared functions keep the same signatures (_cosine_sim(query_emb, doc_emb)
and _mmr_select(query_emb, doc_embs, chunks, top_k, lambda_param=0.5)), update
any references, run/adjust tests and linters, and remove the duplicate local
implementations to avoid divergence.

In `@app/graph/nodes/rag/translate_node.py`:
- Around line 46-49: The flow name added to trace["_flow"] ("translate_ko") is
inconsistent with the trace metadata key trace["translate"]; update either the
flow entry or the metadata key so they match (e.g., change
trace["_flow"].append("translate_ko") to append("translate") or rename
trace["translate"] to trace["translate_ko"]) in translate_node.py so the flow
trace and the node metadata key are consistent; adjust any downstream usages of
trace["translate"] or the flow name accordingly.

In `@app/graph/nodes/tool/event_node.py`:
- Line 5: The TODO comment indicates this node is a placeholder and must be
replaced with real event DB access and LLM response generation; update the
EventNode implementation to query the new events table instead of the
placeholder logic, implementing a function/method like get_events (or
fetch_events) that uses your DB client to retrieve events by id/time/filters,
and replace the placeholder response generation in methods such as
handle_request or generate_answer to call the LLM with the fetched event data
and return structured results; ensure you wire in the existing DB
client/connection, add error handling and tests for EventNode.get_events and
EventNode.generate_answer, and remove the TODO comment once the real logic is in
place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cc851ee7-4208-4d3e-8750-a286234818d8

📥 Commits

Reviewing files that changed from the base of the PR and between e518d6e and f9c8228.

📒 Files selected for processing (25)
  • app/graph/graph_builder.py
  • app/graph/nodes/answer/answer_check.py
  • app/graph/nodes/answer/answer_compose.py
  • app/graph/nodes/answer/answer_generate.py
  • app/graph/nodes/answer/clarify_node.py
  • app/graph/nodes/answer/direct_llm_node.py
  • app/graph/nodes/answer/smalltalk_node.py
  • app/graph/nodes/classify/infotype_gate.py
  • app/graph/nodes/classify/intent_gate.py
  • app/graph/nodes/input/normalize_node.py
  • app/graph/nodes/input/stt_node.py
  • app/graph/nodes/output/tts_builder.py
  • app/graph/nodes/rag/context_pack.py
  • app/graph/nodes/rag/query_rewrite.py
  • app/graph/nodes/rag/retrieve_node.py
  • app/graph/nodes/rag/retrieve_node_v2.py
  • app/graph/nodes/rag/translate_node.py
  • app/graph/nodes/tool/event_node.py
  • app/graph/nodes/tool/map_tool_node.py
  • app/graph/nodes/tool/struct_db_node.py
  • app/graph/routers/answer_check_router.py
  • app/graph/routers/fallback_router.py
  • app/graph/routers/infotype_router.py
  • app/graph/routers/intent_router.py
  • app/graph/state.py
💤 Files with no reviewable changes (6)
  • app/graph/nodes/answer/direct_llm_node.py
  • app/graph/routers/infotype_router.py
  • app/graph/nodes/rag/query_rewrite.py
  • app/graph/nodes/tool/map_tool_node.py
  • app/graph/nodes/answer/answer_compose.py
  • app/graph/nodes/classify/infotype_gate.py

Comment on lines +73 to +75
builder.add_edge("smalltalk", "fallback_dispatch")
builder.add_edge("event", "fallback_dispatch")
builder.add_edge("struct_db", "fallback_dispatch")

Copy link
Copy Markdown

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
set -euo pipefail

for f in \
  app/graph/nodes/answer/smalltalk_node.py \
  app/graph/nodes/tool/event_node.py \
  app/graph/nodes/tool/struct_db_node.py \
  app/graph/routers/fallback_router.py
do
  echo "===== $f ====="
  sed -n '1,220p' "$f"
  echo
done

Repository: Project-Guideon/guideon-AI

Length of output: 7112


event_nodestruct_db_node는 실제 구현 시 성공 반환값에 check_result="good"을 반드시 추가하세요.

현재 상태:

  • smalltalk_node: ✓ 성공 시 check_result="good" 설정 (line 45)
  • event_node: 현재 stub (TODO)으로 항상 check_result="bad" 반환 → 실제 데이터 조회 로직 추가 시 성공 케이스에서 "good" 설정 필요
  • struct_db_node: 현재 stub (TODO)으로 항상 check_result="bad" 반환 → 실제 DB 조회 로직 추가 시 성공 케이스에서 "good" 설정 필요

fallback_dispatch_node의 로직은 올바릅니다: check_result="good"이면 "done", 아니면 다음 의도로 재라우팅됩니다. 따라서 두 노드를 완성할 때 필수적으로 성공 경로에 check_result="good"을 추가해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/graph_builder.py` around lines 73 - 75, The event_node and
struct_db_node currently return check_result="bad" in their stub
implementations; update the actual data-fetching logic inside the event_node and
struct_db_node handlers so that on successful lookup/validation they emit or
return check_result="good" (preserving existing error/bad paths), ensuring
compatibility with fallback_dispatch_node which routes to "done" only when
check_result="good"; locate the implementations referenced as event_node and
struct_db_node in graph_builder.py and change their success branch to set
check_result="good" while leaving failure branches as "bad".

Comment thread app/graph/nodes/classify/intent_gate.py Outdated
Comment on lines +19 to +30
# ── stub: 데이터 없음 → check_result = "bad" ─────────────────────
trace = dict(state.get("trace") or {})
trace["struct_db"] = {"status": "stub", "query": text}

return {"db_result": db_result, "trace": trace}
flow = list(trace.get("_flow") or [])
flow.append("struct_db")
trace["_flow"] = flow
trace["struct_db"] = {"status": "stub", "query": text, "site_id": site_id}

return {
"answer_text": "",
"check_result": "bad",
"trace": trace,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

현재 stub이 실제 라우팅에 노출되어 있습니다.

intent_routergraph_builder에서 struct_db가 실제 분기 대상이라, 이 구현처럼 항상 check_result="bad"를 반환하면 struct_db 의도는 절대 성공할 수 없습니다. 실제 DB 연결 전까지는 최소한 이 intent를 ranking에서 제외하거나, 사용자에게 임시 안내를 good으로 반환하는 방어가 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/nodes/tool/struct_db_node.py` around lines 19 - 30, The current
stub in struct_db_node.py always returns check_result="bad", which prevents the
struct_db intent from ever winning in intent_router/graph_builder; update the
stub behavior so it doesn't permanently fail: either set check_result to a
neutral value (e.g., "skip" or "defer") and add trace["struct_db"]["note"]="stub
- excluded from ranking" so intent_router can ignore it, or return
check_result="good" with a user-facing placeholder answer_text explaining DB is
not available (e.g., "Temporary: DB lookup not available") and keep trace
entries as-is; modify the return object in the struct_db block accordingly and
ensure trace["_flow"] still appends "struct_db" so routing logic can see the
decision.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/graph/nodes/classify/intent_gate.py (1)

10-10: _MAX_INTENTS를 설정값으로 노출하면 운영 튜닝이 훨씬 쉬워집니다.

현재 값이 하드코딩이라 배포 없이 실험이 어렵습니다. 환경변수/설정으로 빼고 1~4 범위로 clamp 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/nodes/classify/intent_gate.py` at line 10, Make _MAX_INTENTS
configurable instead of hardcoded: expose it via a configuration or environment
variable (e.g., read from os.getenv or the app config) and replace the literal
_MAX_INTENTS usage with that value; when parsing, coerce/convert to int and
clamp it to the allowed range 1..4 (use min/max or a clamp helper) so callers of
the intent gate (symbol _MAX_INTENTS in intent_gate.py) always get a value
between 1 and 4; add a sensible default of 2 if the config/ENV is missing or
invalid.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/graph/nodes/classify/intent_gate.py`:
- Around line 14-18: The docstring and function contract in intent_gate.py are
inconsistent: update the docstring to state that the node returns a top-N list
(not "4개 의도") and revise the implementation to explicitly separate and persist
both the full LLM ranking and the top-N tried ranking; specifically, add/ensure
two distinct variables/keys (ranking_full and ranking_tried) are produced and
stored in the trace/log, and make the function return only the top-N list while
keeping ranking_full in the trace for debugging; apply the same change to the
analogous block around the code referenced at lines 91-103 so both docstrings,
return values, and trace keys match (use the existing names ranking_full and
ranking_tried to locate the logic).
- Around line 83-85: The current broad except in intent_gate.py (around the
llm.chat/json.loads handling) hides root causes; replace the single "except
Exception" that sets ranking=_DEFAULT_RANKING and method="llm_fallback_default"
with multiple specific except clauses for json.JSONDecodeError,
connection/timeouts (APIConnectionError, APITimeoutError), RateLimitError, and a
general APIError, and in each branch set ranking=_DEFAULT_RANKING, a distinct
method string (e.g., "llm_fallback_invalid_json",
"llm_fallback_connection_error", "llm_fallback_rate_limit",
"llm_fallback_api_error"), and record the exception text into
trace["intent_gate"]["error"]; ensure this handling wraps the llm.chat(...) and
json.loads(...) logic where ranking, method, trace, and _DEFAULT_RANKING are
used so errors are categorized for later analysis.

---

Nitpick comments:
In `@app/graph/nodes/classify/intent_gate.py`:
- Line 10: Make _MAX_INTENTS configurable instead of hardcoded: expose it via a
configuration or environment variable (e.g., read from os.getenv or the app
config) and replace the literal _MAX_INTENTS usage with that value; when
parsing, coerce/convert to int and clamp it to the allowed range 1..4 (use
min/max or a clamp helper) so callers of the intent gate (symbol _MAX_INTENTS in
intent_gate.py) always get a value between 1 and 4; add a sensible default of 2
if the config/ENV is missing or invalid.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e9dcd4a5-f03c-4468-bac2-36104994fe91

📥 Commits

Reviewing files that changed from the base of the PR and between f9c8228 and 5990254.

📒 Files selected for processing (1)
  • app/graph/nodes/classify/intent_gate.py

Comment on lines +14 to +18
"""LLM-only 의도 분류 노드 팩토리.

4개 의도(rag, smalltalk, event, struct_db)를 순위로 반환.
키워드 룰 없이 LLM 한 번 호출로 분류 + 순위 결정.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

함수 계약(설명/trace/반환값) 불일치로 디버깅 혼선이 생깁니다.

설명은 “4개 의도 반환”인데 실제 반환은 top-N이고, trace에는 전체 ranking만 기록됩니다. ranking_full/ranking_tried를 분리해 명시적으로 남기고 docstring도 top-N 기준으로 맞추는 게 안전합니다.

제안 수정
-    4개 의도(rag, smalltalk, event, struct_db)를 순위로 반환.
+    4개 의도(rag, smalltalk, event, struct_db)를 순위화하고 상위 N개를 반환.

-        trace["intent_gate"] = {
-            "text": text,
-            "ranking": ranking,
-            "method": method,
-        }
-
-        # 상위 N개 의도만 시도
-        ranking = ranking[:_MAX_INTENTS]
+        ranking_full = ranking
+        ranking_tried = ranking_full[:_MAX_INTENTS]
+        trace["intent_gate"] = {
+            "text": text,
+            "ranking_full": ranking_full,
+            "ranking_tried": ranking_tried,
+            "method": method,
+        }

         return {
-            "intent_ranking": ranking,
+            "intent_ranking": ranking_tried,
             "current_intent_index": 0,
             "trace": trace,
         }

Also applies to: 91-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/nodes/classify/intent_gate.py` around lines 14 - 18, The docstring
and function contract in intent_gate.py are inconsistent: update the docstring
to state that the node returns a top-N list (not "4개 의도") and revise the
implementation to explicitly separate and persist both the full LLM ranking and
the top-N tried ranking; specifically, add/ensure two distinct variables/keys
(ranking_full and ranking_tried) are produced and stored in the trace/log, and
make the function return only the top-N list while keeping ranking_full in the
trace for debugging; apply the same change to the analogous block around the
code referenced at lines 91-103 so both docstrings, return values, and trace
keys match (use the existing names ranking_full and ranking_tried to locate the
logic).

Comment thread app/graph/nodes/classify/intent_gate.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/graph/nodes/classify/intent_gate.py (1)

16-20: ⚠️ Potential issue | 🟡 Minor

docstring/trace가 실제로 시도한 ranking과 아직 어긋납니다.

Line 18은 4개 의도를 반환한다고 읽히지만, 실제 반환값은 Line 116-119에서 top-2입니다. 반면 Line 109의 trace에는 pre-slice 전체 ranking이 남아서, 디버깅 시 “기록된 값”과 “실제로 시도한 값”이 달라집니다. ranking_full/ranking_tried를 분리해 남기거나, 의도대로 tried ranking만 trace에 기록하는 쪽으로 맞추는 편이 안전합니다.

🛠️ 제안 수정
-    4개 의도(rag, smalltalk, event, struct_db)를 순위로 반환.
+    4개 의도를 순위화하고, 실제로는 상위 N개만 반환.
-        trace["intent_gate"] = {
-            "text": text,
-            "ranking": ranking,
-            "method": method,
-        }
-        if error:
-            trace["intent_gate"]["error"] = error
-
-        # 상위 N개 의도만 시도
-        ranking = ranking[:_MAX_INTENTS]
+        ranking_full = ranking
+        ranking_tried = ranking_full[:_MAX_INTENTS]
+        trace["intent_gate"] = {
+            "text": text,
+            "ranking_full": ranking_full,
+            "ranking_tried": ranking_tried,
+            "method": method,
+        }
+        if error:
+            trace["intent_gate"]["error"] = error
-            "intent_ranking": ranking,
+            "intent_ranking": ranking_tried,

Also applies to: 107-119

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/nodes/classify/intent_gate.py` around lines 16 - 20, The docstring
and trace disagree about returned intents: the code builds a full ranking
(ranking_full) but actually returns only the top-2 (sliced) in the intent gate
logic (see variables ranking_full and the slice used at return around where
ranking_tried is created); update the implementation so the trace records the
exact "tried" ranking (e.g., create and use ranking_tried for the sliced/top-N
set) or persist both ranking_full and ranking_tried separately in the trace, and
ensure the returned value and process that uses ranking_tried (or the top-N) are
consistent with the docstring and trace output in the IntentGate/intent_gate
functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/graph/nodes/classify/intent_gate.py`:
- Around line 64-66: Validate the LLM response before parsing and using it:
check the return of llm.chat(...) (the local variable raw) is a non-empty
string, then safely call json.loads(raw) inside a try/except, and after parsing
ensure the result is a dict before accessing .get("ranking"); if any check fails
(raw is None/empty, json.loads raises, or parsed is not a dict) set ranking =
_DEFAULT_RANKING and mark method = "fallback" so the fallback path runs; apply
the same pattern to the second occurrence handling lines around where parsed and
ranking are used (the block that currently sets parsed = json.loads(...) and
ranking = parsed.get(...)) to avoid TypeError/AttributeError propagation.

---

Duplicate comments:
In `@app/graph/nodes/classify/intent_gate.py`:
- Around line 16-20: The docstring and trace disagree about returned intents:
the code builds a full ranking (ranking_full) but actually returns only the
top-2 (sliced) in the intent gate logic (see variables ranking_full and the
slice used at return around where ranking_tried is created); update the
implementation so the trace records the exact "tried" ranking (e.g., create and
use ranking_tried for the sliced/top-N set) or persist both ranking_full and
ranking_tried separately in the trace, and ensure the returned value and process
that uses ranking_tried (or the top-N) are consistent with the docstring and
trace output in the IntentGate/intent_gate functions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0bce2c08-96ac-46db-bbdd-e7f369af24b3

📥 Commits

Reviewing files that changed from the base of the PR and between 5990254 and 6bcbeed.

📒 Files selected for processing (1)
  • app/graph/nodes/classify/intent_gate.py

Comment on lines +64 to +66
raw = llm.chat(messages, max_tokens=60)
parsed = json.loads(raw)
ranking = parsed.get("ranking", _DEFAULT_RANKING)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

LLM 응답 shape를 먼저 검증해야 fallback이 살아 있습니다.

Line 64-66은 raw가 문자열인지, json.loads() 결과가 dict인지 확인하기 전에 바로 사용합니다. 그래서 llm.chat()None을 돌려주면 TypeError, 모델이 []"rag"처럼 유효한 JSON이지만 dict가 아닌 값을 주면 AttributeError가 그대로 전파되어 fallback 경로를 못 탑니다. {}도 지금은 _DEFAULT_RANKING으로 내려가면서 method"llm"으로 남습니다.

🛠️ 제안 수정
         try:
             raw = llm.chat(messages, max_tokens=60)
+            if not isinstance(raw, str):
+                raise TypeError(f"Unexpected LLM response type: {type(raw).__name__}")
             parsed = json.loads(raw)
-            ranking = parsed.get("ranking", _DEFAULT_RANKING)
+            if not isinstance(parsed, dict) or "ranking" not in parsed:
+                ranking = _DEFAULT_RANKING
+                method = "llm_fallback_invalid_shape"
+                error = f"Unexpected JSON shape: {type(parsed).__name__}"
+            else:
+                ranking = parsed["ranking"]
-        except json.JSONDecodeError as e:
+        except (TypeError, json.JSONDecodeError) as e:
             ranking = _DEFAULT_RANKING
-            method = "llm_fallback_invalid_json"
+            method = "llm_fallback_invalid_response"
             error = str(e)

Also applies to: 86-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/graph/nodes/classify/intent_gate.py` around lines 64 - 66, Validate the
LLM response before parsing and using it: check the return of llm.chat(...) (the
local variable raw) is a non-empty string, then safely call json.loads(raw)
inside a try/except, and after parsing ensure the result is a dict before
accessing .get("ranking"); if any check fails (raw is None/empty, json.loads
raises, or parsed is not a dict) set ranking = _DEFAULT_RANKING and mark method
= "fallback" so the fallback path runs; apply the same pattern to the second
occurrence handling lines around where parsed and ranking are used (the block
that currently sets parsed = json.loads(...) and ranking = parsed.get(...)) to
avoid TypeError/AttributeError propagation.

@moonhwoo moonhwoo merged commit 4c89536 into develop Mar 13, 2026
1 check passed
@moonhwoo moonhwoo deleted the feature/router-unified-routing branch March 13, 2026 05:28
@coderabbitai coderabbitai Bot mentioned this pull request Mar 18, 2026
This was referenced Apr 6, 2026
This was referenced Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants