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,542 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from langchain_openai import ChatOpenAI
7
+ from langchain.messages import AIMessage, HumanMessage, SystemMessage
8
+ from langchain_core.output_parsers import StrOutputParser
9
+
10
+ from langchain.agents import create_agent
11
+
12
+ from openai import AsyncOpenAI
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class LangChainOpenAIClient:
18
+ """Клиент для работы с OpenAI API через LangChain v1.0"""
19
+
20
+ def __init__(
21
+ self,
22
+ api_key: str,
23
+ model: str = "gpt-5",
24
+ max_tokens: int = 1500,
25
+ temperature: float = 0.7,
26
+ ):
27
+ self.api_key = api_key
28
+ self.model = model
29
+ self.max_tokens = max_tokens
30
+ self.temperature = temperature
31
+
32
+ self.client = AsyncOpenAI(api_key=api_key)
33
+
34
+ # Список инструментов для ChatOpenAI
35
+ self._tools: List[Any] = []
36
+
37
+ # Инициализируем базовую LangChain модель
38
+ self._base_chat_model = ChatOpenAI(
39
+ model=model,
40
+ api_key=api_key,
41
+ max_tokens=max_tokens,
42
+ temperature=temperature,
43
+ reasoning_effort="minimal" if self._is_gpt5() else None,
44
+ max_retries=3
45
+ )
46
+
47
+ self.chat_model = create_agent(model=self._base_chat_model, tools=self._tools)
48
+
49
+ # Получаем лимиты для модели
50
+ self.model_limits = self._get_model_limits()
51
+
52
+ # Для диагностики пустых ответов
53
+ self.last_completion_tokens = 0
54
+
55
+ logger.info(
56
+ f"LangChain OpenAI клиент инициализирован с моделью {model} (GPT-5: {self._is_gpt5()}, лимит: {self.model_limits['total_context']} токенов)"
57
+ )
58
+
59
+ # Логируем информацию об инструментах
60
+ self._log_tools()
61
+
62
+ @property
63
+ def is_gpt5(self) -> bool:
64
+ """Определяет, является ли модель GPT-5"""
65
+ return self._is_gpt5()
66
+
67
+ def _is_gpt5(self) -> bool:
68
+ """Определяет, является ли модель GPT-5"""
69
+ return "gpt-5" in self.model.lower()
70
+
71
+ def _get_model_limits(self) -> Dict[str, int]:
72
+ """Возвращает лимиты для конкретной модели"""
73
+ model_limits = {
74
+ # GPT-3.5
75
+ "gpt-3.5-turbo": 4096,
76
+ "gpt-3.5-turbo-16k": 16385,
77
+ # GPT-4
78
+ "gpt-4": 8192,
79
+ "gpt-4-32k": 32768,
80
+ "gpt-4-turbo": 128000,
81
+ "gpt-4-turbo-preview": 128000,
82
+ "gpt-4o": 128000,
83
+ "gpt-4o-mini": 128000,
84
+ # GPT-5
85
+ "gpt-5-mini": 128000,
86
+ "gpt-5": 200000,
87
+ }
88
+
89
+ # Получаем лимит для текущей модели или используем консервативное значение
90
+ total_limit = model_limits.get(self.model, 8192)
91
+
92
+ # Резервируем место для ответа и буфера
93
+ completion_reserve = min(
94
+ self.max_tokens * 2, total_limit // 4
95
+ ) # Резервируем место для ответа
96
+ buffer_reserve = 500 # Буфер для безопасности
97
+
98
+ return {
99
+ "total_context": total_limit,
100
+ "max_input_tokens": total_limit - completion_reserve - buffer_reserve,
101
+ "completion_reserve": completion_reserve,
102
+ }
103
+
104
+ def _convert_messages_to_langchain(
105
+ self, messages: List[Dict[str, str]]
106
+ ) -> List[Any]:
107
+ """Конвертирует сообщения из формата dict в LangChain messages"""
108
+ langchain_messages = []
109
+ for msg in messages:
110
+ role = msg.get("role", "user")
111
+ content = msg.get("content", "")
112
+
113
+ if role == "system":
114
+ langchain_messages.append(SystemMessage(content=content))
115
+ elif role == "user":
116
+ langchain_messages.append(HumanMessage(content=content))
117
+ elif role == "assistant":
118
+ langchain_messages.append(AIMessage(content=content))
119
+ else:
120
+ # По умолчанию считаем user
121
+ langchain_messages.append(HumanMessage(content=content))
122
+
123
+ return langchain_messages
124
+
125
+ def _convert_langchain_to_dict(self, message: AIMessage) -> Dict[str, Any]:
126
+ """Конвертирует LangChain сообщение обратно в dict для логирования"""
127
+ return {
128
+ "content": message.content if hasattr(message, "content") else str(message),
129
+ "role": "assistant",
130
+ }
131
+
132
+ def _get_completion_params(
133
+ self, max_tokens: Optional[int] = None, temperature: Optional[float] = None
134
+ ) -> Dict[str, Any]:
135
+ """Получает параметры для запроса в зависимости от модели"""
136
+ params = {}
137
+
138
+ if self.is_gpt5:
139
+ # Для GPT-5 используем специальные параметры
140
+ if max_tokens:
141
+ params["max_completion_tokens"] = max_tokens
142
+ params["reasoning_effort"] = "minimal"
143
+ else:
144
+ # Для обычных моделей
145
+ if max_tokens:
146
+ params["max_tokens"] = max_tokens
147
+ elif self.max_tokens:
148
+ params["max_tokens"] = self.max_tokens
149
+
150
+ if temperature is not None:
151
+ params["temperature"] = temperature
152
+ elif self.temperature is not None:
153
+ params["temperature"] = self.temperature
154
+
155
+ return params
156
+
157
+ def _log_tools(self):
158
+ """Логирует информацию о всех подключенных инструментах"""
159
+ if not self._tools:
160
+ logger.info("📋 Подключенные инструменты ChatOpenAI: нет инструментов")
161
+ return
162
+
163
+ logger.info(f"📋 Подключенные инструменты ChatOpenAI ({len(self._tools)} шт.):")
164
+ for i, tool in enumerate(self._tools, 1):
165
+ tool_name = getattr(tool, "name", "Unknown")
166
+ tool_description = getattr(tool, "description", "Нет описания")
167
+ tool_type = type(tool).__name__
168
+
169
+ # Обрезаем описание если слишком длинное
170
+ if len(tool_description) > 100:
171
+ tool_description = tool_description[:97] + "..."
172
+
173
+ logger.info(f" {i}. {tool_name} ({tool_type})")
174
+ logger.info(f" Описание: {tool_description}")
175
+
176
+ def _update_agent(self):
177
+ """Обновляет агента с текущим списком инструментов"""
178
+ self.chat_model = create_agent(model=self._base_chat_model, tools=self._tools)
179
+ logger.info(f"🔄 Агент обновлен с {len(self._tools)} инструментами")
180
+
181
+ def add_tool(self, tool: Any, update_agent: bool = True, log_after: bool = True):
182
+ """
183
+ Добавляет инструмент для ChatOpenAI
184
+
185
+ Args:
186
+ tool: Инструмент LangChain (например, StructuredTool, FunctionTool и т.д.)
187
+ update_agent: Обновлять ли агента после добавления (по умолчанию True)
188
+ log_after: Логировать ли список инструментов после добавления (по умолчанию True)
189
+ """
190
+ if tool not in self._tools:
191
+ self._tools.append(tool)
192
+ tool_name = getattr(tool, "name", str(tool))
193
+ tool_description = getattr(tool, "description", "Нет описания")
194
+ tool_type = type(tool).__name__
195
+
196
+ logger.info(f"✅ Инструмент '{tool_name}' ({tool_type}) добавлен в ChatOpenAI")
197
+ if tool_description and tool_description != "Нет описания":
198
+ # Обрезаем описание если слишком длинное
199
+ desc = tool_description[:100] + "..." if len(tool_description) > 100 else tool_description
200
+ logger.info(f" Описание: {desc}")
201
+
202
+ # Обновляем агента с новым списком инструментов только если нужно
203
+ if update_agent:
204
+ self._update_agent()
205
+
206
+ # Логируем обновленный список инструментов только если нужно
207
+ if log_after:
208
+ self._log_tools()
209
+ else:
210
+ tool_name = getattr(tool, "name", str(tool))
211
+ logger.warning(f"⚠️ Инструмент '{tool_name}' уже зарегистрирован")
212
+
213
+ def add_tools(self, *tools: Any):
214
+ """
215
+ Добавляет несколько инструментов для ChatOpenAI
216
+
217
+ Args:
218
+ *tools: Произвольное количество инструментов LangChain или список(ы) инструментов
219
+
220
+ Examples:
221
+ # Отдельные инструменты
222
+ client.add_tools(tool1, tool2, tool3)
223
+
224
+ # Список инструментов
225
+ client.add_tools([tool1, tool2, tool3])
226
+
227
+ # Комбинация
228
+ client.add_tools([tool1, tool2], tool3)
229
+ """
230
+ if not tools:
231
+ logger.warning("⚠️ add_tools вызван без аргументов")
232
+ return
233
+
234
+ # Распаковываем списки инструментов
235
+ unpacked_tools = []
236
+ for tool in tools:
237
+ if isinstance(tool, (list, tuple)):
238
+ # Если это список или кортеж, добавляем все элементы
239
+ unpacked_tools.extend(tool)
240
+ else:
241
+ # Если это отдельный инструмент, добавляем его
242
+ unpacked_tools.append(tool)
243
+
244
+ if not unpacked_tools:
245
+ logger.warning("⚠️ Не найдено инструментов для добавления")
246
+ return
247
+
248
+ logger.info(f"🔧 Добавление {len(unpacked_tools)} инструментов в ChatOpenAI...")
249
+ # Добавляем инструменты без обновления агента и логирования после каждого
250
+ for tool in unpacked_tools:
251
+ self.add_tool(tool, update_agent=False, log_after=False)
252
+
253
+ # Обновляем агента один раз после добавления всех инструментов
254
+ self._update_agent()
255
+
256
+ # Логируем финальный список инструментов один раз
257
+ logger.info(f"✅ Все инструменты добавлены. Всего подключено: {len(self._tools)}")
258
+ self._log_tools()
259
+
260
+ def get_tools(self) -> List[Any]:
261
+ """Возвращает список зарегистрированных инструментов"""
262
+ return self._tools.copy()
263
+
264
+ def get_tools_description_for_prompt(self) -> str:
265
+ """
266
+ Возвращает описание всех инструментов в формате для добавления в промпт
267
+
268
+ Returns:
269
+ Текст с описанием всех доступных инструментов
270
+ """
271
+ if not self._tools:
272
+ return ""
273
+
274
+ descriptions = []
275
+ descriptions.append("\n### ДОСТУПНЫЕ ИНСТРУМЕНТЫ ###\n")
276
+ descriptions.append("У тебя есть доступ к следующим инструментам. Используй их когда это необходимо:\n")
277
+
278
+ for i, tool in enumerate(self._tools, 1):
279
+ tool_name = getattr(tool, "name", "Unknown")
280
+ tool_description = getattr(tool, "description", "Нет описания")
281
+
282
+ # Получаем параметры инструмента, если доступны
283
+ tool_args = ""
284
+ if hasattr(tool, "args_schema"):
285
+ try:
286
+ args_schema = tool.args_schema
287
+ if hasattr(args_schema, "schema"):
288
+ schema = args_schema.schema()
289
+ properties = schema.get("properties", {})
290
+ if properties:
291
+ args_list = []
292
+ for param_name, param_info in properties.items():
293
+ param_desc = param_info.get("description", "")
294
+ param_type = param_info.get("type", "string")
295
+ args_list.append(f" - {param_name} ({param_type}): {param_desc}")
296
+ if args_list:
297
+ tool_args = "\n" + "\n".join(args_list)
298
+ except Exception as e:
299
+ logger.debug(f"Не удалось получить схему аргументов для {tool_name}: {e}")
300
+
301
+ descriptions.append(f"{i}. **{tool_name}**")
302
+ descriptions.append(f" Описание: {tool_description}")
303
+ if tool_args:
304
+ descriptions.append(f" Параметры:{tool_args}")
305
+ descriptions.append("")
306
+
307
+ descriptions.append("ВАЖНО: Используй инструменты когда это необходимо для ответа на вопрос пользователя.")
308
+ descriptions.append("Например, если пользователь спрашивает о погоде - используй соответствующий инструмент.\n")
309
+
310
+ return "\n".join(descriptions)
311
+
312
+ async def get_completion(
313
+ self,
314
+ messages: List
315
+ ) -> str:
316
+ return await self.chat_model.ainvoke({"messages": messages})
317
+
318
+ async def _prepare_messages(
319
+ self, messages: List
320
+ ) -> List:
321
+ """
322
+ Подготавливает сообщения для отправки в API
323
+ Обрезает контекст если он слишком большой
324
+ """
325
+
326
+ # Более точная оценка токенов для русского текста
327
+ def estimate_message_tokens(msg):
328
+ content = msg.content if hasattr(msg, "content") else str(msg)
329
+ # Для русского текста: примерно 2.5-3 символа на токен
330
+ return len(content) // 2.5
331
+
332
+ total_estimated_tokens = sum(estimate_message_tokens(msg) for msg in messages)
333
+ max_input_tokens = self.model_limits["max_input_tokens"]
334
+
335
+ if total_estimated_tokens <= max_input_tokens:
336
+ return messages
337
+
338
+ logger.info(
339
+ f"Контекст слишком большой ({int(total_estimated_tokens)} токенов), обрезаем до {max_input_tokens}"
340
+ )
341
+
342
+ # Сохраняем системные сообщения
343
+ system_messages = [msg for msg in messages if isinstance(msg, SystemMessage)]
344
+ other_messages = [msg for msg in messages if not isinstance(msg, SystemMessage)]
345
+
346
+ # Рассчитываем токены системных сообщений
347
+ system_tokens = sum(estimate_message_tokens(msg.content) for msg in system_messages)
348
+ available_tokens = max_input_tokens - system_tokens
349
+
350
+ if available_tokens <= 0:
351
+ logger.warning("Системные сообщения занимают весь доступный контекст")
352
+ return system_messages
353
+
354
+ # Берем последние сообщения, помещающиеся в доступные токены
355
+ current_tokens = 0
356
+ trimmed_messages = []
357
+
358
+ for msg in reversed(other_messages):
359
+ msg_tokens = estimate_message_tokens(msg)
360
+ if current_tokens + msg_tokens > available_tokens:
361
+ break
362
+ trimmed_messages.insert(0, msg)
363
+ current_tokens += msg_tokens
364
+
365
+ result_messages = system_messages + trimmed_messages
366
+ logger.info(
367
+ f"Контекст обрезан до {len(result_messages)} сообщений (~{int(current_tokens + system_tokens)} токенов)"
368
+ )
369
+
370
+ return result_messages
371
+
372
+ async def analyze_sentiment(self, text: str) -> Dict[str, Any]:
373
+ """
374
+ Анализирует настроение и намерения сообщения
375
+
376
+ Args:
377
+ text: Текст для анализа
378
+
379
+ Returns:
380
+ Словарь с результатами анализа
381
+ """
382
+ analysis_prompt = f"""
383
+ Проанализируй следующее сообщение пользователя и определи:
384
+ 1. Настроение (позитивное/нейтральное/негативное)
385
+ 2. Уровень заинтересованности (1-10)
386
+ 3. Готовность к покупке (1-10)
387
+ 4. Основные возражения или вопросы
388
+ 5. Рекомендуемая стратегия ответа
389
+
390
+ Сообщение: "{text}"
391
+
392
+ Ответь в формате JSON:
393
+ {{
394
+ "sentiment": "positive/neutral/negative",
395
+ "interest_level": 1-10,
396
+ "purchase_readiness": 1-10,
397
+ "objections": ["список возражений"],
398
+ "key_questions": ["ключевые вопросы"],
399
+ "response_strategy": "рекомендуемая стратегия"
400
+ }}
401
+ """
402
+
403
+ try:
404
+ # Для анализа настроения используем более низкую температуру если поддерживается
405
+ temp = 0.3 if not self.is_gpt5 else None
406
+
407
+ model = ChatOpenAI(
408
+ model=self.model,
409
+ api_key=self.api_key,
410
+ max_tokens=self.max_tokens,
411
+ temperature=temp,
412
+ reasoning_effort="minimal" if self._is_gpt5() else None,
413
+ max_retries=3
414
+ ) | StrOutputParser()
415
+
416
+ response = await model.ainvoke(
417
+ [
418
+ SystemMessage(content="Ты эксперт по анализу намерений клиентов в продажах."),
419
+ HumanMessage(content=analysis_prompt),
420
+ ]
421
+ )
422
+
423
+ return json.loads(response)
424
+
425
+ except Exception as e:
426
+ logger.error(f"Ошибка при анализе настроения: {e}")
427
+ # Возвращаем дефолтные значения
428
+ return {
429
+ "sentiment": "neutral",
430
+ "interest_level": 5,
431
+ "purchase_readiness": 5,
432
+ "objections": [],
433
+ "key_questions": [],
434
+ "response_strategy": "continue_conversation",
435
+ }
436
+
437
+ async def generate_follow_up(
438
+ self, conversation_history: List, analysis: Dict[str, Any]
439
+ ) -> str:
440
+ """
441
+ Генерирует персонализированное продолжение разговора
442
+
443
+ Args:
444
+ conversation_history: История разговора
445
+ analysis: Результат анализа последнего сообщения
446
+
447
+ Returns:
448
+ Персонализированный ответ
449
+ """
450
+ strategy_prompt = f"""
451
+ На основе анализа сообщения клиента:
452
+ - Настроение: {analysis['sentiment']}
453
+ - Уровень заинтересованности: {analysis['interest_level']}/10
454
+ - Готовность к покупке: {analysis['purchase_readiness']}/10
455
+ - Возражения: {analysis['objections']}
456
+ - Стратегия: {analysis['response_strategy']}
457
+
458
+ Сгенерируй персонализированный ответ, который:
459
+ 1. Учитывает текущее настроение клиента
460
+ 2. Отвечает на его ключевые вопросы и возражения
461
+ 3. Мягко направляет к следующему этапу воронки продаж
462
+ 4. Сохраняет доверительный тон общения
463
+ """
464
+
465
+ temp = 0.8 if not self.is_gpt5 else None
466
+
467
+ model = ChatOpenAI(
468
+ model=self.model,
469
+ api_key=self.api_key,
470
+ max_tokens=self.max_tokens,
471
+ temperature=temp,
472
+ reasoning_effort="minimal" if self._is_gpt5() else None,
473
+ max_retries=3
474
+ ) | StrOutputParser()
475
+
476
+ return await model.ainvoke(conversation_history + [SystemMessage(strategy_prompt)])
477
+
478
+ async def check_api_health(self) -> bool:
479
+ """Проверяет доступность OpenAI API"""
480
+ try:
481
+ test_messages = [HumanMessage(content="Привет")]
482
+
483
+ model = ChatOpenAI(
484
+ model=self.model,
485
+ api_key=self.api_key,
486
+ max_tokens=10,
487
+ reasoning_effort="minimal" if self._is_gpt5() else None,
488
+ max_retries=3
489
+ ) | StrOutputParser()
490
+
491
+ await model.ainvoke(test_messages)
492
+ return True
493
+ except Exception as e:
494
+ logger.error(f"OpenAI API недоступен: {e}")
495
+ return False
496
+
497
+ def estimate_tokens(self, text: str) -> int:
498
+ """Более точная оценка количества токенов в тексте"""
499
+ # Для русского текста: примерно 2.5-3 символа на токен
500
+ return int(len(text) / 2.5)
501
+
502
+ async def get_available_models(self) -> List[str]:
503
+ """Получает список доступных моделей"""
504
+ # LangChain не предоставляет прямой доступ к списку моделей
505
+ # Возвращаем стандартный список моделей OpenAI
506
+ return [
507
+ "gpt-3.5-turbo",
508
+ "gpt-3.5-turbo-16k",
509
+ "gpt-4",
510
+ "gpt-4-32k",
511
+ "gpt-4-turbo",
512
+ "gpt-4o",
513
+ "gpt-4o-mini",
514
+ "gpt-5-mini",
515
+ "gpt-5",
516
+ ]
517
+
518
+ async def transcribe_audio(self, audio_file_path: str) -> str:
519
+ """
520
+ Распознает голосовое сообщение через Whisper API
521
+
522
+ Args:
523
+ audio_file_path: Путь к аудио файлу
524
+
525
+ Returns:
526
+ Распознанный текст
527
+ """
528
+ try:
529
+ logger.info(f"🎤 Отправка аудио на распознавание: {audio_file_path}")
530
+
531
+ with open(audio_file_path, "rb") as audio_file:
532
+ transcript = await self.client.audio.transcriptions.create(
533
+ model="whisper-1", file=audio_file, language="ru" # Русский язык
534
+ )
535
+
536
+ text = transcript.text
537
+ logger.info(f"✅ Распознано {len(text)} символов: '{text[:100]}...'")
538
+ return text
539
+
540
+ except Exception as e:
541
+ logger.error(f"❌ Ошибка распознавания аудио: {e}")
542
+ return ""