Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ test
CLAUDE.md
.claude/
backup_file/
AGENTS.md
PLANS.md
SKILL.md
CHECKLIST.md
366 changes: 234 additions & 132 deletions LLM/OSS/formatter.py

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions LLM/OSS/postprocess/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from LLM.OSS.postprocess.engine import run_postprocess

__all__ = ["run_postprocess"]
31 changes: 31 additions & 0 deletions LLM/OSS/postprocess/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from dataclasses import dataclass
from typing import Optional


@dataclass
class PostProcessContext:
user_text: str
mode: str
sub_answer: str
label: Optional[str] = None
phone: Optional[str] = None
url: Optional[str] = None
hint: Optional[dict[str, object]] = None
first_line: str = ""
contact_intent: bool = False

@property
def has_label(self) -> bool:
return bool(self.label)

@property
def has_phone(self) -> bool:
return bool(self.phone)

@property
def has_url(self) -> bool:
return bool(self.url)

@property
def has_sub_answer(self) -> bool:
return bool((self.sub_answer or "").strip())
126 changes: 126 additions & 0 deletions LLM/OSS/postprocess/engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from typing import Optional

from core.settings import get_settings
from LLM.OSS import formatter
from LLM.OSS.postprocess.context import PostProcessContext
from LLM.OSS.postprocess.message_table import MESSAGES
from LLM.OSS.postprocess.registry import MODE_PIPELINES
from LLM.OSS.postprocess.rules_table import CONTACT_RESPONSE_RULES


settings = get_settings()


def _format_message(template_key: str, **values: object) -> str:
template = MESSAGES[template_key]
payload = {
"org_homepage_label": settings.org_homepage_label,
"org_homepage_url": settings.org_homepage_url,
"grad_page_url": settings.grad_page_url,
**values,
}
return template.format(**payload)


def _first_line(sub_answer: str) -> str:
first = (sub_answer or "").strip().splitlines()[0].lstrip("- ").strip() if (sub_answer or "").strip() else ""
if not first:
return MESSAGES["not_found"]
return first if first.endswith(("다.", "요.")) else first + "."


def _build_sub_answer_context(mode: str, user_text: str, sub_answer: str) -> PostProcessContext:
ctx = formatter.build_contact_context(user_text, sub_answer, mode=mode)
if not ctx.first_line:
ctx.first_line = _first_line(sub_answer)
return ctx


def _conditions(ctx: PostProcessContext) -> dict[str, bool]:
return {
"contact_intent": ctx.contact_intent,
"has_label": ctx.has_label,
"has_phone": ctx.has_phone,
"has_url": ctx.has_url,
"has_sub_answer": ctx.has_sub_answer,
"not_has_phone": not ctx.has_phone,
}


def _apply_contact_rules(ctx: PostProcessContext) -> Optional[str]:
flags = _conditions(ctx)
rules = sorted(CONTACT_RESPONSE_RULES, key=lambda item: item["priority"])
for rule in rules:
if ctx.mode not in rule["modes"]:
continue
if all(flags.get(condition, False) for condition in rule["conditions"]):
return _format_message(rule["template_key"], label=ctx.label, phone=ctx.phone, url=ctx.url, user_text=ctx.user_text)
return None


def _run_sub_answer(mode: str, user_text: str, sub_answer: str) -> tuple[str, Optional[str]]:
if not sub_answer:
return MESSAGES["not_found"], None
ctx = _build_sub_answer_context(mode, user_text, sub_answer)
text = _apply_contact_rules(ctx)
if text:
return text, ctx.url
return ctx.first_line, ctx.url


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


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


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


def _run_grad(user_text: str, sub_answer: str) -> tuple[str, Optional[str]]:
summary, url = formatter.extract_grad_summary(user_text, sub_answer)
if not sub_answer:
return _format_message("grad_default"), settings.grad_page_url
if "확인할 수 있습니다." in summary:
return summary, url
return _format_message("grad_with_url", summary=summary, url=url), url


