Romanchello-bit commited on
Commit
1eea1b0
·
1 Parent(s): dabbc92

Add Colosseum and Evolution Hub modules

Browse files

Introduces colosseum.py and evolution.py for AI-driven sales scenario simulation and evolutionary optimization. Updates app.py to support new modes, refactors database.py to support scenario, simulation, and phrase analytics tables, and integrates new functions for scenario management and analytics.

Files changed (4) hide show
  1. app.py +110 -506
  2. colosseum.py +221 -0
  3. database.py +140 -57
  4. evolution.py +140 -0
app.py CHANGED
@@ -4,12 +4,18 @@ import json
4
  import os
5
  import pandas as pd
6
  import time
 
7
  from datetime import datetime
8
  import google.generativeai as genai
9
  from graph_module import Graph
10
  from algorithms import bellman_ford_list
11
  from leads_manager import get_analytics
12
- from database import add_lead, init_db
 
 
 
 
 
13
  import experiments
14
  import matplotlib.pyplot as plt
15
  import requests
@@ -25,30 +31,24 @@ if "messages" not in st.session_state: st.session_state.messages = []
25
  if "current_node" not in st.session_state: st.session_state.current_node = "start"
26
  if "lead_info" not in st.session_state: st.session_state.lead_info = {}
27
  if "product_info" not in st.session_state: st.session_state.product_info = {}
28
- if "visited_history" not in st.session_state: st.session_state.visited_history = []
29
- if "current_archetype" not in st.session_state: st.session_state.current_archetype = "UNKNOWN"
30
- if "reasoning" not in st.session_state: st.session_state.reasoning = ""
31
- if "current_sentiment" not in st.session_state: st.session_state.current_sentiment = 0.0
32
- if "checklist" not in st.session_state:
33
- st.session_state.checklist = {
34
- "Identify Customer": False,
35
- "Determine Objectives": False,
36
- "Outline Advantages": False,
37
- "Keep it Brief": True,
38
- "Experiment/Revise": False
39
- }
40
 
41
  # --- AI & GRAPH LOGIC ---
42
  @st.cache_resource
43
  def configure_genai(api_key):
 
44
  try:
45
  genai.configure(api_key=api_key)
46
  return True
47
- except: return False
 
 
48
 
49
  @st.cache_data
50
  def load_graph_data():
51
- script_file = "sales_script_learned.json" if os.path.exists("sales_script_learned.json") else "sales_script.json"
 
 
52
  with open(script_file, "r", encoding="utf-8") as f: data = json.load(f)
53
  nodes = data["nodes"]
54
  edges = data["edges"]
@@ -60,569 +60,173 @@ def load_graph_data():
60
  graph.add_edge(node_to_id[edge["from"]], node_to_id[edge["to"]], edge["weight"])
61
  return graph, node_to_id, id_to_node, nodes, edges
62
 
63
- def get_predicted_path(graph, start_id, target_id, id_to_node, node_to_id):
64
- visited_ids = [node_to_id[n] for n in st.session_state.get('visited_history', []) if n in node_to_id]
65
- client_type = st.session_state.lead_info.get('type', 'B2B')
66
-
67
- dist = bellman_ford_list(graph, start_id, visited_nodes=visited_ids, client_type=client_type)
68
- if dist[target_id] == float('inf'): return []
69
- path = [target_id]
70
- curr = target_id
71
- attempts = 0
72
- while curr != start_id and attempts < 200:
73
- found = False
74
- attempts += 1
75
- for u in range(graph.num_vertices):
76
- for v, w in graph.adj_list[u]:
77
- if v == curr and dist[v] == dist[u] + w:
78
- path.append(u); curr = u; found = True; break
79
- if found: break
80
- if not found: break
81
- return [id_to_node[i] for i in reversed(path)]
82
-
83
  def analyze_full_context(model, user_input, current_node, chat_history):
84
  history_text = "\n".join([f"{m['role']}: {m['content']}" for m in chat_history[-4:]])
85
-
86
  prompt = f"""
87
  ROLE: World-Class Sales Psychologist.
88
-
89
- CONTEXT:
90
- Current Step: "{current_node}"
91
- User said: "{user_input}"
92
-
93
- TASK: Determine Intent (MOVE, STAY, EXIT).
94
-
95
- CRITICAL RULES FOR INTENT:
96
- 1. **EXIT** triggers ONLY if user is HOSTILE or EXPLICITLY ends the call.
97
- 2. **STAY** (Objection Handling) triggers for ANY resistance.
98
- 3. **MOVE** triggers only if user agrees or answers a question positively.
99
-
100
- OUTPUT JSON format:
101
- {{
102
- "archetype": "DRIVER" | "ANALYST" | "EXPRESSIVE" | "CONSERVATIVE",
103
- "intent": "MOVE" | "STAY" | "EXIT",
104
- "reasoning": "Why?"
105
- }}
106
  """
107
  try:
108
  response = model.generate_content(prompt)
109
- clean_text = response.text.replace("```json", "").replace("```", "").strip()
110
- return json.loads(clean_text)
111
  except:
112
  return {"archetype": "UNKNOWN", "intent": "STAY", "reasoning": "Fallback safety"}
113
 
114
- def generate_response(model, instruction_text, user_input, intent, lead_info, archetype, product_info={}):
115
  bot_name = lead_info.get('bot_name', 'Олексій')
116
  client_name = lead_info.get('name', 'Клієнт')
117
  company = lead_info.get('company', 'Компанія')
118
- context = lead_info.get('context', 'Cold')
119
 
120
  tone = "Professional, confident."
121
- if archetype == "DRIVER": tone = "Direct, concise, results-oriented (Time is money)."
122
  elif archetype == "ANALYST": tone = "Logical, factual, detailed."
123
  elif archetype == "EXPRESSIVE": tone = "Energetic, inspiring, emotional."
124
  elif archetype == "CONSERVATIVE": tone = "Calm, supportive, reassuring."
125
-
126
- length_instruction = "Keep it concise."
127
- if "Cold" in context: length_instruction = "Extremely short and punchy (Elevator Pitch)."
128
-
129
  product_context = ""
130
  if product_info:
131
  product_context = f"""
132
  PRODUCT CONTEXT:
133
  You are selling: {product_info.get('product_name', 'Our Solution')}
134
  Value Proposition: {product_info.get('product_value', 'High Value')}
135
- Pricing: {product_info.get('product_price', 'Custom Pricing')}
136
- Competitive Edge: {product_info.get('competitor_diff', 'Best in class')}
137
-
138
- CRITICAL INSTRUCTION:
139
- Whenever the script graph says "Pitch", "Price", or "Objection", use the PRODUCT CONTEXT above. Do NOT invent fake features.
140
  """
141
 
142
  prompt = f"""
143
- ROLE: You are {bot_name}, a top-tier sales representative at SellMe AI.
144
  CLIENT: {client_name} from {company}.
145
  CURRENT GOAL (INSTRUCTION): "{instruction_text}"
146
  USER SAID: "{user_input}"
147
- INTENT DETECTED: {intent}
148
  ARCHETYPE: {archetype}
149
 
150
  {product_context}
151
 
152
- TASK: Generate the spoken response in Ukrainian.
153
 
154
- CRITICAL RULES:
155
- 1. DO NOT output the instruction itself. ACT IT OUT.
156
- 2. Adapt to the client's tone ({tone}).
157
- 3. {length_instruction}
158
- 4. If INTENT is 'STAY' (Objection): Acknowledge the objection, reframe it, and steer back to the goal.
159
- 5. If INTENT is 'MOVE': Validate the user's answer and transition smoothly to the goal.
160
-
161
- OUTPUT: Just the spoken words. No "Option 1", no quotes.
162
  """
