From 8b9f5bcfe1e9ec24eb35ca11e2767098506b1e8e Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Thu, 30 Apr 2026 23:03:59 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20vocab=20Pydantic=20=EC=8A=A4?= =?UTF-8?q?=ED=82=A4=EB=A7=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VocabExtractRequest (sentences 입력), VocabExtractResponse (word/meaning 출력). 어려운 단어 없을 시 word/meaning 모두 빈 문자열 응답. --- app/schemas/vocab.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/schemas/vocab.py diff --git a/app/schemas/vocab.py b/app/schemas/vocab.py new file mode 100644 index 0000000..2744d61 --- /dev/null +++ b/app/schemas/vocab.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel, Field + + +class VocabExtractRequest(BaseModel): + sentences: list[str] = Field( + ..., min_length=1, description="페이지를 구성하는 문장 리스트 (보통 3개)" + ) + + +class VocabExtractResponse(BaseModel): + """LLM이 추출한 어려운 단어 1개와 유아 눈높이 풀이. + + 어려운 단어가 없다고 판단될 경우 word, meaning 모두 빈 문자열로 응답한다. + """ + + word: str = Field("", description="추출된 어려운 단어 (없으면 빈 문자열)") + meaning: str = Field("", description="유아 눈높이로 재구성된 풀이 (없으면 빈 문자열)") From 27ae5046d62b7de67b4cc434ba5d0a095f09ae94 Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Thu, 30 Apr 2026 23:04:41 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20extract=5Fvocab=20LLM=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3문장에서 유아가 어려워할 단어 1개를 선별하고 유아 눈높이로 풀이. - gpt-4o-mini, temperature 0.3 (재현성 ↑) - 활용형 그대로 처리, 풀이 50자 이내 - 어려운 단어 없으면 word/meaning 모두 빈 문자열 --- app/services/llm_service.py | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/app/services/llm_service.py b/app/services/llm_service.py index af2bf92..98a2069 100644 --- a/app/services/llm_service.py +++ b/app/services/llm_service.py @@ -4,6 +4,53 @@ from app.config import settings from app.schemas.graph import GraphExtractResponse +from app.schemas.vocab import VocabExtractResponse + + +VOCAB_SYSTEM_PROMPT = """당신은 유아 동화에서 어려운 단어를 골라 풀이하는 AI입니다. + +## 규칙 +1. 입력으로 들어오는 3문장 안에서 만 4~7세 유아가 이해하기 어려울 만한 단어를 정확히 1개만 고릅니다. +2. 어려운 단어 후보: + - 한자어 (예: 위험, 모험, 결심) + - 추상 개념 (예: 용기, 지혜, 행복) + - 일상에서 잘 안 쓰는 고유 명사/사물 (예: 용궁, 누각) + - 의성어/의태어 중 어려운 것 +3. 활용형(예: \"알록달록한\", \"감탄하며\")은 활용형 그대로 골라도 됩니다. +4. 풀이는 1~2문장, 50자 이내로 작성하고, 유아가 이해할 수 있는 쉬운 단어와 친근한 어조로 씁니다. +5. 풀이 안에 원래 단어를 그대로 넣지 마세요. 풀어서 설명하세요. +6. 정말로 어려운 단어가 없다고 판단되면 word, meaning 모두 빈 문자열로 응답합니다. + +## 출력 형식 +반드시 아래 JSON 형식으로만 응답하세요. 다른 텍스트를 추가하지 마세요. + +{ + "word": "단어 또는 빈 문자열", + "meaning": "유아용 풀이 또는 빈 문자열" +} +""" + + +def extract_vocab(sentences: list[str]) -> VocabExtractResponse: + client = OpenAI(api_key=settings.openai_api_key) + + joined = "\n".join(sentences) + response = client.chat.completions.create( + model=settings.openai_model, + response_format={"type": "json_object"}, + messages=[ + {"role": "system", "content": VOCAB_SYSTEM_PROMPT}, + {"role": "user", "content": f"다음 3문장에서 어려운 단어 하나를 골라 풀이해주세요:\n\n{joined}"}, + ], + temperature=0.3, + ) + + raw = json.loads(response.choices[0].message.content) + + return VocabExtractResponse( + word=raw.get("word", "") or "", + meaning=raw.get("meaning", "") or "", + ) SYSTEM_PROMPT = """당신은 유아 동화 텍스트에서 지식그래프를 추출하는 AI입니다. From 533991aed7fef0565abace9fb6f0a22c130230a5 Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Thu, 30 Apr 2026 23:05:03 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20POST=20/vocab/extract=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3문장 입력 → 어려운 단어 1개 + 유아 풀이 응답. 실패 시 500 + VOCAB_EXTRACT_FAILED 에러 코드. --- app/routers/vocab.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 app/routers/vocab.py diff --git a/app/routers/vocab.py b/app/routers/vocab.py new file mode 100644 index 0000000..4d6e806 --- /dev/null +++ b/app/routers/vocab.py @@ -0,0 +1,29 @@ +import logging + +from fastapi import APIRouter, HTTPException + +from app.schemas.graph import ErrorResponse +from app.schemas.vocab import VocabExtractRequest, VocabExtractResponse +from app.services.llm_service import extract_vocab + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +@router.post( + "/vocab/extract", + response_model=VocabExtractResponse, + responses={ + 500: {"model": ErrorResponse}, + }, +) +def extract_vocab_endpoint(req: VocabExtractRequest): + try: + return extract_vocab(req.sentences) + except Exception as e: + logger.exception("vocab extract 실패: %s", e) + raise HTTPException( + status_code=500, + detail={"code": "VOCAB_EXTRACT_FAILED", "message": "단어장 추출에 실패했습니다."}, + ) From 3b3e4ac119d66c98e12d606757c2403075451d00 Mon Sep 17 00:00:00 2001 From: zzuhannn Date: Thu, 30 Apr 2026 23:05:28 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20main.py=EC=97=90=20vocab=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=84=B0=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/main.py b/app/main.py index 37fc4bb..e51d58c 100644 --- a/app/main.py +++ b/app/main.py @@ -2,6 +2,7 @@ from fastapi.middleware.cors import CORSMiddleware from app.routers.graph import router as graph_router +from app.routers.vocab import router as vocab_router app = FastAPI(title="Kkumteul AI Server") @@ -14,6 +15,7 @@ ) app.include_router(graph_router) +app.include_router(vocab_router) @app.get("/health")