Romanchello-bit commited on
Commit
84f1fc6
·
1 Parent(s): 5fa037a

Add RL training and enhance analytics dashboard

Browse files

Introduced a reinforcement learning module to analyze past call transcripts and adjust graph edge weights for improved AI sales strategy. Enhanced the dashboard with a training button, detailed call inspector, and improved metrics. Updated leads_manager to store full call transcripts for learning. Refactored sales_script.json to expand sales stages, add new nodes and edges, and update weights for more granular conversation flow.

Files changed (3) hide show
  1. app.py +107 -11
  2. leads_manager.py +4 -3
  3. sales_script.json +109 -81
app.py CHANGED
@@ -7,6 +7,7 @@ from datetime import datetime
7
  import google.generativeai as genai
8
  from graph_module import Graph
9
  from algorithms import bellman_ford_list
 
10
 
11
  # --- CONFIG ---
12
  st.set_page_config(layout="wide", page_title="SellMe AI Engine")
@@ -109,7 +110,7 @@ def get_predicted_path(graph, start_id, target_id, id_to_node, node_to_id):
109
  if dist[target_id] == float('inf'): return []
110
  path = [target_id]
111
  curr = target_id
112
- while curr != start_id:
113
  found = False
114
  for u in range(graph.num_vertices):
115
  for v, w in graph.adj_list[u]:
@@ -225,6 +226,69 @@ def generate_greeting(model, start_instruction, lead_info):
225
  return f"Алло, {client_name}? Це {bot_name}."
226
 
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  # --- UI COMPONENTS ---
229
  def draw_graph(graph_data, current_node, predicted_path):
230
  nodes = graph_data[3]
@@ -318,16 +382,48 @@ graph, node_to_id, id_to_node, nodes, edges = graph_data
318
 
319
  # --- PAGE: DASHBOARD ---
320
  if st.session_state.page == "dashboard":
321
- st.title("📊 CRM Analytics")
322
- init_db()
323
- df = pd.read_csv(LEADS_FILE)
324
- if not df.empty:
 
 
 
 
 
 
 
 
325
  c1, c2, c3 = st.columns(3)
