mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-13 09:29:10 +00:00
feat: init rag service
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
Claude AI 연동 서비스
|
||||
"""
|
||||
from anthropic import Anthropic
|
||||
from typing import Dict, Any, Optional
|
||||
from tenacity import retry, stop_after_attempt, wait_exponential
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClaudeService:
|
||||
"""Claude AI 서비스"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
model: str = "claude-3-5-sonnet-20241022",
|
||||
max_tokens: int = 1024,
|
||||
temperature: float = 0.3
|
||||
):
|
||||
"""
|
||||
초기화
|
||||
|
||||
Args:
|
||||
api_key: Claude API 키
|
||||
model: 모델명
|
||||
max_tokens: 최대 토큰 수
|
||||
temperature: 온도
|
||||
"""
|
||||
self.client = Anthropic(api_key=api_key)
|
||||
self.model = model
|
||||
self.max_tokens = max_tokens
|
||||
self.temperature = temperature
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=2, max=10)
|
||||
)
|
||||
def explain_term(
|
||||
self,
|
||||
term_name: str,
|
||||
definition: str,
|
||||
context: Optional[str],
|
||||
meeting_context: Optional[str] = None,
|
||||
related_docs: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
용어 설명 생성
|
||||
|
||||
Args:
|
||||
term_name: 용어명
|
||||
definition: 용어 정의
|
||||
context: 회사 내 사용 맥락
|
||||
meeting_context: 회의 맥락
|
||||
related_docs: 관련 문서
|
||||
|
||||
Returns:
|
||||
설명 결과
|
||||
"""
|
||||
# 시스템 프롬프트
|
||||
system_prompt = (
|
||||
"당신은 전문 용어를 회의 맥락에 맞춰 설명하는 AI 어시스턴트입니다. "
|
||||
"2-3문장으로 간결하게 설명하세요."
|
||||
)
|
||||
|
||||
# 사용자 프롬프트
|
||||
user_prompt = f"용어: {term_name}\n\n"
|
||||
user_prompt += f"정의: {definition}\n\n"
|
||||
|
||||
if context:
|
||||
user_prompt += f"회사 내 사용 맥락: {context}\n\n"
|
||||
|
||||
if meeting_context:
|
||||
user_prompt += f"회의 맥락: {meeting_context}\n\n"
|
||||
|
||||
if related_docs:
|
||||
user_prompt += f"관련 문서:\n{related_docs}\n\n"
|
||||
|
||||
user_prompt += (
|
||||
"위 정보를 바탕으로 이 용어를 2-3문장으로 간결하게 설명해주세요. "
|
||||
"회의 맥락이 있다면 회의와 연관지어 설명하세요."
|
||||
)
|
||||
|
||||
try:
|
||||
response = self.client.messages.create(
|
||||
model=self.model,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=self.temperature,
|
||||
system=system_prompt,
|
||||
messages=[
|
||||
{"role": "user", "content": user_prompt}
|
||||
]
|
||||
)
|
||||
|
||||
explanation = response.content[0].text
|
||||
tokens_used = response.usage.input_tokens + response.usage.output_tokens
|
||||
|
||||
return {
|
||||
"explanation": explanation,
|
||||
"generated_by": self.model,
|
||||
"tokens_used": tokens_used,
|
||||
"cached": False
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Claude API 호출 실패: {str(e)}")
|
||||
# Fallback: 기본 설명 반환
|
||||
fallback_explanation = f"{definition}"
|
||||
if context:
|
||||
fallback_explanation += f"\n\n{context}"
|
||||
|
||||
return {
|
||||
"explanation": fallback_explanation,
|
||||
"generated_by": "fallback",
|
||||
"tokens_used": 0,
|
||||
"cached": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=1, min=2, max=10)
|
||||
)
|
||||
def summarize_similar_content(
|
||||
self,
|
||||
current_meeting_title: str,
|
||||
current_meeting_date: str,
|
||||
current_meeting_agendas: str,
|
||||
past_meeting_title: str,
|
||||
past_meeting_date: str,
|
||||
past_meeting_content: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
관련 회의록 유사 내용 요약 생성
|
||||
|
||||
Args:
|
||||
current_meeting_title: 현재 회의 제목
|
||||
current_meeting_date: 현재 회의 날짜
|
||||
current_meeting_agendas: 현재 회의 안건
|
||||
past_meeting_title: 과거 회의 제목
|
||||
past_meeting_date: 과거 회의 날짜
|
||||
past_meeting_content: 과거 회의 내용
|
||||
|
||||
Returns:
|
||||
요약 결과
|
||||
"""
|
||||
# 시스템 프롬프트
|
||||
system_prompt = (
|
||||
"당신은 회의록 분석 전문가입니다. "
|
||||
"두 회의록을 비교하여 유사한 내용을 정확하게 추출하고 간결하게 요약합니다.\n\n"
|
||||
"중요한 원칙:\n"
|
||||
"1. 과거 회의록에서 실제로 다뤄진 내용만 포함하세요\n"
|
||||
"2. 환각(Hallucination)을 절대 생성하지 마세요\n"
|
||||
"3. 구체적인 날짜, 수치, 결정사항을 포함하세요\n"
|
||||
"4. 정확히 3문장으로 요약하세요"
|
||||
)
|
||||
|
||||
# 사용자 프롬프트
|
||||
user_prompt = f"""아래 두 회의록을 비교하여 유사한 내용을 정확히 3문장으로 요약해주세요.
|
||||
|
||||
## 현재 회의
|
||||
제목: {current_meeting_title}
|
||||
날짜: {current_meeting_date}
|
||||
안건:
|
||||
{current_meeting_agendas}
|
||||
|
||||
## 과거 회의
|
||||
제목: {past_meeting_title}
|
||||
날짜: {past_meeting_date}
|
||||
내용:
|
||||
{past_meeting_content}
|
||||
|
||||
## 요구사항
|
||||
1. 두 회의에서 공통적으로 논의된 주제나 결정사항을 찾아주세요
|
||||
2. 정확히 3문장으로 요약하세요 (각 문장은 한 문단)
|
||||
3. 구체적인 내용을 포함해주세요 (예: 날짜, 수치, 결정사항)
|
||||
4. 과거 회의에서 실제로 다뤄진 내용만 포함해주세요 (환각 금지)
|
||||
"""
|
||||
|
||||
try:
|
||||
response = self.client.messages.create(
|
||||
model=self.model,
|
||||
max_tokens=self.max_tokens,
|
||||
temperature=self.temperature,
|
||||
system=system_prompt,
|
||||
messages=[
|
||||
{"role": "user", "content": user_prompt}
|
||||
]
|
||||
)
|
||||
|
||||
summary = response.content[0].text
|
||||
tokens_used = response.usage.input_tokens + response.usage.output_tokens
|
||||
|
||||
return {
|
||||
"summary": summary,
|
||||
"generated_by": self.model,
|
||||
"tokens_used": tokens_used,
|
||||
"cached": False
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Claude API 호출 실패: {str(e)}")
|
||||
return {
|
||||
"summary": None,
|
||||
"generated_by": "fallback",
|
||||
"tokens_used": 0,
|
||||
"cached": False,
|
||||
"error": str(e)
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
"""
|
||||
Azure Event Hub Consumer 서비스
|
||||
회의록 확정 이벤트를 consume하여 RAG 저장소에 저장
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, Union, List
|
||||
from datetime import datetime
|
||||
|
||||
from azure.eventhub.aio import EventHubConsumerClient
|
||||
from azure.eventhub.extensions.checkpointstoreblobaio import BlobCheckpointStore
|
||||
|
||||
from ..models.minutes import RagMinutes, MinutesSection
|
||||
from ..db.rag_minutes_db import RagMinutesDB
|
||||
from ..utils.embedding import EmbeddingGenerator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventHubConsumer:
|
||||
"""Event Hub Consumer 서비스"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
connection_string: str,
|
||||
eventhub_name: str,
|
||||
consumer_group: str,
|
||||
storage_connection_string: str,
|
||||
storage_container_name: str,
|
||||
rag_minutes_db: RagMinutesDB,
|
||||
embedding_gen: EmbeddingGenerator
|
||||
):
|
||||
"""
|
||||
초기화
|
||||
|
||||
Args:
|
||||
connection_string: Event Hub 연결 문자열
|
||||
eventhub_name: Event Hub 이름
|
||||
consumer_group: Consumer Group 이름
|
||||
storage_connection_string: Azure Storage 연결 문자열
|
||||
storage_container_name: Checkpoint 저장 컨테이너 이름
|
||||
rag_minutes_db: RAG Minutes 데이터베이스
|
||||
embedding_gen: Embedding 생성기
|
||||
"""
|
||||
self.connection_string = connection_string
|
||||
self.eventhub_name = eventhub_name
|
||||
self.consumer_group = consumer_group
|
||||
self.storage_connection_string = storage_connection_string
|
||||
self.storage_container_name = storage_container_name
|
||||
self.rag_minutes_db = rag_minutes_db
|
||||
self.embedding_gen = embedding_gen
|
||||
self.client: Optional[EventHubConsumerClient] = None
|
||||
self.is_running = False
|
||||
|
||||
async def start(self):
|
||||
"""Consumer 시작"""
|
||||
try:
|
||||
# Checkpoint Store 생성
|
||||
checkpoint_store = BlobCheckpointStore.from_connection_string(
|
||||
self.storage_connection_string,
|
||||
self.storage_container_name
|
||||
)
|
||||
|
||||
# Event Hub Consumer Client 생성
|
||||
self.client = EventHubConsumerClient.from_connection_string(
|
||||
self.connection_string,
|
||||
consumer_group=self.consumer_group,
|
||||
eventhub_name=self.eventhub_name,
|
||||
checkpoint_store=checkpoint_store
|
||||
)
|
||||
|
||||
self.is_running = True
|
||||
logger.info("Event Hub Consumer 시작")
|
||||
|
||||
# 이벤트 수신 시작
|
||||
async with self.client:
|
||||
await self.client.receive(
|
||||
on_event=self._on_event,
|
||||
on_error=self._on_error,
|
||||
starting_position="-1" # 처음부터 읽기
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Event Hub Consumer 시작 실패: {str(e)}")
|
||||
self.is_running = False
|
||||
raise
|
||||
|
||||
async def stop(self):
|
||||
"""Consumer 중지"""
|
||||
self.is_running = False
|
||||
if self.client:
|
||||
await self.client.close()
|
||||
logger.info("Event Hub Consumer 중지")
|
||||
|
||||
async def _on_event(self, partition_context, event):
|
||||
"""
|
||||
이벤트 수신 핸들러
|
||||
|
||||
Args:
|
||||
partition_context: 파티션 컨텍스트
|
||||
event: Event Hub 이벤트
|
||||
"""
|
||||
try:
|
||||
# 이벤트 데이터 파싱
|
||||
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')}")
|
||||
|
||||
# 회의록 확정 이벤트 처리
|
||||
if event_data.get("eventType") == "MINUTES_FINALIZED":
|
||||
await self._process_minutes_event(event_data)
|
||||
|
||||
# Checkpoint 업데이트
|
||||
await partition_context.update_checkpoint(event)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"이벤트 파싱 실패: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"이벤트 처리 실패: {str(e)}")
|
||||
|
||||
async def _on_error(self, partition_context, error):
|
||||
"""
|
||||
에러 핸들러
|
||||
|
||||
Args:
|
||||
partition_context: 파티션 컨텍스트
|
||||
error: 에러 객체
|
||||
"""
|
||||
logger.error(f"Event Hub 에러 (Partition {partition_context.partition_id}): {str(error)}")
|
||||
|
||||
def _convert_datetime_array_to_string(self, value: Union[str, List, None]) -> Optional[str]:
|
||||
"""
|
||||
Java LocalDateTime 배열을 ISO 8601 문자열로 변환
|
||||
|
||||
Java의 Jackson이 LocalDateTime을 배열 형식으로 직렬화할 때 사용
|
||||
배열 형식: [년, 월, 일, 시, 분, 초, 나노초]
|
||||
|
||||
Args:
|
||||
value: datetime 값 (str, list, None)
|
||||
|
||||
Returns:
|
||||
ISO 8601 형식 문자열 또는 None
|
||||
|
||||
Examples:
|
||||
>>> _convert_datetime_array_to_string([2025, 11, 1, 13, 55, 54, 388000000])
|
||||
"2025-11-01T13:55:54.388000"
|
||||
|
||||
>>> _convert_datetime_array_to_string("2025-11-01T13:55:54.388")
|
||||
"2025-11-01T13:55:54.388"
|
||||
|
||||
>>> _convert_datetime_array_to_string(None)
|
||||
None
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# 이미 문자열이면 그대로 반환
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
|
||||
# 배열 형식 [년, 월, 일, 시, 분, 초, 나노초]
|
||||
if isinstance(value, list) and len(value) >= 6:
|
||||
try:
|
||||
year, month, day, hour, minute, second = value[:6]
|
||||
# 나노초를 마이크로초로 변환 (Python datetime은 마이크로초 사용)
|
||||
microsecond = value[6] // 1000 if len(value) > 6 else 0
|
||||
|
||||
dt = datetime(year, month, day, hour, minute, second, microsecond)
|
||||
return dt.isoformat()
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(f"날짜 배열 변환 실패: {value}, 에러: {str(e)}")
|
||||
return None
|
||||
|
||||
logger.warning(f"지원하지 않는 날짜 형식: {type(value)}, 값: {value}")
|
||||
return None
|
||||
|
||||
async def _process_minutes_event(self, event_data: Dict[str, Any]):
|
||||
"""
|
||||
회의록 확정 이벤트 처리
|
||||
|
||||
Args:
|
||||
event_data: 이벤트 데이터
|
||||
"""
|
||||
try:
|
||||
# 회의록 데이터 추출
|
||||
minutes_data = event_data.get("data", {})
|
||||
|
||||
# Meeting 정보
|
||||
meeting_id = minutes_data.get("meetingId")
|
||||
title = minutes_data.get("title")
|
||||
purpose = minutes_data.get("purpose")
|
||||
description = minutes_data.get("description")
|
||||
# Java LocalDateTime 배열을 문자열로 변환
|
||||
scheduled_at = self._convert_datetime_array_to_string(
|
||||
minutes_data.get("scheduledAt")
|
||||
)
|
||||
location = minutes_data.get("location")
|
||||
organizer_id = minutes_data.get("organizerId")
|
||||
|
||||
# Minutes 정보
|
||||
minutes_id = minutes_data.get("minutesId")
|
||||
minutes_status = minutes_data.get("status", "FINALIZED")
|
||||
minutes_version = minutes_data.get("version", 1)
|
||||
created_by = minutes_data.get("createdBy")
|
||||
finalized_by = minutes_data.get("finalizedBy")
|
||||
# Java LocalDateTime 배열을 문자열로 변환
|
||||
finalized_at = self._convert_datetime_array_to_string(
|
||||
minutes_data.get("finalizedAt")
|
||||
)
|
||||
|
||||
# Sections 정보
|
||||
sections_data = minutes_data.get("sections", [])
|
||||
sections = [
|
||||
MinutesSection(
|
||||
section_id=section.get("sectionId"),
|
||||
type=section.get("type"),
|
||||
title=section.get("title"),
|
||||
content=section.get("content", ""),
|
||||
order=section.get("order", 0),
|
||||
verified=section.get("verified", False)
|
||||
)
|
||||
for section in sections_data
|
||||
]
|
||||
|
||||
# 전체 회의록 내용 생성 (검색용)
|
||||
full_content = self._generate_full_content(title, purpose, sections)
|
||||
|
||||
logger.info(f"회의록 내용 생성 완료: {len(full_content)} 글자")
|
||||
|
||||
# Embedding 생성
|
||||
logger.info(f"Embedding 생성 시작: {minutes_id}")
|
||||
embedding = self.embedding_gen.generate_embedding(full_content)
|
||||
logger.info(f"Embedding 생성 완료: {len(embedding)} 차원")
|
||||
|
||||
# RagMinutes 객체 생성
|
||||
rag_minutes = RagMinutes(
|
||||
meeting_id=meeting_id,
|
||||
title=title,
|
||||
purpose=purpose,
|
||||
description=description,
|
||||
scheduled_at=scheduled_at,
|
||||
location=location,
|
||||
organizer_id=organizer_id,
|
||||
minutes_id=minutes_id,
|
||||
minutes_status=minutes_status,
|
||||
minutes_version=minutes_version,
|
||||
created_by=created_by,
|
||||
finalized_by=finalized_by,
|
||||
finalized_at=finalized_at,
|
||||
sections=sections,
|
||||
full_content=full_content,
|
||||
embedding=embedding,
|
||||
created_at=datetime.now().isoformat(),
|
||||
updated_at=datetime.now().isoformat()
|
||||
)
|
||||
|
||||
# 데이터베이스에 저장
|
||||
success = self.rag_minutes_db.insert_minutes(rag_minutes)
|
||||
|
||||
if success:
|
||||
logger.info(f"회의록 RAG 저장 성공: {minutes_id}")
|
||||
else:
|
||||
logger.error(f"회의록 RAG 저장 실패: {minutes_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"회의록 이벤트 처리 실패: {str(e)}")
|
||||
raise
|
||||
|
||||
def _generate_full_content(self, title: str, purpose: Optional[str], sections: list) -> str:
|
||||
"""
|
||||
전체 회의록 내용 생성 (검색용 텍스트)
|
||||
|
||||
Args:
|
||||
title: 회의 제목
|
||||
purpose: 회의 목적
|
||||
sections: 회의록 섹션 목록
|
||||
|
||||
Returns:
|
||||
전체 회의록 내용
|
||||
"""
|
||||
content_parts = []
|
||||
|
||||
# 제목
|
||||
if title:
|
||||
content_parts.append(f"제목: {title}")
|
||||
|
||||
# 목적
|
||||
if purpose:
|
||||
content_parts.append(f"목적: {purpose}")
|
||||
|
||||
# 섹션별 내용
|
||||
for section in sections:
|
||||
if section.content:
|
||||
content_parts.append(f"\n[{section.title}]\n{section.content}")
|
||||
|
||||
return "\n\n".join(content_parts)
|
||||
|
||||
|
||||
async def start_consumer(
|
||||
config: Dict[str, Any],
|
||||
rag_minutes_db: RagMinutesDB,
|
||||
embedding_gen: EmbeddingGenerator
|
||||
):
|
||||
"""
|
||||
Event Hub Consumer 시작 (비동기)
|
||||
|
||||
Args:
|
||||
config: 설정 딕셔너리
|
||||
rag_minutes_db: RAG Minutes 데이터베이스
|
||||
embedding_gen: Embedding 생성기
|
||||
"""
|
||||
eventhub_config = config["eventhub"]
|
||||
|
||||
consumer = EventHubConsumer(
|
||||
connection_string=eventhub_config["connection_string"],
|
||||
eventhub_name=eventhub_config["name"],
|
||||
consumer_group=eventhub_config["consumer_group"],
|
||||
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
|
||||
)
|
||||
|
||||
try:
|
||||
await consumer.start()
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Consumer 종료 신호 수신")
|
||||
await consumer.stop()
|
||||
except Exception as e:
|
||||
logger.error(f"Consumer 실행 중 에러: {str(e)}")
|
||||
await consumer.stop()
|
||||
raise
|
||||
Reference in New Issue
Block a user