cert-study-app / cert_study_app /chains /question_parser_chain.py
github-actions
Sync from GitHub d2682fe6d3fcffe93aa302c286320962009f6436
9381502
Raw
History Blame Contribute Delete
3.37 kB
import json
import time
from typing import List, Optional
from pydantic import BaseModel, Field
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
class QuestionParsedResult(BaseModel):
stem: str = Field(description="๋ฌธ์ œ ๋ณธ๋ฌธ", default="")
options: List[str] = Field(description="๋ณด๊ธฐ ๋ชฉ๋ก (๊ฐ๊ด€์‹์ผ ๊ฒฝ์šฐ)", default_factory=list)
answer: List[str] = Field(description="์ •๋‹ต ๋˜๋Š” ์„ ํƒ๊ฒฐ๊ณผ", default_factory=list)
explanation: str = Field(description="๊ฐ„๋‹จํ•œ ํ•ด์„ค", default="")
question_type: str = Field(description="๋ฌธ์ œ ์œ ํ˜• (mcq, yes_no, sequence, code, scenario)")
code: Optional[str] = Field(description="์ฝ”๋“œ๊ฐ€ ์žˆ๋‹ค๋ฉด ํฌํ•จ", default="")
sequence: Optional[List[str]] = Field(description="์ˆœ์„œํ˜•์ผ ๊ฒฝ์šฐ ํ•ญ๋ชฉ", default_factory=list)
parser = PydanticOutputParser(pydantic_object=QuestionParsedResult)
QUESTION_PARSE_PROMPT = ChatPromptTemplate.from_template(
"""
๋„ˆ๋Š” OCR๋กœ ์ธ์‹๋œ ์‹œํ—˜ ๋ฌธ์ œ๋ฅผ ๋ถ„์„ํ•˜์—ฌ ๊ตฌ์กฐํ™”ํ•˜๋Š” ์ „๋ฌธ ํŒŒ์„œ์ด๋‹ค.
๋ฌธ์ œ ์œ ํ˜•์€ ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜์ผ ์ˆ˜ ์žˆ๋‹ค:
- mcq: ๊ฐ๊ด€์‹
- yes_no: ์ฐธ/๊ฑฐ์ง“ ๋˜๋Š” ์˜ˆ/์•„๋‹ˆ์˜ค
- sequence: ์ˆœ์„œํ˜• ๋ฌธ์ œ
- code: ์ฝ”๋“œ ๊ธฐ๋ฐ˜ ๋ฌธ์ œ
- scenario: ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐ๋ฐ˜ ๋ฌธ์ œ
์ด ๋ฌธ์ œ๋Š” ์ฃผ๋กœ '{question_type}' ์œ ํ˜•์œผ๋กœ ์ถ”์ •๋œ๋‹ค.
{format_instructions}
๋ฌธ์ œ ์›๋ฌธ:
{ocr_text}
"""
)
def detect_question_type(text: str) -> str:
lowered = text.lower()
if any(k in lowered for k in ["์˜ˆ", "์•„๋‹ˆ์˜ค", "yes", "no", "true", "false"]):
return "yes_no"
if any(k in lowered for k in ["์ˆœ์„œ", "์ •๋ ฌ", "drag", "drop", "์ˆœ์„œ๋Œ€๋กœ"]):
return "sequence"
if any(k in lowered for k in ["json", "{", "}", "az ", "set-az", "cli", "powershell", "bash", "cmd", "์ฝ”๋“œ"]):
return "code"
if any(k in lowered for k in ["์‹œ๋‚˜๋ฆฌ์˜ค", "case", "contoso", "litware", "fabrikam", "์กฐ๊ฑด", "์ƒํ™ฉ"]):
return "scenario"
return "mcq"
def build_question_parser_chain(llm):
# LCEL: Prompt -> LLM -> Pydantic Parser
return QUESTION_PARSE_PROMPT | llm | parser
def parse_question_with_chain(llm, page: int, text: str) -> dict:
question_type = detect_question_type(text)
chain = build_question_parser_chain(llm)
try:
started_at = time.time()
# parser.get_format_instructions()๋ฅผ ํ†ตํ•ด ํ”„๋กฌํ”„ํŠธ์— JSON ์Šคํ‚ค๋งˆ๋ฅผ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.
parsed_result = chain.invoke(
{
"question_type": question_type,
"ocr_text": text[:1800],
"format_instructions": parser.get_format_instructions(),
}
)
# Pydantic ๋ชจ๋ธ์„ dict๋กœ ๋ณ€ํ™˜ (Pydantic v1/v2 ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด dict() ์‚ฌ์šฉ)
parsed = parsed_result.dict()
parsed["page"] = page
print(f"[LLM] p{page} ({question_type}) parsed in {time.time() - started_at:.1f}s")
return parsed
except Exception as exc:
print(f"[WARN] LLM parse failed (p{page}): {exc}")
return {
"page": page,
"stem": text[:400],
"options": [],
"answer": [],
"explanation": f"์˜ค๋ฅ˜: {exc}",
"question_type": question_type,
"code": "",
"sequence": [],
}