Skip to content

refactor: postprocess folder add and output process seoarate - #96

Merged
Yu-JeSeung merged 3 commits into
mainfrom
refactor/chatbot_output_process
Apr 27, 2026
Merged

refactor: postprocess folder add and output process seoarate#96
Yu-JeSeung merged 3 commits into
mainfrom
refactor/chatbot_output_process

Conversation

@Yu-JeSeung

@Yu-JeSeung Yu-JeSeung commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

관련 이슈

Close #91

🎯 배경

  • 챗봇 응답하는 프로세스를 구조적으로 나눠 전체 흐름과 기능개선 부분을 위한 리팩토링이 필요했습니다.

🔍 주요 내용

  • postpreocess폴더 생성
  • registry 파이프라인 매핑
  • rules_table 응답 규칙 테이블
  • message_table 문구 테이블
  • synonym_table 동의어 테이블
  • engine 실행
  • service one_sentence -> run_process 변경
  • formatter 알고리즘 로직 유직, 새로운 엔진에서 쓸 수 있는 추출 helper 추가
  • .gitignore harnes engineering file 예외처리

@Yu-JeSeung Yu-JeSeung self-assigned this Apr 25, 2026
@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@Yu-JeSeung has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 8 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 16 minutes and 8 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ee48d10b-f558-4a36-8174-8a7ef8e0feef

📥 Commits

Reviewing files that changed from the base of the PR and between 60d9d1a and e721a96.

📒 Files selected for processing (1)
  • LLM/OSS/postprocess/registry.py
📝 Walkthrough

개요

후처리 기능을 새로운 postprocess 패키지로 분리하고, 데이터 기반의 규칙/동의어 테이블을 도입하며, 모드별 형식화 로직을 재구성합니다. 기존 formatter의 일회성 키워드 검사를 구조화된 파이프라인으로 대체합니다.

변경 사항

Cohort / File(s) 요약
설정 파일 업데이트
.gitignore
AGENTS.md, PLANS.md, SKILL.md, CHECKLIST.md 네 개의 문서 파일을 무시 목록에 추가.
후처리 패키지 신규 생성
LLM/OSS/postprocess/__init__.py, LLM/OSS/postprocess/context.py, LLM/OSS/postprocess/registry.py
후처리 엔진의 공개 API 정의, PostProcessContext 데이터클래스 도입(필수 필드: user_text, mode, sub_answer; 선택 필드: 추출된 메타데이터), 모드별 파이프라인 설정 등록.
규칙 및 메시지 데이터 구조
LLM/OSS/postprocess/message_table.py, LLM/OSS/postprocess/rules_table.py, LLM/OSS/postprocess/synonym_table.py
한국어 템플릿 문자열 매핑(MESSAGES), 조건 기반 응답 규칙(CONTACT_RESPONSE_RULES), 정책/기숙사 동의어 확장 규칙(POLICY_SYNONYM_RULES, DORM_SYNONYM_RULES) 정의.
후처리 엔진 구현
LLM/OSS/postprocess/engine.py
PostProcessContext 구축, 규칙 매칭 및 템플릿 렌더링을 통한 응답 생성. 모드별 파이프라인(fast/policy/dorm/grad/topic)에 따른 조건부 처리 로직.
Formatter 리팩토링
LLM/OSS/formatter.py
데이터 기반 동의어 테이블 도입으로 고정 키워드 검사 제거. PostProcessContext 활용한 연락처 응답 생성 재구성. 주제/정책/기숙사 제목+URL 추출 로직 통합 및 공통 선택 매커니즘 도입. 대학원 응답 생성 분리.
서비스 계층 통합
LLM/OSS/service.py
기존 one_sentence_* formatter 함수 제거 및 run_postprocess API로 통합. 모드별 출력 형식화를 후처리 엔진으로 일원화.

시퀀스 다이어그램

