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.

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