Revert "Revert "Develop" "#77
Conversation
Walkthrough대규모 구조 변경: FastAPI를 라우터별 모듈로 분리하고 중앙 의존성/파이프라인을 도입했으며, 문서 처리(다운로드→청킹→요약→임베딩→DB), WebSocket 기반 STT→QA→TTS 스트리밍, 및 의도 랭킹 기반 폴백 그래프 재설계를 추가/개편했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as 클라이언트
participant WS as WebSocket 엔드포인트
participant STT as STT 서비스
participant TextQA as Text QA 파이프라인
participant LLM as LLM
participant TTS as TTS 서비스
participant Core as Spring Core
Client->>WS: {type:"start"} + 오디오 스트리밍
activate WS
WS->>STT: PCM 오디오 프레임 스트림
STT-->>WS: interim / final transcript
WS-->>Client: stt_start / interim / final
WS->>TextQA: 최종 전사 (query) + site_id + mascot?
activate TextQA
TextQA->>LLM: RAG/LLM 호출 (검색+생성)
LLM-->>TextQA: 답변 텍스트
deactivate TextQA
loop 문장별
WS->>TTS: 문장 텍스트
TTS-->>WS: MP3 청크 (base64)
WS-->>Client: tts_chunk
end
WS-->>Client: final_text / latency_summary / done
deactivate WS
sequenceDiagram
participant API as /v1/documents/process
participant Validator as SSRF 검증
participant Downloader as PDF 다운로드
participant Processor as document_processor
participant Converter as PDF→Markdown (PDF2db_v2)
participant Chunker as 청킹/요약/임베딩
participant DB as PostgreSQL
participant CoreSvc as Spring Core
API->>Validator: storage_url 검증
Validator-->>API: OK / 400
API->>Processor: 백그라운드 작업 스케줄
Processor->>Downloader: GET storage_url
Downloader-->>Processor: pdf bytes
Processor->>Converter: PDF → Markdown → 섹션
Converter-->>Chunker: section texts
Chunker-->>Processor: summaries, keywords, embeddings
Processor->>DB: 트랜잭션: 기존 청크 삭제 → 새 청크 삽입
DB-->>Processor: 저장 완료
Processor->>CoreSvc: PATCH status=COMPLETED (retry on failure)
CoreSvc-->>Processor: 응답
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
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: 19
🧹 Nitpick comments (10)
record.py (1)
6-6: 파일명 하드코딩으로 연계 경로 계약이 깨질 수 있습니다.Line 6에서 파일명을
"궁성담장길이.wav"로 고정하면,app/core/ws_stt_test.py(Line 10)의WAV_PATH = "audio/wav/input4.wav"처럼 고정 경로/이름을 기대하는 소비 코드와 쉽게 불일치가 납니다. 기본 저장 위치와 파일명을 설정값/인자로 분리해 주세요.제안 수정안
+from pathlib import Path + SAMPLE_RATE = 16000 # Google STT 필수 설정 DURATION = 5 # 초 (원하는 만큼 변경) -FILENAME = "궁성담장길이.wav" +OUTPUT_DIR = Path("audio/wav") +FILENAME = OUTPUT_DIR / "input1.wav" print(f"녹음 시작... ({DURATION}초)") audio = sd.rec(int(DURATION * SAMPLE_RATE), samplerate=SAMPLE_RATE, channels=1, dtype="int16") sd.wait() +OUTPUT_DIR.mkdir(parents=True, exist_ok=True) sf.write(FILENAME, audio, SAMPLE_RATE, subtype="PCM_16") print(f"저장 완료: {FILENAME}")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@record.py` at line 6, The FILENAME constant is hardcoded ("궁성담장길이.wav") which can break consumers expecting different paths (e.g., WAV_PATH in app/core/ws_stt_test.py); change FILENAME to be configurable by reading from an environment variable, a configuration object, or accepting it as a function/class parameter and provide a sensible default, and update any callers to pass the expected path (or read the same config) so both record.py's FILENAME and ws_stt_test.py's WAV_PATH come from the same configurable source.audio/m4a.py (1)
10-12: 확장자 필터 외에 파일 타입 체크를 추가하면 더 안전합니다.Line 11 조건만으로는
.m4a이름의 디렉터리도 매칭될 수 있습니다.os.path.isfile(input_path)를 함께 확인해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@audio/m4a.py` around lines 10 - 12, The loop over os.listdir(INPUT_DIR) currently only checks filename.lower().endswith(".m4a") which can match directories named *.m4a; update the condition to also verify the path is a regular file by computing input_path = os.path.join(INPUT_DIR, filename) and adding os.path.isfile(input_path) to the conditional so only real files (not directories) are processed; ensure you reference INPUT_DIR, filename and input_path when making the change.app/core/services/llm_openai.py (2)
72-72: PEP 484에 따라 명시적Optional타입을 사용하세요.
max_tokens: int = None은 암묵적 Optional로, Ruff에서도 경고합니다.♻️ 타입 힌트 수정
- def chat(self, messages: list, max_tokens: int = None) -> str: + def chat(self, messages: list, max_tokens: int | None = None) -> str:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/core/services/llm_openai.py` at line 72, 함수 chat의 시그니처에서 암묵적 Optional인 `max_tokens: int = None`을 명시적으로 바꿔 PEP 484 규약을 따르세요: `max_tokens: Optional[int] = None`로 변경하고 파일 상단(또는 타입 임포트 위치)에 `from typing import Optional`를 추가하여 타입이 정의되도록 하세요; 대상 심볼은 메서드 이름 `chat`을 수정하면 됩니다.
20-23: 한국어 문장 종결 패턴 검증이 필요합니다.현재 정규식
r"(.+?[.!?]|.+?다\.|.+?요\.|.+?니다\.)"는 기본적인 종결 어미를 포함하지만, 일부 케이스에서 의도치 않은 분리가 발생할 수 있습니다:
- "네, 그렇습니다." → 정상 분리
- "123.45입니다." → 숫자 내 마침표에서 잘못 분리될 수 있음
TTS 용도로 사용 시 큰 문제는 아니지만, 참고하시기 바랍니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/core/services/llm_openai.py` around lines 20 - 23, The current sentence-splitting regex _SENTENCE_RE can split on decimal points (e.g., "123.45입니다.") because it treats any period as a sentence terminator; update the pattern used by _SENTENCE_RE to avoid matching periods that are part of numeric decimals by adding a negative lookbehind for digits (so a period is only considered a terminator when not preceded by a digit) and/or require whitespace or end-of-string after the terminator for Korean endings like "다.", "요.", "니다."; modify the _SENTENCE_RE definition accordingly to use a negative lookbehind for digits and to validate what follows the punctuation to prevent splitting inside numbers.app/graph/nodes/tool/fetch_places_node.py (2)
41-45: 동기requests호출이 비동기 파이프라인에서 블로킹될 수 있습니다.이 노드가
asyncio.to_thread를 통해 호출되는 경우에는 문제가 없지만, 직접 async 컨텍스트에서 호출되면 이벤트 루프를 블로킹합니다. 향후httpx또는aiohttp로 전환을 고려해 보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/graph/nodes/tool/fetch_places_node.py` around lines 41 - 45, The synchronous requests.get call (resp = requests.get(...)) blocks the event loop in async contexts; update the fetch_places_node function to perform the HTTP request off the event loop (e.g., wrap the blocking call in asyncio.to_thread when using requests) or convert the node to use an async HTTP client (e.g., httpx.AsyncClient or aiohttp.ClientSession) and replace requests.get with an awaitable async call to avoid blocking; ensure you keep the same params and timeout handling and return shape so callers of fetch_places_node (or the resp handling logic) continue to work.
68-71: 예외 처리는 적절하나, 디버깅을 위한 로깅 개선을 권장합니다.외부 서비스 호출에서 모든 예외를 catch하여 파이프라인 실패를 방지하는 것은 올바른 접근입니다. 다만, 현재는 예외 타입만 로깅되어 문제 진단이 어려울 수 있습니다.
♻️ 에러 메시지 로깅 추가
except Exception as e: - print(f"[fetch_places] 오류: {type(e).__name__}") + print(f"[fetch_places] 오류: {type(e).__name__}: {e}") trace["fetch_places"] = {"status": "error", "error": "core_places_failed"} return {"nearby_places": [], "trace": trace}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/graph/nodes/tool/fetch_places_node.py` around lines 68 - 71, The except block in fetch_places (the except Exception as e handler) only prints the exception type and sets trace to "core_places_failed"; replace the simple print with structured logging that includes the exception message and stack trace (e.g., use logger.exception or logger.error(..., exc_info=True)) and augment the trace entry (trace["fetch_places"]) to include the actual error message (and optionally a short stack/identifier) so debugging external service failures is easier; update references in the function/method handling fetch_places to use the module logger rather than print and keep returning the same result shape.app/graph/nodes/answer/struct_db_node.py (2)
34-36:no_context반환 시emotion과category기본값 누락 가능성.
nearby_places가 없을 때emotion과category를 반환하지 않습니다. 반면 Lines 172-178의 성공 케이스에서는 이 필드들을 포함합니다.GraphState에서emotion: str과category: str이 필수 필드로 정의되어 있다면, 상태 일관성을 위해 기본값을 반환하는 것이 좋습니다.♻️ 수정 제안
if not nearby_places: trace["struct_db"] = {"status": "no_context", "query": text} - return {"answer_text": "", "check_result": "bad", "trace": trace} + return { + "answer_text": "", + "place_id": None, + "emotion": "GUIDING", + "category": "DIRECTION", + "check_result": "bad", + "trace": trace, + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/graph/nodes/answer/struct_db_node.py` around lines 34 - 36, When nearby_places is empty, the function returns without including required GraphState fields emotion and category; update the early-return block in struct_db_node.py (the branch that sets trace["struct_db"] = {"status": "no_context", "query": text} and returns {"answer_text": "", "check_result": "bad", "trace": trace}) to also include default values for emotion and category (e.g., empty strings or defined defaults) so the returned state shape matches the successful branch and GraphState's required fields.
159-161:llm_failed반환 시에도emotion과category기본값 누락.동일하게
llm_failed케이스에서도 일관성을 위해 기본값을 포함하는 것이 좋습니다.♻️ 수정 제안
if not answer_text: trace["struct_db"] = {"status": "llm_failed", "method": method, "error": error_msg} - return {"answer_text": "", "check_result": "bad", "trace": trace} + return { + "answer_text": "", + "place_id": None, + "emotion": "GUIDING", + "category": "DIRECTION", + "check_result": "bad", + "trace": trace, + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/graph/nodes/answer/struct_db_node.py` around lines 159 - 161, The llm_failed branch omits default emotion and category values; update the branch in the function that sets trace["struct_db"] (the block returning {"answer_text": "", "check_result": "bad", "trace": trace}) to include default keys for emotion and category both in trace["struct_db"] (e.g., "emotion": "", "category": "") and in the returned payload so the response shape matches success cases; ensure you modify the same block that sets "status": "llm_failed" and uses variables method and error_msg.app/core/services/tts_google.py (1)
35-36: 빈 텍스트 폴백 메시지가 한국어로 하드코딩되어 있습니다.
language_code가 비한국어로 전달되어도 빈 텍스트 시 한국어 메시지("음성으로 변환할 내용이 없습니다.")가 사용됩니다. 다국어 지원을 고려하면language_code에 맞는 폴백 메시지를 사용하거나, 호출자 측에서 빈 텍스트를 처리하도록 하는 것이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/core/services/tts_google.py` around lines 35 - 36, The fallback for empty text is hardcoded to Korean; update the empty-text handling in the function that sets `text` (in app/core/services/tts_google.py) to use `language_code` instead of a fixed Korean string: either map common language codes to localized fallback messages based on `language_code`, or raise/return an error so the caller handles empty input; ensure the branch that currently does `if not text or not text.strip(): text = "음성으로 변환할 내용이 없습니다."` uses the `language_code` variable to select the appropriate message or propagates an error to the caller.app/core/dependencies.py (1)
18-18:OPENAI_API_KEY누락 시 명확한 에러 메시지 고려.
os.environ["OPENAI_API_KEY"]는 키가 없을 때KeyError를 발생시킵니다. 모듈 임포트 시점에 실패하므로 빠른 피드백이 되지만, 더 명확한 에러 메시지를 제공하는 것이 디버깅에 도움이 됩니다.💡 대안 제안
api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise ValueError("OPENAI_API_KEY environment variable is required") client = OpenAI(api_key=api_key)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/core/dependencies.py` at line 18, The code instantiates client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) which raises a generic KeyError on import; change to read the key via os.getenv (e.g., api_key = os.getenv("OPENAI_API_KEY")), validate that api_key is present and if not raise a clear ValueError("OPENAI_API_KEY environment variable is required"), then pass that api_key into OpenAI(...) to create client; update references to the client variable as needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.gitignore:
- Around line 18-19: The .gitignore currently contains a broad "*.json" rule
which will exclude important config and schema files; remove the "*.json" entry
and instead explicitly ignore only credential files like
"google-credentials.json" (and any other secrets you have), or if you must keep
a wildcard, add explicit allow-lists for important files such as "package.json",
"tsconfig.json", and your API/schema/test fixture files (e.g., using negation
patterns like "!package.json" or listing "schemas/*.json") so that only
sensitive JSONs are ignored and config/artifact JSONs remain tracked.
In `@app/api/document_legacy.py`:
- Around line 60-61: The legacy endpoint reads the entire upload into memory via
await file.read() (producing pdf_bytes and file_hash); change this to stream the
upload in chunks while updating a SHA256 hasher and accumulated size, enforce
the same 50MB guard used in app/core/services/document_processor.py (or return
HTTP 413 when exceeded), and use the final hexdigest as file_hash—apply the same
chunked/hash approach to both locations where await file.read() is used to avoid
unbounded memory usage.
In `@app/api/document.py`:
- Around line 39-41: The current check lets requests through when
_INTERNAL_TOKEN is empty, causing a fail-open; change the logic in the request
validation around _INTERNAL_TOKEN and x_internal_token so that absence of
_INTERNAL_TOKEN results in rejection by default (raise HTTPException 403) unless
an explicit DEBUG/dev flag is true; when _INTERNAL_TOKEN is set require
x_internal_token to match it; ensure this replaces the current unconditional
pass and does not rely on allowlist for bypass (refer to symbols
_INTERNAL_TOKEN, x_internal_token, allowlist, and introduce/consult DEBUG).
In `@app/api/qa.py`:
- Around line 119-135: The code currently treats any result with
result["check_result"] != "good" as not found and always returns a fallback;
instead, preserve generated answer_text unless it's empty: compute answer =
result.get("answer_text", "") and set answer_found = bool(answer) (or use answer
!= "") rather than result.get("check_result") == "good"; then when building
InternalQaResponse only apply fallback_answers.get(lang2, ...) if answer is
empty, and set emotion to "GUIDING" when answer is present otherwise "SORRY";
keep other fields (placeId, language/lang2, category, answerFound) as before.
In `@app/api/websocket.py`:
- Around line 157-163: send_tts_chunks currently computes is_final by treating
the provided sentences list as the entire stream, which causes realtime mode
(where sentences are sent one-by-one) to mark every chunk as final; change
send_tts_chunks signature to accept an explicit boolean like is_last_sentence
(or is_final_override) and use that to set is_final for the last chunk, and
update the realtime call sites (the realtime branch that invokes send_tts_chunks
around the areas referenced in the diff: the caller(s) near the existing
realtime logic at the first occurrence and the other two spots mentioned) to
pass True only for the actual last sentence and False otherwise so clients
receive correct final flags.
- Around line 539-545: The exception handler that sends errors to the client
(the block around websocket.send_text in app/api/websocket.py) currently uses
str(e) which may leak sensitive upstream details; change it to return a fixed,
non-sensitive client-facing message (e.g., "An internal error occurred") and
keep the trace_id, while logging the full exception and stack trace on the
server side (use the existing logger with trace_id) instead of including str(e)
in the JSON payload; update the payload construction in the websocket.send_text
call to remove or replace the "message" value and ensure the server log captures
e and traceback together.
- Around line 273-285: The audio queue is created without a bound which can let
incoming audio accumulate indefinitely; change the creation of audio_q to use a
bounded queue (e.g., asyncio.Queue(maxsize=YOUR_DESIRED_LIMIT)) so backpressure
prevents unbounded memory growth, and make the limit configurable (via a
constant or config value) so it can be tuned; update any producer code that
calls audio_q.put_nowait() to use await audio_q.put() (or handle QueueFull) and
keep references like audio_q, audio_receiver, and recv_task in place.
- Around line 135-144: The JSON parsing of control frames using json.loads in
audio_receiver can raise and cause the function to exit without placing the None
sentinel into audio_q, deadlocking stt.stream_events; wrap the json.loads(...)
call (and any immediate processing of the control frame) in a try/except (catch
json.JSONDecodeError or Exception), and in the except ensure you always await
audio_q.put(None) to unblock the consumer and then re-raise or propagate the
error so the failure is visible to the caller; make this change in the
audio_receiver code path that reads msg["text"] and handles msg_type == "stop".
In `@app/core/DB/PDF2db_v2.py`:
- Around line 351-360: The code prints sensitive summary content and section
titles (variables raw_content, raw, section_title) for every chunk; change this
so only non-sensitive metadata (like finish_reason and raw_content length) is
always logged, and the actual summary text or section title is only emitted when
an explicit debug flag is enabled (e.g., a module-level DEBUG_SUMMARY_LOGGING or
logger.isEnabledFor(logging.DEBUG)). Update the print calls in the block that
reads raw_content/finish_reason to use structured logging of
length/finish_reason and replace raw/section_title prints with conditional
debug-only logs tied to that flag or debug logger level (ensure raw_content is
never printed in normal ops). Also keep the existing None check and safe
fallback but avoid printing raw snippets except under the debug guard.
- Around line 137-158: The branch that returns early when there are no header
matches skips the common split/merge logic and causes very long chunks; instead
of returning immediately in the if not matches block, create a synthetic section
(e.g., one or multiple sections from paragraphs with "section_title" like "본문"
or "본문 {i+1}") and add them to the same sections list used by the downstream
splitting/merging code (the logic that handles max_chunk_size and merging short
chunks), then let the function continue into the existing split/merge pipeline
(i.e., do not return from inside the no-matches branch); locate the code around
md_text/matches handling in PDF2db_v2.py and replace the early returns with code
that constructs synthetic section dicts and appends them so the rest of the
function (split/merge routines) runs normally.
In `@app/core/services/document_processor.py`:
- Around line 165-172: The code reads the entire HTTP response into memory via
resp.content before enforcing MAX_PDF_BYTES, which can blow memory and
bandwidth; change the download to stream the response (use
httpx.AsyncClient.get(..., stream=True) and iterate
resp.aiter_bytes()/aiter_raw()) accumulate bytes into pdf_bytes (or a BytesIO)
while tracking total_size, and if total_size exceeds MAX_PDF_BYTES immediately
close/aiterator/raise ValueError to abort the download; ensure you still call
resp.aclose() or use "async with" for the streamed response and keep the rest of
the logic (PDF size check and subsequent use of pdf_bytes) intact.
- Around line 99-104: The embedding input in document_processor.py builds
search_text with f"{section_title} {summary} {' '.join(keywords)}" which is the
old format and causes inconsistent vectors; replace that inline construction by
calling the existing helper build_search_text(section_title, summary, keywords)
so the tb_doc_chunk_v2/pipeline_version="v2" path uses the same
"[section]/[summary]/[keywords]" format as PDF2db_v2.py; update the code that
calls openai_client_v2.embeddings.create(...) to pass the build_search_text
result and remove the manual join to ensure all v2 pipelines use identical input
formatting.
In `@app/core/ws_stt_test.py`:
- Around line 118-122: The prints compute latencies by subtracting timestamps
but can raise TypeError when the other operand is None; update the checks to
ensure both timestamps exist before computing/subtracting: for the first latency
verify both t_llm_first and t_stt_final are not None before printing, and for
the second verify both t_tts_first and t_llm_first are not None before printing
(references: t_llm_first, t_stt_final, t_tts_first).
In `@app/graph/nodes/answer/event_node.py`:
- Around line 150-157: The code passes a naive datetime string to
_build_event_messages (now_str=datetime.now().strftime(...)) which can cause
day-boundary errors across timezones; change to supply a timezone-aware
timestamp and prefer using the site/site-config timezone injected into this node
(fallback to UTC). Modify the call that builds messages in event_node.py to
obtain the timezone-aware now via the node/state/config (e.g., site_tz) and
format it with an offset (ISO 8601 or include %z) when setting now_str; if no
site timezone is available, use utcnow() with UTC tzinfo so
_build_event_messages always receives an offset-aware timestamp. Ensure the
unique identifiers _build_event_messages and now_str are updated accordingly.
In `@app/graph/nodes/answer/struct_db_node.py`:
- Line 98: The code assigns persona_block using an unnecessary f-string:
persona_block = "\n".join(persona_lines) if persona_lines else f"You are a
friendly tourist guide assistant."; change the else branch to a plain string
literal (remove the leading f) so the fallback becomes "You are a friendly
tourist guide assistant." to eliminate the unused f-string. Ensure this change
is applied where persona_block and persona_lines are used in struct_db_node.py.
In `@app/graph/nodes/classify/intent_gate.py`:
- Around line 69-78: json.loads(raw) can return non-dict values (list/str) and
the subsequent parsed.get(...) will raise AttributeError; update the logic after
llm.chat(...) to validate that parsed is a dict and if not set ranking =
_DEFAULT_RANKING and place_category = None before attempting parsed.get; use the
same symbols (llm.chat, parsed, ranking, place_category, _DEFAULT_RANKING,
_ALLOWED_PLACE_CATEGORIES) so you locate the block, then only call parsed.get
when isinstance(parsed, dict) and otherwise fall back safely.
In `@audio/m4a.py`:
- Around line 27-33: The subprocess.run call that executes ffmpeg (the call
using cmd and storing result in result) must include a timeout and explicit
exception handling: wrap the call in a try/except that catches
subprocess.TimeoutExpired and FileNotFoundError (and optionally other
Exceptions), use a sensible timeout constant (e.g., FFMPEG_TIMEOUT) when calling
subprocess.run(..., timeout=FFMPEG_TIMEOUT, capture_output=True, text=True), and
on TimeoutExpired and FileNotFoundError log distinct, clear messages referencing
the filename (and the expected FFMPEG_EXE) instead of treating them the same as
a non-zero return code; preserve the existing success branch that prints saved
output_path and keep the non-zero returncode handling for ffmpeg failures.
- Around line 1-7: The constant FFMPEG_EXE in audio/m4a.py is a hardcoded,
user-specific absolute path; remove that literal and instead resolve ffmpeg at
runtime by first checking an environment variable (e.g. FFMPEG_EXE or
FFMPEG_PATH) and then falling back to locating "ffmpeg" on PATH (shutil.which or
equivalent) or a sensible default command string; update any code that
references FFMPEG_EXE to use the resolved value and raise a clear error if
ffmpeg cannot be found.
- Around line 8-35: The module runs the conversion loop on import, causing side
effects; wrap the existing top-level logic (the os.makedirs call, the for
filename in os.listdir(INPUT_DIR) loop, subprocess.run invocation and related
variables like INPUT_DIR, OUTPUT_DIR, FFMPEG_EXE) into a function named main()
and add an execution guard if __name__ == "__main__": main() so the conversion
only runs when executed as a script and not when imported.
---
Nitpick comments:
In `@app/core/dependencies.py`:
- Line 18: The code instantiates client =
OpenAI(api_key=os.environ["OPENAI_API_KEY"]) which raises a generic KeyError on
import; change to read the key via os.getenv (e.g., api_key =
os.getenv("OPENAI_API_KEY")), validate that api_key is present and if not raise
a clear ValueError("OPENAI_API_KEY environment variable is required"), then pass
that api_key into OpenAI(...) to create client; update references to the client
variable as needed.
In `@app/core/services/llm_openai.py`:
- Line 72: 함수 chat의 시그니처에서 암묵적 Optional인 `max_tokens: int = None`을 명시적으로 바꿔 PEP
484 규약을 따르세요: `max_tokens: Optional[int] = None`로 변경하고 파일 상단(또는 타입 임포트 위치)에
`from typing import Optional`를 추가하여 타입이 정의되도록 하세요; 대상 심볼은 메서드 이름 `chat`을 수정하면
됩니다.
- Around line 20-23: The current sentence-splitting regex _SENTENCE_RE can split
on decimal points (e.g., "123.45입니다.") because it treats any period as a
sentence terminator; update the pattern used by _SENTENCE_RE to avoid matching
periods that are part of numeric decimals by adding a negative lookbehind for
digits (so a period is only considered a terminator when not preceded by a
digit) and/or require whitespace or end-of-string after the terminator for
Korean endings like "다.", "요.", "니다."; modify the _SENTENCE_RE definition
accordingly to use a negative lookbehind for digits and to validate what follows
the punctuation to prevent splitting inside numbers.
In `@app/core/services/tts_google.py`:
- Around line 35-36: The fallback for empty text is hardcoded to Korean; update
the empty-text handling in the function that sets `text` (in
app/core/services/tts_google.py) to use `language_code` instead of a fixed
Korean string: either map common language codes to localized fallback messages
based on `language_code`, or raise/return an error so the caller handles empty
input; ensure the branch that currently does `if not text or not text.strip():
text = "음성으로 변환할 내용이 없습니다."` uses the `language_code` variable to select the
appropriate message or propagates an error to the caller.
In `@app/graph/nodes/answer/struct_db_node.py`:
- Around line 34-36: When nearby_places is empty, the function returns without
including required GraphState fields emotion and category; update the
early-return block in struct_db_node.py (the branch that sets trace["struct_db"]
= {"status": "no_context", "query": text} and returns {"answer_text": "",
"check_result": "bad", "trace": trace}) to also include default values for
emotion and category (e.g., empty strings or defined defaults) so the returned
state shape matches the successful branch and GraphState's required fields.
- Around line 159-161: The llm_failed branch omits default emotion and category
values; update the branch in the function that sets trace["struct_db"] (the
block returning {"answer_text": "", "check_result": "bad", "trace": trace}) to
include default keys for emotion and category both in trace["struct_db"] (e.g.,
"emotion": "", "category": "") and in the returned payload so the response shape
matches success cases; ensure you modify the same block that sets "status":
"llm_failed" and uses variables method and error_msg.
In `@app/graph/nodes/tool/fetch_places_node.py`:
- Around line 41-45: The synchronous requests.get call (resp =
requests.get(...)) blocks the event loop in async contexts; update the
fetch_places_node function to perform the HTTP request off the event loop (e.g.,
wrap the blocking call in asyncio.to_thread when using requests) or convert the
node to use an async HTTP client (e.g., httpx.AsyncClient or
aiohttp.ClientSession) and replace requests.get with an awaitable async call to
avoid blocking; ensure you keep the same params and timeout handling and return
shape so callers of fetch_places_node (or the resp handling logic) continue to
work.
- Around line 68-71: The except block in fetch_places (the except Exception as e
handler) only prints the exception type and sets trace to "core_places_failed";
replace the simple print with structured logging that includes the exception
message and stack trace (e.g., use logger.exception or logger.error(...,
exc_info=True)) and augment the trace entry (trace["fetch_places"]) to include
the actual error message (and optionally a short stack/identifier) so debugging
external service failures is easier; update references in the function/method
handling fetch_places to use the module logger rather than print and keep
returning the same result shape.
In `@audio/m4a.py`:
- Around line 10-12: The loop over os.listdir(INPUT_DIR) currently only checks
filename.lower().endswith(".m4a") which can match directories named *.m4a;
update the condition to also verify the path is a regular file by computing
input_path = os.path.join(INPUT_DIR, filename) and adding
os.path.isfile(input_path) to the conditional so only real files (not
directories) are processed; ensure you reference INPUT_DIR, filename and
input_path when making the change.
In `@record.py`:
- Line 6: The FILENAME constant is hardcoded ("궁성담장길이.wav") which can break
consumers expecting different paths (e.g., WAV_PATH in app/core/ws_stt_test.py);
change FILENAME to be configurable by reading from an environment variable, a
configuration object, or accepting it as a function/class parameter and provide
a sensible default, and update any callers to pass the expected path (or read
the same config) so both record.py's FILENAME and ws_stt_test.py's WAV_PATH come
from the same configurable source.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: abf45233-3225-4238-9495-cd26adc93165
⛔ Files ignored due to path filters (2)
input.wavis excluded by!**/*.wavout.mp3is excluded by!**/*.mp3
📒 Files selected for processing (56)
.coderabbit.yaml.gitignoreapp/api/__init__.pyapp/api/document.pyapp/api/document_legacy.pyapp/api/health.pyapp/api/qa.pyapp/api/websocket.pyapp/core/DB/PDF2db_v2.pyapp/core/dependencies.pyapp/core/main_fastapi.pyapp/core/services/document_processor.pyapp/core/services/llm_openai.pyapp/core/services/pipeline.pyapp/core/services/stt_google.pyapp/core/services/tts_google.pyapp/core/ws_stt_test.pyapp/graph/graph_builder.pyapp/graph/nodes/answer/__init__.pyapp/graph/nodes/answer/answer_check.pyapp/graph/nodes/answer/answer_generate.pyapp/graph/nodes/answer/clarify_node.pyapp/graph/nodes/answer/event_node.pyapp/graph/nodes/answer/smalltalk_node.pyapp/graph/nodes/answer/struct_db_node.pyapp/graph/nodes/answer_compose.pyapp/graph/nodes/classify/__init__.pyapp/graph/nodes/classify/intent_gate.pyapp/graph/nodes/direct_llm_node.pyapp/graph/nodes/infotype_gate.pyapp/graph/nodes/input/__init__.pyapp/graph/nodes/input/normalize_node.pyapp/graph/nodes/input/stt_node.pyapp/graph/nodes/intent_gate.pyapp/graph/nodes/map_tool_node.pyapp/graph/nodes/output/__init__.pyapp/graph/nodes/output/tts_builder.pyapp/graph/nodes/query_rewrite.pyapp/graph/nodes/rag/__init__.pyapp/graph/nodes/rag/context_pack.pyapp/graph/nodes/rag/retrieve_node.pyapp/graph/nodes/rag/retrieve_node_v2.pyapp/graph/nodes/rag/translate_node.pyapp/graph/nodes/smalltalk_node.pyapp/graph/nodes/struct_db_node.pyapp/graph/nodes/tool/__init__.pyapp/graph/nodes/tool/fetch_places_node.pyapp/graph/routers/answer_check_router.pyapp/graph/routers/fallback_router.pyapp/graph/routers/infotype_router.pyapp/graph/routers/intent_router.pyapp/graph/state.pyapp/main.pyaudio/m4a.pyrecord.pyseed_gyeongbokgung_test.sql
💤 Files with no reviewable changes (10)
- app/graph/nodes/struct_db_node.py
- app/graph/nodes/map_tool_node.py
- app/graph/routers/infotype_router.py
- app/graph/nodes/answer_compose.py
- app/graph/nodes/direct_llm_node.py
- app/graph/nodes/query_rewrite.py
- app/graph/nodes/intent_gate.py
- app/graph/nodes/smalltalk_node.py
- app/graph/nodes/infotype_gate.py
- app/core/main_fastapi.py
| google-credentials.json | ||
| *.json |
There was a problem hiding this comment.
*.json 패턴이 너무 광범위합니다.
모든 JSON 파일을 무시하면 다음과 같은 중요한 파일들이 실수로 추적에서 제외될 수 있습니다:
package.json,tsconfig.json등 설정 파일- API 스키마 정의 파일
- 테스트 fixture 데이터
자격 증명 파일만 명시적으로 나열하는 것이 더 안전합니다.
🔧 권장 수정안
# credentials & environment
google_key.json
google-credentials.json
-*.json
+service-account*.json
+credentials*.json
.env🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.gitignore around lines 18 - 19, The .gitignore currently contains a broad
"*.json" rule which will exclude important config and schema files; remove the
"*.json" entry and instead explicitly ignore only credential files like
"google-credentials.json" (and any other secrets you have), or if you must keep
a wildcard, add explicit allow-lists for important files such as "package.json",
"tsconfig.json", and your API/schema/test fixture files (e.g., using negation
patterns like "!package.json" or listing "schemas/*.json") so that only
sensitive JSONs are ignored and config/artifact JSONs remain tracked.
| f"- CRITICAL: The style MUST be visible in your response.\n" | ||
| f"- CRITICAL: Do NOT use the original Korean words — always translate them into {lang_name}." | ||
| ) | ||
| persona_block = "\n".join(persona_lines) if persona_lines else f"You are a friendly tourist guide assistant." |
There was a problem hiding this comment.
불필요한 f-string 접두사를 제거하세요.
플레이스홀더가 없는 f-string입니다.
🔧 수정 제안
- persona_block = "\n".join(persona_lines) if persona_lines else f"You are a friendly tourist guide assistant."
+ persona_block = "\n".join(persona_lines) if persona_lines else "You are a friendly tourist guide assistant."📝 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.
| persona_block = "\n".join(persona_lines) if persona_lines else f"You are a friendly tourist guide assistant." | |
| persona_block = "\n".join(persona_lines) if persona_lines else "You are a friendly tourist guide assistant." |
🧰 Tools
🪛 Ruff (0.15.9)
[error] 98-98: f-string without any placeholders
Remove extraneous f prefix
(F541)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/graph/nodes/answer/struct_db_node.py` at line 98, The code assigns
persona_block using an unnecessary f-string: persona_block =
"\n".join(persona_lines) if persona_lines else f"You are a friendly tourist
guide assistant."; change the else branch to a plain string literal (remove the
leading f) so the fallback becomes "You are a friendly tourist guide assistant."
to eliminate the unused f-string. Ensure this change is applied where
persona_block and persona_lines are used in struct_db_node.py.
| raw = llm.chat(messages, max_tokens=60) | ||
| parsed = json.loads(raw) | ||
| ranking = parsed.get("ranking", _DEFAULT_RANKING) | ||
| # 1순위가 struct_db일 때만 place_category 적용, 허용 enum 외 값은 None | ||
| raw_category = parsed.get("place_category") | ||
| place_category = ( | ||
| raw_category.upper() | ||
| if isinstance(raw_category, str) and raw_category.upper() in _ALLOWED_PLACE_CATEGORIES | ||
| else None | ||
| ) |
There was a problem hiding this comment.
유효한 JSON이어도 객체가 아니면 여기서 바로 실패합니다.
Line 70의 json.loads()는 리스트나 문자열도 정상 반환할 수 있는데, Line 71에서 바로 parsed.get(...)를 호출하고 있습니다. 모델이 ["rag", ...]처럼 shape만 어긋난 JSON을 내놓으면 AttributeError로 노드 전체가 깨지고, 아래 fallback 경로도 타지 못합니다. dict 여부를 먼저 검증하고 아니면 _DEFAULT_RANKING으로 강등하는 편이 안전합니다.
💡 예시 수정안
try:
raw = llm.chat(messages, max_tokens=60)
parsed = json.loads(raw)
+ if not isinstance(parsed, dict):
+ parsed = {}
+ method = "llm_fallback_invalid_shape"
ranking = parsed.get("ranking", _DEFAULT_RANKING)🤖 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 69 - 78,
json.loads(raw) can return non-dict values (list/str) and the subsequent
parsed.get(...) will raise AttributeError; update the logic after llm.chat(...)
to validate that parsed is a dict and if not set ranking = _DEFAULT_RANKING and
place_category = None before attempting parsed.get; use the same symbols
(llm.chat, parsed, ranking, place_category, _DEFAULT_RANKING,
_ALLOWED_PLACE_CATEGORIES) so you locate the block, then only call parsed.get
when isinstance(parsed, dict) and otherwise fall back safely.
| import os | ||
| import subprocess | ||
|
|
||
| INPUT_DIR = "." | ||
| OUTPUT_DIR = "wav" | ||
| FFMPEG_EXE = r"C:\Users\hjsju\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-8.1-full_build\bin\ffmpeg.exe" | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "m4a.py" -type fRepository: Project-Guideon/guideon-AI
Length of output: 84
🏁 Script executed:
cat -n ./audio/m4a.pyRepository: Project-Guideon/guideon-AI
Length of output: 1323
Line 6의 하드코딩된 로컬 ffmpeg 절대 경로를 제거하세요.
C:\Users\hjsju\AppData\Local\...는 특정 PC에서만 동작하며, 사용자 이름이 노출됩니다. CI 환경과 다른 개발 머신에서 즉시 실패합니다.
대신 환경 변수나 PATH에서 ffmpeg를 찾도록 변경하세요:
제안 패치
import os
+import shutil
import subprocess
INPUT_DIR = "."
OUTPUT_DIR = "wav"
-FFMPEG_EXE = r"C:\Users\hjsju\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-8.1-full_build\bin\ffmpeg.exe"
+FFMPEG_EXE = os.getenv("FFMPEG_EXE") or shutil.which("ffmpeg")
+if not FFMPEG_EXE:
+ raise RuntimeError("ffmpeg 실행 파일을 찾지 못했습니다. PATH 또는 FFMPEG_EXE를 설정하세요.")📝 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.
| import os | |
| import subprocess | |
| INPUT_DIR = "." | |
| OUTPUT_DIR = "wav" | |
| FFMPEG_EXE = r"C:\Users\hjsju\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-8.1-full_build\bin\ffmpeg.exe" | |
| import os | |
| import shutil | |
| import subprocess | |
| INPUT_DIR = "." | |
| OUTPUT_DIR = "wav" | |
| FFMPEG_EXE = os.getenv("FFMPEG_EXE") or shutil.which("ffmpeg") | |
| if not FFMPEG_EXE: | |
| raise RuntimeError("ffmpeg 실행 파일을 찾지 못했습니다. PATH 또는 FFMPEG_EXE를 설정하세요.") | |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@audio/m4a.py` around lines 1 - 7, The constant FFMPEG_EXE in audio/m4a.py is
a hardcoded, user-specific absolute path; remove that literal and instead
resolve ffmpeg at runtime by first checking an environment variable (e.g.
FFMPEG_EXE or FFMPEG_PATH) and then falling back to locating "ffmpeg" on PATH
(shutil.which or equivalent) or a sensible default command string; update any
code that references FFMPEG_EXE to use the resolved value and raise a clear
error if ffmpeg cannot be found.
| os.makedirs(OUTPUT_DIR, exist_ok=True) | ||
|
|
||
| for filename in os.listdir(INPUT_DIR): | ||
| if filename.lower().endswith(".m4a"): | ||
| input_path = os.path.join(INPUT_DIR, filename) | ||
| output_name = os.path.splitext(filename)[0] + ".wav" | ||
| output_path = os.path.join(OUTPUT_DIR, output_name) | ||
|
|
||
| print(f"Converting: {filename} → {output_name}") | ||
|
|
||
| cmd = [ | ||
| FFMPEG_EXE, | ||
| "-y", | ||
| "-i", input_path, | ||
| "-ac", "1", | ||
| "-ar", "16000", | ||
| output_path | ||
| ] | ||
|
|
||
| result = subprocess.run(cmd, capture_output=True, text=True) | ||
|
|
||
| if result.returncode != 0: | ||
| print(f"❌ failed: {filename}") | ||
| print(result.stderr) | ||
| else: | ||
| print(f"✅ saved: {output_path}") | ||
|
|
||
| print("done") No newline at end of file |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "m4a.py" -type fRepository: Project-Guideon/guideon-AI
Length of output: 84
🏁 Script executed:
cat -n ./audio/m4a.pyRepository: Project-Guideon/guideon-AI
Length of output: 1323
🏁 Script executed:
rg "from audio import|import audio\.m4a|import audio" --type pyRepository: Project-Guideon/guideon-AI
Length of output: 52
🏁 Script executed:
rg "m4a" --type pyRepository: Project-Guideon/guideon-AI
Length of output: 124
모듈 import 시 자동 실행되는 부작용을 분리해주세요.
현재 audio/m4a.py를 import하는 순간 파일 변환 루프가 실행됩니다. 코드 블록 전체를 main() 함수로 감싼 후 if __name__ == "__main__": 가드를 추가하여 직접 실행할 때만 동작하도록 변경해야 합니다.
제안 패치
-os.makedirs(OUTPUT_DIR, exist_ok=True)
-
-for filename in os.listdir(INPUT_DIR):
- if filename.lower().endswith(".m4a"):
- input_path = os.path.join(INPUT_DIR, filename)
- output_name = os.path.splitext(filename)[0] + ".wav"
- output_path = os.path.join(OUTPUT_DIR, output_name)
-
- print(f"Converting: {filename} → {output_name}")
-
- cmd = [
- FFMPEG_EXE,
- "-y",
- "-i", input_path,
- "-ac", "1",
- "-ar", "16000",
- output_path
- ]
-
- result = subprocess.run(cmd, capture_output=True, text=True)
-
- if result.returncode != 0:
- print(f"❌ failed: {filename}")
- print(result.stderr)
- else:
- print(f"✅ saved: {output_path}")
-
-print("done")
+def main() -> None:
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
+ for filename in os.listdir(INPUT_DIR):
+ if filename.lower().endswith(".m4a"):
+ input_path = os.path.join(INPUT_DIR, filename)
+ output_name = os.path.splitext(filename)[0] + ".wav"
+ output_path = os.path.join(OUTPUT_DIR, output_name)
+ print(f"Converting: {filename} → {output_name}")
+ # ... existing conversion logic ...
+ print("done")
+
+if __name__ == "__main__":
+ main()🧰 Tools
🪛 Ruff (0.15.9)
[error] 27-27: subprocess call: check for execution of untrusted input
(S603)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@audio/m4a.py` around lines 8 - 35, The module runs the conversion loop on
import, causing side effects; wrap the existing top-level logic (the os.makedirs
call, the for filename in os.listdir(INPUT_DIR) loop, subprocess.run invocation
and related variables like INPUT_DIR, OUTPUT_DIR, FFMPEG_EXE) into a function
named main() and add an execution guard if __name__ == "__main__": main() so the
conversion only runs when executed as a script and not when imported.
| result = subprocess.run(cmd, capture_output=True, text=True) | ||
|
|
||
| if result.returncode != 0: | ||
| print(f"❌ failed: {filename}") | ||
| print(result.stderr) | ||
| else: | ||
| print(f"✅ saved: {output_path}") |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "m4a.py" | head -20Repository: Project-Guideon/guideon-AI
Length of output: 84
🏁 Script executed:
cat -n ./audio/m4a.pyRepository: Project-Guideon/guideon-AI
Length of output: 1323
subprocess.run() 호출에 timeout과 예외 처리를 추가하세요.
Line 27의 subprocess.run()은 timeout 없이 ffmpeg가 무한 대기할 수 있으며, FFMPEG_EXE 경로가 존재하지 않으면 FileNotFoundError가 발생합니다. 배치 변환 스크립트에서는 실패 원인을 명시적으로 분리하여 처리하는 것이 필수입니다.
제안 패치
- result = subprocess.run(cmd, capture_output=True, text=True)
-
- if result.returncode != 0:
- print(f"❌ failed: {filename}")
- print(result.stderr)
- else:
- print(f"✅ saved: {output_path}")
+ try:
+ result = subprocess.run(
+ cmd,
+ capture_output=True,
+ text=True,
+ timeout=120,
+ check=False,
+ )
+ except FileNotFoundError:
+ print("❌ failed: ffmpeg 실행 파일을 찾지 못했습니다.")
+ break
+ except subprocess.TimeoutExpired:
+ print(f"❌ failed: {filename} (timeout)")
+ continue
+
+ if result.returncode != 0:
+ print(f"❌ failed: {filename}")
+ print(result.stderr)
+ else:
+ print(f"✅ saved: {output_path}")📝 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.
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| if result.returncode != 0: | |
| print(f"❌ failed: {filename}") | |
| print(result.stderr) | |
| else: | |
| print(f"✅ saved: {output_path}") | |
| try: | |
| result = subprocess.run( | |
| cmd, | |
| capture_output=True, | |
| text=True, | |
| timeout=120, | |
| check=False, | |
| ) | |
| except FileNotFoundError: | |
| print("❌ failed: ffmpeg 실행 파일을 찾지 못했습니다.") | |
| break | |
| except subprocess.TimeoutExpired: | |
| print(f"❌ failed: {filename} (timeout)") | |
| continue | |
| if result.returncode != 0: | |
| print(f"❌ failed: {filename}") | |
| print(result.stderr) | |
| else: | |
| print(f"✅ saved: {output_path}") |
🧰 Tools
🪛 Ruff (0.15.9)
[error] 27-27: subprocess call: check for execution of untrusted input
(S603)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@audio/m4a.py` around lines 27 - 33, The subprocess.run call that executes
ffmpeg (the call using cmd and storing result in result) must include a timeout
and explicit exception handling: wrap the call in a try/except that catches
subprocess.TimeoutExpired and FileNotFoundError (and optionally other
Exceptions), use a sensible timeout constant (e.g., FFMPEG_TIMEOUT) when calling
subprocess.run(..., timeout=FFMPEG_TIMEOUT, capture_output=True, text=True), and
on TimeoutExpired and FileNotFoundError log distinct, clear messages referencing
the filename (and the expected FFMPEG_EXE) instead of treating them the same as
a non-zero return code; preserve the existing success branch that prints saved
output_path and keep the non-zero returncode handling for ffmpeg failures.
- 스프링 bff에서 이미 검증하고 있으므로 중복 검증되어 삭제하였습니다. -PDFdb_v2에 맞춰서 build_search_text 추가했습니다.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/core/services/document_processor.py`:
- Around line 191-193: The exception message passed to _callback_core as str(e)
may leak sensitive data (signed URL tokens, API keys, DB details); sanitize the
exception before sending by reusing the same URL redaction logic used elsewhere
(urlsplit/urlunsplit) and stripping or redacting known sensitive patterns (query
strings, Authorization/API keys, long JSON bodies) so only a safe summary is
stored; update the try/except handler that currently calls await
_callback_core(site_id, doc_id, "FAILED", str(e)) to build a redacted_message
(e.g., remove query params from any URLs in the exception, truncate long texts,
and mask token-like substrings) and pass that redacted_message to _callback_core
instead of raw str(e).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: df17d0a0-beaf-4f5c-8f8d-cb891d096f13
📒 Files selected for processing (1)
app/core/services/document_processor.py
- 재시도 로직:콜백 실패 시 0.5초, 1초 간격으로 최대 3회 재시도 - 에러 메시지 정제 : URL에 토큰/쿼리 파라미터 포함된 경우 제거 후 500자 제한
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
app/core/services/document_processor.py (1)
195-198:⚠️ Potential issue | 🟠 Major
_sanitize_failed_reason()가 “문장 내부 URL”의 query/token을 제거하지 못합니다.Line 196-197 로직은
reason문자열 전체가 URL일 때만 정제됩니다. 예외 메시지에 URL이 일부로 포함되면 query가 그대로 남을 수 있습니다.🔧 제안 수정안
+import re + +_URL_RE = re.compile(r"https?://[^\s)]+") + def _sanitize_failed_reason(reason: str, max_len: int = 500) -> str: - p = urlsplit(reason) - scrubbed = urlunsplit((p.scheme, p.netloc, p.path, "", "")) if p.scheme and p.netloc else reason + def _strip_query(m: re.Match[str]) -> str: + u = m.group(0) + p = urlsplit(u) + return urlunsplit((p.scheme, p.netloc, p.path, "", "")) + + scrubbed = _URL_RE.sub(_strip_query, reason) return scrubbed[:max_len]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/core/services/document_processor.py` around lines 195 - 198, The _sanitize_failed_reason function only strips query/fragments when the whole reason string parses as a URL; update it to detect and sanitize URLs embedded inside a larger sentence by finding URL substrings (e.g., via a URL regex), parsing each match with urlsplit and replacing the matched substring with urlunsplit((scheme, netloc, path, "", "")) so query/fragment/token parts are removed, then return the scrubbed result truncated to max_len; target the _sanitize_failed_reason function and its local variables (p, scrubbed) when applying this change.
🤖 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/core/services/document_processor.py`:
- Around line 83-87: Update the inaccurate comment around
rows_to_insert/generate_search_summary to match the real summarization model
used (not “GPT-4o”); open the code that defines the v2 summary model (referenced
as MODEL_NAME in PDF2db_v2.py) and replace “GPT-4o” in the comment with the
actual model identifier and any pertinent behavior (e.g., v2 summary model name,
temperature/settings, keyword extraction approach) so the comment accurately
reflects generate_search_summary and the embedding input composition ("section
title + summary + keywords").
---
Duplicate comments:
In `@app/core/services/document_processor.py`:
- Around line 195-198: The _sanitize_failed_reason function only strips
query/fragments when the whole reason string parses as a URL; update it to
detect and sanitize URLs embedded inside a larger sentence by finding URL
substrings (e.g., via a URL regex), parsing each match with urlsplit and
replacing the matched substring with urlunsplit((scheme, netloc, path, "", ""))
so query/fragment/token parts are removed, then return the scrubbed result
truncated to max_len; target the _sanitize_failed_reason function and its local
variables (p, scrubbed) when applying this change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9cd6b93e-ae68-41f6-b98a-bfb7e1589f4a
📒 Files selected for processing (1)
app/core/services/document_processor.py
| # - generate_search_summary: GPT-4o로 섹션 요약 + 키워드 추출 | ||
| # - 임베딩 대상: "섹션제목 + 요약 + 키워드" 합성 텍스트 | ||
| # (원문 대신 요약 텍스트를 임베딩하여 검색 정확도 향상) | ||
| # - 임베딩 모델: MODEL_NAME (PDF2db_v2.py에서 정의) | ||
| rows_to_insert = [] |
There was a problem hiding this comment.
주석의 모델 설명이 현재 구현과 불일치할 수 있습니다.
Line 83의 “GPT-4o” 설명은 실제 v2 요약 모델 설정과 다를 수 있어 운영/장애 분석 시 혼선을 줄 수 있습니다. 주석을 실제 모델 소스 기준으로 맞춰 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/core/services/document_processor.py` around lines 83 - 87, Update the
inaccurate comment around rows_to_insert/generate_search_summary to match the
real summarization model used (not “GPT-4o”); open the code that defines the v2
summary model (referenced as MODEL_NAME in PDF2db_v2.py) and replace “GPT-4o” in
the comment with the actual model identifier and any pertinent behavior (e.g.,
v2 summary model name, temperature/settings, keyword extraction approach) so the
comment accurately reflects generate_search_summary and the embedding input
composition ("section title + summary + keywords").
There was a problem hiding this comment.
추후 모델 개발 완성 후 추가하겠습니다. 이슈 부탁드립니다.
There was a problem hiding this comment.
🧩 Analysis chain
이슈를 생성하겠습니다.
🏁 Script executed:
gh issue create \
--repo Project-Guideon/guideon-AI \
--title "[document_processor] 주석의 모델 설명을 실제 요약 모델로 업데이트" \
--body "## 개요
\`app/core/services/document_processor.py\`의 Line 83 주석에 \`GPT-4o\`로 명시되어 있으나, 실제 v2 요약 모델(\`PDF2db_v2.py\`의 \`MODEL_NAME\` 기준)과 불일치할 수 있습니다. 운영 및 장애 분석 시 혼선을 줄이기 위해, 모델 개발 완료 후 주석을 실제 사용 모델 식별자 및 관련 설정(temperature, 키워드 추출 방식 등)에 맞게 수정이 필요합니다.
## 수정 대상
- 파일: \`app/core/services/document_processor.py\`
- 위치: Line 83 ~ 87 (\`generate_search_summary\` 관련 주석)
- 현재 내용: \"GPT-4o로 섹션 요약 + 키워드 추출\" 등 모델명 하드코딩
- 수정 방향: \`PDF2db_v2.py\`의 \`MODEL_NAME\` 또는 실제 요약 모델 식별자로 교체하고, 해당 모델의 주요 설정 및 동작을 반영
## 관련 링크
- PR: https://github.com/Project-Guideon/guideon-AI/pull/77
- 리뷰 코멘트: https://github.com/Project-Guideon/guideon-AI/pull/77#discussion_r3044762881
- 요청자: `@hwangtae123`" \
--assignee hwangtae123
Length of output: 55
@hwangtae123, 이슈를 생성했습니다: #80 - [document_processor] 주석의 모델 설명을 실제 요약 모델로 업데이트
모델 개발 완료 후 app/core/services/document_processor.py Line 83의 주석을 실제 MODEL_NAME 기준으로 수정해 주시면 됩니다. 감사합니다! 🙏
🧠 Learnings used
Learnt from: hwangtae123
Repo: Project-Guideon/guideon-AI PR: 44
File: app/api/document.py:48-53
Timestamp: 2026-04-01T17:28:20.980Z
Learning: In the guideon-AI project, document processing idempotency is managed entirely by the Spring Boot Core service. Core enforces a PENDING → PROCESSING → COMPLETED state machine with a unique constraint in the DB, so the FastAPI endpoint at app/api/document.py (POST /v1/documents/process) does not need an additional in-progress guard or distributed lock. Duplicate requests are blocked on the Core side before they reach the AI processing pipeline.
Reverts #76
revert 한거를 다시 revert -> 즉 develop ->main 내용 추가
Summary by CodeRabbit
새로운 기능
개선 사항
기타