PROCESSORS = {
"sub_answer": _run_sub_answer,
"topic": _run_topic,
"policy": _run_policy,
"dorm": _run_dorm,
"grad": _run_grad,
}


def run_postprocess(mode: str, user_text: str, sub_answer: str) -> tuple[str, Optional[str]]:
config = MODE_PIPELINES.get(mode)
if not config:
return formatter.one_sentence_from_sub_answer(user_text, sub_answer)
processor = PROCESSORS[config["processor"]]
if config["processor"] == "sub_answer":
return processor(mode, user_text, sub_answer)
return processor(user_text, sub_answer)
14 changes: 14 additions & 0 deletions LLM/OSS/postprocess/message_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
MESSAGES = {
"not_found": "요청하신 정보를 찾지 못했습니다.",
"contact_with_label_and_phone": "{label} 전화번호는 {phone}입니다.",
"contact_with_phone_only": "요청하신 부서 담당자 전화번호는 {phone}입니다.",
"contact_retry_full_name": "담당자 연락처를 바로 찾지 못했습니다. 학과(또는 부서) 풀네임으로 다시 입력해 주세요.",
"topic_default": "‘{user_text}’ 관련 정보는 {org_homepage_label}에서 확인할 수 있습니다.",
"policy_default": "‘{user_text}’ 관련 정보는 {org_homepage_label}의 학사안내에서 확인할 수 있습니다.",
"dorm_default": "‘{user_text}’ 관련 정보는 {org_homepage_label}의 생활관 안내에서 확인할 수 있습니다.",
"topic_page": "‘{user_text}’ 관련 정보는 ‘{title}’ 페이지({url})에서 확인할 수 있습니다.",
"official_page": "‘{user_text}’ 관련 공식 안내는 ‘{title}’ 페이지({url})에서 확인할 수 있습니다.",
"label_with_url": "{label} 정보는 {url}에서 확인할 수 있습니다.",
"grad_default": "졸업 관련 정보는 {grad_page_url}에서 확인할 수 있습니다.",
"grad_with_url": "{summary} 자세한 내용은 {url}에서 확인할 수 있습니다.",
}
17 changes: 17 additions & 0 deletions LLM/OSS/postprocess/registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
MODE_PIPELINES = {
"fast": {
"processor": "sub_answer",
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"policy": {
"processor": "policy",
},
"dorm": {
"processor": "dorm",
},
"grad": {
"processor": "grad",
},
"topic": {
"processor": "topic",
},
}
30 changes: 30 additions & 0 deletions LLM/OSS/postprocess/rules_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
CONTACT_RESPONSE_RULES = [
{
"name": "contact_with_label_and_phone",
"modes": {"fast", "policy", "dorm", "grad"},
"conditions": ("contact_intent", "has_label", "has_phone"),
"template_key": "contact_with_label_and_phone",
"priority": 10,
},
{
"name": "contact_with_phone_only",
"modes": {"fast", "policy", "dorm", "grad"},
"conditions": ("contact_intent", "has_phone"),
"template_key": "contact_with_phone_only",
"priority": 20,
},
{
"name": "contact_retry_full_name",
"modes": {"fast", "policy", "dorm", "grad"},
"conditions": ("contact_intent", "not_has_phone"),
"template_key": "contact_retry_full_name",
"priority": 30,
},
{
"name": "label_with_url",
"modes": {"fast"},
"conditions": ("has_label", "has_url"),
"template_key": "label_with_url",
"priority": 40,
},
]
21 changes: 21 additions & 0 deletions LLM/OSS/postprocess/synonym_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
POLICY_SYNONYM_RULES = (
{
"triggers": ("복학", "휴복학", "휴·복학", "휴 학"),
"synonyms": ("복학", "휴학", "휴복학", "휴·복학", "휴학/복학"),
},
{
"triggers": ("휴학",),
"synonyms": ("휴학", "복학", "휴복학", "휴·복학", "휴학/복학"),
},
{
"triggers": ("학적",),
"synonyms": ("학적", "학적변동", "휴학", "복학", "재입학", "자퇴", "전과"),
},
)

DORM_SYNONYM_RULES = (
{
"triggers": ("기숙사", "생활관", "학생생활관", "사생", "입사", "퇴사", "입실", "퇴실", "생활관비"),
"synonyms": ("기숙사", "생활관", "학생생활관", "입사", "퇴사", "입사신청", "생활관비", "생활관 안내", "생활관 규정"),
},
)
22 changes: 9 additions & 13 deletions LLM/OSS/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@
from core.settings import get_settings
from LLM.OSS.formatter import (
dept_clarification_message,
one_sentence_dorm,
one_sentence_from_sub_answer,
one_sentence_grad,
one_sentence_policy,
one_sentence_topic,
render_chatty_schedule,
scrub_non_contact,
)
Expand All @@ -32,6 +27,7 @@
looks_like_schedule,
looks_like_topic,
)
from LLM.OSS.postprocess import run_postprocess
from LLM.rule_book.graph import run_rule_book
from LLM.sub_model.query_index import build_answer
from LLM.sub_model.schedule_index import schedule_search
Expand Down Expand Up @@ -249,39 +245,39 @@ def cache_and_return(response: dict) -> dict:
return cache_and_return({"engine": "fast", "text": clarification})

