diff --git a/vector/app/services/claude_service.py b/vector/app/services/claude_service.py index 38ec659..4d86004 100644 --- a/vector/app/services/claude_service.py +++ b/vector/app/services/claude_service.py @@ -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 + # ============================================================================= # 헬스체크 및 상태 확인 메서드들 # ============================================================================= diff --git a/vector/app/services/vector_service.py b/vector/app/services/vector_service.py index eb9ba99..9920c32 100644 --- a/vector/app/services/vector_service.py +++ b/vector/app/services/vector_service.py @@ -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 \ No newline at end of file