674 lines
27 KiB
Python
674 lines
27 KiB
Python
# vector/app/main.py
|
||
import os
|
||
import sys
|
||
|
||
# =============================================================================
|
||
# .env 파일 로딩 (다른 import보다 먼저)
|
||
# =============================================================================
|
||
def is_kubernetes_env():
|
||
"""Kubernetes 환경 감지"""
|
||
return (
|
||
os.path.exists('/var/run/secrets/kubernetes.io/serviceaccount') or
|
||
os.getenv('KUBERNETES_SERVICE_HOST') or
|
||
os.getenv('ENVIRONMENT') == 'production'
|
||
)
|
||
|
||
# 조건부 dotenv 로딩
|
||
if not is_kubernetes_env():
|
||
try:
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
from dotenv import load_dotenv
|
||
load_dotenv()
|
||
print("✅ 로컬 개발환경: .env 파일 로딩")
|
||
except ImportError:
|
||
print("⚠️ python-dotenv 없음, 환경변수만 사용")
|
||
else:
|
||
print("ℹ️ Kubernetes/Production: ConfigMap/Secret 사용")
|
||
|
||
import logging
|
||
from contextlib import asynccontextmanager
|
||
from datetime import datetime
|
||
from typing import Optional
|
||
import asyncio
|
||
from fastapi import FastAPI, HTTPException, Depends
|
||
from fastapi.responses import HTMLResponse, JSONResponse
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from pydantic import BaseModel, Field
|
||
|
||
# 프로젝트 모듈 import
|
||
from app.config.settings import settings
|
||
from app.models.restaurant_models import RestaurantInfo
|
||
from app.models.vector_models import (
|
||
VectorBuildRequest, VectorBuildResponse,
|
||
ActionRecommendationRequest, ActionRecommendationResponse,
|
||
ActionRecommendationSimpleResponse,
|
||
VectorDBStatusResponse, VectorDBStatus,
|
||
FindReviewsResponse, StoredDataInfo
|
||
)
|
||
|
||
from app.services.restaurant_service import RestaurantService
|
||
from app.services.review_service import ReviewService
|
||
from app.services.vector_service import VectorService
|
||
from app.services.claude_service import ClaudeService
|
||
from app.utils.category_utils import extract_food_category
|
||
|
||
# 로깅 설정
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||
handlers=[logging.StreamHandler(sys.stdout)]
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 🔧 전역 변수 대신 애플리케이션 상태로 관리
|
||
app_state = {
|
||
"vector_service": None,
|
||
"restaurant_service": None,
|
||
"review_service": None,
|
||
"claude_service": None,
|
||
"initialization_errors": {},
|
||
"startup_completed": False
|
||
}
|
||
|
||
# 추가 모델 정의 (find-reviews API용)
|
||
class FindReviewsRequest(BaseModel):
|
||
"""리뷰 검색 요청 모델"""
|
||
region: str = Field(
|
||
...,
|
||
description="지역 (시군구 + 읍면동)",
|
||
example="서울특별시 강남구 역삼동"
|
||
)
|
||
store_name: str = Field(
|
||
...,
|
||
description="가게명",
|
||
example="맛있는 한식당"
|
||
)
|
||
|
||
class RestaurantInfo(BaseModel):
|
||
"""음식점 정보 모델"""
|
||
id: str = Field(description="카카오 장소 ID")
|
||
place_name: str = Field(description="장소명")
|
||
category_name: str = Field(description="카테고리명")
|
||
address_name: str = Field(description="전체 주소")
|
||
phone: str = Field(description="전화번호")
|
||
place_url: str = Field(description="장소 상세페이지 URL")
|
||
x: str = Field(description="X 좌표값 (경도)")
|
||
y: str = Field(description="Y 좌표값 (위도)")
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
"""🔧 애플리케이션 생명주기 관리 - 안전한 서비스 초기화"""
|
||
# 🚀 Startup 이벤트
|
||
logger.info("🚀 Vector API 서비스 시작 중...")
|
||
startup_start_time = datetime.now()
|
||
|
||
# 각 서비스 안전하게 초기화
|
||
services_to_init = [
|
||
("restaurant_service", RestaurantService, "Restaurant API 서비스"),
|
||
("review_service", ReviewService, "Review API 서비스"),
|
||
("claude_service", ClaudeService, "Claude AI 서비스"),
|
||
("vector_service", VectorService, "Vector DB 서비스") # 마지막에 초기화
|
||
]
|
||
|
||
initialized_count = 0
|
||
|
||
for service_key, service_class, service_name in services_to_init:
|
||
try:
|
||
logger.info(f"🔧 {service_name} 초기화 중...")
|
||
app_state[service_key] = service_class()
|
||
logger.info(f"✅ {service_name} 초기화 완료")
|
||
initialized_count += 1
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ {service_name} 초기화 실패: {e}")
|
||
app_state["initialization_errors"][service_key] = str(e)
|
||
# 🔧 중요: 서비스 초기화 실패해도 앱은 시작 (헬스체크에서 확인)
|
||
continue
|
||
|
||
startup_time = (datetime.now() - startup_start_time).total_seconds()
|
||
app_state["startup_completed"] = True
|
||
|
||
logger.info(f"✅ Vector API 서비스 시작 완료!")
|
||
logger.info(f"📊 초기화 결과: {initialized_count}/{len(services_to_init)}개 서비스 성공")
|
||
logger.info(f"⏱️ 시작 소요시간: {startup_time:.2f}초")
|
||
|
||
if app_state["initialization_errors"]:
|
||
logger.warning(f"⚠️ 초기화 실패 서비스: {list(app_state['initialization_errors'].keys())}")
|
||
|
||
yield
|
||
|
||
# 🛑 Shutdown 이벤트
|
||
logger.info("🛑 Vector API 서비스 종료 중...")
|
||
|
||
# 리소스 정리
|
||
for service_key in ["vector_service", "restaurant_service", "review_service", "claude_service"]:
|
||
if app_state[service_key] is not None:
|
||
try:
|
||
# 서비스별 정리 작업이 있다면 여기서 수행
|
||
logger.info(f"🔧 {service_key} 정리 중...")
|
||
except Exception as e:
|
||
logger.warning(f"⚠️ {service_key} 정리 실패: {e}")
|
||
finally:
|
||
app_state[service_key] = None
|
||
|
||
app_state["startup_completed"] = False
|
||
logger.info("✅ Vector API 서비스 종료 완료")
|
||
|
||
# 🔧 FastAPI 앱 초기화 (lifespan 이벤트 포함)
|
||
app = FastAPI(
|
||
title=settings.APP_TITLE,
|
||
description=f"""
|
||
{settings.APP_DESCRIPTION}
|
||
|
||
**주요 기능:**
|
||
- 지역과 가게명으로 대상 가게 찾기
|
||
- 동종 업체 리뷰 수집 및 분석
|
||
- Vector DB 구축 및 관리
|
||
- Claude AI 기반 액션 추천
|
||
- 영속적 Vector DB 저장
|
||
|
||
**API 연동:**
|
||
- Restaurant API: {settings.get_restaurant_api_url() if hasattr(settings, 'get_restaurant_api_url') else 'N/A'}
|
||
- Review API: {settings.get_review_api_url() if hasattr(settings, 'get_review_api_url') else 'N/A'}
|
||
- Claude AI API: {settings.CLAUDE_MODEL}
|
||
|
||
**Vector DB:**
|
||
- 경로: {settings.VECTOR_DB_PATH}
|
||
- 컬렉션: {settings.VECTOR_DB_COLLECTION}
|
||
- 임베딩 모델: {settings.EMBEDDING_MODEL}
|
||
|
||
**버전:** {settings.APP_VERSION}
|
||
""",
|
||
version=settings.APP_VERSION,
|
||
contact={
|
||
"name": "개발팀",
|
||
"email": "admin@example.com"
|
||
},
|
||
lifespan=lifespan # 🔧 lifespan 이벤트 등록
|
||
)
|
||
|
||
# CORS 설정
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
# 🔧 Dependency Injection - 서비스 제공자들
|
||
def get_vector_service() -> VectorService:
|
||
"""VectorService 의존성 주입"""
|
||
if app_state["vector_service"] is None:
|
||
error_msg = app_state["initialization_errors"].get("vector_service")
|
||
if error_msg:
|
||
raise HTTPException(
|
||
status_code=503,
|
||
detail=f"Vector service not available: {error_msg}"
|
||
)
|
||
|
||
# 런타임에 재시도
|
||
try:
|
||
logger.info("🔧 VectorService 런타임 초기화 시도...")
|
||
app_state["vector_service"] = VectorService()
|
||
logger.info("✅ VectorService 런타임 초기화 성공")
|
||
except Exception as e:
|
||
logger.error(f"❌ VectorService 런타임 초기화 실패: {e}")
|
||
raise HTTPException(
|
||
status_code=503,
|
||
detail=f"Vector service initialization failed: {str(e)}"
|
||
)
|
||
|
||
return app_state["vector_service"]
|
||
|
||
def get_restaurant_service() -> RestaurantService:
|
||
"""RestaurantService 의존성 주입"""
|
||
if app_state["restaurant_service"] is None:
|
||
error_msg = app_state["initialization_errors"].get("restaurant_service")
|
||
if error_msg:
|
||
raise HTTPException(
|
||
status_code=503,
|
||
detail=f"Restaurant service not available: {error_msg}"
|
||
)
|
||
|
||
try:
|
||
app_state["restaurant_service"] = RestaurantService()
|
||
except Exception as e:
|
||
raise HTTPException(status_code=503, detail=str(e))
|
||
|
||
return app_state["restaurant_service"]
|
||
|
||
def get_review_service() -> ReviewService:
|
||
"""ReviewService 의존성 주입"""
|
||
if app_state["review_service"] is None:
|
||
error_msg = app_state["initialization_errors"].get("review_service")
|
||
if error_msg:
|
||
raise HTTPException(
|
||
status_code=503,
|
||
detail=f"Review service not available: {error_msg}"
|
||
)
|
||
|
||
try:
|
||
app_state["review_service"] = ReviewService()
|
||
except Exception as e:
|
||
raise HTTPException(status_code=503, detail=str(e))
|
||
|
||
return app_state["review_service"]
|
||
|
||
def get_claude_service() -> ClaudeService:
|
||
"""ClaudeService 의존성 주입"""
|
||
if app_state["claude_service"] is None:
|
||
error_msg = app_state["initialization_errors"].get("claude_service")
|
||
if error_msg:
|
||
raise HTTPException(
|
||
status_code=503,
|
||
detail=f"Claude service not available: {error_msg}"
|
||
)
|
||
|
||
try:
|
||
app_state["claude_service"] = ClaudeService()
|
||
except Exception as e:
|
||
raise HTTPException(status_code=503, detail=str(e))
|
||
|
||
return app_state["claude_service"]
|
||
|
||
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||
async def root():
|
||
"""메인 페이지"""
|
||
# 🔧 안전한 DB 상태 조회
|
||
try:
|
||
vector_service = app_state.get("vector_service")
|
||
if vector_service:
|
||
db_status = vector_service.get_db_status()
|
||
else:
|
||
db_status = {
|
||
'collection_name': settings.VECTOR_DB_COLLECTION,
|
||
'total_documents': 0,
|
||
'total_stores': 0,
|
||
'db_path': settings.VECTOR_DB_PATH,
|
||
'status': 'not_initialized'
|
||
}
|
||
except Exception as e:
|
||
logger.warning(f"DB 상태 조회 실패: {e}")
|
||
db_status = {'status': 'error', 'error': str(e)}
|
||
|
||
return f"""
|
||
<html>
|
||
<head>
|
||
<title>{settings.APP_TITLE}</title>
|
||
<style>
|
||
body {{ font-family: Arial, sans-serif; margin: 40px; background-color: #f5f5f5; }}
|
||
.container {{ max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
|
||
h1 {{ color: #333; text-align: center; }}
|
||
.status {{ background: #e7f3ff; padding: 15px; border-radius: 5px; margin: 20px 0; }}
|
||
.info {{ background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0; }}
|
||
.link {{ display: inline-block; margin: 10px; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px; }}
|
||
.link:hover {{ background: #0056b3; }}
|
||
pre {{ background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto; }}
|
||
ul {{ list-style-type: none; padding: 0; }}
|
||
li {{ margin: 5px 0; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>🍽️ {settings.APP_TITLE}</h1>
|
||
<p>{settings.APP_DESCRIPTION}</p>
|
||
|
||
<div class="status">
|
||
<h2>📊 Vector DB 상태</h2>
|
||
<ul>
|
||
<li><strong>컬렉션:</strong> {db_status.get('collection_name', 'N/A')}</li>
|
||
<li><strong>총 문서:</strong> {db_status.get('total_documents', 0)}</li>
|
||
<li><strong>총 가게:</strong> {db_status.get('total_stores', 0)}</li>
|
||
<li><strong>상태:</strong> {db_status.get('status', 'unknown')}</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="info">
|
||
<h2>🔧 시스템 구성</h2>
|
||
<ul>
|
||
<li><strong>Claude Model:</strong> {settings.CLAUDE_MODEL}</li>
|
||
<li><strong>Embedding Model:</strong> {settings.EMBEDDING_MODEL}</li>
|
||
<li><strong>Vector DB Path:</strong> {settings.VECTOR_DB_PATH}</li>
|
||
<li><strong>환경:</strong> {'Kubernetes' if hasattr(settings, 'IS_K8S_ENV') and settings.IS_K8S_ENV else 'Local'}</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>📚 API 문서</h2>
|
||
<a href="/docs" class="link">Swagger UI 문서</a>
|
||
<a href="/redoc" class="link">ReDoc 문서</a>
|
||
<a href="/health" class="link">헬스 체크</a>
|
||
<a href="/vector-status" class="link">Vector DB 상태</a>
|
||
|
||
<h2>🛠️ 사용 방법</h2>
|
||
<p><strong>POST /find-reviews</strong> - 리뷰 검색 및 Vector DB 저장 (본인 가게 우선)</p>
|
||
<pre>
|
||
{{
|
||
"region": "서울특별시 강남구 역삼동",
|
||
"store_name": "맛있는 한식당"
|
||
}}
|
||
</pre>
|
||
|
||
<p><strong>POST /action-recommendation-simple</strong> - 간소화된 액션 추천 요청</p>
|
||
<pre>
|
||
{{
|
||
"store_id": "12345",
|
||
"context": "매출이 감소하고 있어서 개선이 필요합니다"
|
||
}}
|
||
</pre>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
@app.post("/find-reviews", response_model=FindReviewsResponse)
|
||
async def find_reviews(
|
||
request: VectorBuildRequest, # VectorBuildRequest 재사용
|
||
vector_service: VectorService = Depends(get_vector_service),
|
||
restaurant_service: RestaurantService = Depends(get_restaurant_service),
|
||
review_service: ReviewService = Depends(get_review_service)
|
||
):
|
||
"""
|
||
지역과 가게명으로 리뷰를 찾아 Vector DB에 저장하고,
|
||
실제 저장된 JSON 구조를 응답으로 리턴합니다.
|
||
🔥 본인 가게 리뷰는 반드시 포함됩니다.
|
||
|
||
Note: force_rebuild 파라미터는 이 API에서는 무시됩니다.
|
||
"""
|
||
start_time = datetime.now()
|
||
logger.info(f"🔍 리뷰 검색 요청: {request.region} - {request.store_name}")
|
||
|
||
# force_rebuild 파라미터는 find-reviews에서는 무시
|
||
# (VectorBuildRequest를 재사용하지만 이 기능은 사용하지 않음)
|
||
|
||
try:
|
||
# 1단계: 본인 가게 검색
|
||
logger.info("1단계: 본인 가게 검색 중...")
|
||
target_restaurant = await restaurant_service.find_store_by_name_and_region(
|
||
request.region, request.store_name
|
||
)
|
||
|
||
if not target_restaurant:
|
||
logger.error(f"❌ 본인 가게를 찾을 수 없음: {request.store_name}")
|
||
return FindReviewsResponse(
|
||
success=False,
|
||
message=f"처리 중 오류가 발생했습니다: {str(e)}",
|
||
target_store=None,
|
||
food_category=None,
|
||
total_reviews=0,
|
||
total_stores=0,
|
||
execution_time=execution_time,
|
||
stored_data=None
|
||
)
|
||
|
||
logger.info(f"✅ 본인 가게 찾기 성공: {target_restaurant.place_name}")
|
||
|
||
# 2단계: 음식 카테고리 추출
|
||
food_category = extract_food_category(target_restaurant.category_name)
|
||
logger.info(f"🍽️ 추출된 음식 카테고리: {food_category}")
|
||
|
||
# 3단계: 본인 가게 리뷰 수집 (우선순위 1)
|
||
logger.info("3단계: 본인 가게 리뷰 수집 중...")
|
||
target_store_info, target_reviews = await review_service.collect_store_reviews(
|
||
target_restaurant.id, settings.MAX_REVIEWS_PER_RESTAURANT
|
||
)
|
||
|
||
if not target_reviews:
|
||
logger.warning("⚠️ 본인 가게 리뷰가 없습니다")
|
||
|
||
# 4단계: 동종 업체 검색
|
||
logger.info("4단계: 동종 업체 검색 중...")
|
||
similar_stores = await restaurant_service.find_similar_stores(
|
||
request.region,
|
||
food_category,
|
||
max_count=settings.MAX_RESTAURANTS_PER_CATEGORY # 50 → 환경변수
|
||
)
|
||
|
||
# 5단계: 동종 업체 리뷰 수집
|
||
logger.info("5단계: 동종 업체 리뷰 수집 중...")
|
||
review_results = []
|
||
|
||
# 본인 가게를 첫 번째로 추가
|
||
if target_store_info and target_reviews:
|
||
review_results.append((target_restaurant.id, target_store_info, target_reviews))
|
||
|
||
# 동종 업체 리뷰 수집 (본인 가게 제외)
|
||
similar_store_names = []
|
||
max_similar_reviews = min(settings.MAX_REVIEWS_PER_RESTAURANT // 2, 20) # 절반 또는 최대 20개
|
||
for store in similar_stores[:settings.MAX_RESTAURANTS_PER_CATEGORY]: # 환경변수 활용
|
||
store_info, reviews = await review_service.collect_store_reviews(
|
||
store.id,
|
||
max_reviews=max_similar_reviews # 동종업체는 더 적게
|
||
)
|
||
if store_info and reviews:
|
||
review_results.append((store.id, store_info, reviews))
|
||
similar_store_names.append(store.place_name)
|
||
|
||
# 6단계: Vector DB 저장 및 저장 데이터 수집
|
||
logger.info("6단계: Vector DB 저장 중...")
|
||
try:
|
||
target_store_info_dict = {
|
||
'id': target_restaurant.id,
|
||
'place_name': target_restaurant.place_name,
|
||
'category_name': target_restaurant.category_name,
|
||
'address_name': target_restaurant.address_name,
|
||
'phone': target_restaurant.phone,
|
||
'place_url': target_restaurant.place_url,
|
||
'x': target_restaurant.x,
|
||
'y': target_restaurant.y
|
||
}
|
||
|
||
# Vector DB에 저장하기 전에 저장될 데이터 구조 생성
|
||
stored_data = {}
|
||
|
||
# combine_store_and_reviews 함수를 사용하여 실제 저장될 JSON 구조 생성
|
||
from app.utils.data_utils import combine_store_and_reviews
|
||
import json
|
||
|
||
for store_id, store_info, reviews in review_results:
|
||
# Vector DB에 저장되는 실제 JSON 구조 생성
|
||
json_data = combine_store_and_reviews(store_info, reviews)
|
||
parsed_data = json.loads(json_data)
|
||
|
||
store_key = store_info.get('name', store_id)
|
||
stored_data[store_key] = StoredDataInfo(
|
||
store_info=parsed_data['store_info'],
|
||
reviews=parsed_data['reviews'],
|
||
review_summary=parsed_data['review_summary'],
|
||
combined_at=parsed_data['combined_at']
|
||
)
|
||
|
||
# Vector DB에 저장
|
||
vector_result = await vector_service.build_vector_store(
|
||
target_store_info_dict, review_results, food_category, request.region
|
||
)
|
||
|
||
if not vector_result.get('success', False):
|
||
raise Exception(f"Vector DB 저장 실패: {vector_result.get('error', 'Unknown error')}")
|
||
|
||
logger.info("✅ Vector DB 구축 완료")
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ Vector DB 구축 실패: {e}")
|
||
return FindReviewsResponse(
|
||
success=False,
|
||
message=f"Vector DB 구축 중 오류가 발생했습니다: {str(e)}",
|
||
target_store={
|
||
'id': target_restaurant.id,
|
||
'place_name': target_restaurant.place_name,
|
||
'category_name': target_restaurant.category_name,
|
||
'address_name': target_restaurant.address_name,
|
||
'phone': target_restaurant.phone,
|
||
'place_url': target_restaurant.place_url,
|
||
'x': target_restaurant.x,
|
||
'y': target_restaurant.y
|
||
},
|
||
food_category=food_category
|
||
)
|
||
|
||
# 성공 응답 - Vector DB 저장 데이터 포함
|
||
total_reviews = sum(len(reviews) for _, _, reviews in review_results)
|
||
execution_time = (datetime.now() - start_time).total_seconds()
|
||
|
||
return FindReviewsResponse(
|
||
success=True,
|
||
message=f"✅ 본인 가게 '{target_restaurant.place_name}' 리뷰 분석 완료! "
|
||
f"총 {total_reviews}개 리뷰, {len(review_results)}개 업체 분석됨",
|
||
target_store={
|
||
'id': target_restaurant.id,
|
||
'place_name': target_restaurant.place_name,
|
||
'category_name': target_restaurant.category_name,
|
||
'address_name': target_restaurant.address_name,
|
||
'phone': target_restaurant.phone,
|
||
'place_url': target_restaurant.place_url,
|
||
'x': target_restaurant.x,
|
||
'y': target_restaurant.y
|
||
},
|
||
food_category=food_category,
|
||
total_reviews=total_reviews,
|
||
total_stores=len(review_results),
|
||
execution_time=execution_time,
|
||
stored_data=stored_data
|
||
)
|
||
|
||
except Exception as e:
|
||
execution_time = (datetime.now() - start_time).total_seconds()
|
||
logger.error(f"❌ find_reviews 처리 실패: {str(e)}")
|
||
|
||
return FindReviewsResponse(
|
||
success=False,
|
||
message=f"처리 중 오류가 발생했습니다: {str(e)}",
|
||
execution_time=execution_time
|
||
)
|
||
|
||
@app.post(
|
||
"/action-recommendation-simple",
|
||
response_model=ActionRecommendationSimpleResponse,
|
||
summary="간소화된 액션 추천 요청",
|
||
description="JSON 추천 결과만 반환하는 최적화된 엔드포인트 + INPUT 데이터 포함"
|
||
)
|
||
async def action_recommendation_simple(
|
||
request: ActionRecommendationRequest,
|
||
claude_service: ClaudeService = Depends(get_claude_service),
|
||
vector_service: VectorService = Depends(get_vector_service)
|
||
):
|
||
"""🧠 최적화된 Claude AI 액션 추천 API - JSON만 반환 + INPUT 데이터 포함"""
|
||
try:
|
||
logger.info(f"간소화된 액션 추천 요청: store_id={request.store_id}")
|
||
|
||
# 1단계: Vector DB에서 최적화된 컨텍스트 조회 (개선된 버전)
|
||
context_data = None
|
||
structured_input = None
|
||
try:
|
||
# 개선된 검색 메소드 사용 - 이제 Dict 반환
|
||
context_data_dict = vector_service.search_similar_cases_improved(
|
||
request.store_id,
|
||
request.context
|
||
)
|
||
|
||
if context_data_dict:
|
||
# Claude에게 전달할 텍스트 형태로 변환
|
||
context_data = json.dumps(context_data_dict, ensure_ascii=False, indent=2)
|
||
# API 응답에 포함할 구조화된 INPUT 데이터
|
||
structured_input = context_data_dict
|
||
|
||
except Exception as e:
|
||
logger.warning(f"Vector DB 조회 실패 (계속 진행): {e}")
|
||
|
||
# 2단계: Claude AI 호출 - JSON만 추출
|
||
try:
|
||
# 컨텍스트 구성
|
||
full_context = f"가게 ID: {request.store_id}\n점주 요청: {request.context}"
|
||
|
||
# Claude AI 호출
|
||
claude_response, parsed_json = await claude_service.generate_action_recommendations(
|
||
context=full_context,
|
||
additional_context=context_data
|
||
)
|
||
|
||
if not parsed_json:
|
||
# JSON 파싱 실패시 재시도
|
||
logger.warning("JSON 파싱 실패 - 재시도")
|
||
parsed_json = claude_service.parse_recommendation_response(claude_response)
|
||
|
||
if not parsed_json:
|
||
raise Exception("Claude AI 응답을 JSON으로 파싱할 수 없습니다")
|
||
|
||
return ActionRecommendationSimpleResponse(
|
||
success=True,
|
||
recommendation=parsed_json,
|
||
input_data=structured_input # 새로 추가: Claude에게 전달된 INPUT 데이터
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Claude AI 호출 실패: {e}")
|
||
return ActionRecommendationSimpleResponse(
|
||
success=False,
|
||
error_message=f"AI 추천 생성 중 오류: {str(e)}",
|
||
input_data=structured_input # 실패해도 INPUT 데이터는 포함
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"액션 추천 처리 실패: {e}")
|
||
return ActionRecommendationSimpleResponse(
|
||
success=False,
|
||
error_message=f"서버 내부 오류: {str(e)}",
|
||
input_data=None
|
||
)
|
||
|
||
@app.get(
|
||
"/vector-status",
|
||
response_model=VectorDBStatusResponse,
|
||
summary="Vector DB 상태 조회",
|
||
description="Vector DB의 현재 상태를 조회합니다."
|
||
)
|
||
async def get_vector_status(vector_service: VectorService = Depends(get_vector_service)):
|
||
"""Vector DB 상태를 조회합니다."""
|
||
try:
|
||
db_status = vector_service.get_db_status()
|
||
|
||
status = VectorDBStatus(
|
||
collection_name=db_status['collection_name'],
|
||
total_documents=db_status['total_documents'],
|
||
total_stores=db_status['total_stores'],
|
||
db_path=db_status['db_path'],
|
||
last_updated=db_status.get('last_updated')
|
||
)
|
||
|
||
return VectorDBStatusResponse(
|
||
success=True,
|
||
status=status,
|
||
message="Vector DB 상태 조회 성공"
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Vector DB 상태 조회 실패: {e}")
|
||
return VectorDBStatusResponse(
|
||
success=False,
|
||
status=VectorDBStatus(
|
||
collection_name="unknown",
|
||
total_documents=0,
|
||
total_stores=0,
|
||
db_path="unknown"
|
||
),
|
||
message=f"상태 조회 실패: {str(e)}"
|
||
)
|
||
|
||
@app.get("/health")
|
||
async def health_check():
|
||
"""헬스체크 엔드포인트"""
|
||
return {
|
||
"status": "healthy",
|
||
"timestamp": datetime.now().isoformat(),
|
||
"services": {
|
||
"vector_service": app_state["vector_service"] is not None,
|
||
"restaurant_service": app_state["restaurant_service"] is not None,
|
||
"review_service": app_state["review_service"] is not None,
|
||
"claude_service": app_state["claude_service"] is not None,
|
||
},
|
||
"initialization_errors": app_state["initialization_errors"]
|
||
}
|
||
|
||
if __name__ == "__main__":
|
||
import uvicorn
|
||
uvicorn.run(app, host=settings.HOST, port=settings.PORT) |