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