smart-bot-factory 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of smart-bot-factory might be problematic. Click here for more details.
- smart_bot_factory/__init__.py +51 -0
- smart_bot_factory/admin/__init__.py +16 -0
- smart_bot_factory/admin/admin_logic.py +430 -0
- smart_bot_factory/admin/admin_manager.py +141 -0
- smart_bot_factory/admin/admin_migration.sql +136 -0
- smart_bot_factory/admin/admin_tester.py +151 -0
- smart_bot_factory/admin/timeout_checker.py +499 -0
- smart_bot_factory/analytics/__init__.py +7 -0
- smart_bot_factory/analytics/analytics_manager.py +355 -0
- smart_bot_factory/cli.py +642 -0
- smart_bot_factory/config.py +235 -0
- smart_bot_factory/configs/growthmed-helper/env_example.txt +1 -0
- smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +9 -0
- smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +582 -0
- smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +66 -0
- smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +232 -0
- smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +28 -0
- smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +7 -0
- smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +16 -0
- 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
- smart_bot_factory/configs/growthmed-october-24/prompts/1sales_context.txt +16 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/2product_info.txt +582 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/3objection_handling.txt +66 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +212 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/help_message.txt +28 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/welcome_message.txt +8 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064229.txt +818 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064335.txt +32 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064638.txt +35 -0
- smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +66 -0
- smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml +108 -0
- smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml +46 -0
- smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt +16 -0
- 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
- smart_bot_factory/core/__init__.py +22 -0
- smart_bot_factory/core/bot_utils.py +693 -0
- smart_bot_factory/core/conversation_manager.py +536 -0
- smart_bot_factory/core/decorators.py +229 -0
- smart_bot_factory/core/message_sender.py +249 -0
- smart_bot_factory/core/states.py +14 -0
- smart_bot_factory/creation/__init__.py +8 -0
- smart_bot_factory/creation/bot_builder.py +329 -0
- smart_bot_factory/creation/bot_testing.py +986 -0
- smart_bot_factory/database/database_structure.sql +57 -0
- smart_bot_factory/database/schema.sql +1094 -0
- smart_bot_factory/handlers/handlers.py +583 -0
- smart_bot_factory/integrations/__init__.py +9 -0
- smart_bot_factory/integrations/openai_client.py +435 -0
- smart_bot_factory/integrations/supabase_client.py +592 -0
- smart_bot_factory/setup_checker.py +476 -0
- smart_bot_factory/utils/__init__.py +9 -0
- smart_bot_factory/utils/debug_routing.py +103 -0
- smart_bot_factory/utils/prompt_loader.py +427 -0
- smart_bot_factory/uv.lock +2004 -0
- smart_bot_factory-0.1.3.dist-info/METADATA +126 -0
- smart_bot_factory-0.1.3.dist-info/RECORD +59 -0
- smart_bot_factory-0.1.3.dist-info/licenses/LICENSE +24 -0
- smart_bot_factory-0.1.2.dist-info/METADATA +0 -31
- smart_bot_factory-0.1.2.dist-info/RECORD +0 -4
- {smart_bot_factory-0.1.2.dist-info → smart_bot_factory-0.1.3.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.1.2.dist-info → smart_bot_factory-0.1.3.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
|