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.

Files changed (61) hide show
  1. smart_bot_factory/__init__.py +51 -0
  2. smart_bot_factory/admin/__init__.py +16 -0
  3. smart_bot_factory/admin/admin_logic.py +430 -0
  4. smart_bot_factory/admin/admin_manager.py +141 -0
  5. smart_bot_factory/admin/admin_migration.sql +136 -0
  6. smart_bot_factory/admin/admin_tester.py +151 -0
  7. smart_bot_factory/admin/timeout_checker.py +499 -0
  8. smart_bot_factory/analytics/__init__.py +7 -0
  9. smart_bot_factory/analytics/analytics_manager.py +355 -0
  10. smart_bot_factory/cli.py +642 -0
  11. smart_bot_factory/config.py +235 -0
  12. smart_bot_factory/configs/growthmed-helper/env_example.txt +1 -0
  13. smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +9 -0
  14. smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +582 -0
  15. smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +66 -0
  16. smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +232 -0
  17. smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +28 -0
  18. smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +7 -0
  19. smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +16 -0
  20. smart_bot_factory/configs/growthmed-helper/welcome_file//342/225/250/320/267/342/225/250/342/225/241/342/225/250/342/225/221 /342/225/250/342/225/227/342/225/250/342/225/225/342/225/244/320/221/342/225/244/320/222 /342/225/250/342/224/220/342/225/250/342/225/233 152/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/225/225 323/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/224/244/342/225/250/342/225/227/342/225/244/320/237 /342/225/250/342/225/235/342/225/250/342/225/241/342/225/250/342/224/244/342/225/250/342/225/225/342/225/244/320/226/342/225/250/342/225/225/342/225/250/342/225/234/342/225/244/320/233.pdf +0 -0
  21. smart_bot_factory/configs/growthmed-october-24/prompts/1sales_context.txt +16 -0
  22. smart_bot_factory/configs/growthmed-october-24/prompts/2product_info.txt +582 -0
  23. smart_bot_factory/configs/growthmed-october-24/prompts/3objection_handling.txt +66 -0
  24. smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +212 -0
  25. smart_bot_factory/configs/growthmed-october-24/prompts/help_message.txt +28 -0
  26. smart_bot_factory/configs/growthmed-october-24/prompts/welcome_message.txt +8 -0
  27. smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064229.txt +818 -0
  28. smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064335.txt +32 -0
  29. smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064638.txt +35 -0
  30. smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +66 -0
  31. smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml +108 -0
  32. smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml +46 -0
  33. smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt +16 -0
  34. smart_bot_factory/configs/growthmed-october-24/welcome_file//342/225/250/320/267/342/225/250/342/225/241/342/225/250/342/225/221 /342/225/250/342/225/227/342/225/250/342/225/225/342/225/244/320/221/342/225/244/320/222 /342/225/250/342/224/220/342/225/250/342/225/233 152/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/225/225 323/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/224/244/342/225/250/342/225/227/342/225/244/320/237 /342/225/250/342/225/235/342/225/250/342/225/241/342/225/250/342/224/244/342/225/250/342/225/225/342/225/244/320/226/342/225/250/342/225/225/342/225/250/342/225/234/342/225/244/320/233.pdf +0 -0
  35. smart_bot_factory/core/__init__.py +22 -0
  36. smart_bot_factory/core/bot_utils.py +693 -0
  37. smart_bot_factory/core/conversation_manager.py +536 -0
  38. smart_bot_factory/core/decorators.py +229 -0
  39. smart_bot_factory/core/message_sender.py +249 -0
  40. smart_bot_factory/core/states.py +14 -0
  41. smart_bot_factory/creation/__init__.py +8 -0
  42. smart_bot_factory/creation/bot_builder.py +329 -0
  43. smart_bot_factory/creation/bot_testing.py +986 -0
  44. smart_bot_factory/database/database_structure.sql +57 -0
  45. smart_bot_factory/database/schema.sql +1094 -0
  46. smart_bot_factory/handlers/handlers.py +583 -0
  47. smart_bot_factory/integrations/__init__.py +9 -0
  48. smart_bot_factory/integrations/openai_client.py +435 -0
  49. smart_bot_factory/integrations/supabase_client.py +592 -0
  50. smart_bot_factory/setup_checker.py +476 -0
  51. smart_bot_factory/utils/__init__.py +9 -0
  52. smart_bot_factory/utils/debug_routing.py +103 -0
  53. smart_bot_factory/utils/prompt_loader.py +427 -0
  54. smart_bot_factory/uv.lock +2004 -0
  55. smart_bot_factory-0.1.3.dist-info/METADATA +126 -0
  56. smart_bot_factory-0.1.3.dist-info/RECORD +59 -0
  57. smart_bot_factory-0.1.3.dist-info/licenses/LICENSE +24 -0
  58. smart_bot_factory-0.1.2.dist-info/METADATA +0 -31
  59. smart_bot_factory-0.1.2.dist-info/RECORD +0 -4
  60. {smart_bot_factory-0.1.2.dist-info → smart_bot_factory-0.1.3.dist-info}/WHEEL +0 -0
  61. {smart_bot_factory-0.1.2.dist-info → smart_bot_factory-0.1.3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,435 @@
1
+ import logging
2
+ import asyncio
3
+ from typing import List, Dict, Any, Optional
4
+ from openai import AsyncOpenAI
5
+ from openai.types.chat import ChatCompletion
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class OpenAIClient:
10
+ """Клиент для работы с OpenAI API"""
11
+
12
+ def __init__(
13
+ self,
14
+ api_key: str,
15
+ model: str = "gpt-4",
16
+ max_tokens: int = 1500,
17
+ temperature: float = 0.7
18
+ ):
19
+ self.client = AsyncOpenAI(api_key=api_key)
20
+ self.model = model
21
+ self.max_tokens = max_tokens
22
+ self.temperature = temperature
23
+
24
+ # Настройки для повторных попыток
25
+ self.max_retries = 3
26
+ self.retry_delay = 1 # секунды
27
+
28
+ # Определяем, является ли это GPT-5 моделью
29
+ self.is_gpt5 = 'gpt-5' in model.lower()
30
+
31
+ # Получаем лимиты для модели
32
+ self.model_limits = self._get_model_limits()
33
+
34
+ # 🆕 Для диагностики пустых ответов
35
+ self.last_completion_tokens = 0
36
+
37
+ logger.info(f"OpenAI клиент инициализирован с моделью {model} (GPT-5: {self.is_gpt5}, лимит: {self.model_limits['total_context']} токенов)")
38
+
39
+ def _get_model_limits(self) -> Dict[str, int]:
40
+ """Возвращает лимиты для конкретной модели"""
41
+ model_limits = {
42
+ # GPT-3.5
43
+ 'gpt-3.5-turbo': 4096,
44
+ 'gpt-3.5-turbo-16k': 16385,
45
+
46
+ # 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
+
54
+ # GPT-5
55
+ 'gpt-5-mini': 128000,
56
+ 'gpt-5': 200000,
57
+ }
58
+
59
+ # Получаем лимит для текущей модели или используем консервативное значение
60
+ total_limit = model_limits.get(self.model, 8192)
61
+
62
+ # Резервируем место для ответа и буфера
63
+ completion_reserve = min(self.max_tokens * 2, total_limit // 4) # Резервируем место для ответа
64
+ buffer_reserve = 500 # Буфер для безопасности
65
+
66
+ return {
67
+ 'total_context': total_limit,
68
+ 'max_input_tokens': total_limit - completion_reserve - buffer_reserve,
69
+ 'completion_reserve': completion_reserve
70
+ }
71
+
72
+ def _get_completion_params(self, max_tokens: Optional[int] = None, temperature: Optional[float] = None) -> Dict[str, Any]:
73
+ """Возвращает параметры для создания completion в зависимости от модели"""
74
+ tokens = max_tokens or self.max_tokens
75
+ temp = temperature or self.temperature
76
+
77
+ params = {
78
+ "model": self.model,
79
+ }
80
+
81
+ # 🆕 ИСПРАВЛЕННАЯ ЛОГИКА ДЛЯ GPT-5
82
+ if self.is_gpt5:
83
+ # 🔧 ИСПОЛЬЗУЕМ ТОКЕНЫ ИЗ КОНФИГА (как настроил пользователь)
84
+ params["max_completion_tokens"] = tokens
85
+
86
+ # 🔧 БЫСТРЫЕ ОТВЕТЫ с минимальным reasoning
87
+ # Допустимые значения reasoning_effort:
88
+ # "minimal" - самые быстрые ответы, минимальное reasoning
89
+ # "low" - немного больше reasoning
90
+ # "medium" - сбалансированное reasoning (медленнее)
91
+ # "high" - максимальное reasoning (самые медленные, но качественные)
92
+ params["reasoning_effort"] = "minimal" # Быстрые ответы
93
+
94
+ logger.debug(f"GPT-5 параметры: max_completion_tokens={tokens}, reasoning_effort=minimal")
95
+ else:
96
+ # Для остальных моделей стандартные параметры
97
+ params["max_tokens"] = tokens
98
+ params["temperature"] = temp
99
+
100
+ return params
101
+
102
+ async def get_completion(
103
+ self,
104
+ messages: List[Dict[str, str]],
105
+ max_tokens: Optional[int] = None,
106
+ temperature: Optional[float] = None,
107
+ stream: bool = False
108
+ ) -> str:
109
+ """Получает ответ от OpenAI API с поддержкой GPT-5"""
110
+ if not messages:
111
+ raise ValueError("Список сообщений не может быть пустым")
112
+
113
+ for attempt in range(self.max_retries):
114
+ try:
115
+ logger.debug(f"Отправка запроса к OpenAI (попытка {attempt + 1})")
116
+
117
+ # Обрезаем сообщения если контекст слишком большой
118
+ processed_messages = await self._prepare_messages(messages)
119
+
120
+ # Получаем параметры в зависимости от модели
121
+ params = self._get_completion_params(max_tokens, temperature)
122
+ params["messages"] = processed_messages
123
+ params["stream"] = stream
124
+
125
+ logger.info(f"🚀 Отправляем запрос к {self.model}")
126
+ logger.info(f"📝 Сообщений в контексте: {len(processed_messages)}")
127
+
128
+ # Логируем параметры в зависимости от модели
129
+ 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}")
133
+ 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
+
140
+ if stream:
141
+ return await self._handle_stream_response(response)
142
+ else:
143
+ # 🔧 ПРАВИЛЬНЫЙ ПАРСИНГ ДЛЯ GPT-5 (может быть несколько choices)
144
+ if self.is_gpt5 and len(response.choices) > 1:
145
+ logger.info(f"📊 GPT-5 вернул {len(response.choices)} choices, ищем пользовательский контент")
146
+
147
+ # Ищем choice с пользовательским контентом (не reasoning summary)
148
+ content = None
149
+ finish_reason = None
150
+
151
+ for i, choice in enumerate(response.choices):
152
+ choice_content = choice.message.content
153
+ 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
+
157
+ # Берем первый choice с реальным контентом
158
+ if choice_content and choice_content.strip():
159
+ content = choice_content
160
+ finish_reason = choice_finish
161
+ logger.info(f" ✅ Используем choice {i} как пользовательский контент")
162
+ break
163
+
164
+ # Если не нашли контент ни в одном choice
165
+ if not content:
166
+ logger.warning(f" ❌ Не найден пользовательский контент ни в одном choice")
167
+ content = ""
168
+ finish_reason = response.choices[0].finish_reason
169
+ else:
170
+ # Стандартная обработка для не-GPT-5 или одного choice
171
+ finish_reason = response.choices[0].finish_reason
172
+ content = response.choices[0].message.content
173
+
174
+ logger.info(f"📊 OpenAI ответ получен:")
175
+ logger.info(f" 🏁 Finish reason: {finish_reason}")
176
+ logger.info(f" 🔤 Тип content: {type(content)}")
177
+ logger.info(f" 📏 Длина: {len(content) if content else 0}")
178
+
179
+ # Логируем использование токенов
180
+ if response.usage:
181
+ self.last_completion_tokens = response.usage.completion_tokens
182
+
183
+ logger.info(f"💰 Использование токенов:")
184
+ logger.info(f" 📥 Prompt: {response.usage.prompt_tokens}")
185
+ logger.info(f" 📤 Completion: {response.usage.completion_tokens}")
186
+ logger.info(f" 🔢 Total: {response.usage.total_tokens}")
187
+
188
+ # 🆕 ЛОГИРУЕМ REASONING TOKENS ДЛЯ GPT-5
189
+ if hasattr(response.usage, 'reasoning_tokens'):
190
+ reasoning_tokens = getattr(response.usage, 'reasoning_tokens', 0)
191
+ if reasoning_tokens > 0:
192
+ logger.info(f" 💭 Reasoning: {reasoning_tokens}")
193
+
194
+ # 🔧 ИСПРАВЛЕННАЯ ОБРАБОТКА GPT-5
195
+ if self.is_gpt5:
196
+ # Проверяем, есть ли reasoning токены
197
+ reasoning_tokens = 0
198
+ if response.usage and hasattr(response.usage, 'reasoning_tokens'):
199
+ reasoning_tokens = getattr(response.usage, 'reasoning_tokens', 0)
200
+
201
+ # Если есть 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
+
205
+ # Если это не последняя попытка, пробуем с большим количеством токенов
206
+ if attempt < self.max_retries - 1:
207
+ current_tokens = max_tokens or self.max_tokens
208
+ increased_tokens = min(current_tokens * 2, 8000) # Удваиваем, максимум 8000
209
+
210
+ logger.info(f"🔄 Пробуем с увеличенными токенами: {increased_tokens}")
211
+ return await self.get_completion(
212
+ messages,
213
+ max_tokens=increased_tokens,
214
+ temperature=temperature,
215
+ stream=stream
216
+ )
217
+ else:
218
+ # Последняя попытка - возвращаем информативное сообщение
219
+ return "Извините, модель потратила время на анализ, но не смогла сформулировать ответ. Попробуйте переформулировать вопрос."
220
+
221
+ # Обычная обработка finish_reason
222
+ if finish_reason == 'length':
223
+ logger.warning(f"⚠️ Модель достигла лимита токенов")
224
+
225
+ if content and content.strip():
226
+ # Есть частичный контент - возвращаем его
227
+ return content
228
+ else:
229
+ # Нет контента - информируем пользователя
230
+ return "Ответ слишком длинный. Попробуйте задать более конкретный вопрос."
231
+
232
+ elif finish_reason == 'stop':
233
+ logger.info(f"✅ Нормальное завершение")
234
+
235
+ elif finish_reason == 'content_filter':
236
+ logger.warning(f"🚫 Ответ заблокирован фильтром контента")
237
+ return "Извините, не могу ответить на этот вопрос."
238
+
239
+ # Детальное логирование содержимого
240
+ if content:
241
+ logger.info(f" 🔍 Начало (50 символов): '{content[:50]}'")
242
+ logger.info(f" 🔍 Конец (50 символов): '{content[-50:]}'")
243
+ else:
244
+ logger.warning(f" ❌ content is None или пустой")
245
+
246
+ return content or ""
247
+
248
+ except Exception as e:
249
+ logger.warning(f"Ошибка при обращении к OpenAI (попытка {attempt + 1}): {e}")
250
+
251
+ if attempt == self.max_retries - 1:
252
+ logger.error(f"Все попытки исчерпаны. Последняя ошибка: {e}")
253
+ raise
254
+
255
+ await asyncio.sleep(self.retry_delay * (attempt + 1))
256
+
257
+ raise Exception("Не удалось получить ответ от OpenAI")
258
+
259
+ async def _prepare_messages(self, messages: List[Dict[str, str]]) -> List[Dict[str, str]]:
260
+ """
261
+ Подготавливает сообщения для отправки в API
262
+ Обрезает контекст если он слишком большой
263
+ """
264
+ # Более точная оценка токенов для русского текста
265
+ def estimate_message_tokens(msg):
266
+ content = msg.get('content', '')
267
+ # Для русского текста: примерно 2.5-3 символа на токен
268
+ return len(content) // 2.5
269
+
270
+ total_estimated_tokens = sum(estimate_message_tokens(msg) for msg in messages)
271
+ max_input_tokens = self.model_limits['max_input_tokens']
272
+
273
+ if total_estimated_tokens <= max_input_tokens:
274
+ return messages
275
+
276
+ logger.info(f"Контекст слишком большой ({int(total_estimated_tokens)} токенов), обрезаем до {max_input_tokens}")
277
+
278
+ # Сохраняем системные сообщения
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
+
282
+ # Рассчитываем токены системных сообщений
283
+ system_tokens = sum(estimate_message_tokens(msg) for msg in system_messages)
284
+ available_tokens = max_input_tokens - system_tokens
285
+
286
+ if available_tokens <= 0:
287
+ logger.warning("Системные сообщения занимают весь доступный контекст")
288
+ return system_messages
289
+
290
+ # Берем последние сообщения, помещающиеся в доступные токены
291
+ current_tokens = 0
292
+ trimmed_messages = []
293
+
294
+ for msg in reversed(other_messages):
295
+ msg_tokens = estimate_message_tokens(msg)
296
+ if current_tokens + msg_tokens > available_tokens:
297
+ break
298
+ trimmed_messages.insert(0, msg)
299
+ current_tokens += msg_tokens
300
+
301
+ result_messages = system_messages + trimmed_messages
302
+ logger.info(f"Контекст обрезан до {len(result_messages)} сообщений (~{int(current_tokens + system_tokens)} токенов)")
303
+
304
+ return result_messages
305
+
306
+ async def _handle_stream_response(self, response) -> str:
307
+ """Обрабатывает потоковый ответ"""
308
+ full_content = ""
309
+
310
+ async for chunk in response:
311
+ if chunk.choices[0].delta.content is not None:
312
+ full_content += chunk.choices[0].delta.content
313
+
314
+ return full_content
315
+
316
+ async def analyze_sentiment(self, text: str) -> Dict[str, Any]:
317
+ """
318
+ Анализирует настроение и намерения сообщения
319
+
320
+ Args:
321
+ text: Текст для анализа
322
+
323
+ Returns:
324
+ Словарь с результатами анализа
325
+ """
326
+ analysis_prompt = f"""
327
+ Проанализируй следующее сообщение пользователя и определи:
328
+ 1. Настроение (позитивное/нейтральное/негативное)
329
+ 2. Уровень заинтересованности (1-10)
330
+ 3. Готовность к покупке (1-10)
331
+ 4. Основные возражения или вопросы
332
+ 5. Рекомендуемая стратегия ответа
333
+
334
+ Сообщение: "{text}"
335
+
336
+ Ответь в формате JSON:
337
+ {{
338
+ "sentiment": "positive/neutral/negative",
339
+ "interest_level": 1-10,
340
+ "purchase_readiness": 1-10,
341
+ "objections": ["список возражений"],
342
+ "key_questions": ["ключевые вопросы"],
343
+ "response_strategy": "рекомендуемая стратегия"
344
+ }}
345
+ """
346
+
347
+ try:
348
+ # Для анализа настроения используем более низкую температуру если поддерживается
349
+ 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
+
356
+ # Пытаемся распарсить JSON
357
+ import json
358
+ return json.loads(response)
359
+
360
+ except Exception as e:
361
+ logger.error(f"Ошибка при анализе настроения: {e}")
362
+ # Возвращаем дефолтные значения
363
+ return {
364
+ "sentiment": "neutral",
365
+ "interest_level": 5,
366
+ "purchase_readiness": 5,
367
+ "objections": [],
368
+ "key_questions": [],
369
+ "response_strategy": "continue_conversation"
370
+ }
371
+
372
+ async def generate_follow_up(
373
+ self,
374
+ conversation_history: List[Dict[str, str]],
375
+ analysis: Dict[str, Any]
376
+ ) -> str:
377
+ """
378
+ Генерирует персонализированное продолжение разговора
379
+
380
+ Args:
381
+ conversation_history: История разговора
382
+ analysis: Результат анализа последнего сообщения
383
+
384
+ Returns:
385
+ Персонализированный ответ
386
+ """
387
+ strategy_prompt = f"""
388
+ На основе анализа сообщения клиента:
389
+ - Настроение: {analysis['sentiment']}
390
+ - Уровень заинтересованности: {analysis['interest_level']}/10
391
+ - Готовность к покупке: {analysis['purchase_readiness']}/10
392
+ - Возражения: {analysis['objections']}
393
+ - Стратегия: {analysis['response_strategy']}
394
+
395
+ Сгенерируй персонализированный ответ, который:
396
+ 1. Учитывает текущее настроение клиента
397
+ 2. Отвечает на его ключевые вопросы и возражения
398
+ 3. Мягко направляет к следующему этапу воронки продаж
399
+ 4. Сохраняет доверительный тон общения
400
+ """
401
+
402
+ messages = conversation_history + [
403
+ {"role": "system", "content": strategy_prompt}
404
+ ]
405
+
406
+ # Для творческих задач используем более высокую температуру если поддерживается
407
+ temp = 0.8 if not self.is_gpt5 else None
408
+
409
+ return await self.get_completion(messages, temperature=temp)
410
+
411
+ async def check_api_health(self) -> bool:
412
+ """Проверяет доступность OpenAI API"""
413
+ try:
414
+ params = self._get_completion_params(max_tokens=10)
415
+ params["messages"] = [{"role": "user", "content": "Привет"}]
416
+
417
+ response = await self.client.chat.completions.create(**params)
418
+ return True
419
+ except Exception as e:
420
+ logger.error(f"OpenAI API недоступен: {e}")
421
+ return False
422
+
423
+ def estimate_tokens(self, text: str) -> int:
424
+ """Более точная оценка количества токенов в тексте"""
425
+ # Для русского текста: примерно 2.5-3 символа на токен
426
+ return int(len(text) / 2.5)
427
+
428
+ async def get_available_models(self) -> List[str]:
429
+ """Получает список доступных моделей"""
430
+ try:
431
+ models = await self.client.models.list()
432
+ return [model.id for model in models.data if 'gpt' in model.id.lower()]
433
+ except Exception as e:
434
+ logger.error(f"Ошибка при получении списка моделей: {e}")
435
+ return []