smart-bot-factory 0.3.7__py3-none-any.whl → 0.3.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of smart-bot-factory might be problematic. Click here for more details.

Files changed (45) hide show
  1. smart_bot_factory/admin/__init__.py +7 -7
  2. smart_bot_factory/admin/admin_events.py +483 -383
  3. smart_bot_factory/admin/admin_logic.py +234 -158
  4. smart_bot_factory/admin/admin_manager.py +68 -53
  5. smart_bot_factory/admin/admin_tester.py +46 -40
  6. smart_bot_factory/admin/timeout_checker.py +201 -153
  7. smart_bot_factory/aiogram_calendar/__init__.py +11 -3
  8. smart_bot_factory/aiogram_calendar/common.py +12 -18
  9. smart_bot_factory/aiogram_calendar/dialog_calendar.py +126 -64
  10. smart_bot_factory/aiogram_calendar/schemas.py +49 -28
  11. smart_bot_factory/aiogram_calendar/simple_calendar.py +94 -50
  12. smart_bot_factory/analytics/analytics_manager.py +414 -392
  13. smart_bot_factory/cli.py +204 -148
  14. smart_bot_factory/config.py +123 -102
  15. smart_bot_factory/core/bot_utils.py +474 -332
  16. smart_bot_factory/core/conversation_manager.py +287 -200
  17. smart_bot_factory/core/decorators.py +1129 -749
  18. smart_bot_factory/core/message_sender.py +287 -266
  19. smart_bot_factory/core/router.py +170 -100
  20. smart_bot_factory/core/router_manager.py +121 -83
  21. smart_bot_factory/core/states.py +4 -3
  22. smart_bot_factory/creation/__init__.py +1 -1
  23. smart_bot_factory/creation/bot_builder.py +320 -242
  24. smart_bot_factory/creation/bot_testing.py +440 -365
  25. smart_bot_factory/dashboard/__init__.py +1 -3
  26. smart_bot_factory/event/__init__.py +2 -7
  27. smart_bot_factory/handlers/handlers.py +676 -472
  28. smart_bot_factory/integrations/openai_client.py +218 -168
  29. smart_bot_factory/integrations/supabase_client.py +928 -637
  30. smart_bot_factory/message/__init__.py +18 -22
  31. smart_bot_factory/router/__init__.py +2 -2
  32. smart_bot_factory/setup_checker.py +162 -126
  33. smart_bot_factory/supabase/__init__.py +1 -1
  34. smart_bot_factory/supabase/client.py +631 -515
  35. smart_bot_factory/utils/__init__.py +2 -3
  36. smart_bot_factory/utils/debug_routing.py +38 -27
  37. smart_bot_factory/utils/prompt_loader.py +153 -120
  38. smart_bot_factory/utils/user_prompt_loader.py +55 -56
  39. smart_bot_factory/utm_link_generator.py +123 -116
  40. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/METADATA +3 -1
  41. smart_bot_factory-0.3.8.dist-info/RECORD +59 -0
  42. smart_bot_factory-0.3.7.dist-info/RECORD +0 -59
  43. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/WHEEL +0 -0
  44. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/entry_points.txt +0 -0
  45. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/licenses/LICENSE +0 -0
@@ -1,88 +1,94 @@
1
- import logging
2
1
  import asyncio
3
- from typing import List, Dict, Any, Optional
2
+ import logging
3
+ from typing import Any, Dict, List, Optional
4
+
4
5
  from openai import AsyncOpenAI
5
6
  from openai.types.chat import ChatCompletion
6
7
 
7
8
  logger = logging.getLogger(__name__)
8
9
 
10
+
9
11
  class OpenAIClient:
10
12
  """Клиент для работы с OpenAI API"""
11
-
13
+
12
14
  def __init__(
13
- self,
14
- api_key: str,
15
- model: str = "gpt-4",
15
+ self,
16
+ api_key: str,
17
+ model: str = "gpt-4",
16
18
  max_tokens: int = 1500,
17
- temperature: float = 0.7
19
+ temperature: float = 0.7,
18
20
  ):
19
21
  self.client = AsyncOpenAI(api_key=api_key)
20
22
  self.model = model
21
23
  self.max_tokens = max_tokens
22
24
  self.temperature = temperature
23
-
25
+
24
26
  # Настройки для повторных попыток
