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
@@ -2,19 +2,21 @@
2
2
 
3
3
  import logging
4
4
  from datetime import datetime, timedelta, timezone
5
- from typing import List, Dict, Any, Optional
6
- from supabase import create_client, Client
5
+ from typing import Any, Dict, List, Optional
6
+
7
7
  from postgrest.exceptions import APIError
8
+ from supabase import Client, create_client
8
9
 
9
10
  logger = logging.getLogger(__name__)
10
11
 
12
+
11
13
  class SupabaseClient:
12
14
  """Клиент для работы с Supabase с поддержкой bot_id для мультиботовой архитектуры"""
13
-
15
+
14
16
  def __init__(self, url: str, key: str, bot_id: str = None):
15
17
  """
16
18
  Инициализация клиента Supabase
17
-
19
+
18
20
  Args:
19
21
  url: URL Supabase проекта
20
22
  key: API ключ Supabase
@@ -24,211 +26,264 @@ class SupabaseClient:
24
26
  self.key = key
25
27
  self.bot_id = bot_id # 🆕 Теперь опционально!
26
28
  self.client: Optional[Client] = None
27
-
29
+
28
30
  if self.bot_id:
29
31
  logger.info(f"Инициализация SupabaseClient для bot_id: {self.bot_id}")
30
32
  else:
31
- logger.warning("SupabaseClient инициализирован БЕЗ bot_id - мультиботовая изоляция отключена")
32
-
33
+ logger.warning(
34
+ "SupabaseClient инициализирован БЕЗ bot_id - мультиботовая изоляция отключена"
35
+ )
36
+
33
37
  async def initialize(self):
34
38
  """Инициализация клиента Supabase"""
35
39
  try:
36
40
  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 ''}")
41
+ logger.info(
42
+ f"Supabase client инициализирован{f' для bot_id: {self.bot_id}' if self.bot_id else ''}"
43
+ )
38
44
  except Exception as e:
39
45
  logger.error(f"Ошибка инициализации Supabase client: {e}")
40
46
  raise
41
-
47
+
42
48
  async def create_or_get_user(self, user_data: Dict[str, Any]) -> int:
43
49
  """Создает или получает пользователя с учетом bot_id (если указан)"""
44
50
  try:
45
51
  # 🆕 Если bot_id указан, фильтруем по нему
46
- query = self.client.table('sales_users').select('telegram_id').eq(
47
- 'telegram_id', user_data['telegram_id']
52
+ query = (
53
+ self.client.table("sales_users")
54
+ .select("telegram_id")
55
+ .eq("telegram_id", user_data["telegram_id"])
48
56
  )
49
57
  if self.bot_id:
50
- query = query.eq('bot_id', self.bot_id)
51
-
58
+ query = query.eq("bot_id", self.bot_id)
59
+
52
60
  response = query.execute()
53
-
61
+
54
62
  if response.data:
55
63
  # Получаем текущие данные пользователя для мержинга UTM и сегментов
56
- existing_user_query = self.client.table('sales_users').select(
57
- 'source', 'medium', 'campaign', 'content', 'term', 'segments'
58
- ).eq('telegram_id', user_data['telegram_id'])
59
-
64
+ existing_user_query = (
65
+ self.client.table("sales_users")
66
+ .select(
67
+ "source", "medium", "campaign", "content", "term", "segments"
68
+ )
69
+ .eq("telegram_id", user_data["telegram_id"])
70
+ )
71
+
60
72
  if self.bot_id:
61
- existing_user_query = existing_user_query.eq('bot_id', self.bot_id)
62
-
73
+ existing_user_query = existing_user_query.eq("bot_id", self.bot_id)
74
+
63
75
  existing_response = existing_user_query.execute()
64
- existing_utm = existing_response.data[0] if existing_response.data else {}
65
-
76
+ existing_utm = (
77
+ existing_response.data[0] if existing_response.data else {}
78
+ )
79
+
66
80
  # Формируем данные для обновления
67
81
  update_data = {
68
- 'username': user_data.get('username'),
69
- 'first_name': user_data.get('first_name'),
70
- 'last_name': user_data.get('last_name'),
71
- 'language_code': user_data.get('language_code'),
72
- 'updated_at': datetime.now().isoformat(),
73
- 'is_active': True
82
+ "username": user_data.get("username"),
83
+ "first_name": user_data.get("first_name"),
84
+ "last_name": user_data.get("last_name"),
85
+ "language_code": user_data.get("language_code"),
86
+ "updated_at": datetime.now().isoformat(),
87
+ "is_active": True,
74
88
  }
75
-
89
+
76
90
  # Мержим UTM данные: обновляем только если новое значение не None
77
- utm_fields = ['source', 'medium', 'campaign', 'content', 'term']
91
+ utm_fields = ["source", "medium", "campaign", "content", "term"]
78
92
  for field in utm_fields:
79
93
  new_value = user_data.get(field)
80
94
  if new_value is not None:
81
95
  # Есть новое значение - обновляем
82
96
  update_data[field] = new_value
83
97
  if existing_utm.get(field) != new_value:
84
- logger.info(f"📊 UTM обновление: {field} = '{new_value}' (было: '{existing_utm.get(field)}')")
98
+ logger.info(
99
+ f"📊 UTM обновление: {field} = '{new_value}' (было: '{existing_utm.get(field)}')"
100
+ )
85
101
  else:
86
102
  # Нового значения нет - сохраняем старое
87
103
  update_data[field] = existing_utm.get(field)
88
-
104
+
89
105
  # Обрабатываем сегменты с накоплением через запятую
90
- new_segment = user_data.get('segment')
106
+ new_segment = user_data.get("segment")
91
107
  if new_segment:
92
- existing_segments = existing_utm.get('segments', '') or ''
108
+ existing_segments = existing_utm.get("segments", "") or ""
93
109
  if existing_segments:
94
110
  # Разбираем существующие сегменты
95
- segments_list = [s.strip() for s in existing_segments.split(',') if s.strip()]
111
+ segments_list = [
112
+ s.strip() for s in existing_segments.split(",") if s.strip()
113
+ ]
96
114
  # Добавляем новый сегмент, если его еще нет
97
115
  if new_segment not in segments_list:
98
116
  segments_list.append(new_segment)
99
- update_data['segments'] = ', '.join(segments_list)
100
- logger.info(f"📊 Сегмент добавлен: '{new_segment}' (было: '{existing_segments}')")
117
+ update_data["segments"] = ", ".join(segments_list)
118
+ logger.info(
119
+ f"📊 Сегмент добавлен: '{new_segment}' (было: '{existing_segments}')"
120
+ )
101
121
  else:
102
- update_data['segments'] = existing_segments
122
+ update_data["segments"] = existing_segments
103
123
  logger.info(f"📊 Сегмент '{new_segment}' уже существует")
104
124
  else:
105
125
  # Первый сегмент
106
- update_data['segments'] = new_segment
126
+ update_data["segments"] = new_segment
107
127
  logger.info(f"📊 Первый сегмент добавлен: '{new_segment}'")
108
128
  else:
109
129
  # Нового сегмента нет - сохраняем старое значение
110
- update_data['segments'] = existing_utm.get('segments')
111
-
130
+ update_data["segments"] = existing_utm.get("segments")
131
+
112
132
  # Обновляем пользователя
113
- update_query = self.client.table('sales_users').update(update_data).eq('telegram_id', user_data['telegram_id'])
114
-
133
+ update_query = (
134
+ self.client.table("sales_users")
135
+ .update(update_data)
136
+ .eq("telegram_id", user_data["telegram_id"])
137
+ )
138
+
115
139
  if self.bot_id:
116
- update_query = update_query.eq('bot_id', self.bot_id)
117
-
140
+ update_query = update_query.eq("bot_id", self.bot_id)
141
+
118
142
  update_query.execute()
119
-
120
- logger.info(f"Обновлен пользователь {user_data['telegram_id']}{f' для bot_id {self.bot_id}' if self.bot_id else ''}")
121
- return user_data['telegram_id']
143
+
144
+ logger.info(
145
+ f"Обновлен пользователь {user_data['telegram_id']}{f' для bot_id {self.bot_id}' if self.bot_id else ''}"
146
+ )
147
+ return user_data["telegram_id"]
122
148
  else:
123
149
  # 🆕 Создаем нового пользователя с bot_id (если указан)
124
150
  user_insert_data = {
125
- 'telegram_id': user_data['telegram_id'],
126
- 'username': user_data.get('username'),
127
- 'first_name': user_data.get('first_name'),
128
- 'last_name': user_data.get('last_name'),
129
- 'language_code': user_data.get('language_code'),
130
- 'is_active': True,
131
- 'source': user_data.get('source'),
132
- 'medium': user_data.get('medium'),
133
- 'campaign': user_data.get('campaign'),
134
- 'content': user_data.get('content'),
135
- 'term': user_data.get('term'),
136
- 'segments': user_data.get('segment'), # Первый сегмент при создании
151
+ "telegram_id": user_data["telegram_id"],
152
+ "username": user_data.get("username"),
153
+ "first_name": user_data.get("first_name"),
154
+ "last_name": user_data.get("last_name"),
155
+ "language_code": user_data.get("language_code"),
156
+ "is_active": True,
157
+ "source": user_data.get("source"),
158
+ "medium": user_data.get("medium"),
159
+ "campaign": user_data.get("campaign"),
160
+ "content": user_data.get("content"),
161
+ "term": user_data.get("term"),
162
+ "segments": user_data.get("segment"), # Первый сегмент при создании
137
163
  }
138
164
  if self.bot_id:
139
- user_insert_data['bot_id'] = self.bot_id
140
-
141
- response = self.client.table('sales_users').insert(user_insert_data).execute()
142
-
143
- if user_data.get('segment'):
144
- logger.info(f"Создан новый пользователь {user_data['telegram_id']} с сегментом '{user_data.get('segment')}'{f' для bot_id {self.bot_id}' if self.bot_id else ''}")
165
+ user_insert_data["bot_id"] = self.bot_id
166
+
167
+ response = (
168
+ self.client.table("sales_users").insert(user_insert_data).execute()
169
+ )
170
+
171
+ if user_data.get("segment"):
172
+ logger.info(
173
+ f"Создан новый пользователь {user_data['telegram_id']} с сегментом '{user_data.get('segment')}'{f' для bot_id {self.bot_id}' if self.bot_id else ''}"
174
+ )
145
175
  else:
146
- logger.info(f"Создан новый пользователь {user_data['telegram_id']}{f' для bot_id {self.bot_id}' if self.bot_id else ''}")
147
- return user_data['telegram_id']
148
-
176
+ logger.info(
177
+ f"Создан новый пользователь {user_data['telegram_id']}{f' для bot_id {self.bot_id}' if self.bot_id else ''}"
178
+ )
179
+ return user_data["telegram_id"]
180
+
149
181
  except APIError as e:
150
182
  logger.error(f"Ошибка при работе с пользователем: {e}")
151
183
  raise
152
-
153
- async def create_chat_session(self, user_data: Dict[str, Any], system_prompt: str) -> str:
184
+
185
+ async def create_chat_session(
186
+ self, user_data: Dict[str, Any], system_prompt: str
187
+ ) -> str:
154
188
  """Создает новую сессию чата с учетом bot_id (если указан)"""
155
189
  try:
156
190
  # Создаем или обновляем пользователя
157
191
  user_id = await self.create_or_get_user(user_data)
158
-
192
+
159
193
  # 🆕 Завершаем активные сессии пользователя (с учетом bot_id)
160
194
  await self.close_active_sessions(user_id)
161
-
195
+
162
196
  # 🆕 Создаем новую сессию с bot_id (если указан)
163
197
  session_data = {
164
- 'user_id': user_id,
165
- 'system_prompt': system_prompt,
166
- 'status': 'active',
167
- 'current_stage': 'introduction',
168
- 'lead_quality_score': 5,
169
- 'metadata': {
170
- 'user_agent': user_data.get('user_agent', ''),
171
- 'start_timestamp': datetime.now().isoformat()
172
- }
198
+ "user_id": user_id,
199
+ "system_prompt": system_prompt,
200
+ "status": "active",
201
+ "current_stage": "introduction",
202
+ "lead_quality_score": 5,
203
+ "metadata": {
204
+ "user_agent": user_data.get("user_agent", ""),
205
+ "start_timestamp": datetime.now().isoformat(),
206
+ },
173
207
  }
174
208
  if self.bot_id:
175
- session_data['bot_id'] = self.bot_id
176
- session_data['metadata']['bot_id'] = self.bot_id
177
-
178
- response = self.client.table('sales_chat_sessions').insert(session_data).execute()
179
-
180
- session_id = response.data[0]['id']
181
-
209
+ session_data["bot_id"] = self.bot_id
210
+ session_data["metadata"]["bot_id"] = self.bot_id
211
+
212
+ response = (
213
+ self.client.table("sales_chat_sessions").insert(session_data).execute()
214
+ )
215
+
216
+ session_id = response.data[0]["id"]
217
+
182
218
  # Создаем запись аналитики
183
219
  await self.create_session_analytics(session_id)
184
-
185
- logger.info(f"Создана новая сессия {session_id} для пользователя {user_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}")
220
+
221
+ logger.info(
222
+ f"Создана новая сессия {session_id} для пользователя {user_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}"
223
+ )
186
224
  return session_id
187
-
225
+
188
226
  except APIError as e:
189
227
  logger.error(f"Ошибка при создании сессии: {e}")
190
228
  raise
191
-
229
+
192
230
  async def close_active_sessions(self, user_id: int):
193
231
  """Закрывает активные сессии пользователя с учетом bot_id (если указан)"""
194
232
  try:
195
233
  # 🆕 Закрываем только сессии этого бота (если bot_id указан)
196
- query = self.client.table('sales_chat_sessions').update({
197
- 'status': 'completed',
198
- 'updated_at': datetime.now().isoformat()
199
- }).eq('user_id', user_id).eq('status', 'active')
200
-
234
+ query = (
235
+ self.client.table("sales_chat_sessions")
236
+ .update(
237
+ {"status": "completed", "updated_at": datetime.now().isoformat()}
238
+ )
239
+ .eq("user_id", user_id)
240
+ .eq("status", "active")
241
+ )
242
+
201
243
  if self.bot_id:
202
- query = query.eq('bot_id', self.bot_id)
203
-
244
+ query = query.eq("bot_id", self.bot_id)
245
+
204
246
  query.execute()
205
-
206
- logger.info(f"Закрыты активные сессии для пользователя {user_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}")
207
-
247
+
248
+ logger.info(
249
+ f"Закрыты активные сессии для пользователя {user_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}"
250
+ )
251
+
208
252
  except APIError as e:
209
253
  logger.error(f"Ошибка при закрытии сессий: {e}")
210
254
  raise
211
-
255
+
212
256
  async def get_active_session(self, telegram_id: int) -> Optional[Dict[str, Any]]:
213
257
  """Получает активную сессию пользователя с учетом bot_id (если указан)"""
214
258
  try:
215
259
  # 🆕 Ищем активную сессию с учетом bot_id (если указан)
216
- query = self.client.table('sales_chat_sessions').select(
217
- 'id', 'system_prompt', 'created_at', 'current_stage', 'lead_quality_score'
218
- ).eq('user_id', telegram_id).eq('status', 'active')
219
-
260
+ query = (
261
+ self.client.table("sales_chat_sessions")
262
+ .select(
263
+ "id",
264
+ "system_prompt",
265
+ "created_at",
266
+ "current_stage",
267
+ "lead_quality_score",
268
+ )
269
+ .eq("user_id", telegram_id)
270
+ .eq("status", "active")
271
+ )
272
+
220
273
  if self.bot_id:
221
- query = query.eq('bot_id', self.bot_id)
222
-
274
+ query = query.eq("bot_id", self.bot_id)
275
+
223
276
  response = query.execute()
224
-
277
+
225
278
  if response.data:
226
279
  session_info = response.data[0]
227
- logger.info(f"Найдена активная сессия {session_info['id']} для пользователя {telegram_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}")
280
+ logger.info(
281
+ f"Найдена активная сессия {session_info['id']} для пользователя {telegram_id}{f', bot_id {self.bot_id}' if self.bot_id else ''}"
282
+ )
228
283
  return session_info
229
-
284
+
230
285
  return None
231
-
286
+
232
287
  except APIError as e:
233
288
  logger.error(f"Ошибка при поиске активной сессии: {e}")
234
289
  return None
@@ -236,825 +291,1057 @@ class SupabaseClient:
236
291
  async def create_session_analytics(self, session_id: str):
237
292
  """Создает запись аналитики для сессии"""
238
293
  try:
239
- self.client.table('sales_session_analytics').insert({
240
- 'session_id': session_id,
241
- 'total_messages': 0,
242
- 'total_tokens': 0,
243
- 'average_response_time_ms': 0,
244
- 'conversion_stage': 'initial',
245
- 'lead_quality_score': 5
246
- }).execute()
247
-
294
+ self.client.table("sales_session_analytics").insert(
295
+ {
296
+ "session_id": session_id,
297
+ "total_messages": 0,
298
+ "total_tokens": 0,
299
+ "average_response_time_ms": 0,
300
+ "conversion_stage": "initial",
301
+ "lead_quality_score": 5,
302
+ }
303
+ ).execute()
304
+
248
305
  logger.debug(f"Создана аналитика для сессии {session_id}")
249
-
306
+
250
307
  except APIError as e:
251
308
  logger.error(f"Ошибка при создании аналитики: {e}")
252
309
  raise
253
-
310
+
254
311
  async def add_message(
255
- self,
256
- session_id: str,
257
- role: str,
258
- content: str,
259
- message_type: str = 'text',
312
+ self,
313
+ session_id: str,
314
+ role: str,
315
+ content: str,
316
+ message_type: str = "text",
260
317
  tokens_used: int = 0,
261
318
  processing_time_ms: int = 0,
262
319
  metadata: Dict[str, Any] = None,
263
- ai_metadata: Dict[str, Any] = None
320
+ ai_metadata: Dict[str, Any] = None,
264
321
  ) -> int:
265
322
  """Добавляет сообщение в базу данных"""
266
323
  try:
267
- response = self.client.table('sales_messages').insert({
268
- 'session_id': session_id,
269
- 'role': role,
270
- 'content': content,
271
- 'message_type': message_type,
272
- 'tokens_used': tokens_used,
273
- 'processing_time_ms': processing_time_ms,
274
- 'metadata': metadata or {},
275
- 'ai_metadata': ai_metadata or {}
276
- }).execute()
277
-
278
- message_id = response.data[0]['id']
279
-
324
+ response = (
325
+ self.client.table("sales_messages")
326
+ .insert(
327
+ {
328
+ "session_id": session_id,
329
+ "role": role,
330
+ "content": content,
331
+ "message_type": message_type,
332
+ "tokens_used": tokens_used,
333
+ "processing_time_ms": processing_time_ms,
334
+ "metadata": metadata or {},
335
+ "ai_metadata": ai_metadata or {},
336
+ }
337
+ )
338
+ .execute()
339
+ )
340
+
341
+ message_id = response.data[0]["id"]
342
+
280
343
  # Обновляем аналитику сессии
281
- await self.update_session_analytics(session_id, tokens_used, processing_time_ms)
282
-
344
+ await self.update_session_analytics(
345
+ session_id, tokens_used, processing_time_ms
346
+ )
347
+
283
348
  logger.debug(f"Добавлено сообщение {message_id} в сессию {session_id}")
284
349
  return message_id
285
-
350
+
286
351
  except APIError as e:
287
352
  logger.error(f"Ошибка при добавлении сообщения: {e}")
288
353
  raise
289
-
290
- async def get_chat_history(self, session_id: str, limit: int = 50) -> List[Dict[str, Any]]:
354
+
355
+ async def get_chat_history(
356
+ self, session_id: str, limit: int = 50
357
+ ) -> List[Dict[str, Any]]:
291
358
  """Получает историю сообщений для сессии"""
292
359
  try:
293
- response = self.client.table('sales_messages').select(
294
- 'id', 'role', 'content', 'message_type', 'created_at', 'metadata', 'ai_metadata'
295
- ).eq('session_id', session_id).order('created_at', desc=True).limit(limit).execute()
296
-
360
+ response = (
361
+ self.client.table("sales_messages")
362
+ .select(
363
+ "id",
364
+ "role",
365
+ "content",
366
+ "message_type",
367
+ "created_at",
368
+ "metadata",
369
+ "ai_metadata",
370
+ )
371
+ .eq("session_id", session_id)
372
+ .order("created_at", desc=True)
373
+ .limit(limit)
374
+ .execute()
375
+ )
376
+
297
377
  # Фильтруем системные сообщения из истории
298
- messages = [msg for msg in response.data if msg['role'] != 'system']
299
-
378
+ messages = [msg for msg in response.data if msg["role"] != "system"]
379
+
300
380
  # Переворачиваем в хронологический порядок (старые -> новые)
301
381
  messages.reverse()
302
-
382
+
303
383
  logger.debug(f"Получено {len(messages)} сообщений для сессии {session_id}")
304
384
  return messages
305
-
385
+
306
386
  except APIError as e:
307
387
  logger.error(f"Ошибка при получении истории: {e}")
308
388
  raise
309
-
389
+
310
390
  async def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
311
391
  """Получает информацию о сессии с проверкой bot_id (если указан)"""
312
392
  try:
313
- response = self.client.table('sales_chat_sessions').select(
314
- 'id', 'user_id', 'bot_id', 'system_prompt', 'status', 'created_at',
315
- 'metadata', 'current_stage', 'lead_quality_score'
316
- ).eq('id', session_id).execute()
317
-
393
+ response = (
394
+ self.client.table("sales_chat_sessions")
395
+ .select(
396
+ "id",
397
+ "user_id",
398
+ "bot_id",
399
+ "system_prompt",
400
+ "status",
401
+ "created_at",
402
+ "metadata",
403
+ "current_stage",
404
+ "lead_quality_score",
405
+ )
406
+ .eq("id", session_id)
407
+ .execute()
408
+ )
409
+
318
410
  if response.data:
319
411
  session = response.data[0]
320
412
  # 🆕 Дополнительная проверка bot_id для безопасности (если указан)
321
- if self.bot_id and session.get('bot_id') != self.bot_id:
322
- logger.warning(f"Попытка доступа к сессии {session_id} другого бота: {session.get('bot_id')} != {self.bot_id}")
413
+ if self.bot_id and session.get("bot_id") != self.bot_id:
414
+ logger.warning(
415
+ f"Попытка доступа к сессии {session_id} другого бота: {session.get('bot_id')} != {self.bot_id}"
416
+ )
323
417
  return None
324
418
  return session
325
419
  return None
326
-
420
+
327
421
  except APIError as e:
328
422
  logger.error(f"Ошибка при получении информации о сессии: {e}")
329
423
  raise
330
-
331
- async def update_session_stage(self, session_id: str, stage: str = None, quality_score: int = None):
424
+
425
+ async def update_session_stage(
426
+ self, session_id: str, stage: str = None, quality_score: int = None
427
+ ):
332
428
  """Обновляет этап сессии и качество лида"""
333
429
  try:
334
- update_data = {'updated_at': datetime.now().isoformat()}
335
-
430
+ update_data = {"updated_at": datetime.now().isoformat()}
431
+
336
432
  if stage:
337
- update_data['current_stage'] = stage
433
+ update_data["current_stage"] = stage
338
434
  if quality_score is not None:
339
- update_data['lead_quality_score'] = quality_score
340
-
435
+ update_data["lead_quality_score"] = quality_score
436
+
341
437
  # 🆕 Дополнительная проверка bot_id при обновлении (если указан)
342
438
  if self.bot_id:
343
- response = self.client.table('sales_chat_sessions').select('bot_id').eq('id', session_id).execute()
344
- if response.data and response.data[0].get('bot_id') != self.bot_id:
345
- logger.warning(f"Попытка обновления сессии {session_id} другого бота")
439
+ response = (
440
+ self.client.table("sales_chat_sessions")
441
+ .select("bot_id")
442
+ .eq("id", session_id)
443
+ .execute()
444
+ )
445
+ if response.data and response.data[0].get("bot_id") != self.bot_id:
446
+ logger.warning(
447
+ f"Попытка обновления сессии {session_id} другого бота"
448
+ )
346
449
  return
347
-
348
- self.client.table('sales_chat_sessions').update(update_data).eq(
349
- 'id', session_id
450
+
451
+ self.client.table("sales_chat_sessions").update(update_data).eq(
452
+ "id", session_id
350
453
  ).execute()
351
-
352
- logger.debug(f"Обновлен этап сессии {session_id}: stage={stage}, quality={quality_score}")
353
-
454
+
455
+ logger.debug(
456
+ f"Обновлен этап сессии {session_id}: stage={stage}, quality={quality_score}"
457
+ )
458
+
354
459
  except APIError as e:
355
460
  logger.error(f"Ошибка при обновлении этапа сессии: {e}")
356
461
  raise
357
-
462
+
358
463
  async def get_user_sessions(self, telegram_id: int) -> List[Dict[str, Any]]:
359
464
  """Получает все сессии пользователя с учетом bot_id (если указан)"""
360
465
  try:
361
466
  # 🆕 Получаем только сессии этого бота (если bot_id указан)
362
- query = self.client.table('sales_chat_sessions').select(
363
- 'id', 'status', 'created_at', 'updated_at', 'current_stage', 'lead_quality_score'
364
- ).eq('user_id', telegram_id).order('created_at', desc=True)
365
-
467
+ query = (
468
+ self.client.table("sales_chat_sessions")
469
+ .select(
470
+ "id",
471
+ "status",
472
+ "created_at",
473
+ "updated_at",
474
+ "current_stage",
475
+ "lead_quality_score",
476
+ )
477
+ .eq("user_id", telegram_id)
478
+ .order("created_at", desc=True)
479
+ )
480
+
366
481
  if self.bot_id:
367
- query = query.eq('bot_id', self.bot_id)
368
-
482
+ query = query.eq("bot_id", self.bot_id)
483
+
369
484
  response = query.execute()
370
485
  return response.data
371
-
486
+
372
487
  except APIError as e:
373
488
  logger.error(f"Ошибка при получении сессий пользователя: {e}")
374
489
  raise
375
-
490
+
376
491
  # 🆕 Новые методы для админской системы с поддержкой bot_id
377
-
378
- async def add_session_event(self, session_id: str, event_type: str, event_info: str) -> int:
492
+
493
+ async def add_session_event(
494
+ self, session_id: str, event_type: str, event_info: str
495
+ ) -> int:
379
496
  """Добавляет событие в сессию"""
380
497
  try:
381
- response = self.client.table('session_events').insert({
382
- 'session_id': session_id,
383
- 'event_type': event_type,
384
- 'event_info': event_info,
385
- 'notified_admins': []
386
- }).execute()
387
-
388
- event_id = response.data[0]['id']
498
+ response = (
499
+ self.client.table("session_events")
500
+ .insert(
501
+ {
502
+ "session_id": session_id,
503
+ "event_type": event_type,
504
+ "event_info": event_info,
505
+ "notified_admins": [],
506
+ }
507
+ )
508
+ .execute()
509
+ )
510
+
511
+ event_id = response.data[0]["id"]
389
512
  logger.info(f"Добавлено событие {event_type} для сессии {session_id}")
390
513
  return event_id
391
-
514
+
392
515
  except APIError as e:
393
516
  logger.error(f"Ошибка при добавлении события: {e}")
394
517
  raise
395
-
518
+
396
519
  async def sync_admin(self, admin_data: Dict[str, Any]):
397
520
  """Синхронизирует админа в БД (админы общие для всех ботов)"""
398
521
  try:
399
522
  # Проверяем существует ли админ
400
- response = self.client.table('sales_admins').select('telegram_id').eq(
401
- 'telegram_id', admin_data['telegram_id'],
402
- ).eq('bot_id', self.bot_id).execute()
403
-
523
+ response = (
524
+ self.client.table("sales_admins")
525
+ .select("telegram_id")
526
+ .eq(
527
+ "telegram_id",
528
+ admin_data["telegram_id"],
529
+ )
530
+ .eq("bot_id", self.bot_id)
531
+ .execute()
532
+ )
533
+
404
534
  if response.data:
405
535
  # Обновляем существующего
406
- self.client.table('sales_admins').update({
407
- 'username': admin_data.get('username'),
408
- 'first_name': admin_data.get('first_name'),
409
- 'last_name': admin_data.get('last_name'),
410
- 'is_active': True
411
- }).eq('telegram_id', admin_data['telegram_id']).eq('bot_id', self.bot_id).execute()
412
-
536
+ self.client.table("sales_admins").update(
537
+ {
538
+ "username": admin_data.get("username"),
539
+ "first_name": admin_data.get("first_name"),
540
+ "last_name": admin_data.get("last_name"),
541
+ "is_active": True,
542
+ }
543
+ ).eq("telegram_id", admin_data["telegram_id"]).eq(
544
+ "bot_id", self.bot_id
545
+ ).execute()
546
+
413
547
  logger.debug(f"Обновлен админ {admin_data['telegram_id']}")
414
548
  else:
415
549
  # Создаем нового
416
- self.client.table('sales_admins').insert({
417
- 'telegram_id': admin_data['telegram_id'],
418
- 'bot_id': self.bot_id,
419
- 'username': admin_data.get('username'),
420
- 'first_name': admin_data.get('first_name'),
421
- 'last_name': admin_data.get('last_name'),
422
- 'role': 'admin',
423
- 'is_active': True
424
- }).execute()
425
-
550
+ self.client.table("sales_admins").insert(
551
+ {
552
+ "telegram_id": admin_data["telegram_id"],
553
+ "bot_id": self.bot_id,
554
+ "username": admin_data.get("username"),
555
+ "first_name": admin_data.get("first_name"),
556
+ "last_name": admin_data.get("last_name"),
557
+ "role": "admin",
558
+ "is_active": True,
559
+ }
560
+ ).execute()
561
+
426
562
  logger.info(f"Создан новый админ {admin_data['telegram_id']}")
427
-
563
+
428
564
  except APIError as e:
429
565
  logger.error(f"Ошибка при синхронизации админа: {e}")
430
566
  raise
431
-
432
- async def start_admin_conversation(self, admin_id: int, user_id: int, session_id: str) -> int:
567
+
568
+ async def start_admin_conversation(
569
+ self, admin_id: int, user_id: int, session_id: str
570
+ ) -> int:
433
571
  """Начинает диалог между админом и пользователем"""
434
572
  try:
435
573
  # Завершаем активные диалоги этого админа
436
574
  await self.end_admin_conversations(admin_id)
437
-
438
- response = self.client.table('admin_user_conversations').insert({
439
- 'admin_id': admin_id,
440
- 'user_id': user_id,
441
- 'session_id': session_id,
442
- 'status': 'active',
443
- 'auto_end_at': (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat()
444
- }).execute()
445
-
446
- conversation_id = response.data[0]['id']
447
- logger.info(f"Начат диалог {conversation_id}: админ {admin_id} с пользователем {user_id}")
575
+
576
+ response = (
577
+ self.client.table("admin_user_conversations")
578
+ .insert(
579
+ {
580
+ "admin_id": admin_id,
581
+ "user_id": user_id,
582
+ "session_id": session_id,
583
+ "status": "active",
584
+ "auto_end_at": (
585
+ datetime.now(timezone.utc) + timedelta(minutes=30)
586
+ ).isoformat(),
587
+ }
588
+ )
589
+ .execute()
590
+ )
591
+
592
+ conversation_id = response.data[0]["id"]
593
+ logger.info(
594
+ f"Начат диалог {conversation_id}: админ {admin_id} с пользователем {user_id}"
595
+ )
448
596
  return conversation_id
449
-
597
+
450
598
  except APIError as e:
451
599
  logger.error(f"Ошибка при начале диалога: {e}")
452
600
  raise
453
-
454
- async def end_admin_conversations(self, admin_id: int = None, user_id: int = None) -> int:
601
+
602
+ async def end_admin_conversations(
603
+ self, admin_id: int = None, user_id: int = None
604
+ ) -> int:
455
605
  """Завершает активные диалоги админа или пользователя"""
456
606
  try:
457
- query = self.client.table('admin_user_conversations').update({
458
- 'status': 'ended',
459
- 'ended_at': datetime.now(timezone.utc).isoformat()
460
- }).eq('status', 'active')
461
-
607
+ query = (
608
+ self.client.table("admin_user_conversations")
609
+ .update(
610
+ {
611
+ "status": "ended",
612
+ "ended_at": datetime.now(timezone.utc).isoformat(),
613
+ }
614
+ )
615
+ .eq("status", "active")
616
+ )
617
+
462
618
  if admin_id:
463
- query = query.eq('admin_id', admin_id)
619
+ query = query.eq("admin_id", admin_id)
464
620
  if user_id:
465
- query = query.eq('user_id', user_id)
466
-
621
+ query = query.eq("user_id", user_id)
622
+
467
623
  response = query.execute()
468
624
  ended_count = len(response.data)
469
-
625
+
470
626
  if ended_count > 0:
471
627
  logger.info(f"Завершено {ended_count} активных диалогов")
472
-
628
+
473
629
  return ended_count
474
-
630
+
475
631
  except APIError as e:
476
632
  logger.error(f"Ошибка при завершении диалогов: {e}")
477
633
  return 0
478
-
479
- async def get_admin_active_conversation(self, admin_id: int) -> Optional[Dict[str, Any]]:
634
+
635
+ async def get_admin_active_conversation(
636
+ self, admin_id: int
637
+ ) -> Optional[Dict[str, Any]]:
480
638
  """Получает активный диалог админа"""
481
639
  try:
482
- response = self.client.table('admin_user_conversations').select(
483
- 'id', 'user_id', 'session_id', 'started_at', 'auto_end_at'
484
- ).eq('admin_id', admin_id).eq('status', 'active').execute()
485
-
640
+ response = (
641
+ self.client.table("admin_user_conversations")
642
+ .select("id", "user_id", "session_id", "started_at", "auto_end_at")
643
+ .eq("admin_id", admin_id)
644
+ .eq("status", "active")
645
+ .execute()
646
+ )
647
+
486
648
  return response.data[0] if response.data else None
487
-
649
+
488
650
  except APIError as e:
489
651
  logger.error(f"Ошибка при получении диалога админа: {e}")
490
652
  return None
491
-
653
+
492
654
  async def get_user_conversation(self, user_id: int) -> Optional[Dict[str, Any]]:
493
655
  """Получает активный диалог пользователя"""
494
656
  try:
495
- response = self.client.table('admin_user_conversations').select(
496
- 'id', 'admin_id', 'session_id', 'started_at', 'auto_end_at'
497
- ).eq('user_id', user_id).eq('status', 'active').execute()
498
-
657
+ response = (
658
+ self.client.table("admin_user_conversations")
659
+ .select("id", "admin_id", "session_id", "started_at", "auto_end_at")
660
+ .eq("user_id", user_id)
661
+ .eq("status", "active")
662
+ .execute()
663
+ )
664
+
499
665
  return response.data[0] if response.data else None
500
-
666
+
501
667
  except APIError as e:
502
668
  logger.error(f"Ошибка при получении диалога пользователя: {e}")
503
669
  return None
504
-
670
+
505
671
  # 🆕 Методы совместимости - добавляем недостающие методы из старого кода
506
-
672
+
507
673
  async def cleanup_expired_conversations(self) -> int:
508
674
  """Завершает просроченные диалоги админов"""
509
675
  try:
510
676
  now = datetime.now(timezone.utc).isoformat()
511
-
512
- response = self.client.table('admin_user_conversations').update({
513
- 'status': 'expired',
514
- 'ended_at': now
515
- }).eq('status', 'active').lt('auto_end_at', now).execute()
516
-
677
+
678
+ response = (
679
+ self.client.table("admin_user_conversations")
680
+ .update({"status": "expired", "ended_at": now})
681
+ .eq("status", "active")
682
+ .lt("auto_end_at", now)
683
+ .execute()
684
+ )
685
+
517
686
  ended_count = len(response.data)
518
687
  if ended_count > 0:
519
- logger.info(f"Автоматически завершено {ended_count} просроченных диалогов")
520
-
688
+ logger.info(
689
+ f"Автоматически завершено {ended_count} просроченных диалогов"
690
+ )
691
+
521
692
  return ended_count
522
-
693
+
523
694
  except APIError as e:
524
695
  logger.error(f"Ошибка при завершении просроченных диалогов: {e}")
525
696
  return 0
526
-
697
+
527
698
  async def end_expired_conversations(self) -> int:
528
699
  """Алиас для cleanup_expired_conversations для обратной совместимости"""
529
700
  return await self.cleanup_expired_conversations()
530
-
531
- async def get_user_admin_conversation(self, user_id: int) -> Optional[Dict[str, Any]]:
701
+
702
+ async def get_user_admin_conversation(
703
+ self, user_id: int
704
+ ) -> Optional[Dict[str, Any]]:
532
705
  """Проверяет, ведется ли диалог с пользователем (для совместимости)"""
533
706
  return await self.get_user_conversation(user_id)
534
-
707
+
535
708
  # 🆕 Методы аналитики с фильтрацией по bot_id
536
-
709
+
537
710
  async def get_analytics_summary(self, days: int = 7) -> Dict[str, Any]:
538
711
  """Получает сводку аналитики за последние дни с учетом bot_id (если указан)"""
539
712
  try:
540
713
  cutoff_date = datetime.now() - timedelta(days=days)
541
-
714
+
542
715
  # 🆕 Получаем сессии с учетом bot_id (если указан)
543
- query = self.client.table('sales_chat_sessions').select(
544
- 'id', 'current_stage', 'lead_quality_score', 'created_at'
545
- ).gte('created_at', cutoff_date.isoformat())
546
-
716
+ query = (
717
+ self.client.table("sales_chat_sessions")
718
+ .select("id", "current_stage", "lead_quality_score", "created_at")
719
+ .gte("created_at", cutoff_date.isoformat())
720
+ )
721
+
547
722
  if self.bot_id:
548
- query = query.eq('bot_id', self.bot_id)
549
-
723
+ query = query.eq("bot_id", self.bot_id)
724
+
550
725
  sessions_response = query.execute()
551
-
726
+
552
727
  sessions = sessions_response.data
553
728
  total_sessions = len(sessions)
554
-
729
+
555
730
  # Группировка по этапам
556
731
  stages = {}
557
732
  quality_scores = []
558
-
733
+
559
734
  for session in sessions:
560
- stage = session.get('current_stage', 'unknown')
735
+ stage = session.get("current_stage", "unknown")
561
736
  stages[stage] = stages.get(stage, 0) + 1
562
-
563
- score = session.get('lead_quality_score', 5)
737
+
738
+ score = session.get("lead_quality_score", 5)
564
739
  if score:
565
740
  quality_scores.append(score)
566
-
567
- avg_quality = sum(quality_scores) / len(quality_scores) if quality_scores else 5
568
-
741
+
742
+ avg_quality = (
743
+ sum(quality_scores) / len(quality_scores) if quality_scores else 5
744
+ )
745
+
569
746
  return {
570
- 'bot_id': self.bot_id,
571
- 'period_days': days,
572
- 'total_sessions': total_sessions,
573
- 'stages': stages,
574
- 'average_lead_quality': round(avg_quality, 1),
575
- 'generated_at': datetime.now().isoformat()
747
+ "bot_id": self.bot_id,
748
+ "period_days": days,
749
+ "total_sessions": total_sessions,
750
+ "stages": stages,
751
+ "average_lead_quality": round(avg_quality, 1),
752
+ "generated_at": datetime.now().isoformat(),
576
753
  }
577
-
754
+
578
755
  except APIError as e:
579
756
  logger.error(f"Ошибка при получении аналитики: {e}")
580
757
  return {
581
- 'bot_id': self.bot_id,
582
- 'error': str(e),
583
- 'generated_at': datetime.now().isoformat()
758
+ "bot_id": self.bot_id,
759
+ "error": str(e),
760
+ "generated_at": datetime.now().isoformat(),
584
761
  }
585
-
762
+
586
763
  async def update_session_analytics(
587
- self,
588
- session_id: str,
589
- tokens_used: int = 0,
590
- processing_time_ms: int = 0
764
+ self, session_id: str, tokens_used: int = 0, processing_time_ms: int = 0
591
765
  ):
592
766
  """Обновляет аналитику сессии"""
593
767
  try:
594
768
  # Получаем текущую аналитику
595
- response = self.client.table('sales_session_analytics').select(
596
- 'total_messages', 'total_tokens', 'average_response_time_ms'
597
- ).eq('session_id', session_id).execute()
598
-
769
+ response = (
770
+ self.client.table("sales_session_analytics")
771
+ .select("total_messages", "total_tokens", "average_response_time_ms")
772
+ .eq("session_id", session_id)
773
+ .execute()
774
+ )
775
+
599
776
  if response.data:
600
777
  current = response.data[0]
601
- new_total_messages = current['total_messages'] + 1
602
- new_total_tokens = current['total_tokens'] + tokens_used
603
-
778
+ new_total_messages = current["total_messages"] + 1
779
+ new_total_tokens = current["total_tokens"] + tokens_used
780
+
604
781
  # Вычисляем среднее время ответа
605
782
  if processing_time_ms > 0:
606
- current_avg = current['average_response_time_ms']
607
- new_avg = ((current_avg * (new_total_messages - 1)) + processing_time_ms) / new_total_messages
783
+ current_avg = current["average_response_time_ms"]
784
+ new_avg = (
785
+ (current_avg * (new_total_messages - 1)) + processing_time_ms
786
+ ) / new_total_messages
608
787
  else:
609
- new_avg = current['average_response_time_ms']
610
-
788
+ new_avg = current["average_response_time_ms"]
789
+
611
790
  # Обновляем аналитику
612
- self.client.table('sales_session_analytics').update({
613
- 'total_messages': new_total_messages,
614
- 'total_tokens': new_total_tokens,
615
- 'average_response_time_ms': int(new_avg),
616
- 'updated_at': datetime.now().isoformat()
617
- }).eq('session_id', session_id).execute()
618
-
791
+ self.client.table("sales_session_analytics").update(
792
+ {
793
+ "total_messages": new_total_messages,
794
+ "total_tokens": new_total_tokens,
795
+ "average_response_time_ms": int(new_avg),
796
+ "updated_at": datetime.now().isoformat(),
797
+ }
798
+ ).eq("session_id", session_id).execute()
799
+
619
800
  except APIError as e:
620
801
  logger.error(f"Ошибка при обновлении аналитики: {e}")
621
802
  # Не прерываем выполнение, аналитика не критична
622
-
803
+
623
804
  # Методы совместимости
624
- async def update_conversion_stage(self, session_id: str, stage: str, quality_score: int = None):
805
+ async def update_conversion_stage(
806
+ self, session_id: str, stage: str, quality_score: int = None
807
+ ):
625
808
  """Обновляет этап конверсии и качество лида (для совместимости)"""
626
809
  await self.update_session_stage(session_id, stage, quality_score)
627
-
810
+
628
811
  async def archive_old_sessions(self, days: int = 7):
629
812
  """Архивирует старые завершенные сессии с учетом bot_id (если указан)"""
630
813
  try:
631
814
  cutoff_date = datetime.now() - timedelta(days=days)
632
-
815
+
633
816
  # 🆕 Архивируем только сессии этого бота (если bot_id указан)
634
- query = self.client.table('sales_chat_sessions').update({
635
- 'status': 'archived'
636
- }).eq('status', 'completed').lt('updated_at', cutoff_date.isoformat())
637
-
817
+ query = (
818
+ self.client.table("sales_chat_sessions")
819
+ .update({"status": "archived"})
820
+ .eq("status", "completed")
821
+ .lt("updated_at", cutoff_date.isoformat())
822
+ )
823
+
638
824
  if self.bot_id:
639
- query = query.eq('bot_id', self.bot_id)
640
-
825
+ query = query.eq("bot_id", self.bot_id)
826
+
641
827
  query.execute()
642
-
643
- logger.info(f"Архивированы сессии старше {days} дней{f' для bot_id {self.bot_id}' if self.bot_id else ''}")
644
-
828
+
829
+ logger.info(
830
+ f"Архивированы сессии старше {days} дней{f' для bot_id {self.bot_id}' if self.bot_id else ''}"
831
+ )
832
+
645
833
  except APIError as e:
646
834
  logger.error(f"Ошибка при архивировании сессий: {e}")
647
835
  raise
648
-
836
+
649
837
  async def get_sent_files(self, user_id: int) -> List[str]:
650
838
  """Получает список отправленных файлов для пользователя