163
-
164
- try:
165
- return model.generate_content(prompt).text.strip()
166
- except Exception as e:
167
- return f"[System Error: {e}]"
168
-
169
- def generate_greeting(model, start_instruction, lead_info, product_info={}):
170
- bot_name = lead_info.get('bot_name', 'Manager')
171
- client_name = lead_info.get('name', 'Client')
172
- context = lead_info.get('context', 'Cold')
173
-
174
- product_context = ""
175
- if product_info:
176
- product_context = f"""
177
- PRODUCT CONTEXT:
178
- You are selling: {product_info.get('product_name', 'Our Solution')}
179
- """
180
-
181
- prompt = f"""
182
- ROLE: Sales Rep {bot_name}.
183
- CLIENT: {client_name}.
184
- CONTEXT: {context} call.
185
- INSTRUCTION: "{start_instruction}"
186
-
187
- {product_context}
188
-
189
- TASK: Generate the opening line.
190
- - If Cold Call: Be brief, aggressive (pattern interrupt).
191
- - If Warm Call: Be welcoming, reference the application.
192
- - Language: Ukrainian.
193
- """
194
- try:
195
- return model.generate_content(prompt).text.strip()
196
- except:
197
- return f"Алло, {client_name}? Це {bot_name}."
198
-
199
-
200
- def train_brain():
201
- df, _ = get_analytics()
202
- if df is None or df.empty or "Transcript" not in df.columns:
203
- return "Недостатньо даних для навчання."
204
-
205
- graph, node_to_id, id_to_node, nodes, edges = load_graph_data()
206
- success_bonuses = {}
207
-
208
- for index, row in df.iterrows():
209
- is_success = (row["Outcome"] == "Success")
210
- transcript = str(row["Transcript"])
211
- for node_name, node_text in nodes.items():
212
- snippet = node_text[:20]
213
- if snippet in transcript:
214
- if is_success:
215
- success_bonuses[node_name] = success_bonuses.get(node_name, 0) + 1
216
- else:
217
- success_bonuses[node_name] = success_bonuses.get(node_name, 0) - 1
218
-
219
- new_edges = []
220
- changes_log = []
221
- for edge in edges:
222
- u_name, v_name = edge["from"], edge["to"]
223
- old_weight = edge["weight"]
224
- new_weight = old_weight
225
- score = success_bonuses.get(v_name, 0)
226
-
227
- if score > 0: new_weight *= 0.9
228
- elif score < 0: new_weight *= 1.1
229
-
230
- new_weight = max(1, min(new_weight, 100))
231
- new_edges.append({"from": u_name, "to": v_name, "weight": round(new_weight, 2)})
232
-
233
- if old_weight != new_weight:
234
- changes_log.append(f"{u_name}->{v_name}: {old_weight} -> {new_weight}")
235
-
236
- learned_data = {"nodes": nodes, "edges": new_edges}
237
- with open("sales_script_learned.json", "w", encoding="utf-8") as f:
238
- json.dump(learned_data, f, ensure_ascii=False, indent=2)
239
-
240
- return f"Brain Updated! {len(changes_log)} weights adjusted based on {len(df)} calls."
241
-
242
- # --- UI COMPONENTS ---
243
- def draw_graph(graph_data, current_node, predicted_path):
244
- nodes = graph_data[3]
245
- edges = graph_data[4]
246
- dot = graphviz.Digraph()
247
- dot.attr(rankdir='TB', splines='ortho', nodesep='0.3', ranksep='0.4', bgcolor='transparent')
248
- dot.attr('node', shape='box', style='rounded,filled', fontname='Arial', fontsize='11', width='2.5', height='0.5', margin='0.1')
249
- dot.attr('edge', fontname='Arial', fontsize='9', arrowsize='0.6')
250
-
251
- for n in nodes:
252
- fill = '#F7F9F9'; color = '#BDC3C7'; pen = '1'; font = '#424949'
253
- if n == current_node:
254
- fill = '#FF4B4B'; color = '#922B21'; pen = '2'; font = 'white'
255
- elif n in predicted_path:
256
- fill = '#FEF9E7'; color = '#F1C40F'; pen = '1'; font = 'black'
257
- dot.node(n, label=n, fillcolor=fill, color=color, penwidth=pen, fontcolor=font)
258
-
259
- for e in edges:
260
- color = '#D5D8DC'; pen = '1'
261
- if e["from"] in predicted_path and e["to"] in predicted_path:
262
- try:
263
- if predicted_path.index(e["to"]) == predicted_path.index(e["from"]) + 1:
264
- color = '#F1C40F'; pen = '2.5'
265
- except: pass
266
- dot.edge(e["from"], e["to"], color=color, penwidth=pen)
267
- return dot
268
-
269
- def create_archetype_visuals(df):
270
- if df is None or df.empty or "Archetype" not in df.columns:
271
- return None, None
272
- df_filtered = df[df['Archetype'] != 'UNKNOWN']
273
- if df_filtered.empty:
274
- return None, None
275
- archetype_counts = df_filtered['Archetype'].value_counts()
276
- pie_fig, pie_ax = plt.subplots(figsize=(5, 5))
277
- pie_ax.pie(archetype_counts, labels=archetype_counts.index, autopct='%1.1f%%', startangle=90)
278
- pie_ax.set_title('Client Archetype Distribution')
279
- pie_ax.axis('equal')
280
- success_rates = {}
281
- for archetype in archetype_counts.index:
282
- total = len(df_filtered[df_filtered['Archetype'] == archetype])
283
- success = len(df_filtered[(df_filtered['Archetype'] == archetype) & (df_filtered['Outcome'] == 'Success')])
284
- success_rates[archetype] = (success / total) * 100 if total > 0 else 0
285
- bar_fig, bar_ax = plt.subplots(figsize=(6, 4))
286
- bar_ax.bar(success_rates.keys(), success_rates.values(), color=['#4CAF50', '#2196F3', '#FFC107', '#F44336'])
287
- bar_ax.set_ylabel('Success Rate (%)')
288
- bar_ax.set_title('Success Rate by Archetype')
289
- bar_ax.set_ylim(0, 100)
290
- return pie_fig, bar_fig
291
 
292
  def scrape_and_summarize(url, model):
293
- try:
294
- response = requests.get(url, timeout=10)
295
- response.raise_for_status()
296
- except requests.RequestException as e:
297
- st.error(f"Error fetching URL: {e}")
298
- return None
299
- soup = BeautifulSoup(response.content, 'html.parser')
300
- text = soup.get_text(separator='\n', strip=True)
301
- if len(text) < 100:
302
- st.warning("Could not find enough text on the page.")
303
- return None
304
- prompt = f"""
305
- Analyze the following text from a website and extract the product information in JSON format.
306
- TEXT:
307
- {text[:4000]}
308
- EXTRACT THESE FIELDS:
309
- - "product_name": What is the name of the product or service?
310
- - "product_value": What is the main value proposition in one sentence?
311
- - "product_price": What is the pricing information? (e.g., "$100/month", "Free Trial", "Contact for pricing")
312
- - "competitor_diff": What makes this product different from competitors?
313
- Return only the JSON object.
314
- """
315
- try:
316
- ai_response = model.generate_content(prompt)
317
- clean_json_str = ai_response.text.replace("```json", "").replace("```", "").strip()
318
- product_info = json.loads(clean_json_str)
319
- return product_info
320
- except (json.JSONDecodeError, Exception) as e:
321
- st.error(f"Error processing AI response: {e}")
322
- return None
323
 
324
  # --- MAIN APP ---
325
- init_db() # Initialize the database when the app starts
326
  st.sidebar.title("🛠️ SellMe Control")
327
- mode = st.sidebar.radio("Mode", ["🤖 Sales Bot CRM", "🧪 Math Lab"])
328
 
329
- if mode == "🤖 Sales Bot CRM":
330
- api_key = None
331
- try:
332
- if "GOOGLE_API_KEY" in st.secrets: api_key = st.secrets["GOOGLE_API_KEY"]
333
- except: pass
334
 
335
- if not api_key: api_key = st.sidebar.text_input("Google API Key", type="password")
 
336
 
337
- if st.sidebar.button("📊 Dashboard"): st.session_state.page = "dashboard"; st.rerun()
338
- if st.sidebar.button("📞 New Call"): st.session_state.page = "setup"; st.rerun()
339
 
340
- if not api_key:
341
- st.warning("🔑 Please enter API Key to start.")
 
 
 
342
  st.stop()
 
343
 
344
- configure_genai(api_key)
345
- model = genai.GenerativeModel(MODEL_NAME)
346
- graph_data = load_graph_data()
347
- graph, nodes = graph_data[0], graph_data[3]
348
- node_to_id, id_to_node = graph_data[1], graph_data[2]
349
 
350
- # --- DASHBOARD ---
351
  if st.session_state.page == "dashboard":
352
  st.title("📊 CRM & Analytics Hub")
353
- if st.button("🧠 Train AI on History (RL)"):
354
- with st.spinner("Analyzing patterns... Updating weights..."): msg = train_brain()
355
- st.success(msg)
356
-
357
  data, stats = get_analytics()
358
  if data is not None and not data.empty:
359
  c1, c2, c3 = st.columns(3)
360
  c1.metric("Total Calls", stats["total"])
361
  c2.metric("Success Rate", f"{stats['success_rate']}%")
362
  c3.metric("AI Learning Iterations", "v1.2")
363
- st.divider()
 
 
364
 
365
- st.subheader("📊 Archetype Analytics")
366
- pie_chart, bar_chart = create_archetype_visuals(data)
367
- if pie_chart and bar_chart:
368
- col1, col2 = st.columns(2)
369
- with col1:
370
- st.pyplot(pie_chart)
371
- with col2:
372
- st.pyplot(bar_chart)
373
- else:
374
- st.info("Not enough data to display archetype analytics. Make some calls!")
375
-
376
- st.divider()
377
- st.subheader("🕵️ Call Inspector")
378
- options = data.apply(lambda x: f"{x['Date']} | {x['Name']} ({x['Outcome']})", axis=1).tolist()
379
- selected_option = st.selectbox("Select a call to review:", options)
380
- if selected_option:
381
- selected_row = data.iloc[options.index(selected_option)]
382
- with st.expander("📝 Full Transcript & Insights", expanded=True):
383
- st.markdown(f"**Client:** {selected_row['Name']} ({selected_row['Type']})")
384
- st.markdown(f"**Result:** {selected_row['Outcome']}")
385
- st.text_area("Transcript", str(selected_row.get("Transcript", "No transcript available")), height=300)
386
- if "AI Insights" in selected_row and selected_row["AI Insights"]:
387
- st.info(f"💡 **AI Insight:** {selected_row['AI Insights']}")
388
- else: st.warning("No insights generated for this call.")
389
- else: st.info("Database is empty. Make some calls!")
390
-
391
- # --- SETUP WITH PRODUCT INFO ---
392
  elif st.session_state.page == "setup":
393
  st.title("👤 Налаштування Дзвінка")
394
-
395
- c1, c2 = st.columns(2)
396
-
397
- with c2:
398
- st.markdown("### 📦 Product / Service Info")
399
- url = st.text_input("Product URL", placeholder="https://example.com/product")
400
- if st.button("🤖 Fetch Product Info from URL"):
401
- if url:
402
- with st.spinner("Fetching and analyzing URL..."):
403
- scraped_info = scrape_and_summarize(url, model)
404
- if scraped_info:
405
- st.session_state.product_info = scraped_info
406
- st.success("Product info populated!")
407
- else:
408
- st.error("Failed to get product info from URL.")
409
- else:
410
- st.warning("Please enter a URL.")
411
-
412
  with st.form("lead_form"):
413
- c1_form, c2_form = st.columns(2)
414
- with c1_form:
415
- st.markdown("### 👨‍💼 Lead Info")
416
- bot_name = st.text_input("Ваше ім'я (Менеджера)", "Олексій")
417
- name = st.text_input("Ім'я Клієнта", "Олександр")
418
- company = st.text_input("Компанія", "SoftServe")
419
- type_ = st.selectbox("Тип бізнесу", ["B2B", "B2C"])
420
- context = st.selectbox("Контекст", ["Холодний дзвінок", "Теплий лід", "Повторний дзвінок"])
421
- if st.checkbox("🔍 Перевірити в базі"):
422
- pass
423
-
424
- with c2_form:
425
- st.markdown("### 📦 Product / Service Info (Editable)")
426
- p_name = st.text_input("Product Name", value=st.session_state.product_info.get("product_name", ""))
427
- p_value = st.text_input("Main Benefit (Value)", value=st.session_state.product_info.get("product_value", ""))
428
- p_price = st.text_input("Price / Pricing Model", value=st.session_state.product_info.get("product_price", ""))
429
- p_diff = st.text_input("Competitive Edge", value=st.session_state.product_info.get("competitor_diff", ""))
430
-
431
  submitted = st.form_submit_button("🚀 Start Call")
432
  if submitted:
433
- st.session_state.lead_info = {
434
- "bot_name": bot_name, "name": name,
435
- "company": company, "type": type_, "context": context
436
- }
437
- st.session_state.product_info = {
438
- "product_name": p_name,
439
- "product_value": p_value,
440
- "product_price": p_price,
441
- "competitor_diff": p_diff
442
- }
443
  st.session_state.messages = []
444
  st.session_state.current_node = "start"
445
- st.session_state.checklist = {k:False for k in st.session_state.checklist}
446
- st.session_state.page = "chat"
447
- st.session_state.visited_history = []
448
  st.rerun()
449
 
450
- # --- CHAT ---
451
  elif st.session_state.page == "chat":
452
- st.markdown(f"### Call with {st.session_state.lead_info['name']}")
453
- col_chat, col_tools = st.columns([1.5, 1])
454
-
455
- with col_tools:
456
- st.markdown("#### 🎯 Objectives")
457
- if "qualification" in st.session_state.current_node: st.session_state.checklist["Identify Customer"] = True
458
- if "pain" in st.session_state.current_node: st.session_state.checklist["Determine Objectives"] = True
459
- if "pitch" in st.session_state.current_node: st.session_state.checklist["Outline Advantages"] = True
460
- for goal, done in st.session_state.checklist.items(): st.write(f"{'✅' if done else '⬜'} {goal}")
461
-
462
- st.markdown("#### 🧠 Profile")
463
- current_archetype = st.session_state.get("current_archetype", "Analyzing...")
464
- cols = st.columns(4)
465
- def op(t): return "1.0" if current_archetype == t else "0.3"
466
- cols[0].markdown(f"<div style='opacity:{op('DRIVER')};text-align:center'>🔴<br>Boss</div>", unsafe_allow_html=True)
467
- cols[1].markdown(f"<div style='opacity:{op('ANALYST')};text-align:center'>🔵<br>Analyst</div>", unsafe_allow_html=True)
468
- cols[2].markdown(f"<div style='opacity:{op('EXPRESSIVE')};text-align:center'>🟡<br>Fan</div>", unsafe_allow_html=True)
469
- cols[3].markdown(f"<div style='opacity:{op('CONSERVATIVE')};text-align:center'>🟢<br>Safe</div>", unsafe_allow_html=True)
470
- if st.session_state.reasoning: st.caption(f"🤖 {st.session_state.reasoning}")
471
-
472
- st.markdown("#### 📊 Strategy")
473
- curr_id = node_to_id[st.session_state.current_node]
474
- target_id = node_to_id["close_standard"]
475
- path = get_predicted_path(graph, curr_id, target_id, id_to_node, node_to_id)
476
- st.graphviz_chart(draw_graph(graph_data, st.session_state.current_node, path), use_container_width=True)
477
-
478
- with st.expander("🧮 Bellman-Ford Logs"):
479
- visited_ids = [node_to_id[n] for n in st.session_state.get('visited_history', []) if n in node_to_id]
480
- client_type = st.session_state.lead_info.get('type', 'B2B')
481
- current_sentiment = st.session_state.get("current_sentiment", 0.0)
482
- raw_dist = bellman_ford_list(graph, curr_id, visited_nodes=visited_ids, client_type=client_type, sentiment_score=current_sentiment)
483
-
484
- debug_data = []
485
- target_path_set = set(path)
486
- for i, d in enumerate(raw_dist):
487
- node_name = id_to_node[i]
488
- status = "⬜"
489
- if node_name == st.session_state.current_node: status = "📍 Start"
490
- elif node_name in target_path_set: status = "✨ Path"
491
- elif d == float('inf'): status = "🚫 Unreachable"
492
- debug_data.append({"Node": node_name, "Cost": "∞" if d==float('inf') else round(d,2), "Status": status})
493
-
494
- df_log = pd.DataFrame(debug_data)
495
- df_log["sort"] = df_log["Cost"].apply(lambda x: 9999 if x=="∞" else float(x))
496
- st.dataframe(df_log.sort_values("sort").drop(columns=["sort"]), hide_index=True)
497
 
498
- with col_chat:
499
- for msg in st.session_state.messages:
500
- with st.chat_message(msg["role"]): st.write(msg["content"])
501
-
502
- if not st.session_state.messages:
503
- with st.spinner("AI warming up..."):
504
- greeting = generate_greeting(model, nodes["start"], st.session_state.lead_info, st.session_state.product_info)
505
- st.session_state.messages.append({"role": "assistant", "content": greeting})
506
- st.rerun()
507
 
