Spaces:
Sleeping
Sleeping
Commit
·
f41fb66
1
Parent(s):
7b43e9f
sellme_v1
Browse files- algorithms.py +41 -21
- app.py +366 -0
- leads.csv +1 -0
- leads_database.csv +1 -0
- leads_manager.py +51 -0
- packages.txt +1 -0
- requirements.txt +4 -0
- sales_script.json +105 -24
algorithms.py
CHANGED
|
@@ -1,26 +1,46 @@
|
|
| 1 |
-
def bellman_ford_list(graph, start_node):
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
adj_list = graph.get_list()
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
#
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 5 |
-
"
|
| 6 |
-
"
|
| 7 |
-
"
|
| 8 |
-
"
|
| 9 |
-
"
|
| 10 |
-
"
|
| 11 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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": "
|
| 27 |
"weight": 2
|
| 28 |
},
|
| 29 |
{
|
| 30 |
"from": "qualification",
|
| 31 |
-
"to": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
"weight": 2
|
| 33 |
},
|
| 34 |
{
|
| 35 |
-
"from": "
|
| 36 |
-
"to": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
"weight": 2
|
| 38 |
},
|
| 39 |
{
|
| 40 |
-
"from": "
|
| 41 |
-
"to": "
|
| 42 |
"weight": 3
|
| 43 |
},
|
| 44 |
{
|
| 45 |
-
"from": "
|
| 46 |
-
"to": "
|
| 47 |
-
"weight":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
},
|
| 49 |
{
|
| 50 |
-
"from": "
|
| 51 |
"to": "close_deal",
|
| 52 |
"weight": 10
|
| 53 |
},
|
| 54 |
{
|
| 55 |
-
"from": "
|
| 56 |
-
"to": "
|
| 57 |
-
"weight":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
},
|
| 59 |
{
|
| 60 |
"from": "objection_expensive",
|
| 61 |
-
"to": "
|
| 62 |
"weight": 1
|
| 63 |
},
|
| 64 |
{
|
| 65 |
-
"from": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
}
|