651
-
839
+
652
840
  Args:
653
841
  user_id: Telegram ID пользователя
654
-
842
+
655
843
  Returns:
656
844
  List[str]: Список имен файлов, разделенных запятой
657
845
  """
658
846
  try:
659
- query = self.client.table('sales_users').select('files').eq('telegram_id', user_id)
660
-
847
+ query = (
848
+ self.client.table("sales_users")
849
+ .select("files")
850
+ .eq("telegram_id", user_id)
851
+ )
852
+
661
853
  if self.bot_id:
662
- query = query.eq('bot_id', self.bot_id)
663
-
854
+ query = query.eq("bot_id", self.bot_id)
855
+
664
856
  response = query.execute()
665
-
666
- if response.data and response.data[0].get('files'):
667
- files_str = response.data[0]['files']
668
- return [f.strip() for f in files_str.split(',') if f.strip()]
669
-
857
+
858
+ if response.data and response.data[0].get("files"):
859
+ files_str = response.data[0]["files"]
860
+ return [f.strip() for f in files_str.split(",") if f.strip()]
861
+
670
862
  return []
671
-
863
+
672
864
  except Exception as e:
673
- logger.error(f"Ошибка получения отправленных файлов для пользователя {user_id}: {e}")
865
+ logger.error(
866
+ f"Ошибка получения отправленных файлов для пользователя {user_id}: {e}"
867
+ )
674
868
  return []
675
-
869
+
676
870
  async def get_sent_directories(self, user_id: int) -> List[str]:
677
871
  """Получает список отправленных каталогов для пользователя
678
-
872
+
679
873
  Args:
680
874
  user_id: Telegram ID пользователя
681
-
875
+
682
876
  Returns:
683
877
  List[str]: Список путей каталогов, разделенных запятой
684
878
  """
685
879
  try:
686
- query = self.client.table('sales_users').select('directories').eq('telegram_id', user_id)
687
-
880
+ query = (
881
+ self.client.table("sales_users")
882
+ .select("directories")
883
+ .eq("telegram_id", user_id)
884
+ )
885
+
688
886
  if self.bot_id:
689
- query = query.eq('bot_id', self.bot_id)
690
-
887
+ query = query.eq("bot_id", self.bot_id)
888
+
691
889
  response = query.execute()
692
-
693
- if response.data and response.data[0].get('directories'):
694
- dirs_str = response.data[0]['directories']
695
- return [d.strip() for d in dirs_str.split(',') if d.strip()]
696
-
890
+
891
+ if response.data and response.data[0].get("directories"):
892
+ dirs_str = response.data[0]["directories"]
893
+ return [d.strip() for d in dirs_str.split(",") if d.strip()]
894
+
697
895
  return []
698
-
896
+
699
897
  except Exception as e:
700
- logger.error(f"Ошибка получения отправленных каталогов для пользователя {user_id}: {e}")
898
+ logger.error(
899
+ f"Ошибка получения отправленных каталогов для пользователя {user_id}: {e}"
900
+ )
701
901
  return []
702
-
902
+
703
903
  async def add_sent_files(self, user_id: int, files_list: List[str]):
704
904
  """Добавляет файлы в список отправленных для пользователя
