dammmmmmmmm commited on
Commit
d70abfc
·
verified ·
1 Parent(s): 6a11351

Update backend/ai/core/logic_flow.py

Browse files
Files changed (1) hide show
  1. backend/ai/core/logic_flow.py +59 -70
backend/ai/core/logic_flow.py CHANGED
@@ -9,28 +9,27 @@ import random
9
  import os
10
  from pathlib import Path
11
 
12
- # --- SETUP ĐƯỜNG DẪN THÔNG MINH ---
13
  CURRENT_FILE = Path(__file__).resolve()
14
- CORE_DIR = CURRENT_FILE.parent # backend/ai/core
15
- AI_DIR = CORE_DIR.parent # backend/ai
16
- DATA_DIR = AI_DIR / "data" # backend/ai/data
17
- STRATEGY_DIR = AI_DIR / "strategy" # backend/ai/strategy
18
 
19
- # Thêm tất cả đường dẫn vào sys.path
20
- sys.path.append(str(CORE_DIR))
21
  sys.path.append(str(AI_DIR))
22
  sys.path.append(str(DATA_DIR))
23
  sys.path.append(str(STRATEGY_DIR))
24
 
25
- # --- IMPORT MODULE VỆ TINH ---
26
  try:
27
  from services import AIServices
28
  from database import db
29
- except ImportError as e:
30
  try:
31
  from backend.ai.services import AIServices
32
  from backend.ai.database import db
33
- except ImportError:
34
  print(f"❌ Lỗi Import Core: {e}")
35
  sys.exit(1)
36
 
@@ -40,9 +39,8 @@ try:
40
  from strategy_competitor import CompetitorStrategy
41
  from strategy_low_data import LowDataStrategy
42
  from strategy_network import NetworkStrategy
43
- print(" [SYSTEM] Đã load đầy đủ các module vệ tinh.")
44
- except ImportError as e:
45
- print(f"⚠️ CẢNH BÁO: Thiếu file vệ tinh ({e}). Kích hoạt Dummy Class.")
46
  class VN_VoiceBot_Parallel:
47
  def analyze_sentiment(self, text): return 0.0, "neutral"
48
  def classify_issue(self, text): return None
@@ -50,16 +48,12 @@ except ImportError as e:
50
  class DataEngine:
51
  def __init__(self, *args): pass
52
  def get_full_context(self, cid): return {}
53
- class DummyStrategy:
54
- def __init__(self, *args): pass
55
- def execute_stream_gen(self, *args):
56
- async def gen(): yield "Dạ em nghe rồi ạ."
57
- return gen()
58
- def execute(self, *args): return "Dạ em nghe rồi ạ."
59
-
60
- if 'CompetitorStrategy' not in locals(): CompetitorStrategy = DummyStrategy
61
- if 'LowDataStrategy' not in locals(): LowDataStrategy = DummyStrategy
62
- if 'NetworkStrategy' not in locals(): NetworkStrategy = DummyStrategy
63
 
64
  # --- ADAPTER ---
65
  class GeminiAdapter:
@@ -86,19 +80,18 @@ class GeminiAdapter:
86
  class TelesalesAgent:
87
  def __init__(self):
88
  print("\n" + "="*40)
89
- print(" [SYSTEM] KHỞI ĐỘNG LOGIC v3.4 (FIX TRÙNG LẶP)")
90
  print(f" [DEBUG] Data Path: {DATA_DIR}")
91
  print("="*40 + "\n")
92
 
93
  self.ai_service = AIServices()
94
  self.analyzer = VN_VoiceBot_Parallel()
95
 
 
96
  csv_path = DATA_DIR / "test_customer.csv"
97
  json_path = DATA_DIR / "product_collection.json"
98
-
99
- if not csv_path.exists(): print(f"❌ KHÔNG TÌM THẤY: {csv_path}")
100
-
101
  self.data_engine = DataEngine(str(csv_path), str(json_path))
 
102
  self.adapter = GeminiAdapter(self.ai_service)
103
 
104
  self.strategies = {
@@ -119,12 +112,11 @@ class TelesalesAgent:
119
  def _load_scripts(self):
120
  try:
121
  script_path = DATA_DIR / 'final_voice_scripts.json'
122
- if not script_path.exists(): return
123
- with open(script_path, 'r', encoding='utf-8') as f:
124
- scripts = json.load(f)
125
- for item in scripts:
126
- self.sales_data[str(item['customer_id'])] = item
127
- print(f" [DATA] Đã load {len(self.sales_data)} kịch bản.")
128
  except: pass
129
 
130
  def _create_metrics_payload(self, stt_latency, start_process_time, sentiment, intent):
@@ -138,16 +130,16 @@ class TelesalesAgent:
138
  }
139
  }) + "\n"
140
 
141
- # --- HÀM LƯU DB (Đã thêm cơ chế chống trùng lặp) ---
142
- def _save_call_to_db(self, cid, session, sentiment_score, intent, is_upsell=False):
143
- try:
144
- # 🛑 CHẶN LƯU NHIỀU LẦN TRONG 1 SESSION
145
- if session.get('saved_to_db', False):
146
- return
147
 
 
148
  session_start = session.get('start_time', time.time() - 30)
149
  duration = time.time() - session_start
150
- if duration < 5: duration = 30
151
 
152
  cost_vnd = int(1000 + (duration * 50))
153
 
@@ -157,7 +149,7 @@ class TelesalesAgent:
157
 
158
  csat_val = 5 if sent_label == "positive" else (2 if sent_label == "negative" else 4)
159
 
160
- # Map intent sang chuẩn DB
161
  intent_mapping = {
162
  "Mạng nghẽn": "network_issue",
163
  "Mạng yếu": "network_issue",
@@ -172,29 +164,30 @@ class TelesalesAgent:
172
  }
173
  final_intent = intent_mapping.get(intent, "general")
174
 
 
175
  db.add_call(
176
  customer_id=cid,
177
  duration=int(duration),
178
- intent=final_intent,
179
  sentiment=sent_label,
180
  ai_resolved=True,
181
  upsell=is_upsell,
182
  cost={'value': cost_vnd, 'csat': csat_val}
183
  )
184
 
185
- # ✅ Đánh dấu là đã lưu để không lưu lại lần nữa
186
  session['saved_to_db'] = True
187
- print(f" [DB SAVED] Đã lưu cuộc gọi {cid} vào Database. Intent: {final_intent}")
188
 
189
  except Exception as e:
190
- print(f" [DB ERROR] Lỗi lưu DB: {e}")
191
 
192
  async def process_stream(self, customer_id_str, user_audio):
193
  cid = str(customer_id_str)
194
  current_loop = asyncio.get_running_loop()
195
  self.adapter.set_main_loop(current_loop)
196
 
197
- # 1. INIT SESSION
198
  if cid not in self.sessions or user_audio is None:
199
  print(f"\n--- NEW SESSION: {cid} ---")
200
  ctx = self.data_engine.get_full_context(cid)
@@ -203,8 +196,14 @@ class TelesalesAgent:
203
  gender_raw = cust_data.get('Gender', '').lower()
204
  pronoun = "Anh" if gender_raw in ['nam', 'male'] else ("Chị" if gender_raw in ['nu', 'female'] else "Mình")
205
 
206
- # Reset trạng thái saved_to_db = False khi bắt đầu cuộc gọi mới
207
- self.sessions[cid] = {'step': 1, 'history': [], 'pronoun': pronoun, 'start_time': time.time(), 'detected_intent': "General", 'saved_to_db': False}
 
 
 
 
 
 
208
 
209
  greeting = f"Dạ, em chào {pronoun} {cust_name}, em là nhân viên CSKH VNPT."
210
  greeting = self._normalize_pronouns(greeting, pronoun)
@@ -244,12 +243,13 @@ class TelesalesAgent:
244
  is_agreed = any(w in user_text.lower() for w in triggers_agree)
245
 
246
  if is_agreed:
 
247
  response_text = self.MSG_CLOSING
248
  end_of_mvp = True; session['step'] = 0
249
  sentiment_score = 1.0
250
 
251
- # [LƯU DB: KHI CHỐT ĐƠN]
252
- self._save_call_to_db(cid, session, sentiment_score, "Buying Signal", is_upsell=True)
253
 
254
  yield self._create_metrics_payload(stt_latency, start_process, sentiment_score, detected_intent)
255
  response_text = self._normalize_pronouns(response_text, current_pronoun)
@@ -265,35 +265,26 @@ class TelesalesAgent:
265
  detected_intent = issue
266
  session['detected_intent'] = issue
267
 
268
- # Kiểm tra Toxic / Từ chối trước
269
- toxic_keywords = ["lừa đảo", "mất dạy", "biến đi", "cút"]
270
  if any(kw in user_text.lower() for kw in toxic_keywords) or sentiment_score <= -0.8:
271
- print(f" [ALERT] Toxic -> Chuyển tư vấn viên.")
272
- response_text = f"Dạ, em thành thật xin lỗi. Em sẽ nối máy tới chuyên viên để hỗ trợ {current_pronoun.lower()} ngay ạ."
273
  end_of_mvp = True
274
-
275
- # [LƯU DB: KHI KHÁCH CÁU]
276
- self._save_call_to_db(cid, session, -1.0, "Angry/Handover", is_upsell=False)
277
-
278
  async for chunk in self._stream_text_and_audio(response_text): yield chunk
279
 
280
- elif re.search(r"(không\s+cần|không\s+muốn|không\s+đăng\s+ký|thôi\s+em|tắt\s+máy)", user_text.lower()):
281
- print(f" [INTENT] Khách từ chối -> Kết thúc.")
282
  response_text = f"Dạ vâng, em cảm ơn {current_pronoun.lower()}. Chào {current_pronoun.lower()} ạ."
283
  end_of_mvp = True
284
-
285
- # [LƯU DB: KHI KHÁCH TỪ CHỐI]
286
- self._save_call_to_db(cid, session, 0.0, "cancel_package", is_upsell=False)
287
-
288
  async for chunk in self._stream_text_and_audio(response_text): yield chunk
289
 
290
- # Nếu không phải kết thúc thì chạy Strategy
291
  elif issue and issue in self.strategies:
292
  print(f" [PIPELINE] Kích hoạt Strategy: {issue}")
293
  if step == 1: session['step'] = 2
294
-
295
- # 🛑 ĐÃ XÓA LỆNH LƯU DB Ở ĐÂY ĐỂ TRÁNH TRÙNG LẶP
296
-
297
  yield self._create_metrics_payload(stt_latency, start_process, sentiment_score, detected_intent)
298
 
299
  strategy_obj = self.strategies[issue]
@@ -342,8 +333,6 @@ class TelesalesAgent:
342
  response_text = self.MSG_FALLBACK
343
  detected_intent = "Fallback"
344
 
345
- # 🛑 ĐÃ XÓA LỆNH LƯU DB Ở ĐÂY ĐỂ TRÁNH TRÙNG LẶP
346
-
347
  yield self._create_metrics_payload(stt_latency, start_process, sentiment_score, detected_intent)
348
  response_text = self._normalize_pronouns(response_text, current_pronoun)
349
  async for chunk in self._stream_text_and_audio(response_text): yield chunk
 
9
  import os
10
  from pathlib import Path
11
 
12
+ # --- SETUP ĐƯỜNG DẪN (Để đảm bảo tìm thấy file data) ---
13
  CURRENT_FILE = Path(__file__).resolve()
14
+ CORE_DIR = CURRENT_FILE.parent
15
+ AI_DIR = CORE_DIR.parent
16
+ DATA_DIR = AI_DIR / "data"
17
+ STRATEGY_DIR = AI_DIR / "strategy"
18
 
19
+ sys.path.append(str(CORE_DIR))
 
20
  sys.path.append(str(AI_DIR))
21
  sys.path.append(str(DATA_DIR))
22
  sys.path.append(str(STRATEGY_DIR))
23
 
24
+ # --- IMPORT ---
25
  try:
26
  from services import AIServices
27
  from database import db
28
+ except ImportError:
29
  try:
30
  from backend.ai.services import AIServices
31
  from backend.ai.database import db
32
+ except ImportError as e:
33
  print(f"❌ Lỗi Import Core: {e}")
34
  sys.exit(1)
35
 
 
39
  from strategy_competitor import CompetitorStrategy
40
  from strategy_low_data import LowDataStrategy
41
  from strategy_network import NetworkStrategy
42
+ except ImportError:
43
+ # Dummy classes nếu thiếu file (để tránh crash)
 
44
  class VN_VoiceBot_Parallel:
45
  def analyze_sentiment(self, text): return 0.0, "neutral"
46
  def classify_issue(self, text): return None
 
48
  class DataEngine:
49
  def __init__(self, *args): pass
50
  def get_full_context(self, cid): return {}
51
+ if 'CompetitorStrategy' not in locals():
52
+ class CompetitorStrategy:
53
+ def __init__(self, *args): pass
54
+ def execute(self, *args): return "Dạ vâng ạ."
55
+ if 'LowDataStrategy' not in locals(): LowDataStrategy = CompetitorStrategy
56
+ if 'NetworkStrategy' not in locals(): NetworkStrategy = CompetitorStrategy
 
 
 
 
57
 
58
  # --- ADAPTER ---
59
  class GeminiAdapter:
 
80
  class TelesalesAgent:
81
  def __init__(self):
82
  print("\n" + "="*40)
83
+ print(" [SYSTEM] KHỞI ĐỘNG LOGIC (FIX DUPLICATE CALLS)")
84
  print(f" [DEBUG] Data Path: {DATA_DIR}")
85
  print("="*40 + "\n")
86
 
87
  self.ai_service = AIServices()
88
  self.analyzer = VN_VoiceBot_Parallel()
89
 
90
+ # Dùng đường dẫn tuyệt đối cho DataEngine
91
  csv_path = DATA_DIR / "test_customer.csv"
92
  json_path = DATA_DIR / "product_collection.json"
 
 
 
93
  self.data_engine = DataEngine(str(csv_path), str(json_path))
94
+
95
  self.adapter = GeminiAdapter(self.ai_service)
96
 
97
  self.strategies = {
 
112
  def _load_scripts(self):
113
  try:
114
  script_path = DATA_DIR / 'final_voice_scripts.json'
115
+ if script_path.exists():
116
+ with open(script_path, 'r', encoding='utf-8') as f:
117
+ scripts = json.load(f)
118
+ for item in scripts:
119
+ self.sales_data[str(item['customer_id'])] = item
 
120
  except: pass
121
 
122
  def _create_metrics_payload(self, stt_latency, start_process_time, sentiment, intent):
 
130
  }
131
  }) + "\n"
132
 
133
+ # --- HÀM HELPER: LƯU DB MỘT LẦN DUY NHẤT ---
134
+ def _save_call_once(self, cid, session, sentiment_score, intent, is_upsell=False):
135
+ # Kiểm tra nếu đã lưu rồi thì THOÁT NGAY
136
+ if session.get('saved_to_db', False):
137
+ return
 
138
 
139
+ try:
140
  session_start = session.get('start_time', time.time() - 30)
141
  duration = time.time() - session_start
142
+ if duration < 5: duration = 30
143
 
144
  cost_vnd = int(1000 + (duration * 50))
145
 
 
149
 
150
  csat_val = 5 if sent_label == "positive" else (2 if sent_label == "negative" else 4)
151
 
152
+ # Map intent
153
  intent_mapping = {
154
  "Mạng nghẽn": "network_issue",
155
  "Mạng yếu": "network_issue",
 
164
  }
165
  final_intent = intent_mapping.get(intent, "general")
166
 
167
+ # Lưu vào DB
168
  db.add_call(
169
  customer_id=cid,
170
  duration=int(duration),
171
+ intent=final_intent,
172
  sentiment=sent_label,
173
  ai_resolved=True,
174
  upsell=is_upsell,
175
  cost={'value': cost_vnd, 'csat': csat_val}
176
  )
177
 
178
+ # ĐÁNH DẤU LÀ ĐÃ LƯU -> Để lần sau không lưu nữa
179
  session['saved_to_db'] = True
180
+ print(f" [DB SAVED] Đã lưu thành công ID: {cid}. Intent: {final_intent}")
181
 
182
  except Exception as e:
183
+ print(f" [DB ERROR] {e}")
184
 
185
  async def process_stream(self, customer_id_str, user_audio):
186
  cid = str(customer_id_str)
187
  current_loop = asyncio.get_running_loop()
188
  self.adapter.set_main_loop(current_loop)
189
 
190
+ # 1. INIT SESSION (Thêm cờ saved_to_db)
191
  if cid not in self.sessions or user_audio is None:
192
  print(f"\n--- NEW SESSION: {cid} ---")
193
  ctx = self.data_engine.get_full_context(cid)
 
196
  gender_raw = cust_data.get('Gender', '').lower()
197
  pronoun = "Anh" if gender_raw in ['nam', 'male'] else ("Chị" if gender_raw in ['nu', 'female'] else "Mình")
198
 
199
+ self.sessions[cid] = {
200
+ 'step': 1,
201
+ 'history': [],
202
+ 'pronoun': pronoun,
203
+ 'start_time': time.time(),
204
+ 'detected_intent': "General",
205
+ 'saved_to_db': False # <--- Cờ quan trọng
206
+ }
207
 
208
  greeting = f"Dạ, em chào {pronoun} {cust_name}, em là nhân viên CSKH VNPT."
209
  greeting = self._normalize_pronouns(greeting, pronoun)
 
243
  is_agreed = any(w in user_text.lower() for w in triggers_agree)
244
 
245
  if is_agreed:
246
+ # === KHÁCH CHỐT ĐƠN ===
247
  response_text = self.MSG_CLOSING
248
  end_of_mvp = True; session['step'] = 0
249
  sentiment_score = 1.0
250
 
251
+ # Lưu DB ngay khi chốt (Chỉ 1 lần)
252
+ self._save_call_once(cid, session, sentiment_score, "Buying Signal", is_upsell=True)
253
 
254
  yield self._create_metrics_payload(stt_latency, start_process, sentiment_score, detected_intent)
255
  response_text = self._normalize_pronouns(response_text, current_pronoun)
 
265
  detected_intent = issue
266
  session['detected_intent'] = issue
267
 
268
+ # Kiểm tra Toxic / Từ chối
269
+ toxic_keywords = ["lừa đảo", "mất dạy", "cút", "biến"]
270
  if any(kw in user_text.lower() for kw in toxic_keywords) or sentiment_score <= -0.8:
271
+ # === KHÁCH CÁU GẮT ===
272
+ response_text = f"Dạ, em thành thật xin lỗi. Em sẽ nối máy tới chuyên viên ngay ạ."
273
  end_of_mvp = True
274
+ self._save_call_once(cid, session, -1.0, "Angry/Handover", is_upsell=False)
 
 
 
275
  async for chunk in self._stream_text_and_audio(response_text): yield chunk
276
 
277
+ elif re.search(r"(không\s+cần|không\s+muốn|không\s+đăng\s+ký|thôi\s+em)", user_text.lower()):
278
+ # === KHÁCH TỪ CHỐI ===
279
  response_text = f"Dạ vâng, em cảm ơn {current_pronoun.lower()}. Chào {current_pronoun.lower()} ạ."
280
  end_of_mvp = True
281
+ self._save_call_once(cid, session, 0.0, "cancel_package", is_upsell=False)
 
 
 
282
  async for chunk in self._stream_text_and_audio(response_text): yield chunk
283
 
284
+ # Xử lý vấn đề (KHÔNG LƯU DB Ở ĐÂY NỮA)
285
  elif issue and issue in self.strategies:
286
  print(f" [PIPELINE] Kích hoạt Strategy: {issue}")
287
  if step == 1: session['step'] = 2
 
 
 
288
  yield self._create_metrics_payload(stt_latency, start_process, sentiment_score, detected_intent)
289
 
290
  strategy_obj = self.strategies[issue]
 
333
  response_text = self.MSG_FALLBACK
334
  detected_intent = "Fallback"
335
 
 
 
336
  yield self._create_metrics_payload(stt_latency, start_process, sentiment_score, detected_intent)
337
  response_text = self._normalize_pronouns(response_text, current_pronoun)
338
  async for chunk in self._stream_text_and_audio(response_text): yield chunk