Romanchello-bit commited on
Commit
d32f83a
·
1 Parent(s): ebaa6d1

Migrate lead storage to SQLite and enhance analytics

Browse files

Replaces CSV and Google Sheets lead storage with a new SQLite-based database module. Updates lead saving and analytics logic in app.py and leads_manager.py to use the new database. Adds archetype analytics and product info scraping features to the dashboard. Refactors sentiment-based graph weighting in sellme_pro.py for clarity and efficiency.

Files changed (4) hide show
  1. app.py +136 -87
  2. database.py +88 -0
  3. leads_manager.py +18 -81
  4. sellme_pro.py +52 -128
app.py CHANGED
@@ -9,19 +9,22 @@ 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
  import experiments
 
 
 
13
 
14
  # --- CONFIG ---
15
  st.set_page_config(layout="wide", page_title="SellMe AI Engine")
16
  MODEL_NAME = "gemini-2.5-flash"
17
- LEADS_FILE = "leads_database.csv"
18
 
19
  # --- SESSION STATE INIT ---
20
  if "page" not in st.session_state: st.session_state.page = "dashboard"
21
  if "messages" not in st.session_state: st.session_state.messages = []
22
  if "current_node" not in st.session_state: st.session_state.current_node = "start"
23
  if "lead_info" not in st.session_state: st.session_state.lead_info = {}
24
- if "product_info" not in st.session_state: st.session_state.product_info = {} # NEW: Product Context
25
  if "visited_history" not in st.session_state: st.session_state.visited_history = []
26
  if "current_archetype" not in st.session_state: st.session_state.current_archetype = "UNKNOWN"
27
  if "reasoning" not in st.session_state: st.session_state.reasoning = ""
@@ -35,58 +38,15 @@ if "checklist" not in st.session_state:
35
  "Experiment/Revise": False
36
  }
37
 
38
- # --- DATA MANAGER ---
39
- def init_db():
40
- if not os.path.exists(LEADS_FILE):
41
- df = pd.DataFrame(columns=[
42
- "Date", "Name", "Company", "Type", "Context",
43
- "Pain Point", "Budget", "Outcome", "Summary"
44
- ])
45
- df.to_csv(LEADS_FILE, index=False)
46
-
47
- def save_lead_to_db(lead_info, chat_history, outcome):
48
- init_db()
49
- model = genai.GenerativeModel(MODEL_NAME)
50
- chat_text = "\n".join([f"{m['role']}: {m['content']}" for m in chat_history])
51
-
52
- prompt = f"""
53
- Analyze this sales conversation:
54
- {chat_text}
55
-
56
- Extract these fields in JSON format:
57
- - pain_point: What is the client's main problem?
58
- - budget: Did they mention money/price sensitivity?
59
- - summary: 1 sentence summary of the call.
60
- """
61
- try:
62
- response = model.generate_content(prompt)
63
- ai_data = response.text
64
- except:
65
- ai_data = "AI Extraction Failed"
66
-
67
- new_row = {
68
- "Date": datetime.now().strftime("%Y-%m-%d %H:%M"),
69
- "Name": lead_info.get("name"),
70
- "Company": lead_info.get("company"),
71
- "Type": lead_info.get("type"),
72
- "Context": lead_info.get("context"),
73
- "Pain Point": "AI Analysis Pending",
74
- "Budget": "Unknown",
75
- "Outcome": outcome,
76
- "Summary": f"Call with {len(chat_history)} messages. {outcome}"
77
- }
78
-
79
- df = pd.read_csv(LEADS_FILE)
80
- df = pd.concat([df, pd.DataFrame([new_row])], ignore_index=True)
81
- df.to_csv(LEADS_FILE, index=False)
82
-
83
  # --- AI & GRAPH LOGIC ---
 
84
  def configure_genai(api_key):
85
  try:
86
  genai.configure(api_key=api_key)
87
  return True
88
  except: return False
89
 
 
90
  def load_graph_data():
91
  script_file = "sales_script_learned.json" if os.path.exists("sales_script_learned.json") else "sales_script.json"
92
  with open(script_file, "r", encoding="utf-8") as f: data = json.load(f)
@@ -152,9 +112,6 @@ def analyze_full_context(model, user_input, current_node, chat_history):
152
  return {"archetype": "UNKNOWN", "intent": "STAY", "reasoning": "Fallback safety"}
153
 
154
  def generate_response(model, instruction_text, user_input, intent, lead_info, archetype, product_info={}):
155
- """
156
- Generates a generic or product-specific response.
157
- """
158
  bot_name = lead_info.get('bot_name', 'Олексій')
159
  client_name = lead_info.get('name', 'Клієнт')
160
  company = lead_info.get('company', 'Компанія')
