smart-bot-factory 0.3.7__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 +474 -332
  16. smart_bot_factory/core/conversation_manager.py +287 -200
  17. smart_bot_factory/core/decorators.py +1129 -749
  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 +676 -472
  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.7.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.7.dist-info/RECORD +0 -59
  43. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/WHEEL +0 -0
  44. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/entry_points.txt +0 -0
  45. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/licenses/LICENSE +0 -0
@@ -1,515 +1,631 @@
1
- """
2
- Supabase клиент с автоматической загрузкой настроек из .env файла
3
- """
4
-
5
- import os
6
- import logging
7
- from datetime import datetime, timedelta
8
- from typing import List, Dict, Any, Optional
9
- from pathlib import Path
10
- from dotenv import load_dotenv
11
- from supabase import create_client, Client
12
- from postgrest.exceptions import APIError
13
-
14
- from project_root_finder import root
15
-
16
- PROJECT_ROOT = root
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
- class SupabaseClient:
21
- """Клиент для работы с Supabase с автоматической загрузкой настроек из .env"""
22
-
23
- def __init__(self, bot_id: str):
24
- """
25
- Инициализация клиента Supabase с автоматической загрузкой настроек
26
-
27
- Args:
28
- bot_id: Идентификатор бота (обязательно) - код сам найдет все настройки
29
- """
30
- self.bot_id = bot_id
31
-
32
- # Автоматически загружаем настройки из .env
33
- self._load_env_config()
34
-
35
- # Инициализируем клиент СИНХРОННО прямо в __init__
36
- self.client = create_client(self.url, self.key)
37
-
38
- logger.info(f"✅ SupabaseClient инициализирован для bot_id: {self.bot_id}")
39
-
40
- def _load_env_config(self):
41
- """Загружает конфигурацию из .env файла"""
42
- try:
43
- # Автоматический поиск .env файла
44
- env_path = self._find_env_file()
45
-
46
- if not env_path or not env_path.exists():
47
- raise FileNotFoundError(f".env файл не найден: {env_path}")
48
-
49
- # Загружаем переменные окружения
50
- load_dotenv(env_path)
51
-
52
- # Получаем настройки Supabase
53
- self.url = os.getenv('SUPABASE_URL')
54
- self.key = os.getenv('SUPABASE_KEY')
55
-
56
- if not self.url or not self.key:
57
- missing_vars = []
58
- if not self.url:
59
- missing_vars.append('SUPABASE_URL')
60
- if not self.key:
61
- missing_vars.append('SUPABASE_KEY')
62
- raise ValueError(f"Отсутствуют обязательные переменные в .env: {', '.join(missing_vars)}")
63
-
64
- logger.info(f" Настройки Supabase загружены из {env_path}")
65
-
66
- except Exception as e:
67
- logger.error(f" Ошибка загрузки конфигурации Supabase: {e}")
68
- raise
69
-
70
- def _find_env_file(self) -> Optional[Path]:
71
- """Автоматически находит .env файл для указанного бота"""
72
- # Ищем .env файл в папке конкретного бота
73
- bot_env_path = PROJECT_ROOT / "bots" / self.bot_id / ".env"
74
-
75
- if bot_env_path.exists():
76
- logger.info(f"🔍 Найден .env файл для бота {self.bot_id}: {bot_env_path}")
77
- return bot_env_path
78
-
79
- logger.error(f" .env файл не найден для бота {self.bot_id}")
80
- logger.error(f" Искали в: {bot_env_path}")
81
- return None
82
-
83
- # =============================================================================
84
- # МЕТОДЫ ДЛЯ РАБОТЫ С ПОЛЬЗОВАТЕЛЯМИ
85
- # =============================================================================
86
-
87
- async def create_or_get_user(self, user_data: Dict[str, Any]) -> int:
88
- """Создает или получает пользователя с учетом bot_id (если указан)"""
89
- try:
90
- # Если bot_id указан, фильтруем по нему
91
- query = self.client.table('sales_users').select('telegram_id').eq(
92
- 'telegram_id', user_data['telegram_id']
93
- )
94
- if self.bot_id:
95
- query = query.eq('bot_id', self.bot_id)
96
-
97
- response = query.execute()
98
-
99
- if response.data:
100
- # Обновляем данные существующего пользователя
101
- update_query = self.client.table('sales_users').update({
102
- 'username': user_data.get('username'),
103
- 'first_name': user_data.get('first_name'),
104
- 'last_name': user_data.get('last_name'),
105
- 'language_code': user_data.get('language_code'),
106
- 'updated_at': datetime.now().isoformat(),
107
- 'is_active': True
108
- }).eq('telegram_id', user_data['telegram_id'])
109
-
110
- if self.bot_id:
111
- update_query = update_query.eq('bot_id', self.bot_id)
112
-
113
- update_query.execute()
114
-
115
- logger.info(f" Обновлен пользователь {user_data['telegram_id']}{f' для bot_id {self.bot_id}' if self.bot_id else ''}")
116
- return user_data['telegram_id']
117
- else:
118
- # Создаем нового пользователя с bot_id (если указан)
119
- user_insert_data = {
120
- 'telegram_id': user_data['telegram_id'],
121
- 'username': user_data.get('username'),
122
- 'first_name': user_data.get('first_name'),
123
- 'last_name': user_data.get('last_name'),
124
- 'language_code': user_data.get('language_code'),
125
- 'is_active': True,
126
- 'source': user_data.get('source'),
127
- 'medium': user_data.get('medium'),
128
- 'campaign': user_data.get('campaign'),
129
- 'content': user_data.get('content'),
130
- 'term': user_data.get('term'),
131
- }
132
- if self.bot_id:
133
- user_insert_data['bot_id'] = self.bot_id
134
-
135
- response = self.client.table('sales_users').insert(user_insert_data).execute()
136
-
137
- logger.info(f" Создан новый пользователь {user_data['telegram_id']}{f' для bot_id {self.bot_id}' if self.bot_id else ''}")
138
- return user_data['telegram_id']
139
-
140
- except APIError as e:
141
- logger.error(f"❌ Ошибка при работе с пользователем: {e}")
142
- raise
143
-
144
- # =============================================================================
145
- # МЕТОДЫ ДЛЯ РАБОТЫ С СЕССИЯМИ
146
- # =============================================================================
147
-
148
- async def create_chat_session(self, user_data: Dict[str, Any], system_prompt: str) -> str:
149
- """Создает новую сессию чата с учетом bot_id (если указан)"""
150
- try:
151
- # Создаем или обновляем пользователя
152
- user_id = await self.create_or_get_user(user_data)
153
-
154
- # Завершаем активные сессии пользователя (с учетом bot_id)
155
- await self.close_active_sessions(user_id)
156
-
157
- # Создаем новую сессию с bot_id (если указан)
158
- session_data = {
159
- 'user_id': user_id,
160
- 'system_prompt': system_prompt,
161
- 'status': 'active',
162
- 'current_stage': 'introduction',
163
- 'lead_quality_score': 5,
164
- 'metadata': {
165
- 'user_agent': user_data.get('user_agent', ''),
166
- 'start_timestamp': datetime.now().isoformat()
167
- }
168
- }
169
- if self.bot_id:
170
- session_data['bot_id'] = self.bot_id
171
- session_data['metadata']['bot_id'] = self.bot_id
172
-
173
- response = self.client.table('sales_chat_sessions').insert(session_data).execute()
174
-
175
- session_id = response.data[0]['id']
176
-
177
- # Создаем запись аналитики
178
- await self.create_session_analytics(session_id)
179
-
180
- logger.info(f" Создана новая сессия {session_id} для пользователя {user_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}")
181
- return session_id
182
-
183
- except APIError as e:
184
- logger.error(f" Ошибка при создании сессии: {e}")
185
- raise
186
-
187
- async def close_active_sessions(self, user_id: int):
188
- """Закрывает активные сессии пользователя с учетом bot_id (если указан)"""
189
- try:
190
- # Закрываем только сессии этого бота (если bot_id указан)
191
- query = self.client.table('sales_chat_sessions').update({
192
- 'status': 'completed',
193
- 'updated_at': datetime.now().isoformat()
194
- }).eq('user_id', user_id).eq('status', 'active')
195
-
196
- if self.bot_id:
197
- query = query.eq('bot_id', self.bot_id)
198
-
199
- query.execute()
200
-
201
- logger.info(f"✅ Закрыты активные сессии для пользователя {user_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}")
202
-
203
- except APIError as e:
204
- logger.error(f"❌ Ошибка при закрытии сессий: {e}")
205
- raise
206
-
207
- async def get_active_session(self, telegram_id: int) -> Optional[Dict[str, Any]]:
208
- """Получает активную сессию пользователя с учетом bot_id (если указан)"""
209
- try:
210
- # Ищем активную сессию с учетом bot_id (если указан)
211
- query = self.client.table('sales_chat_sessions').select(
212
- 'id', 'system_prompt', 'created_at', 'current_stage', 'lead_quality_score'
213
- ).eq('user_id', telegram_id).eq('status', 'active')
214
-
215
- if self.bot_id:
216
- query = query.eq('bot_id', self.bot_id)
217
-
218
- response = query.execute()
219
-
220
- if response.data:
221
- session_info = response.data[0]
222
- logger.info(f"✅ Найдена активная сессия {session_info['id']} для пользователя {telegram_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}")
223
- return session_info
224
-
225
- return None
226
-
227
- except APIError as e:
228
- logger.error(f"❌ Ошибка при поиске активной сессии: {e}")
229
- return None
230
-
231
- # =============================================================================
232
- # МЕТОДЫ ДЛЯ РАБОТЫ С СООБЩЕНИЯМИ
233
- # =============================================================================
234
-
235
- async def add_message(
236
- self,
237
- session_id: str,
238
- role: str,
239
- content: str,
240
- message_type: str = 'text',
241
- tokens_used: int = 0,
242
- processing_time_ms: int = 0,
243
- metadata: Dict[str, Any] = None,
244
- ai_metadata: Dict[str, Any] = None
245
- ) -> int:
246
- """Добавляет сообщение в базу данных"""
247
- try:
248
- response = self.client.table('sales_messages').insert({
249
- 'session_id': session_id,
250
- 'role': role,
251
- 'content': content,
252
- 'message_type': message_type,
253
- 'tokens_used': tokens_used,
254
- 'processing_time_ms': processing_time_ms,
255
- 'metadata': metadata or {},
256
- 'ai_metadata': ai_metadata or {}
257
- }).execute()
258
-
259
- message_id = response.data[0]['id']
260
-
261
- # Обновляем аналитику сессии
262
- await self.update_session_analytics(session_id, tokens_used, processing_time_ms)
263
-
264
- logger.debug(f"✅ Добавлено сообщение {message_id} в сессию {session_id}")
265
- return message_id
266
-
267
- except APIError as e:
268
- logger.error(f"❌ Ошибка при добавлении сообщения: {e}")
269
- raise
270
-
271
- async def get_chat_history(self, session_id: str, limit: int = 50) -> List[Dict[str, Any]]:
272
- """Получает историю сообщений для сессии"""
273
- try:
274
- response = self.client.table('sales_messages').select(
275
- 'id', 'role', 'content', 'message_type', 'created_at', 'metadata', 'ai_metadata'
276
- ).eq('session_id', session_id).order('created_at', desc=True).limit(limit).execute()
277
-
278
- # Фильтруем системные сообщения из истории
279
- messages = [msg for msg in response.data if msg['role'] != 'system']
280
-
281
- # Переворачиваем в хронологический порядок (старые -> новые)
282
- messages.reverse()
283
-
284
- logger.debug(f"✅ Получено {len(messages)} сообщений для сессии {session_id}")
285
- return messages
286
-
287
- except APIError as e:
288
- logger.error(f"❌ Ошибка при получении истории: {e}")
289
- raise
290
-
291
- # =============================================================================
292
- # МЕТОДЫ ДЛЯ АНАЛИТИКИ
293
- # =============================================================================
294
-
295
- async def create_session_analytics(self, session_id: str):
296
- """Создает запись аналитики для сессии"""
297
- try:
298
- self.client.table('sales_session_analytics').insert({
299
- 'session_id': session_id,
300
- 'total_messages': 0,
301
- 'total_tokens': 0,
302
- 'average_response_time_ms': 0,
303
- 'conversion_stage': 'initial',
304
- 'lead_quality_score': 5
305
- }).execute()
306
-
307
- logger.debug(f"✅ Создана аналитика для сессии {session_id}")
308
-
309
- except APIError as e:
310
- logger.error(f"❌ Ошибка при создании аналитики: {e}")
311
- raise
312
-
313
- async def update_session_analytics(
314
- self,
315
- session_id: str,
316
- tokens_used: int = 0,
317
- processing_time_ms: int = 0
318
- ):
319
- """Обновляет аналитику сессии"""
320
- try:
321
- # Получаем текущую аналитику
322
- response = self.client.table('sales_session_analytics').select(
323
- 'total_messages', 'total_tokens', 'average_response_time_ms'
324
- ).eq('session_id', session_id).execute()
325
-
326
- if response.data:
327
- current = response.data[0]
328
- new_total_messages = current['total_messages'] + 1
329
- new_total_tokens = current['total_tokens'] + tokens_used
330
-
331
- # Вычисляем среднее время ответа
332
- if processing_time_ms > 0:
333
- current_avg = current['average_response_time_ms']
334
- new_avg = ((current_avg * (new_total_messages - 1)) + processing_time_ms) / new_total_messages
335
- else:
336
- new_avg = current['average_response_time_ms']
337
-
338
- # Обновляем аналитику
339
- self.client.table('sales_session_analytics').update({
340
- 'total_messages': new_total_messages,
341
- 'total_tokens': new_total_tokens,
342
- 'average_response_time_ms': int(new_avg),
343
- 'updated_at': datetime.now().isoformat()
344
- }).eq('session_id', session_id).execute()
345
-
346
- except APIError as e:
347
- logger.error(f"❌ Ошибка при обновлении аналитики: {e}")
348
- # Не прерываем выполнение, аналитика не критична
349
-
350
- async def update_session_stage(self, session_id: str, stage: str = None, quality_score: int = None):
351
- """Обновляет этап сессии и качество лида"""
352
- try:
353
- update_data = {'updated_at': datetime.now().isoformat()}
354
-
355
- if stage:
356
- update_data['current_stage'] = stage
357
- if quality_score is not None:
358
- update_data['lead_quality_score'] = quality_score
359
-
360
- # Дополнительная проверка bot_id при обновлении (если указан)
361
- if self.bot_id:
362
- response = self.client.table('sales_chat_sessions').select('bot_id').eq('id', session_id).execute()
363
- if response.data and response.data[0].get('bot_id') != self.bot_id:
364
- logger.warning(f"⚠️ Попытка обновления сессии {session_id} другого бота")
365
- return
366
-
367
- self.client.table('sales_chat_sessions').update(update_data).eq(
368
- 'id', session_id
369
- ).execute()
370
-
371
- logger.debug(f"✅ Обновлен этап сессии {session_id}: stage={stage}, quality={quality_score}")
372
-
373
- except APIError as e:
374
- logger.error(f" Ошибка при обновлении этапа сессии: {e}")
375
- raise
376
-
377
- # =============================================================================
378
- # МЕТОДЫ ДЛЯ РАБОТЫ С ФАЙЛАМИ
379
- # =============================================================================
380
-
381
- async def get_sent_files(self, user_id: int) -> List[str]:
382
- """Получает список отправленных файлов для пользователя"""
383
- try:
384
- query = self.client.table('sales_users').select('files').eq('telegram_id', user_id)
385
-
386
- if self.bot_id:
387
- query = query.eq('bot_id', self.bot_id)
388
-
389
- response = query.execute()
390
-
391
- if response.data and response.data[0].get('files'):
392
- files_str = response.data[0]['files']
393
- return [f.strip() for f in files_str.split(',') if f.strip()]
394
-
395
- return []
396
-
397
- except Exception as e:
398
- logger.error(f"❌ Ошибка получения отправленных файлов для пользователя {user_id}: {e}")
399
- return []
400
-
401
- async def add_sent_files(self, user_id: int, files_list: List[str]):
402
- """Добавляет файлы в список отправленных для пользователя"""
403
- try:
404
- logger.info(f"📁 Добавление файлов для пользователя {user_id}: {files_list}")
405
-
406
- # Получаем текущий список
407
- current_files = await self.get_sent_files(user_id)
408
- logger.info(f"📁 Текущие файлы в БД: {current_files}")
409
-
410
- # Объединяем с новыми файлами (без дубликатов)
411
- all_files = list(set(current_files + files_list))
412
- logger.info(f"📁 Объединенный список файлов: {all_files}")
413
-
414
- # Сохраняем обратно
415
- files_str = ', '.join(all_files)
416
- logger.info(f"📁 Сохраняем строку: {files_str}")
417
-
418
- query = self.client.table('sales_users').update({
419
- 'files': files_str
420
- }).eq('telegram_id', user_id)
421
-
422
- if self.bot_id:
423
- query = query.eq('bot_id', self.bot_id)
424
- logger.info(f"📁 Фильтр по bot_id: {self.bot_id}")
425
-
426
- response = query.execute()
427
- logger.info(f"📁 Ответ от БД: {response.data}")
428
-
429
- logger.info(f" Добавлено {len(files_list)} файлов для пользователя {user_id}")
430
-
431
- except Exception as e:
432
- logger.error(f"❌ Ошибка добавления отправленных файлов для пользователя {user_id}: {e}")
433
- logger.exception("Полный стек ошибки:")
434
-
435
- # =============================================================================
436
- # МЕТОДЫ ДЛЯ АНАЛИТИКИ И СТАТИСТИКИ
437
- # =============================================================================
438
-
439
- async def get_analytics_summary(self, days: int = 7) -> Dict[str, Any]:
440
- """Получает сводку аналитики за последние дни с учетом bot_id (если указан)"""
441
- try:
442
- cutoff_date = datetime.now() - timedelta(days=days)
443
-
444
- # Получаем сессии с учетом bot_id (если указан)
445
- query = self.client.table('sales_chat_sessions').select(
446
- 'id', 'current_stage', 'lead_quality_score', 'created_at'
447
- ).gte('created_at', cutoff_date.isoformat())
448
-
449
- if self.bot_id:
450
- query = query.eq('bot_id', self.bot_id)
451
-
452
- sessions_response = query.execute()
453
-
454
- sessions = sessions_response.data
455
- total_sessions = len(sessions)
456
-
457
- # Группировка по этапам
458
- stages = {}
459
- quality_scores = []
460
-
461
- for session in sessions:
462
- stage = session.get('current_stage', 'unknown')
463
- stages[stage] = stages.get(stage, 0) + 1
464
-
465
- score = session.get('lead_quality_score', 5)
466
- if score:
467
- quality_scores.append(score)
468
-
469
- avg_quality = sum(quality_scores) / len(quality_scores) if quality_scores else 5
470
-
471
- return {
472
- 'bot_id': self.bot_id,
473
- 'period_days': days,
474
- 'total_sessions': total_sessions,
475
- 'stages': stages,
476
- 'average_lead_quality': round(avg_quality, 1),
477
- 'generated_at': datetime.now().isoformat()
478
- }
479
-
480
- except APIError as e:
481
- logger.error(f"❌ Ошибка при получении аналитики: {e}")
482
- return {
483
- 'bot_id': self.bot_id,
484
- 'error': str(e),
485
- 'generated_at': datetime.now().isoformat()
486
- }
487
-
488
- # =============================================================================
489
- # МЕТОДЫ СОВМЕСТИМОСТИ
490
- # =============================================================================
491
-
492
- async def update_conversion_stage(self, session_id: str, stage: str, quality_score: int = None):
493
- """Обновляет этап конверсии и качество лида (для совместимости)"""
494
- await self.update_session_stage(session_id, stage, quality_score)
495
-
496
- async def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
497
- """Получает информацию о сессии с проверкой bot_id (если указан)"""
498
- try:
499
- response = self.client.table('sales_chat_sessions').select(
500
- 'id', 'user_id', 'bot_id', 'system_prompt', 'status', 'created_at',
501
- 'metadata', 'current_stage', 'lead_quality_score'
502
- ).eq('id', session_id).execute()
503
-
504
- if response.data:
505
- session = response.data[0]
506
- # Дополнительная проверка bot_id для безопасности (если указан)
507
- if self.bot_id and session.get('bot_id') != self.bot_id:
508
- logger.warning(f"⚠️ Попытка доступа к сессии {session_id} другого бота: {session.get('bot_id')} != {self.bot_id}")
509
- return None
510
- return session
511
- return None
512
-
513
- except APIError as e:
514
- logger.error(f"❌ Ошибка при получении информации о сессии: {e}")
515
- raise
1
+ """
2
+ Supabase клиент с автоматической загрузкой настроек из .env файла
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ from datetime import datetime, timedelta
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from dotenv import load_dotenv
12
+ from postgrest.exceptions import APIError
13
+ from project_root_finder import root
14
+ from supabase import create_client
15
+
16
+ PROJECT_ROOT = root
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class SupabaseClient:
22
+ """Клиент для работы с Supabase с автоматической загрузкой настроек из .env"""
23
+
24
+ def __init__(self, bot_id: str):
25
+ """
26
+ Инициализация клиента Supabase с автоматической загрузкой настроек
27
+
28
+ Args:
29
+ bot_id: Идентификатор бота (обязательно) - код сам найдет все настройки
30
+ """
31
+ self.bot_id = bot_id
32
+
33
+ # Автоматически загружаем настройки из .env
34
+ self._load_env_config()
35
+
36
+ # Инициализируем клиент СИНХРОННО прямо в __init__
37
+ self.client = create_client(self.url, self.key)
38
+
39
+ logger.info(f"✅ SupabaseClient инициализирован для bot_id: {self.bot_id}")
40
+
41
+ def _load_env_config(self):
42
+ """Загружает конфигурацию из .env файла"""
43
+ try:
44
+ # Автоматический поиск .env файла
45
+ env_path = self._find_env_file()
46
+
47
+ if not env_path or not env_path.exists():
48
+ raise FileNotFoundError(f".env файл не найден: {env_path}")
49
+
50
+ # Загружаем переменные окружения
51
+ load_dotenv(env_path)
52
+
53
+ # Получаем настройки Supabase
54
+ self.url = os.getenv("SUPABASE_URL")
55
+ self.key = os.getenv("SUPABASE_KEY")
56
+
57
+ if not self.url or not self.key:
58
+ missing_vars = []
59
+ if not self.url:
60
+ missing_vars.append("SUPABASE_URL")
61
+ if not self.key:
62
+ missing_vars.append("SUPABASE_KEY")
63
+ raise ValueError(
64
+ f"Отсутствуют обязательные переменные в .env: {', '.join(missing_vars)}"
65
+ )
66
+
67
+ logger.info(f" Настройки Supabase загружены из {env_path}")
68
+
69
+ except Exception as e:
70
+ logger.error(f"❌ Ошибка загрузки конфигурации Supabase: {e}")
71
+ raise
72
+
73
+ def _find_env_file(self) -> Optional[Path]:
74
+ """Автоматически находит .env файл для указанного бота"""
75
+ # Ищем .env файл в папке конкретного бота
76
+ bot_env_path = PROJECT_ROOT / "bots" / self.bot_id / ".env"
77
+
78
+ if bot_env_path.exists():
79
+ logger.info(f"🔍 Найден .env файл для бота {self.bot_id}: {bot_env_path}")
80
+ return bot_env_path
81
+
82
+ logger.error(f"❌ .env файл не найден для бота {self.bot_id}")
83
+ logger.error(f" Искали в: {bot_env_path}")
84
+ return None
85
+
86
+ # =============================================================================
87
+ # МЕТОДЫ ДЛЯ РАБОТЫ С ПОЛЬЗОВАТЕЛЯМИ
88
+ # =============================================================================
89
+
90
+ async def create_or_get_user(self, user_data: Dict[str, Any]) -> int:
91
+ """Создает или получает пользователя с учетом bot_id (если указан)"""
92
+ try:
93
+ # Если bot_id указан, фильтруем по нему
94
+ query = (
95
+ self.client.table("sales_users")
96
+ .select("telegram_id")
97
+ .eq("telegram_id", user_data["telegram_id"])
98
+ )
99
+ if self.bot_id:
100
+ query = query.eq("bot_id", self.bot_id)
101
+
102
+ response = query.execute()
103
+
104
+ if response.data:
105
+ # Обновляем данные существующего пользователя
106
+ update_query = (
107
+ self.client.table("sales_users")
108
+ .update(
109
+ {
110
+ "username": user_data.get("username"),
111
+ "first_name": user_data.get("first_name"),
112
+ "last_name": user_data.get("last_name"),
113
+ "language_code": user_data.get("language_code"),
114
+ "updated_at": datetime.now().isoformat(),
115
+ "is_active": True,
116
+ }
117
+ )
118
+ .eq("telegram_id", user_data["telegram_id"])
119
+ )
120
+
121
+ if self.bot_id:
122
+ update_query = update_query.eq("bot_id", self.bot_id)
123
+
124
+ update_query.execute()
125
+
126
+ logger.info(
127
+ f"✅ Обновлен пользователь {user_data['telegram_id']}{f' для bot_id {self.bot_id}' if self.bot_id else ''}"
128
+ )
129
+ return user_data["telegram_id"]
130
+ else:
131
+ # Создаем нового пользователя с bot_id (если указан)
132
+ user_insert_data = {
133
+ "telegram_id": user_data["telegram_id"],
134
+ "username": user_data.get("username"),
135
+ "first_name": user_data.get("first_name"),
136
+ "last_name": user_data.get("last_name"),
137
+ "language_code": user_data.get("language_code"),
138
+ "is_active": True,
139
+ "source": user_data.get("source"),
140
+ "medium": user_data.get("medium"),
141
+ "campaign": user_data.get("campaign"),
142
+ "content": user_data.get("content"),
143
+ "term": user_data.get("term"),
144
+ }
145
+ if self.bot_id:
146
+ user_insert_data["bot_id"] = self.bot_id
147
+
148
+ response = (
149
+ self.client.table("sales_users").insert(user_insert_data).execute()
150
+ )
151
+
152
+ logger.info(
153
+ f"✅ Создан новый пользователь {user_data['telegram_id']}{f' для bot_id {self.bot_id}' if self.bot_id else ''}"
154
+ )
155
+ return user_data["telegram_id"]
156
+
157
+ except APIError as e:
158
+ logger.error(f"❌ Ошибка при работе с пользователем: {e}")
159
+ raise
160
+
161
+ # =============================================================================
162
+ # МЕТОДЫ ДЛЯ РАБОТЫ С СЕССИЯМИ
163
+ # =============================================================================
164
+
165
+ async def create_chat_session(
166
+ self, user_data: Dict[str, Any], system_prompt: str
167
+ ) -> str:
168
+ """Создает новую сессию чата с учетом bot_id (если указан)"""
169
+ try:
170
+ # Создаем или обновляем пользователя
171
+ user_id = await self.create_or_get_user(user_data)
172
+
173
+ # Завершаем активные сессии пользователя (с учетом bot_id)
174
+ await self.close_active_sessions(user_id)
175
+
176
+ # Создаем новую сессию с bot_id (если указан)
177
+ session_data = {
178
+ "user_id": user_id,
179
+ "system_prompt": system_prompt,
180
+ "status": "active",
181
+ "current_stage": "introduction",
182
+ "lead_quality_score": 5,
183
+ "metadata": {
184
+ "user_agent": user_data.get("user_agent", ""),
185
+ "start_timestamp": datetime.now().isoformat(),
186
+ },
187
+ }
188
+ if self.bot_id:
189
+ session_data["bot_id"] = self.bot_id
190
+ session_data["metadata"]["bot_id"] = self.bot_id
191
+
192
+ response = (
193
+ self.client.table("sales_chat_sessions").insert(session_data).execute()
194
+ )
195
+
196
+ session_id = response.data[0]["id"]
197
+
198
+ # Создаем запись аналитики
199
+ await self.create_session_analytics(session_id)
200
+
201
+ logger.info(
202
+ f"✅ Создана новая сессия {session_id} для пользователя {user_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}"
203
+ )
204
+ return session_id
205
+
206
+ except APIError as e:
207
+ logger.error(f"❌ Ошибка при создании сессии: {e}")
208
+ raise
209
+
210
+ async def close_active_sessions(self, user_id: int):
211
+ """Закрывает активные сессии пользователя с учетом bot_id (если указан)"""
212
+ try:
213
+ # Закрываем только сессии этого бота (если bot_id указан)
214
+ query = (
215
+ self.client.table("sales_chat_sessions")
216
+ .update(
217
+ {"status": "completed", "updated_at": datetime.now().isoformat()}
218
+ )
219
+ .eq("user_id", user_id)
220
+ .eq("status", "active")
221
+ )
222
+
223
+ if self.bot_id:
224
+ query = query.eq("bot_id", self.bot_id)
225
+
226
+ query.execute()
227
+
228
+ logger.info(
229
+ f"✅ Закрыты активные сессии для пользователя {user_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}"
230
+ )
231
+
232
+ except APIError as e:
233
+ logger.error(f"❌ Ошибка при закрытии сессий: {e}")
234
+ raise
235
+
236
+ async def get_active_session(self, telegram_id: int) -> Optional[Dict[str, Any]]:
237
+ """Получает активную сессию пользователя с учетом bot_id (если указан)"""
238
+ try:
239
+ # Ищем активную сессию с учетом bot_id (если указан)
240
+ query = (
241
+ self.client.table("sales_chat_sessions")
242
+ .select(
243
+ "id",
244
+ "system_prompt",
245
+ "created_at",
246
+ "current_stage",
247
+ "lead_quality_score",
248
+ )
249
+ .eq("user_id", telegram_id)
250
+ .eq("status", "active")
251
+ )
252
+
253
+ if self.bot_id:
254
+ query = query.eq("bot_id", self.bot_id)
255
+
256
+ response = query.execute()
257
+
258
+ if response.data:
259
+ session_info = response.data[0]
260
+ logger.info(
261
+ f"✅ Найдена активная сессия {session_info['id']} для пользователя {telegram_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}"
262
+ )
263
+ return session_info
264
+
265
+ return None
266
+
267
+ except APIError as e:
268
+ logger.error(f"❌ Ошибка при поиске активной сессии: {e}")
269
+ return None
270
+
271
+ # =============================================================================
272
+ # МЕТОДЫ ДЛЯ РАБОТЫ С СООБЩЕНИЯМИ
273
+ # =============================================================================
274
+
275
+ async def add_message(
276
+ self,
277
+ session_id: str,
278
+ role: str,
279
+ content: str,
280
+ message_type: str = "text",
281
+ tokens_used: int = 0,
282
+ processing_time_ms: int = 0,
283
+ metadata: Dict[str, Any] = None,
284
+ ai_metadata: Dict[str, Any] = None,
285
+ ) -> int:
286
+ """Добавляет сообщение в базу данных"""
287
+ try:
288
+ response = (
289
+ self.client.table("sales_messages")
290
+ .insert(
291
+ {
292
+ "session_id": session_id,
293
+ "role": role,
294
+ "content": content,
295
+ "message_type": message_type,
296
+ "tokens_used": tokens_used,
297
+ "processing_time_ms": processing_time_ms,
298
+ "metadata": metadata or {},
299
+ "ai_metadata": ai_metadata or {},
300
+ }
301
+ )
302
+ .execute()
303
+ )
304
+
305
+ message_id = response.data[0]["id"]
306
+
307
+ # Обновляем аналитику сессии
308
+ await self.update_session_analytics(
309
+ session_id, tokens_used, processing_time_ms
310
+ )
311
+
312
+ logger.debug(f"✅ Добавлено сообщение {message_id} в сессию {session_id}")
313
+ return message_id
314
+
315
+ except APIError as e:
316
+ logger.error(f"❌ Ошибка при добавлении сообщения: {e}")
317
+ raise
318
+
319
+ async def get_chat_history(
320
+ self, session_id: str, limit: int = 50
321
+ ) -> List[Dict[str, Any]]:
322
+ """Получает историю сообщений для сессии"""
323
+ try:
324
+ response = (
325
+ self.client.table("sales_messages")
326
+ .select(
327
+ "id",
328
+ "role",
329
+ "content",
330
+ "message_type",
331
+ "created_at",
332
+ "metadata",
333
+ "ai_metadata",
334
+ )
335
+ .eq("session_id", session_id)
336
+ .order("created_at", desc=True)
337
+ .limit(limit)
338
+ .execute()
339
+ )
340
+
341
+ # Фильтруем системные сообщения из истории
342
+ messages = [msg for msg in response.data if msg["role"] != "system"]
343
+
344
+ # Переворачиваем в хронологический порядок (старые -> новые)
345
+ messages.reverse()
346
+
347
+ logger.debug(
348
+ f"✅ Получено {len(messages)} сообщений для сессии {session_id}"
349
+ )
350
+ return messages
351
+
352
+ except APIError as e:
353
+ logger.error(f"❌ Ошибка при получении истории: {e}")
354
+ raise
355
+
356
+ # =============================================================================
357
+ # МЕТОДЫ ДЛЯ АНАЛИТИКИ
358
+ # =============================================================================
359
+
360
+ async def create_session_analytics(self, session_id: str):
361
+ """Создает запись аналитики для сессии"""
362
+ try:
363
+ self.client.table("sales_session_analytics").insert(
364
+ {
365
+ "session_id": session_id,
366
+ "total_messages": 0,
367
+ "total_tokens": 0,
368
+ "average_response_time_ms": 0,
369
+ "conversion_stage": "initial",
370
+ "lead_quality_score": 5,
371
+ }
372
+ ).execute()
373
+
374
+ logger.debug(f" Создана аналитика для сессии {session_id}")
375
+
376
+ except APIError as e:
377
+ logger.error(f"❌ Ошибка при создании аналитики: {e}")
378
+ raise
379
+
380
+ async def update_session_analytics(
381
+ self, session_id: str, tokens_used: int = 0, processing_time_ms: int = 0
382
+ ):
383
+ """Обновляет аналитику сессии"""
384
+ try:
385
+ # Получаем текущую аналитику
386
+ response = (
387
+ self.client.table("sales_session_analytics")
388
+ .select("total_messages", "total_tokens", "average_response_time_ms")
389
+ .eq("session_id", session_id)
390
+ .execute()
391
+ )
392
+
393
+ if response.data:
394
+ current = response.data[0]
395
+ new_total_messages = current["total_messages"] + 1
396
+ new_total_tokens = current["total_tokens"] + tokens_used
397
+
398
+ # Вычисляем среднее время ответа
399
+ if processing_time_ms > 0:
400
+ current_avg = current["average_response_time_ms"]
401
+ new_avg = (
402
+ (current_avg * (new_total_messages - 1)) + processing_time_ms
403
+ ) / new_total_messages
404
+ else:
405
+ new_avg = current["average_response_time_ms"]
406
+
407
+ # Обновляем аналитику
408
+ self.client.table("sales_session_analytics").update(
409
+ {
410
+ "total_messages": new_total_messages,
411
+ "total_tokens": new_total_tokens,
412
+ "average_response_time_ms": int(new_avg),
413
+ "updated_at": datetime.now().isoformat(),
414
+ }
415
+ ).eq("session_id", session_id).execute()
416
+
417
+ except APIError as e:
418
+ logger.error(f"❌ Ошибка при обновлении аналитики: {e}")
419
+ # Не прерываем выполнение, аналитика не критична
420
+
421
+ async def update_session_stage(
422
+ self, session_id: str, stage: str = None, quality_score: int = None
423
+ ):
424
+ """Обновляет этап сессии и качество лида"""
425
+ try:
426
+ update_data = {"updated_at": datetime.now().isoformat()}
427
+
428
+ if stage:
429
+ update_data["current_stage"] = stage
430
+ if quality_score is not None:
431
+ update_data["lead_quality_score"] = quality_score
432
+
433
+ # Дополнительная проверка bot_id при обновлении (если указан)
434
+ if self.bot_id:
435
+ response = (
436
+ self.client.table("sales_chat_sessions")
437
+ .select("bot_id")
438
+ .eq("id", session_id)
439
+ .execute()
440
+ )
441
+ if response.data and response.data[0].get("bot_id") != self.bot_id:
442
+ logger.warning(
443
+ f"⚠️ Попытка обновления сессии {session_id} другого бота"
444
+ )
445
+ return
446
+
447
+ self.client.table("sales_chat_sessions").update(update_data).eq(
448
+ "id", session_id
449
+ ).execute()
450
+
451
+ logger.debug(
452
+ f"✅ Обновлен этап сессии {session_id}: stage={stage}, quality={quality_score}"
453
+ )
454
+
455
+ except APIError as e:
456
+ logger.error(f"❌ Ошибка при обновлении этапа сессии: {e}")
457
+ raise
458
+
459
+ # =============================================================================
460
+ # МЕТОДЫ ДЛЯ РАБОТЫ С ФАЙЛАМИ
461
+ # =============================================================================
462
+
463
+ async def get_sent_files(self, user_id: int) -> List[str]:
464
+ """Получает список отправленных файлов для пользователя"""
465
+ try:
466
+ query = (
467
+ self.client.table("sales_users")
468
+ .select("files")
469
+ .eq("telegram_id", user_id)
470
+ )
471
+
472
+ if self.bot_id:
473
+ query = query.eq("bot_id", self.bot_id)
474
+
475
+ response = query.execute()
476
+
477
+ if response.data and response.data[0].get("files"):
478
+ files_str = response.data[0]["files"]
479
+ return [f.strip() for f in files_str.split(",") if f.strip()]
480
+
481
+ return []
482
+
483
+ except Exception as e:
484
+ logger.error(
485
+ f"❌ Ошибка получения отправленных файлов для пользователя {user_id}: {e}"
486
+ )
487
+ return []
488
+
489
+ async def add_sent_files(self, user_id: int, files_list: List[str]):
490
+ """Добавляет файлы в список отправленных для пользователя"""
491
+ try:
492
+ logger.info(
493
+ f"📁 Добавление файлов для пользователя {user_id}: {files_list}"
494
+ )
495
+
496
+ # Получаем текущий список
497
+ current_files = await self.get_sent_files(user_id)
498
+ logger.info(f"📁 Текущие файлы в БД: {current_files}")
499
+
500
+ # Объединяем с новыми файлами (без дубликатов)
501
+ all_files = list(set(current_files + files_list))
502
+ logger.info(f"📁 Объединенный список файлов: {all_files}")
503
+
504
+ # Сохраняем обратно
505
+ files_str = ", ".join(all_files)
506
+ logger.info(f"📁 Сохраняем строку: {files_str}")
507
+
508
+ query = (
509
+ self.client.table("sales_users")
510
+ .update({"files": files_str})
511
+ .eq("telegram_id", user_id)
512
+ )
513
+
514
+ if self.bot_id:
515
+ query = query.eq("bot_id", self.bot_id)
516
+ logger.info(f"📁 Фильтр по bot_id: {self.bot_id}")
517
+
518
+ response = query.execute()
519
+ logger.info(f"📁 Ответ от БД: {response.data}")
520
+
521
+ logger.info(
522
+ f"✅ Добавлено {len(files_list)} файлов для пользователя {user_id}"
523
+ )
524
+
525
+ except Exception as e:
526
+ logger.error(
527
+ f"❌ Ошибка добавления отправленных файлов для пользователя {user_id}: {e}"
528
+ )
529
+ logger.exception("Полный стек ошибки:")
530
+
531
+ # =============================================================================
532
+ # МЕТОДЫ ДЛЯ АНАЛИТИКИ И СТАТИСТИКИ
533
+ # =============================================================================
534
+
535
+ async def get_analytics_summary(self, days: int = 7) -> Dict[str, Any]:
536
+ """Получает сводку аналитики за последние дни с учетом bot_id (если указан)"""
537
+ try:
538
+ cutoff_date = datetime.now() - timedelta(days=days)
539
+
540
+ # Получаем сессии с учетом bot_id (если указан)
541
+ query = (
542
+ self.client.table("sales_chat_sessions")
543
+ .select("id", "current_stage", "lead_quality_score", "created_at")
544
+ .gte("created_at", cutoff_date.isoformat())
545
+ )
546
+
547
+ if self.bot_id:
548
+ query = query.eq("bot_id", self.bot_id)
549
+
550
+ sessions_response = query.execute()
551
+
552
+ sessions = sessions_response.data
553
+ total_sessions = len(sessions)
554
+
555
+ # Группировка по этапам
556
+ stages = {}
557
+ quality_scores = []
558
+
559
+ for session in sessions:
560
+ stage = session.get("current_stage", "unknown")
561
+ stages[stage] = stages.get(stage, 0) + 1
562
+
563
+ score = session.get("lead_quality_score", 5)
564
+ if score:
565
+ quality_scores.append(score)
566
+
567
+ avg_quality = (
568
+ sum(quality_scores) / len(quality_scores) if quality_scores else 5
569
+ )
570
+
571
+ return {
572
+ "bot_id": self.bot_id,
573
+ "period_days": days,
574
+ "total_sessions": total_sessions,
575
+ "stages": stages,
576
+ "average_lead_quality": round(avg_quality, 1),
577
+ "generated_at": datetime.now().isoformat(),
578
+ }
579
+
580
+ except APIError as e:
581
+ logger.error(f"❌ Ошибка при получении аналитики: {e}")
582
+ return {
583
+ "bot_id": self.bot_id,
584
+ "error": str(e),
585
+ "generated_at": datetime.now().isoformat(),
586
+ }
587
+
588
+ # =============================================================================
589
+ # МЕТОДЫ СОВМЕСТИМОСТИ
590
+ # =============================================================================
591
+
592
+ async def update_conversion_stage(
593
+ self, session_id: str, stage: str, quality_score: int = None
594
+ ):
595
+ """Обновляет этап конверсии и качество лида (для совместимости)"""
596
+ await self.update_session_stage(session_id, stage, quality_score)
597
+
598
+ async def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
599
+ """Получает информацию о сессии с проверкой bot_id (если указан)"""
600
+ try:
601
+ response = (
602
+ self.client.table("sales_chat_sessions")
603
+ .select(
604
+ "id",
605
+ "user_id",
606
+ "bot_id",
607
+ "system_prompt",
608
+ "status",
609
+ "created_at",
610
+ "metadata",
611
+ "current_stage",
612
+ "lead_quality_score",
613
+ )
614
+ .eq("id", session_id)
615
+ .execute()
616
+ )
617
+
618
+ if response.data:
619
+ session = response.data[0]
620
+ # Дополнительная проверка bot_id для безопасности (если указан)
621
+ if self.bot_id and session.get("bot_id") != self.bot_id:
622
+ logger.warning(
623
+ f"⚠️ Попытка доступа к сессии {session_id} другого бота: {session.get('bot_id')} != {self.bot_id}"
624
+ )
625
+ return None
626
+ return session
627
+ return None
628
+
629
+ except APIError as e:
630
+ logger.error(f"❌ Ошибка при получении информации о сессии: {e}")
631
+ raise