sequenceDiagram
    participant Client as 호출자
    participant Engine as run_postprocess 엔진
    participant Reg as MODE_PIPELINES
    participant Ctx as PostProcessContext
    participant Rules as CONTACT_RESPONSE_RULES
    participant Msg as MESSAGES 템플릿
    
    Client->>Engine: run_postprocess(mode, user_text, sub_answer)
    Engine->>Reg: MODE_PIPELINES[mode] 조회
    Engine->>Ctx: PostProcessContext 구축
    Engine->>Rules: 조건별 규칙 정렬 및 매칭
    alt 규칙 매칭 성공
        Rules->>Msg: template_key 기반 템플릿 조회
        Msg->>Engine: 포맷 문자열 반환
        Engine->>Engine: {label}, {phone}, {url} 등으로 치환
    else 규칙 미매칭
        Engine->>Engine: first_line 또는 기본 메시지 사용
    end
    Engine->>Client: (formatted_text, url) 반환
Loading

예상 코드 리뷰 소요 시간

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

관련 PR

  • refactor: chatbot service logic and routting code separate #94: 동일한 코드 영역(LLM/OSS/formatter.py, LLM/OSS/service.py)을 수정하며, 해당 PR에서 라우팅/서비스 분리와 one_sentence_* formatter를 도입했고, 본 PR에서 후처리를 LLM/OSS/postprocess 패키지로 추출하여 run_postprocess로 통합하는 직접적인 코드 수준의 연관성이 있습니다.
🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning PR 설명이 기본 구조를 따르고 있으나 이슈 참조 형식이 부정확하고 주요 내용이 오타 및 불완전한 표현을 포함하고 있습니다. 이슈 참조를 'Closes #91' 형식으로 수정하고, 오타('postpreocess' → 'postprocess', 'one_sentence' → 'run_postprocess')를 수정하며, 각 항목을 더 명확하게 설명해주세요.
Title check ❓ Inconclusive PR 제목이 전체 변경사항의 주요 내용을 부분적으로 반영하지만, 문법 오류('seoarate' → 'separate')와 모호한 표현('output process seoarate')으로 인해 변경사항의 핵심을 명확하게 전달하지 못합니다. 제목을 'refactor: separate postprocess folder and output pipeline'과 같이 더 명확하고 정확한 표현으로 수정하여 리팩토링의 핵심 의도를 전달하도록 개선하세요.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/chatbot_output_process

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.

@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

🧹 Nitpick comments (5)
LLM/OSS/formatter.py (2)

587-588: 하드코딩된 키워드를 테이블로 분리 고려

_policy_candidate_score_dorm_candidate_score에 하드코딩된 키워드들이 있습니다. 향후 synonym_table.py와 유사하게 별도 테이블로 분리하면 일관성이 높아질 수 있습니다.

# 예: Lines 587-588의 하드코딩된 키워드
("휴학", "복학", "휴·복학", "휴복학", "학적", "학사안내")

# 예: Lines 606-607의 하드코딩된 키워드
("학생생활관", "생활관", "기숙사", "입사", "생활관비", "생활관 안내")

Also applies to: 606-607

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

In `@LLM/OSS/formatter.py` around lines 587 - 588, The two scoring blocks in
formatter.py (_policy_candidate_score and _dorm_candidate_score) use hardcoded
keyword tuples (e.g., ("휴학","복학","휴·복학","휴복학","학적","학사안내") and
("학생생활관","생활관","기숙사","입사","생활관비","생활관 안내")); extract these into a shared lookup
(e.g., POLICY_KEYWORDS and DORM_KEYWORDS) or move them into a new
synonyms/keyword table module similar to synonym_table.py, import and reference
those constants from _policy_candidate_score and _dorm_candidate_score, and
update the any(...) checks to use the new constants so keyword lists are
centralized and reusable.

623-627: 조건 검사 순서 최적화 가능

sub_answer 검사를 TITLE_URL_PAT.finditer 호출 전에 수행하면 불필요한 정규식 파싱을 피할 수 있습니다.