705
-
905
+
706
906
  Args:
707
907
  user_id: Telegram ID пользователя
708
908
  files_list: Список имен файлов для добавления
709
909
  """
710
910
  try:
711
911
  logger.info(f"Добавление файлов для пользователя {user_id}: {files_list}")
712
-
912
+
713
913
  # Получаем текущий список
714
914
  current_files = await self.get_sent_files(user_id)
715
915
  logger.info(f"Текущие файлы в БД: {current_files}")
716
-
916
+
717
917
  # Объединяем с новыми файлами (без дубликатов)
718
918
  all_files = list(set(current_files + files_list))
719
919
  logger.info(f"Объединенный список файлов: {all_files}")
720
-
920
+
721
921
  # Сохраняем обратно
722
- files_str = ', '.join(all_files)
922
+ files_str = ", ".join(all_files)
723
923
  logger.info(f"Сохраняем строку: {files_str}")
724
-
725
- query = self.client.table('sales_users').update({
726
- 'files': files_str
727
- }).eq('telegram_id', user_id)
728
-
924
+
925
+ query = (
926
+ self.client.table("sales_users")
927
+ .update({"files": files_str})
928
+ .eq("telegram_id", user_id)
929
+ )
930
+
729
931
  if self.bot_id:
730
- query = query.eq('bot_id', self.bot_id)
932
+ query = query.eq("bot_id", self.bot_id)
731
933
  logger.info(f"Фильтр по bot_id: {self.bot_id}")
732
-
934
+
733
935
  response = query.execute()
734
936
  logger.info(f"Ответ от БД: {response.data}")
735
-
736
- logger.info(f"✅ Добавлено {len(files_list)} файлов для пользователя {user_id}")
737
-
937
+
938
+ logger.info(
939
+ f"✅ Добавлено {len(files_list)} файлов для пользователя {user_id}"
940
+ )
941
+
738
942
  except Exception as e:
739
- logger.error(f"❌ Ошибка добавления отправленных файлов для пользователя {user_id}: {e}")
943
+ logger.error(
944
+ f"❌ Ошибка добавления отправленных файлов для пользователя {user_id}: {e}"
945
+ )
740
946
  logger.exception("Полный стек ошибки:")
741
-
947
+
742
948
  async def add_sent_directories(self, user_id: int, dirs_list: List[str]):
743
949
  """Добавляет каталоги в список отправленных для пользователя
