feat: init rag service

This commit is contained in:
djeon
2025-10-29 05:54:08 +09:00
parent 44ae9c546f
commit 5d897cb845
54 changed files with 6425 additions and 0 deletions
View File
Binary file not shown.
Binary file not shown.
+506
View File
@@ -0,0 +1,506 @@
"""
Vector DB 통합 시스템 FastAPI 애플리케이션
"""
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from typing import List, Dict, Any
import logging
from pathlib import Path
from ..models.term import (
Term,
TermSearchRequest,
TermSearchResult,
TermExplainRequest,
TermExplanation,
TermStats
)
from ..models.document import (
DocumentSearchRequest,
DocumentSearchResult,
DocumentStats
)
from ..models.minutes import (
MinutesSearchRequest,
MinutesSearchResult
)
from ..db.postgres_vector import PostgresVectorDB
from ..db.azure_search import AzureAISearchDB
from ..db.rag_minutes_db import RagMinutesDB
from ..services.claude_service import ClaudeService
from ..utils.config import load_config, get_database_url
from ..utils.embedding import EmbeddingGenerator
from ..utils.text_processor import extract_nouns_as_query
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# FastAPI 앱 생성
app = FastAPI(
title="Vector DB 통합 시스템",
description="회의록 작성 시스템을 위한 Vector DB 기반 용어집 및 관련자료 검색 API",
version="1.0.0"
)
# CORS 설정
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 전역 변수 (의존성 주입용)
_config = None
_term_db = None
_doc_db = None
_rag_minutes_db = None
_embedding_gen = None
_claude_service = None
def get_config():
"""설정 로드"""
global _config
if _config is None:
config_path = Path(__file__).parent.parent.parent / "config.yaml"
_config = load_config(str(config_path))
return _config
def get_term_db():
"""용어집 DB 연결"""
global _term_db
if _term_db is None:
config = get_config()
db_url = get_database_url(config)
_term_db = PostgresVectorDB(db_url)
return _term_db
def get_doc_db():
"""관련자료 DB 연결"""
global _doc_db
if _doc_db is None:
config = get_config()
azure_search = config["azure_search"]
_doc_db = AzureAISearchDB(
endpoint=azure_search["endpoint"],
api_key=azure_search["api_key"],
index_name=azure_search["index_name"],
api_version=azure_search["api_version"]
)
return _doc_db
def get_rag_minutes_db():
"""RAG 회의록 DB 연결"""
global _rag_minutes_db
if _rag_minutes_db is None:
config = get_config()
db_url = get_database_url(config)
_rag_minutes_db = RagMinutesDB(db_url)
return _rag_minutes_db
def get_embedding_gen():
"""임베딩 생성기"""
global _embedding_gen
if _embedding_gen is None:
config = get_config()
azure_openai = config["azure_openai"]
_embedding_gen = EmbeddingGenerator(
api_key=azure_openai["api_key"],
endpoint=azure_openai["endpoint"],
model=azure_openai["embedding_model"],
dimension=azure_openai["embedding_dimension"],
api_version=azure_openai["api_version"]
)
return _embedding_gen
def get_claude_service():
"""Claude 서비스"""
global _claude_service
if _claude_service is None:
config = get_config()
claude = config["claude"]
_claude_service = ClaudeService(
api_key=claude["api_key"],
model=claude["model"],
max_tokens=claude["max_tokens"],
temperature=claude["temperature"]
)
return _claude_service
# ============================================================================
# 용어집 API
# ============================================================================
@app.get("/")
async def root():
"""루트 엔드포인트"""
return {
"service": "Vector DB 통합 시스템",
"version": "1.0.0",
"endpoints": {
"용어집": "/api/terms/*",
"관련자료": "/api/documents/*"
}
}
@app.post("/api/terms/search", response_model=List[TermSearchResult])
async def search_terms(
request: TermSearchRequest,
term_db: PostgresVectorDB = Depends(get_term_db),
embedding_gen: EmbeddingGenerator = Depends(get_embedding_gen)
):
"""
용어 검색 (Hybrid: Keyword + Vector)
Args:
request: 검색 요청
Returns:
검색 결과 리스트
"""
try:
config = get_config()
# 명사 추출하여 검색 쿼리 생성
search_query = extract_nouns_as_query(request.query)
logger.info(f"검색 쿼리 변환: '{request.query}''{search_query}'")
if request.search_type == "keyword":
# 키워드 검색
results = term_db.search_by_keyword(
query=search_query,
top_k=request.top_k,
confidence_threshold=request.confidence_threshold
)
elif request.search_type == "vector":
# 벡터 검색 (임베딩은 원본 쿼리 사용)
query_embedding = embedding_gen.generate_embedding(search_query)
results = term_db.search_by_vector(
query_embedding=query_embedding,
top_k=request.top_k,
confidence_threshold=request.confidence_threshold
)
else: # hybrid
# 하이브리드 검색
keyword_results = term_db.search_by_keyword(
query=search_query,
top_k=request.top_k,
confidence_threshold=request.confidence_threshold
)
query_embedding = embedding_gen.generate_embedding(search_query)
vector_results = term_db.search_by_vector(
query_embedding=query_embedding,
top_k=request.top_k,
confidence_threshold=request.confidence_threshold
)
# RRF 통합
keyword_weight = config["term_glossary"]["search"]["keyword_weight"]
vector_weight = config["term_glossary"]["search"]["vector_weight"]
# 간단한 가중합
results = []
seen_ids = set()
for result in keyword_results:
term_id = result["term"].term_id
if term_id not in seen_ids:
result["relevance_score"] *= keyword_weight
result["match_type"] = "hybrid"
results.append(result)
seen_ids.add(term_id)
for result in vector_results:
term_id = result["term"].term_id
if term_id not in seen_ids:
result["relevance_score"] *= vector_weight
result["match_type"] = "hybrid"
results.append(result)
seen_ids.add(term_id)
# 점수 기준 재정렬
results.sort(key=lambda x: x["relevance_score"], reverse=True)
results = results[:request.top_k]
# 응답 형식으로 변환
return [
TermSearchResult(
term=result["term"],
relevance_score=result["relevance_score"],
match_type=result["match_type"]
)
for result in results
]
except Exception as e:
logger.error(f"용어 검색 실패: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/terms/{term_id}", response_model=Term)
async def get_term(
term_id: str,
term_db: PostgresVectorDB = Depends(get_term_db)
):
"""
용어 상세 조회
Args:
term_id: 용어 ID
Returns:
용어 객체
"""
try:
term = term_db.get_term_by_id(term_id)
if not term:
raise HTTPException(status_code=404, detail="용어를 찾을 수 없습니다")
return term
except HTTPException:
raise
except Exception as e:
logger.error(f"용어 조회 실패: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/terms/{term_id}/explain", response_model=TermExplanation)
async def explain_term(
term_id: str,
request: TermExplainRequest,
term_db: PostgresVectorDB = Depends(get_term_db),
claude_service: ClaudeService = Depends(get_claude_service)
):
"""
용어 맥락 기반 설명 생성 (Claude AI)
Args:
term_id: 용어 ID
request: 설명 요청
Returns:
용어 설명
"""
try:
# 용어 조회
term = term_db.get_term_by_id(term_id)
if not term:
raise HTTPException(status_code=404, detail="용어를 찾을 수 없습니다")
# Claude AI 호출
result = claude_service.explain_term(
term_name=term.term_name,
definition=term.definition,
context=term.context,
meeting_context=request.meeting_context
)
return TermExplanation(
term=term,
explanation=result["explanation"],
context_documents=[],
generated_by=result["generated_by"],
cached=result["cached"]
)
except HTTPException:
raise
except Exception as e:
logger.error(f"용어 설명 생성 실패: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/terms/stats", response_model=TermStats)
async def get_term_stats(term_db: PostgresVectorDB = Depends(get_term_db)):
"""용어 통계 조회"""
try:
stats = term_db.get_stats()
return TermStats(
total_terms=stats["total_terms"],
by_category=stats["by_category"],
by_source_type={},
avg_confidence=stats["avg_confidence"]
)
except Exception as e:
logger.error(f"통계 조회 실패: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# 관련자료 API
# ============================================================================
@app.post("/api/documents/search", response_model=List[DocumentSearchResult])
async def search_documents(
request: DocumentSearchRequest,
doc_db: AzureAISearchDB = Depends(get_doc_db),
embedding_gen: EmbeddingGenerator = Depends(get_embedding_gen)
):
"""
관련 문서 검색 (Hybrid Search + Semantic Ranking)
Args:
request: 검색 요청
Returns:
검색 결과 리스트
"""
try:
# 쿼리 임베딩 생성
query_embedding = embedding_gen.generate_embedding(request.query)
# Hybrid Search 실행
results = doc_db.hybrid_search(
query=request.query,
query_embedding=query_embedding,
top_k=request.top_k,
folder=request.folder,
document_type=request.document_type,
semantic_ranking=request.semantic_ranking
)
# 관련도 임계값 필터링
filtered_results = [
r for r in results
if r["relevance_score"] >= request.relevance_threshold
]
# 응답 형식으로 변환
return [
DocumentSearchResult(**result)
for result in filtered_results
]
except Exception as e:
logger.error(f"문서 검색 실패: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/documents/stats", response_model=DocumentStats)
async def get_document_stats(doc_db: AzureAISearchDB = Depends(get_doc_db)):
"""문서 통계 조회"""
try:
stats = doc_db.get_stats()
return DocumentStats(
total_documents=stats["total_documents"],
by_type=stats["by_type"],
by_domain={},
total_chunks=stats["total_chunks"]
)
except Exception as e:
logger.error(f"통계 조회 실패: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# RAG 회의록 API
# ============================================================================
@app.post("/api/minutes/search", response_model=List[MinutesSearchResult])
async def search_related_minutes(
request: MinutesSearchRequest,
rag_minutes_db: RagMinutesDB = Depends(get_rag_minutes_db),
embedding_gen: EmbeddingGenerator = Depends(get_embedding_gen)
):
"""
연관 회의록 검색 (Vector Similarity)
Args:
request: 검색 요청
Returns:
유사 회의록 리스트
"""
try:
# 쿼리 임베딩 생성
logger.info(f"회의록 검색 시작: {request.query[:50]}...")
query_embedding = embedding_gen.generate_embedding(request.query)
# 벡터 유사도 검색
results = rag_minutes_db.search_by_vector(
query_embedding=query_embedding,
top_k=request.top_k,
similarity_threshold=request.similarity_threshold
)
# 응답 형식으로 변환
search_results = [
MinutesSearchResult(
minutes=result["minutes"],
similarity_score=result["similarity_score"]
)
for result in results
]
logger.info(f"회의록 검색 완료: {len(search_results)}개 결과")
return search_results
except Exception as e:
logger.error(f"회의록 검색 실패: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/minutes/{minutes_id}")
async def get_minutes(
minutes_id: str,
rag_minutes_db: RagMinutesDB = Depends(get_rag_minutes_db)
):
"""
회의록 상세 조회
Args:
minutes_id: 회의록 ID
Returns:
회의록 객체
"""
try:
minutes = rag_minutes_db.get_minutes_by_id(minutes_id)
if not minutes:
raise HTTPException(status_code=404, detail="회의록을 찾을 수 없습니다")
return minutes
except HTTPException:
raise
except Exception as e:
logger.error(f"회의록 조회 실패: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/minutes/stats")
async def get_minutes_stats(rag_minutes_db: RagMinutesDB = Depends(get_rag_minutes_db)):
"""회의록 통계 조회"""
try:
stats = rag_minutes_db.get_stats()
return stats
except Exception as e:
logger.error(f"통계 조회 실패: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)