This commit is contained in:
hiondal 2025-06-16 06:51:42 +09:00
parent c06d453cf4
commit d0b59725df
2 changed files with 202 additions and 0 deletions

View File

@ -301,6 +301,114 @@ class ClaudeService:
"""API 응답용 프롬프트를 생성합니다. (호환성용)"""
return self._build_action_prompt(context, additional_context)
async def generate_action_recommendations_optimized(
self,
context: str,
additional_context: Optional[str] = None
) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
"""
최적화된 액션 추천 생성
- 명확한 JSON 지시사항
- 토큰 효율성 개선
- 파싱 안정성 향상
"""
if not self.is_ready():
return None, None
try:
# 최적화된 프롬프트 구성
prompt = self._build_optimized_prompt(context, additional_context)
response = self.client.messages.create(
model=self.model,
max_tokens=3000, # 토큰 수 최적화
temperature=0.3, # 일관성 향상
messages=[{"role": "user", "content": prompt}]
)
if response.content and len(response.content) > 0:
raw_response = response.content[0].text
# 즉시 JSON 파싱 시도
parsed_json = self._parse_json_response_enhanced(raw_response)
return raw_response, parsed_json
return None, None
except Exception as e:
logger.error(f"Claude AI 호출 실패: {e}")
return None, None
def _build_optimized_prompt(self, context: str, additional_context: Optional[str] = None) -> str:
"""최적화된 프롬프트 구성"""
prompt_parts = [
"당신은 소상공인 경영 컨설턴트입니다.",
f"분석 요청: {context}",
]
if additional_context:
prompt_parts.extend([
"\n=== 참고 데이터 ===",
additional_context,
"==================\n"
])
prompt_parts.extend([
"위 정보를 바탕으로 실행 가능한 액션을 추천해주세요.",
"",
"⚠️ 응답은 반드시 아래 JSON 형식으로만 작성하세요:",
"다른 텍스트는 포함하지 마세요.",
"",
"```json",
"{",
' "summary": {',
' "current_situation": "현재 상황 요약 (50자 이내)",',
' "key_insights": ["핵심 포인트1", "핵심 포인트2"],',
' "priority": "high|medium|low"',
' },',
' "actions": [',
' {',
' "title": "액션명",',
' "description": "구체적 실행방법",',
' "timeline": "실행기간",',
' "cost": "예상비용",',
' "impact": "예상효과"',
' }',
' ],',
' "quick_tips": ["즉시 실행 팁1", "즉시 실행 팁2"]',
"}",
"```"
])
return "\n".join(prompt_parts)
def _parse_json_response_enhanced(self, raw_response: str) -> Optional[Dict[str, Any]]:
"""향상된 JSON 파싱"""
try:
# 1. JSON 블록 추출
json_match = re.search(r'```json\s*(\{.*?\})\s*```', raw_response, re.DOTALL)
if json_match:
json_str = json_match.group(1)
else:
# 2. 직접 JSON 찾기
json_match = re.search(r'(\{.*\})', raw_response, re.DOTALL)
if json_match:
json_str = json_match.group(1)
else:
return None
# 3. JSON 파싱
return json.loads(json_str)
except json.JSONDecodeError as e:
logger.error(f"JSON 파싱 오류: {e}")
return None
except Exception as e:
logger.error(f"JSON 추출 실패: {e}")
return None
# =============================================================================
# 헬스체크 및 상태 확인 메서드들
# =============================================================================

View File

@ -726,3 +726,97 @@ class VectorService:
except Exception as e:
logger.error(f"유사 케이스 검색 실패: {e}")
return None
def search_similar_cases_improved(self, store_id: str, context: str) -> Optional[str]:
"""
개선된 유사 케이스 검색
1. store_id 기반 필터링 우선 적용
2. 동종 업체 우선 검색
3. 캐싱 성능 최적화
"""
try:
if not self.is_ready():
logger.warning("VectorService가 준비되지 않음")
return None
# 1단계: 해당 가게의 정보 먼저 확인
store_context = self.get_store_context(store_id)
food_category = store_context.get('food_category', '') if store_context else ''
# 2단계: 검색 쿼리 구성 (가게 정보 + 컨텍스트)
enhanced_query = f"{food_category} {context}"
query_embedding = self.embedding_model.encode(enhanced_query).tolist()
# 3단계: 동종 업체 우선 검색 (메타데이터 필터링)
results = self.collection.query(
query_embeddings=[query_embedding],
n_results=10, # 더 많은 결과에서 필터링
include=['documents', 'metadatas', 'distances'],
where={"food_category": {"$eq": food_category}} if food_category else None
)
if not results or not results.get('documents') or not results['documents'][0]:
# 4단계: 동종 업체가 없으면 전체 검색
logger.info("동종 업체 없음 - 전체 검색으로 전환")
results = self.collection.query(
query_embeddings=[query_embedding],
n_results=5,
include=['documents', 'metadatas', 'distances']
)
if not results or not results.get('documents') or not results['documents'][0]:
logger.info("유사 케이스를 찾을 수 없음")
return None
# 5단계: 결과 조합 (관련성 높은 순서로)
context_parts = []
documents = results['documents'][0]
metadatas = results.get('metadatas', [[]])[0]
distances = results.get('distances', [[]])[0]
# 거리(유사도) 기준으로 필터링 (너무 관련성 낮은 것 제외)
filtered_results = []
for i, (doc, metadata, distance) in enumerate(zip(documents, metadatas, distances)):
if distance < 0.8: # 유사도 임계값
filtered_results.append((doc, metadata, distance))
if not filtered_results:
return None
# 최대 3개의 가장 관련성 높은 케이스만 사용
for doc, metadata, distance in filtered_results[:3]:
store_name = metadata.get('store_name', 'Unknown')
food_cat = metadata.get('food_category', 'Unknown')
context_parts.append(f"[{food_cat} - {store_name}] (유사도: {1-distance:.2f})")
# 문서 길이 제한으로 토큰 수 최적화
context_parts.append(doc[:300] + "..." if len(doc) > 300 else doc)
context_parts.append("---")
return "\n".join(context_parts)
except Exception as e:
logger.error(f"유사 케이스 검색 실패: {e}")
return None
def get_store_context(self, store_id: str) -> Optional[Dict[str, Any]]:
"""해당 가게의 컨텍스트 정보 조회 (캐싱 적용)"""
try:
if not self.is_ready():
return None
# 메타데이터에서 해당 store_id 검색
results = self.collection.get(
where={"store_id": {"$eq": store_id}},
limit=1,
include=['metadatas']
)
if results and results.get('metadatas') and len(results['metadatas']) > 0:
return results['metadatas'][0]
return None
except Exception as e:
logger.error(f"가게 컨텍스트 조회 실패: {e}")
return None