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.
- smart_bot_factory/__init__.py +3 -0
- smart_bot_factory/admin/__init__.py +18 -0
- smart_bot_factory/admin/admin_events.py +1223 -0
- smart_bot_factory/admin/admin_logic.py +553 -0
- smart_bot_factory/admin/admin_manager.py +156 -0
- smart_bot_factory/admin/admin_tester.py +157 -0
- smart_bot_factory/admin/timeout_checker.py +547 -0
- smart_bot_factory/aiogram_calendar/__init__.py +14 -0
- smart_bot_factory/aiogram_calendar/common.py +64 -0
- smart_bot_factory/aiogram_calendar/dialog_calendar.py +259 -0
- smart_bot_factory/aiogram_calendar/schemas.py +99 -0
- smart_bot_factory/aiogram_calendar/simple_calendar.py +224 -0
- smart_bot_factory/analytics/analytics_manager.py +414 -0
- smart_bot_factory/cli.py +806 -0
- smart_bot_factory/config.py +258 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/1sales_context.txt +16 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/2product_info.txt +582 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/3objection_handling.txt +66 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +212 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/help_message.txt +28 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/welcome_message.txt +8 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064229.txt +818 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064335.txt +32 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064638.txt +35 -0
- smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +133 -0
- smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml +108 -0
- smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml +46 -0
- smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt +16 -0
- smart_bot_factory/configs/growthmed-october-24/welcome_file//342/225/250/320/267/342/225/250/342/225/241/342/225/250/342/225/221 /342/225/250/342/225/227/342/225/250/342/225/225/342/225/244/320/221/342/225/244/320/222 /342/225/250/342/224/220/342/225/250/342/225/233 152/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/225/225 323/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/224/244/342/225/250/342/225/227/342/225/244/320/237 /342/225/250/342/225/235/342/225/250/342/225/241/342/225/250/342/224/244/342/225/250/342/225/225/342/225/244/320/226/342/225/250/342/225/225/342/225/250/342/225/234/342/225/244/320/233.pdf +0 -0
- smart_bot_factory/core/bot_utils.py +1108 -0
- smart_bot_factory/core/conversation_manager.py +653 -0
- smart_bot_factory/core/decorators.py +2464 -0
- smart_bot_factory/core/message_sender.py +729 -0
- smart_bot_factory/core/router.py +347 -0
- smart_bot_factory/core/router_manager.py +218 -0
- smart_bot_factory/core/states.py +27 -0
- smart_bot_factory/creation/__init__.py +7 -0
- smart_bot_factory/creation/bot_builder.py +1093 -0
- smart_bot_factory/creation/bot_testing.py +1122 -0
- smart_bot_factory/dashboard/__init__.py +3 -0
- smart_bot_factory/event/__init__.py +7 -0
- smart_bot_factory/handlers/handlers.py +2013 -0
- smart_bot_factory/integrations/langchain_openai.py +542 -0
- smart_bot_factory/integrations/openai_client.py +513 -0
- smart_bot_factory/integrations/supabase_client.py +1678 -0
- smart_bot_factory/memory/__init__.py +8 -0
- smart_bot_factory/memory/memory_manager.py +299 -0
- smart_bot_factory/memory/static_memory.py +214 -0
- smart_bot_factory/message/__init__.py +56 -0
- smart_bot_factory/rag/__init__.py +5 -0
- smart_bot_factory/rag/decorators.py +29 -0
- smart_bot_factory/rag/router.py +54 -0
- smart_bot_factory/rag/templates/__init__.py +3 -0
- smart_bot_factory/rag/templates/create_table.sql +7 -0
- smart_bot_factory/rag/templates/create_table_and_function_template.py +94 -0
- smart_bot_factory/rag/templates/match_function.sql +61 -0
- smart_bot_factory/rag/templates/match_services_template.py +82 -0
- smart_bot_factory/rag/vectorstore.py +449 -0
- smart_bot_factory/router/__init__.py +10 -0
- smart_bot_factory/setup_checker.py +512 -0
- smart_bot_factory/supabase/__init__.py +7 -0
- smart_bot_factory/supabase/client.py +631 -0
- smart_bot_factory/utils/__init__.py +11 -0
- smart_bot_factory/utils/debug_routing.py +114 -0
- smart_bot_factory/utils/prompt_loader.py +529 -0
- smart_bot_factory/utils/tool_router.py +68 -0
- smart_bot_factory/utils/user_prompt_loader.py +55 -0
- smart_bot_factory/utm_link_generator.py +123 -0
- smart_bot_factory-1.1.1.dist-info/METADATA +1135 -0
- smart_bot_factory-1.1.1.dist-info/RECORD +73 -0
- smart_bot_factory-1.1.1.dist-info/WHEEL +4 -0
- smart_bot_factory-1.1.1.dist-info/entry_points.txt +2 -0
- 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 ""
|