Files
tk-factory-services/ai-service/main.py
Hyungi Ahn 5b1b89254c feat: RAG 임베딩 자동 동기화 + AI 서비스 개선
- 부적합 라이프사이클 전 과정에서 Qdrant 임베딩 자동 동기화
  - 관리함 5개 저장 함수 + 수신함 상태 변경 시 fire-and-forget sync
  - 30분 주기 전체 재동기화 안전망 (FastAPI lifespan 백그라운드 태스크)
  - build_document_text에 카테고리(final_category/category) 포함
- RAG 질의에 DB 통계 집계 지원 (카테고리별/부서별 건수)
- Qdrant client.search → query_points API 마이그레이션
- AI 어시스턴트 페이지 권한 추가 (tkuser)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 13:05:32 +09:00

95 lines
3.0 KiB
Python

import asyncio
import logging
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from routers import health, embeddings, classification, daily_report, rag, chatbot
from db.vector_store import vector_store
from db.metadata_store import metadata_store
from services.ollama_client import ollama_client
from middlewares.auth import verify_token
logger = logging.getLogger(__name__)
PUBLIC_PATHS = {"/", "/api/ai/health", "/api/ai/models"}
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if request.method == "OPTIONS" or request.url.path in PUBLIC_PATHS:
return await call_next(request)
try:
request.state.user = await verify_token(request)
except Exception as e:
return JSONResponse(status_code=401, content={"detail": str(e.detail) if hasattr(e, "detail") else "인증 실패"})
return await call_next(request)
async def _periodic_sync():
"""30분마다 전체 이슈 재동기화 (안전망)"""
await asyncio.sleep(60) # 시작 후 1분 대기 (초기화 완료 보장)
while True:
try:
from services.embedding_service import sync_all_issues
result = await sync_all_issues()
logger.info(f"Periodic sync completed: {result}")
except asyncio.CancelledError:
logger.info("Periodic sync task cancelled")
return
except Exception as e:
logger.warning(f"Periodic sync failed: {e}")
await asyncio.sleep(1800) # 30분
@asynccontextmanager
async def lifespan(app: FastAPI):
vector_store.initialize()
metadata_store.initialize()
sync_task = asyncio.create_task(_periodic_sync())
yield
sync_task.cancel()
await ollama_client.close()
app = FastAPI(
title="TK AI Service",
description="AI 서비스 (유사 검색, 분류, 보고서)",
version="1.0.0",
lifespan=lifespan,
)
ALLOWED_ORIGINS = [
"https://tkfb.technicalkorea.net",
"https://tkreport.technicalkorea.net",
"https://tkqc.technicalkorea.net",
"https://tkuser.technicalkorea.net",
]
if os.getenv("ENV", "production") == "development":
ALLOWED_ORIGINS += ["http://localhost:30080", "http://localhost:30180", "http://localhost:30280"]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(AuthMiddleware)
app.include_router(health.router, prefix="/api/ai")
app.include_router(embeddings.router, prefix="/api/ai")
app.include_router(classification.router, prefix="/api/ai")
app.include_router(daily_report.router, prefix="/api/ai")
app.include_router(rag.router, prefix="/api/ai")
app.include_router(chatbot.router, prefix="/api/ai")
@app.get("/")
async def root():
return {"message": "TK AI Service", "version": "1.0.0"}