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
2 changes: 2 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -14,6 +15,7 @@
)

app.include_router(graph_router)
app.include_router(vocab_router)


@app.get("/health")
Expand Down
29 changes: 29 additions & 0 deletions app/routers/vocab.py
Original file line number Diff line number Diff line change
@@ -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": "단어장 추출에 실패했습니다."},
)
17 changes: 17 additions & 0 deletions app/schemas/vocab.py
Original file line number Diff line number Diff line change
@@ -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="유아 눈높이로 재구성된 풀이 (없으면 빈 문자열)")
47 changes: 47 additions & 0 deletions app/services/llm_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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입니다.

Expand Down