744
-
950
+
745
951
  Args:
746
952
  user_id: Telegram ID пользователя
747
953
  dirs_list: Список путей каталогов для добавления
748
954
  """
749
955
  try:
750
956
  logger.info(f"Добавление каталогов для пользователя {user_id}: {dirs_list}")
751
-
957
+
752
958
  # Получаем текущий список
753
959
  current_dirs = await self.get_sent_directories(user_id)
754
960
  logger.info(f"Текущие каталоги в БД: {current_dirs}")
755
-
961
+
756
962
  # Объединяем с новыми каталогами (без дубликатов)
757
963
  all_dirs = list(set(current_dirs + dirs_list))
758
964
  logger.info(f"Объединенный список каталогов: {all_dirs}")
759
-
965
+
760
966
  # Сохраняем обратно
761
- dirs_str = ', '.join(all_dirs)
967
+ dirs_str = ", ".join(all_dirs)
762
968
  logger.info(f"Сохраняем строку: {dirs_str}")
763
-
764
- query = self.client.table('sales_users').update({
765
- 'directories': dirs_str
766
- }).eq('telegram_id', user_id)
767
-
969
+
970
+ query = (
971
+ self.client.table("sales_users")
972
+ .update({"directories": dirs_str})
973
+ .eq("telegram_id", user_id)
974
+ )
975
+
768
976
  if self.bot_id:
769
- query = query.eq('bot_id', self.bot_id)
977
+ query = query.eq("bot_id", self.bot_id)
770
978
  logger.info(f"Фильтр по bot_id: {self.bot_id}")
771
-
979
+
772
980
  response = query.execute()
773
981
  logger.info(f"Ответ от БД: {response.data}")
774
-
775
- logger.info(f"✅ Добавлено {len(dirs_list)} каталогов для пользователя {user_id}")
776
-
982
+
983
+ logger.info(
984
+ f"✅ Добавлено {len(dirs_list)} каталогов для пользователя {user_id}"
985
+ )
986
+
777
987
  except Exception as e:
778
- logger.error(f"❌ Ошибка добавления отправленных каталогов для пользователя {user_id}: {e}")
988
+ logger.error(
989
+ f"❌ Ошибка добавления отправленных каталогов для пользователя {user_id}: {e}"
990
+ )
779
991
  logger.exception("Полный стек ошибки:")
780
992
 
781
993
  # =============================================================================
782
994
  # МЕТОДЫ ДЛЯ АНАЛИТИКИ
783
995
  # =============================================================================
784
-
996
+
785
997
  async def get_funnel_stats(self, days: int = 7) -> Dict[str, Any]:
786
998
  """Получает статистику воронки продаж"""
787
999
  try:
788
1000
  cutoff_date = datetime.now() - timedelta(days=days)
789
-
790
- # Получаем ВСЕ уникальные пользователи из sales_users с фильтром по bot_id
791
- users_query = self.client.table('sales_users').select('telegram_id')
792
-
1001
+
1002
+ # Получаем ВСЕ уникальные пользователи из sales_users с фильтром по bot_id
1003
+ users_query = self.client.table("sales_users").select("telegram_id")
1004
+
793
1005
  if self.bot_id:
794
- users_query = users_query.eq('bot_id', self.bot_id)
795
-
1006
+ users_query = users_query.eq("bot_id", self.bot_id)
1007
+
796
1008
  # Исключаем тестовых пользователей
797
- users_query = users_query.neq('username', 'test_user')
798
-
1009
+ users_query = users_query.neq("username", "test_user")
1010
+
799
1011
  users_response = users_query.execute()
800
1012
  total_unique_users = len(users_response.data) if users_response.data else 0
801
-
1013
+
802
1014
  # Получаем сессии с учетом bot_id за период
803
- sessions_query = self.client.table('sales_chat_sessions').select(
804
- 'id', 'user_id', 'current_stage', 'lead_quality_score', 'created_at'
805
- ).gte('created_at', cutoff_date.isoformat())
1015
+ sessions_query = (
1016
+ self.client.table("sales_chat_sessions")
1017
+ .select(
1018
+ "id", "user_id", "current_stage", "lead_quality_score", "created_at"
1019
+ )
1020
+ .gte("created_at", cutoff_date.isoformat())
1021
+ )
806
1022
 
807
1023
  if self.bot_id:
808
- sessions_query = sessions_query.eq('bot_id', self.bot_id)
1024
+ sessions_query = sessions_query.eq("bot_id", self.bot_id)
809
1025
 
810
1026
  sessions_response = sessions_query.execute()
811
1027
  sessions = sessions_response.data
812
-
1028
+
813
1029
  # Исключаем сессии тестовых пользователей
814
1030
  if sessions:
815
1031
  # Получаем telegram_id тестовых пользователей
816
- test_users_query = self.client.table('sales_users').select('telegram_id').eq('username', 'test_user')
1032
+ test_users_query = (
1033
+ self.client.table("sales_users")
1034
+ .select("telegram_id")
1035
+ .eq("username", "test_user")
1036
+ )
817
1037
  if self.bot_id:
818
- test_users_query = test_users_query.eq('bot_id', self.bot_id)
819
-
1038
+ test_users_query = test_users_query.eq("bot_id", self.bot_id)
1039
+
820
1040
  test_users_response = test_users_query.execute()
821
- test_user_ids = {user['telegram_id'] for user in test_users_response.data} if test_users_response.data else set()
822
-
1041
+ test_user_ids = (
1042
+ {user["telegram_id"] for user in test_users_response.data}
1043
+ if test_users_response.data
1044
+ else set()
1045
+ )
1046
+
823
1047
  # Фильтруем сессии
824
- sessions = [s for s in sessions if s['user_id'] not in test_user_ids]
825
-
1048
+ sessions = [s for s in sessions if s["user_id"] not in test_user_ids]
1049
+
826
1050
  total_sessions = len(sessions)
827
1051
 
828
1052
  # Группировка по этапам
829
-
1053
+
830
1054
  # Группировка по этапам
831
1055
  stages = {}
832
1056
  quality_scores = []
833
-
1057
+
834
1058
  for session in sessions:
835
- stage = session.get('current_stage', 'unknown')
1059
+ stage = session.get("current_stage", "unknown")
836
1060
  stages[stage] = stages.get(stage, 0) + 1
837
-
838
- score = session.get('lead_quality_score', 5)
1061
+
1062
+ score = session.get("lead_quality_score", 5)
839
1063
  if score:
840
1064
  quality_scores.append(score)
841
-
842
- avg_quality = sum(quality_scores) / len(quality_scores) if quality_scores else 5
843
-
1065
+
1066
+ avg_quality = (
1067
+ sum(quality_scores) / len(quality_scores) if quality_scores else 5
1068
+ )
1069
+
844
1070
  return {
845
- 'total_sessions': total_sessions,
846
- 'total_unique_users': total_unique_users, # ✅ ВСЕ уникальные пользователи бота
847
- 'stages': stages,
848
- 'avg_quality': round(avg_quality, 1),
849
- 'period_days': days
1071
+ "total_sessions": total_sessions,
1072
+ "total_unique_users": total_unique_users, # ✅ ВСЕ уникальные пользователи бота
1073
+ "stages": stages,
1074
+ "avg_quality": round(avg_quality, 1),
1075
+ "period_days": days,
850
1076
  }
851
-
1077
+
852
1078
  except APIError as e:
853
1079
  logger.error(f"Ошибка получения статистики воронки: {e}")
854
1080
  return {
855
- 'total_sessions': 0,
856
- 'stages': {},
857
- 'avg_quality': 0,
858
- 'period_days': days
1081
+ "total_sessions": 0,
1082
+ "stages": {},
1083
+ "avg_quality": 0,
1084
+ "period_days": days,
859
1085
  }
860
-
1086
+
861
1087
  async def get_events_stats(self, days: int = 7) -> Dict[str, int]:
862
1088
  """Получает статистику событий"""
863
1089
  try:
864
1090
  cutoff_date = datetime.now() - timedelta(days=days)
865
-
1091
+
866
1092
  # Получаем события с учетом bot_id через сессии
867
- query = self.client.table('session_events').select(
868
- 'event_type', 'session_id'
869
- ).gte('created_at', cutoff_date.isoformat())
870
-
1093
+ query = (
1094
+ self.client.table("session_events")
1095
+ .select("event_type", "session_id")
1096
+ .gte("created_at", cutoff_date.isoformat())
1097
+ )
1098
+
871
1099
  events_response = query.execute()
872
1100
  events = events_response.data if events_response.data else []
873
-
1101
+
874
1102
  # Фильтруем события по bot_id через сессии
875
1103
  if self.bot_id and events:
876
1104
  # Получаем ID сессий этого бота
877
- sessions_query = self.client.table('sales_chat_sessions').select('id', 'user_id').eq('bot_id', self.bot_id)
1105
+ sessions_query = (
1106
+ self.client.table("sales_chat_sessions")
1107
+ .select("id", "user_id")
1108
+ .eq("bot_id", self.bot_id)
1109
+ )
878
1110
  sessions_response = sessions_query.execute()
879
-
1111
+
880
1112
  # Исключаем сессии тестовых пользователей
881
1113
  if sessions_response.data:
882
1114
  # Получаем telegram_id тестовых пользователей
883
- test_users_query = self.client.table('sales_users').select('telegram_id').eq('username', 'test_user')
1115
+ test_users_query = (
1116
+ self.client.table("sales_users")
1117
+ .select("telegram_id")
1118
+ .eq("username", "test_user")
1119
+ )
884
1120
  if self.bot_id:
885
- test_users_query = test_users_query.eq('bot_id', self.bot_id)
886
-
1121
+ test_users_query = test_users_query.eq("bot_id", self.bot_id)
1122
+
887
1123
  test_users_response = test_users_query.execute()
888
- test_user_ids = {user['telegram_id'] for user in test_users_response.data} if test_users_response.data else set()
889
-
1124
+ test_user_ids = (
1125
+ {user["telegram_id"] for user in test_users_response.data}
1126
+ if test_users_response.data
1127
+ else set()
1128
+ )
1129
+
890
1130
  # Фильтруем сессии: только не тестовые
891
- bot_sessions = [s for s in sessions_response.data if s['user_id'] not in test_user_ids]
892
- bot_session_ids = {session['id'] for session in bot_sessions}
1131
+ bot_sessions = [
1132
+ s
1133
+ for s in sessions_response.data
1134
+ if s["user_id"] not in test_user_ids
1135
+ ]
1136
+ bot_session_ids = {session["id"] for session in bot_sessions}
893
1137
  else:
894
1138
  bot_session_ids = set()
895
-
1139
+
896
1140
  # Фильтруем события
897
- events = [event for event in events if event['session_id'] in bot_session_ids]
898
-
1141
+ events = [
1142
+ event for event in events if event["session_id"] in bot_session_ids
1143
+ ]
1144
+
899
1145
  # Группируем по типам событий
900
1146
  event_counts = {}
901
1147
  for event in events:
902
- event_type = event.get('event_type', 'unknown')
1148
+ event_type = event.get("event_type", "unknown")
903
1149
  event_counts[event_type] = event_counts.get(event_type, 0) + 1
904
-
1150
+
905
1151
  return event_counts
906
-
1152
+
907
1153
  except APIError as e:
908
1154
  logger.error(f"Ошибка получения статистики событий: {e}")
909
1155
  return {}
910
1156
 
911
- async def get_user_last_message_info(self, user_id: int) -> Optional[Dict[str, Any]]:
1157
+ async def get_user_last_message_info(
1158
+ self, user_id: int
1159
+ ) -> Optional[Dict[str, Any]]:
912
1160
  """Получает информацию о последней активности пользователя из сессии"""
913
1161
  try:
914
1162
  # Получаем последнюю сессию пользователя
915
- response = self.client.table('sales_chat_sessions').select(
916
- 'id', 'current_stage', 'created_at', 'updated_at'
917
- ).eq('user_id', user_id).order('updated_at', desc=True).limit(1).execute()
918
-
1163
+ response = (
1164
+ self.client.table("sales_chat_sessions")
1165
+ .select("id", "current_stage", "created_at", "updated_at")
1166
+ .eq("user_id", user_id)
1167
+ .order("updated_at", desc=True)
1168
+ .limit(1)
1169
+ .execute()
1170
+ )
1171
+
919
1172
  if not response.data:
920
1173
  return None
921
-
1174
+
922
1175
  session = response.data[0]
923
-
1176
+
924
1177
  return {
925
- 'last_message_at': session['updated_at'],
926
- 'session_id': session['id'],
927
- 'current_stage': session['current_stage'],
928
- 'session_updated_at': session['updated_at']
1178
+ "last_message_at": session["updated_at"],
1179
+ "session_id": session["id"],
1180
+ "current_stage": session["current_stage"],
1181
+ "session_updated_at": session["updated_at"],
929
1182
  }
930
-
1183
+
931
1184
  except Exception as e:
932
- logger.error(f"Ошибка получения информации о последнем сообщении пользователя {user_id}: {e}")
1185
+ logger.error(
1186
+ f"Ошибка получения информации о последнем сообщении пользователя {user_id}: {e}"
1187
+ )
933
1188
  return None
934
1189
 
935
- async def check_user_stage_changed(self, user_id: int, original_session_id: str) -> bool:
1190
+ async def check_user_stage_changed(
1191
+ self, user_id: int, original_session_id: str
1192
+ ) -> bool:
936
1193
  """Проверяет, изменился ли этап пользователя с момента планирования события"""
937
1194
  try:
938
1195
  # Получаем текущую информацию о сессии
939
- current_response = self.client.table('sales_chat_sessions').select(
940
- 'id', 'current_stage'
941
- ).eq('user_telegram_id', user_id).order('created_at', desc=True).limit(1).execute()
942
-
1196
+ current_response = (
1197
+ self.client.table("sales_chat_sessions")
1198
+ .select("id", "current_stage")
1199
+ .eq("user_telegram_id", user_id)
1200
+ .order("created_at", desc=True)
1201
+ .limit(1)
1202
+ .execute()
1203
+ )
1204
+
943
1205
  if not current_response.data:
944
1206
  return False
945
-
1207
+
946
1208
  current_session = current_response.data[0]
947
-
1209
+
948
1210
  # Если сессия изменилась - этап точно изменился
949
- if current_session['id'] != original_session_id:
1211
+ if current_session["id"] != original_session_id:
950
1212
  return True
951
-
1213
+
952
1214
  # Если сессия та же, получаем оригинальный этап из scheduled_events
953
1215
  # и сравниваем с текущим
954
- original_response = self.client.table('sales_chat_sessions').select(
955
- 'current_stage'
956
- ).eq('id', original_session_id).execute()
957
-
1216
+ original_response = (
1217
+ self.client.table("sales_chat_sessions")
1218
+ .select("current_stage")
1219
+ .eq("id", original_session_id)
1220
+ .execute()
1221
+ )
1222
+
958
1223
  if not original_response.data:
959
1224
  # Если не нашли оригинальную сессию, считаем что этап не изменился
960
1225
  return False
961
-
962
- original_stage = original_response.data[0]['current_stage']
963
- current_stage = current_session['current_stage']
964
-
1226
+
1227
+ original_stage = original_response.data[0]["current_stage"]
1228
+ current_stage = current_session["current_stage"]
1229
+
965
1230
  # Проверяем, изменился ли этап внутри той же сессии
966
1231
  if original_stage != current_stage:
967
- logger.info(f"🔄 Этап изменился: {original_stage} -> {current_stage} (сессия {original_session_id})")
1232
+ logger.info(
1233
+ f"🔄 Этап изменился: {original_stage} -> {current_stage} (сессия {original_session_id})"
1234
+ )
968
1235
  return True
969
-
1236
+
970
1237
  return False
971
-
1238
+
972
1239
  except Exception as e:
973
1240
  logger.error(f"Ошибка проверки изменения этапа пользователя {user_id}: {e}")
974
1241
  return False
975
1242
 
976
- async def get_last_event_info_by_user_and_type(self, user_id: int, event_type: str) -> Optional[str]:
1243
+ async def get_last_event_info_by_user_and_type(
1244
+ self, user_id: int, event_type: str
1245
+ ) -> Optional[str]:
977
1246
  """