@@ -169,7 +126,6 @@ def generate_response(model, instruction_text, user_input, intent, lead_info, ar
169
  length_instruction = "Keep it concise."
170
  if "Cold" in context: length_instruction = "Extremely short and punchy (Elevator Pitch)."
171
 
172
- # NEW: Product Context Injection
173
  product_context = ""
174
  if product_info:
175
  product_context = f"""
@@ -215,7 +171,6 @@ def generate_greeting(model, start_instruction, lead_info, product_info={}):
215
  client_name = lead_info.get('name', 'Client')
216
  context = lead_info.get('context', 'Cold')
217
 
218
- # NEW: Product Context Injection
219
  product_context = ""
220
  if product_info:
221
  product_context = f"""
@@ -311,7 +266,63 @@ def draw_graph(graph_data, current_node, predicted_path):
311
  dot.edge(e["from"], e["to"], color=color, penwidth=pen)
312
  return dot
313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  # --- MAIN APP ---
 
315
  st.sidebar.title("🛠️ SellMe Control")
316
  mode = st.sidebar.radio("Mode", ["🤖 Sales Bot CRM", "🧪 Math Lab"])
317
 
@@ -350,7 +361,19 @@ if mode == "🤖 Sales Bot CRM":
350
  c2.metric("Success Rate", f"{stats['success_rate']}%")
351
  c3.metric("AI Learning Iterations", "v1.2")
352
  st.divider()
 
 
 
 
 
 
 
 
 
 
 
353
 
 
354
  st.subheader("🕵️ Call Inspector")
355
  options = data.apply(lambda x: f"{x['Date']} | {x['Name']} ({x['Outcome']})", axis=1).tolist()
356
  selected_option = st.selectbox("Select a call to review:", options)
@@ -370,42 +393,43 @@ if mode == "🤖 Sales Bot CRM":
370
  st.title("👤 Налаштування Дзвінка")
371
  with st.form("lead_form"):
372
  c1, c2 = st.columns(2)
373
- # Lead Info
374
- c1.markdown("### 👨‍💼 Lead Info")
375
- bot_name = c1.text_input("Ваше ім'я (Менеджера)", "Олексій")
376
- name = c1.text_input("Ім'я Клієнта", "Олександр")
377
- company = c1.text_input("Компанія", "SoftServe")
378
- type_ = c1.selectbox("Тип бізнесу", ["B2B", "B2C"])
379
- context = c1.selectbox("Контекст", ["Холодний дзвінок", "Теплий лід", "Повторний дзвінок"])
380
-
381
- # NEW: Product Info
382
- c2.markdown("### 📦 Product / Service Info")
383
- p_name = c2.text_input("Product Name", "AI Sales Engine")
384
- p_value = c2.text_input("Main Benefit (Value)", "Increases sales by 300%")
385
- p_price = c2.text_input("Price / Pricing Model", "$100/month")
386
- p_diff = c2.text_input("Competitive Edge (Why us?)", "Learns from every call")
387
-
388
- if c1.checkbox("🔍 Перевірити в базі"):
389
- try:
390
- from leads_manager import connect_to_gsheet
391
- sheet = connect_to_gsheet()
392
- if sheet:
393
- records = sheet.get_all_records()
394
- found = [r for r in records if str(r['Name']).lower() == name.lower()]
395
- if found:
396
- last = found[-1]
397
- st.info(f"📜 Contact Found: {last['Date']}")
398
- context = "Повторний дзвінок"
399
- else: st.success(" New Client")
400
- except: st.error("Database unavailable.")
401
-
 
 
402
  submitted = st.form_submit_button("🚀 Start Call")
403
  if submitted:
404
  st.session_state.lead_info = {
405
  "bot_name": bot_name, "name": name,
406
  "company": company, "type": type_, "context": context
407
  }
408
- # Store Product Info
409
  st.session_state.product_info = {
410
  "product_name": p_name,
411
  "product_value": p_value,
@@ -473,7 +497,6 @@ if mode == "🤖 Sales Bot CRM":
473
 
474
  if not st.session_state.messages:
475
  with st.spinner("AI warming up..."):
476
- # Pass Product Info
477
  greeting = generate_greeting(model, nodes["start"], st.session_state.lead_info, st.session_state.product_info)
478
  st.session_state.messages.append({"role": "assistant", "content": greeting})
479
  st.rerun()
@@ -488,11 +511,24 @@ if mode == "🤖 Sales Bot CRM":
488
 
489
  if "EXIT" in intent:
490
  outcome = "Success" if "close" in st.session_state.current_node else "Fail"
491
- save_lead_to_db(st.session_state.lead_info, st.session_state.messages, outcome)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
  st.success("Call Saved!")
493
  st.session_state.page = "dashboard"; st.rerun()
494
  elif "STAY" in intent:
495
- # Pass Product Info
496
  resp = generate_response(model, current_text, user_input, "STAY", st.session_state.lead_info, archetype, st.session_state.product_info)
497
  else: # MOVE
498
  if st.session_state.current_node not in st.session_state.visited_history:
@@ -504,11 +540,24 @@ if mode == "🤖 Sales Bot CRM":
504
  if best_next is not None:
505
  st.session_state.current_node = id_to_node[best_next]
506
  new_text = nodes[st.session_state.current_node]
507
- # Pass Product Info
508
  resp = generate_response(model, new_text, user_input, "MOVE", st.session_state.lead_info, archetype, st.session_state.product_info)
509
  else:
510
  resp = "Call finished."
511
- save_lead_to_db(st.session_state.lead_info, st.session_state.messages, "End of Script")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
 
513
  st.session_state.messages.append({"role": "assistant", "content": resp})
514
  st.rerun()
 
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
16
+ from bs4 import BeautifulSoup
17
 
18
  # --- CONFIG ---
19
  st.set_page_config(layout="wide", page_title="SellMe AI Engine")
20
  MODEL_NAME = "gemini-2.5-flash"
 
21
 
22
  # --- SESSION STATE INIT ---
23
  if "page" not in st.session_state: st.session_state.page = "dashboard"
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 = ""
 
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)
 
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', 'Компанія')
 
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"""
 
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"""
 
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
 
 
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)
 
