Merge pull request 'feat(study): Mac mini derived-worker (PR-MacMini-Derived-Worker-1)' (#21) from feat/macmini-derived-explanation into main
Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""PR-MacMini-Derived-Worker-1 internal endpoint.
|
||||
|
||||
Mac mini derived-worker 가 study explanation 가공을 위해 호출.
|
||||
GPU = RAG context provider (LLM generation X), Mac mini = LLM 가공 공장.
|
||||
Bearer token 보호 (settings.internal_worker_token).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Path, Response, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.config import settings
|
||||
from core.database import async_session
|
||||
from models.study_question import StudyQuestion
|
||||
from services.study.explanation_rag import gather_explanation_context, render_evidence_block
|
||||
from workers.study_explanation_worker import _render_envelope_prompt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _verify_token(authorization: str | None = Header(default=None)) -> None:
|
||||
if not settings.internal_worker_token:
|
||||
raise HTTPException(status_code=503, detail="internal_worker_token not configured")
|
||||
if not authorization or not authorization.lower().startswith("bearer "):
|
||||
raise HTTPException(status_code=401, detail="missing Bearer token")
|
||||
token = authorization[7:].strip()
|
||||
if token != settings.internal_worker_token:
|
||||
raise HTTPException(status_code=403, detail="invalid token")
|
||||
|
||||
|
||||
async def _session() -> AsyncSession:
|
||||
async with async_session() as s:
|
||||
yield s
|
||||
|
||||
|
||||
@router.get("/explanation-context/{question_id}")
|
||||
async def get_explanation_context(
|
||||
question_id: int = Path(..., ge=1),
|
||||
_auth: None = Depends(_verify_token),
|
||||
session: AsyncSession = Depends(_session),
|
||||
):
|
||||
question = await session.get(StudyQuestion, question_id)
|
||||
if question is None or question.deleted_at is not None:
|
||||
raise HTTPException(status_code=410, detail="question deleted or missing")
|
||||
if question.ai_explanation_status == "ready":
|
||||
raise HTTPException(status_code=410, detail="explanation already ready")
|
||||
|
||||
ctx = await gather_explanation_context(session, question.user_id, question)
|
||||
docs_count = len(ctx.documents)
|
||||
qs_count = len(ctx.questions)
|
||||
if docs_count == 0 and qs_count == 0:
|
||||
return Response(status_code=204)
|
||||
|
||||
doc_block = render_evidence_block(ctx.documents)
|
||||
q_block = render_evidence_block(ctx.questions)
|
||||
rendered_prompt = _render_envelope_prompt(question, doc_block, q_block)
|
||||
|
||||
logger.info(
|
||||
"internal_study_context qid=%s docs=%s questions=%s prompt_len=%s",
|
||||
question_id, docs_count, qs_count, len(rendered_prompt),
|
||||
)
|
||||
|
||||
return {
|
||||
"question_id": question.id,
|
||||
"question_correct_choice": question.correct_choice,
|
||||
"rendered_prompt": rendered_prompt,
|
||||
"evidence_summary": {
|
||||
"documents_count": docs_count,
|
||||
"questions_count": qs_count,
|
||||
},
|
||||
}
|
||||
@@ -101,11 +101,20 @@ class Settings(BaseModel):
|
||||
# 업로드 한도 (authoritative policy)
|
||||
upload: UploadConfig = UploadConfig()
|
||||
|
||||
# PR-MacMini-Derived-Worker-1: study explanation owner = Mac mini
|
||||
# GPU 측은 false 로 설정 (.env), explanation 분기 skip guard 트리거.
|
||||
study_explanation_enabled: bool = True
|
||||
|
||||
# internal endpoint Bearer token (Mac mini derived-worker 호출용)
|
||||
internal_worker_token: str = ""
|
||||
|
||||
|
||||
def load_settings() -> Settings:
|
||||
"""config.yaml + 환경변수에서 설정 로딩"""
|
||||
# 환경변수 (docker-compose에서 주입)
|
||||
database_url = os.getenv("DATABASE_URL", "")
|
||||
study_explanation_enabled = os.getenv("STUDY_EXPLANATION_ENABLED", "true").lower() in ("1", "true", "yes")
|
||||
internal_worker_token = os.getenv("INTERNAL_WORKER_TOKEN", "")
|
||||
jwt_secret = os.getenv("JWT_SECRET", "")
|
||||
totp_secret = os.getenv("TOTP_SECRET", "")
|
||||
eval_runner_token = os.getenv("EVAL_RUNNER_TOKEN", "")
|
||||
@@ -186,6 +195,8 @@ def load_settings() -> Settings:
|
||||
taxonomy=taxonomy,
|
||||
document_types=document_types,
|
||||
upload=upload_cfg,
|
||||
study_explanation_enabled=study_explanation_enabled,
|
||||
internal_worker_token=internal_worker_token,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy import func, select, text
|
||||
|
||||
from api.audio import router as audio_router
|
||||
from api.internal_study import router as internal_study_router
|
||||
from api.auth import router as auth_router
|
||||
from api.briefing import router as briefing_router
|
||||
from api.config import router as config_router
|
||||
@@ -143,6 +144,7 @@ app.include_router(news_router, prefix="/api/news", tags=["news"])
|
||||
app.include_router(digest_router, prefix="/api/digest", tags=["digest"])
|
||||
app.include_router(briefing_router, prefix="/api/briefing", tags=["briefing"])
|
||||
app.include_router(audio_router, prefix="/api/audio", tags=["audio"])
|
||||
app.include_router(internal_study_router, prefix="/internal/study", tags=["internal-study"])
|
||||
app.include_router(video_router, prefix="/api/video", tags=["video"])
|
||||
app.include_router(study_sessions_router, prefix="/api/study-sessions", tags=["study-sessions"])
|
||||
app.include_router(study_topics_router, prefix="/api/study-topics", tags=["study-topics"])
|
||||
|
||||
@@ -16,6 +16,7 @@ from sqlalchemy import select, update
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from core.database import async_session
|
||||
from core.config import settings
|
||||
from core.utils import setup_logger
|
||||
from models.study_question_job import StudyQuestionJob
|
||||
from workers.study_explanation_worker import run_explanation_job
|
||||
@@ -80,6 +81,11 @@ async def consume_study_queue() -> None:
|
||||
continue # 다른 cycle 에서 이미 처리
|
||||
|
||||
if job.kind == "explanation":
|
||||
if not settings.study_explanation_enabled:
|
||||
# PR-MacMini-Derived-Worker-1: explanation owner = Mac mini.
|
||||
# status/attempts 변경하지 않고 pending 그대로 유지 → Mac mini worker 가 흡수.
|
||||
logger.info("skip explanation owner=macmini job_id=%s qid=%s", job.id, job.study_question_id)
|
||||
continue
|
||||
await run_explanation_job(s, job)
|
||||
elif job.kind == "session_summary":
|
||||
# Phase 4-B 미구현 — 즉시 skipped 처리 (lost in queue 방지)
|
||||
|
||||
+5
-2
@@ -9,7 +9,7 @@ services:
|
||||
POSTGRES_USER: pkm
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
ports:
|
||||
- "127.0.0.1:15432:5432"
|
||||
- "100.110.63.63:15432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U pkm"]
|
||||
interval: 5s
|
||||
@@ -173,7 +173,7 @@ services:
|
||||
fastapi:
|
||||
build: ./app
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
- "100.110.63.63:8000:8000"
|
||||
volumes:
|
||||
- ${NAS_NFS_PATH:-/mnt/nas/Document_Server}:/documents
|
||||
- ./config.yaml:/app/config.yaml:ro
|
||||
@@ -200,6 +200,9 @@ services:
|
||||
- STT_ENDPOINT=http://stt-service:3300
|
||||
# KGS Code 등 외부 학습 자료 추가 스캔 경로 (host .env 에서 주입). 빈 값이면 비활성.
|
||||
- ADDITIONAL_WATCH_TARGETS=${ADDITIONAL_WATCH_TARGETS:-}
|
||||
# PR-MacMini-Derived-Worker-1
|
||||
- STUDY_EXPLANATION_ENABLED=${STUDY_EXPLANATION_ENABLED:-true}
|
||||
- INTERNAL_WORKER_TOKEN=${INTERNAL_WORKER_TOKEN}
|
||||
# Voice Memo PoC v1 — bot 계정 한정 long-expiry access token. default false → 일반 운영 영향 0.
|
||||
# 활성화: host .env 에 VOICE_MEMO_BOT_TOKEN_ENABLED=true. plan: rosy-launching-otter.md
|
||||
- VOICE_MEMO_BOT_TOKEN_ENABLED=${VOICE_MEMO_BOT_TOKEN_ENABLED:-false}
|
||||
|
||||
Reference in New Issue
Block a user