978
1247
  Получает event_info последнего события определенного типа для пользователя
979
-
1248
+
980
1249
  Args:
981
1250
  user_id: Telegram ID пользователя
982
1251
  event_type: Тип события для поиска
983
-
1252
+
984
1253
  Returns:
985
1254
  str: event_info последнего найденного события или None если не найдено
986
1255
  """
987
1256
  try:
988
1257
  # 1. Получаем последнюю сессию пользователя
989
- sessions_query = self.client.table('sales_chat_sessions').select(
990
- 'id'
991
- ).eq('user_id', user_id).order('created_at', desc=True).limit(1)
992
-
1258
+ sessions_query = (
1259
+ self.client.table("sales_chat_sessions")
1260
+ .select("id")
1261
+ .eq("user_id", user_id)
1262
+ .order("created_at", desc=True)
1263
+ .limit(1)
1264
+ )
1265
+
993
1266
  # Фильтруем по bot_id если указан
994
1267
  if self.bot_id:
995
- sessions_query = sessions_query.eq('bot_id', self.bot_id)
996
-
1268
+ sessions_query = sessions_query.eq("bot_id", self.bot_id)
1269
+
997
1270
  sessions_response = sessions_query.execute()
998
-
1271
+
999
1272
  if not sessions_response.data:
1000
1273
  logger.info(f"Пользователь {user_id} не найден в сессиях")
1001
1274
  return None
1002
-
1003
- session_id = sessions_response.data[0]['id']
1004
- logger.info(f"Найдена последняя сессия {session_id} для пользователя {user_id}")
1005
-
1275
+
1276
+ session_id = sessions_response.data[0]["id"]
1277
+ logger.info(
1278
+ f"Найдена последняя сессия {session_id} для пользователя {user_id}"
1279
+ )
1280
+
1006
1281
  # 2. Ищем последнее событие с этим session_id и event_type
1007
- events_response = self.client.table('session_events').select(
1008
- 'event_info', 'created_at'
1009
- ).eq('session_id', session_id).eq('event_type', event_type).order(
1010
- 'created_at', desc=True
1011
- ).limit(1).execute()
1012
-
1282
+ events_response = (
1283
+ self.client.table("session_events")
1284
+ .select("event_info", "created_at")
1285
+ .eq("session_id", session_id)
1286
+ .eq("event_type", event_type)
1287
+ .order("created_at", desc=True)
1288
+ .limit(1)
1289
+ .execute()
1290
+ )
1291
+
1013
1292
  if not events_response.data:
1014
- logger.info(f"События типа '{event_type}' не найдены для сессии {session_id}")
1293
+ logger.info(
1294
+ f"События типа '{event_type}' не найдены для сессии {session_id}"
1295
+ )
1015
1296
  return None
1016
-
1017
- event_info = events_response.data[0]['event_info']
1018
- created_at = events_response.data[0]['created_at']
1019
-
1020
- logger.info(f"Найдено последнее событие '{event_type}' для пользователя {user_id}: {event_info[:50]}... (создано: {created_at})")
1021
-
1297
+
1298
+ event_info = events_response.data[0]["event_info"]
1299
+ created_at = events_response.data[0]["created_at"]
1300
+
1301
+ logger.info(
1302
+ f"Найдено последнее событие '{event_type}' для пользователя {user_id}: {event_info[:50]}... (создано: {created_at})"
1303
+ )
1304
+
1022
1305
  return event_info
1023
-
1306
+
1024
1307
  except Exception as e:
1025
- logger.error(f"Ошибка получения последнего события для пользователя {user_id}, тип '{event_type}': {e}")
1308
+ logger.error(
1309
+ f"Ошибка получения последнего события для пользователя {user_id}, тип '{event_type}': {e}"
1310
+ )
1026
1311
  return None
1027
1312
 
1028
1313
  async def get_all_segments(self) -> List[str]:
1029
1314
  """
