diff --git a/.env.example b/.env.example index 1c6e1a0..892a81a 100644 --- a/.env.example +++ b/.env.example @@ -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 @@ -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 diff --git a/app/modules/AssignmentGrader_V1/text_grader.py b/app/modules/AssignmentGrader_V1/text_grader.py index 378769f..2d18a5c 100644 --- a/app/modules/AssignmentGrader_V1/text_grader.py +++ b/app/modules/AssignmentGrader_V1/text_grader.py @@ -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 @@ -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__) @@ -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( @@ -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 키 미설정은 즉시 명확한 오류로 전환한다(빈 채점 결과 방지) diff --git a/app/modules/ChapterStudio_V1/.env.example b/app/modules/ChapterStudio_V1/.env.example index d74a7b5..d0ffacf 100644 --- a/app/modules/ChapterStudio_V1/.env.example +++ b/app/modules/ChapterStudio_V1/.env.example @@ -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을 그대로 사용) diff --git a/app/modules/ChapterStudio_V1/common/config.py b/app/modules/ChapterStudio_V1/common/config.py index 0b47262..012c349 100644 --- a/app/modules/ChapterStudio_V1/common/config.py +++ b/app/modules/ChapterStudio_V1/common/config.py @@ -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: