Spaces:
Sleeping
Sleeping
Update backend/ai/core/logic_flow.py
Browse files- 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
|
| 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 |
-
|
| 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
|
| 26 |
try:
|
| 27 |
from services import AIServices
|
| 28 |
from database import db
|
| 29 |
-
except ImportError
|
| 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 |
-
|
| 44 |
-
|
| 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 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 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
|
| 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
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 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
|
| 142 |
-
def
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 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
|
| 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 |
-
#
|
| 186 |
session['saved_to_db'] = True
|
| 187 |
-
print(f" [DB SAVED] Đã lưu
|
| 188 |
|
| 189 |
except Exception as e:
|
| 190 |
-
print(f" [DB ERROR]
|
| 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 |
-
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 252 |
-
self.
|
| 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
|
| 269 |
-
toxic_keywords = ["lừa đảo", "mất dạy", "
|
| 270 |
if any(kw in user_text.lower() for kw in toxic_keywords) or sentiment_score <= -0.8:
|
| 271 |
-
|
| 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
|
| 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
|
| 281 |
-
|
| 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 |
-
#
|
| 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
|