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
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ DB_POOL_INIT_MAX_BACKOFF_SEC=5
# 배포 시에만 .env 의 선택자 값을 바꿔 사용한다(평소 비활성).
AI_MODEL=gemini_flash
ACTIVE_TEXT_MODEL=gemini_flash
# 강의생성 전용 텍스트 모델 오버라이드(미설정 시 ACTIVE_TEXT_MODEL 사용). 예: qwen27b_modal
LECTURE_TEXT_MODEL=
AI_MODEL_TTS=gemini-tts
AI_MODEL_ASR=gemini-asr
AI_MODEL_OCR=gemini-ocr
Expand All @@ -52,7 +54,8 @@ CLAUDE_SONNET_API_KEY=
GEMINI_API_KEY=
GOOGLE_API_KEY=

# Claude Sonnet 설정
# Claude Sonnet 설정 (AssignmentGrader 텍스트 채점 1순위 — 실패 시 GEMINI_TEXT_MODEL 폴백)
# API 키: CLAUDE_SONNET_API_KEY 또는 ANTHROPIC_API_KEY
CLAUDE_SONNET_MODEL=claude-sonnet-4-6
CLAUDE_SONNET_MAX_CONCURRENCY=4
CLAUDE_SONNET_TIMEOUT_SEC=300
Expand Down
42 changes: 35 additions & 7 deletions app/modules/AssignmentGrader_V1/text_grader.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Gemini 로 텍스트 답안을 채점하는 원자적 모듈이다.
"""Claude Sonnet(1순위) → Gemini(폴백)로 텍스트 답안을 채점하는 원자적 모듈이다.

IN : assignment_title, assignment_description, assignment_questions, answer_text
OUT: TextGradingResult(score, feedback, ai_confidence)

채점 호출은 동기 google-genai SDK 로 수행한다(상위 핸들러가 asyncio.to_thread 로 감싼다).
채점 호출은 동기 SDK 로 수행한다(상위 핸들러가 asyncio.to_thread 로 감싼다).
"""

from __future__ import annotations
Expand All @@ -13,9 +13,13 @@
import os
from dataclasses import dataclass

from anthropic import Anthropic

from ai_connectors import _gemini_throttle as _throttle
from ai_connectors._gemini_common import build_genai_client, google_api_key
from ai_connectors.errors import AIConnectorError
from ai_connectors.text.claude_sonnet_connector import _first_text_block
from common.text_config import claude_sonnet_api_key, claude_sonnet_model

_LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -114,20 +118,25 @@ def grade_text_submission(
answer_text: str,
timeout_seconds: int = 90,
) -> TextGradingResult:
"""Gemini 로 텍스트 답안을 채점하고 결과를 반환한다.
"""Claude Sonnet → Gemini 폴백으로 텍스트 답안을 채점하고 결과를 반환한다.

동기 google-genai SDK 로 호출한다(상위 핸들러가 asyncio.to_thread 로 감싼다).
동기 SDK 로 호출한다(상위 핸들러가 asyncio.to_thread 로 감싼다).
호출/파싱 실패 시 TextGradingError를 raise한다. timeout_seconds 는 SDK
HTTP 옵션으로 전달해 무한 대기를 막는다.
"""
prompt = _build_grading_prompt(
assignment_title, assignment_description, assignment_questions, answer_text
)
_LOG.info("[TextGrader] Gemini 채점 시작 | 답안 길이: %d자", len(answer_text))
_LOG.info("[TextGrader] Claude 채점 시작 | 답안 길이: %d자", len(answer_text))

raw_output = _call_gemini(prompt, timeout_seconds)
raw_output: str | None = None
try:
raw_output = _call_claude(prompt, timeout_seconds)
except Exception as exc:
_LOG.warning("[TextGrader] Claude 채점 실패, Gemini 폴백 | %s", exc)
raw_output = _call_gemini(prompt, timeout_seconds)
if not raw_output:
raise TextGradingError("Gemini 채점 출력이 비어 있다")
raise TextGradingError("채점 출력이 비어 있다")

grading_result = _parse_grading_json(raw_output)
_LOG.info(
Expand All @@ -138,6 +147,25 @@ def grade_text_submission(
return grading_result


def _call_claude(prompt: str, timeout_seconds: int) -> str:
"""동기 Anthropic 호출로 채점 응답 텍스트를 받는다."""
api_key = claude_sonnet_api_key()
if api_key is None:
raise TextGradingError("CLAUDE_SONNET_API_KEY 또는 ANTHROPIC_API_KEY가 설정되지 않았다.")
try:
client = Anthropic(api_key=api_key, timeout=timeout_seconds)
msg = client.messages.create(
model=claude_sonnet_model(),
max_tokens=8192,
temperature=0.0,
messages=[{"role": "user", "content": prompt}],
)
except Exception as exc:
raise TextGradingError(f"Claude 채점 호출 실패: {exc}") from exc

return _first_text_block(msg).strip()


def _call_gemini(prompt: str, timeout_seconds: int) -> str:
"""동기 google-genai 호출로 채점 응답 텍스트를 받는다."""
# API 키 미설정은 즉시 명확한 오류로 전환한다(빈 채점 결과 방지)
Expand Down
2 changes: 2 additions & 0 deletions app/modules/ChapterStudio_V1/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ DATABASE_URL=postgresql://sw_ez:changeme@postgres:5432/sw_ez
DATABASE_SCHEMA=chapter_studio
AI_MODEL=qwen27b_modal
ACTIVE_TEXT_MODEL=qwen27b_modal
# 강의생성 전용 텍스트 모델 오버라이드(미설정 시 ACTIVE_TEXT_MODEL 사용). 예: qwen27b_modal
LECTURE_TEXT_MODEL=
ACTIVE_PLANNER_MODEL=opus46
ACTIVE_TTS_MODEL=gemini_tts
# 내용 정확성 검증 전용 커넥터(미설정 시 ACTIVE_TEXT_MODEL을 그대로 사용)
Expand Down
7 changes: 6 additions & 1 deletion app/modules/ChapterStudio_V1/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ def modal_token_secret() -> str | None:


def active_text_model() -> str:
return _optional_value("ACTIVE_TEXT_MODEL") or _optional_value("AI_MODEL") or "qwen27b_modal"
return (
_optional_value("LECTURE_TEXT_MODEL")
or _optional_value("ACTIVE_TEXT_MODEL")
or _optional_value("AI_MODEL")
or "qwen27b_modal"
)


def active_planner_model() -> str:
Expand Down
Loading