음성 재생과 운영 배포 정리

This commit is contained in:
Yun Chan 2026-06-28 12:18:20 +09:00
parent 8ed185ce6c
commit ac7db95542
1020 changed files with 46863 additions and 2175 deletions

View file

@ -22,7 +22,7 @@ from pydantic import BaseModel, Field
from ..db import acquire, get_pool
from ..deps import AIView, Principal, Role, require_role
from ..services import rag
from ..services import live_coach, rag
router = APIRouter(prefix="/kb", tags=["kb"])
@ -104,6 +104,24 @@ class IndexResponse(BaseModel):
degraded: bool = False
class LiveCoachSourcePackSyncItem(BaseModel):
source_id: str
doc_id: Optional[int]
chunks_indexed: int
skipped_unchanged: bool
embedded: bool
degraded: bool = False
class LiveCoachSourcePackSyncResponse(BaseModel):
sources_upserted: int
chunks_indexed: int
skipped_unchanged: int
embedded: bool
degraded: bool = False
items: list[LiveCoachSourcePackSyncItem] = Field(default_factory=list)
# ── 헬퍼: rag.NotConfigured → 503 ───────────────────────
def _to_chunk_out(c: rag.RetrievedChunk) -> ChunkOut:
return ChunkOut(
@ -222,6 +240,7 @@ async def eval_grounding(body: KBSearchRequest) -> KBSearchResponse:
query=body.query,
k=body.k,
kinds=body.kb_kind,
source_ids=body.source_id,
rerank=body.rerank,
)
try:
@ -319,3 +338,80 @@ async def index_document(
embedded=result.embedded,
degraded=result.degraded,
)
@router.post(
"/live-coach/source-packs/sync",
response_model=LiveCoachSourcePackSyncResponse,
status_code=status.HTTP_202_ACCEPTED,
)
async def sync_live_coach_source_packs(
principal: Annotated[Principal, Depends(require_role(Role.ADMIN))],
) -> LiveCoachSourcePackSyncResponse:
"""허가된 라이브 코칭 source pack을 kb.source/kb.chunk RAG 색인에 적재한다.
로컬 `data/kb/live_coaching_*.json` UI 즉시 코칭의 기본 근거이고, 경로는 같은 자료를
evaluator RAG 검색에도 올린다. source row를 먼저 upsert한 content_hash 기반 증분 색인을
수행한다. 임베딩 모델 미가용 BM25-only degraded 색인으로 이어진다.
"""
source_rows = live_coach.build_rag_source_rows()
index_payloads = live_coach.build_rag_index_payloads()
if not source_rows or not index_payloads:
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="live coach source packs are empty",
)
items: list[LiveCoachSourcePackSyncItem] = []
source_row_by_id = {row["source_id"]: row for row in source_rows}
try:
async with acquire() as conn:
for row in source_rows:
await conn.execute(
"""
INSERT INTO kb.source
(source_id, title, kb_kind, license_class, origin_path, citation, external_llm_ok)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (source_id) DO UPDATE SET
title = EXCLUDED.title,
kb_kind = EXCLUDED.kb_kind,
license_class = EXCLUDED.license_class,
origin_path = EXCLUDED.origin_path,
citation = EXCLUDED.citation,
external_llm_ok = EXCLUDED.external_llm_ok
""",
row["source_id"],
row["title"],
row["kb_kind"],
row["license_class"],
row["origin_path"],
row["citation"],
row["external_llm_ok"],
)
for payload in index_payloads:
if payload["source_id"] not in source_row_by_id:
continue
result = await rag.index_document(conn, rag.IndexRequest(**payload))
items.append(
LiveCoachSourcePackSyncItem(
source_id=payload["source_id"],
doc_id=result.doc_id,
chunks_indexed=result.chunks_indexed,
skipped_unchanged=result.skipped_unchanged,
embedded=result.embedded,
degraded=result.degraded,
)
)
except rag.NotConfigured as exc:
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, detail=f"RAG not configured: {exc}") from exc
except RuntimeError as exc:
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, detail=f"DB not ready: {exc}") from exc
return LiveCoachSourcePackSyncResponse(
sources_upserted=len(source_rows),
chunks_indexed=sum(item.chunks_indexed for item in items),
skipped_unchanged=sum(1 for item in items if item.skipped_unchanged),
embedded=all(item.embedded for item in items) if items else False,
degraded=any(item.degraded for item in items),
items=items,
)