1030
1315
  Получает все уникальные сегменты из таблицы sales_users
1031
-
1316
+
1032
1317
  Returns:
1033
1318
  List[str]: Список уникальных сегментов
1034
1319
  """
1035
1320
  try:
1036
1321
  # Запрос всех непустых сегментов
1037
- query = self.client.table('sales_users').select('segments').neq('segments', '')
1038
-
1322
+ query = (
1323
+ self.client.table("sales_users").select("segments").neq("segments", "")
1324
+ )
1325
+
1039
1326
  if self.bot_id:
1040
- query = query.eq('bot_id', self.bot_id)
1041
-
1327
+ query = query.eq("bot_id", self.bot_id)
1328
+
1042
1329
  response = query.execute()
1043
-
1330
+
1044
1331
  # Собираем все уникальные сегменты
1045
1332
  all_segments = set()
1046
1333
  for row in response.data:
1047
- segments_str = row.get('segments', '')
1334
+ segments_str = row.get("segments", "")
1048
1335
  if segments_str:
1049
1336
  # Разбираем сегменты через запятую
1050
- segments = [s.strip() for s in segments_str.split(',') if s.strip()]
1337
+ segments = [s.strip() for s in segments_str.split(",") if s.strip()]
1051
1338
  all_segments.update(segments)
1052
-
1339
+
1053
1340
  segments_list = sorted(list(all_segments))
1054
1341
  logger.info(f"Найдено {len(segments_list)} уникальных сегментов")
1055
-
1342
+
1056
1343
  return segments_list
1057
-
1344
+
1058
1345
  except Exception as e:
1059
1346
  logger.error(f"Ошибка получения сегментов: {e}")
1060
1347
  return []
@@ -1062,192 +1349,191 @@ class SupabaseClient:
1062
1349
  async def get_users_by_segment(self, segment: str = None) -> List[Dict[str, Any]]:
1063
1350
  """
1064
1351
  Получает пользователей по сегменту или всех пользователей
1065
-
1352
+
1066
1353
  Args:
1067
1354
  segment: Название сегмента (если None - возвращает всех)
1068
-
1355
+
1069
1356
  Returns:
1070
1357
  List[Dict]: Список пользователей с telegram_id
1071
1358
  """
1072
1359
  try:
1073
- query = self.client.table('sales_users').select('telegram_id, segments')
1074
-
1360
+ query = self.client.table("sales_users").select("telegram_id, segments")
1361
+
1075
1362
  if self.bot_id:
1076
- query = query.eq('bot_id', self.bot_id)
1077
-
1363
+ query = query.eq("bot_id", self.bot_id)
1364
+
1078
1365
  response = query.execute()
1079
-
1366
+
1080
1367
  if segment is None:
1081
1368
  # Все пользователи
1082
1369
  logger.info(f"Получено {len(response.data)} всех пользователей")
1083
1370
  return response.data
1084
-
1371
+
1085
1372
  # Фильтруем по сегменту
1086
1373
  users = []
1087
1374
  for row in response.data:
1088
- segments_str = row.get('segments', '')
1375
+ segments_str = row.get("segments", "")
1089
1376
  if segments_str:
1090
- segments = [s.strip() for s in segments_str.split(',') if s.strip()]
1377
+ segments = [s.strip() for s in segments_str.split(",") if s.strip()]
1091
1378
  if segment in segments:
1092
1379
  users.append(row)
1093
-
1380
+
1094
1381
  logger.info(f"Найдено {len(users)} пользователей с сегментом '{segment}'")
1095
1382
  return users
1096
-
1383
+
1097
1384
  except Exception as e:
1098
1385
  logger.error(f"Ошибка получения пользователей по сегменту '{segment}': {e}")
1099
1386
  return []
1100
-
1387
+
1101
1388
  # =============================================================================
1102
1389
  # МЕТОДЫ ДЛЯ РАБОТЫ С ФАЙЛАМИ СОБЫТИЙ В SUPABASE STORAGE
1103
1390
  # =============================================================================
1104
-
1391
+
1105
1392
  async def upload_event_file(
1106
- self,
1107
- event_id: str,
1108
- file_data: bytes,
1109
- original_name: str,
1110
- file_id: str
1393
+ self, event_id: str, file_data: bytes, original_name: str, file_id: str
1111
1394
  ) -> Dict[str, str]:
1112
1395
  """
