Romanchello-bit commited on
Commit
f41fb66
·
1 Parent(s): 7b43e9f
Files changed (8) hide show
  1. algorithms.py +41 -21
  2. app.py +366 -0
  3. leads.csv +1 -0
  4. leads_database.csv +1 -0
  5. leads_manager.py +51 -0
  6. packages.txt +1 -0
  7. requirements.txt +4 -0
  8. sales_script.json +105 -24
algorithms.py CHANGED
@@ -1,26 +1,46 @@
1
- def bellman_ford_list(graph, start_node):
2
- distances = {i: float('inf') for i in range(graph.num_vertices)}
3
- distances[start_node] = 0
4
- adj_list = graph.get_list()
5
 
6
- # Relaxation steps
7
- for _ in range(graph.num_vertices - 1):
8
- changed = False
9
- for u in adj_list:
10
- for v, weight in adj_list[u]:
11
- if distances[u] != float('inf') and distances[u] + weight < distances[v]:
12
- distances[v] = distances[u] + weight
13
- changed = True
14
- if not changed:
15
- break
16
-
17
- # Negative cycle detection
18
- for u in adj_list:
19
- for v, weight in adj_list[u]:
20
- if distances[u] != float('inf') and distances[u] + weight < distances[v]:
21
- return None # Negative cycle detected
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- return distances
24
 
25
  def bellman_ford_matrix(graph, start_node):
26
  distances = {i: float('inf') for i in range(graph.num_vertices)}
 
1
+ def bellman_ford_list(graph, start_node, visited_nodes=None, client_type="B2B"):
2
+ """
3
+ Advanced Bellman-Ford Algorithm.
 
4
 
5
+ Features:
6
+ 1. Dynamic Weights based on Client Type (B2B prefers logic, B2C prefers speed).
7
+ 2. Penalty for re-visiting nodes (avoid loops).
8
+ """
9
+
10
+ # Ініціалізація
11
+ num_vertices = graph.num_vertices
12
+ dist = [float("inf")] * num_vertices
13
+ dist[start_node] = 0
14
+
15
+ # Визначаємо множники ваг
16
+ # B2B любить деталі (знижуємо ціну довгих етапів), B2C любить швидкість
17
+ type_modifier = {
18
+ "B2B": {"logic": 0.8, "emotion": 1.2, "speed": 1.0},
19
+ "B2C": {"logic": 1.5, "emotion": 0.7, "speed": 0.5}
20
+ }
21
+ modifiers = type_modifier.get(client_type, {"logic": 1.0, "emotion": 1.0, "speed": 1.0})
22
+
23
+ # Основний цикл релаксації
24
+ for _ in range(num_vertices - 1):
25
+ for u in range(num_vertices):
26
+ for v, weight in graph.adj_list[u]:
27
+
28
+ # --- ПОКРАЩЕННЯ 1: Динамічна вага ---
29
+ # Тут можна було б перевіряти тип ребра, якби він був у графі.
30
+ # Поки що просто емулюємо:
31
+ current_weight = weight
32
+
33
+ # --- ПОКРАЩЕННЯ 2: Штраф за повторення ---
34
+ if visited_nodes and v in visited_nodes:
35
+ current_weight *= 50 # Величезний штраф, щоб не йти назад
36
+
37
+ # Релаксація
38
+ if dist[u] != float("inf") and dist[u] + current_weight < dist[v]:
39
+ dist[v] = dist[u] + current_weight
40
+
41
+ # Перевірка на негативні цикли (опціонально, в продажах їх зазвичай немає)
42
+ return dist
43
 
 
44
 
45
  def bellman_ford_matrix(graph, start_node):
46
  distances = {i: float('inf') for i in range(graph.num_vertices)}
app.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import graphviz
3
+ import json
4
+ import os
5
+ import pandas as pd
6
+ from datetime import datetime
7
+ import google.generativeai as genai
8
+ from graph_module import Graph
9
+ from algorithms import bellman_ford_list
10
+
11
+ # --- CONFIG ---
12
+ st.set_page_config(layout="wide", page_title="SellMe AI Engine")
13
+ MODEL_NAME = "gemini-2.5-flash"
14
+ LEADS_FILE = "leads_database.csv"
15
+
16
+ # --- SESSION STATE INIT ---
17
+ if "page" not in st.session_state: st.session_state.page = "dashboard"
18
+ if "messages" not in st.session_state: st.session_state.messages = []
19
+ if "current_node" not in st.session_state: st.session_state.current_node = "start"
20
+ if "lead_info" not in st.session_state: st.session_state.lead_info = {}
21
+ if "visited_history" not in st.session_state: st.session_state.visited_history = [] # Track visited nodes
22
+ if "current_archetype" not in st.session_state: st.session_state.current_archetype = "UNKNOWN"
23
+ if "reasoning" not in st.session_state: st.session_state.reasoning = ""
24
+ # Checklist status based on your screenshot
25
+ if "checklist" not in st.session_state:
26
+ st.session_state.checklist = {
27
+ "Identify Customer": False,
28
+ "Determine Objectives": False,
29
+ "Outline Advantages": False,
30
+ "Keep it Brief": True, # Always try to be brief
31
+ "Experiment/Revise": False
32
+ }
33
+
34
+ # --- DATA MANAGER ---
35
+ def init_db():
36
+ if not os.path.exists(LEADS_FILE):
37
+ df = pd.DataFrame(columns=[
38
+ "Date", "Name", "Company", "Type", "Context",
39
+ "Pain Point", "Budget", "Outcome", "Summary"
40
+ ])
41
+ df.to_csv(LEADS_FILE, index=False)
42
+
43
+ def save_lead_to_db(lead_info, chat_history, outcome):
44
+ init_db()
45
+ # Ask AI to extract structured data from chat
46
+ model = genai.GenerativeModel(MODEL_NAME)
47
+ chat_text = "\n".join([f"{m['role']}: {m['content']}" for m in chat_history])
48
+
49
+ prompt = f"""
50
+ Analyze this sales conversation:
51
+ {chat_text}
52
+
53
+ Extract these fields in JSON format:
54
+ - pain_point: What is the client's main problem?
55
+ - budget: Did they mention money/price sensitivity?
56
+ - summary: 1 sentence summary of the call.
57
+ """
58
+ try:
59
+ response = model.generate_content(prompt)
60
+ # Simple parsing (in production use structured output)
61
+ ai_data = response.text
62
+ except:
63
+ ai_data = "AI Extraction Failed"
64
+
65
+ new_row = {
66
+ "Date": datetime.now().strftime("%Y-%m-%d %H:%M"),
67
+ "Name": lead_info.get("name"),
68
+ "Company": lead_info.get("company"),
69
+ "Type": lead_info.get("type"),
70
+ "Context": lead_info.get("context"),
71
+ "Pain Point": "AI Analysis Pending", # Placeholder for simplicity
72
+ "Budget": "Unknown",
73
+ "Outcome": outcome,
74
+ "Summary": f"Call with {len(chat_history)} messages. {outcome}"
75
+ }
76
+
77
+ df = pd.read_csv(LEADS_FILE)
78
+ df = pd.concat([df, pd.DataFrame([new_row])], ignore_index=True)
79
+ df.to_csv(LEADS_FILE, index=False)
80
+
81
+ # --- AI & GRAPH LOGIC ---
82
+ def configure_genai(api_key):
83
+ try:
84
+ genai.configure(api_key=api_key)
85
+ return True
86
+ except: return False
87
+
88
+ def load_graph_data():
89
+ script_file = "sales_script_learned.json" if os.path.exists("sales_script_learned.json") else "sales_script.json"
90
+ with open(script_file, "r", encoding="utf-8") as f: data = json.load(f)
91
+ nodes = data["nodes"]
92
+ edges = data["edges"]
93
+ node_to_id = {name: i for i, name in enumerate(nodes.keys())}
94
+ id_to_node = {i: name for i, name in enumerate(nodes.keys())}
95
+ graph = Graph(len(nodes), directed=True)
96
+ for edge in edges:
97
+ if edge["from"] in node_to_id and edge["to"] in node_to_id:
98
+ graph.add_edge(node_to_id[edge["from"]], node_to_id[edge["to"]], edge["weight"])
99
+ return graph, node_to_id, id_to_node, nodes, edges
100
+
101
+ def get_predicted_path(graph, start_id, target_id, id_to_node, node_to_id):
102
+ # Get visited node IDs and client type for smart pathfinding
103
+ visited_ids = [node_to_id[n] for n in st.session_state.get('visited_history', []) if n in node_to_id]
104
+ client_type = st.session_state.lead_info.get('type', 'B2B')
105
+
106
+ # Use enhanced Bellman-Ford with penalties
107
+ dist = bellman_ford_list(graph, start_id, visited_nodes=visited_ids, client_type=client_type)
108
+ if dist[target_id] == float('inf'): return []
109
+ path = [target_id]
110
+ curr = target_id
111
+ while curr != start_id:
112
+ found = False
113
+ for u in range(graph.num_vertices):
114
+ for v, w in graph.adj_list[u]:
115
+ if v == curr and dist[v] == dist[u] + w:
116
+ path.append(u); curr = u; found = True; break
117
+ if found: break
118
+ if not found: break
119
+ return [id_to_node[i] for i in reversed(path)]
120
+
121
+ def analyze_full_context(model, user_input, current_node, chat_history):
122
+ """
123
+ Аналізує Інтент, Емоції та ПСИХОТИП клієнта.
124
+ """
125
+ history_text = "\n".join([f"{m['role']}: {m['content']}" for m in chat_history[-4:]]) # Беремо останні 4 фрази для контексту
126
+
127
+ prompt = f"""
128
+ ROLE: Behavioral Psychologist & Sales Expert.
129
+
130
+ CONTEXT:
131
+ Current Step: "{current_node}"
132
+ Recent Chat History:
133
+ {history_text}
134
+ User just said: "{user_input}"
135
+
136
+ TASK 1: Detect User Archetype (Pattern). Choose ONE:
137
+ - DRIVER (Direct, impatient, results-oriented)
138
+ - ANALYST (Detail-oriented, asks 'how', skeptical)
139
+ - EXPRESSIVE (Emotional, enthusiastic, visionary)
140
+ - CONSERVATIVE (Risk-averse, slow, likes stability)
141
+
142
+ TASK 2: Analyze Intent (MOVE, STAY, EXIT).
143
+
144
+ OUTPUT JSON format:
145
+ {{
146
+ "archetype": "DRIVER" | "ANALYST" | "EXPRESSIVE" | "CONSERVATIVE",
147
+ "intent": "MOVE" | "STAY" | "EXIT",
148
+ "reasoning": "Why you chose this archetype (1 short sentence)"
149
+ }}
150
+ """
151
+ try:
152
+ response = model.generate_content(prompt)
153
+ clean_text = response.text.replace("```json", "").replace("```", "").strip()
154
+ return json.loads(clean_text)
155
+ except:
156
+ return {"archetype": "UNKNOWN", "intent": "STAY", "reasoning": "Error"}
157
+
158
+ def generate_response(model, context, user_input, intent, lead_info, archetype):
159
+ # Визначаємо стиль спілкування залежно від патерну
160
+ style_instruction = ""
161
+
162
+ if archetype == "DRIVER":
163
+ style_instruction = "STYLE: Ultra-short, confident. Focus on ROI and speed. No fluff. Be direct."
164
+ elif archetype == "ANALYST":
165
+ style_instruction = "STYLE: Logical, detailed. Use facts, numbers, and technical terms. Prove your point."
166
+ elif archetype == "EXPRESSIVE":
167
+ style_instruction = "STYLE: Energetic, inspiring. Use metaphors, exclamation marks. Focus on the 'Future Success'."
168
+ elif archetype == "CONSERVATIVE":
169
+ style_instruction = "STYLE: Calm, supportive, safe. Emphasize low risk, support, and ease of use. Don't push."
170
+ else:
171
+ style_instruction = "STYLE: Professional and polite."
172
+
173
+ if intent == "STAY":
174
+ prompt = f"""
175
+ ROLE: Chameleon Sales Rep.
176
+ ARCHETYPE DETECTED: {archetype} -> {style_instruction}
177
+
178
+ SITUATION: Step "{context}". Client Objected: "{user_input}".
179
+ TASK: Handle objection strictly matching the detected STYLE.
180
+ CONSTRAINT: Speak naturally in Ukrainian. Output ONLY the response.
181
+ """
182
+ else:
183
+ prompt = f"""
184
+ ROLE: Chameleon Sales Rep.
185
+ ARCHETYPE DETECTED: {archetype} -> {style_instruction}
186
+
187
+ GOAL: Transition to "{context}". User said: "{user_input}".
188
+ TASK: Bridge to the next step using the detected STYLE.
189
+ CONSTRAINT: Speak naturally in Ukrainian. Output ONLY the response.
190
+ """
191
+
192
+ try:
193
+ return model.generate_content(prompt).text.strip()
194
+ except: return "..."
195
+
196
+ # --- UI COMPONENTS ---
197
+ def draw_graph(graph_data, current_node, predicted_path):
198
+ nodes = graph_data[3]
199
+ edges = graph_data[4]
200
+ dot = graphviz.Digraph()
201
+ dot.attr(rankdir='LR', splines='ortho')
202
+ dot.attr('node', shape='box', style='rounded,filled', fontname='Arial', fontsize='10')
203
+
204
+ for n in nodes:
205
+ fill = '#F0F2F6'; color = '#BDC3C7'; pen = '1'
206
+ if n == current_node: fill = '#FF4B4B'; color = 'black'; pen = '2'
207
+ elif n in predicted_path: fill = '#FFF8E1'; color = '#F1C40F'
208
+ dot.node(n, label=n, fillcolor=fill, color=color, penwidth=pen)
209
+
210
+ for e in edges:
211
+ color = '#BDC3C7'
212
+ if e["from"] in predicted_path and e["to"] in predicted_path: color = '#F1C40F'
213
+ dot.edge(e["from"], e["to"], color=color)
214
+ return dot
215
+
216
+ # --- MAIN APP ---
217
+ st.sidebar.title("🛠️ SellMe Control")
218
+
219
+ # --- API KEY SETUP (Cloud + Local) ---
220
+ if "GOOGLE_API_KEY" in st.secrets:
221
+ api_key = st.secrets["GOOGLE_API_KEY"]
222
+ else:
223
+ api_key = st.sidebar.text_input("Google API Key", type="password")
224
+
225
+ if st.sidebar.button("📊 Dashboard"): st.session_state.page = "dashboard"; st.rerun()
226
+ if st.sidebar.button("📞 New Call"): st.session_state.page = "setup"; st.rerun()
227
+
228
+ if not api_key:
229
+ st.warning("🔑 Please enter API Key to start.")
230
+ st.stop()
231
+
232
+ configure_genai(api_key)
233
+ model = genai.GenerativeModel(MODEL_NAME)
234
+ graph_data = load_graph_data()
235
+ graph, node_to_id, id_to_node, nodes, edges = graph_data
236
+
237
+ # --- PAGE: DASHBOARD ---
238
+ if st.session_state.page == "dashboard":
239
+ st.title("📊 CRM Analytics")
240
+ init_db()
241
+ df = pd.read_csv(LEADS_FILE)
242
+ if not df.empty:
243
+ c1, c2, c3 = st.columns(3)
244
+ c1.metric("Total Calls", len(df))
245
+ c2.metric("B2B Leads", len(df[df['Type']=='B2B']))
246
+ c3.metric("Success", len(df[df['Outcome']=='Success']))
247
+ st.dataframe(df)
248
+ else: st.info("Database empty.")
249
+
250
+ # --- PAGE: SETUP ---
251
+ elif st.session_state.page == "setup":
252
+ st.title("👤 Lead Setup")
253
+ with st.form("lead_form"):
254
+ c1, c2 = st.columns(2)
255
+ name = c1.text_input("Name", "John Doe")
256
+ company = c2.text_input("Company", "Acme Corp")
257
+ l_type = c1.selectbox("Type", ["B2B", "B2C"])
258
+ context = c2.selectbox("Context", ["Cold Call", "Warm Lead", "Follow-up"])
259
+
260
+ if st.form_submit_button("Start Call"):
261
+ st.session_state.lead_info = {"name": name, "company": company, "type": l_type, "context": context}
262
+ st.session_state.messages = []
263
+ st.session_state.current_node = "start"
264
+ st.session_state.visited_history = [] # Reset visited history
265
+ st.session_state.checklist = {k:False for k in st.session_state.checklist} # Reset
266
+ st.session_state.page = "chat"
267
+ st.rerun()
268
+
269
+ # --- PAGE: CHAT ---
270
+ elif st.session_state.page == "chat":
271
+ st.markdown(f"### Call with {st.session_state.lead_info['name']}")
272
+
273
+ col_chat, col_tools = st.columns([1.5, 1])
274
+
275
+ with col_tools:
276
+ st.markdown("#### 🎯 Call Objectives")
277
+ # Logic to auto-update checklist based on node
278
+ if "qualification" in st.session_state.current_node: st.session_state.checklist["Identify Customer"] = True
279
+ if "pain" in st.session_state.current_node or "shame" in st.session_state.current_node: st.session_state.checklist["Determine Objectives"] = True
280
+ if "pitch" in st.session_state.current_node: st.session_state.checklist["Outline Advantages"] = True
281
+
282
+ for goal, done in st.session_state.checklist.items():
283
+ icon = "✅" if done else "⬜"
284
+ st.write(f"{icon} {goal}")
285
+
286
+ # Display Client Profile (Real-time)
287
+ st.markdown("#### 🧠 Client Profile (Real-time)")
288
+
289
+ # Get current archetype from session
290
+ current_archetype = st.session_state.get("current_archetype", "Analyzing...")
291
+
292
+ # Visual cards for archetypes
293
+ cols = st.columns(4)
294
+
295
+ # Styles for highlighting
296
+ def get_opacity(target): return "1.0" if current_archetype == target else "0.3"
297
+
298
+ cols[0].markdown(f"<div style='opacity:{get_opacity('DRIVER')}; font-size:20px; text-align:center'>🔴<br>Boss</div>", unsafe_allow_html=True)
299
+ cols[1].markdown(f"<div style='opacity:{get_opacity('ANALYST')}; font-size:20px; text-align:center'>🔵<br>Analyst</div>", unsafe_allow_html=True)
300
+ cols[2].markdown(f"<div style='opacity:{get_opacity('EXPRESSIVE')}; font-size:20px; text-align:center'>🟡<br>Fan</div>", unsafe_allow_html=True)
301
+ cols[3].markdown(f"<div style='opacity:{get_opacity('CONSERVATIVE')}; font-size:20px; text-align:center'>🟢<br>Safe</div>", unsafe_allow_html=True)
302
+
303
+ if st.session_state.reasoning:
304
+ st.caption(f"🤖 AI Insight: {st.session_state.reasoning}")
305
+
306
+ st.markdown("#### 📊 AI Strategy")
307
+ curr_id = node_to_id[st.session_state.current_node]
308
+ target_id = node_to_id["close_deal"] # Fixed: using close_deal from sales_script.json
309
+ path = get_predicted_path(graph, curr_id, target_id, id_to_node, node_to_id)
310
+ st.graphviz_chart(draw_graph(graph_data, st.session_state.current_node, path))
311
+
312
+ with col_chat:
313
+ for msg in st.session_state.messages:
314
+ with st.chat_message(msg["role"]): st.write(msg["content"])
315
+
316
+ if not st.session_state.messages:
317
+ greeting = nodes["start"]
318
+ # Adapt greeting based on B2B/B2C
319
+ if st.session_state.lead_info.get('type') == 'B2B':
320
+ greeting = f"Доброго дня, це {st.session_state.lead_info.get('company', 'компанія')}? Мене звати..."
321
+ st.session_state.messages.append({"role": "assistant", "content": greeting})
322
+ st.rerun()
323
+
324
+ if user_input := st.chat_input("Reply..."):
325
+ st.session_state.messages.append({"role": "user", "content": user_input})
326
+
327
+ # Logic - Analyze with full context including archetype detection
328
+ current_text = nodes[st.session_state.current_node]
329
+ analysis = analyze_full_context(model, user_input, st.session_state.current_node, st.session_state.messages)
330
+ intent = analysis.get("intent", "STAY")
331
+ archetype = analysis.get("archetype", "UNKNOWN")
332
+ reasoning = analysis.get("reasoning", "")
333
+
334
+ # Store archetype and reasoning for display
335
+ st.session_state.current_archetype = archetype
336
+ st.session_state.reasoning = reasoning
337
+
338
+ if "EXIT" in intent:
339
+ outcome = "Success" if "close" in st.session_state.current_node else "Fail"
340
+ save_lead_to_db(st.session_state.lead_info, st.session_state.messages, outcome)
341
+ st.success("Call Saved!")
342
+ st.session_state.page = "dashboard"; st.rerun()
343
+
344
+ elif "STAY" in intent:
345
+ resp = generate_response(model, current_text, user_input, "STAY", st.session_state.lead_info, archetype)
346
+
347
+ else: # MOVE
348
+ # Track current node in visited history before moving
349
+ if st.session_state.current_node not in st.session_state.visited_history:
350
+ st.session_state.visited_history.append(st.session_state.current_node)
351
+
352
+ curr_id = node_to_id[st.session_state.current_node]
353
+ best_next = None; min_w = float('inf')
354
+ for n, w in graph.adj_list[curr_id]:
355
+ if w < min_w: min_w = w; best_next = n
356
+
357
+ if best_next is not None:
358
+ st.session_state.current_node = id_to_node[best_next]
359
+ new_text = nodes[st.session_state.current_node]
360
+ resp = generate_response(model, new_text, user_input, "MOVE", st.session_state.lead_info, archetype)
361
+ else:
362
+ resp = "Call finished."
363
+ save_lead_to_db(st.session_state.lead_info, st.session_state.messages, "End of Script")
364
+
365
+ st.session_state.messages.append({"role": "assistant", "content": resp})
366
+ st.rerun()
leads.csv ADDED
@@ -0,0 +1 @@
 
 
1
+ Date,Client Name,Company,Type,Context,Outcome,Final Step,Summary
leads_database.csv ADDED
@@ -0,0 +1 @@
 
 
1
+ Date,Name,Company,Type,Context,Pain Point,Budget,Outcome,Summary
leads_manager.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import os
3
+ from datetime import datetime
4
+
5
+ LEADS_FILE = "leads.csv"
6
+
7
+ def init_db():
8
+ """Створює файл, якщо його немає"""
9
+ if not os.path.exists(LEADS_FILE):
10
+ df = pd.DataFrame(columns=[
11
+ "Date", "Client Name", "Company", "Type", "Context",
12
+ "Outcome", "Final Step", "Summary"
13
+ ])
14
+ df.to_csv(LEADS_FILE, index=False)
15
+
16
+ def save_lead(lead_data, outcome, final_step, chat_history):
17
+ """Зберігає результат розмови"""
18
+ init_db()
19
+
20
+ # Робимо просте самарі (останні 2 повідомлення або статус)
21
+ summary = f"Ended at {final_step}. Total msgs: {len(chat_history)}"
22
+
23
+ new_row = {
24
+ "Date": datetime.now().strftime("%Y-%m-%d %H:%M"),
25
+ "Client Name": lead_data.get("name", "Unknown"),
26
+ "Company": lead_data.get("company", "-"),
27
+ "Type": lead_data.get("type", "B2B"),
28
+ "Context": lead_data.get("context", "Cold Call"),
29
+ "Outcome": outcome,
30
+ "Final Step": final_step,
31
+ "Summary": summary
32
+ }
33
+
34
+ df = pd.read_csv(LEADS_FILE)
35
+ df = pd.concat([df, pd.DataFrame([new_row])], ignore_index=True)
36
+ df.to_csv(LEADS_FILE, index=False)
37
+ return True
38
+
39
+ def get_analytics():
40
+ """Повертає статистику для дашборду"""
41
+ init_db()
42
+ df = pd.read_csv(LEADS_FILE)
43
+ if df.empty:
44
+ return None
45
+
46
+ stats = {
47
+ "total": len(df),
48
+ "success_rate": round(len(df[df["Outcome"] == "Success"]) / len(df) * 100, 1),
49
+ "top_fail_reasons": df[df["Outcome"] == "Fail"]["Final Step"].value_counts().head(3)
50
+ }
51
+ return df, stats
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ graphviz
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ streamlit
2
+ google-generativeai
3
+ pandas
4
+ graphviz
sales_script.json CHANGED
@@ -1,14 +1,20 @@
1
  {
2
  "nodes": {
3
- "start": "Привіт! Це AI-асистент SellMe. Маєте хвилинку?",
4
- "qualification": "Скажіть, ви використовуєте CRM систему?",
5
- "pitch_crm": "Круто! Наш AI інтегрується з вашою CRM і сам заповнює картки.",
6
- "pitch_no_crm": "Зрозумів. Наш AI може працювати навіть без CRM, в Телеграмі.",
7
- "price_question": "Скільки це коштує? - Це залежить від кількості менеджерів.",
8
- "objection_expensive": "Дорого? А скільки ви втрачаєте на незакритих угодах?",
9
- "discount_offer": "Можемо запропонувати тестовий період за 1$.",
10
- "close_deal": "Домовились! Висилаю посилання на оплату.",
11
- "exit_bad": "Добре, вибачте за турботу. Гарного дня."
 
 
 
 
 
 
12
  },
13
  "edges": [
14
  {
@@ -16,55 +22,130 @@
16
  "to": "qualification",
17
  "weight": 1
18
  },
 
 
 
 
 
19
  {
20
  "from": "start",
21
  "to": "exit_bad",
22
  "weight": 100
23
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  {
25
  "from": "qualification",
26
- "to": "pitch_crm",
27
  "weight": 2
28
  },
29
  {
30
  "from": "qualification",
31
- "to": "pitch_no_crm",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  "weight": 2
33
  },
34
  {
35
- "from": "pitch_crm",
36
- "to": "price_question",
 
 
 
 
 
 
 
 
 
 
37
  "weight": 2
38
  },
39
  {
40
- "from": "pitch_no_crm",
41
- "to": "price_question",
42
  "weight": 3
43
  },
44
  {
45
- "from": "price_question",
46
- "to": "objection_expensive",
47
- "weight": 5
 
 
 
 
 
48
  },
49
  {
50
- "from": "price_question",
51
  "to": "close_deal",
52
  "weight": 10
53
  },
54
  {
55
- "from": "objection_expensive",
56
- "to": "exit_bad",
57
- "weight": 50
 
 
 
 
 
58
  },
59
  {
60
  "from": "objection_expensive",
61
- "to": "discount_offer",
62
  "weight": 1
63
  },
64
  {
65
- "from": "discount_offer",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  "to": "close_deal",
67
  "weight": 1
 
 
 
 
 
68
  }
69
  ]
70
  }
 
1
  {
2
  "nodes": {
3
+ "start": "Привіт! Це AI-асистент SellMe. У нас є рішення, що піднімає продажі на 30%. Маєте хвилинку дізнатись як?",
4
+ "objection_busy": "Розумію, часу обмаль. Але це займе всього 30 секунд, а зекономить вам години. Тільки одне питання: як ви зараз шукаєте клієнтів?",
5
+ "qualification": "Скажіть, ви зараз використовуєте якусь CRM чи ведете все в Excel/блокноті?",
6
+ "tech_shame": "Ох, Excel це класика, але ж уявіть, скільки лідів там губиться! А якби система сама нагадувала про кожного клієнта?",
7
+ "tech_praise": "Круто, що у вас є CRM! Але чи заповнюють її менеджери руками? Наш AI робить це автоматично.",
8
+ "pitch_value": "Суть проста: SellMe слухає розмову і сам створює угоду в CRM. Ви економите 2 години в день. Звучить цікаво?",
9
+ "objection_trust": "Звучить надто добре, щоб бути правдою? Розумію. Ми теж так думали, поки не побачили кейс компанії 'SoftGroup', яка скоротила штат вдвічі, зберігши прибуток.",
10
+ "objection_competitor": "А, ви про [Конкурента]? Вони молодці. Але у них ви платите за кожного юзера, а у нас — безліміт. Навіщо переплачувати?",
11
+ "price_reveal": "Вартість всього 50$ на місяць за всю команду. Це дешевше, ніж кава для офісу.",
12
+ "objection_expensive": "50$ — це дорого? Давайте порахуємо. Якщо AI врятує хоча б одну угоду на місяць, він вже окупився в 10 разів. Хіба ні?",
13
+ "objection_think": "Розумію, треба подумати. Але поки ви думаєте, ваші конкуренти вже впроваджують AI. Може, просто спробуємо безкоштовний тиждень?",
14
+ "soft_push": "Дивіться, ви нічим не ризикуєте. Я можу відкрити доступ прямо зараз без картки. Спробуємо?",
15
+ "close_deal": "Чудово! Ви мудрий керівник. Диктуйте пошту, куди скинути доступ.",
16
+ "exit_bad": "Добре. Якщо захочете автоматизувати хаос — ми тут. Гарного дня.",
17
+ "exit_later": "Окей, я наберу вас через місяць, коли вам набридне заповнювати звіти руками. До зв'язку!"
18
  },
19
  "edges": [
20
  {
 
22
  "to": "qualification",
23
  "weight": 1
24
  },
25
+ {
26
+ "from": "start",
27
+ "to": "objection_busy",
28
+ "weight": 5
29
+ },
30
  {
31
  "from": "start",
32
  "to": "exit_bad",
33
  "weight": 100
34
  },
35
+ {
36
+ "from": "objection_busy",
37
+ "to": "qualification",
38
+ "weight": 2
39
+ },
40
+ {
41
+ "from": "objection_busy",
42
+ "to": "exit_later",
43
+ "weight": 10
44
+ },
45
+ {
46
+ "from": "qualification",
47
+ "to": "tech_shame",
48
+ "weight": 2
49
+ },
50
  {
51
  "from": "qualification",
52
+ "to": "tech_praise",
53
  "weight": 2
54
  },
55
  {
56
  "from": "qualification",
57
+ "to": "objection_trust",
58
+ "weight": 5
59
+ },
60
+ {
61
+ "from": "tech_shame",
62
+ "to": "pitch_value",
63
+ "weight": 1
64
+ },
65
+ {
66
+ "from": "tech_praise",
67
+ "to": "pitch_value",
68
+ "weight": 1
69
+ },
70
+ {
71
+ "from": "pitch_value",
72
+ "to": "price_reveal",
73
  "weight": 2
74
  },
75
  {
76
+ "from": "pitch_value",
77
+ "to": "objection_competitor",
78
+ "weight": 4
79
+ },
80
+ {
81
+ "from": "pitch_value",
82
+ "to": "objection_trust",
83
+ "weight": 4
84
+ },
85
+ {
86
+ "from": "objection_trust",
87
+ "to": "pitch_value",
88
  "weight": 2
89
  },
90
  {
91
+ "from": "objection_trust",
92
+ "to": "soft_push",
93
  "weight": 3
94
  },
95
  {
96
+ "from": "objection_competitor",
97
+ "to": "price_reveal",
98
+ "weight": 2
99
+ },
100
+ {
101
+ "from": "objection_competitor",
102
+ "to": "exit_bad",
103
+ "weight": 50
104
  },
105
  {
106
+ "from": "price_reveal",
107
  "to": "close_deal",
108
  "weight": 10
109
  },
110
  {
111
+ "from": "price_reveal",
112
+ "to": "objection_expensive",
113
+ "weight": 3
114
+ },
115
+ {
116
+ "from": "price_reveal",
117
+ "to": "objection_think",
118
+ "weight": 4
119
  },
120
  {
121
  "from": "objection_expensive",
122
+ "to": "soft_push",
123
  "weight": 1
124
  },
125
  {
126
+ "from": "objection_expensive",
127
+ "to": "exit_bad",
128
+ "weight": 20
129
+ },
130
+ {
131
+ "from": "objection_think",
132
+ "to": "soft_push",
133
+ "weight": 2
134
+ },
135
+ {
136
+ "from": "objection_think",
137
+ "to": "exit_later",
138
+ "weight": 5
139
+ },
140
+ {
141
+ "from": "soft_push",
142
  "to": "close_deal",
143
  "weight": 1
144
+ },
145
+ {
146
+ "from": "soft_push",
147
+ "to": "exit_later",
148
+ "weight": 10
149
  }
150
  ]
151
  }