smart-bot-factory 0.3.6__py3-none-any.whl → 0.3.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of smart-bot-factory might be problematic. Click here for more details.
- smart_bot_factory/admin/__init__.py +7 -7
- smart_bot_factory/admin/admin_events.py +483 -383
- smart_bot_factory/admin/admin_logic.py +234 -158
- smart_bot_factory/admin/admin_manager.py +68 -53
- smart_bot_factory/admin/admin_tester.py +46 -40
- smart_bot_factory/admin/timeout_checker.py +201 -153
- smart_bot_factory/aiogram_calendar/__init__.py +11 -3
- smart_bot_factory/aiogram_calendar/common.py +12 -18
- smart_bot_factory/aiogram_calendar/dialog_calendar.py +126 -64
- smart_bot_factory/aiogram_calendar/schemas.py +49 -28
- smart_bot_factory/aiogram_calendar/simple_calendar.py +94 -50
- smart_bot_factory/analytics/analytics_manager.py +414 -392
- smart_bot_factory/cli.py +204 -148
- smart_bot_factory/config.py +123 -102
- smart_bot_factory/core/bot_utils.py +480 -324
- smart_bot_factory/core/conversation_manager.py +287 -200
- smart_bot_factory/core/decorators.py +1145 -739
- smart_bot_factory/core/message_sender.py +287 -266
- smart_bot_factory/core/router.py +170 -100
- smart_bot_factory/core/router_manager.py +121 -83
- smart_bot_factory/core/states.py +4 -3
- smart_bot_factory/creation/__init__.py +1 -1
- smart_bot_factory/creation/bot_builder.py +320 -242
- smart_bot_factory/creation/bot_testing.py +440 -365
- smart_bot_factory/dashboard/__init__.py +1 -3
- smart_bot_factory/event/__init__.py +2 -7
- smart_bot_factory/handlers/handlers.py +682 -466
- smart_bot_factory/integrations/openai_client.py +218 -168
- smart_bot_factory/integrations/supabase_client.py +928 -637
- smart_bot_factory/message/__init__.py +18 -22
- smart_bot_factory/router/__init__.py +2 -2
- smart_bot_factory/setup_checker.py +162 -126
- smart_bot_factory/supabase/__init__.py +1 -1
- smart_bot_factory/supabase/client.py +631 -515
- smart_bot_factory/utils/__init__.py +2 -3
- smart_bot_factory/utils/debug_routing.py +38 -27
- smart_bot_factory/utils/prompt_loader.py +153 -120
- smart_bot_factory/utils/user_prompt_loader.py +55 -56
- smart_bot_factory/utm_link_generator.py +123 -116
- {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/METADATA +3 -1
- smart_bot_factory-0.3.8.dist-info/RECORD +59 -0
- smart_bot_factory-0.3.6.dist-info/RECORD +0 -59
- {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,392 +1,414 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
from ..core.bot_utils import parse_ai_response
|
|
6
|
-
|
|
7
|
-
logger = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
"",
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"",
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from ..core.bot_utils import parse_ai_response
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AnalyticsManager:
|
|
11
|
+
"""Управление аналитикой и статистикой бота"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, supabase_client):
|
|
14
|
+
self.supabase = supabase_client
|
|
15
|
+
|
|
16
|
+
async def get_funnel_stats(self, days: int = 7) -> Dict[str, Any]:
|
|
17
|
+
"""Получает статистику воронки продаж"""
|
|
18
|
+
try:
|
|
19
|
+
# Основная статистика
|
|
20
|
+
stats = await self.supabase.get_funnel_stats(days)
|
|
21
|
+
|
|
22
|
+
# Добавляем новых пользователей
|
|
23
|
+
|
|
24
|
+
# Добавляем новых пользователей
|
|
25
|
+
cutoff_date = datetime.now() - timedelta(days=days)
|
|
26
|
+
|
|
27
|
+
# Запрос на новых пользователей С УЧЕТОМ bot_id
|
|
28
|
+
query = (
|
|
29
|
+
self.supabase.client.table("sales_users")
|
|
30
|
+
.select("id")
|
|
31
|
+
.gte("created_at", cutoff_date.isoformat())
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Фильтруем по bot_id если он указан
|
|
35
|
+
if self.supabase.bot_id:
|
|
36
|
+
query = query.eq("bot_id", self.supabase.bot_id)
|
|
37
|
+
logger.info(
|
|
38
|
+
f"📊 Фильтр новых пользователей по bot_id: {self.supabase.bot_id}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Исключаем тестовых пользователей
|
|
42
|
+
query = query.neq("username", "test_user")
|
|
43
|
+
|
|
44
|
+
response = query.execute()
|
|
45
|
+
|
|
46
|
+
new_users = len(response.data) if response.data else 0
|
|
47
|
+
|
|
48
|
+
logger.info(f"🆕 Новых пользователей за {days} дней: {new_users}")
|
|
49
|
+
|
|
50
|
+
# Обогащаем статистику
|
|
51
|
+
stats["new_users"] = new_users
|
|
52
|
+
stats["period_days"] = days
|
|
53
|
+
|
|
54
|
+
return stats
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"Ошибка получения статистики воронки: {e}")
|
|
58
|
+
return {
|
|
59
|
+
"total_sessions": 0,
|
|
60
|
+
"new_users": 0,
|
|
61
|
+
"stages": {},
|
|
62
|
+
"avg_quality": 0,
|
|
63
|
+
"period_days": days,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async def get_events_stats(self, days: int = 7) -> Dict[str, int]:
|
|
67
|
+
"""Получает статистику событий"""
|
|
68
|
+
try:
|
|
69
|
+
return await self.supabase.get_events_stats(days)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.error(f"Ошибка получения статистики событий: {e}")
|
|
72
|
+
return {}
|
|
73
|
+
|
|
74
|
+
async def get_user_journey(self, user_id: int) -> List[Dict[str, Any]]:
|
|
75
|
+
"""Получает ВСЕ СООБЩЕНИЯ для активной сессии пользователя"""
|
|
76
|
+
try:
|
|
77
|
+
# 1. Получаем ОДНУ активную сессию
|
|
78
|
+
session_info = await self.supabase.get_active_session(user_id)
|
|
79
|
+
|
|
80
|
+
if not session_info:
|
|
81
|
+
logger.warning(f"У пользователя {user_id} нет активной сессии")
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
session_id = session_info["id"]
|
|
85
|
+
logger.info(f"Загружаем ВСЕ сообщения для активной сессии {session_id}")
|
|
86
|
+
|
|
87
|
+
# 2. Получаем ВСЕ сообщения этой сессии (кроме системных)
|
|
88
|
+
messages_response = (
|
|
89
|
+
self.supabase.client.table("sales_messages")
|
|
90
|
+
.select("role", "content", "created_at", "message_type")
|
|
91
|
+
.eq("session_id", str(session_id))
|
|
92
|
+
.neq("role", "system")
|
|
93
|
+
.order("created_at")
|
|
94
|
+
.execute()
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
messages = messages_response.data if messages_response.data else []
|
|
98
|
+
logger.info(f"Найдено {len(messages)} сообщений для сессии {session_id}")
|
|
99
|
+
|
|
100
|
+
# 3. Получаем события для этой сессии
|
|
101
|
+
events_response = (
|
|
102
|
+
self.supabase.client.table("session_events")
|
|
103
|
+
.select("event_type", "event_info", "created_at")
|
|
104
|
+
.eq("session_id", str(session_id))
|
|
105
|
+
.order("created_at")
|
|
106
|
+
.execute()
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
events = events_response.data if events_response.data else []
|
|
110
|
+
logger.info(f"Найдено {len(events)} событий для сессии {session_id}")
|
|
111
|
+
|
|
112
|
+
# 4. Формируем ОДИН объект сессии со ВСЕМИ сообщениями
|
|
113
|
+
session_with_messages = {
|
|
114
|
+
"id": session_id,
|
|
115
|
+
"current_stage": session_info.get("current_stage", "unknown"),
|
|
116
|
+
"lead_quality_score": session_info.get("lead_quality_score", 0),
|
|
117
|
+
"created_at": session_info["created_at"],
|
|
118
|
+
"status": "active",
|
|
119
|
+
"messages": messages, # ВСЕ сообщения сессии
|
|
120
|
+
"events": events,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return [session_with_messages]
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.error(
|
|
127
|
+
f"Ошибка получения сообщений активной сессии пользователя {user_id}: {e}"
|
|
128
|
+
)
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
def _truncate_message_for_history(self, text: str, max_length: int = 150) -> str:
|
|
132
|
+
"""Сокращает сообщения для истории"""
|
|
133
|
+
if not text:
|
|
134
|
+
return ""
|
|
135
|
+
|
|
136
|
+
# Убираем переносы строк для компактности
|
|
137
|
+
text = text.replace("\n", " ").strip()
|
|
138
|
+
|
|
139
|
+
if len(text) <= max_length:
|
|
140
|
+
return text
|
|
141
|
+
|
|
142
|
+
return text[: max_length - 3] + "..."
|
|
143
|
+
|
|
144
|
+
def format_funnel_stats(self, stats: Dict[str, Any]) -> str:
|
|
145
|
+
"""Форматирует статистику воронки для отображения"""
|
|
146
|
+
if not stats or stats["total_sessions"] == 0:
|
|
147
|
+
return "📊 Нет данных за указанный период"
|
|
148
|
+
|
|
149
|
+
# Эмодзи для этапов
|
|
150
|
+
stage_emojis = {
|
|
151
|
+
"introduction": "👋",
|
|
152
|
+
"consult": "💬",
|
|
153
|
+
"offer": "💼",
|
|
154
|
+
"contacts": "📱",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Названия этапов
|
|
158
|
+
stage_names = {
|
|
159
|
+
"introduction": "Знакомство",
|
|
160
|
+
"consult": "Консультация",
|
|
161
|
+
"offer": "Предложение",
|
|
162
|
+
"contacts": "Контакты",
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
lines = [
|
|
166
|
+
f"📊 ВОРОНКА ЗА {stats['period_days']} ДНЕЙ",
|
|
167
|
+
"",
|
|
168
|
+
f"👥 Всего пользователей: {stats.get('total_unique_users', 0)}",
|
|
169
|
+
f"🆕 Новых: {stats.get('new_users', 0)}",
|
|
170
|
+
"",
|
|
171
|
+
"📈 ЭТАПЫ ВОРОНКИ:",
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
# Добавляем этапы
|
|
175
|
+
stages = stats.get("stages", {})
|
|
176
|
+
total = stats["total_sessions"]
|
|
177
|
+
|
|
178
|
+
for stage, count in stages.items():
|
|
179
|
+
emoji = stage_emojis.get(stage, "📌")
|
|
180
|
+
name = stage_names.get(stage, stage)
|
|
181
|
+
percentage = (count / total * 100) if total > 0 else 0
|
|
182
|
+
lines.append(f"{emoji} {name}: {count} чел ({percentage:.1f}%)")
|
|
183
|
+
|
|
184
|
+
# Средняя оценка качества
|
|
185
|
+
avg_quality = stats.get("avg_quality", 0)
|
|
186
|
+
if avg_quality > 0:
|
|
187
|
+
lines.extend(["", f"⭐ Средний скоринг: {avg_quality:.1f}"])
|
|
188
|
+
|
|
189
|
+
return "\n".join(lines)
|
|
190
|
+
|
|
191
|
+
def format_events_stats(self, events: Dict[str, int]) -> str:
|
|
192
|
+
"""Форматирует статистику событий для отображения"""
|
|
193
|
+
if not events:
|
|
194
|
+
return "🔥 События: нет данных"
|
|
195
|
+
|
|
196
|
+
# Эмодзи для событий
|
|
197
|
+
event_emojis = {
|
|
198
|
+
"телефон": "📱",
|
|
199
|
+
"консультация": "💬",
|
|
200
|
+
"покупка": "💰",
|
|
201
|
+
"отказ": "❌",
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
lines = ["🔥 СОБЫТИЯ:"]
|
|
205
|
+
|
|
206
|
+
for event_type, count in events.items():
|
|
207
|
+
emoji = event_emojis.get(event_type, "🔔")
|
|
208
|
+
# Экранируем потенциально проблемные символы
|
|
209
|
+
safe_event_type = event_type.replace("_", " ").title()
|
|
210
|
+
lines.append(f"{emoji} {safe_event_type}: {count}")
|
|
211
|
+
|
|
212
|
+
return "\n".join(lines)
|
|
213
|
+
|
|
214
|
+
def format_user_journey(self, user_id: int, journey: List[Dict[str, Any]]) -> str:
|
|
215
|
+
"""Форматирует детальную историю пользователя с сообщениями"""
|
|
216
|
+
if not journey:
|
|
217
|
+
return f"👤 Пользователь {user_id}\nИстория не найдена"
|
|
218
|
+
|
|
219
|
+
session = journey[0]
|
|
220
|
+
messages = session.get("messages", [])
|
|
221
|
+
events = session.get("events", [])
|
|
222
|
+
|
|
223
|
+
# Заголовок
|
|
224
|
+
created_at = datetime.fromisoformat(
|
|
225
|
+
session["created_at"].replace("Z", "+00:00")
|
|
226
|
+
)
|
|
227
|
+
date_str = created_at.strftime("%d.%m %H:%M")
|
|
228
|
+
stage = session.get("current_stage", "unknown")
|
|
229
|
+
quality = session.get("lead_quality_score", 0)
|
|
230
|
+
|
|
231
|
+
lines = [
|
|
232
|
+
f"👤 Пользователь {user_id}",
|
|
233
|
+
f"📅 {date_str} | {stage} | ⭐{quality}",
|
|
234
|
+
f"📊 {len(messages)} сообщений, {len(events)} событий",
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
# События в сессии
|
|
238
|
+
if events:
|
|
239
|
+
lines.append("")
|
|
240
|
+
lines.append("🔥 События:")
|
|
241
|
+
for event in events:
|
|
242
|
+
event_time = datetime.fromisoformat(
|
|
243
|
+
event["created_at"].replace("Z", "+00:00")
|
|
244
|
+
)
|
|
245
|
+
time_str = event_time.strftime("%H:%M")
|
|
246
|
+
emoji = {
|
|
247
|
+
"телефон": "📱",
|
|
248
|
+
"консультация": "💬",
|
|
249
|
+
"покупка": "💰",
|
|
250
|
+
"отказ": "❌",
|
|
251
|
+
}.get(event["event_type"], "🔔")
|
|
252
|
+
lines.append(
|
|
253
|
+
f" {emoji} {time_str} {event['event_type']}: {event['event_info']}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
lines.append(f"\n{'━' * 40}")
|
|
257
|
+
lines.append("💬 ДИАЛОГ:")
|
|
258
|
+
|
|
259
|
+
# Сообщения
|
|
260
|
+
for i, msg in enumerate(messages, 1):
|
|
261
|
+
msg_time = datetime.fromisoformat(msg["created_at"].replace("Z", "+00:00"))
|
|
262
|
+
time_str = msg_time.strftime("%H:%M")
|
|
263
|
+
|
|
264
|
+
role = "👤 Пользователь" if msg["role"] == "user" else "🤖 Бот"
|
|
265
|
+
|
|
266
|
+
# Очищаем JSON из ответов бота
|
|
267
|
+
content = msg["content"]
|
|
268
|
+
if msg["role"] == "assistant" and content.strip().startswith("{"):
|
|
269
|
+
try:
|
|
270
|
+
clean_content, _ = parse_ai_response(content)
|
|
271
|
+
content = clean_content if clean_content else content
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
# Сокращаем длинные сообщения
|
|
276
|
+
if len(content) > 200:
|
|
277
|
+
content = content[:197] + "..."
|
|
278
|
+
|
|
279
|
+
lines.append(f"\n{i}. {role} в {time_str}:")
|
|
280
|
+
lines.append(f" {content}")
|
|
281
|
+
|
|
282
|
+
return "\n".join(lines)
|
|
283
|
+
|
|
284
|
+
async def get_daily_summary(self) -> str:
|
|
285
|
+
"""Получает сводку за сегодня"""
|
|
286
|
+
try:
|
|
287
|
+
today_stats = await self.get_funnel_stats(1)
|
|
288
|
+
today_events = await self.get_events_stats(1)
|
|
289
|
+
|
|
290
|
+
lines = [
|
|
291
|
+
"📈 СВОДКА ЗА СЕГОДНЯ",
|
|
292
|
+
"",
|
|
293
|
+
f"👥 Новых сессий: {today_stats['total_sessions']}",
|
|
294
|
+
f"🆕 Новых пользователей: {today_stats.get('new_users', 0)}",
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
if today_events:
|
|
298
|
+
lines.append("")
|
|
299
|
+
lines.append("🔥 События:")
|
|
300
|
+
for event_type, count in today_events.items():
|
|
301
|
+
emoji = {
|
|
302
|
+
"телефон": "📱",
|
|
303
|
+
"консультация": "💬",
|
|
304
|
+
"покупка": "💰",
|
|
305
|
+
"отказ": "❌",
|
|
306
|
+
}.get(event_type, "🔔")
|
|
307
|
+
lines.append(f" {emoji} {event_type}: {count}")
|
|
308
|
+
|
|
309
|
+
return "\n".join(lines)
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"Ошибка получения сводки за сегодня: {e}")
|
|
313
|
+
return "❌ Ошибка получения сводки"
|
|
314
|
+
|
|
315
|
+
async def get_performance_metrics(self) -> Dict[str, Any]:
|
|
316
|
+
"""Получает метрики производительности"""
|
|
317
|
+
try:
|
|
318
|
+
# Конверсия по этапам воронки
|
|
319
|
+
stats_7d = await self.get_funnel_stats(7)
|
|
320
|
+
stages = stats_7d.get("stages", {})
|
|
321
|
+
total = stats_7d["total_sessions"]
|
|
322
|
+
|
|
323
|
+
metrics = {
|
|
324
|
+
"total_sessions_7d": total,
|
|
325
|
+
"conversion_rates": {},
|
|
326
|
+
"avg_quality": stats_7d.get("avg_quality", 0),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if total > 0:
|
|
330
|
+
# Рассчитываем конверсии
|
|
331
|
+
intro_count = stages.get("introduction", 0)
|
|
332
|
+
consult_count = stages.get("consult", 0)
|
|
333
|
+
offer_count = stages.get("offer", 0)
|
|
334
|
+
contacts_count = stages.get("contacts", 0)
|
|
335
|
+
|
|
336
|
+
metrics["conversion_rates"] = {
|
|
337
|
+
"intro_to_consult": (
|
|
338
|
+
(consult_count / intro_count * 100) if intro_count > 0 else 0
|
|
339
|
+
),
|
|
340
|
+
"consult_to_offer": (
|
|
341
|
+
(offer_count / consult_count * 100) if consult_count > 0 else 0
|
|
342
|
+
),
|
|
343
|
+
"offer_to_contacts": (
|
|
344
|
+
(contacts_count / offer_count * 100) if offer_count > 0 else 0
|
|
345
|
+
),
|
|
346
|
+
"intro_to_contacts": (
|
|
347
|
+
(contacts_count / intro_count * 100) if intro_count > 0 else 0
|
|
348
|
+
),
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return metrics
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.error(f"Ошибка получения метрик производительности: {e}")
|
|
355
|
+
return {}
|
|
356
|
+
|
|
357
|
+
def format_performance_metrics(self, metrics: Dict[str, Any]) -> str:
|
|
358
|
+
"""Форматирует метрики производительности"""
|
|
359
|
+
if not metrics:
|
|
360
|
+
return "📊 Метрики недоступны"
|
|
361
|
+
|
|
362
|
+
lines = [
|
|
363
|
+
"📊 МЕТРИКИ ЭФФЕКТИВНОСТИ",
|
|
364
|
+
"",
|
|
365
|
+
f"👥 Сессий за 7 дней: {metrics.get('total_sessions_7d', 0)}",
|
|
366
|
+
f"⭐ Средняя оценка: {metrics.get('avg_quality', 0):.1f}",
|
|
367
|
+
"",
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
conversions = metrics.get("conversion_rates", {})
|
|
371
|
+
if conversions:
|
|
372
|
+
lines.append("🎯 КОНВЕРСИИ:")
|
|
373
|
+
lines.append(f"👋➡️💬 {conversions.get('intro_to_consult', 0):.1f}%")
|
|
374
|
+
lines.append(f"💬➡️💼 {conversions.get('consult_to_offer', 0):.1f}%")
|
|
375
|
+
lines.append(f"💼➡️📱 {conversions.get('offer_to_contacts', 0):.1f}%")
|
|
376
|
+
lines.append(f"👋➡️📱 {conversions.get('intro_to_contacts', 0):.1f}%")
|
|
377
|
+
|
|
378
|
+
return "\n".join(lines)
|
|
379
|
+
|
|
380
|
+
async def get_top_performing_hours(self) -> List[int]:
|
|
381
|
+
"""Получает часы с наибольшей активностью"""
|
|
382
|
+
try:
|
|
383
|
+
# Запрос на получение активности по часам за последние 7 дней
|
|
384
|
+
cutoff_date = datetime.now() - timedelta(days=7)
|
|
385
|
+
|
|
386
|
+
response = (
|
|
387
|
+
self.supabase.client.table("sales_messages")
|
|
388
|
+
.select("created_at")
|
|
389
|
+
.gte("created_at", cutoff_date.isoformat())
|
|
390
|
+
.eq("role", "user")
|
|
391
|
+
.execute()
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
if not response.data:
|
|
395
|
+
return []
|
|
396
|
+
|
|
397
|
+
# Группируем по часам
|
|
398
|
+
hour_counts = {}
|
|
399
|
+
for message in response.data:
|
|
400
|
+
created_at = datetime.fromisoformat(
|
|
401
|
+
message["created_at"].replace("Z", "+00:00")
|
|
402
|
+
)
|
|
403
|
+
hour = created_at.hour
|
|
404
|
+
hour_counts[hour] = hour_counts.get(hour, 0) + 1
|
|
405
|
+
|
|
406
|
+
# Сортируем по количеству сообщений
|
|
407
|
+
sorted_hours = sorted(hour_counts.items(), key=lambda x: x[1], reverse=True)
|
|
408
|
+
|
|
409
|
+
# Возвращаем топ-5 часов
|
|
410
|
+
return [hour for hour, count in sorted_hours[:5]]
|
|
411
|
+
|
|
412
|
+
except Exception as e:
|
|
413
|
+
logger.error(f"Ошибка получения топ часов: {e}")
|
|
414
|
+
return []
|