1113
1396
  Загружает файл события в Supabase Storage
1114
-
1397
+
1115
1398
  Args:
1116
1399
  event_id: ID события из БД (используется как папка)
1117
1400
  file_data: Байты файла
1118
1401
  original_name: Оригинальное имя файла (для метаданных)
1119
1402
  file_id: Уникальный ID файла для хранения
1120
-
1403
+
1121
1404
  Returns:
1122
1405
  Dict с storage_path и original_name
1123
1406
  """
1124
1407
  try:
1125
- bucket_name = 'admin-events'
1126
-
1408
+ bucket_name = "admin-events"
1409
+
1127
1410
  # Формируем путь: admin-events/event_id/file_id.ext
1128
- extension = original_name.split('.')[-1] if '.' in original_name else ''
1411
+ extension = original_name.split(".")[-1] if "." in original_name else ""
1129
1412
  storage_name = f"{file_id}.{extension}" if extension else file_id
1130
1413
  storage_path = f"events/{event_id}/files/{storage_name}"
1131
-
1414
+
1132
1415
  # Определяем MIME-type по оригинальному имени файла
1133
1416
  import mimetypes
1417
+
1134
1418
  content_type, _ = mimetypes.guess_type(original_name)
1135
1419
  if not content_type:
1136
- content_type = 'application/octet-stream'
1137
-
1420
+ content_type = "application/octet-stream"
1421
+
1138
1422
  # Загружаем в Storage
1139
1423
  self.client.storage.from_(bucket_name).upload(
1140
- storage_path,
1141
- file_data,
1142
- file_options={"content-type": content_type}
1424
+ storage_path, file_data, file_options={"content-type": content_type}
1143
1425
  )
1144
-
1426
+
1145
1427
  logger.info(f"✅ Файл загружен в Storage: {storage_path}")
1146
-
1147
- return {
1148
- 'storage_path': storage_path
1149
- }
1150
-
1428
+
1429
+ return {"storage_path": storage_path}
1430
+
1151
1431
  except Exception as e:
1152
1432
  logger.error(f"❌ Ошибка загрузки файла в Storage: {e}")
1153
1433
  raise
1154
-
1434
+
1155
1435
  async def download_event_file(self, event_id: str, storage_path: str) -> bytes:
1156
1436
  """
1157
1437
  Скачивает файл события из Supabase Storage
1158
-
1438
+
1159
1439
  Args:
1160
1440
  event_id: ID события
1161
1441
  storage_path: Полный путь к файлу в Storage
1162
-
1442
+
1163
1443
  Returns:
1164
1444
  bytes: Содержимое файла
1165
1445
  """
1166
1446
  try:
1167
- bucket_name = 'admin-events'
1168
-
1447
+ bucket_name = "admin-events"
1448
+
1169
1449
  # Скачиваем файл
1170
1450
  file_data = self.client.storage.from_(bucket_name).download(storage_path)
1171
-
1451
+
1172
1452
  logger.info(f"✅ Файл скачан из Storage: {storage_path}")
1173
1453
  return file_data
1174
-
1454
+
1175
1455
  except Exception as e:
1176
1456
  logger.error(f"❌ Ошибка скачивания файла из Storage: {e}")
1177
1457
  raise
1178
-
1458
+
1179
1459
  async def delete_event_files(self, event_id: str):
1180
1460
  """
1181
1461
  Удаляет ВСЕ файлы события из Supabase Storage
1182
-
1462
+
1183
1463
  Args:
1184
1464
  event_id: ID события
1185
1465
  """
1186
1466
  try:
1187
- bucket_name = 'admin-events'
1467
+ bucket_name = "admin-events"
1188
1468
  event_path = f"events/{event_id}/files"
1189
-
1469
+
1190
1470
  # Получаем список всех файлов в папке события
1191
1471
  files_list = self.client.storage.from_(bucket_name).list(event_path)
1192
-
1472
+
1193
1473
  if not files_list:
1194
1474
  logger.info(f"ℹ️ Нет файлов для удаления в событии '{event_id}'")
1195
1475
  return
1196
-
1476
+
1197
1477
  # Формируем пути для удаления
1198
1478
  file_paths = [f"{event_path}/{file['name']}" for file in files_list]
1199
-
1479
+
1200
1480
  # Удаляем файлы
1201
1481
  self.client.storage.from_(bucket_name).remove(file_paths)
1202
-
1203
- logger.info(f"✅ Удалено {len(file_paths)} файлов события '{event_id}' из Storage")
1204
-
1482
+
1483
+ logger.info(
1484
+ f"✅ Удалено {len(file_paths)} файлов события '{event_id}' из Storage"
1485
+ )
1486
+
1205
1487
  except Exception as e:
1206
1488
  logger.error(f"❌ Ошибка удаления файлов события из Storage: {e}")
1207
1489
  # Не прерываем выполнение, только логируем
1208
1490
 
1209
1491
  async def save_admin_event(
1210
- self,
1211
- event_name: str,
1212
- event_data: Dict[str, Any],
1213
- scheduled_datetime: datetime
1492
+ self, event_name: str, event_data: Dict[str, Any], scheduled_datetime: datetime
1214
1493
  ) -> Dict[str, Any]:
1215
1494
  """
1216
1495
  Сохраняет админское событие в таблицу scheduled_events
1217
-
1496
+
1218
1497
  Args:
1219
1498
  event_name: Название события
1220
1499
  event_data: Данные события (сегмент, сообщение, файлы)
1221
1500
  scheduled_datetime: Дата и время отправки (должно быть в UTC с timezone info)
1222
-
1501
+
1223
1502
  Returns:
1224
1503
  Dict[str, Any]: {'id': str, 'event_type': str, ...} - все данные созданного события
1225
1504
  """
1226
1505
  try:
1227
1506
  import json
1228
-
1507
+
1229
1508
  # Убеждаемся что datetime в правильном формате для PostgreSQL
1230
1509
  # Если есть timezone info - используем, иначе предполагаем что это UTC
1231
1510
  if scheduled_datetime.tzinfo is None:
1232
- logger.warning("⚠️ scheduled_datetime без timezone info, предполагаем UTC")
1511
+ logger.warning(
1512
+ "⚠️ scheduled_datetime без timezone info, предполагаем UTC"
1513
+ )
1233
1514
  from datetime import timezone
1515
+
1234
1516
  scheduled_datetime = scheduled_datetime.replace(tzinfo=timezone.utc)
1235
-
1517
+
1236
1518
  event_record = {
1237
- 'event_type': event_name,
1238
- 'event_category': 'admin_event',
1239
- 'user_id': None, # Для всех пользователей
1240
- 'event_data': json.dumps(event_data, ensure_ascii=False),
1241
- 'scheduled_at': scheduled_datetime.isoformat(),
1242
- 'status': 'pending'
1519
+ "event_type": event_name,
1520
+ "event_category": "admin_event",
1521
+ "user_id": None, # Для всех пользователей
1522
+ "event_data": json.dumps(event_data, ensure_ascii=False),
1523
+ "scheduled_at": scheduled_datetime.isoformat(),
1524
+ "status": "pending",
1243
1525
  }
1244
-
1245
- response = self.client.table('scheduled_events').insert(event_record).execute()
1526
+
1527
+ response = (
1528
+ self.client.table("scheduled_events").insert(event_record).execute()
1529
+ )
1246
1530
  event = response.data[0]
1247
-
1248
- logger.info(f"💾 Админское событие '{event_name}' сохранено в БД: {event['id']} на {scheduled_datetime.isoformat()}")
1531
+
1532
+ logger.info(
1533
+ f"💾 Админское событие '{event_name}' сохранено в БД: {event['id']} на {scheduled_datetime.isoformat()}"
1534
+ )
1249
1535
  return event
1250
-
1536
+
1251
1537
  except Exception as e:
1252
1538
  logger.error(f"❌ Ошибка сохранения админского события: {e}")
1253
1539
  raise
@@ -1255,54 +1541,59 @@ class SupabaseClient:
1255
1541
  async def get_admin_events(self, status: str = None) -> List[Dict[str, Any]]:
1256
1542
  """
1257
1543
  Получает админские события
1258
-
1544
+
1259
1545
  Args:
1260
1546
  status: Фильтр по статусу (pending, completed, cancelled)
1261
-
1547
+
1262
1548
  Returns:
1263
1549
  List[Dict]: Список админских событий
1264
1550
  """
1265
1551
  try:
1266
- query = self.client.table('scheduled_events').select(
1267
- '*'
1268
- ).eq('event_category', 'admin_event')
1269
-
1552
+ query = (
1553
+ self.client.table("scheduled_events")
1554
+ .select("*")
1555
+ .eq("event_category", "admin_event")
1556
+ )
1557
+
1270
1558
  if status:
1271
- query = query.eq('status', status)
1272
-
1273
- response = query.order('scheduled_at', desc=False).execute()
1274
-
1559
+ query = query.eq("status", status)
1560
+
1561
+ response = query.order("scheduled_at", desc=False).execute()
1562
+
1275
1563
  logger.info(f"Найдено {len(response.data)} админских событий")
1276
1564
  return response.data
1277
-
1565
+
1278
1566
  except Exception as e:
1279
1567
  logger.error(f"Ошибка получения админских событий: {e}")
1280
1568
  return []
1281
-
1569
+
1282
1570
  async def check_event_name_exists(self, event_name: str) -> bool:
1283
1571
  """
1284
1572
  Проверяет существует ли активное событие с таким названием
1285
-
1573
+
1286
1574
  Args:
1287
1575
  event_name: Название события для проверки
1288
-
1576
+
1289
1577
  Returns:
1290
1578
  bool: True если активное событие с таким именем существует
1291
1579
  """
1292
1580
  try:
1293
- response = self.client.table('scheduled_events').select(
1294
- 'id', 'event_type', 'status'
1295
- ).eq('event_category', 'admin_event').eq(
1296
- 'event_type', event_name
1297
- ).eq('status', 'pending').execute()
1298
-
1581
+ response = (
1582
+ self.client.table("scheduled_events")
1583
+ .select("id", "event_type", "status")
1584
+ .eq("event_category", "admin_event")
1585
+ .eq("event_type", event_name)
1586
+ .eq("status", "pending")
1587
+ .execute()
1588
+ )
1589
+
1299
1590
  exists = len(response.data) > 0
1300
-
1591
+
1301
1592
  if exists:
1302
1593
  logger.info(f"⚠️ Найдено активное событие с названием '{event_name}'")
1303
-
1594
+
1304
1595
  return exists
1305
-
1596
+
1306
1597
  except Exception as e:
1307
1598
  logger.error(f"Ошибка проверки названия события: {e}")
1308
- return False
1599
+ return False