326
- c1.metric("Total Calls", len(df))
327
- c2.metric("B2B Leads", len(df[df['Type']=='B2B']))
328
- c3.metric("Success", len(df[df['Outcome']=='Success']))
329
- st.dataframe(df)
330
- else: st.info("Database empty.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
  # --- PAGE: SETUP ---
333
  elif st.session_state.page == "setup":
@@ -426,7 +522,7 @@ elif st.session_state.page == "chat":
426
 
427
  st.markdown("#### 📊 AI Strategy")
428
  curr_id = node_to_id[st.session_state.current_node]
429
- target_id = node_to_id["close_deal"] # Fixed: using close_deal from sales_script.json
430
  path = get_predicted_path(graph, curr_id, target_id, id_to_node, node_to_id)
431
  st.graphviz_chart(
432
  draw_graph(graph_data, st.session_state.current_node, path),
 
7
  import google.generativeai as genai
8
  from graph_module import Graph
9
  from algorithms import bellman_ford_list
10
+ from leads_manager import get_analytics
11
 
12
  # --- CONFIG ---
13
  st.set_page_config(layout="wide", page_title="SellMe AI Engine")
 
110
  if dist[target_id] == float('inf'): return []
111
  path = [target_id]
112
  curr = target_id
113
+ while curr != start_id and attempts < 200:
114
  found = False
115
  for u in range(graph.num_vertices):
116
  for v, w in graph.adj_list[u]:
 
226
  return f"Алло, {client_name}? Це {bot_name}."
227
 
228
 
229
+ def train_brain():
230
+ """
231
+ RL MODULE: Аналізує минулі діалоги і оновлює ваги графа.
232
+ """
233
+ # 1. Завантажуємо статистику
234
+ df, _ = get_analytics()
235
+ if df is None or df.empty or "Transcript" not in df.columns:
236
+ return "Недостатньо даних для навчання."
237
+
238
+ # 2. Завантажуємо поточний граф
239
+ graph, node_to_id, id_to_node, nodes, edges = load_graph_data()
240
+
241
+ # 3. Аналіз успішних/провальних шляхів
242
+ # (Це спрощена логіка: ми шукаємо ключові слова кроків у транскрипті)
243
+ success_bonuses = {} # node_name -> bonus
244
+
245
+ for index, row in df.iterrows():
246
+ is_success = (row["Outcome"] == "Success")
247
+ transcript = str(row["Transcript"])
248
+
249
+ # Проходимо по всіх вузлах і шукаємо, чи були вони в діалозі
250
+ # (Це грубий метод, в ідеалі треба зберігати шлях ID у базу)
251
+ for node_name, node_text in nodes.items():
252
+ # Шукаємо унікальні шматки тексту інструкції в транскрипті, щоб зрозуміти, чи були ми там
253
+ # Або просто перевіряємо, чи згадується цей етап в логах
254
+ snippet = node_text[:20]
255
+ if snippet in transcript:
256
+ if is_success:
257
+ success_bonuses[node_name] = success_bonuses.get(node_name, 0) + 1
258
+ else:
259
+ success_bonuses[node_name] = success_bonuses.get(node_name, 0) - 1
260
+
261
+ # 4. Оновлення ваг (Reinforcement)
262
+ new_edges = []
263
+ changes_log = []
264
+
265
+ for edge in edges:
266
+ u_name, v_name = edge["from"], edge["to"]
267
+ old_weight = edge["weight"]
268
+ new_weight = old_weight
269
+
270
+ # Якщо вузол 'to' часто зустрічається в успішних діалогах -> зменшуємо вагу вхідних ребер
271
+ score = success_bonuses.get(v_name, 0)
272
+
273
+ if score > 0: # Успішний вузол
274
+ new_weight *= 0.9 # Знижка 10%
275
+ elif score < 0: # Провальний вузол
276
+ new_weight *= 1.1 # Штраф 10%
277
+
278
+ # Обмеження щоб ваги не зламались
279
+ new_weight = max(1, min(new_weight, 100))
280
+ new_edges.append({"from": u_name, "to": v_name, "weight": round(new_weight, 2)})
281
+
282
+ if old_weight != new_weight:
283
+ changes_log.append(f"{u_name}->{v_name}: {old_weight} -> {new_weight}")
284
+
285
+ # 5. Збереження "Розумного" файлу
286
+ learned_data = {"nodes": nodes, "edges": new_edges}
287
+ with open("sales_script_learned.json", "w", encoding="utf-8") as f:
288
+ json.dump(learned_data, f, ensure_ascii=False, indent=2)
289
+
290
+ return f"Brain Updated! {len(changes_log)} weights adjusted based on {len(df)} calls."
291
+
292
  # --- UI COMPONENTS ---
293
  def draw_graph(graph_data, current_node, predicted_path):
294
  nodes = graph_data[3]
 
382
 
383
  # --- PAGE: DASHBOARD ---
384
  if st.session_state.page == "dashboard":
385
+ st.title("📊 CRM & Analytics Hub")
386
+
387
+ # Кнопка для запуску навчання
388
+ if st.button("🧠 Train AI on History (RL)"):
389
+ with st.spinner("Analyzing patterns... Updating weights..."):
390
+ msg = train_brain()
391
+ st.success(msg)
392
+
393
+ data, stats = get_analytics()
394
+
395
+ if data is not None and not data.empty:
396
+ # Метрики
397
  c1, c2, c3 = st.columns(3)
398
+ c1.metric("Total Calls", stats["total"])
399
+ c2.metric("Success Rate", f"{stats['success_rate']}%")
400
+ c3.metric("AI Learning Iterations", "v1.2") # Фейкова метрика для краси
401
+
402
+ st.divider()
403
+
404
+ # Вибір дзвінка для детального аналізу
405
+ st.subheader("🕵️ Call Inspector")
406
+
407
+ # Створюємо список для селектора: "Дата - Ім'я - Результат"
408
+ options = data.apply(lambda x: f"{x['Date']} | {x['Name']} ({x['Outcome']})", axis=1).tolist()
409
+ selected_option = st.selectbox("Select a call to review:", options)
410
+
411
+ if selected_option:
412
+ # Знаходимо вибраний рядок
413
+ selected_row = data.iloc[options.index(selected_option)]
414
+
415
+ with st.expander("📝 Full Transcript & Insights", expanded=True):
416
+ st.markdown(f"**Client:** {selected_row['Name']} ({selected_row['Type']})")
417
+ st.markdown(f"**Result:** {selected_row['Outcome']}")
418
+ st.text_area("Transcript", str(selected_row.get("Transcript", "No transcript available")), height=300)
419
+
420
+ if "AI Insights" in selected_row and selected_row["AI Insights"]:
421
+ st.info(f"💡 **AI Insight:** {selected_row['AI Insights']}")
422
+ else:
423
+ st.warning("No insights generated for this call.")
424
+
425
+ else:
426
+ st.info("Database is empty. Make some calls!")
427
 
428
  # --- PAGE: SETUP ---
429
  elif st.session_state.page == "setup":
 
522
 
523
  st.markdown("#### 📊 AI Strategy")
524
  curr_id = node_to_id[st.session_state.current_node]
525
+ target_id = node_to_id["close_standard"]
526
  path = get_predicted_path(graph, curr_id, target_id, id_to_node, node_to_id)
527
  st.graphviz_chart(
528
  draw_graph(graph_data, st.session_state.current_node, path),
leads_manager.py CHANGED
@@ -43,13 +43,14 @@ def save_lead_to_db(lead_info, chat_history, outcome):
43
  if not sheet.get_all_values():
44
  sheet.append_row([
45
  "Date", "Name", "Company", "Type", "Context",
46
- "Pain Point", "Budget", "Outcome", "Summary"
47
  ])
48
  except:
49
  pass # Таблиця може бути новою
50
 
51
  # Формуємо рядок даних
52
- summary_text = f"Msgs: {len(chat_history)}"
 
53
 
54
  row = [
55
  datetime.now().strftime("%Y-%m-%d %H:%M"),
@@ -60,7 +61,7 @@ def save_lead_to_db(lead_info, chat_history, outcome):
60
  "AI Pending", # Тут можна додати AI аналіз
61
  "Unknown",
62
  outcome,
63
- summary_text
64
  ]
65
 
66
  # Додаємо рядок
 
43
  if not sheet.get_all_values():
44
  sheet.append_row([
45
  "Date", "Name", "Company", "Type", "Context",
46
+ "Pain Point", "Budget", "Outcome", "Transcript"
47
  ])
48
  except:
49
  pass # Таблиця може бути новою
50
 
51
  # Формуємо рядок даних
52
+ # Збираємо весь текст діалогу для навчання
53
+ transcript = "\n".join([f"{msg['role']}: {msg['content']}" for msg in chat_history])
54
 
55
  row = [
56
  datetime.now().strftime("%Y-%m-%d %H:%M"),
 
61
  "AI Pending", # Тут можна додати AI аналіз
62
  "Unknown",
63
  outcome,
64
+ transcript
65
  ]
66
 
67
  # Додаємо рядок
sales_script.json CHANGED
@@ -1,151 +1,179 @@
1
  {
2
  "nodes": {
3
- "start": "ПРИВІТАННЯ. Мета: Представитися, переконатися, що говоримо з ЛПР (особою, що приймає рішення), і зачепити увагу інтригою (згадати про ріст продажів).",
4
- "objection_busy": "Розумію, часу обмаль. Але це займе всього 30 секунд, а зекономить вам години. Тільки одне питання: як ви зараз шукаєте клієнтів?",
5
- "qualification": "Скажіть, ви зараз використовуєте якусь CRM чи ведете все в Excel/блокноті?",
6
- "tech_shame": "Ох, Excel це класика, але ж уявіть, скільки лідів там губиться! А якби система сама нагадувала про кожного клієнта?",
7
- "tech_praise": "Круто, що у вас є CRM! Але чи заповнюють її менеджери руками? Наш AI робить це автоматично.",
8
- "pitch_value": "Суть проста: SellMe слухає розмову і сам створює угоду в CRM. Ви економите 2 години в день. Звучить цікаво?",
9
- "objection_trust": "Звучить надто добре, щоб бути правдою? Розумію. Ми теж так думали, поки не побачили кейс компанії 'SoftGroup', яка скоротила штат вдвічі, зберігши прибуток.",
10
- "objection_competitor": "А, ви про [Конкурента]? Вони молодці. Але у них ви платите за кожного юзера, а у нас безліміт. Навіщо переплачувати?",
11
- "price_reveal": "Вартість всього 50$ на місяць за всю команду. Це дешевше, ніж кава для офісу.",
12
- "objection_expensive": "50$ це дорого? Давайте порахуємо. Якщо AI врятує хоча б одну угоду на місяць, він вже окупився в 10 разів. Хіба ні?",
13
- "objection_think": "Розумію, треба подумати. Але поки ви думаєте, ваші конкуренти вже впроваджують AI. Може, просто спробуємо безкоштовний тиждень?",
14
- "soft_push": "Дивіться, ви нічим не ризикуєте. Я можу відкрити доступ прямо зараз без картки. Спробуємо?",
15
- "close_deal": "Чудово! Ви мудрий керівник. Диктуйте пошту, куди скинути доступ.",
16
- "exit_bad": "Добре. Якщо захочете автоматизувати хаос ми тут. Гарного дня.",
17
- "exit_later": "Окей, я наберу вас через місяць, коли вам набридне заповнювати звіти руками. До зв'язку!"
 
 
 
18
  },
19
  "edges": [
20
  {
21
  "from": "start",
22
- "to": "qualification",
23
- "weight": 1
24
  },
25
  {
26
  "from": "start",
27
- "to": "objection_busy",
28
- "weight": 5
29
  },
30
  {
31
  "from": "start",
32
- "to": "exit_bad",
33
- "weight": 100
34
  },
35
  {
36
- "from": "objection_busy",
37
- "to": "qualification",
38
- "weight": 2
39
  },
40
  {
41
- "from": "objection_busy",
42
- "to": "exit_later",
43
- "weight": 10
44
  },
45
  {
46
- "from": "qualification",
47
- "to": "tech_shame",
48
- "weight": 2
49
  },
50
  {
51
- "from": "qualification",
52
- "to": "tech_praise",
53
- "weight": 2
54
  },
55
  {
56
- "from": "qualification",
57
- "to": "objection_trust",
58
  "weight": 5
59
  },
60
  {
61
- "from": "tech_shame",
62
- "to": "pitch_value",
63
  "weight": 1
64
  },
65
  {
66
- "from": "tech_praise",
67
- "to": "pitch_value",
68
  "weight": 1
69
  },
70
  {
71
- "from": "pitch_value",
72
- "to": "price_reveal",
73
- "weight": 2
74
  },
75
  {
76
- "from": "pitch_value",
77
- "to": "objection_competitor",
78
- "weight": 4
79
  },
80
  {
81
- "from": "pitch_value",
82
- "to": "objection_trust",
83
  "weight": 4
84
  },
85
  {
86
- "from": "objection_trust",
87
- "to": "pitch_value",
88
- "weight": 2
89
  },
90
  {
91
- "from": "objection_trust",
92
- "to": "soft_push",
93
- "weight": 3
94
  },
95
  {
96
- "from": "objection_competitor",
97
- "to": "price_reveal",
98
  "weight": 2
99
  },
100
  {
101
- "from": "objection_competitor",
102
- "to": "exit_bad",
103
- "weight": 50
104
  },
105
  {
106
- "from": "price_reveal",
107
- "to": "close_deal",
108
  "weight": 10
109
  },
110
  {
111
- "from": "price_reveal",
112
- "to": "objection_expensive",
113
- "weight": 3
 
 
 
 
 
114
  },
115
  {
116
- "from": "price_reveal",
117
- "to": "objection_think",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  "weight": 4
119
  },
120
  {
121
- "from": "objection_expensive",
122
- "to": "soft_push",
 
 
 
 
 
123
  "weight": 1
124
  },
125
  {
126
- "from": "objection_expensive",
127
- "to": "exit_bad",
128
- "weight": 20
129
  },
130
  {
131
- "from": "objection_think",
132
- "to": "soft_push",
133
- "weight": 2
134
  },
135
  {
136
- "from": "objection_think",
137
- "to": "exit_later",
138
- "weight": 5
139
  },
140
  {
141
- "from": "soft_push",
142
- "to": "close_deal",
143
  "weight": 1
144
  },
145
  {
146
- "from": "soft_push",
147
- "to": "exit_later",
148
- "weight": 10
149
  }
150
  ]
151
  }
 
1
  {
2
  "nodes": {
3
+ "start": "ЕТАП: ПРИВІТАННЯ. Задача: Коротко представитися (назви своє ім'я та компанію SellMe). Перевір, чи зручно говорити. Зроби 'гачок' про автоматизацію, але не продавай в лоб.",
4
+ "hook_rejection": "ЕТАП: УТРИМАННЯ УВАГИ. Клієнт хоче кинути трубку. Задача: Не сперечатися. Використати техніку 'Тільки 30 секунд'. Скажи, що якщо не сподобається — сам підеш. Будь впевненим.",
5
+ "qualification_needs": "ЕТАП: КВАЛІФІКАЦІЯ. Задача: Дізнатися, як вони зараз працюють. Запитай про CRM або Excel. Ціль змусити клієнта визнати, що ручна робота — це незручно.",
6
+ "pain_followup": "ЕТАП: ТИСК НА БІЛЬ. Клієнт сказав, що менеджери забувають дзвонити. Задача: Підсилити цей біль. Запитай: 'А скільки грошей ви втрачаєте через це щомісяця?'.",
7
+ "pain_closing": "ЕТАП: ТИСК НА БІЛЬ. Проблема із закриттям угод. Задача: Поясни, що це людський фактор. Скажи, що AI не втомлюється і пам'ятає всі скрипти.",
8
+ "pitch_main": "ЕТАП: ПРЕЗЕНТАЦІЯ РІШЕННЯ. Задача: Презентуй SellMe як 'Суфлера для менеджерів'. Говори мовою вигоди (економія часу, ріст конверсії). Назви ціну (100$) в кінці.",
9
+ "price_shock": "ЕТ��П: РОБОТА З 'ДОРОГО'. Задача: Не виправдовуйся. Порівняй ціну з вартістю втраченого клієнта. Використай прийом: 'Це дешевше, ніж кава для офісу'.",
10
+ "downsell_offer": "ЕТАП: ПОНИЖЕННЯ СТАВКИ. Задача: Клієнт не тягне повну версію. Запропонуй Light-версію за 20$. Скажи, що це ідеально для старту.",
11
+ "upsell_offer": "ЕТАП: ПІДВИЩЕННЯ ЧЕКУ. Клієнт зацікавлений. Задача: Запропонуй VIP-інтеграцію. Скажи, що це зробить систему повністю автономною.",
12
+ "objection_tech": "ЕТАП: ТЕХНІЧНІ СУМНІВИ. Задача: Заспокой клієнта. Скажи, що це плагін для браузера, ставиться за 1 хвилину. Ніяких айтішників не треба.",
13
+ "competitor_trap": "ЕТАП: ВІДСТРОЙКА ВІД КОНКУРЕНТІВ. Задача: Похвали конкурента, але покажи нашу унікальність (ми не просто пишемо дзвінки, ми підказуємо онлайн).",
14
+ "close_standard": "ЕТАП: ЗАКРИТТЯ (Standard). Задача: Переходь до діла. Запитай куди виставити рахунок або коли зручно підписати договір.",
15
+ "close_light": "ЕТАП: ЗАКРИТТЯ (Light). Задача: Закрий на дешевий тариф. Головне — почати співпрацю.",
16
+ "close_vip": "ЕТАП: ЗАКРИТТЯ (VIP). Задача: Закріпи успіх. Привітай з правильним рішенням.",
17
+ "referral_ask": "ЕТАП: РЕКОМЕНДАЦІЯ. Задача: Угода зірвалася. Ввічливо попроси контакт партнера, кому це може бути цікаво.",
18
+ "exit_success": "ЕТАП: ПРОЩАННЯ (Успіх). Задача: Подякуй, домовся про наступний крок (лист/дзвінок). Попрощайся на позитиві.",
19
+ "exit_fail": "ЕТАП: ПРОЩАННЯ (Відмова). Задача: Залиш двері відкритими. Скажи, що будеш радий допомогти в майбутньому.",
20
+ "exit_neutral": "ЕТАП: ПРОЩАННЯ (Нейтрально). Задача: Ввічливо заверши розмову."
21
  },
22
  "edges": [
23
  {
24
  "from": "start",
25
+ "to": "hook_rejection",
26
+ "weight": 2
27
  },
28
  {
29
  "from": "start",
30
+ "to": "qualification_needs",
31
+ "weight": 1
32
  },
33
  {
34
  "from": "start",
35
+ "to": "exit_fail",
36
+ "weight": 1000
37
  },
38
  {
39
+ "from": "hook_rejection",
40
+ "to": "qualification_needs",
41
+ "weight": 1
42
  },
43
  {
44
+ "from": "hook_rejection",
45
+ "to": "exit_fail",
46
+ "weight": 50
47
  },
48
  {
49
+ "from": "qualification_needs",
50
+ "to": "pain_followup",
51
+ "weight": 1
52
  },
53
  {
54
+ "from": "qualification_needs",
55
+ "to": "pain_closing",
56
+ "weight": 1
57
  },
58
  {
59
+ "from": "qualification_needs",
60
+ "to": "pitch_main",
61
  "weight": 5
62
  },
63
  {
64
+ "from": "pain_followup",
65
+ "to": "pitch_main",
66
  "weight": 1
67
  },
68
  {
69
+ "from": "pain_closing",
70
+ "to": "pitch_main",
71
  "weight": 1
72
  },
73
  {
74
+ "from": "pitch_main",
75
+ "to": "price_shock",
76
+ "weight": 5
77
  },
78
  {
79
+ "from": "pitch_main",
80
+ "to": "objection_tech",
81
+ "weight": 3
82
  },
83
  {
84
+ "from": "pitch_main",
85
+ "to": "competitor_trap",
86
  "weight": 4
87
  },
88
  {
89
+ "from": "pitch_main",
90
+ "to": "upsell_offer",
91
+ "weight": 10
92
  },
93
  {
94
+ "from": "pitch_main",
95
+ "to": "close_standard",
96
+ "weight": 2
97
  },
98
  {
99
+ "from": "price_shock",
100
+ "to": "downsell_offer",
101
  "weight": 2
102
  },
103
  {
104
+ "from": "price_shock",
105
+ "to": "pitch_main",
106
+ "weight": 5
107
  },
108
  {
109
+ "from": "price_shock",
110
+ "to": "close_standard",
111
  "weight": 10
112
  },
113
  {
114
+ "from": "downsell_offer",
115
+ "to": "close_light",
116
+ "weight": 1
117
+ },
118
+ {
119
+ "from": "downsell_offer",
120
+ "to": "exit_fail",
121
+ "weight": 10
122
  },
123
  {
124
+ "from": "objection_tech",
125
+ "to": "pitch_main",
126
+ "weight": 2
127
+ },
128
+ {
129
+ "from": "objection_tech",
130
+ "to": "upsell_offer",
131
+ "weight": 5
132
+ },
133
+ {
134
+ "from": "competitor_trap",
135
+ "to": "pitch_main",
136
+ "weight": 2
137
+ },
138
+ {
139
+ "from": "competitor_trap",
140
+ "to": "downsell_offer",
141
  "weight": 4
142
  },
143
  {
144
+ "from": "upsell_offer",
145
+ "to": "close_vip",
146
+ "weight": 2
147
+ },
148
+ {
149
+ "from": "upsell_offer",
150
+ "to": "close_standard",
151
  "weight": 1
152
  },
153
  {
154
+ "from": "close_standard",
155
+ "to": "exit_success",
156
+ "weight": 0
157
  },
158
  {
159
+ "from": "close_light",
160
+ "to": "exit_success",
161
+ "weight": 0
162
  },
163
  {
164
+ "from": "close_vip",
165
+ "to": "exit_success",
166
+ "weight": 0
167
  },
168
  {
169
+ "from": "exit_fail",
170
+ "to": "referral_ask",
171
  "weight": 1
172
  },
173
  {
174
+ "from": "referral_ask",
175
+ "to": "exit_neutral",
176
+ "weight": 0
177
  }
178
  ]
179
  }