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,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 ""
|