sub_answer = call_submodel(user_text)
text, url = one_sentence_from_sub_answer(user_text, sub_answer)
text, url = run_postprocess("fast", user_text, sub_answer)
response = {"engine": "fast", "text": text}
if url:
response["url"] = url
return cache_and_return(response)

if mode == "policy":
sub_answer = call_submodel(user_text)
text, url = one_sentence_policy(user_text, sub_answer)
text, url = run_postprocess("policy", user_text, sub_answer)
response = {"engine": "policy", "text": text}
if url:
response["url"] = url
return cache_and_return(response)

if mode == "dorm":
sub_answer = call_submodel(user_text)
text, url = one_sentence_dorm(user_text, sub_answer)
text, url = run_postprocess("dorm", user_text, sub_answer)
response = {"engine": "dorm", "text": text}
if url:
response["url"] = url
return cache_and_return(response)

if mode == "grad":
sub_answer = call_submodel(user_text)
text, url = one_sentence_grad(user_text, sub_answer)
text, url = run_postprocess("grad", user_text, sub_answer)
response = {"engine": "grad", "text": text}
if url:
response["url"] = url
return cache_and_return(response)

if mode == "topic":
sub_answer = call_submodel(user_text)
text, url = one_sentence_topic(user_text, sub_answer)
text, url = run_postprocess("topic", user_text, sub_answer)
response = {"engine": "topic", "text": text}
if url:
response["url"] = url
Expand Down Expand Up @@ -312,9 +308,9 @@ def cache_and_return(response: dict) -> dict:
return response
if sub_answer:
if looks_like_topic(user_text):
text, _ = one_sentence_topic(user_text, sub_answer)
text, _ = run_postprocess("topic", user_text, sub_answer)
else:
text, _ = one_sentence_from_sub_answer(user_text, sub_answer)
text, _ = run_postprocess("fast", user_text, sub_answer)
output = text
else:
output = "잘 이해하지 못했어요. 다시 질문해주세요."
Expand All @@ -336,7 +332,7 @@ def cache_and_return(response: dict) -> dict:
temperature=0.2,
)
if not fused:
text, _ = one_sentence_topic(user_text, sub_answer)
text, _ = run_postprocess("topic", user_text, sub_answer)
fused = text if looks_like_topic(user_text) else "좋아요, 무엇을 이야기해 볼까요?"

latency = int((time.monotonic() - start) * 1000)
Expand Down