feat: rag 서비스 Event Hub 연동 및 연관 회의록 API 추가

This commit is contained in:
djeon
2025-10-29 15:29:40 +09:00
parent 5859b1c498
commit ad7975efbd
20 changed files with 2855 additions and 22 deletions
Binary file not shown.
+138 -1
View File
@@ -22,7 +22,9 @@ from ..models.document import (
)
from ..models.minutes import (
MinutesSearchRequest,
MinutesSearchResult
MinutesSearchResult,
RelatedMinutesRequest,
RelatedMinutesResponse
)
from ..db.postgres_vector import PostgresVectorDB
from ..db.azure_search import AzureAISearchDB
@@ -31,6 +33,7 @@ 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
from ..utils.redis_cache import RedisCache
# 로깅 설정
logging.basicConfig(
@@ -62,6 +65,7 @@ _doc_db = None
_rag_minutes_db = None
_embedding_gen = None
_claude_service = None
_redis_cache = None
def get_config():
@@ -139,6 +143,22 @@ def get_claude_service():
return _claude_service
def get_redis_cache():
"""Redis 캐시"""
global _redis_cache
if _redis_cache is None:
config = get_config()
redis_config = config["redis"]
_redis_cache = RedisCache(
host=redis_config["host"],
port=redis_config["port"],
db=redis_config["db"],
password=redis_config.get("password"),
decode_responses=redis_config.get("decode_responses", True)
)
return _redis_cache
# ============================================================================
# 용어집 API
# ============================================================================
@@ -501,6 +521,123 @@ async def get_minutes_stats(rag_minutes_db: RagMinutesDB = Depends(get_rag_minut
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/minutes/related", response_model=List[RelatedMinutesResponse])
async def get_related_minutes(
request: RelatedMinutesRequest,
rag_minutes_db: RagMinutesDB = Depends(get_rag_minutes_db),
embedding_gen: EmbeddingGenerator = Depends(get_embedding_gen),
redis_cache: RedisCache = Depends(get_redis_cache)
):
"""
연관 회의록 조회 (Option A: DB 조회 후 벡터 검색 + Redis 캐싱)
Args:
request: 연관 회의록 조회 요청
- minute_id: 기준 회의록 ID
- meeting_title: 회의 제목 (미사용)
- summary: 회의록 요약 (미사용)
- top_k: 반환할 최대 결과 수
- similarity_threshold: 최소 유사도 임계값
Returns:
연관 회의록 리스트
Process:
1. Redis 캐시에서 연관 회의록 결과 조회
2. 캐시 MISS 시:
a. minute_id로 rag_minutes 테이블에서 회의록 조회 (캐싱)
b. full_content를 벡터 임베딩으로 변환
c. 벡터 DB에서 유사도 검색 (자기 자신 제외)
d. 결과를 Redis에 캐싱
3. 연관 회의록 목록 반환
"""
try:
config = get_config()
cache_config = config.get("rag_minutes", {}).get("cache", {})
cache_prefix = cache_config.get("prefix", "minutes:")
minutes_ttl = cache_config.get("ttl", 1800)
related_ttl = cache_config.get("related_ttl", 3600)
logger.info(f"연관 회의록 조회 시작: minute_id={request.minute_id}")
# 1. 캐시 키 생성
related_cache_key = (
f"{cache_prefix}related:{request.minute_id}:"
f"{request.top_k}:{request.similarity_threshold}"
)
# 2. 캐시 조회
cached_results = redis_cache.get(related_cache_key)
if cached_results:
logger.info(f"연관 회의록 캐시 HIT: {related_cache_key}")
return [
RelatedMinutesResponse(**result)
for result in cached_results
]
# 3. 캐시 MISS - DB 조회
logger.info(f"연관 회의록 캐시 MISS: {related_cache_key}")
# 3-1. 회의록 조회 (캐싱)
minutes_cache_key = f"{cache_prefix}{request.minute_id}"
base_minutes = redis_cache.get(minutes_cache_key)
if base_minutes:
logger.info(f"회의록 캐시 HIT: {minutes_cache_key}")
# RagMinutes 객체로 변환
from ..models.minutes import RagMinutes
base_minutes = RagMinutes(**base_minutes)
else:
logger.info(f"회의록 캐시 MISS: {minutes_cache_key}")
base_minutes = rag_minutes_db.get_minutes_by_id(request.minute_id)
if not base_minutes:
raise HTTPException(
status_code=404,
detail=f"회의록을 찾을 수 없습니다: {request.minute_id}"
)
# 캐시 저장
redis_cache.set(minutes_cache_key, base_minutes.dict(), minutes_ttl)
logger.info(f"기준 회의록 조회 완료: {base_minutes.title}")
# 3-2. full_content를 벡터 임베딩으로 변환
query_embedding = embedding_gen.generate_embedding(base_minutes.full_content)
logger.info(f"임베딩 생성 완료: {len(query_embedding)}차원")
# 3-3. 벡터 유사도 검색 (자기 자신 제외)
results = rag_minutes_db.search_by_vector(
query_embedding=query_embedding,
top_k=request.top_k,
similarity_threshold=request.similarity_threshold,
exclude_minutes_id=request.minute_id
)
# 4. 응답 형식으로 변환
related_minutes = [
RelatedMinutesResponse(
minutes=result["minutes"],
similarity_score=result["similarity_score"]
)
for result in results
]
# 5. 결과 캐싱
redis_cache.set(
related_cache_key,
[r.dict() for r in related_minutes],
related_ttl
)
logger.info(f"연관 회의록 조회 완료: {len(related_minutes)}개 결과")
return related_minutes
except HTTPException:
raise
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)