Feature/event#55
Conversation
WalkthroughWebSocket 실시간 스트리밍 경로를 새로운 RAG 검색 및 비동기 문장 스트리밍 컴포넌트로 교체하고, 이벤트 전송을 인라인화하며, event_node에서 JSON 기반 응답 구문 분석을 도입하고, 추적 메커니즘을 조정했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as 클라이언트
participant WS as WebSocket API
participant RAG as RAG 검색
participant LLM as LLM 스트리밍
participant TTS as TTS 합성
Client->>WS: STT 완료 (쿼리)
WS->>RAG: traced_rag_retrieve(쿼리)
RAG-->>WS: 후보 정보
WS->>LLM: traced_llm_sentence_stream 시작
loop 각 문장마다
LLM-->>WS: 문장 생성
WS->>Client: llm_sentence 이벤트 전송
WS->>TTS: 문장 합성 (선택사항)
TTS-->>WS: 오디오 청크
WS->>Client: tts_chunk 이벤트 전송
end
LLM-->>WS: 스트림 완료
WS->>Client: final_text + 레이턴시 요약
WS->>Client: done
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
app/api/websocket.py (1)
437-486:llm_total_ms에 TTS 시간이 섞여 측정됩니다.Line 437 이후 루프 안에서 TTS 합성과 전송까지 기다린 뒤에 Line 483-486에서
llm_total_ms를 계산하고 있습니다.tts_stream=True일 때는 LLM 생성 시간보다 TTS backpressure가 더해진 값이 기록돼서, 이 지표로 LLM 성능 변화를 보기 어려워집니다. LLM 스트림 종료 시점을 TTS 처리 전에 따로 찍거나, TTS를 별도 consumer로 분리하는 편이 정확합니다.Also applies to: 501-513
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/websocket.py` around lines 437 - 486, The LLM total time is inflated by awaiting TTS inside the traced_llm_sentence_stream loop; capture the LLM end time immediately when the LLM stream completes (inside the async for block after the loop finishes or right before starting any TTS work) by setting timing["llm_end_at"] = time.perf_counter(), then compute total_llm_ms using ms_delta(timing["llm_start_at"], timing["llm_end_at"]) instead of using the later time.perf_counter() that includes TTS backpressure; update the code around the traced_llm_sentence_stream loop (reference traced_llm_sentence_stream, timing, tts_stream, tts_result, final_answer, ms_delta) and apply the same change to the other occurrence noted (lines ~501-513) so TTS time is not mixed into LLM metrics.app/graph/nodes/answer/smalltalk_node.py (1)
5-5: 구현 상태와 반대인 문구는 지우는 편이 낫습니다.
"아직 구현 안되어 있어요"는 아래 팩토리가 이미 동작하는 현재 파일 상태와 맞지 않아서, 이후 유지보수자가 이 노드를 미구현 상태로 오해하기 쉽습니다. 미구현 표시가 필요하면 실제 TODO 주석으로 남기고, 지금 문자열 자체는 제거하는 편이 명확합니다.🤖 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` at line 5, Remove the misleading module-level string literal "아직 구현 안되어 있어요" from app/graph/nodes/answer/smalltalk_node.py; if a placeholder is desired replace it with an explicit TODO comment (e.g., a Python comment or proper TODO docstring) so maintainers won't think the node is unimplemented—target the module-level docstring/string at the top of smalltalk_node.py.app/graph/nodes/answer/event_node.py (1)
181-190: 오류를 전부no_match로 기록하면 추적 지표가 섞입니다.지금 구현은
method에llm_invalid_json이나llm_api_error를 남기더라도,status는 결국 모두no_match가 됩니다. "관련 정보가 없음"과 "모델/인프라 실패"는 운영 관점에서 다른 사건이므로, trace 단계에서 분리해 두는 편이 이후 분석과 알람에 유리합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/graph/nodes/answer/event_node.py` around lines 181 - 190, The trace currently sets trace["event"]["status"] to "ok" or "no_match" based solely on check_result, which mixes genuine "no information" cases with LLM or API failures; update the logic around the trace["event"] assignment (the block that builds "status", "method", and uses check_result) so that when method is "llm_invalid_json" or "llm_api_error" (or when check_result indicates an LLM/infra failure) you set status to a distinct value like "error" (or "llm_error"), otherwise keep "ok" or "no_match" for normal flows; ensure selected_ids, candidate_count, infos_count and raw remain unchanged and only the status value is altered to reflect error vs no-match.
🤖 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/api/websocket.py`:
- Around line 224-225: The realtime flag defaulting to True bypasses the graph
routing (intent_gate/event/smalltalk/fetch_places/struct_db) because the
realtime branch only calls traced_rag_retrieve() and
llm.stream_generate_sentences(); change the default to False when reading
start.get("realtime", ...) in app/api/websocket.py (variable realtime) so normal
WebSocket requests go through the graph routing, or alternatively update the
realtime branch to invoke the same graph routing path (calling the
intent_gate/event/smalltalk/fetch_places/struct_db flow) to preserve parity;
apply the same change for the other occurrence around the 420-437 region where
realtime is read.
- Around line 149-158: send_tts_chunks currently marks the last sentence of
every batch with is_final=True, but the real-time websocket path calls it
repeatedly with single-sentence batches, causing every tts_chunk to be flagged
final; change send_tts_chunks (and callers in the websocket streaming route) so
that is_final is only set when the overall TTS stream is finished: add a boolean
parameter (e.g., overall_is_final or mark_final_only_if_stream_end) to
send_tts_chunks and use that to set "is_final" (instead of idx ==
len(non_empty_sentences)-1), and update the websocket streaming caller to pass
True only for the true final chunk while relying on the existing tts_done event
as the stream terminator.
- Around line 540-545: Do not send internal exception strings to clients: in the
websocket error branch where websocket.send_text is called (using variables e
and trace_id), replace "message": str(e) with a fixed, generic message like "An
internal error occurred" (or a localized equivalent) and ensure the full
exception and stacktrace are logged server-side against trace_id (e.g., via the
existing logger) so clients only receive the generic message and trace_id while
detailed error information stays in logs.
In `@app/graph/nodes/answer/event_node.py`:
- Around line 122-125: The code currently slices daily_infos before validation
which drops later relevant items and can raise inside _serialize_daily_info;
instead, first filter/validate the input list (e.g., implement or call a
validator like _is_valid_daily_info to remove malformed entries), then sort the
remaining entries by your chosen priority (relevance/date/score) and only after
that take the top N (8) and map them through _serialize_daily_info to build
candidate_infos; also wrap serialization in a safe handler to surface/skip
serialization errors rather than letting one bad element crash the whole
operation.
- Around line 134-141: The code passes a naive local datetime into
_build_event_messages via now_str using datetime.now(), which can cause
incorrect event/date validation across deployments; change the now_str
generation to use a timezone-aware datetime (e.g., datetime.now(timezone.utc) or
datetime.now(tz=ZoneInfo(site_timezone)) as appropriate) and format that
tz-aware value into the string before passing to _build_event_messages so the
LLM receives a timestamp with offset; update imports if necessary and ensure the
symbol now_str (and callsite building it) uses the tz-aware datetime rather than
datetime.now().
---
Nitpick comments:
In `@app/api/websocket.py`:
- Around line 437-486: The LLM total time is inflated by awaiting TTS inside the
traced_llm_sentence_stream loop; capture the LLM end time immediately when the
LLM stream completes (inside the async for block after the loop finishes or
right before starting any TTS work) by setting timing["llm_end_at"] =
time.perf_counter(), then compute total_llm_ms using
ms_delta(timing["llm_start_at"], timing["llm_end_at"]) instead of using the
later time.perf_counter() that includes TTS backpressure; update the code around
the traced_llm_sentence_stream loop (reference traced_llm_sentence_stream,
timing, tts_stream, tts_result, final_answer, ms_delta) and apply the same
change to the other occurrence noted (lines ~501-513) so TTS time is not mixed
into LLM metrics.
In `@app/graph/nodes/answer/event_node.py`:
- Around line 181-190: The trace currently sets trace["event"]["status"] to "ok"
or "no_match" based solely on check_result, which mixes genuine "no information"
cases with LLM or API failures; update the logic around the trace["event"]
assignment (the block that builds "status", "method", and uses check_result) so
that when method is "llm_invalid_json" or "llm_api_error" (or when check_result
indicates an LLM/infra failure) you set status to a distinct value like "error"
(or "llm_error"), otherwise keep "ok" or "no_match" for normal flows; ensure
selected_ids, candidate_count, infos_count and raw remain unchanged and only the
status value is altered to reflect error vs no-match.
In `@app/graph/nodes/answer/smalltalk_node.py`:
- Line 5: Remove the misleading module-level string literal "아직 구현 안되어 있어요" from
app/graph/nodes/answer/smalltalk_node.py; if a placeholder is desired replace it
with an explicit TODO comment (e.g., a Python comment or proper TODO docstring)
so maintainers won't think the node is unimplemented—target the module-level
docstring/string at the top of smalltalk_node.py.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c9754ea9-32ab-4b57-a369-ee711212d177
📒 Files selected for processing (4)
app/api/websocket.pyapp/graph/graph_builder.pyapp/graph/nodes/answer/event_node.pyapp/graph/nodes/answer/smalltalk_node.py
| await websocket.send_text(json.dumps({ | ||
| "type": "tts_chunk", | ||
| "seq": current_seq, | ||
| "text": sentence, | ||
| "audio_format": "mp3", | ||
| "audio_b64": base64.b64encode(audio_chunk).decode("utf-8"), | ||
| "language_code": tts_language_code, | ||
| "is_final": idx == len(non_empty_sentences) - 1, | ||
| "trace_id": trace_id, | ||
| }, ensure_ascii=False)) |
There was a problem hiding this comment.
실시간 TTS에서는 모든 청크가 마지막 청크로 표시됩니다.
send_tts_chunks()는 전달받은 배치의 마지막 문장에 is_final=True를 넣는데, 실시간 경로는 이 함수를 문장 하나짜리 리스트로 반복 호출합니다. 그래서 뒤에 문장이 더 남아 있어도 모든 tts_chunk가 is_final=True가 됩니다. 클라이언트가 이 플래그로 스트림 종료를 판단하면 재생/버퍼링이 깨질 수 있으니, 스트리밍 모드에서는 tts_done만 종료 신호로 쓰거나 호출부에서 최종 청크만 명시적으로 표시해야 합니다.
수정 예시
async def send_tts_chunks(
websocket: WebSocket,
sentences: list[str],
tts_language_code: str,
trace_id: str,
start_seq: int = 0,
+ mark_last_chunk_final: bool = True,
) -> dict:
@@
- "is_final": idx == len(non_empty_sentences) - 1,
+ "is_final": mark_last_chunk_final and idx == len(non_empty_sentences) - 1, tts_result = await send_tts_chunks(
websocket=websocket,
sentences=[sentence],
tts_language_code=tts_language_code,
trace_id=trace_id,
start_seq=tts_seq,
+ mark_last_chunk_final=False,
)Also applies to: 462-468
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/websocket.py` around lines 149 - 158, send_tts_chunks currently marks
the last sentence of every batch with is_final=True, but the real-time websocket
path calls it repeatedly with single-sentence batches, causing every tts_chunk
to be flagged final; change send_tts_chunks (and callers in the websocket
streaming route) so that is_final is only set when the overall TTS stream is
finished: add a boolean parameter (e.g., overall_is_final or
mark_final_only_if_stream_end) to send_tts_chunks and use that to set "is_final"
(instead of idx == len(non_empty_sentences)-1), and update the websocket
streaming caller to pass True only for the true final chunk while relying on the
existing tts_done event as the stream terminator.
| tts_stream = bool(start.get("tts_stream", True)) | ||
| realtime = bool(start.get("realtime", True)) |
There was a problem hiding this comment.
기본 실시간 경로가 의도 라우팅을 완전히 우회합니다.
realtime 기본값이 True인데, 이 분기에서는 traced_rag_retrieve()와 llm.stream_generate_sentences()만 호출합니다. 그래서 app/graph/graph_builder.py Line 50-65와 Line 91-126에서 연결한 intent_gate/event/smalltalk/fetch_places/struct_db 경로는 기본 WebSocket 요청에서 아예 타지 않습니다. 결과적으로 이벤트/잡담/장소 안내 질문도 RAG로만 처리하게 되고, 이번 PR의 event_node 변경 역시 기본 경로에는 적용되지 않습니다. 실시간 모드도 그래프 라우팅을 타게 하거나, 기능 동등성이 맞춰질 때까지 realtime 기본값을 False로 두는 편이 안전합니다.
Also applies to: 420-437
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/websocket.py` around lines 224 - 225, The realtime flag defaulting to
True bypasses the graph routing
(intent_gate/event/smalltalk/fetch_places/struct_db) because the realtime branch
only calls traced_rag_retrieve() and llm.stream_generate_sentences(); change the
default to False when reading start.get("realtime", ...) in app/api/websocket.py
(variable realtime) so normal WebSocket requests go through the graph routing,
or alternatively update the realtime branch to invoke the same graph routing
path (calling the intent_gate/event/smalltalk/fetch_places/struct_db flow) to
preserve parity; apply the same change for the other occurrence around the
420-437 region where realtime is read.
| await websocket.send_text(json.dumps({ | ||
| "type": "error", | ||
| "code": "INTERNAL", | ||
| "message": str(e), | ||
| "trace_id": trace_id, | ||
| }, ensure_ascii=False)) |
There was a problem hiding this comment.
예상치 못한 예외 문자열을 그대로 클라이언트에 노출하지 마세요.
여기서 str(e)를 그대로 보내면 DB/모델/내부 구현 세부정보가 외부 프로토콜에 노출될 수 있습니다. 클라이언트에는 고정된 일반 메시지만 내려주고, 상세 원인은 trace/log에만 남기는 편이 안전합니다.
수정 예시
await websocket.send_text(json.dumps({
"type": "error",
"code": "INTERNAL",
- "message": str(e),
+ "message": "internal server error",
"trace_id": trace_id,
}, ensure_ascii=False))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/websocket.py` around lines 540 - 545, Do not send internal exception
strings to clients: in the websocket error branch where websocket.send_text is
called (using variables e and trace_id), replace "message": str(e) with a fixed,
generic message like "An internal error occurred" (or a localized equivalent)
and ensure the full exception and stacktrace are logged server-side against
trace_id (e.g., via the existing logger) so clients only receive the generic
message and trace_id while detailed error information stays in logs.
| candidate_infos = [ | ||
| _serialize_daily_info(idx, d) | ||
| for idx, d in enumerate(daily_infos[:8]) | ||
| ] |
There was a problem hiding this comment.
후보 생성 전에 입력을 검증하고 우선순위를 매기세요.
daily_infos[:8]로 먼저 잘라 버려서 9번째 이후의 관련 공지는 영원히 후보에 못 들어오고, 잘못된 원소가 섞여 있으면 _serialize_daily_info()에서 바로 예외가 납니다. 외부 입력은 먼저 형태를 걸러낸 뒤 질의/일자 기준으로 정렬하고, 그 다음에 상한을 적용하는 편이 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/graph/nodes/answer/event_node.py` around lines 122 - 125, The code
currently slices daily_infos before validation which drops later relevant items
and can raise inside _serialize_daily_info; instead, first filter/validate the
input list (e.g., implement or call a validator like _is_valid_daily_info to
remove malformed entries), then sort the remaining entries by your chosen
priority (relevance/date/score) and only after that take the top N (8) and map
them through _serialize_daily_info to build candidate_infos; also wrap
serialization in a safe handler to surface/skip serialization errors rather than
letting one bad element crash the whole operation.
| messages = _build_event_messages( | ||
| query=text, | ||
| user_language=user_language, | ||
| now_str=datetime.now().strftime("%Y-%m-%d %H:%M"), | ||
| candidate_infos=candidate_infos, | ||
| system_prompt_base=system_prompt_base, | ||
| answer_style=answer_style, | ||
| ) |
There was a problem hiding this comment.
❓ Verification inconclusive
Script executed:
#!/bin/bash
# 저장소에 이미 공통 timezone 소스가 있는지 확인합니다.
rg -n --type=py 'zoneinfo|timezone|tzinfo|Asia/Seoul|site_timezone|user_timezone|datetime\.now\('Repository: Project-Guideon/guideon-AI
Repository: Project-Guideon/guideon-AI
Exit code: 0
stdout:
app/graph/nodes/answer/event_node.py:137: now_str=datetime.now().strftime("%Y-%m-%d %H:%M"),
app/api/document_legacy.py:12:from datetime import datetime, timezone
app/api/document_legacy.py:63: created_at = datetime.now(timezone.utc).isoformat()
app/api/document_legacy.py:169: created_at = datetime.now(timezone.utc).isoformat()
Script executed:
# 1. event_node.py의 전체 함수 컨텍스트 확인
sed -n '100,170p' app/graph/nodes/answer/event_node.pyRepository: Project-Guideon/guideon-AI
Repository: Project-Guideon/guideon-AI
Exit code: 0
stdout:
user_language: str = state.get("user_language", "ko")
daily_infos: list[dict[str, Any]] = state.get("daily_infos") or []
site_id: int = state.get("site_id", 1)
trace = dict(state.get("trace") or {})
flow = list(trace.get("_flow") or [])
flow.append("event")
trace["_flow"] = flow
if not daily_infos:
trace["event"] = {
"status": "no_context",
"query": text,
"site_id": site_id,
"infos_count": 0,
}
return {
"answer_text": "",
"check_result": "bad",
"trace": trace,
}
candidate_infos = [
_serialize_daily_info(idx, d)
for idx, d in enumerate(daily_infos[:8])
]
system_prompt_base: str = state.get("system_prompt") or ""
answer_style: str = (
state.get("mascot_event_style")
or state.get("mascot_base_persona")
or ""
)
messages = _build_event_messages(
query=text,
user_language=user_language,
now_str=datetime.now().strftime("%Y-%m-%d %H:%M"),
candidate_infos=candidate_infos,
system_prompt_base=system_prompt_base,
answer_style=answer_style,
)
raw = ""
answer_text = ""
selected_ids: list[int] = []
method = "llm"
error_msg = ""
check_result = "bad"
try:
raw = llm.chat(messages, max_tokens=250)
parsed = json.loads(raw)
no_match = bool(parsed.get("no_match", True))
answer_text = (parsed.get("answer") or "").strip()
selected_ids_raw = parsed.get("selected_ids", [])
if isinstance(selected_ids_raw, list):
selected_ids = [
int(x) for x in selected_ids_raw
if isinstance(x, int) or str(x).isdigit()
]
if (not no_match) and answer_text:
check_result = "good"
else:
check_result = "bad"
except json.JSONDecodeError as e:
method = "llm_invalid_json"
Script executed:
# 2. 타임존 설정 패턴 검색
rg -n --type=py 'site_timezone|pytz|ZoneInfo|timezone_aware|get_timezone' -B2 -A2Repository: Project-Guideon/guideon-AI
Repository: Project-Guideon/guideon-AI
Exit code: 1
Script executed:
# 3. _build_event_messages 함수 정의 찾기
rg -n 'def _build_event_messages' --type=py -A 10Repository: Project-Guideon/guideon-AI
Repository: Project-Guideon/guideon-AI
Exit code: 0
stdout:
app/graph/nodes/answer/event_node.py:37:def _build_event_messages(
app/graph/nodes/answer/event_node.py-38- query: str,
app/graph/nodes/answer/event_node.py-39- user_language: str,
app/graph/nodes/answer/event_node.py-40- now_str: str,
app/graph/nodes/answer/event_node.py-41- candidate_infos: list[dict[str, Any]],
app/graph/nodes/answer/event_node.py-42- system_prompt_base: str,
app/graph/nodes/answer/event_node.py-43- answer_style: str,
app/graph/nodes/answer/event_node.py-44-) -> list[dict[str, str]]:
app/graph/nodes/answer/event_node.py-45- candidate_json = json.dumps(candidate_infos, ensure_ascii=False, indent=2)
app/graph/nodes/answer/event_node.py-46-
app/graph/nodes/answer/event_node.py-47- character_lines = []
Script executed:
# _build_event_messages 함수 전체 구현 확인
sed -n '37,100p' app/graph/nodes/answer/event_node.pyRepository: Project-Guideon/guideon-AI
Repository: Project-Guideon/guideon-AI
Exit code: 0
stdout:
def _build_event_messages(
query: str,
user_language: str,
now_str: str,
candidate_infos: list[dict[str, Any]],
system_prompt_base: str,
answer_style: str,
) -> list[dict[str, str]]:
candidate_json = json.dumps(candidate_infos, ensure_ascii=False, indent=2)
character_lines = []
if system_prompt_base:
character_lines.append(system_prompt_base)
if answer_style:
character_lines.append(f"답변 스타일: {answer_style}")
character_instruction = "\n".join(character_lines).strip()
lang_name = _LANG_NAMES.get(user_language, user_language.upper())
lang_instruction = (
"answer는 반드시 한국어로 작성하세요. 2~3문장으로 자연스럽고 음성 안내처럼 답변하세요. "
"이모지나 특수문자는 사용하지 마세요."
if user_language == "ko"
else f'The "answer" field must be written in {lang_name}. '
f"Use 2-3 natural, speech-friendly sentences. No emoji, no special characters."
)
return [
{
"role": "system",
"content": (
"You are the event/operation information answering module for a multilingual tourism chatbot.\n"
"Use ONLY the provided candidate records.\n"
"Do not invent facts.\n"
"Select only records relevant to the user's question.\n"
"If nothing is relevant, return no_match=true.\n"
"If date/time fields exist, reflect them clearly in the answer.\n"
"If the question is about restrictions, closures, repairs, or route changes, "
"explain the affected place and restriction clearly.\n"
f"Current datetime: {now_str}\n\n"
f"{character_instruction}\n\n"
f"{lang_instruction}\n\n"
"Return ONLY valid JSON in this exact format:\n"
'{'
'"no_match": false, '
'"selected_ids": [1, 2], '
'"answer": "..."'
'}'
),
},
{
"role": "user",
"content": (
f"user_language: {user_language}\n"
f"user_question: {query}\n\n"
f"candidate_records:\n{candidate_json}"
),
},
]
def make_event_node(llm: OpenAILLM):
def event_node(state: GraphState) -> dict:
text: str = (state.get("normalized_text") or "").strip()
user_language: str = state.get("user_language", "ko")
Script executed:
# now_str과 daily_infos의 날짜 비교 로직 확인
rg -n 'now_str|daily_info|date_time|deadline|start_time' app/graph/nodes/answer/event_node.py -A2 -B2Repository: Project-Guideon/guideon-AI
Repository: Project-Guideon/guideon-AI
Exit code: 0
stdout:
21-
22-
23:def _serialize_daily_info(idx: int, d: dict[str, Any]) -> dict[str, Any]:
24- return {
25- "id": d.get("id", idx + 1),
--
29- "start_date": d.get("startDate", ""),
30- "end_date": d.get("endDate", ""),
31: "start_time": d.get("startTime", ""),
32- "end_time": d.get("endTime", ""),
33- "all_day": d.get("allDay", False),
--
38- query: str,
39- user_language: str,
40: now_str: str,
41- candidate_infos: list[dict[str, Any]],
42- system_prompt_base: str,
--
73- "If the question is about restrictions, closures, repairs, or route changes, "
74- "explain the affected place and restriction clearly.\n"
75: f"Current datetime: {now_str}\n\n"
76- f"{character_instruction}\n\n"
77- f"{lang_instruction}\n\n"
--
99- text: str = (state.get("normalized_text") or "").strip()
100- user_language: str = state.get("user_language", "ko")
101: daily_infos: list[dict[str, Any]] = state.get("daily_infos") or []
102- site_id: int = state.get("site_id", 1)
103-
--
107- trace["_flow"] = flow
108-
109: if not daily_infos:
110- trace["event"] = {
111- "status": "no_context",
--
121-
122- candidate_infos = [
123: _serialize_daily_info(idx, d)
124: for idx, d in enumerate(daily_infos[:8])
125- ]
126-
--
135- query=text,
136- user_language=user_language,
137: now_str=datetime.now().strftime("%Y-%m-%d %H:%M"),
138- candidate_infos=candidate_infos,
139- system_prompt_base=system_prompt_base,
--
183- "query": text,
184- "site_id": site_id,
185: "infos_count": len(daily_infos),
186- "candidate_count": len(candidate_infos),
187- "selected_ids": selected_ids,
Script executed:
# site별 타임존 관리 설정 확인
rg -n 'site|Site' app/graph/nodes/answer/event_node.py -A2 -B2 | head -50Repository: Project-Guideon/guideon-AI
Repository: Project-Guideon/guideon-AI
Exit code: 0
stdout:
100- user_language: str = state.get("user_language", "ko")
101- daily_infos: list[dict[str, Any]] = state.get("daily_infos") or []
102: site_id: int = state.get("site_id", 1)
103-
104- trace = dict(state.get("trace") or {})
--
111- "status": "no_context",
112- "query": text,
113: "site_id": site_id,
114- "infos_count": 0,
115- }
--
182- "status": "ok" if check_result == "good" else "no_match",
183- "query": text,
184: "site_id": site_id,
185- "infos_count": len(daily_infos),
186- "candidate_count": len(candidate_infos),
현재 시각은 timezone-aware 값으로 넣어야 합니다.
Line 137의 datetime.now()는 서버의 로컬 타임존을 따르므로, 배포 환경이 UTC이거나 사이트의 실제 시간대와 다르면 이벤트/마감 날짜 판단이 틀어질 수 있습니다. now_str이 LLM의 시스템 프롬프트에 전달되어 일일 정보(start_date, end_date, start_time, end_time)의 유효성을 판단하는 데 사용되므로, 특히 다중 사이트 시스템에서 중요합니다. timezone.utc를 사용하거나 오프셋이 포함된 timezone-aware datetime을 사용하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/graph/nodes/answer/event_node.py` around lines 134 - 141, The code passes
a naive local datetime into _build_event_messages via now_str using
datetime.now(), which can cause incorrect event/date validation across
deployments; change the now_str generation to use a timezone-aware datetime
(e.g., datetime.now(timezone.utc) or datetime.now(tz=ZoneInfo(site_timezone)) as
appropriate) and format that tz-aware value into the string before passing to
_build_event_messages so the LLM receives a timestamp with offset; update
imports if necessary and ensure the symbol now_str (and callsite building it)
uses the tz-aware datetime rather than datetime.now().
Summary by CodeRabbit
기능 개선
버그 수정