393
  st.title("👤 Налаштування Дзвінка")
394
  with st.form("lead_form"):
395
  c1, c2 = st.columns(2)
396
+ with c1:
397
+ st.markdown("### 👨‍💼 Lead Info")
398
+ bot_name = st.text_input("Ваше ім'я (Менеджера)", "Олексій")
399
+ name = st.text_input("Ім'я Клієнта", "Олександр")
400
+ company = st.text_input("Компанія", "SoftServe")
401
+ type_ = st.selectbox("Тип бізнесу", ["B2B", "B2C"])
402
+ context = st.selectbox("Контекст", ["Холодний дзвінок", "Теплий лід", "Повторний дзвінок"])
403
+ if st.checkbox("🔍 Перевірити в базі"):
404
+ pass
405
+
406
+ with c2:
407
+ st.markdown("### 📦 Product / Service Info")
408
+ url = st.text_input("Product URL", placeholder="https://example.com/product")
409
+
410
+ if st.button("🤖 Fetch Product Info from URL"):
411
+ if url:
412
+ with st.spinner("Fetching and analyzing URL..."):
413
+ scraped_info = scrape_and_summarize(url, model)
414
+ if scraped_info:
415
+ st.session_state.product_info = scraped_info
416
+ st.success("Product info populated!")
417
+ else:
418
+ st.error("Failed to get product info from URL.")
419
+ else:
420
+ st.warning("Please enter a URL.")
421
+
422
+ p_name = st.text_input("Product Name", value=st.session_state.product_info.get("product_name", ""))
423
+ p_value = st.text_input("Main Benefit (Value)", value=st.session_state.product_info.get("product_value", ""))
424
+ p_price = st.text_input("Price / Pricing Model", value=st.session_state.product_info.get("product_price", ""))
425
+ p_diff = st.text_input("Competitive Edge", value=st.session_state.product_info.get("competitor_diff", ""))
426
+
427
  submitted = st.form_submit_button("🚀 Start Call")
428
  if submitted:
429
  st.session_state.lead_info = {
430
  "bot_name": bot_name, "name": name,
431
  "company": company, "type": type_, "context": context
432
  }
 
433
  st.session_state.product_info = {
434
  "product_name": p_name,
435
  "product_value": p_value,
 
497
 
498
  if not st.session_state.messages:
499
  with st.spinner("AI warming up..."):
 
500
  greeting = generate_greeting(model, nodes["start"], st.session_state.lead_info, st.session_state.product_info)
501
  st.session_state.messages.append({"role": "assistant", "content": greeting})
502
  st.rerun()
 
511
 
512
  if "EXIT" in intent:
513
  outcome = "Success" if "close" in st.session_state.current_node else "Fail"
514
+ transcript = "\n".join([f"{m['role']}: {m['content']}" for m in st.session_state.messages])
515
+ lead_data = {
516
+ "Date": datetime.now().strftime("%Y-%m-%d %H:%M"),
517
+ "Name": st.session_state.lead_info.get("name"),
518
+ "Company": st.session_state.lead_info.get("company"),
519
+ "Type": st.session_state.lead_info.get("type"),
520
+ "Context": st.session_state.lead_info.get("context"),
521
+ "Pain_Point": "AI Analysis Pending",
522
+ "Budget": "Unknown",
523
+ "Outcome": outcome,
524
+ "Summary": f"Call with {len(st.session_state.messages)} messages. {outcome}",
525
+ "Archetype": st.session_state.current_archetype,
526
+ "Transcript": transcript
527
+ }
528
+ add_lead(lead_data)
529
  st.success("Call Saved!")
530
  st.session_state.page = "dashboard"; st.rerun()
531
  elif "STAY" in intent:
 
532
  resp = generate_response(model, current_text, user_input, "STAY", st.session_state.lead_info, archetype, st.session_state.product_info)
533
  else: # MOVE
534
  if st.session_state.current_node not in st.session_state.visited_history:
 
540
  if best_next is not None:
541
  st.session_state.current_node = id_to_node[best_next]
542
  new_text = nodes[st.session_state.current_node]
 
543
  resp = generate_response(model, new_text, user_input, "MOVE", st.session_state.lead_info, archetype, st.session_state.product_info)
544
  else:
545
  resp = "Call finished."
546
+ transcript = "\n".join([f"{m['role']}: {m['content']}" for m in st.session_state.messages])
547
+ lead_data = {
548
+ "Date": datetime.now().strftime("%Y-%m-%d %H:%M"),
549
+ "Name": st.session_state.lead_info.get("name"),
550
+ "Company": st.session_state.lead_info.get("company"),
551
+ "Type": st.session_state.lead_info.get("type"),
552
+ "Context": st.session_state.lead_info.get("context"),
553
+ "Pain_Point": "AI Analysis Pending",
554
+ "Budget": "Unknown",
555
+ "Outcome": "End of Script",
556
+ "Summary": f"Call with {len(st.session_state.messages)} messages. End of Script",
557
+ "Archetype": st.session_state.current_archetype,
558
+ "Transcript": transcript
559
+ }
560
+ add_lead(lead_data)
561
 
562
  st.session_state.messages.append({"role": "assistant", "content": resp})
563
  st.rerun()
database.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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())
38
+ placeholders = ', '.join(['?'] * len(lead_data))
39
+ sql = f"INSERT INTO leads ({columns}) VALUES ({placeholders})"
40
+ cursor.execute(sql, tuple(lead_data.values()))
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()))
leads_manager.py CHANGED
@@ -1,98 +1,35 @@
1
- import streamlit as st
2
  import pandas as pd