25
27
  self.max_retries = 3
26
28
  self.retry_delay = 1 # секунды
27
-
29
+
28
30
  # Определяем, является ли это GPT-5 моделью
29
- self.is_gpt5 = 'gpt-5' in model.lower()
30
-
31
+ self.is_gpt5 = "gpt-5" in model.lower()
32
+
31
33
  # Получаем лимиты для модели
32
34
  self.model_limits = self._get_model_limits()
33
-
35
+
34
36
  # 🆕 Для диагностики пустых ответов
35
37
  self.last_completion_tokens = 0
36
-
37
- logger.info(f"OpenAI клиент инициализирован с моделью {model} (GPT-5: {self.is_gpt5}, лимит: {self.model_limits['total_context']} токенов)")
38
-
38
+
39
+ logger.info(
40
+ f"OpenAI клиент инициализирован с моделью {model} (GPT-5: {self.is_gpt5}, лимит: {self.model_limits['total_context']} токенов)"
41
+ )
42
+
39
43
  def _get_model_limits(self) -> Dict[str, int]:
40
44
  """Возвращает лимиты для конкретной модели"""
41
45
  model_limits = {
42
46
  # GPT-3.5
43
- 'gpt-3.5-turbo': 4096,
44
- 'gpt-3.5-turbo-16k': 16385,
45
-
47
+ "gpt-3.5-turbo": 4096,
48
+ "gpt-3.5-turbo-16k": 16385,
46
49
  # GPT-4
47
- 'gpt-4': 8192,
48
- 'gpt-4-32k': 32768,
49
- 'gpt-4-turbo': 128000,
50
- 'gpt-4-turbo-preview': 128000,
51
- 'gpt-4o': 128000,
52
- 'gpt-4o-mini': 128000,
53
-
50
+ "gpt-4": 8192,
51
+ "gpt-4-32k": 32768,
52
+ "gpt-4-turbo": 128000,
53
+ "gpt-4-turbo-preview": 128000,
54
+ "gpt-4o": 128000,
55
+ "gpt-4o-mini": 128000,
54
56
  # GPT-5
55
- 'gpt-5-mini': 128000,
56
- 'gpt-5': 200000,
57
+ "gpt-5-mini": 128000,
58
+ "gpt-5": 200000,
57
59
  }
58
-
60
+
59
61
  # Получаем лимит для текущей модели или используем консервативное значение
60
62
  total_limit = model_limits.get(self.model, 8192)
61
-
63
+
62
64
  # Резервируем место для ответа и буфера
63
- completion_reserve = min(self.max_tokens * 2, total_limit // 4) # Резервируем место для ответа
65
+ completion_reserve = min(
66
+ self.max_tokens * 2, total_limit // 4
67
+ ) # Резервируем место для ответа
64
68
  buffer_reserve = 500 # Буфер для безопасности
65
-
69
+
66
70
  return {
67
- 'total_context': total_limit,
68
- 'max_input_tokens': total_limit - completion_reserve - buffer_reserve,
69
- 'completion_reserve': completion_reserve
71
+ "total_context": total_limit,
72
+ "max_input_tokens": total_limit - completion_reserve - buffer_reserve,
73
+ "completion_reserve": completion_reserve,
70
74
  }
71
-
72
- def _get_completion_params(self, max_tokens: Optional[int] = None, temperature: Optional[float] = None) -> Dict[str, Any]:
75
+
76
+ def _get_completion_params(
77
+ self, max_tokens: Optional[int] = None, temperature: Optional[float] = None
78
+ ) -> Dict[str, Any]:
73
79
  """Возвращает параметры для создания completion в зависимости от модели"""
74
80
  tokens = max_tokens or self.max_tokens
75
81
  temp = temperature or self.temperature
76
-
82
+
77
83
  params = {
78
84
  "model": self.model,
79
85
  }
80
-
86
+
81
87
  # 🆕 ИСПРАВЛЕННАЯ ЛОГИКА ДЛЯ GPT-5
82
88
  if self.is_gpt5:
83
89
  # 🔧 ИСПОЛЬЗУЕМ ТОКЕНЫ ИЗ КОНФИГА (как настроил пользователь)
84
90
  params["max_completion_tokens"] = tokens
85
-
91
+
86
92
  # 🔧 БЫСТРЫЕ ОТВЕТЫ с минимальным reasoning
87
93
  # Допустимые значения reasoning_effort:
88
94
  # "minimal" - самые быстрые ответы, минимальное reasoning
@@ -90,236 +96,277 @@ class OpenAIClient:
90
96
  # "medium" - сбалансированное reasoning (медленнее)
91
97
  # "high" - максимальное reasoning (самые медленные, но качественные)
92
98
  params["reasoning_effort"] = "minimal" # Быстрые ответы
93
-
94
- logger.debug(f"GPT-5 параметры: max_completion_tokens={tokens}, reasoning_effort=minimal")
99
+
100
+ logger.debug(
101
+ f"GPT-5 параметры: max_completion_tokens={tokens}, reasoning_effort=minimal"
102
+ )
95
103
  else:
96
104
  # Для остальных моделей стандартные параметры
97
105
  params["max_tokens"] = tokens
98
106
  params["temperature"] = temp
99
-
107
+
100
108
  return params
101
109
 
102
110
  async def get_completion(
103
- self,
104
- messages: List[Dict[str, str]],
111
+ self,
112
+ messages: List[Dict[str, str]],
105
113
  max_tokens: Optional[int] = None,
106
114
  temperature: Optional[float] = None,
107
- stream: bool = False
115
+ stream: bool = False,
108
116
  ) -> str:
109
117
  """Получает ответ от OpenAI API с поддержкой GPT-5"""
110
118
  if not messages:
111
119
  raise ValueError("Список сообщений не может быть пустым")
112
-
120
+
113
121
  for attempt in range(self.max_retries):
114
122
  try:
115
123
  logger.debug(f"Отправка запроса к OpenAI (попытка {attempt + 1})")
116
-
124
+
117
125
  # Обрезаем сообщения если контекст слишком большой
118
126
  processed_messages = await self._prepare_messages(messages)
119
-
127
+
120
128
  # Получаем параметры в зависимости от модели
121
129
  params = self._get_completion_params(max_tokens, temperature)
122
130
  params["messages"] = processed_messages
123
131
  params["stream"] = stream
124
-
132
+
125
133
  logger.info(f"🚀 Отправляем запрос к {self.model}")
126
134
  logger.info(f"📝 Сообщений в контексте: {len(processed_messages)}")
127
-
135
+
128
136
  # Логируем параметры в зависимости от модели
129
137
  if self.is_gpt5:
130
- max_tokens_param = params.get('max_completion_tokens')
131
- reasoning_effort = params.get('reasoning_effort')
132
- logger.info(f"⚙️ GPT-5 параметры: max_completion_tokens={max_tokens_param}, reasoning_effort={reasoning_effort}")
138
+ max_tokens_param = params.get("max_completion_tokens")
139
+ reasoning_effort = params.get("reasoning_effort")
140
+ logger.info(
141
+ f"⚙️ GPT-5 параметры: max_completion_tokens={max_tokens_param}, reasoning_effort={reasoning_effort}"
142
+ )
133
143
  else:
134
- max_tokens_param = params.get('max_tokens')
135
- temp_param = params.get('temperature')
136
- logger.info(f"⚙️ Стандартные параметры: max_tokens={max_tokens_param}, temp={temp_param}")
137
-
138
- response: ChatCompletion = await self.client.chat.completions.create(**params)
139
-
144
+ max_tokens_param = params.get("max_tokens")
145
+ temp_param = params.get("temperature")
146
+ logger.info(
147
+ f"⚙️ Стандартные параметры: max_tokens={max_tokens_param}, temp={temp_param}"
148
+ )
149
+
150
+ response: ChatCompletion = await self.client.chat.completions.create(
151
+ **params
152
+ )
153
+
140
154
  if stream:
141
155
  return await self._handle_stream_response(response)
142
156
  else:
143
157
  # 🔧 ПРАВИЛЬНЫЙ ПАРСИНГ ДЛЯ GPT-5 (может быть несколько choices)
144
158
  if self.is_gpt5 and len(response.choices) > 1:
145
- logger.info(f"📊 GPT-5 вернул {len(response.choices)} choices, ищем пользовательский контент")
146
-
159
+ logger.info(
160
+ f"📊 GPT-5 вернул {len(response.choices)} choices, ищем пользовательский контент"
161
+ )
162
+
147
163
  # Ищем choice с пользовательским контентом (не reasoning summary)
148
164
  content = None
149
165
  finish_reason = None
150
-
166
+
151
167
  for i, choice in enumerate(response.choices):
152
168
  choice_content = choice.message.content
153
169
  choice_finish = choice.finish_reason
154
-
155
- logger.info(f" Choice {i}: content={len(choice_content) if choice_content else 0} chars, finish={choice_finish}")
156
-
170
+
171
+ logger.info(
172
+ f" Choice {i}: content={len(choice_content) if choice_content else 0} chars, finish={choice_finish}"
173
+ )
174
+
157
175
  # Берем первый choice с реальным контентом
158
176
  if choice_content and choice_content.strip():
159
177
  content = choice_content
160
178
  finish_reason = choice_finish
161
- logger.info(f" ✅ Используем choice {i} как пользовательский контент")
179
+ logger.info(
180
+ f" ✅ Используем choice {i} как пользовательский контент"
181
+ )
162
182
  break
163
-
183
+
164
184
  # Если не нашли контент ни в одном choice
165
185
  if not content:
166
- logger.warning(f" ❌ Не найден пользовательский контент ни в одном choice")
186
+ logger.warning(
187
+ " ❌ Не найден пользовательский контент ни в одном choice"
188
+ )
167
189
  content = ""
168
190
  finish_reason = response.choices[0].finish_reason
169
191
  else:
170
192
  # Стандартная обработка для не-GPT-5 или одного choice
171
193
  finish_reason = response.choices[0].finish_reason
172
194
  content = response.choices[0].message.content
173
-
174
- logger.info(f"📊 OpenAI ответ получен:")
195
+
196
+ logger.info("📊 OpenAI ответ получен:")
175
197
  logger.info(f" 🏁 Finish reason: {finish_reason}")
176
198
  logger.info(f" 🔤 Тип content: {type(content)}")
177
199
  logger.info(f" 📏 Длина: {len(content) if content else 0}")
178
-
200
+
179
201
  # Логируем использование токенов
180
202
  if response.usage:
181
203
  self.last_completion_tokens = response.usage.completion_tokens
182
-
183
- logger.info(f"💰 Использование токенов:")
204
+
205
+ logger.info("💰 Использование токенов:")
184
206
  logger.info(f" 📥 Prompt: {response.usage.prompt_tokens}")
185
- logger.info(f" 📤 Completion: {response.usage.completion_tokens}")
207
+ logger.info(
208
+ f" 📤 Completion: {response.usage.completion_tokens}"
209
+ )
186
210
  logger.info(f" 🔢 Total: {response.usage.total_tokens}")
187
-
211
+
188
212
  # 🆕 ЛОГИРУЕМ REASONING TOKENS ДЛЯ GPT-5
189
- if hasattr(response.usage, 'reasoning_tokens'):
190
- reasoning_tokens = getattr(response.usage, 'reasoning_tokens', 0)
213
+ if hasattr(response.usage, "reasoning_tokens"):
214
+ reasoning_tokens = getattr(
215
+ response.usage, "reasoning_tokens", 0
216
+ )
191
217
  if reasoning_tokens > 0:
192
218
  logger.info(f" 💭 Reasoning: {reasoning_tokens}")
193
-
219
+
194
220
  # 🔧 ИСПРАВЛЕННАЯ ОБРАБОТКА GPT-5
195
221
  if self.is_gpt5:
196
222
  # Проверяем, есть ли reasoning токены
197
223
  reasoning_tokens = 0
198
- if response.usage and hasattr(response.usage, 'reasoning_tokens'):
199
- reasoning_tokens = getattr(response.usage, 'reasoning_tokens', 0)
200
-
224
+ if response.usage and hasattr(
225
+ response.usage, "reasoning_tokens"
226
+ ):
227
+ reasoning_tokens = getattr(
228
+ response.usage, "reasoning_tokens", 0
229
+ )
230
+
201
231
  # Если есть reasoning но нет content, увеличиваем токены
202
- if reasoning_tokens > 0 and (not content or not content.strip()):
203
- logger.warning(f"🔧 GPT-5: reasoning_tokens={reasoning_tokens}, но content пустой")
204
-
232
+ if reasoning_tokens > 0 and (
233
+ not content or not content.strip()
234
+ ):
235
+ logger.warning(
236
+ f"🔧 GPT-5: reasoning_tokens={reasoning_tokens}, но content пустой"
237
+ )
238
+
205
239
  # Если это не последняя попытка, пробуем с большим количеством токенов
206
240
  if attempt < self.max_retries - 1:
207
241
  current_tokens = max_tokens or self.max_tokens
208
- increased_tokens = min(current_tokens * 2, 8000) # Удваиваем, максимум 8000
209
-
210
- logger.info(f"🔄 Пробуем с увеличенными токенами: {increased_tokens}")
242
+ increased_tokens = min(
243
+ current_tokens * 2, 8000
244
+ ) # Удваиваем, максимум 8000
245
+
246
+ logger.info(
247
+ f"🔄 Пробуем с увеличенными токенами: {increased_tokens}"
248
+ )
211
249
  return await self.get_completion(
212
- messages,
213
- max_tokens=increased_tokens,
214
- temperature=temperature,
215
- stream=stream
250
+ messages,
251
+ max_tokens=increased_tokens,
252
+ temperature=temperature,
253
+ stream=stream,
216
254
  )
217
255
  else:
218
256
  # Последняя попытка - возвращаем информативное сообщение
219
257
  return "Извините, модель потратила время на анализ, но не смогла сформулировать ответ. Попробуйте переформулировать вопрос."
220
-
258
+
221
259
  # Обычная обработка finish_reason
222
- if finish_reason == 'length':
223
- logger.warning(f"⚠️ Модель достигла лимита токенов")
224
-
260
+ if finish_reason == "length":
261
+ logger.warning("⚠️ Модель достигла лимита токенов")
262
+
225
263
  if content and content.strip():
226
264
  # Есть частичный контент - возвращаем его
227
265
  return content
228
266
  else:
229
267
  # Нет контента - информируем пользователя
230
268
  return "Ответ слишком длинный. Попробуйте задать более конкретный вопрос."
231
-
232
- elif finish_reason == 'stop':
233
- logger.info(f"✅ Нормальное завершение")
234
-
235
- elif finish_reason == 'content_filter':
236
- logger.warning(f"🚫 Ответ заблокирован фильтром контента")
269
+
270
+ elif finish_reason == "stop":
271
+ logger.info("✅ Нормальное завершение")
272
+
273
+ elif finish_reason == "content_filter":
274
+ logger.warning("🚫 Ответ заблокирован фильтром контента")
237
275
  return "Извините, не могу ответить на этот вопрос."
238
-
276
+
239
277
  # Детальное логирование содержимого
240
278
  if content:
241
279
  logger.info(f" 🔍 Начало (50 символов): '{content[:50]}'")
242
280
  logger.info(f" 🔍 Конец (50 символов): '{content[-50:]}'")
243
281
  else:
244
- logger.warning(f" ❌ content is None или пустой")
245
-
282
+ logger.warning(" ❌ content is None или пустой")
283
+
246
284
  return content or ""
247
-
285
+
248
286
  except Exception as e:
249
- logger.warning(f"Ошибка при обращении к OpenAI (попытка {attempt + 1}): {e}")
250
-
287
+ logger.warning(
288
+ f"Ошибка при обращении к OpenAI (попытка {attempt + 1}): {e}"
289
+ )
290
+
251
291
  if attempt == self.max_retries - 1:
252
292
  logger.error(f"Все попытки исчерпаны. Последняя ошибка: {e}")
253
293
  raise
254
-
294
+
255
295
  await asyncio.sleep(self.retry_delay * (attempt + 1))
256
-
296
+
257
297
  raise Exception("Не удалось получить ответ от OpenAI")
258
-
259
- async def _prepare_messages(self, messages: List[Dict[str, str]]) -> List[Dict[str, str]]:
298
+
299
+ async def _prepare_messages(
300
+ self, messages: List[Dict[str, str]]
301
+ ) -> List[Dict[str, str]]:
260
302
  """
261
303
  Подготавливает сообщения для отправки в API
262
304
  Обрезает контекст если он слишком большой
263
305
  """
306
+
264
307
  # Более точная оценка токенов для русского текста
265
308
  def estimate_message_tokens(msg):
266
- content = msg.get('content', '')
309
+ content = msg.get("content", "")
267
310
  # Для русского текста: примерно 2.5-3 символа на токен
268
311
  return len(content) // 2.5
269
-
312
+
270
313
  total_estimated_tokens = sum(estimate_message_tokens(msg) for msg in messages)
271
- max_input_tokens = self.model_limits['max_input_tokens']
272
-
314
+ max_input_tokens = self.model_limits["max_input_tokens"]
315
+
273
316
  if total_estimated_tokens <= max_input_tokens:
274
317
  return messages
275
-
276
- logger.info(f"Контекст слишком большой ({int(total_estimated_tokens)} токенов), обрезаем до {max_input_tokens}")
277
-
318
+
319
+ logger.info(
320
+ f"Контекст слишком большой ({int(total_estimated_tokens)} токенов), обрезаем до {max_input_tokens}"
321
+ )
322
+
278
323
  # Сохраняем системные сообщения
279
- system_messages = [msg for msg in messages if msg.get('role') == 'system']
280
- other_messages = [msg for msg in messages if msg.get('role') != 'system']
281
-
324
+ system_messages = [msg for msg in messages if msg.get("role") == "system"]
325
+ other_messages = [msg for msg in messages if msg.get("role") != "system"]
326
+
282
327
  # Рассчитываем токены системных сообщений
283
328
  system_tokens = sum(estimate_message_tokens(msg) for msg in system_messages)
284
329
  available_tokens = max_input_tokens - system_tokens
285
-
330
+
286
331
  if available_tokens <= 0:
287
332
  logger.warning("Системные сообщения занимают весь доступный контекст")
288
333
  return system_messages
289
-
334
+
290
335
  # Берем последние сообщения, помещающиеся в доступные токены
291
336
  current_tokens = 0
292
337
  trimmed_messages = []
293
-
338
+
294
339
  for msg in reversed(other_messages):
295
340
  msg_tokens = estimate_message_tokens(msg)
296
341
  if current_tokens + msg_tokens > available_tokens:
297
342
  break
298
343
  trimmed_messages.insert(0, msg)
299
344
  current_tokens += msg_tokens
300
-
345
+
301
346
  result_messages = system_messages + trimmed_messages
302
- logger.info(f"Контекст обрезан до {len(result_messages)} сообщений (~{int(current_tokens + system_tokens)} токенов)")
303
-
347
+ logger.info(
348
+ f"Контекст обрезан до {len(result_messages)} сообщений (~{int(current_tokens + system_tokens)} токенов)"
349
+ )
350
+
304
351
  return result_messages
305
-
352
+
306
353
  async def _handle_stream_response(self, response) -> str:
307
354
  """Обрабатывает потоковый ответ"""
308
355
  full_content = ""
309
-
356
+
310
357
  async for chunk in response:
311
358
  if chunk.choices[0].delta.content is not None:
312
359
  full_content += chunk.choices[0].delta.content
313
-
360
+
314
361
  return full_content
315
-
362
+
316
363
  async def analyze_sentiment(self, text: str) -> Dict[str, Any]:
317
364
  """
318
365
  Анализирует настроение и намерения сообщения
319
-
366
+
320
367
  Args:
321
368
  text: Текст для анализа
322
-
369
+
323
370
  Returns:
324
371
  Словарь с результатами анализа
325
372
  """
@@ -343,20 +390,27 @@ class OpenAIClient:
343
390
  "response_strategy": "рекомендуемая стратегия"
344
391
  }}