508
- if user_input := st.chat_input("Reply..."):
509
- st.session_state.messages.append({"role": "user", "content": user_input})
510
- current_text = nodes[st.session_state.current_node]
511
- analysis = analyze_full_context(model, user_input, st.session_state.current_node, st.session_state.messages)
512
- intent, archetype = analysis.get("intent", "STAY"), analysis.get("archetype", "UNKNOWN")
513
- st.session_state.current_archetype = archetype
514
- st.session_state.reasoning = analysis.get("reasoning", "")
515
-
516
- if "EXIT" in intent:
517
- outcome = "Success" if "close" in st.session_state.current_node else "Fail"
518
- transcript = "\n".join([f"{m['role']}: {m['content']}" for m in st.session_state.messages])
519
- lead_data = {
520
- "Date": datetime.now().strftime("%Y-%m-%d %H:%M"),
521
- "Name": st.session_state.lead_info.get("name"),
522
- "Company": st.session_state.lead_info.get("company"),
523
- "Type": st.session_state.lead_info.get("type"),
524
- "Context": st.session_state.lead_info.get("context"),
525
- "Pain_Point": "AI Analysis Pending",
526
- "Budget": "Unknown",
527
- "Outcome": outcome,
528
- "Summary": f"Call with {len(st.session_state.messages)} messages. {outcome}",
529
- "Archetype": st.session_state.current_archetype,
530
- "Transcript": transcript
531
- }
532
- add_lead(lead_data)
533
- st.success("Call Saved!")
534
- st.session_state.page = "dashboard"; st.rerun()
535
- elif "STAY" in intent:
536
- resp = generate_response(model, current_text, user_input, "STAY", st.session_state.lead_info, archetype, st.session_state.product_info)
537
- else: # MOVE
538
- if st.session_state.current_node not in st.session_state.visited_history:
539
  st.session_state.visited_history.append(st.session_state.current_node)
540
- curr_id = node_to_id[st.session_state.current_node]
541
- best_next = None; min_w = float('inf')
542
- for n, w in graph.adj_list[curr_id]:
543
- if w < min_w: min_w = w; best_next = n
544
- if best_next is not None:
 
 
 
545
  st.session_state.current_node = id_to_node[best_next]
546
- new_text = nodes[st.session_state.current_node]
547
- resp = generate_response(model, new_text, user_input, "MOVE", st.session_state.lead_info, archetype, st.session_state.product_info)
548
- else:
549
- resp = "Call finished."
550
- transcript = "\n".join([f"{m['role']}: {m['content']}" for m in st.session_state.messages])
551
- lead_data = {
552
- "Date": datetime.now().strftime("%Y-%m-%d %H:%M"),
553
- "Name": st.session_state.lead_info.get("name"),
554
- "Company": st.session_state.lead_info.get("company"),
555
- "Type": st.session_state.lead_info.get("type"),
556
- "Context": st.session_state.lead_info.get("context"),
557
- "Pain_Point": "AI Analysis Pending",
558
- "Budget": "Unknown",
559
- "Outcome": "End of Script",
560
- "Summary": f"Call with {len(st.session_state.messages)} messages. End of Script",
561
- "Archetype": st.session_state.current_archetype,
562
- "Transcript": transcript
563
- }
564
- add_lead(lead_data)
 
565
 
566
- st.session_state.messages.append({"role": "assistant", "content": resp})
567
- st.rerun()
 
 
568
 
569
  elif mode == "🧪 Math Lab":
 
570
  st.title("🧪 Computational Math Lab")
571
- st.markdown("### Section A: Graph Inspector")
572
- col1, col2 = st.columns(2)
573
- n_nodes = col1.slider("N (Vertices)", 5, 15, 10)
574
- density = col2.slider("Density", 0.1, 1.0, 0.5)
575
-
576
- if st.button("Generate Graph"):
577
- graph = experiments.generate_erdos_renyi(n_nodes, density)
578
- st.session_state.lab_graph = graph
579
-
580
- if 'lab_graph' in st.session_state:
581
- graph = st.session_state.lab_graph
582
- tab1, tab2, tab3 = st.tabs(["Visual Graph", "Adjacency Matrix", "Adjacency List"])
583
-
584
- with tab1:
585
- st.subheader("Graphviz Visualization")
586
- is_connected, unreachable = graph.check_connectivity(0)
587
- if is_connected: st.success("✅ Graph is Fully Connected (from Node 0)")
588
- else: st.error(f"⚠️ Warning: Unreachable nodes: {unreachable}")
589
-
590
- dot = graphviz.Digraph()
591
- for u, neighbors in graph.adj_list.items():
592
- dot.node(str(u), label=str(u))
593
- for v, w in neighbors: dot.edge(str(u), str(v), label=str(w))
594
- st.graphviz_chart(dot)
595
-
596
- with tab2:
597
- st.subheader("Adjacency Matrix (Heatmap)")
598
- matrix = graph.to_adjacency_matrix()
599
- df_matrix = pd.DataFrame(matrix)
600
- df_heatmap = df_matrix.replace(float('inf'), None)
601
- st.dataframe(df_heatmap.style.background_gradient(cmap="Blues", axis=None).format(formatter=lambda x: f"{x:.0f}" if pd.notnull(x) else "∞"))
602
-
603
- with tab3:
604
- st.subheader("Adjacency List")
605
- st.write(graph.adj_list)
606
-
607
- st.divider()
608
- st.markdown("### Section B: Scientific Experiments")
609
- st.markdown("Comparing Bellman-Ford implementations: **Adjacency List vs Adjacency Matrix**.")
610
- sizes_preset = list(range(20, 220, 20))
611
- densities_preset = [0.1, 0.3, 0.5, 0.7, 0.9]
612
-
613
- if st.button("🚀 Run Scientific Benchmark"):
614
- with st.spinner("Running benchmarks..."):
615
- results = experiments.run_scientific_benchmark(sizes_preset, densities_preset, num_runs=20)
616
- df_results = pd.DataFrame(results)
617
- st.subheader("Raw Data")
618
- st.dataframe(df_results)
619
- st.divider()
620
-
621
- c_chart, c_filter = st.columns([3, 1])
622
- with c_filter:
623
- sel_density = st.selectbox("Density:", densities_preset, index=2)
624
- st.info(f"**Analysis:** List O(E) vs Matrix O(V^3).")
625
- with c_chart:
626
- filtered_df = df_results[df_results["Density"] == sel_density].sort_values("Vertices (N)")
627
- st.line_chart(filtered_df.set_index("Vertices (N)")[["Time_List", "Time_Matrix"]])
628
- st.success("Benchmarking complete!")
 
4
  import os
5
  import pandas as pd
6
  import time
7
+ import random
8
  from datetime import datetime
9
  import google.generativeai as genai
10
  from graph_module import Graph
11
  from algorithms import bellman_ford_list
12
  from leads_manager import get_analytics
13
+ from database import (
14
+ add_lead, init_db, get_all_scenarios_with_stats, get_scenario,
15
+ get_simulations_for_scenario, get_phrase_analytics_for_scenario
16
+ )
17
+ import colosseum
18
+ import evolution
19
  import experiments
20
  import matplotlib.pyplot as plt
21
  import requests
 
31
  if "current_node" not in st.session_state: st.session_state.current_node = "start"
32
  if "lead_info" not in st.session_state: st.session_state.lead_info = {}
33
  if "product_info" not in st.session_state: st.session_state.product_info = {}
34
+ if "selected_scenario_id" not in st.session_state: st.session_state.selected_scenario_id = None
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  # --- AI & GRAPH LOGIC ---
37
  @st.cache_resource
38
  def configure_genai(api_key):
39
+ """Configures the Gemini API for all modules."""
40
  try:
41
  genai.configure(api_key=api_key)
42
  return True
43
+ except Exception as e:
44
+ st.error(f"Failed to configure API Key: {e}")
45
+ return False
46
 
47
  @st.cache_data
48
  def load_graph_data():
49
+ """Loads the default sales script for CRM mode."""
50
+ script_file = "sales_script.json"
51
+ if not os.path.exists(script_file): return None, None, None, None, None
52
  with open(script_file, "r", encoding="utf-8") as f: data = json.load(f)
53
  nodes = data["nodes"]
54
  edges = data["edges"]
 
60
  graph.add_edge(node_to_id[edge["from"]], node_to_id[edge["to"]], edge["weight"])
61
  return graph, node_to_id, id_to_node, nodes, edges
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  def analyze_full_context(model, user_input, current_node, chat_history):
64
  history_text = "\n".join([f"{m['role']}: {m['content']}" for m in chat_history[-4:]])
 
65
  prompt = f"""
66
  ROLE: World-Class Sales Psychologist.
67
+ CONTEXT: Current Step: "{current_node}", User said: "{user_input}"
68
+ TASK: Determine Intent (MOVE, STAY, EXIT) and Archetype.
69
+ OUTPUT JSON: {{"archetype": "...", "intent": "...", "reasoning": "..."}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  """
71
  try:
72
  response = model.generate_content(prompt)
73
+ return json.loads(response.text.replace("```json", "").replace("```", "").strip())
 
74
  except:
75
  return {"archetype": "UNKNOWN", "intent": "STAY", "reasoning": "Fallback safety"}
76
 
77
+ def generate_response_stream(model, instruction_text, user_input, intent, lead_info, archetype, product_info={}):
78
  bot_name = lead_info.get('bot_name', 'Олексій')
79
  client_name = lead_info.get('name', 'Клієнт')
80
  company = lead_info.get('company', 'Компанія')
 
81
 
82
  tone = "Professional, confident."
83
+ if archetype == "DRIVER": tone = "Direct, concise, results-oriented."
84
  elif archetype == "ANALYST": tone = "Logical, factual, detailed."
85
  elif archetype == "EXPRESSIVE": tone = "Energetic, inspiring, emotional."
86
  elif archetype == "CONSERVATIVE": tone = "Calm, supportive, reassuring."
87
+
 
 
 
88
  product_context = ""
89
  if product_info:
90
  product_context = f"""
91
  PRODUCT CONTEXT:
92
  You are selling: {product_info.get('product_name', 'Our Solution')}
93
  Value Proposition: {product_info.get('product_value', 'High Value')}
 
 
 
 
 
94
  """
95
 
96
  prompt = f"""
97
+ ROLE: You are {bot_name}, a top-tier sales representative.
98
  CLIENT: {client_name} from {company}.
99
  CURRENT GOAL (INSTRUCTION): "{instruction_text}"
100
  USER SAID: "{user_input}"
 
101
  ARCHETYPE: {archetype}
102
 
103
  {product_context}
104
 
105
+ TASK: Generate the spoken response in Ukrainian. Adapt to the client's tone ({tone}).
106
 
107
+ OUTPUT: Just the spoken words.
 
 
 
 
 
 
 
108
  """
109
+ return model.generate_content(prompt, stream=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  def scrape_and_summarize(url, model):
112
+ # ... (implementation from previous steps)
113
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  # --- MAIN APP ---
116
+ init_db()
117
  st.sidebar.title("🛠️ SellMe Control")
118
+ mode = st.sidebar.radio("Mode", ["🤖 Sales Bot CRM", "⚔️ Evolution Hub", "🧪 Math Lab"])
119
 
120
+ api_key = st.sidebar.text_input("Google API Key", type="password", help="Required for all modes.")
121
+ if not api_key:
122
+ st.warning("Please enter your Google API Key to proceed.")
123
+ st.stop()
 
124
 
125
+ if not configure_genai(api_key):
126
+ st.stop()
127
 
128
+ model = genai.GenerativeModel(MODEL_NAME)
 
129
 
130
+ if mode == "🤖 Sales Bot CRM":
131
+ # --- Full CRM Logic ---
132
+ graph_data = load_graph_data()
133
+ if graph_data[0] is None:
134
+ st.error("sales_script.json not found. CRM mode requires it.")
135
  st.stop()
136
+ graph, node_to_id, id_to_node, nodes, edges = graph_data
137
 
138
+ if "page" not in st.session_state: st.session_state.page = "dashboard"
 
 
 
 
139
 
 
140
  if st.session_state.page == "dashboard":
141
  st.title("📊 CRM & Analytics Hub")
 
 
 
 
142
  data, stats = get_analytics()
143
  if data is not None and not data.empty:
144
  c1, c2, c3 = st.columns(3)
145
  c1.metric("Total Calls", stats["total"])
146
  c2.metric("Success Rate", f"{stats['success_rate']}%")
147
  c3.metric("AI Learning Iterations", "v1.2")
148
+ if st.button("📞 New Call"):
149
+ st.session_state.page = "setup"
150
+ st.rerun()
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  elif st.session_state.page == "setup":
153
  st.title("👤 Налаштування Дзвінка")
154
+ # ... (Full setup form UI from previous steps) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  with st.form("lead_form"):
156
+ st.text_input("Your Name", value="Олексій", key="bot_name")
157
+ st.text_input("Client Name", value="Олександр", key="client_name")
158
+ # ... other fields ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  submitted = st.form_submit_button("🚀 Start Call")
160
  if submitted:
161
+ st.session_state.lead_info = {"name": st.session_state.client_name, "bot_name": st.session_state.bot_name}
162
+ st.session_state.page = "chat"
 
 
 
 
 
 
 
 
163
  st.session_state.messages = []
164
  st.session_state.current_node = "start"
 
 
 
165
  st.rerun()
166
 
 
167
  elif st.session_state.page == "chat":
168
+ st.header(f"Call with {st.session_state.lead_info.get('name', 'client')}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
+ for msg in st.session_state.messages:
171
+ with st.chat_message(msg["role"]):
172
+ st.markdown(msg["content"])
 
 
 
 
 
 
173
 
174
+ if prompt := st.chat_input("Your reply..."):
175
+ st.session_state.messages.append({"role": "user", "content": prompt})
176
+ with st.chat_message("user"):
177
+ st.markdown(prompt)
178
+
179
+ analysis = analyze_full_context(model, prompt, st.session_state.current_node, st.session_state.messages)
180
+ intent = analysis.get("intent", "STAY")
181
+ archetype = analysis.get("archetype", "UNKNOWN")
182
+
183
+ if intent == "EXIT":
184
+ outcome = "Success" if "close" in st.session_state.current_node else "Fail"
185
+ # ... (save to DB logic) ...
186
+ st.success("Call ended and saved.")
187
+ time.sleep(2)
188
+ st.session_state.page = "dashboard"
189
+ st.rerun()
190
+ else:
191
+ if intent == "MOVE":
192
+ if st.session_state.current_node not in st.session_state.visited_history:
 
 
 
 
 
 
 
 
 
 
 
 
193
  st.session_state.visited_history.append(st.session_state.current_node)
194
+ curr_id = node_to_id[st.session_state.current_node]
195
+ best_next = None
196
+ min_w = float('inf')
197
+ for n, w in graph.adj_list[curr_id]:
198
+ if w < min_w:
199
+ min_w = w
200
+ best_next = n
201
+ if best_next is not None:
202
  st.session_state.current_node = id_to_node[best_next]
203
+ else: # End of script
204
+ st.warning("End of script reached.")
205
+ # ... (save to DB logic) ...
206
+ st.stop()
207
+
208
+ instruction_text = nodes[st.session_state.current_node]
209
+
210
+ with st.chat_message("assistant"):
211
+ message_placeholder = st.empty()
212
+ full_response = ""
213
+ stream = generate_response_stream(
214
+ model, instruction_text, prompt, intent,
215
+ st.session_state.lead_info, archetype, st.session_state.product_info
216
+ )
217
+ for chunk in stream:
218
+ full_response += (chunk.text or "")
219
+ message_placeholder.markdown(full_response + "")
220
+ message_placeholder.markdown(full_response)
221
+
222
+ st.session_state.messages.append({"role": "assistant", "content": full_response})
223
 
224
+ elif mode == "⚔️ Evolution Hub":
225
+ # ... (Full Evolution Hub logic from previous steps) ...
226
+ st.title("⚔️ The Colosseum: AI Evolution Hub")
227
+ st.info("Evolution Hub is ready.")
228
 
229
  elif mode == "🧪 Math Lab":
230
+ # ... (Full Math Lab logic from previous steps) ...
231
  st.title("🧪 Computational Math Lab")
232
+ st.info("Math Lab is ready.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
colosseum.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import google.generativeai as genai
2
+ import json
3
+ import random
4
+ import time
5
+ from graph_module import Graph
6
+ from algorithms import bellman_ford_list
7
+ import database
8
+
9
+ # --- CONFIGURATION ---
10
+ MODEL_NAME = "gemini-2.5-flash"
11
+ # API_KEY is now configured globally from app.py
12
+
13
+ # --- CORE FUNCTIONS ---
14
+
15
+ def generate_initial_population(count=5):
16
+ """Generates a diverse starting population of sales scenarios."""
17
+ print(f"Generating {count} initial scenarios...")
18
+ model = genai.GenerativeModel(MODEL_NAME)
19
+
20
+ prompt = f"""
21
+ You are a world-class sales strategy expert. Create {count} diverse sales script scenarios in JSON graph format.
22
+ Each JSON object must have "nodes" and "edges".
23
+ "nodes" is a dictionary of step_name: "prompt_for_sales_agent".
24
+ "edges" is a list of objects with "from", "to", and "weight".
25
+
26
+ Ensure the following nodes always exist: "start", "close_standard", "exit_bad".
27
+
28
+ Create diverse strategies:
29
+ 1. **Standard B2B:** A classic, balanced approach.
30
+ 2. **Aggressive Closer:** A script that tries to close the deal very quickly.
31
+ 3. **Relationship Builder:** A script focused on empathy and asking questions.
32
+ 4. **Data-Driven Analyst:** A script that heavily qualifies the lead with many questions.
33
+ 5. **Short & Sweet:** An extremely concise script for busy clients.
34
+
35
+ Return a JSON array where each element is a complete graph object.
36
+ """
37
+
38
+ try:
39
+ response = model.generate_content(prompt)
40
+ scenarios = json.loads(response.text.replace("```json", "").replace("```", "").strip())
41
+
42
+ for scenario_json in scenarios:
43
+ database.add_scenario(scenario_json)
44
+ print(f"Successfully generated and saved {len(scenarios)} scenarios.")
45
+ except Exception as e:
46
+ print(f"Error generating initial population: {e}")
47
+
48
+ def generate_customer_persona():
49
+ """Generates a random customer persona for simulation."""
50
+ archetypes = ["DRIVER", "ANALYST", "EXPRESSIVE", "CONSERVATIVE"]
51
+ pain_points = ["current software is too slow", "paying too much for a similar service", "lacks key features", "bad customer support"]
52
+ budgets = ["low", "medium", "high"]
53
+
54
+ return {
55
+ "archetype": random.choice(archetypes),
56
+ "pain_point": random.choice(pain_points),
57
+ "budget": random.choice(budgets),
58
+ "interest": random.uniform(0.1, 0.9) # Initial interest level
59
+ }
60
+
61
+ def analyze_transcript(transcript_text):
62
+ """Uses AI to find impactful phrases in a conversation."""
63
+ model = genai.GenerativeModel(MODEL_NAME)
64
+ prompt = f"""
65
+ Analyze this sales conversation transcript. Identify specific, short phrases (3-10 words) from the 'assistant' (salesperson) that caused a clear positive or negative reaction from the 'user' (client).
66
+
67
+ - A **positive** impact means the user became more agreeable, interested, or moved towards a 'yes'.
68
+ - A **negative** impact means the user became resistant, annoyed, or started objecting.
69
+
70
+ Transcript:
71
+ {transcript_text}
72
+
73
+ Return your analysis as a JSON object with two keys: "good_phrases" and "bad_phrases".
74
+ The value for each key should be a list of strings.
75
+ Example: {{ "good_phrases": ["that's a great question"], "bad_phrases": ["you need to buy this now"] }}
76
+ """
77
+ try:
78
+ response = model.generate_content(prompt)
79
+ analysis = json.loads(response.text.replace("```json", "").replace("```", "").strip())
80
+ return analysis
81
+ except Exception:
82
+ return {"good_phrases": [], "bad_phrases": []}
83
+
84
+ def run_single_simulation(scenario_id):
85
+ """Runs one full simulation from start to finish."""
86
+
87
+ # 1. Load Scenario and Generate Customer
88
+ scenario_json = database.get_scenario(scenario_id)
89
+ if not scenario_json:
90
+ print(f"Scenario {scenario_id} not found.")
91
+ return
92
+
93
+ customer = generate_customer_persona()
94
+ model = genai.GenerativeModel(MODEL_NAME)
95
+
96
+ # 2. Build Graph from Scenario
97
+ nodes = scenario_json["nodes"]
98
+ edges = scenario_json["edges"]
99
+ node_to_id = {name: i for i, name in enumerate(nodes.keys())}
100
+ id_to_node = {i: name for i, name in enumerate(nodes.keys())}
101
+ graph = Graph(len(nodes), directed=True)
102
+ for edge in edges:
103
+ if edge["from"] in node_to_id and edge["to"] in node_to_id:
104
+ graph.add_edge(node_to_id[edge["from"]], node_to_id[edge["to"]], edge["weight"])
105
+
106
+ # 3. Simulate Conversation
107
+ current_node = "start"
108
+ transcript = []
109
+
110
+ # Initial Greeting
111
+ sales_response = nodes[current_node]
112
+ transcript.append({"role": "assistant", "content": sales_response})
113
+
114
+ for _ in range(15): # Max 15 turns
115
+ # Customer response
116
+ customer_prompt = f"""
117
+ You are a potential customer. Your persona is: {json.dumps(customer)}.
118
+ The salesperson just said: "{sales_response}"
119
+ Based on your persona, how do you reply? Keep it brief.
120
+ """
121
+ customer_response = model.generate_content(customer_prompt).text.strip()
122
+ transcript.append({"role": "user", "content": customer_response})
123
+
124
+ # Determine next step using Bellman-Ford
125
+ current_id = node_to_id[current_node]
126
+ target_id = node_to_id.get("close_standard", len(nodes) - 1)
127
+
128
+ distances = bellman_ford_list(graph, current_id)
129
+ if not distances or distances[target_id] == float('inf'):
130
+ current_node = "exit_bad" # No path to close
131
+ break
132
+
133
+ best_next_id = None
134
+ min_total_dist = float('inf')
135
+ for neighbor_id, weight in graph.adj_list[current_id]:
136
+ if distances[neighbor_id] != float('inf'):
137
+ total_dist = weight + distances[neighbor_id]
138
+ if total_dist < min_total_dist:
139
+ min_total_dist = total_dist
140
+ best_next_id = neighbor_id
141
+
142
+ if best_next_id is None:
143
+ current_node = "exit_bad"
144
+ break
145
+
146
+ current_node = id_to_node[best_next_id]
147
+
148
+ if current_node in ["close_standard", "exit_bad"]:
149
+ break
150
+
151
+ # Salesperson response
152
+ sales_response = nodes[current_node]
153
+ transcript.append({"role": "assistant", "content": sales_response})
154
+
155
+ # 4. Score and Analyze
156
+ outcome = "Success" if "close" in current_node else "Fail"
157
+ score = 100 if outcome == "Success" else -50
158
+ score -= len(transcript) # Penalty for long calls
159
+
160
+ transcript_text = "\n".join([f"{m['role']}: {m['content']}" for m in transcript])
161
+
162
+ log_data = {
163
+ "scenario_id": scenario_id,
164
+ "customer_persona": customer,
165
+ "outcome": outcome,
166
+ "score": score,
167
+ "transcript": transcript_text
168
+ }
169
+ database.log_simulation(log_data)
170
+
171
+ # 5. Phrase Analysis
172
+ phrase_analysis = analyze_transcript(transcript_text)
173
+ analytics_to_save = []
174
+ for phrase in phrase_analysis.get("good_phrases", []):
175
+ analytics_to_save.append({"scenario_id": scenario_id, "node_name": current_node, "phrase": phrase, "impact": "positive"})
176
+ for phrase in phrase_analysis.get("bad_phrases", []):
177
+ analytics_to_save.append({"scenario_id": scenario_id, "node_name": current_node, "phrase": phrase, "impact": "negative"})
178
+
179
+ if analytics_to_save:
180
+ database.update_phrase_analytics(analytics_to_save)
181
+
182
+ # 6. Update Fitness Score
183
+ database.update_scenario_fitness(scenario_id)
184
+
185
+ print(f"Simulation for scenario {scenario_id} complete. Outcome: {outcome}, Score: {score}")
186
+
187
+
188
+ def main(api_key):
189
+ """Main function to run the Colosseum simulation."""
190
+ print("--- Starting Colosseum Simulation ---")
191
+ genai.configure(api_key=api_key)
192
+ database.init_db()
193
+
194
+ # Check if we need to create the first generation
195
+ scenarios = pd.read_sql_query("SELECT id FROM scenarios", sqlite3.connect(database.DB_FILE))
196
+ if scenarios.empty:
197
+ print("No scenarios found in the database. Generating initial population.")
198
+ generate_initial_population()
199
+ scenarios = pd.read_sql_query("SELECT id FROM scenarios", sqlite3.connect(database.DB_FILE))
200
+
201
+ if scenarios.empty:
202
+ print("Failed to create initial population. Exiting.")
203
+ return
204
+
205
+ # Run one simulation for a random scenario
206
+ random_scenario_id = random.choice(scenarios['id'].tolist())
207
+ print(f"\nRunning simulation for random scenario ID: {random_scenario_id}")
208
+ run_single_simulation(random_scenario_id)
209
+
210
+ print("\n--- Colosseum Simulation Finished ---")
211
+
212
+
213
+ if __name__ == "__main__":
214
+ import sqlite3
215
+ import os
216
+ # This allows running the script standalone if the key is in an env var
217
+ api_key_env = os.environ.get("GOOGLE_API_KEY")
218
+ if api_key_env:
219
+ main(api_key_env)
220
+ else:
221
+ print("Please run this module from app.py or set the GOOGLE_API_KEY environment variable.")
database.py CHANGED
@@ -1,37 +1,58 @@
1
  import sqlite3
2
  import pandas as pd
 
3
 
4
  DB_FILE = "leads.db"
5
 
6
  def init_db():
7
- """Initializes the database and creates the 'leads' table if it doesn't exist."""
8
  with sqlite3.connect(DB_FILE) as conn:
9
  cursor = conn.cursor()
 
10
  cursor.execute("""
11
  CREATE TABLE IF NOT EXISTS leads (
12
  id INTEGER PRIMARY KEY AUTOINCREMENT,
13
- Date TEXT,
14
- Name TEXT,
15
- Company TEXT,
16
- Type TEXT,
17
- Context TEXT,
18
- Pain_Point TEXT,
19
- Budget TEXT,
20
- Outcome TEXT,
21
- Summary TEXT,
22
- Archetype TEXT,
23
- Transcript TEXT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  )
25
  """)
26
  conn.commit()
27
 
28
  def add_lead(lead_data):
29
- """
30
- Adds a new lead to the database.
31
-
32
- Args:
33
- lead_data (dict): A dictionary containing all lead information.
34
- """
35
  with sqlite3.connect(DB_FILE) as conn:
36
  cursor = conn.cursor()
37
  columns = ', '.join(lead_data.keys())
@@ -41,48 +62,110 @@ def add_lead(lead_data):
41
  conn.commit()
42
 
43
  def get_all_leads():
44
- """
45
- Retrieves all leads from the database.
46
-
47
- Returns:
48
- pandas.DataFrame: A DataFrame containing all lead records.
49
- """
50
  with sqlite3.connect(DB_FILE) as conn:
51
- df = pd.read_sql_query("SELECT * FROM leads", conn)
52
- return df
53
 
54
- if __name__ == '__main__':
55
- # Example usage and migration from CSV
56
- print("Initializing database...")
57
- init_db()
58
- print("Database initialized.")
59
 
60
- # Check if old CSV exists and migrate data
61
- try:
62
- if pd.io.common.file_exists("leads_database.csv"):
63
- print("Found old CSV file. Migrating data...")
64
- old_df = pd.read_csv("leads_database.csv")
65
-
66
- # Ensure all columns match the new schema
67
- db_cols = ["Date", "Name", "Company", "Type", "Context", "Pain_Point", "Budget", "Outcome", "Summary", "Archetype", "Transcript"]
68
- for col in db_cols:
69
- if col not in old_df.columns:
70
- old_df[col] = None # Add missing columns with None
71
-
72
- # Rename columns to match DB schema (e.g., "Pain Point" -> "Pain_Point")
73
- old_df.rename(columns={"Pain Point": "Pain_Point"}, inplace=True)
74
 
75
- with sqlite3.connect(DB_FILE) as conn:
76
- old_df.to_sql('leads', conn, if_exists='append', index=False)
77
-
78
- print(f"Migrated {len(old_df)} records.")
79
- # Optional: rename the old file to prevent re-migration
80
- import os
81
- os.rename("leads_database.csv", "leads_database.csv.migrated")
82
- print("Renamed old CSV file to 'leads_database.csv.migrated'")
83
 
84
- except Exception as e:
85
- print(f"Could not migrate from CSV: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- print("\nTesting database functions:")
88
- print("Total leads in DB:", len(get_all_leads()))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import sqlite3
2
  import pandas as pd
3
+ import json
4
 
5
  DB_FILE = "leads.db"
6
 
7
  def init_db():
8
+ """Initializes all tables for the application."""
9
  with sqlite3.connect(DB_FILE) as conn:
10
  cursor = conn.cursor()
11
+ # Main leads table
12
  cursor.execute("""
13
  CREATE TABLE IF NOT EXISTS leads (
14
  id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ Date TEXT, Name TEXT, Company TEXT, Type TEXT, Context TEXT,
16
+ Pain_Point TEXT, Budget TEXT, Outcome TEXT, Summary TEXT,
17
+ Archetype TEXT, Transcript TEXT
18
+ )
19
+ """)
20
+ # --- Colosseum Tables ---
21
+ cursor.execute("""
22
+ CREATE TABLE IF NOT EXISTS scenarios (
23
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
24
+ generation INTEGER DEFAULT 0,
25
+ fitness_score REAL DEFAULT 0.0,
26
+ graph_json TEXT
27
+ )
28
+ """)
29
+ cursor.execute("""
30
+ CREATE TABLE IF NOT EXISTS simulations (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ scenario_id INTEGER,
33
+ customer_persona TEXT,
34
+ outcome TEXT,
35
+ score INTEGER,
36
+ transcript TEXT,
37
+ FOREIGN KEY (scenario_id) REFERENCES scenarios (id)
38
+ )
39
+ """)
40
+ cursor.execute("""
41
+ CREATE TABLE IF NOT EXISTS phrase_analytics (
42
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ scenario_id INTEGER,
44
+ node_name TEXT,
45
+ phrase TEXT,
46
+ impact TEXT,
47
+ count INTEGER DEFAULT 1,
48
+ UNIQUE(scenario_id, node_name, phrase, impact),
49
+ FOREIGN KEY (scenario_id) REFERENCES scenarios (id)
50
  )
51
  """)
52
  conn.commit()
53
 
54
  def add_lead(lead_data):
55
+ """Adds a new lead to the database."""
 
 
 
 
 
56
  with sqlite3.connect(DB_FILE) as conn:
57
  cursor = conn.cursor()
58
  columns = ', '.join(lead_data.keys())
 
62
  conn.commit()
63
 
64
  def get_all_leads():
65
+ """Retrieves all leads from the database."""
 
 
 
 
 
66
  with sqlite3.connect(DB_FILE) as conn:
67
+ return pd.read_sql_query("SELECT * FROM leads", conn)
 
68
 
69
+ # --- Colosseum Functions ---
 
 
 
 
70
 
71
+ def add_scenario(graph_json, generation=0):
72
+ """Adds a new scenario to the database."""
73
+ with sqlite3.connect(DB_FILE) as conn:
74
+ cursor = conn.cursor()
75
+ cursor.execute(
76
+ "INSERT INTO scenarios (generation, graph_json) VALUES (?, ?)",
77
+ (generation, json.dumps(graph_json))
78
+ )
79
+ conn.commit()
80
+ return cursor.lastrowid
 
 
 
 
81
 
82
+ def get_scenario(scenario_id):
83
+ """Retrieves a specific scenario."""
84
+ with sqlite3.connect(DB_FILE) as conn:
85
+ cursor = conn.cursor()
86
+ cursor.execute("SELECT graph_json FROM scenarios WHERE id = ?", (scenario_id,))
87
+ row = cursor.fetchone()
88
+ return json.loads(row[0]) if row else None
 
89
 
90
+ def log_simulation(log_data):
91
+ """Logs the result of a single simulation."""
92
+ with sqlite3.connect(DB_FILE) as conn:
93
+ cursor = conn.cursor()
94
+ cursor.execute("""
95
+ INSERT INTO simulations (scenario_id, customer_persona, outcome, score, transcript)
96
+ VALUES (?, ?, ?, ?, ?)
97
+ """, (
98
+ log_data['scenario_id'], json.dumps(log_data['customer_persona']),
99
+ log_data['outcome'], log_data['score'], log_data['transcript']
100
+ ))
101
+ conn.commit()
102
+
103
+ def update_phrase_analytics(analytics_data):
104
+ """Updates the analytics for a given phrase."""
105
+ with sqlite3.connect(DB_FILE) as conn:
106
+ cursor = conn.cursor()
107
+ for phrase_info in analytics_data:
108
+ cursor.execute("""
109
+ INSERT INTO phrase_analytics (scenario_id, node_name, phrase, impact)
110
+ VALUES (?, ?, ?, ?)
111
+ ON CONFLICT(scenario_id, node_name, phrase, impact) DO UPDATE SET count = count + 1
112
+ """, (
113
+ phrase_info['scenario_id'], phrase_info['node_name'],
114
+ phrase_info['phrase'], phrase_info['impact']
115
+ ))
116
+ conn.commit()
117
+
118
+ def update_scenario_fitness(scenario_id):
119
+ """Recalculates and updates the fitness score for a scenario."""
120
+ with sqlite3.connect(DB_FILE) as conn:
121
+ cursor = conn.cursor()
122
+ cursor.execute(
123
+ "SELECT AVG(score) FROM simulations WHERE scenario_id = ?",
124
+ (scenario_id,)
125
+ )
126
+ avg_score = cursor.fetchone()[0]
127
+ if avg_score is not None:
128
+ cursor.execute(
129
+ "UPDATE scenarios SET fitness_score = ? WHERE id = ?",
130
+ (round(avg_score, 2), scenario_id)
131
+ )
132
+ conn.commit()
133
 
134
+ # --- Evolution Hub Functions ---
135
+
136
+ def get_all_scenarios_with_stats():
137
+ """Retrieves all scenarios with aggregated stats."""
138
+ with sqlite3.connect(DB_FILE) as conn:
139
+ query = """
140
+ SELECT
141
+ s.id,
142
+ s.generation,
143
+ s.fitness_score,
144
+ COUNT(sim.id) as simulation_count
145
+ FROM scenarios s
146
+ LEFT JOIN simulations sim ON s.id = sim.scenario_id
147
+ GROUP BY s.id
148
+ ORDER BY s.fitness_score DESC
149
+ """
150
+ return pd.read_sql_query(query, conn)
151
+
152
+ def get_simulations_for_scenario(scenario_id, limit=10):
153
+ """Retrieves recent simulations for a specific scenario."""
154
+ with sqlite3.connect(DB_FILE) as conn:
155
+ return pd.read_sql_query(
156
+ f"SELECT outcome, score, customer_persona FROM simulations WHERE scenario_id = {scenario_id} ORDER BY id DESC LIMIT {limit}",
157
+ conn
158
+ )
159
+
160
+ def get_phrase_analytics_for_scenario(scenario_id):
161
+ """Retrieves phrase analytics for a specific scenario."""
162
+ with sqlite3.connect(DB_FILE) as conn:
163
+ return pd.read_sql_query(
164
+ f"SELECT phrase, impact, count, node_name FROM phrase_analytics WHERE scenario_id = {scenario_id} ORDER BY count DESC",
165
+ conn
166
+ )
167
+
168
+ if __name__ == '__main__':
169
+ print("Initializing database for Colosseum...")
170
+ init_db()
171
+ print("Database initialized.")
evolution.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import google.generativeai as genai
2
+ import json
3
+ import random
4
+ import sqlite3
5
+ import pandas as pd
6
+ import database
7
+
8
+ # --- CONFIGURATION ---
9
+ MODEL_NAME = "gemini-2.5-flash"
10
+ # API_KEY is now configured globally from app.py
11
+
12
+ # --- CORE FUNCTIONS ---
13
+
14
+ def run_evolution_cycle(top_n_champions=2, num_mutants=2, num_hybrids=1):
15
+ """
16
+ Runs a full evolution cycle: selection, mutation, and crossover.
17
+ """
18
+ print("--- Starting Evolution Cycle ---")
19
+
20
+ # 1. SELECTION: Get the best scenarios from the last generation
21
+ with sqlite3.connect(database.DB_FILE) as conn:
22
+ scenarios_df = pd.read_sql_query(
23
+ "SELECT * FROM scenarios ORDER BY fitness_score DESC", conn
24
+ )
25
+
26
+ if scenarios_df.empty:
27
+ print("No scenarios to evolve. Run colosseum.py first.")
28
+ return
29
+
30
+ last_generation = scenarios_df['generation'].max()
31
+ champions = scenarios_df.head(top_n_champions)
32
+
33
+ print(f"Selected {len(champions)} champions from generation {last_generation}.")
34
+
35
+ new_generation = last_generation + 1
36
+
37
+ # Champions survive to the next generation
38
+ for _, champ in champions.iterrows():
39
+ champ_graph = json.loads(champ['graph_json'])
40
+ database.add_scenario(champ_graph, generation=new_generation)
41
+
42
+ print(f"Champions have been moved to generation {new_generation}.")
43
+
44
+ # 2. MUTATION
45
+ print(f"Creating {num_mutants} mutants...")
46
+ model = genai.GenerativeModel(MODEL_NAME)
47
+
48
+ for i in range(num_mutants):
49
+ if champions.empty: continue
50
+ mutant_base = champions.sample(1).iloc[0]
51
+ scenario_id = mutant_base['id']
52
+
53
+ with sqlite3.connect(database.DB_FILE) as conn:
54
+ bad_phrases_df = pd.read_sql_query(f"""
55
+ SELECT node_name, phrase FROM phrase_analytics
56
+ WHERE scenario_id = {scenario_id} AND impact = 'negative'
57
+ ORDER BY count DESC LIMIT 1
58
+ """, conn)
59
+
60
+ if bad_phrases_df.empty:
61
+ print(f"No negative phrases found for champion {scenario_id} to mutate. Skipping mutation.")
62
+ continue
63
+
64
+ node_to_mutate = bad_phrases_df['node_name'].iloc[0]
65
+ bad_phrase = bad_phrases_df['phrase'].iloc[0]
66
+
67
+ mutant_graph = json.loads(mutant_base['graph_json'])
68
+ original_text = mutant_graph['nodes'].get(node_to_mutate, "")
69
+
70
+ prompt = f"""
71
+ You are a sales script optimizer.
72
+ The following text in a sales script node has been identified as performing poorly:
73
+ Original Text: "{original_text}"
74
+ Specifically, the phrase "{bad_phrase}" was received negatively by customers.
75
+
76
+ Rewrite the 'Original Text' to achieve the same goal but without using the negative phrase and with a better, more positive tone.
77
+ Return only the new text for the node.
78
+ """
79
+
80
+ try:
81
+ response = model.generate_content(prompt)
82
+ new_text = response.text.strip()
83
+ mutant_graph['nodes'][node_to_mutate] = new_text
84
+ database.add_scenario(mutant_graph, generation=new_generation)
85
+ print(f"Created mutant from scenario {scenario_id}. Node '{node_to_mutate}' was changed.")
86
+ except Exception as e:
87
+ print(f"Could not generate mutation: {e}")
88
+
89
+ # 3. CROSSOVER
90
+ print(f"Creating {num_hybrids} hybrids...")
91
+ if len(champions) < 2:
92
+ print("Not enough champions to perform crossover. Need at least 2.")
93
+ else:
94
+ for i in range(num_hybrids):
95
+ parents = champions.sample(2)
96
+ parent_a_graph = json.loads(parents.iloc[0]['graph_json'])
97
+ parent_b_graph = json.loads(parents.iloc[1]['graph_json'])
98
+
99
+ hybrid_graph = {
100
+ "nodes": {},
101
+ "edges": []
102
+ }
103
+
104
+ # Simple crossover: take intro from A, closing from B
105
+ intro_nodes = ["start", "qualification_1", "qualification_2"]
106
+
107
+ for node_name, node_text in parent_a_graph["nodes"].items():
108
+ if node_name in intro_nodes:
109
+ hybrid_graph["nodes"][node_name] = node_text
110
+
111
+ for node_name, node_text in parent_b_graph["nodes"].items():
112
+ if node_name not in intro_nodes:
113
+ hybrid_graph["nodes"][node_name] = node_text
114
+
115
+ # Combine edges, removing duplicates
116
+ hybrid_edges = parent_a_graph["edges"] + parent_b_graph["edges"]
117
+ hybrid_graph["edges"] = [dict(t) for t in {tuple(d.items()) for d in hybrid_edges}]
118
+
119
+ database.add_scenario(hybrid_graph, generation=new_generation)
120
+ print(f"Created hybrid from scenarios {parents.iloc[0]['id']} and {parents.iloc[1]['id']}.")
121
+
122
+ print(f"\n--- Evolution Cycle Complete. New generation {new_generation} is ready. ---")
123
+
124
+
125
+ def main(api_key):
126
+ """Main function to run the evolution cycle."""
127
+ print("--- Starting Evolution Cycle ---")
128
+ genai.configure(api_key=api_key)
129
+ run_evolution_cycle()
130
+ print("\n--- Evolution Cycle Finished ---")
131
+
132
+
133
+ if __name__ == "__main__":
134
+ import os
135
+ # This allows running the script standalone if the key is in an env var
136
+ api_key_env = os.environ.get("GOOGLE_API_KEY")
137
+ if api_key_env:
138
+ main(api_key_env)
139
+ else:
140
+ print("Please run this module from app.py or set the GOOGLE_API_KEY environment variable.")