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
+129 -8
View File
@@ -1,6 +1,6 @@
"""
Azure Event Hub Consumer 서비스
회의록 확정 이벤트를 consume하여 RAG 저장소에 저장
회의록 확정 이벤트 및 세그먼트 생성 이벤트를 consume
"""
import asyncio
import json
@@ -13,7 +13,9 @@ from azure.eventhub.extensions.checkpointstoreblobaio import BlobCheckpointStore
from ..models.minutes import RagMinutes, MinutesSection
from ..db.rag_minutes_db import RagMinutesDB
from ..db.postgres_vector import PostgresVectorDB
from ..utils.embedding import EmbeddingGenerator
from ..utils.text_processor import extract_nouns_as_query
logger = logging.getLogger(__name__)
@@ -29,7 +31,9 @@ class EventHubConsumer:
storage_connection_string: str,
storage_container_name: str,
rag_minutes_db: RagMinutesDB,
embedding_gen: EmbeddingGenerator
embedding_gen: EmbeddingGenerator,
term_db: Optional[PostgresVectorDB] = None,
config: Optional[Dict[str, Any]] = None
):
"""
초기화
@@ -42,6 +46,8 @@ class EventHubConsumer:
storage_container_name: Checkpoint 저장 컨테이너 이름
rag_minutes_db: RAG Minutes 데이터베이스
embedding_gen: Embedding 생성기
term_db: 용어집 데이터베이스 (선택)
config: 설정 딕셔너리 (선택)
"""
self.connection_string = connection_string
self.eventhub_name = eventhub_name
@@ -50,6 +56,8 @@ class EventHubConsumer:
self.storage_container_name = storage_container_name
self.rag_minutes_db = rag_minutes_db
self.embedding_gen = embedding_gen
self.term_db = term_db
self.config = config or {}
self.client: Optional[EventHubConsumerClient] = None
self.is_running = False
@@ -106,13 +114,18 @@ class EventHubConsumer:
event_body = event.body_as_str()
event_data = json.loads(event_body)
logger.info(f"이벤트 수신: {event_data.get('eventType', 'unknown')}")
logger.info(f"이벤트 수신: {event_data.get('data', 'unknown')}")
event_type = event_data.get('eventType', 'unknown')
logger.info(f"이벤트 수신: {event_type}")
# 회의록 확정 이벤트 처리
if event_data.get("eventType") == "MINUTES_FINALIZED":
# 이벤트 타입별 처리
if event_type == "MINUTES_FINALIZED":
# 회의록 확정 이벤트
await self._process_minutes_event(event_data)
elif event_type == "SegmentCreated":
# 세그먼트 생성 이벤트 - 용어검색 실행
await self._process_segment_event(event_data)
# Checkpoint 업데이트
await partition_context.update_checkpoint(event)
@@ -131,6 +144,110 @@ class EventHubConsumer:
"""
logger.error(f"Event Hub 에러 (Partition {partition_context.partition_id}): {str(error)}")
async def _process_segment_event(self, event_data: Dict[str, Any]):
"""
세그먼트 생성 이벤트 처리 - 용어검색 실행
Args:
event_data: 이벤트 데이터
"""
try:
# 용어집 DB가 없으면 스킵
if not self.term_db:
logger.debug("용어집 DB가 설정되지 않아 용어검색을 스킵합니다")
return
# 세그먼트 데이터 추출
segment_id = event_data.get("segmentId")
text = event_data.get("text", "")
meeting_id = event_data.get("meetingId")
if not text:
logger.warning(f"세그먼트 {segment_id}에 텍스트가 없습니다")
return
logger.info(f"세그먼트 용어검색 시작: {segment_id} (회의: {meeting_id})")
logger.info(f"텍스트: {text[:100]}...")
# 1. 명사 추출하여 검색 쿼리 생성
search_query = extract_nouns_as_query(text)
logger.info(f"검색 쿼리 변환: '{text[:30]}...''{search_query}'")
# 2. 용어검색 설정
config = self.config.get("term_glossary", {})
search_config = config.get("search", {})
top_k = search_config.get("top_k", 5)
confidence_threshold = search_config.get("confidence_threshold", 0.7)
keyword_weight = search_config.get("keyword_weight", 0.4)
vector_weight = search_config.get("vector_weight", 0.6)
# 3. 키워드 검색
keyword_results = self.term_db.search_by_keyword(
query=search_query,
top_k=top_k,
confidence_threshold=confidence_threshold
)
# 4. 벡터 검색
query_embedding = self.embedding_gen.generate_embedding(search_query)
vector_results = self.term_db.search_by_vector(
query_embedding=query_embedding,
top_k=top_k,
confidence_threshold=confidence_threshold
)
# 5. 하이브리드 검색 결과 통합 (RRF)
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)
else:
# 이미 있는 경우 점수 합산
for r in results:
if r["term"].term_id == term_id:
r["relevance_score"] += result["relevance_score"] * vector_weight
break
# 점수 기준 재정렬
results.sort(key=lambda x: x["relevance_score"], reverse=True)
results = results[:top_k]
# 6. 검색 결과 로깅
if results:
logger.info(f"세그먼트 {segment_id} 용어검색 완료: {len(results)}개 발견")
for idx, result in enumerate(results, 1):
term = result["term"]
score = result["relevance_score"]
logger.info(
f" [{idx}] {term.term_name} "
f"(카테고리: {term.category}, 점수: {score:.3f})"
)
else:
logger.info(f"세그먼트 {segment_id}에서 매칭되는 용어를 찾지 못했습니다")
# 7. 선택적: 검색 결과를 별도 테이블에 저장하거나 Event Hub로 발행
# TODO: 필요시 검색 결과를 저장하거나 downstream 서비스로 전달
except Exception as e:
logger.error(f"세그먼트 이벤트 처리 실패: {str(e)}", exc_info=True)
def _convert_datetime_array_to_string(self, value: Union[str, List, None]) -> Optional[str]:
"""
Java LocalDateTime 배열을 ISO 8601 문자열로 변환
@@ -302,7 +419,8 @@ class EventHubConsumer:
async def start_consumer(
config: Dict[str, Any],
rag_minutes_db: RagMinutesDB,
embedding_gen: EmbeddingGenerator
embedding_gen: EmbeddingGenerator,
term_db: Optional[PostgresVectorDB] = None
):
"""
Event Hub Consumer 시작 (비동기)
@@ -311,6 +429,7 @@ async def start_consumer(
config: 설정 딕셔너리
rag_minutes_db: RAG Minutes 데이터베이스
embedding_gen: Embedding 생성기
term_db: 용어집 데이터베이스 (선택)
"""
eventhub_config = config["eventhub"]
@@ -321,7 +440,9 @@ async def start_consumer(
storage_connection_string=eventhub_config["storage"]["connection_string"],
storage_container_name=eventhub_config["storage"]["container_name"],
rag_minutes_db=rag_minutes_db,
embedding_gen=embedding_gen
embedding_gen=embedding_gen,
term_db=term_db,
config=config
)
try: