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