Spaces:
Runtime error
Runtime error
youdie006 commited on
Commit ·
add744f
1
Parent(s): b2664e5
fix: debug
Browse files- src/api/chat.py +2 -2
- src/services/aihub_processor.py +12 -15
- src/services/openai_client.py +33 -21
src/api/chat.py
CHANGED
|
@@ -78,12 +78,12 @@ async def run_pipeline(session_id: str, message: str) -> dict:
|
|
| 78 |
"B_rule_based_adaptation": pre_adapted, "C_final_gpt4_prompt": final_prompt,
|
| 79 |
"D_final_response": final_response}
|
| 80 |
else:
|
| 81 |
-
#
|
| 82 |
inspirational_docs = [doc.get("system_response", "") for doc in expert_responses]
|
| 83 |
final_response, final_prompt = await openai_client.create_direct_response(
|
| 84 |
user_message=message,
|
| 85 |
conversation_history=conversation_history,
|
| 86 |
-
inspirational_docs=inspirational_docs
|
| 87 |
)
|
| 88 |
debug_info["step6_generation"] = {"strategy": strategy, "A_final_gpt4_prompt": final_prompt,
|
| 89 |
"B_final_response": final_response}
|
|
|
|
| 78 |
"B_rule_based_adaptation": pre_adapted, "C_final_gpt4_prompt": final_prompt,
|
| 79 |
"D_final_response": final_response}
|
| 80 |
else:
|
| 81 |
+
# RAG-Fusion 적용: 실패한 RAG 결과를 '영감'으로 제공
|
| 82 |
inspirational_docs = [doc.get("system_response", "") for doc in expert_responses]
|
| 83 |
final_response, final_prompt = await openai_client.create_direct_response(
|
| 84 |
user_message=message,
|
| 85 |
conversation_history=conversation_history,
|
| 86 |
+
inspirational_docs=inspirational_docs
|
| 87 |
)
|
| 88 |
debug_info["step6_generation"] = {"strategy": strategy, "A_final_gpt4_prompt": final_prompt,
|
| 89 |
"B_final_response": final_response}
|
src/services/aihub_processor.py
CHANGED
|
@@ -3,6 +3,8 @@ AI Hub 공감형 대화 데이터 처리기
|
|
| 3 |
"""
|
| 4 |
from typing import Dict, List, Optional
|
| 5 |
from loguru import logger
|
|
|
|
|
|
|
| 6 |
|
| 7 |
class TeenEmpathyDataProcessor:
|
| 8 |
def __init__(self, vector_store):
|
|
@@ -10,27 +12,23 @@ class TeenEmpathyDataProcessor:
|
|
| 10 |
logger.info("TeenEmpathyDataProcessor 초기화 완료. Vector Store가 주입되었습니다.")
|
| 11 |
|
| 12 |
async def search_similar_contexts(self, query: str, emotion: Optional[str] = None,
|
| 13 |
-
|
| 14 |
-
"""
|
| 15 |
-
[수정됨] 원본 쿼리와 메타데이터 필터를 사용하여 유사한 대화 맥락을 정확하게 검색합니다.
|
| 16 |
-
"""
|
| 17 |
try:
|
| 18 |
-
# 1. 메타데이터 필터 구성 (ChromaDB의 올바른 $and 문법 사용)
|
| 19 |
conditions = []
|
| 20 |
if emotion: conditions.append({"emotion": {"$eq": emotion}})
|
| 21 |
if relationship: conditions.append({"relationship": {"$eq": relationship}})
|
| 22 |
|
| 23 |
search_filter = None
|
| 24 |
-
if len(conditions) > 1:
|
| 25 |
-
|
|
|
|
|
|
|
| 26 |
|
| 27 |
logger.info(f"🔍 벡터 검색 시작 - Query: '{query}', Filter: {search_filter}")
|
| 28 |
|
| 29 |
-
# 2. 원본 쿼리로 벡터 검색 실행
|
| 30 |
results = await self.vector_store.search(
|
| 31 |
-
query=query,
|
| 32 |
-
top_k=top_k,
|
| 33 |
-
filter_metadata=search_filter
|
| 34 |
)
|
| 35 |
|
| 36 |
formatted_results = [{
|
|
@@ -38,11 +36,9 @@ class TeenEmpathyDataProcessor:
|
|
| 38 |
"system_response": r.metadata.get("system_response", ""),
|
| 39 |
"emotion": r.metadata.get("emotion", ""),
|
| 40 |
"relationship": r.metadata.get("relationship", ""),
|
| 41 |
-
"empathy_label": r.metadata.get("empathy_label", ""),
|
| 42 |
"similarity_score": r.score
|
| 43 |
} for r in results]
|
| 44 |
|
| 45 |
-
formatted_results.sort(key=lambda x: x["similarity_score"], reverse=True)
|
| 46 |
logger.info(f"✅ 검색 완료: {len(formatted_results)}개 결과")
|
| 47 |
return formatted_results
|
| 48 |
|
|
@@ -50,12 +46,13 @@ class TeenEmpathyDataProcessor:
|
|
| 50 |
logger.error(f"❌ 유사 사례 검색 실패: {e}")
|
| 51 |
return []
|
| 52 |
|
| 53 |
-
|
| 54 |
_processor_instance = None
|
|
|
|
|
|
|
| 55 |
async def get_teen_empathy_processor() -> TeenEmpathyDataProcessor:
|
| 56 |
global _processor_instance
|
| 57 |
if _processor_instance is None:
|
| 58 |
-
from ..core.vector_store import get_vector_store
|
| 59 |
vector_store = await get_vector_store()
|
| 60 |
_processor_instance = TeenEmpathyDataProcessor(vector_store=vector_store)
|
| 61 |
return _processor_instance
|
|
|
|
| 3 |
"""
|
| 4 |
from typing import Dict, List, Optional
|
| 5 |
from loguru import logger
|
| 6 |
+
from ..core.vector_store import get_vector_store
|
| 7 |
+
|
| 8 |
|
| 9 |
class TeenEmpathyDataProcessor:
|
| 10 |
def __init__(self, vector_store):
|
|
|
|
| 12 |
logger.info("TeenEmpathyDataProcessor 초기화 완료. Vector Store가 주입되었습니다.")
|
| 13 |
|
| 14 |
async def search_similar_contexts(self, query: str, emotion: Optional[str] = None,
|
| 15 |
+
relationship: Optional[str] = None, top_k: int = 5) -> List[Dict]:
|
| 16 |
+
"""원본 쿼리와 메타데이터 필터를 사용하여 유사한 대화 맥락을 정확하게 검색합니다."""
|
|
|
|
|
|
|
| 17 |
try:
|
|
|
|
| 18 |
conditions = []
|
| 19 |
if emotion: conditions.append({"emotion": {"$eq": emotion}})
|
| 20 |
if relationship: conditions.append({"relationship": {"$eq": relationship}})
|
| 21 |
|
| 22 |
search_filter = None
|
| 23 |
+
if len(conditions) > 1:
|
| 24 |
+
search_filter = {"$and": conditions}
|
| 25 |
+
elif len(conditions) == 1:
|
| 26 |
+
search_filter = conditions[0]
|
| 27 |
|
| 28 |
logger.info(f"🔍 벡터 검색 시작 - Query: '{query}', Filter: {search_filter}")
|
| 29 |
|
|
|
|
| 30 |
results = await self.vector_store.search(
|
| 31 |
+
query=query, top_k=top_k, filter_metadata=search_filter
|
|
|
|
|
|
|
| 32 |
)
|
| 33 |
|
| 34 |
formatted_results = [{
|
|
|
|
| 36 |
"system_response": r.metadata.get("system_response", ""),
|
| 37 |
"emotion": r.metadata.get("emotion", ""),
|
| 38 |
"relationship": r.metadata.get("relationship", ""),
|
|
|
|
| 39 |
"similarity_score": r.score
|
| 40 |
} for r in results]
|
| 41 |
|
|
|
|
| 42 |
logger.info(f"✅ 검색 완료: {len(formatted_results)}개 결과")
|
| 43 |
return formatted_results
|
| 44 |
|
|
|
|
| 46 |
logger.error(f"❌ 유사 사례 검색 실패: {e}")
|
| 47 |
return []
|
| 48 |
|
| 49 |
+
|
| 50 |
_processor_instance = None
|
| 51 |
+
|
| 52 |
+
|
| 53 |
async def get_teen_empathy_processor() -> TeenEmpathyDataProcessor:
|
| 54 |
global _processor_instance
|
| 55 |
if _processor_instance is None:
|
|
|
|
| 56 |
vector_store = await get_vector_store()
|
| 57 |
_processor_instance = TeenEmpathyDataProcessor(vector_store=vector_store)
|
| 58 |
return _processor_instance
|
src/services/openai_client.py
CHANGED
|
@@ -7,6 +7,7 @@ from openai import AsyncOpenAI
|
|
| 7 |
from loguru import logger
|
| 8 |
from ..models.function_models import EmotionType, RelationshipType
|
| 9 |
|
|
|
|
| 10 |
class OpenAIClient:
|
| 11 |
def __init__(self):
|
| 12 |
self.client = None
|
|
@@ -21,7 +22,8 @@ class OpenAIClient:
|
|
| 21 |
- **공감 우선:** 조언보다는 먼저 사용자의 감정을 알아주고 공감하는 말을 해줘. (예: "정말 속상했겠다.", "네 마음 충분히 이해돼.")
|
| 22 |
- **영어 절대 금지:** 답변은 반드시 한글로만 생성해야 해.
|
| 23 |
"""
|
| 24 |
-
self.conversion_map = {
|
|
|
|
| 25 |
|
| 26 |
async def initialize(self):
|
| 27 |
if not self.api_key or "your_" in self.api_key.lower(): raise ValueError("올바른 OpenAI API 키를 설정해주세요")
|
|
@@ -30,8 +32,11 @@ class OpenAIClient:
|
|
| 30 |
logger.info("✅ OpenAI 클라이언트 초기화 완료")
|
| 31 |
|
| 32 |
async def _test_connection(self):
|
| 33 |
-
try:
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
async def create_completion(self, messages: List[Dict[str, str]], **kwargs) -> str:
|
| 37 |
if not self.client: await self.initialize()
|
|
@@ -42,7 +47,9 @@ class OpenAIClient:
|
|
| 42 |
return response.choices[0].message.content
|
| 43 |
|
| 44 |
async def rewrite_query_with_history(self, user_message: str, conversation_history: List[Dict]) -> str:
|
| 45 |
-
|
|
|
|
|
|
|
| 46 |
history_str = "\n".join([f"[{msg['role']}] {msg['content']}" for msg in conversation_history])
|
| 47 |
prompt = f"""당신은 사용자의 대화 전체를 깊이 이해하여, 벡터 검색에 가장 적합한 검색 문장을 생성하는 '쿼리 재작성 전문가'입니다.
|
| 48 |
### 임무
|
|
@@ -50,7 +57,8 @@ class OpenAIClient:
|
|
| 50 |
### 규칙
|
| 51 |
1. 반드시 사용자의 입장에서, 사용자가 겪는 문제 상황을 중심으로 서술해야 합니다.
|
| 52 |
2. 단순 키워드 나열은 절대 금지됩니다.
|
| 53 |
-
3.
|
|
|
|
| 54 |
---
|
| 55 |
### 모범 답안 예시
|
| 56 |
[이전 대화 내용]
|
|
@@ -68,7 +76,8 @@ class OpenAIClient:
|
|
| 68 |
"{user_message}"
|
| 69 |
[재작성된 검색 쿼리]
|
| 70 |
"""
|
| 71 |
-
rewritten_query = await self.create_completion(messages=[{"role": "user", "content": prompt}], temperature=0.0,
|
|
|
|
| 72 |
logger.info(f"대화형 쿼리 재작성: '{user_message}' -> '{rewritten_query.strip()}'")
|
| 73 |
return rewritten_query.strip()
|
| 74 |
|
|
@@ -77,8 +86,9 @@ class OpenAIClient:
|
|
| 77 |
relationship_list = [r.value for r in RelationshipType]
|
| 78 |
analysis_prompt = f"다음 청소년의 메시지에서 primary_emotion과 relationship_context를 추출해줘. 반드시 아래 목록의 한글 단어 중에서만 선택해서 JSON으로 응답해야 해.\n- primary_emotion: {emotion_list}\n- relationship_context: {relationship_list}\n\n메시지: \"{text}\""
|
| 79 |
try:
|
| 80 |
-
response_content = await self.create_completion(messages=[{"role": "user", "content": analysis_prompt}],
|
| 81 |
-
|
|
|
|
| 82 |
return json.loads(response_content.strip())
|
| 83 |
except Exception:
|
| 84 |
return {"primary_emotion": EmotionType.ANXIETY.value, "relationship_context": RelationshipType.FRIEND.value}
|
|
@@ -89,30 +99,29 @@ class OpenAIClient:
|
|
| 89 |
|
| 90 |
async def verify_rag_relevance(self, user_message: str, retrieved_doc: str) -> bool:
|
| 91 |
prompt = f"사용자의 현재 메시지와 ���색된 전문가 조언이 의미적으로 관련이 있는지 판단해줘. 반드시 'Yes' 또는 'No'로만 대답해.\n- 사용자 메시지: \"{user_message}\"\n- 검색된 조언: \"{retrieved_doc}\"\n\n관련이 있는가? (Yes/No):"
|
| 92 |
-
response = await self.create_completion(messages=[{"role": "user", "content": prompt}], temperature=0.0,
|
|
|
|
| 93 |
logger.info(f"RAG 검증 결과: {response.strip()}")
|
| 94 |
return "yes" in response.strip().lower()
|
| 95 |
|
| 96 |
-
async def adapt_expert_response(self, expert_response: str, user_situation: str,
|
|
|
|
| 97 |
pre_adapted_response = self._apply_simple_conversions(expert_response)
|
| 98 |
-
messages = [{"role": "system", "content": self.teen_empathy_system_prompt}, *conversation_history,
|
|
|
|
|
|
|
| 99 |
final_prompt_for_debug = "\n".join([f"[{msg['role']}] {msg['content']}" for msg in messages])
|
| 100 |
final_response = await self.create_completion(messages=messages, temperature=0.5, max_tokens=400)
|
| 101 |
return expert_response, pre_adapted_response, final_response, final_prompt_for_debug
|
| 102 |
|
| 103 |
-
async def create_direct_response(self, user_message: str, conversation_history: List[Dict],
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
messages = [
|
| 107 |
-
{"role": "system", "content": self.teen_empathy_system_prompt},
|
| 108 |
-
*conversation_history
|
| 109 |
-
]
|
| 110 |
|
| 111 |
inspiration_prompt = ""
|
| 112 |
if inspirational_docs:
|
| 113 |
inspiration_prompt = "\n\n### 참고 자료 (직접 언급하지 말고, 답변을 만들 때 영감을 얻는 용도로만 사용해)\n"
|
| 114 |
-
for doc in inspirational_docs:
|
| 115 |
-
inspiration_prompt += f"- {doc}\n"
|
| 116 |
|
| 117 |
final_user_prompt = f"""'마음이'의 페르소나(친한 친구, 반말)를 완벽하게 지키면서 다음 메시지에 공감하는 답변을 해줘.{inspiration_prompt}
|
| 118 |
|
|
@@ -120,11 +129,14 @@ class OpenAIClient:
|
|
| 120 |
"""
|
| 121 |
messages.append({"role": "user", "content": final_user_prompt})
|
| 122 |
|
| 123 |
-
final_response = await self.create_completion(messages=messages, temperature=0.7, max_tokens=300)
|
| 124 |
prompt_for_debug = "\n".join([f"[{msg['role']}] {msg['content']}" for msg in messages])
|
|
|
|
| 125 |
return final_response, prompt_for_debug
|
| 126 |
|
|
|
|
| 127 |
_openai_client_instance = None
|
|
|
|
|
|
|
| 128 |
async def get_openai_client() -> OpenAIClient:
|
| 129 |
global _openai_client_instance
|
| 130 |
if _openai_client_instance is None:
|
|
|
|
| 7 |
from loguru import logger
|
| 8 |
from ..models.function_models import EmotionType, RelationshipType
|
| 9 |
|
| 10 |
+
|
| 11 |
class OpenAIClient:
|
| 12 |
def __init__(self):
|
| 13 |
self.client = None
|
|
|
|
| 22 |
- **공감 우선:** 조언보다는 먼저 사용자의 감정을 알아주고 공감하는 말을 해줘. (예: "정말 속상했겠다.", "네 마음 충분히 이해돼.")
|
| 23 |
- **영어 절대 금지:** 답변은 반드시 한글로만 생성해야 해.
|
| 24 |
"""
|
| 25 |
+
self.conversion_map = {"자기야": "너", "당신": "너", "직장": "학교", "회사": "학교", "업무": "공부", "동료": "친구", "상사": "선생님",
|
| 26 |
+
"하세요": "해", "어떠세요": "어때", "해보세요": "해봐", "~ㅂ니다": "~야", "~습니다": "~어"}
|
| 27 |
|
| 28 |
async def initialize(self):
|
| 29 |
if not self.api_key or "your_" in self.api_key.lower(): raise ValueError("올바른 OpenAI API 키를 설정해주세요")
|
|
|
|
| 32 |
logger.info("✅ OpenAI 클라이언트 초기화 완료")
|
| 33 |
|
| 34 |
async def _test_connection(self):
|
| 35 |
+
try:
|
| 36 |
+
await self.client.chat.completions.create(model=self.default_model,
|
| 37 |
+
messages=[{"role": "user", "content": "Hello"}], max_tokens=5)
|
| 38 |
+
except Exception as e:
|
| 39 |
+
raise e
|
| 40 |
|
| 41 |
async def create_completion(self, messages: List[Dict[str, str]], **kwargs) -> str:
|
| 42 |
if not self.client: await self.initialize()
|
|
|
|
| 47 |
return response.choices[0].message.content
|
| 48 |
|
| 49 |
async def rewrite_query_with_history(self, user_message: str, conversation_history: List[Dict]) -> str:
|
| 50 |
+
"""One-shot 예제가 포함된, 대화 맥락 기반 쿼리 재작성 함수"""
|
| 51 |
+
if not conversation_history:
|
| 52 |
+
return user_message
|
| 53 |
history_str = "\n".join([f"[{msg['role']}] {msg['content']}" for msg in conversation_history])
|
| 54 |
prompt = f"""당신은 사용자의 대화 전체를 깊이 이해하여, 벡터 검색에 가장 적합한 검색 문장을 생성하는 '쿼리 재작성 전문가'입니다.
|
| 55 |
### 임무
|
|
|
|
| 57 |
### 규칙
|
| 58 |
1. 반드시 사용자의 입장에서, 사용자가 겪는 문제 상황을 중심으로 서술해야 합니다.
|
| 59 |
2. 단순 키워드 나열은 절대 금지됩니다.
|
| 60 |
+
3. 재작성된 문장은 그 자체로 완전한 의미를 가져야 합니다.
|
| 61 |
+
4. 오직 '재작성된 검색 쿼리:' 부분의 내용만 결과로 출력해야 합니다.
|
| 62 |
---
|
| 63 |
### 모범 답안 예시
|
| 64 |
[이전 대화 내용]
|
|
|
|
| 76 |
"{user_message}"
|
| 77 |
[재작성된 검색 쿼리]
|
| 78 |
"""
|
| 79 |
+
rewritten_query = await self.create_completion(messages=[{"role": "user", "content": prompt}], temperature=0.0,
|
| 80 |
+
max_tokens=200)
|
| 81 |
logger.info(f"대화형 쿼리 재작성: '{user_message}' -> '{rewritten_query.strip()}'")
|
| 82 |
return rewritten_query.strip()
|
| 83 |
|
|
|
|
| 86 |
relationship_list = [r.value for r in RelationshipType]
|
| 87 |
analysis_prompt = f"다음 청소년의 메시지에서 primary_emotion과 relationship_context를 추출해줘. 반드시 아래 목록의 한글 단어 중에서만 선택해서 JSON으로 응답해야 해.\n- primary_emotion: {emotion_list}\n- relationship_context: {relationship_list}\n\n메시지: \"{text}\""
|
| 88 |
try:
|
| 89 |
+
response_content = await self.create_completion(messages=[{"role": "user", "content": analysis_prompt}],
|
| 90 |
+
temperature=0.0, max_tokens=200)
|
| 91 |
+
import json;
|
| 92 |
return json.loads(response_content.strip())
|
| 93 |
except Exception:
|
| 94 |
return {"primary_emotion": EmotionType.ANXIETY.value, "relationship_context": RelationshipType.FRIEND.value}
|
|
|
|
| 99 |
|
| 100 |
async def verify_rag_relevance(self, user_message: str, retrieved_doc: str) -> bool:
|
| 101 |
prompt = f"사용자의 현재 메시지와 ���색된 전문가 조언이 의미적으로 관련이 있는지 판단해줘. 반드시 'Yes' 또는 'No'로만 대답해.\n- 사용자 메시지: \"{user_message}\"\n- 검색된 조언: \"{retrieved_doc}\"\n\n관련이 있는가? (Yes/No):"
|
| 102 |
+
response = await self.create_completion(messages=[{"role": "user", "content": prompt}], temperature=0.0,
|
| 103 |
+
max_tokens=5)
|
| 104 |
logger.info(f"RAG 검증 결과: {response.strip()}")
|
| 105 |
return "yes" in response.strip().lower()
|
| 106 |
|
| 107 |
+
async def adapt_expert_response(self, expert_response: str, user_situation: str,
|
| 108 |
+
conversation_history: List[Dict]) -> Tuple[str, str, str, str]:
|
| 109 |
pre_adapted_response = self._apply_simple_conversions(expert_response)
|
| 110 |
+
messages = [{"role": "system", "content": self.teen_empathy_system_prompt}, *conversation_history,
|
| 111 |
+
{"role": "user",
|
| 112 |
+
"content": f"내 친구의 현재 상황은 '{user_situation}'이야. 내가 참고할 전문가 조언은 '{pre_adapted_response}'인데, 이 조언을 내 친구에게 말하듯 자연스럽고 따뜻한 반말로 바꿔줘."}]
|
| 113 |
final_prompt_for_debug = "\n".join([f"[{msg['role']}] {msg['content']}" for msg in messages])
|
| 114 |
final_response = await self.create_completion(messages=messages, temperature=0.5, max_tokens=400)
|
| 115 |
return expert_response, pre_adapted_response, final_response, final_prompt_for_debug
|
| 116 |
|
| 117 |
+
async def create_direct_response(self, user_message: str, conversation_history: List[Dict],
|
| 118 |
+
inspirational_docs: Optional[List[str]] = None) -> Tuple[str, str]:
|
| 119 |
+
messages = [{"role": "system", "content": self.teen_empathy_system_prompt}, *conversation_history]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
inspiration_prompt = ""
|
| 122 |
if inspirational_docs:
|
| 123 |
inspiration_prompt = "\n\n### 참고 자료 (직접 언급하지 말고, 답변을 만들 때 영감을 얻는 용도로만 사용해)\n"
|
| 124 |
+
for doc in inspirational_docs: inspiration_prompt += f"- {doc}\n"
|
|
|
|
| 125 |
|
| 126 |
final_user_prompt = f"""'마음이'의 페르소나(친한 친구, 반말)를 완벽하게 지키면서 다음 메시지에 공감하는 답변을 해줘.{inspiration_prompt}
|
| 127 |
|
|
|
|
| 129 |
"""
|
| 130 |
messages.append({"role": "user", "content": final_user_prompt})
|
| 131 |
|
|
|
|
| 132 |
prompt_for_debug = "\n".join([f"[{msg['role']}] {msg['content']}" for msg in messages])
|
| 133 |
+
final_response = await self.create_completion(messages=messages, temperature=0.7, max_tokens=300)
|
| 134 |
return final_response, prompt_for_debug
|
| 135 |
|
| 136 |
+
|
| 137 |
_openai_client_instance = None
|
| 138 |
+
|
| 139 |
+
|
| 140 |
async def get_openai_client() -> OpenAIClient:
|
| 141 |
global _openai_client_instance
|
| 142 |
if _openai_client_instance is None:
|