release
This commit is contained in:
parent
c06d453cf4
commit
d0b59725df
@ -301,6 +301,114 @@ class ClaudeService:
|
|||||||
"""API 응답용 프롬프트를 생성합니다. (호환성용)"""
|
"""API 응답용 프롬프트를 생성합니다. (호환성용)"""
|
||||||
return self._build_action_prompt(context, additional_context)
|
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
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 헬스체크 및 상태 확인 메서드들
|
# 헬스체크 및 상태 확인 메서드들
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@ -726,3 +726,97 @@ class VectorService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"유사 케이스 검색 실패: {e}")
|
logger.error(f"유사 케이스 검색 실패: {e}")
|
||||||
return None
|
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
|
||||||
Loading…
x
Reference in New Issue
Block a user