345
392
  """
346
-
393
+
347
394
  try:
348
395
  # Для анализа настроения используем более низкую температуру если поддерживается
349
396
  temp = 0.3 if not self.is_gpt5 else None
350
-
351
- response = await self.get_completion([
352
- {"role": "system", "content": "Ты эксперт по анализу намерений клиентов в продажах."},
353
- {"role": "user", "content": analysis_prompt}
354
- ], temperature=temp)
355
-
397
+
398
+ response = await self.get_completion(
399
+ [
400
+ {
401
+ "role": "system",
402
+ "content": "Ты эксперт по анализу намерений клиентов в продажах.",
403
+ },
404
+ {"role": "user", "content": analysis_prompt},
405
+ ],
406
+ temperature=temp,
407
+ )
408
+
356
409
  # Пытаемся распарсить JSON
357
410
  import json
411
+
358
412
  return json.loads(response)
359
-
413
+
360
414
  except Exception as e:
361
415
  logger.error(f"Ошибка при анализе настроения: {e}")
362
416
  # Возвращаем дефолтные значения
@@ -366,21 +420,19 @@ class OpenAIClient:
366
420
  "purchase_readiness": 5,
367
421
  "objections": [],
368
422
  "key_questions": [],
369
- "response_strategy": "continue_conversation"
423
+ "response_strategy": "continue_conversation",
370
424
  }
371
-
425
+
372
426
  async def generate_follow_up(
373
- self,
374
- conversation_history: List[Dict[str, str]],
375
- analysis: Dict[str, Any]
427
+ self, conversation_history: List[Dict[str, str]], analysis: Dict[str, Any]
376
428
  ) -> str:
377
429
  """
