youdie006 commited on
Commit
add744f
·
1 Parent(s): b2664e5

fix: debug

Browse files
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
- # [최종 업그레이드] 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}
 
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
- relationship: Optional[str] = None, top_k: int = 3) -> List[Dict]:
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: search_filter = {"$and": conditions}
25
- elif len(conditions) == 1: search_filter = conditions[0]
 
 
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: await self.client.chat.completions.create(model=self.default_model, messages=[{"role": "user", "content": "Hello"}], max_tokens=5)
34
- except Exception as e: raise e
 
 
 
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
- if not conversation_history: return user_message
 
 
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, max_tokens=200)
 
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}], temperature=0.0, max_tokens=200)
81
- import json
 
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, max_tokens=5)
 
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, conversation_history: List[Dict]) -> Tuple[str, str, str, str]:
 
97
  pre_adapted_response = self._apply_simple_conversions(expert_response)
98
- messages = [{"role": "system", "content": self.teen_empathy_system_prompt}, *conversation_history, {"role": "user", "content": f"내 친구의 현재 상황은 '{user_situation}'이야. 내가 참고할 전문가 조언은 '{pre_adapted_response}'인데, 이 조언을 내 친구에게 말하듯 자연스럽고 따뜻한 반말로 바꿔줘."}]
 
 
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], inspirational_docs: Optional[List[str]] = None) -> Tuple[str, str]:
104
- """[최종 수정] '영감'을 위한 참고 자료(inspirational_docs) 인자로 받아 프롬프트에 추가"""
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: