Spaces:
Sleeping
Sleeping
Commit
·
3519f60
1
Parent(s):
4919e19
first big update lmao
Browse files- engine.py +110 -0
- sales_script.json +70 -0
- sellme_demo.py +157 -0
- sellme_pro.py +270 -0
engine.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from graph_module import Graph
|
| 3 |
+
from algorithms import bellman_ford_list
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class SalesEngine:
|
| 7 |
+
def __init__(self, json_file='sales_script.json'):
|
| 8 |
+
"""Initialize the SalesEngine by loading the sales script from JSON."""
|
| 9 |
+
# Load the sales script
|
| 10 |
+
with open(json_file, 'r') as f:
|
| 11 |
+
data = json.load(f)
|
| 12 |
+
|
| 13 |
+
self.nodes_data = data['nodes']
|
| 14 |
+
self.edges_data = data['edges']
|
| 15 |
+
|
| 16 |
+
# Create mapping from string IDs to integer IDs
|
| 17 |
+
self.str_to_int = {}
|
| 18 |
+
self.int_to_str = {}
|
| 19 |
+
self.node_text = {}
|
| 20 |
+
|
| 21 |
+
for idx, node_name in enumerate(self.nodes_data.keys()):
|
| 22 |
+
self.str_to_int[node_name] = idx
|
| 23 |
+
self.int_to_str[idx] = node_name
|
| 24 |
+
self.node_text[node_name] = self.nodes_data[node_name]
|
| 25 |
+
|
| 26 |
+
# Build the Graph object
|
| 27 |
+
num_nodes = len(self.nodes_data)
|
| 28 |
+
self.graph = Graph(num_nodes, directed=True)
|
| 29 |
+
|
| 30 |
+
# Add edges with weights
|
| 31 |
+
for edge in self.edges_data:
|
| 32 |
+
from_node = self.str_to_int[edge['from']]
|
| 33 |
+
to_node = self.str_to_int[edge['to']]
|
| 34 |
+
weight = edge['weight']
|
| 35 |
+
self.graph.add_edge(from_node, to_node, weight)
|
| 36 |
+
|
| 37 |
+
def get_best_next_step(self, current_step_name):
|
| 38 |
+
"""
|
| 39 |
+
Find the best next step from the current position to reach 'close_deal'.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
current_step_name: String name of the current step
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
Tuple of (next_step_name, next_step_text) for the optimal next step
|
| 46 |
+
"""
|
| 47 |
+
# Find integer ID of current step
|
| 48 |
+
current_id = self.str_to_int[current_step_name]
|
| 49 |
+
|
| 50 |
+
# Run Bellman-Ford from current step
|
| 51 |
+
distances = bellman_ford_list(self.graph, current_id)
|
| 52 |
+
|
| 53 |
+
if distances is None:
|
| 54 |
+
raise ValueError("Negative cycle detected in the sales script graph!")
|
| 55 |
+
|
| 56 |
+
# Find the close_deal node ID
|
| 57 |
+
close_deal_id = self.str_to_int['close_deal']
|
| 58 |
+
|
| 59 |
+
# If we're already at close_deal, return it
|
| 60 |
+
if current_id == close_deal_id:
|
| 61 |
+
return current_step_name, self.node_text[current_step_name]
|
| 62 |
+
|
| 63 |
+
# Find the best immediate next step
|
| 64 |
+
# Look at all neighbors of current node and pick the one with shortest total distance to close_deal
|
| 65 |
+
adj_list = self.graph.get_list()
|
| 66 |
+
best_next_id = None
|
| 67 |
+
best_total_distance = float('inf')
|
| 68 |
+
|
| 69 |
+
for neighbor_id, edge_weight in adj_list[current_id]:
|
| 70 |
+
# Total distance = edge weight + distance from neighbor to close_deal
|
| 71 |
+
total_distance = edge_weight + distances[close_deal_id]
|
| 72 |
+
if total_distance < best_total_distance:
|
| 73 |
+
best_total_distance = total_distance
|
| 74 |
+
best_next_id = neighbor_id
|
| 75 |
+
|
| 76 |
+
if best_next_id is None:
|
| 77 |
+
raise ValueError(f"No path found from '{current_step_name}' to 'close_deal'")
|
| 78 |
+
|
| 79 |
+
# Convert back to string name and get text
|
| 80 |
+
next_step_name = self.int_to_str[best_next_id]
|
| 81 |
+
next_step_text = self.node_text[next_step_name]
|
| 82 |
+
|
| 83 |
+
return next_step_name, next_step_text
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
if __name__ == "__main__":
|
| 87 |
+
# Simulate a conversation path
|
| 88 |
+
engine = SalesEngine()
|
| 89 |
+
|
| 90 |
+
current_step = "start"
|
| 91 |
+
print(f"Starting sales conversation at: {current_step}")
|
| 92 |
+
print(f">>> {engine.node_text[current_step]}\n")
|
| 93 |
+
|
| 94 |
+
step_count = 0
|
| 95 |
+
max_steps = 10 # Safety limit to prevent infinite loops
|
| 96 |
+
|
| 97 |
+
while current_step != "close_deal" and step_count < max_steps:
|
| 98 |
+
# Get the best next step
|
| 99 |
+
next_step, next_text = engine.get_best_next_step(current_step)
|
| 100 |
+
|
| 101 |
+
print(f"Best next move: {next_step}")
|
| 102 |
+
print(f">>> {next_text}\n")
|
| 103 |
+
|
| 104 |
+
current_step = next_step
|
| 105 |
+
step_count += 1
|
| 106 |
+
|
| 107 |
+
if current_step == "close_deal":
|
| 108 |
+
print("[SUCCESS] Successfully reached the deal closure!")
|
| 109 |
+
else:
|
| 110 |
+
print("[WARNING] Reached maximum steps without closing the deal.")
|
sales_script.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
{
|
| 15 |
+
"from": "start",
|
| 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 |
+
}
|
sellme_demo.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import google.generativeai as genai
|
| 4 |
+
from graph_module import Graph
|
| 5 |
+
from algorithms import bellman_ford_list
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def main():
|
| 9 |
+
# Configuration: Get API Key from user
|
| 10 |
+
print("=" * 60)
|
| 11 |
+
print("SellMe AI Sales Demo - Powered by Gemini")
|
| 12 |
+
print("=" * 60)
|
| 13 |
+
api_key = input("\nEnter your Gemini API Key: ").strip()
|
| 14 |
+
|
| 15 |
+
# Configure Gemini
|
| 16 |
+
genai.configure(api_key=api_key)
|
| 17 |
+
model = genai.GenerativeModel('gemini-1.5-flash')
|
| 18 |
+
|
| 19 |
+
print("\n[INFO] Gemini configured successfully!")
|
| 20 |
+
print("[INFO] Loading sales script...\n")
|
| 21 |
+
|
| 22 |
+
# Load sales_script.json
|
| 23 |
+
with open('sales_script.json', 'r', encoding='utf-8') as f:
|
| 24 |
+
data = json.load(f)
|
| 25 |
+
|
| 26 |
+
nodes_data = data['nodes']
|
| 27 |
+
edges_data = data['edges']
|
| 28 |
+
|
| 29 |
+
# Create mappings: string IDs <-> integer IDs
|
| 30 |
+
str_to_int = {}
|
| 31 |
+
int_to_str = {}
|
| 32 |
+
|
| 33 |
+
for idx, node_name in enumerate(nodes_data.keys()):
|
| 34 |
+
str_to_int[node_name] = idx
|
| 35 |
+
int_to_str[idx] = node_name
|
| 36 |
+
|
| 37 |
+
# Build Graph object
|
| 38 |
+
num_nodes = len(nodes_data)
|
| 39 |
+
graph = Graph(num_nodes, directed=True)
|
| 40 |
+
|
| 41 |
+
# Add edges with weights
|
| 42 |
+
for edge in edges_data:
|
| 43 |
+
from_node = str_to_int[edge['from']]
|
| 44 |
+
to_node = str_to_int[edge['to']]
|
| 45 |
+
weight = edge['weight']
|
| 46 |
+
graph.add_edge(from_node, to_node, weight)
|
| 47 |
+
|
| 48 |
+
print("[INFO] Sales graph built successfully!")
|
| 49 |
+
print(f"[INFO] Nodes: {num_nodes}, Edges: {len(edges_data)}\n")
|
| 50 |
+
print("=" * 60)
|
| 51 |
+
print("Starting Sales Conversation")
|
| 52 |
+
print("=" * 60)
|
| 53 |
+
print("(Type 'quit' to exit)\n")
|
| 54 |
+
|
| 55 |
+
# Start conversation
|
| 56 |
+
current_step = "start"
|
| 57 |
+
conversation_count = 0
|
| 58 |
+
max_steps = 20 # Safety limit
|
| 59 |
+
|
| 60 |
+
while current_step not in ["close_deal", "exit_bad"] and conversation_count < max_steps:
|
| 61 |
+
# Get current node ID
|
| 62 |
+
current_id = str_to_int[current_step]
|
| 63 |
+
|
| 64 |
+
# Calculate best path to close_deal using Bellman-Ford
|
| 65 |
+
distances = bellman_ford_list(graph, current_id)
|
| 66 |
+
|
| 67 |
+
if distances is None:
|
| 68 |
+
print("[ERROR] Negative cycle detected in sales graph!")
|
| 69 |
+
break
|
| 70 |
+
|
| 71 |
+
# Get close_deal node ID
|
| 72 |
+
close_deal_id = str_to_int['close_deal']
|
| 73 |
+
|
| 74 |
+
# Find the best immediate next step
|
| 75 |
+
adj_list = graph.get_list()
|
| 76 |
+
neighbors = adj_list[current_id]
|
| 77 |
+
|
| 78 |
+
if not neighbors:
|
| 79 |
+
print(f"[ERROR] No path forward from '{current_step}'")
|
| 80 |
+
break
|
| 81 |
+
|
| 82 |
+
# Pick the neighbor with shortest total distance to close_deal
|
| 83 |
+
best_next_id = None
|
| 84 |
+
best_total_distance = float('inf')
|
| 85 |
+
|
| 86 |
+
for neighbor_id, edge_weight in neighbors:
|
| 87 |
+
# Run Bellman-Ford from this neighbor to find distance to close_deal
|
| 88 |
+
neighbor_distances = bellman_ford_list(graph, neighbor_id)
|
| 89 |
+
if neighbor_distances and neighbor_distances[close_deal_id] != float('inf'):
|
| 90 |
+
total_distance = edge_weight + neighbor_distances[close_deal_id]
|
| 91 |
+
if total_distance < best_total_distance:
|
| 92 |
+
best_total_distance = total_distance
|
| 93 |
+
best_next_id = neighbor_id
|
| 94 |
+
|
| 95 |
+
if best_next_id is None:
|
| 96 |
+
print(f"[ERROR] No path found from '{current_step}' to 'close_deal'")
|
| 97 |
+
break
|
| 98 |
+
|
| 99 |
+
# Get next step name and script text
|
| 100 |
+
next_step_name = int_to_str[best_next_id]
|
| 101 |
+
script_text = nodes_data[next_step_name]
|
| 102 |
+
|
| 103 |
+
# Get user input (simulating client)
|
| 104 |
+
print(f"\n[CURRENT STEP: {current_step}]")
|
| 105 |
+
print(f"[NEXT TARGET: {next_step_name}]")
|
| 106 |
+
user_input = input("\nYou (Client): ").strip()
|
| 107 |
+
|
| 108 |
+
if user_input.lower() == 'quit':
|
| 109 |
+
print("\n[INFO] Exiting demo. Goodbye!")
|
| 110 |
+
break
|
| 111 |
+
|
| 112 |
+
# Create prompt for Gemini
|
| 113 |
+
prompt = f"""You are a professional sales representative for SellMe, an AI sales assistant platform.
|
| 114 |
+
|
| 115 |
+
Your goal is to move the conversation toward this step: '{next_step_name}'.
|
| 116 |
+
The sales script for this step says: '{script_text}'.
|
| 117 |
+
The client just said: '{user_input}'.
|
| 118 |
+
|
| 119 |
+
Generate a natural, conversational response in Ukrainian that:
|
| 120 |
+
1. Acknowledges what the client said
|
| 121 |
+
2. Smoothly guides toward the script message
|
| 122 |
+
3. Sounds human and friendly, not robotic
|
| 123 |
+
4. Keep it brief (1-2 sentences max)
|
| 124 |
+
|
| 125 |
+
Response:"""
|
| 126 |
+
|
| 127 |
+
# Get Gemini's response
|
| 128 |
+
print("\n[AI is thinking...]")
|
| 129 |
+
try:
|
| 130 |
+
response = model.generate_content(prompt)
|
| 131 |
+
ai_response = response.text.strip()
|
| 132 |
+
|
| 133 |
+
print(f"\nSellMe AI: {ai_response}")
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
print(f"\n[ERROR] Gemini API error: {e}")
|
| 137 |
+
print(f"[FALLBACK] Using script: {script_text}")
|
| 138 |
+
|
| 139 |
+
# Move to next step
|
| 140 |
+
current_step = next_step_name
|
| 141 |
+
conversation_count += 1
|
| 142 |
+
|
| 143 |
+
# End of conversation
|
| 144 |
+
print("\n" + "=" * 60)
|
| 145 |
+
if current_step == "close_deal":
|
| 146 |
+
print("[SUCCESS] Deal closed! 🎉")
|
| 147 |
+
print(f"Final message: {nodes_data[current_step]}")
|
| 148 |
+
elif current_step == "exit_bad":
|
| 149 |
+
print("[EXIT] Client not interested.")
|
| 150 |
+
print(f"Final message: {nodes_data[current_step]}")
|
| 151 |
+
else:
|
| 152 |
+
print(f"[INFO] Conversation ended at step: {current_step}")
|
| 153 |
+
print("=" * 60)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
if __name__ == "__main__":
|
| 157 |
+
main()
|
sellme_pro.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import google.generativeai as genai
|
| 4 |
+
from graph_module import Graph
|
| 5 |
+
from algorithms import bellman_ford_list
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def get_sentiment(user_text, model):
|
| 9 |
+
"""
|
| 10 |
+
Analyze user sentiment using Gemini.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
user_text: The user's message
|
| 14 |
+
model: Gemini model instance
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
Float score from -1 (Angry) to +1 (Happy)
|
| 18 |
+
"""
|
| 19 |
+
prompt = f"""Analyze the sentiment of this message and return ONLY a number between -1 and +1.
|
| 20 |
+
-1 = Very angry, hostile, frustrated
|
| 21 |
+
-0.5 = Slightly negative, uncertain
|
| 22 |
+
0 = Neutral
|
| 23 |
+
+0.5 = Slightly positive, interested
|
| 24 |
+
+1 = Very happy, enthusiastic, eager
|
| 25 |
+
|
| 26 |
+
Message: "{user_text}"
|
| 27 |
+
|
| 28 |
+
Return only the number, nothing else:"""
|
| 29 |
+
|
| 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
|
| 41 |
+
|
| 42 |
+
|
| 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-1.5-pro')
|
| 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 |
+
|
| 159 |
+
if user_input.lower() == 'quit':
|
| 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()
|