378
430
  Генерирует персонализированное продолжение разговора
379
-
431
+
380
432
  Args:
381
433
  conversation_history: История разговора
382
434
  analysis: Результат анализа последнего сообщения
383
-
435
+
384
436
  Returns:
385
437
  Персонализированный ответ
386
438
  """
@@ -398,66 +450,64 @@ class OpenAIClient:
398
450
  3. Мягко направляет к следующему этапу воронки продаж
399
451
  4. Сохраняет доверительный тон общения
400
452
  """
401
-
453
+
402
454
  messages = conversation_history + [
403
455
  {"role": "system", "content": strategy_prompt}
404
456
  ]
405
-
457
+
406
458
  # Для творческих задач используем более высокую температуру если поддерживается
407
459
  temp = 0.8 if not self.is_gpt5 else None
408
-
460
+
409
461
  return await self.get_completion(messages, temperature=temp)
410
-
462
+
411
463
  async def check_api_health(self) -> bool:
412
464
  """Проверяет доступность OpenAI API"""
413
465
  try:
414
466
  params = self._get_completion_params(max_tokens=10)
415
467
  params["messages"] = [{"role": "user", "content": "Привет"}]
416
-
417
- response = await self.client.chat.completions.create(**params)
468
+
469
+ await self.client.chat.completions.create(**params)
418
470
  return True