♻️ 제안된 수정
 def _best_title_url_candidate(
     user_text: str,
     sub_answer: str,
     default_text: str,
     scorer,
     prefer_good_url: bool = False,
     require_positive_score: bool = False,
 ) -> tuple[Optional[str], Optional[str], str]:
-    items = list(TITLE_URL_PAT.finditer(sub_answer or ""))
     if not sub_answer:
         return None, None, default_text
+    items = list(TITLE_URL_PAT.finditer(sub_answer))
     if not items:
         return None, None, _first_line_or_default(sub_answer, default_text)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@LLM/OSS/formatter.py` around lines 623 - 627, The code currently runs
TITLE_URL_PAT.finditer(sub_answer or "") before checking if sub_answer is falsy,
causing unnecessary regex work; change the order so you first check if
sub_answer is falsy (returning None, None, default_text) and only then call
TITLE_URL_PAT.finditer(sub_answer) to populate items; preserve the subsequent
logic that returns _first_line_or_default(sub_answer, default_text) when items
is empty and keep variable names TITLE_URL_PAT, sub_answer, items,
_first_line_or_default, and default_text unchanged.
LLM/OSS/postprocess/context.py (1)

13-13: hint 필드 타입 힌트 개선 고려

dict[str, object] 대신 dict[str, Any]를 사용하거나, hint의 구조가 고정되어 있다면 TypedDict를 정의하여 타입 안전성을 높일 수 있습니다. 현재 코드에서 hintcanon, aliases, path_base 키를 사용하는 것으로 보입니다.

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

In `@LLM/OSS/postprocess/context.py` at line 13, The type for the
variable/parameter named "hint" currently declared as Optional[dict[str,
object]] should be made more precise: either change dict[str, object] to
dict[str, Any] (importing Any) or define a TypedDict (e.g., HintDict with keys
"canon", "aliases", "path_base") and use Optional[HintDict]; update the hint
declaration in LLM/OSS/postprocess/context.py and add the necessary typing
imports (Any or TypedDict) so callers and static checkers see the exact
structure.
LLM/OSS/postprocess/engine.py (2)

25-29: 가독성 개선을 위한 간소화 권장

Line 26의 조건식이 복잡하고 (sub_answer or "").strip()이 두 번 평가됩니다. 변수로 분리하면 가독성이 향상됩니다.

♻️ 가독성 개선 제안
 def _first_line(sub_answer: str) -> str:
-    first = (sub_answer or "").strip().splitlines()[0].lstrip("- ").strip() if (sub_answer or "").strip() else ""
+    stripped = (sub_answer or "").strip()
+    first = stripped.splitlines()[0].lstrip("- ").strip() if stripped else ""
     if not first:
         return MESSAGES["not_found"]
     return first if first.endswith(("다.", "요.")) else first + "."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@LLM/OSS/postprocess/engine.py` around lines 25 - 29, The condition in