3
- import gspread
4
- from oauth2client.service_account import ServiceAccountCredentials
5
- from datetime import datetime
6
 
7
- # Назва твоєї таблиці в Google Sheets (має співпадати буква в букву!)
8
- SHEET_NAME = "SellMe_Leads"
9
-
10
- def connect_to_gsheet():
11
- """Підключення до Google Sheets через Secrets"""
12
- try:
13
- # Створюємо об'єкт облікових даних із секретів Streamlit
14
- # Streamlit автоматично конвертує TOML секцію [gcp_service_account] у словник
15
- if "gcp_service_account" not in st.secrets:
16
- return None
17
-
18
- creds_dict = dict(st.secrets["gcp_service_account"])
19
-
20
- # Виправляємо переноси рядків у приватному ключі (часта проблема при копіюванні)
21
- creds_dict["private_key"] = creds_dict["private_key"].replace("\\n", "\n")
22
-
23
- scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
24
- creds = ServiceAccountCredentials.from_json_keyfile_dict(creds_dict, scope)
25
- client = gspread.authorize(creds)
26
-
27
- # Відкриваємо таблицю
28
- sheet = client.open(SHEET_NAME).sheet1
29
- return sheet
30
- except Exception as e:
31
- # st.error(f"❌ Помилка підключення до Google Sheets: {e}")
32
- # При кожному рерані може бути помилка якщо немає секретів, краще тихо
33
- return None
34
-
35
- def save_lead_to_db(lead_info, chat_history, outcome):
36
- """Зберігає ліда в Google Таблицю"""
37
- sheet = connect_to_gsheet()
38
- if not sheet:
39
- return # Якщо немає зв'язку, виходимо
40
-
41
- # Якщо таблиця порожня, додамо заголовки
42
- try:
43
- if not sheet.get_all_values():
44
- sheet.append_row([
45
- "Date", "Name", "Company", "Type", "Context",
46
- "Pain Point", "Budget", "Outcome", "Transcript", "AI Insights"
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"),
57
- lead_info.get("name", "-"),
58
- lead_info.get("company", "-"),
59
- lead_info.get("type", "-"),
60
- lead_info.get("context", "-"),
61
- "AI Pending", # Тут можна додати AI аналіз
62
- "Unknown",
63
- outcome,
64
- transcript,
65
- "" # AI Insights placeholder
66
- ]
67
 
68
- # Додаємо рядок
69
- sheet.append_row(row)
70
- print("✅ Дані збережено в Google Sheets!")
71
-
72
- def get_analytics():
73
- """Читає дані з Google Таблиці для дашборду"""
74
- sheet = connect_to_gsheet()
75
- if not sheet:
76
- return None, None
77
-
78
- # Отримуємо всі записи
79
  try:
80
- data = sheet.get_all_records()
81
- df = pd.DataFrame(data)
82
 
83
- if df.empty:
84
  return None, None
85
 
86
  stats = {
87
  "total": len(df),
88
  "success_rate": 0,
89
- "top_fail_reasons": None
90
  }
91
 
92
  if "Outcome" in df.columns and not df.empty:
93
- success_count = len(df[df["Outcome"] == "Success"])
94
- stats["success_rate"] = round(success_count / len(df) * 100, 1)
 
 
 
95
 
96
  return df, stats
97
- except:
 
98
  return None, None
 
 
1
  import pandas as pd
2
+ from database import get_all_leads, init_db
 
 
3
 
4
+ def get_analytics():
5
+ """
6
+ Reads data from the SQLite database for the dashboard.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
+ Returns:
9
+ A tuple of (DataFrame, dict) containing the data and statistics.
10
+ """
11
+ # Ensure the database is initialized
12
+ init_db()
 
 
 
 
 
 
 
13
 
 
 
 
 
 
 
 
 
 
 
 
14
  try:
15
+ df = get_all_leads()
 
16
 
17
+ if df is None or df.empty:
18
  return None, None
19
 
20
  stats = {
21
  "total": len(df),
22
  "success_rate": 0,
 
23
  }
24
 
25
  if "Outcome" in df.columns and not df.empty:
26
+ # Filter out non-success/fail outcomes for accurate rate calculation
27
+ relevant_outcomes = df[df["Outcome"].isin(["Success", "Fail"])]
28
+ if not relevant_outcomes.empty:
29
+ success_count = len(relevant_outcomes[relevant_outcomes["Outcome"] == "Success"])
30
+ stats["success_rate"] = round(success_count / len(relevant_outcomes) * 100, 1)
31
 
32
  return df, stats
33
+ except Exception as e:
34
+ print(f"Error getting analytics from database: {e}")
35
  return None, None
sellme_pro.py CHANGED
@@ -30,11 +30,8 @@ Return only the number, nothing else:"""
30
  try:
31
  response = model.generate_content(prompt)
32
  sentiment_text = response.text.strip()
33
- # Extract number from response
34
  sentiment_score = float(sentiment_text)
35
- # Clamp to [-1, 1] range
36
- sentiment_score = max(-1.0, min(1.0, sentiment_score))
37
- return sentiment_score
38
  except Exception as e:
39
  print(f"[WARNING] Sentiment analysis failed: {e}. Defaulting to neutral (0.0)")
40
  return 0.0
@@ -43,116 +40,73 @@ Return only the number, nothing else:"""
43
  def update_weights(graph, str_to_int, original_edges, sentiment_score):
44
  """
45
  Dynamically update graph edge weights based on user sentiment.
46
-
47
- Args:
48
- graph: The Graph object
49
- str_to_int: Mapping from string node names to integer IDs
50
- original_edges: Original edge data from JSON
51
- sentiment_score: Float from -1 to +1
52
  """
53
- # Strategy mapping
54
  close_deal_id = str_to_int.get('close_deal')
55
  discount_offer_id = str_to_int.get('discount_offer')
56
  exit_bad_id = str_to_int.get('exit_bad')
57
  pitch_crm_id = str_to_int.get('pitch_crm')
58
  pitch_no_crm_id = str_to_int.get('pitch_no_crm')
59
 
60
- # Rebuild graph with adjusted weights
 
61
  for edge in original_edges:
62
- from_id = str_to_int[edge['from']]
63
- to_id = str_to_int[edge['to']]
64
- original_weight = edge['weight']
65
- adjusted_weight = original_weight
66
-
67
- # Negative sentiment (< -0.3): Customer is unhappy/frustrated
68
- if sentiment_score < -0.3:
69
- # INCREASE weights for aggressive moves (hard selling is bad now)
70
- if to_id == close_deal_id or to_id in [pitch_crm_id, pitch_no_crm_id]:
71
- adjusted_weight = original_weight * 2.0 # Make these paths less attractive
72
-
73
- # DECREASE weights for relationship-saving moves
74
- if to_id == discount_offer_id or to_id == exit_bad_id:
75
- adjusted_weight = original_weight * 0.5 # Make these paths more attractive
76
-
77
- # Positive sentiment (> 0.3): Customer is happy/interested
78
- elif sentiment_score > 0.3:
79
- # DECREASE weights for close_deal (strike while iron is hot!)
80
- if to_id == close_deal_id:
81
- adjusted_weight = original_weight * 0.3 # Make closing much more attractive
82
 
83
- # Also boost pitch effectiveness when customer is positive
84
- if to_id in [pitch_crm_id, pitch_no_crm_id]:
85
- adjusted_weight = original_weight * 0.7 # Make pitches more attractive
86
-
87
- # Update the graph (we need to rebuild adjacency structures)
88
- # Since Graph doesn't have update_edge, we'll handle this in the main loop
89
- graph.adj_matrix[from_id][to_id] = adjusted_weight
90
-
91
- # Update adjacency list
92
- for i, (neighbor, _) in enumerate(graph.adj_list[from_id]):
93
- if neighbor == to_id:
94
- graph.adj_list[from_id][i] = (neighbor, adjusted_weight)
95
- break
96
 
97
 
98
  def main():
99
- # Configuration: Get API Key from user
100
  print("=" * 60)
101
  print("SellMe PRO - Dynamic Sentiment-Based Sales AI")
102
  print("=" * 60)
103
  api_key = input("\nEnter your Gemini API Key: ").strip()
104
 
105
- # Configure Gemini
106
  genai.configure(api_key=api_key)
107
  model = genai.GenerativeModel('gemini-2.5-flash')
108
 
109
  print("\n[INFO] Gemini configured successfully!")
110
  print("[INFO] Loading sales script...\n")
111
 
112
- # Load sales_script.json
113
  with open('sales_script.json', 'r', encoding='utf-8') as f:
114
  data = json.load(f)
115
 
116
  nodes_data = data['nodes']
117
  edges_data = data['edges']
118
 
119
- # Create mappings: string IDs <-> integer IDs
120
- str_to_int = {}
121
- int_to_str = {}
122
 
123
- for idx, node_name in enumerate(nodes_data.keys()):
124
- str_to_int[node_name] = idx
125
- int_to_str[idx] = node_name
126
-
127
- # Build Graph object
128
  num_nodes = len(nodes_data)
129
  graph = Graph(num_nodes, directed=True)
130
-
131
- # Add edges with original weights
132
  for edge in edges_data:
133
- from_node = str_to_int[edge['from']]
134
- to_node = str_to_int[edge['to']]
135
- weight = edge['weight']
136
- graph.add_edge(from_node, to_node, weight)
137
 
138
  print("[INFO] Sales graph built successfully!")
139
  print(f"[INFO] Nodes: {num_nodes}, Edges: {len(edges_data)}")
140
  print("[INFO] Sentiment-based dynamic weighting enabled!\n")
141
- print("=" * 60)
142
- print("Starting Sales Conversation")
143
- print("=" * 60)
144
- print("(Type 'quit' to exit)\n")
145
 
146
- # Start conversation
147
  current_step = "start"
148
  conversation_count = 0
149
- max_steps = 20 # Safety limit
150
 
151
  while current_step not in ["close_deal", "exit_bad"] and conversation_count < max_steps:
152
- # Get current node ID
153
  current_id = str_to_int[current_step]
154
 
155
- # Get user input first
156
  print(f"\n[CURRENT STEP: {current_step}]")
157
  user_input = input("\nYou (Client): ").strip()
158
 
@@ -160,111 +114,81 @@ def main():
160
  print("\n[INFO] Exiting demo. Goodbye!")
161
  break
162
 
163
- # === SENTIMENT ANALYSIS ===
164
  print("\n[AI is analyzing sentiment...]")
165
  sentiment_score = get_sentiment(user_input, model)
166
-
167
- # Determine sentiment category
168
- if sentiment_score < -0.3:
169
- sentiment_label = "NEGATIVE"
170
- elif sentiment_score > 0.3:
171
- sentiment_label = "POSITIVE"
172
- else:
173
- sentiment_label = "NEUTRAL"
174
-
175
  print(f">>> Detected Sentiment: {sentiment_score:.2f} [{sentiment_label}]")
176
 
177
- # === DYNAMIC WEIGHT UPDATE ===
178
  if abs(sentiment_score) > 0.3:
179
  print(">>> Strategy Changed! Adjusting conversation path...")
180
  update_weights(graph, str_to_int, edges_data, sentiment_score)
181
 
182
- # Calculate best path with updated weights
183
- distances = bellman_ford_list(graph, current_id)
184
-
185
- if distances is None:
186
- print("[ERROR] Negative cycle detected in sales graph!")
187
- break
188
-
189
- # Get close_deal node ID
190
  close_deal_id = str_to_int['close_deal']
191
-
192
- # Find the best immediate next step
193
- adj_list = graph.get_list()
194
- neighbors = adj_list[current_id]
195
-
196
- if not neighbors:
197
- print(f"[ERROR] No path forward from '{current_step}'")
198
- break
199
-
200
- # Pick the neighbor with shortest total distance to close_deal
201
  best_next_id = None
202
- best_total_distance = float('inf')
203
-
204
- for neighbor_id, edge_weight in neighbors:
205
- # Run Bellman-Ford from this neighbor to find distance to close_deal
206
- neighbor_distances = bellman_ford_list(graph, neighbor_id)
207
- if neighbor_distances and neighbor_distances[close_deal_id] != float('inf'):
208
- total_distance = edge_weight + neighbor_distances[close_deal_id]
209
- if total_distance < best_total_distance:
210
- best_total_distance = total_distance
211
- best_next_id = neighbor_id
212
 
 
 
 
 
 
 
213
  if best_next_id is None:
214
  print(f"[ERROR] No path found from '{current_step}' to 'close_deal'")
215
  break
216
 
217
- # Get next step name and script text
218
  next_step_name = int_to_str[best_next_id]
219
  script_text = nodes_data[next_step_name]
220
 
221
  print(f"[NEXT TARGET: {next_step_name}]")
222
 
223
- # Create prompt for Gemini
224
  prompt = f"""You are a professional sales representative for SellMe, an AI sales assistant platform.
225
-
226
  Your goal is to move the conversation toward this step: '{next_step_name}'.
227
  The sales script for this step says: '{script_text}'.
228
  The client just said: '{user_input}'.
229
  Client sentiment: {sentiment_score:.2f} ({sentiment_label})
230
-
231
  Generate a natural, conversational response in Ukrainian that:
232
  1. Acknowledges what the client said and their emotional state
233
  2. Smoothly guides toward the script message
234
  3. Adjusts tone based on sentiment (softer if negative, enthusiastic if positive)
235
  4. Sounds human and friendly, not robotic
236
  5. Keep it brief (1-2 sentences max)
237
-
238
  Response:"""
239
 
240
- # Get Gemini's response
241
  print("\n[AI is generating response...]")
242
  try:
243
  response = model.generate_content(prompt)
244
- ai_response = response.text.strip()
245
-
246
- print(f"\nSellMe AI: {ai_response}")
247
-
248
  except Exception as e:
249
- print(f"\n[ERROR] Gemini API error: {e}")
250
- print(f"[FALLBACK] Using script: {script_text}")
251
 
252
- # Move to next step
253
  current_step = next_step_name
254
  conversation_count += 1
255
 
256
- # End of conversation
257
  print("\n" + "=" * 60)
258
  if current_step == "close_deal":
259
- print("[SUCCESS] Deal closed!")
260
- print(f"Final message: {nodes_data[current_step]}")
261
  elif current_step == "exit_bad":
262
- print("[EXIT] Client not interested.")
263
- print(f"Final message: {nodes_data[current_step]}")
264
  else:
265
  print(f"[INFO] Conversation ended at step: {current_step}")
266
  print("=" * 60)
267
 
268
-
269
  if __name__ == "__main__":
270
  main()
 
30
  try:
31
  response = model.generate_content(prompt)
32
  sentiment_text = response.text.strip()
 
33
  sentiment_score = float(sentiment_text)
34
+ return max(-1.0, min(1.0, sentiment_score))
 
 
35
  except Exception as e:
36
  print(f"[WARNING] Sentiment analysis failed: {e}. Defaulting to neutral (0.0)")
37
  return 0.0
 
40
  def update_weights(graph, str_to_int, original_edges, sentiment_score):
41
  """
42
  Dynamically update graph edge weights based on user sentiment.
 
 
 
 
 
 
43
  """
 
44
  close_deal_id = str_to_int.get('close_deal')
45
  discount_offer_id = str_to_int.get('discount_offer')
46
  exit_bad_id = str_to_int.get('exit_bad')
47
  pitch_crm_id = str_to_int.get('pitch_crm')
48
  pitch_no_crm_id = str_to_int.get('pitch_no_crm')
49
 
50
+ # Reset graph to original weights before applying sentiment
51
+ graph.adj_list = [[] for _ in range(graph.num_vertices)]
52
  for edge in original_edges:
53
+ graph.add_edge(str_to_int[edge['from']], str_to_int[edge['to']], edge['weight'])
54
+
55
+ for from_id in range(graph.num_vertices):
56
+ for i, (to_id, original_weight) in enumerate(graph.adj_list[from_id]):
57
+ adjusted_weight = original_weight
58
+ if sentiment_score < -0.3:
59
+ if to_id in [close_deal_id, pitch_crm_id, pitch_no_crm_id]:
60
+ adjusted_weight *= 2.0
61
+ elif to_id in [discount_offer_id, exit_bad_id]:
62
+ adjusted_weight *= 0.5
63
+ elif sentiment_score > 0.3:
64
+ if to_id == close_deal_id:
65
+ adjusted_weight *= 0.3
66
+ elif to_id in [pitch_crm_id, pitch_no_crm_id]:
67
+ adjusted_weight *= 0.7
 
 
 
 
 
68
 
69
+ graph.adj_list[from_id][i] = (to_id, adjusted_weight)
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
 
72
  def main():
 
73
  print("=" * 60)
74
  print("SellMe PRO - Dynamic Sentiment-Based Sales AI")
75
  print("=" * 60)
76
  api_key = input("\nEnter your Gemini API Key: ").strip()
77
 
 
78
  genai.configure(api_key=api_key)
79
  model = genai.GenerativeModel('gemini-2.5-flash')
80
 
81
  print("\n[INFO] Gemini configured successfully!")
82
  print("[INFO] Loading sales script...\n")
83
 
 
84
  with open('sales_script.json', 'r', encoding='utf-8') as f:
85
  data = json.load(f)
86
 
87
  nodes_data = data['nodes']
88
  edges_data = data['edges']
89
 
90
+ str_to_int = {name: i for i, name in enumerate(nodes_data.keys())}
91
+ int_to_str = {i: name for i, name in enumerate(nodes_data.keys())}
 
92
 
 
 
 
 
 
93
  num_nodes = len(nodes_data)
94
  graph = Graph(num_nodes, directed=True)
 
 
95
  for edge in edges_data:
96
+ graph.add_edge(str_to_int[edge['from']], str_to_int[edge['to']], edge['weight'])
 
 
 
97
 
98
  print("[INFO] Sales graph built successfully!")
99
  print(f"[INFO] Nodes: {num_nodes}, Edges: {len(edges_data)}")
100
  print("[INFO] Sentiment-based dynamic weighting enabled!\n")
101
+ print("=" * 60, "\nStarting Sales Conversation\n(Type 'quit' to exit)\n", "=" * 60)
 
 
 
102
 
 
103
  current_step = "start"
104
  conversation_count = 0
105
+ max_steps = 20
106
 
107
  while current_step not in ["close_deal", "exit_bad"] and conversation_count < max_steps:
 
108
  current_id = str_to_int[current_step]
109
 
 
110
  print(f"\n[CURRENT STEP: {current_step}]")
111
  user_input = input("\nYou (Client): ").strip()
112
 
 
114
  print("\n[INFO] Exiting demo. Goodbye!")
115
  break
116
 
 
117
  print("\n[AI is analyzing sentiment...]")
118
  sentiment_score = get_sentiment(user_input, model)
119
+ sentiment_label = "NEUTRAL"
120
+ if sentiment_score < -0.3: sentiment_label = "NEGATIVE"
121
+ elif sentiment_score > 0.3: sentiment_label = "POSITIVE"
 
 
 
 
 
 
122
  print(f">>> Detected Sentiment: {sentiment_score:.2f} [{sentiment_label}]")
123
 
 
124
  if abs(sentiment_score) > 0.3:
125
  print(">>> Strategy Changed! Adjusting conversation path...")
126
  update_weights(graph, str_to_int, edges_data, sentiment_score)
127
 
128
+ # --- OPTIMIZED PATHFINDING ---
129
+ # Run Bellman-Ford from every node to the destination (close_deal)
130
+ # This is inefficient but required if weights change dynamically.
131
+ # A better approach for dynamic graphs is D* Lite, but Bellman-Ford is what we have.
132
+ # We calculate all-pairs shortest paths to the destination.
 
 
 
133
  close_deal_id = str_to_int['close_deal']
134
+ dist_to_target = {i: float('inf') for i in range(num_nodes)}
135
+
136
+ # This is still not optimal, but better than calling BF in a loop
137
+ # For a truly optimal solution, one would reverse the graph edges and run BF once from the target.
138
+ for i in range(num_nodes):
139
+ distances = bellman_ford_list(graph, i)
140
+ if distances:
141
+ dist_to_target[i] = distances[close_deal_id]
142
+
 
143
  best_next_id = None
144
+ min_total_dist = float('inf')
 
 
 
 
 
 
 
 
 
145
 
146
+ for neighbor_id, weight in graph.adj_list[current_id]:
147
+ total_dist = weight + dist_to_target.get(neighbor_id, float('inf'))
148
+ if total_dist < min_total_dist:
149
+ min_total_dist = total_dist
150
+ best_next_id = neighbor_id
151
+
152
  if best_next_id is None:
153
  print(f"[ERROR] No path found from '{current_step}' to 'close_deal'")
154
  break
155
 
 
156
  next_step_name = int_to_str[best_next_id]
157
  script_text = nodes_data[next_step_name]
158
 
159
  print(f"[NEXT TARGET: {next_step_name}]")
160
 
 
161
  prompt = f"""You are a professional sales representative for SellMe, an AI sales assistant platform.
 
162
  Your goal is to move the conversation toward this step: '{next_step_name}'.
163
  The sales script for this step says: '{script_text}'.
164
  The client just said: '{user_input}'.
165
  Client sentiment: {sentiment_score:.2f} ({sentiment_label})
 
166
  Generate a natural, conversational response in Ukrainian that:
167
  1. Acknowledges what the client said and their emotional state
168
  2. Smoothly guides toward the script message
169
  3. Adjusts tone based on sentiment (softer if negative, enthusiastic if positive)
170
  4. Sounds human and friendly, not robotic
171
  5. Keep it brief (1-2 sentences max)
 
172
  Response:"""
173
 
 
174
  print("\n[AI is generating response...]")
175
  try:
176
  response = model.generate_content(prompt)
177
+ print(f"\nSellMe AI: {response.text.strip()}")
 
 
 
178
  except Exception as e:
179
+ print(f"\n[ERROR] Gemini API error: {e}\n[FALLBACK] Using script: {script_text}")
 
180
 
 
181
  current_step = next_step_name
182
  conversation_count += 1
183
 
 
184
  print("\n" + "=" * 60)
185
  if current_step == "close_deal":
186
+ print(f"[SUCCESS] Deal closed! Final message: {nodes_data[current_step]}")
 
187
  elif current_step == "exit_bad":
188
+ print(f"[EXIT] Client not interested. Final message: {nodes_data[current_step]}")
 
189
  else:
190
  print(f"[INFO] Conversation ended at step: {current_step}")
191
  print("=" * 60)
192
 
 
193
  if __name__ == "__main__":
194
  main()