419
471
  except Exception as e:
420
472
  logger.error(f"OpenAI API недоступен: {e}")
421
473
  return False
422
-
474
+
423
475
  def estimate_tokens(self, text: str) -> int:
424
476
  """Более точная оценка количества токенов в тексте"""
425
477
  # Для русского текста: примерно 2.5-3 символа на токен
426
478
  return int(len(text) / 2.5)
427
-
479
+
428
480
  async def get_available_models(self) -> List[str]:
429
481
  """Получает список доступных моделей"""
430
482
  try:
431
483
  models = await self.client.models.list()
432
- return [model.id for model in models.data if 'gpt' in model.id.lower()]
484
+ return [model.id for model in models.data if "gpt" in model.id.lower()]
433
485
  except Exception as e:
434
486
  logger.error(f"Ошибка при получении списка моделей: {e}")
435
487
  return []
436
-
488
+
437
489
  async def transcribe_audio(self, audio_file_path: str) -> str:
438
490
  """
439
491
  Распознает голосовое сообщение через Whisper API
440
-
492
+
441
493
  Args:
442
494
  audio_file_path: Путь к аудио файлу
443
-
495
+
444
496
  Returns:
445
497
  Распознанный текст
446
498
  """
447
499
  try:
448
500
  logger.info(f"🎤 Отправка аудио на распознавание: {audio_file_path}")
449
-
450
- with open(audio_file_path, 'rb') as audio_file:
501
+
502
+ with open(audio_file_path, "rb") as audio_file:
451
503
  transcript = await self.client.audio.transcriptions.create(
452
- model="whisper-1",
453
- file=audio_file,
454
- language="ru" # Русский язык
504
+ model="whisper-1", file=audio_file, language="ru" # Русский язык
455
505
  )
456
-
506
+
457
507
  text = transcript.text
458
508
  logger.info(f"✅ Распознано {len(text)} символов: '{text[:100]}...'")
459
509
  return text
460
-
510
+
461
511
  except Exception as e:
462
512
  logger.error(f"❌ Ошибка распознавания аудио: {e}")
463
- return ""
513
+ return ""