_first_line duplicates (sub_answer or "").strip(); refactor by first assigning
stripped = (sub_answer or "").strip(), then if not stripped return
MESSAGES["not_found"], else compute first = stripped.splitlines()[0].lstrip("-
").strip() and finally return first if first.endswith(("다.", "요.")) else first +
"."; update _first_line to use these variable names to improve readability and
avoid re-evaluating the expression.

71-98: 코드 중복 - 공통 로직 추출 권장

_run_topic, _run_policy, _run_dorm 세 함수가 거의 동일한 구조를 가지고 있습니다. 추출 함수와 메시지 키만 다릅니다. 공통 헬퍼 함수로 통합하면 유지보수성이 향상됩니다.

♻️ 공통 헬퍼 함수 추출 제안
+def _run_page_processor(
+    user_text: str,
+    sub_answer: str,
+    extractor,
+    default_key: str,
+    page_key: str,
+) -> tuple[str, Optional[str]]:
+    title, url, first_line = extractor(user_text, sub_answer)
+    if not sub_answer:
+        return _format_message(default_key, user_text=user_text), settings.org_homepage_url
+    if title and url:
+        url = formatter.ensure_layout_unknown(url)
+        return _format_message(page_key, user_text=user_text, title=title, url=url), url
+    return first_line, None
+
+
 def _run_topic(user_text: str, sub_answer: str) -> tuple[str, Optional[str]]:
-    title, url, first_line = formatter.extract_topic_candidate(user_text, sub_answer)
-    if not sub_answer:
-        return _format_message("topic_default", user_text=user_text), settings.org_homepage_url
-    if title and url:
-        url = formatter.ensure_layout_unknown(url)
-        return _format_message("topic_page", user_text=user_text, title=title, url=url), url
-    return first_line, None
+    return _run_page_processor(
+        user_text, sub_answer,
+        formatter.extract_topic_candidate, "topic_default", "topic_page"
+    )


 def _run_policy(user_text: str, sub_answer: str) -> tuple[str, Optional[str]]:
-    title, url, first_line = formatter.extract_policy_candidate(user_text, sub_answer)
-    if not sub_answer:
-        return _format_message("policy_default", user_text=user_text), settings.org_homepage_url
-    if title and url:
-        url = formatter.ensure_layout_unknown(url)
-        return _format_message("official_page", user_text=user_text, title=title, url=url), url
-    return first_line, None
+    return _run_page_processor(
+        user_text, sub_answer,
+        formatter.extract_policy_candidate, "policy_default", "official_page"
+    )


 def _run_dorm(user_text: str, sub_answer: str) -> tuple[str, Optional[str]]:
-    title, url, first_line = formatter.extract_dorm_candidate(user_text, sub_answer)
-    if not sub_answer:
-        return _format_message("dorm_default", user_text=user_text), settings.org_homepage_url
-    if title and url:
-        url = formatter.ensure_layout_unknown(url)
-        return _format_message("official_page", user_text=user_text, title=title, url=url), url
-    return first_line, None
+    return _run_page_processor(
+        user_text, sub_answer,
+        formatter.extract_dorm_candidate, "dorm_default", "official_page"
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@LLM/OSS/postprocess/engine.py` around lines 71 - 98, The three functions
_run_topic, _run_policy, and _run_dorm duplicate the same control flow; extract
a shared helper (e.g., _run_candidate) that accepts the extractor function
(formatter.extract_topic_candidate / formatter.extract_policy_candidate /
formatter.extract_dorm_candidate) and the default/message keys ("topic_default",
"policy_default" or "dorm_default" and "topic_page"/"official_page") as
parameters, calls the extractor to get (title, url, first_line), handles the not
sub_answer case by returning _format_message(...), uses
formatter.ensure_layout_unknown(url) when title and url are present, and
otherwise returns first_line and None; then replace each of
_run_topic/_run_policy/_run_dorm with a one-line call to that helper, preserving
the tuple[str, Optional[str]] return shape and usage of
settings.org_homepage_url and _format_message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@LLM/OSS/postprocess/registry.py`:
- Around line 2-7: The "fast" mode in postprocess/registry.py defines flags
use_schedule_first and use_dept_clarification that are not used; either remove
these unused keys from the "fast" dict in registry.py, or wire them into the
runtime by having engine.py read the mode config and pass them into the
processing pipeline (e.g., when selecting the processor for a request).
Concretely: update the registry entry for "fast" (remove use_schedule_first and
use_dept_clarification) OR update engine.py (functions like the mode lookup or
request handler such as process_request / Engine.process) to call the registry
getter (e.g., get_mode_config) and propagate those boolean flags into the
sub_answer processor invocation so the processor receives and acts on
use_schedule_first and use_dept_clarification.

---

Nitpick comments:
In `@LLM/OSS/formatter.py`:
- Around line 587-588: The two scoring blocks in formatter.py
(_policy_candidate_score and _dorm_candidate_score) use hardcoded keyword tuples
(e.g., ("휴학","복학","휴·복학","휴복학","학적","학사안내") and
("학생생활관","생활관","기숙사","입사","생활관비","생활관 안내")); extract these into a shared lookup
(e.g., POLICY_KEYWORDS and DORM_KEYWORDS) or move them into a new
synonyms/keyword table module similar to synonym_table.py, import and reference
those constants from _policy_candidate_score and _dorm_candidate_score, and
update the any(...) checks to use the new constants so keyword lists are
centralized and reusable.
- Around line 623-627: The code currently runs TITLE_URL_PAT.finditer(sub_answer
or "") before checking if sub_answer is falsy, causing unnecessary regex work;
change the order so you first check if sub_answer is falsy (returning None,
None, default_text) and only then call TITLE_URL_PAT.finditer(sub_answer) to
populate items; preserve the subsequent logic that returns
_first_line_or_default(sub_answer, default_text) when items is empty and keep
variable names TITLE_URL_PAT, sub_answer, items, _first_line_or_default, and
default_text unchanged.

In `@LLM/OSS/postprocess/context.py`:
- Line 13: The type for the variable/parameter named "hint" currently declared
as Optional[dict[str, object]] should be made more precise: either change
dict[str, object] to dict[str, Any] (importing Any) or define a TypedDict (e.g.,
HintDict with keys "canon", "aliases", "path_base") and use Optional[HintDict];
update the hint declaration in LLM/OSS/postprocess/context.py and add the
necessary typing imports (Any or TypedDict) so callers and static checkers see
the exact structure.

In `@LLM/OSS/postprocess/engine.py`:
- Around line 25-29: The condition in _first_line duplicates (sub_answer or
"").strip(); refactor by first assigning stripped = (sub_answer or "").strip(),
then if not stripped return MESSAGES["not_found"], else compute first =
stripped.splitlines()[0].lstrip("- ").strip() and finally return first if
first.endswith(("다.", "요.")) else first + "."; update _first_line to use these
variable names to improve readability and avoid re-evaluating the expression.
- Around line 71-98: The three functions _run_topic, _run_policy, and _run_dorm
duplicate the same control flow; extract a shared helper (e.g., _run_candidate)
that accepts the extractor function (formatter.extract_topic_candidate /
formatter.extract_policy_candidate / formatter.extract_dorm_candidate) and the
default/message keys ("topic_default", "policy_default" or "dorm_default" and
"topic_page"/"official_page") as parameters, calls the extractor to get (title,
url, first_line), handles the not sub_answer case by returning
_format_message(...), uses formatter.ensure_layout_unknown(url) when title and
url are present, and otherwise returns first_line and None; then replace each of
_run_topic/_run_policy/_run_dorm with a one-line call to that helper, preserving
the tuple[str, Optional[str]] return shape and usage of
settings.org_homepage_url and _format_message.
🪄 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: 31487d12-f702-4e2c-bea9-3b206e87be08

📥 Commits

Reviewing files that changed from the base of the PR and between b6e6765 and 60d9d1a.

📒 Files selected for processing (10)
  • .gitignore
  • LLM/OSS/formatter.py
  • LLM/OSS/postprocess/__init__.py
  • LLM/OSS/postprocess/context.py
  • LLM/OSS/postprocess/engine.py
  • LLM/OSS/postprocess/message_table.py
  • LLM/OSS/postprocess/registry.py
  • LLM/OSS/postprocess/rules_table.py
  • LLM/OSS/postprocess/synonym_table.py
  • LLM/OSS/service.py

Comment thread LLM/OSS/postprocess/registry.py
@Yu-JeSeung
Yu-JeSeung merged commit d070944 into main Apr 27, 2026
1 check passed
@Yu-JeSeung
Yu-JeSeung deleted the refactor/chatbot_output_process branch April 27, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] AI 서비스 전체 코드 리팩토링 진행

1 participant