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
+84
View File
@@ -0,0 +1,84 @@
"""
RAG Minutes 테이블 초기화 스크립트
"""
import psycopg2
import sys
from pathlib import Path
# 프로젝트 루트를 Python 경로에 추가
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.utils.config import load_config, get_database_url
def init_rag_minutes_table(db_url: str, migration_file: str):
"""
RAG Minutes 테이블 초기화
Args:
db_url: 데이터베이스 연결 URL
migration_file: 마이그레이션 SQL 파일 경로
"""
try:
print(f"데이터베이스 연결 중...")
conn = psycopg2.connect(db_url)
cur = conn.cursor()
print(f"마이그레이션 파일 읽기: {migration_file}")
with open(migration_file, 'r', encoding='utf-8') as f:
sql = f.read()
print("마이그레이션 실행 중...")
cur.execute(sql)
conn.commit()
print("✓ RAG Minutes 테이블 초기화 완료")
# 테이블 확인
cur.execute("""
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_name = 'rag_minutes'
""")
count = cur.fetchone()[0]
if count > 0:
print(f"✓ rag_minutes 테이블이 생성되었습니다")
# 인덱스 확인
cur.execute("""
SELECT indexname
FROM pg_indexes
WHERE tablename = 'rag_minutes'
""")
indexes = cur.fetchall()
print(f"✓ 생성된 인덱스: {len(indexes)}")
for idx in indexes:
print(f" - {idx[0]}")
else:
print("✗ 테이블 생성 실패")
cur.close()
conn.close()
except Exception as e:
print(f"✗ 에러 발생: {str(e)}")
raise
if __name__ == "__main__":
# 설정 로드
config_path = Path(__file__).parent.parent / "config.yaml"
config = load_config(str(config_path))
db_url = get_database_url(config)
db_url = "postgresql://hgzerouser:Hi5Jessica!@4.217.133.186:5432/ragdb"
# 마이그레이션 파일 경로
migration_file = Path(__file__).parent.parent / "migrations" / "V1__create_rag_minutes_table.sql"
if not migration_file.exists():
print(f"✗ 마이그레이션 파일을 찾을 수 없습니다: {migration_file}")
sys.exit(1)
# 초기화 실행
init_rag_minutes_table(db_url, str(migration_file))
+246
View File
@@ -0,0 +1,246 @@
"""
관련자료 데이터 로딩 스크립트
"""
import sys
import json
import logging
from pathlib import Path
from typing import List
from datetime import datetime
# 프로젝트 루트 디렉토리를 Python 경로에 추가
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from src.models.document import Document, DocumentChunk, DocumentMetadata
from src.db.azure_search import AzureAISearchDB
from src.utils.config import load_config
from src.utils.embedding import EmbeddingGenerator
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def load_documents_from_json(file_path: Path) -> List[Document]:
"""
JSON 파일에서 문서 데이터 로드
Args:
file_path: JSON 파일 경로
Returns:
문서 리스트
"""
logger.info(f"JSON 파일 로딩: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
documents = []
# 업무 도메인별 데이터 처리
for domain, doc_types in data.get("sample_data", {}).items():
for doc_type, docs in doc_types.items():
for doc_data in docs:
# Metadata 파싱
metadata = None
if "metadata" in doc_data:
metadata = DocumentMetadata(**doc_data["metadata"])
# Document 객체 생성
doc = Document(
document_id=doc_data["document_id"],
document_type=doc_data["document_type"],
business_domain=doc_data.get("business_domain"),
title=doc_data["title"],
content=doc_data["content"],
summary=doc_data["summary"],
keywords=doc_data.get("keywords", []),
created_date=doc_data.get("created_date"),
participants=doc_data.get("participants", []),
metadata=metadata,
embedding=None # 나중에 생성
)
documents.append(doc)
logger.info(f"{len(documents)}개 문서 로드 완료")
return documents
def create_chunks(
document: Document,
embedding_gen: EmbeddingGenerator,
max_tokens: int = 2000
) -> List[DocumentChunk]:
"""
문서를 청크로 분할 및 임베딩 생성
Args:
document: 문서
embedding_gen: 임베딩 생성기
max_tokens: 최대 토큰 수
Returns:
문서 청크 리스트
"""
chunks = []
# 전체 문서를 하나의 청크로 처리 (간단한 구현)
# 실제로는 안건 단위로 분할해야 함
content = document.content
token_count = embedding_gen.get_token_count(content)
if token_count > max_tokens:
# 간단한 분할: 문단 단위
paragraphs = content.split("\n\n")
current_chunk = ""
chunk_index = 0
for para in paragraphs:
test_chunk = current_chunk + "\n\n" + para if current_chunk else para
if embedding_gen.get_token_count(test_chunk) > max_tokens:
# 현재 청크 저장
if current_chunk:
chunks.append({
"content": current_chunk,
"chunk_index": chunk_index
})
chunk_index += 1
current_chunk = para
else:
current_chunk = test_chunk
# 마지막 청크 저장
if current_chunk:
chunks.append({
"content": current_chunk,
"chunk_index": chunk_index
})
else:
# 토큰 수가 적으면 하나의 청크로
chunks.append({
"content": content,
"chunk_index": 0
})
# 임베딩 생성
chunk_texts = [chunk["content"] for chunk in chunks]
embeddings = embedding_gen.generate_embeddings_batch(chunk_texts)
# DocumentChunk 객체 생성
document_chunks = []
for chunk_data, embedding in zip(chunks, embeddings):
chunk = DocumentChunk(
id=f"{document.document_id}_chunk_{chunk_data['chunk_index']}",
document_id=document.document_id,
document_type=document.document_type,
title=document.title,
folder=document.metadata.folder if document.metadata else None,
created_date=document.created_date,
participants=document.participants,
keywords=document.keywords,
agenda_id=None, # 간단한 구현에서는 None
agenda_title=None,
chunk_index=chunk_data["chunk_index"],
content=chunk_data["content"],
content_vector=embedding,
token_count=embedding_gen.get_token_count(chunk_data["content"])
)
document_chunks.append(chunk)
return document_chunks
def main():
"""메인 함수"""
logger.info("=" * 60)
logger.info("관련자료 데이터 로딩 시작")
logger.info("=" * 60)
# 1. 설정 로드
config = load_config(str(project_root / "config.yaml"))
logger.info("✓ 설정 로드 완료")
# 2. Azure AI Search 연결
azure_search = config["azure_search"]
search_db = AzureAISearchDB(
endpoint=azure_search["endpoint"],
api_key=azure_search["api_key"],
index_name=azure_search["index_name"],
api_version=azure_search["api_version"]
)
logger.info("✓ Azure AI Search 연결 완료")
# 3. 인덱스 생성
search_db.create_index()
logger.info("✓ 인덱스 생성 완료")
# 4. 임베딩 생성기 초기화
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"]
)
logger.info("✓ 임베딩 생성기 초기화 완료")
# 5. 문서 데이터 로딩
data_file = project_root.parent / config["data"]["documents_file"]
if not data_file.exists():
logger.error(f"❌ 파일 없음: {data_file}")
sys.exit(1)
documents = load_documents_from_json(data_file)
logger.info(f"✓ 총 {len(documents)}개 문서 로드 완료")
# 6. 청킹 및 임베딩 생성
logger.info("청킹 및 임베딩 생성 시작")
all_chunks = []
for i, doc in enumerate(documents, 1):
logger.info(f" 처리 중: {i}/{len(documents)} - {doc.title}")
chunks = create_chunks(doc, embedding_gen)
all_chunks.extend(chunks)
logger.info(f"✓ 총 {len(all_chunks)}개 청크 생성 완료")
# 7. Azure AI Search에 업로드
logger.info("Azure AI Search 업로드 시작")
success = search_db.upload_documents(all_chunks)
if success:
logger.info(f"{len(all_chunks)}개 청크 업로드 완료")
else:
logger.error("❌ 업로드 실패")
sys.exit(1)
# 8. 통계 조회
stats = search_db.get_stats()
logger.info("=" * 60)
logger.info("관련자료 통계")
logger.info("=" * 60)
logger.info(f"전체 문서: {stats['total_documents']}")
logger.info(f"전체 청크: {stats['total_chunks']}")
logger.info("\n문서 타입별 통계:")
for doc_type, count in sorted(stats['by_type'].items(), key=lambda x: x[1], reverse=True):
logger.info(f" - {doc_type}: {count}")
logger.info("=" * 60)
logger.info("관련자료 데이터 로딩 완료")
logger.info("=" * 60)
if __name__ == "__main__":
try:
main()
except Exception as e:
logger.error(f"오류 발생: {str(e)}", exc_info=True)
sys.exit(1)
+196
View File
@@ -0,0 +1,196 @@
"""
용어집 데이터 로딩 스크립트
"""
import sys
import json
import logging
from pathlib import Path
from typing import List
# 프로젝트 루트 디렉토리를 Python 경로에 추가
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from src.models.term import Term, DocumentSource
from src.db.postgres_vector import PostgresVectorDB
from src.utils.config import load_config, get_database_url
from src.utils.embedding import EmbeddingGenerator
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def load_terms_from_json(file_path: Path) -> List[Term]:
"""
JSON 파일에서 용어 데이터 로드
Args:
file_path: JSON 파일 경로
Returns:
용어 리스트
"""
logger.info(f"JSON 파일 로딩: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
terms = []
# 도메인별 데이터 처리
for domain_data in data.get("terms", []):
domain = domain_data.get("domain")
source_type = domain_data.get("source_type")
for term_data in domain_data.get("data", []):
# DocumentSource 파싱
doc_source = None
if "document_source" in term_data:
doc_source = DocumentSource(**term_data["document_source"])
# Term 객체 생성
term = Term(
term_id=term_data["term_id"],
term_name=term_data["term_name"],
normalized_name=term_data["normalized_name"],
category=term_data["category"],
definition=term_data["definition"],
context=term_data.get("context", ""),
synonyms=term_data.get("synonyms", []),
related_terms=term_data.get("related_terms", []),
document_source=doc_source,
confidence_score=term_data.get("confidence_score", 0.0),
usage_count=term_data.get("usage_count", 0),
last_updated=term_data.get("last_updated"),
embedding=None # 나중에 생성
)
terms.append(term)
logger.info(f"{len(terms)}개 용어 로드 완료")
return terms
def generate_embeddings(
terms: List[Term],
embedding_gen: EmbeddingGenerator
) -> List[Term]:
"""
용어 임베딩 생성
Args:
terms: 용어 리스트
embedding_gen: 임베딩 생성기
Returns:
임베딩이 추가된 용어 리스트
"""
logger.info(f"임베딩 생성 시작: {len(terms)}개 용어")
# 임베딩 텍스트 준비 (용어명 + 정의 + 맥락)
texts = []
for term in terms:
text = f"{term.term_name}\n{term.definition}"
if term.context:
text += f"\n{term.context}"
texts.append(text)
# 배치 임베딩 생성
embeddings = embedding_gen.generate_embeddings_batch(texts, batch_size=50)
# 임베딩 추가
for term, embedding in zip(terms, embeddings):
term.embedding = embedding
logger.info(f" → 임베딩 생성 완료")
return terms
def main():
"""메인 함수"""
logger.info("=" * 60)
logger.info("용어집 데이터 로딩 시작")
logger.info("=" * 60)
# 1. 설정 로드
config = load_config(str(project_root / "config.yaml"))
logger.info("✓ 설정 로드 완료")
# 2. PostgreSQL 연결
db_url = get_database_url(config)
db = PostgresVectorDB(db_url)
logger.info("✓ PostgreSQL 연결 완료")
# 3. 데이터베이스 초기화
db.init_database()
logger.info("✓ 데이터베이스 초기화 완료")
# 4. 임베딩 생성기 초기화
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"]
)
logger.info("✓ 임베딩 생성기 초기화 완료")
# 5. 용어 데이터 로딩
all_terms = []
data_dir = project_root.parent / config["data"]["terms_dir"]
for filename in config["data"]["terms_files"]:
file_path = data_dir / filename
if file_path.exists():
terms = load_terms_from_json(file_path)
all_terms.extend(terms)
else:
logger.warning(f"⚠ 파일 없음: {file_path}")
logger.info(f"✓ 총 {len(all_terms)}개 용어 로드 완료")
# 6. 임베딩 생성
all_terms = generate_embeddings(all_terms, embedding_gen)
# 7. 데이터베이스에 삽입
logger.info(f"데이터베이스 삽입 시작: {len(all_terms)}개 용어")
success_count = 0
fail_count = 0
for i, term in enumerate(all_terms, 1):
if db.insert_term(term):
success_count += 1
else:
fail_count += 1
if i % 100 == 0:
logger.info(f" 진행: {i}/{len(all_terms)} (성공: {success_count}, 실패: {fail_count})")
logger.info(f"✓ 삽입 완료: 성공 {success_count}, 실패 {fail_count}")
# 8. 통계 조회
stats = db.get_stats()
logger.info("=" * 60)
logger.info("용어집 통계")
logger.info("=" * 60)
logger.info(f"전체 용어: {stats['total_terms']}")
logger.info(f"평균 신뢰도: {stats['avg_confidence']:.2f}")
logger.info("\n카테고리별 통계:")
for category, count in sorted(stats['by_category'].items(), key=lambda x: x[1], reverse=True):
logger.info(f" - {category}: {count}")
logger.info("=" * 60)
logger.info("용어집 데이터 로딩 완료")
logger.info("=" * 60)
if __name__ == "__main__":
try:
main()
except Exception as e:
logger.error(f"오류 발생: {str(e)}", exc_info=True)
sys.exit(1)
+245
View File
@@ -0,0 +1,245 @@
"""
Vector DB 통합 시스템 설정 검증 스크립트
사용법: python scripts/validate_setup.py
"""
import sys
import os
from pathlib import Path
from typing import List, Tuple
# 프로젝트 루트 디렉토리
project_root = Path(__file__).parent.parent
def check_file_exists(file_path: Path, description: str) -> bool:
"""파일 존재 여부 확인"""
exists = file_path.exists()
status = "" if exists else ""
print(f" {status} {description}: {file_path.name}")
return exists
def check_directory_exists(dir_path: Path, description: str) -> bool:
"""디렉토리 존재 여부 확인"""
exists = dir_path.exists() and dir_path.is_dir()
status = "" if exists else ""
print(f" {status} {description}: {dir_path.name}/")
return exists
def check_python_version() -> bool:
"""Python 버전 확인"""
version = sys.version_info
is_valid = version.major == 3 and version.minor >= 9
status = "" if is_valid else ""
print(f" {status} Python 버전: {version.major}.{version.minor}.{version.micro}")
if not is_valid:
print(f" → Python 3.9 이상이 필요합니다")
return is_valid
def check_dependencies() -> bool:
"""필수 패키지 설치 확인"""
required_packages = [
"fastapi",
"uvicorn",
"psycopg2",
"openai",
"anthropic",
"azure.search.documents",
"pydantic",
"pyyaml",
"tenacity"
]
missing_packages = []
for package in required_packages:
try:
__import__(package.replace("-", "_").split(".")[0])
print(f"{package}")
except ImportError:
print(f"{package}")
missing_packages.append(package)
if missing_packages:
print(f"\n → 누락된 패키지를 설치하세요: pip install {' '.join(missing_packages)}")
return False
return True
def check_env_variables() -> Tuple[bool, List[str]]:
"""환경 변수 설정 확인"""
required_vars = [
"POSTGRES_HOST",
"POSTGRES_PORT",
"POSTGRES_DATABASE",
"POSTGRES_USER",
"POSTGRES_PASSWORD",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_ENDPOINT",
"AZURE_SEARCH_ENDPOINT",
"AZURE_SEARCH_API_KEY",
"CLAUDE_API_KEY"
]
# .env 파일 확인
env_file = project_root / ".env"
if env_file.exists():
print(f" ✓ .env 파일 존재")
# .env 파일 로드 시뮬레이션
with open(env_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if '=' in line and not line.startswith('#'):
key, value = line.split('=', 1)
if value and value != f"your_{key.lower()}_here":
os.environ[key] = value
else:
print(f" ✗ .env 파일 없음")
print(f" → .env.example을 .env로 복사하고 실제 값으로 수정하세요")
missing_vars = []
for var in required_vars:
value = os.environ.get(var, "")
has_value = bool(value) and not value.startswith("your_")
if has_value:
# API 키는 앞 4자리만 표시
if "KEY" in var or "PASSWORD" in var:
display_value = value[:4] + "..." if len(value) > 4 else "***"
else:
display_value = value
print(f"{var}: {display_value}")
else:
print(f"{var}: 설정 필요")
missing_vars.append(var)
return len(missing_vars) == 0, missing_vars
def check_data_files() -> bool:
"""샘플 데이터 파일 확인"""
data_dir = project_root.parent / "design/aidata"
meet_ref_file = project_root.parent / "design/meet-ref.json"
all_exists = True
# 용어 데이터 파일
term_files = ["terms-01.json", "terms-02.json", "terms-03.json", "terms-04.json"]
for filename in term_files:
file_path = data_dir / filename
exists = file_path.exists()
status = "" if exists else ""
print(f" {status} {filename}")
all_exists = all_exists and exists
# 관련 문서 데이터 파일
exists = meet_ref_file.exists()
status = "" if exists else ""
print(f" {status} meet-ref.json")
all_exists = all_exists and exists
if not all_exists:
print(f"\n → 데이터 파일이 design/ 디렉토리에 있는지 확인하세요")
return all_exists
def main():
"""메인 검증 함수"""
print("\n" + "=" * 70)
print("Vector DB 통합 시스템 설정 검증")
print("=" * 70 + "\n")
results = {}
# 1. Python 버전 확인
print("1. Python 버전 확인")
results["python"] = check_python_version()
print()
# 2. 프로젝트 구조 확인
print("2. 프로젝트 구조 확인")
structure_checks = [
(project_root / "config.yaml", "설정 파일"),
(project_root / "requirements.txt", "의존성 파일"),
(project_root / "README.md", "문서"),
(project_root / "src", "소스 디렉토리"),
(project_root / "src/models", "모델 디렉토리"),
(project_root / "src/db", "DB 디렉토리"),
(project_root / "src/services", "서비스 디렉토리"),
(project_root / "src/api", "API 디렉토리"),
(project_root / "scripts", "스크립트 디렉토리"),
(project_root / "tests", "테스트 디렉토리")
]
structure_ok = True
for path, desc in structure_checks:
if path.is_dir():
structure_ok = check_directory_exists(path, desc) and structure_ok
else:
structure_ok = check_file_exists(path, desc) and structure_ok
results["structure"] = structure_ok
print()
# 3. 의존성 패키지 확인
print("3. 의존성 패키지 확인")
results["dependencies"] = check_dependencies()
print()
# 4. 환경 변수 확인
print("4. 환경 변수 확인")
env_ok, missing_vars = check_env_variables()
results["environment"] = env_ok
print()
# 5. 데이터 파일 확인
print("5. 샘플 데이터 파일 확인")
results["data_files"] = check_data_files()
print()
# 결과 요약
print("=" * 70)
print("검증 결과 요약")
print("=" * 70)
all_passed = all(results.values())
for category, passed in results.items():
status = "✓ 통과" if passed else "✗ 실패"
print(f" {status}: {category}")
print()
if all_passed:
print("🎉 모든 검증을 통과했습니다!")
print()
print("다음 단계:")
print(" 1. 데이터베이스 초기화: python scripts/load_terms.py")
print(" 2. 관련자료 로딩: python scripts/load_documents.py")
print(" 3. API 서버 실행: python -m src.api.main")
print(" 4. API 문서 확인: http://localhost:8000/docs")
else:
print("⚠️ 일부 검증에 실패했습니다.")
print()
print("실패한 항목을 확인하고 수정한 후 다시 실행하세요.")
if not results["dependencies"]:
print("\n의존성 설치 명령:")
print(" pip install -r requirements.txt")
if not results["environment"]:
print("\n환경 변수 설정 방법:")
print(" 1. .env.example을 .env로 복사")
print(" 2. .env 파일을 열어 실제 값으로 수정")
print()
print("=" * 70)
return 0 if all_passed else 1
if __name__ == "__main__":
sys.exit(main())