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.
- smart_bot_factory/__init__.py +51 -0
- smart_bot_factory/admin/__init__.py +16 -0
- smart_bot_factory/admin/admin_logic.py +430 -0
- smart_bot_factory/admin/admin_manager.py +141 -0
- smart_bot_factory/admin/admin_migration.sql +136 -0
- smart_bot_factory/admin/admin_tester.py +151 -0
- smart_bot_factory/admin/timeout_checker.py +499 -0
- smart_bot_factory/analytics/__init__.py +7 -0
- smart_bot_factory/analytics/analytics_manager.py +355 -0
- smart_bot_factory/cli.py +642 -0
- smart_bot_factory/config.py +235 -0
- smart_bot_factory/configs/growthmed-helper/env_example.txt +1 -0
- smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +9 -0
- smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +582 -0
- smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +66 -0
- smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +232 -0
- smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +28 -0
- smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +7 -0
- smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +16 -0
- 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
- 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 +66 -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/__init__.py +22 -0
- smart_bot_factory/core/bot_utils.py +693 -0
- smart_bot_factory/core/conversation_manager.py +536 -0
- smart_bot_factory/core/decorators.py +229 -0
- smart_bot_factory/core/message_sender.py +249 -0
- smart_bot_factory/core/states.py +14 -0
- smart_bot_factory/creation/__init__.py +8 -0
- smart_bot_factory/creation/bot_builder.py +329 -0
- smart_bot_factory/creation/bot_testing.py +986 -0
- smart_bot_factory/database/database_structure.sql +57 -0
- smart_bot_factory/database/schema.sql +1094 -0
- smart_bot_factory/handlers/handlers.py +583 -0
- smart_bot_factory/integrations/__init__.py +9 -0
- smart_bot_factory/integrations/openai_client.py +435 -0
- smart_bot_factory/integrations/supabase_client.py +592 -0
- smart_bot_factory/setup_checker.py +476 -0
- smart_bot_factory/utils/__init__.py +9 -0
- smart_bot_factory/utils/debug_routing.py +103 -0
- smart_bot_factory/utils/prompt_loader.py +427 -0
- smart_bot_factory/uv.lock +2004 -0
- smart_bot_factory-0.1.3.dist-info/METADATA +126 -0
- smart_bot_factory-0.1.3.dist-info/RECORD +59 -0
- smart_bot_factory-0.1.3.dist-info/licenses/LICENSE +24 -0
- smart_bot_factory-0.1.2.dist-info/METADATA +0 -31
- smart_bot_factory-0.1.2.dist-info/RECORD +0 -4
- {smart_bot_factory-0.1.2.dist-info → smart_bot_factory-0.1.3.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.1.2.dist-info → smart_bot_factory-0.1.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Декораторы для обработчиков событий и временных задач
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Callable, Any, Dict, Optional
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from functools import wraps
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Глобальный реестр обработчиков событий
|
|
14
|
+
_event_handlers: Dict[str, Callable] = {}
|
|
15
|
+
_scheduled_tasks: Dict[str, Dict[str, Any]] = {}
|
|
16
|
+
|
|
17
|
+
def event_handler(event_type: str, description: str = ""):
|
|
18
|
+
"""
|
|
19
|
+
Декоратор для регистрации обработчика события
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
event_type: Тип события (например, 'appointment_booking', 'phone_collection')
|
|
23
|
+
description: Описание что делает обработчик (для добавления в промпт)
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
@event_handler("appointment_booking", "Записывает пользователя на прием")
|
|
27
|
+
async def book_appointment(user_id: int, appointment_data: dict):
|
|
28
|
+
# Логика записи на прием
|
|
29
|
+
return {"status": "success", "appointment_id": "123"}
|
|
30
|
+
"""
|
|
31
|
+
def decorator(func: Callable) -> Callable:
|
|
32
|
+
_event_handlers[event_type] = {
|
|
33
|
+
'handler': func,
|
|
34
|
+
'description': description,
|
|
35
|
+
'name': func.__name__
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
logger.info(f"📝 Зарегистрирован обработчик события '{event_type}': {func.__name__}")
|
|
39
|
+
|
|
40
|
+
@wraps(func)
|
|
41
|
+
async def wrapper(*args, **kwargs):
|
|
42
|
+
try:
|
|
43
|
+
logger.info(f"🔧 Выполняем обработчик события '{event_type}'")
|
|
44
|
+
result = await func(*args, **kwargs)
|
|
45
|
+
logger.info(f"✅ Обработчик '{event_type}' выполнен успешно")
|
|
46
|
+
return result
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"❌ Ошибка в обработчике '{event_type}': {e}")
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
return wrapper
|
|
52
|
+
return decorator
|
|
53
|
+
|
|
54
|
+
def schedule_task(task_name: str, description: str = ""):
|
|
55
|
+
"""
|
|
56
|
+
Декоратор для регистрации задачи, которую можно запланировать на время
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
task_name: Название задачи (например, 'send_reminder', 'follow_up')
|
|
60
|
+
description: Описание задачи (для добавления в промпт)
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
@schedule_task("send_reminder", "Отправляет напоминание пользователю")
|
|
64
|
+
async def send_reminder(user_id: int, message: str):
|
|
65
|
+
# Логика отправки напоминания
|
|
66
|
+
return {"status": "sent"}
|
|
67
|
+
"""
|
|
68
|
+
def decorator(func: Callable) -> Callable:
|
|
69
|
+
_scheduled_tasks[task_name] = {
|
|
70
|
+
'handler': func,
|
|
71
|
+
'description': description,
|
|
72
|
+
'name': func.__name__
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logger.info(f"⏰ Зарегистрирована задача '{task_name}': {func.__name__}")
|
|
76
|
+
|
|
77
|
+
@wraps(func)
|
|
78
|
+
async def wrapper(*args, **kwargs):
|
|
79
|
+
try:
|
|
80
|
+
logger.info(f"⏰ Выполняем запланированную задачу '{task_name}'")
|
|
81
|
+
result = await func(*args, **kwargs)
|
|
82
|
+
logger.info(f"✅ Задача '{task_name}' выполнена успешно")
|
|
83
|
+
return result
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(f"❌ Ошибка в задаче '{task_name}': {e}")
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
return wrapper
|
|
89
|
+
return decorator
|
|
90
|
+
|
|
91
|
+
def get_event_handlers() -> Dict[str, Dict[str, Any]]:
|
|
92
|
+
"""Возвращает все зарегистрированные обработчики событий"""
|
|
93
|
+
return _event_handlers.copy()
|
|
94
|
+
|
|
95
|
+
def get_scheduled_tasks() -> Dict[str, Dict[str, Any]]:
|
|
96
|
+
"""Возвращает все зарегистрированные задачи"""
|
|
97
|
+
return _scheduled_tasks.copy()
|
|
98
|
+
|
|
99
|
+
def get_handlers_for_prompt() -> str:
|
|
100
|
+
"""
|
|
101
|
+
Возвращает описание всех обработчиков для добавления в промпт
|
|
102
|
+
"""
|
|
103
|
+
if not _event_handlers and not _scheduled_tasks:
|
|
104
|
+
return ""
|
|
105
|
+
|
|
106
|
+
prompt_parts = []
|
|
107
|
+
|
|
108
|
+
if _event_handlers:
|
|
109
|
+
prompt_parts.append("ДОСТУПНЫЕ ОБРАБОТЧИКИ СОБЫТИЙ:")
|
|
110
|
+
for event_type, handler_info in _event_handlers.items():
|
|
111
|
+
prompt_parts.append(f"- {event_type}: {handler_info['description']}")
|
|
112
|
+
|
|
113
|
+
if _scheduled_tasks:
|
|
114
|
+
prompt_parts.append("\nДОСТУПНЫЕ ЗАДАЧИ ДЛЯ ПЛАНИРОВАНИЯ:")
|
|
115
|
+
for task_name, task_info in _scheduled_tasks.items():
|
|
116
|
+
prompt_parts.append(f"- {task_name}: {task_info['description']}")
|
|
117
|
+
|
|
118
|
+
return "\n".join(prompt_parts)
|
|
119
|
+
|
|
120
|
+
async def execute_event_handler(event_type: str, *args, **kwargs) -> Any:
|
|
121
|
+
"""Выполняет обработчик события по типу"""
|
|
122
|
+
if event_type not in _event_handlers:
|
|
123
|
+
raise ValueError(f"Обработчик события '{event_type}' не найден")
|
|
124
|
+
|
|
125
|
+
handler_info = _event_handlers[event_type]
|
|
126
|
+
return await handler_info['handler'](*args, **kwargs)
|
|
127
|
+
|
|
128
|
+
async def execute_scheduled_task(task_name: str, *args, **kwargs) -> Any:
|
|
129
|
+
"""Выполняет запланированную задачу по имени"""
|
|
130
|
+
if task_name not in _scheduled_tasks:
|
|
131
|
+
raise ValueError(f"Задача '{task_name}' не найдена")
|
|
132
|
+
|
|
133
|
+
task_info = _scheduled_tasks[task_name]
|
|
134
|
+
return await task_info['handler'](*args, **kwargs)
|
|
135
|
+
|
|
136
|
+
async def schedule_task_for_later(task_name: str, delay_seconds: int, *args, **kwargs):
|
|
137
|
+
"""
|
|
138
|
+
Планирует выполнение задачи через указанное время
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
task_name: Название задачи
|
|
142
|
+
delay_seconds: Задержка в секундах
|
|
143
|
+
*args, **kwargs: Аргументы для задачи
|
|
144
|
+
"""
|
|
145
|
+
if task_name not in _scheduled_tasks:
|
|
146
|
+
raise ValueError(f"Задача '{task_name}' не найдена")
|
|
147
|
+
|
|
148
|
+
logger.info(f"⏰ Планируем задачу '{task_name}' через {delay_seconds} секунд")
|
|
149
|
+
|
|
150
|
+
async def delayed_task():
|
|
151
|
+
await asyncio.sleep(delay_seconds)
|
|
152
|
+
await execute_scheduled_task(task_name, *args, **kwargs)
|
|
153
|
+
|
|
154
|
+
# Запускаем задачу в фоне
|
|
155
|
+
asyncio.create_task(delayed_task())
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"status": "scheduled",
|
|
159
|
+
"task_name": task_name,
|
|
160
|
+
"delay_seconds": delay_seconds,
|
|
161
|
+
"scheduled_at": datetime.now().isoformat()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_info: str):
|
|
165
|
+
"""
|
|
166
|
+
Выполняет запланированную задачу на основе события от ИИ
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
user_id: ID пользователя
|
|
170
|
+
task_name: Название задачи
|
|
171
|
+
event_info: Информация от ИИ (содержит время и сообщение)
|
|
172
|
+
"""
|
|
173
|
+
if task_name not in _scheduled_tasks:
|
|
174
|
+
raise ValueError(f"Задача '{task_name}' не найдена")
|
|
175
|
+
|
|
176
|
+
# Парсим event_info для извлечения времени и сообщения
|
|
177
|
+
# Формат: "через 2 часа: напомнить о приеме"
|
|
178
|
+
try:
|
|
179
|
+
if ":" in event_info:
|
|
180
|
+
time_part, message = event_info.split(":", 1)
|
|
181
|
+
time_part = time_part.strip()
|
|
182
|
+
message = message.strip()
|
|
183
|
+
else:
|
|
184
|
+
time_part = event_info
|
|
185
|
+
message = "Напоминание"
|
|
186
|
+
|
|
187
|
+
# Парсим время
|
|
188
|
+
delay_seconds = _parse_time_to_seconds(time_part)
|
|
189
|
+
|
|
190
|
+
# Планируем задачу
|
|
191
|
+
result = await schedule_task_for_later(task_name, delay_seconds, user_id, message)
|
|
192
|
+
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error(f"Ошибка парсинга времени из event_info '{event_info}': {e}")
|
|
197
|
+
# Fallback - планируем через 1 час
|
|
198
|
+
return await schedule_task_for_later(task_name, 3600, user_id, event_info)
|
|
199
|
+
|
|
200
|
+
def _parse_time_to_seconds(time_str: str) -> int:
|
|
201
|
+
"""
|
|
202
|
+
Парсит строку времени в секунды
|
|
203
|
+
Поддерживает форматы:
|
|
204
|
+
- "через 2 часа"
|
|
205
|
+
- "через 30 минут"
|
|
206
|
+
- "через 1 день"
|
|
207
|
+
- "через 2 часа 30 минут"
|
|
208
|
+
"""
|
|
209
|
+
import re
|
|
210
|
+
|
|
211
|
+
time_str = time_str.lower().strip()
|
|
212
|
+
|
|
213
|
+
# Ищем часы
|
|
214
|
+
hours_match = re.search(r'(\d+)\s*час', time_str)
|
|
215
|
+
hours = int(hours_match.group(1)) if hours_match else 0
|
|
216
|
+
|
|
217
|
+
# Ищем минуты
|
|
218
|
+
minutes_match = re.search(r'(\d+)\s*минут', time_str)
|
|
219
|
+
minutes = int(minutes_match.group(1)) if minutes_match else 0
|
|
220
|
+
|
|
221
|
+
# Ищем дни
|
|
222
|
+
days_match = re.search(r'(\d+)\s*дн', time_str)
|
|
223
|
+
days = int(days_match.group(1)) if days_match else 0
|
|
224
|
+
|
|
225
|
+
# Конвертируем в секунды
|
|
226
|
+
total_seconds = (days * 24 * 3600) + (hours * 3600) + (minutes * 60)
|
|
227
|
+
|
|
228
|
+
# Минимум 1 минута
|
|
229
|
+
return max(total_seconds, 60)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Функции для отправки сообщений через ИИ и от человека
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Dict, Any, Optional
|
|
9
|
+
import pytz
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
async def send_message_by_ai(
|
|
14
|
+
user_id: int,
|
|
15
|
+
message_text: str,
|
|
16
|
+
session_id: str = None
|
|
17
|
+
) -> Dict[str, Any]:
|
|
18
|
+
"""
|
|
19
|
+
Отправляет сообщение пользователю через ИИ (копирует логику process_user_message)
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
user_id: ID пользователя в Telegram
|
|
23
|
+
message_text: Текст сообщения для обработки ИИ
|
|
24
|
+
session_id: ID сессии чата (если не указан, будет использована активная сессия)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Результат отправки
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
# Импортируем необходимые компоненты
|
|
31
|
+
from .bot_utils import parse_ai_response, process_events
|
|
32
|
+
from .config import Config
|
|
33
|
+
from .openai_client import OpenAIClient
|
|
34
|
+
from .supabase_client import SupabaseClient
|
|
35
|
+
from .prompt_loader import PromptLoader
|
|
36
|
+
from aiogram import Bot
|
|
37
|
+
|
|
38
|
+
# Получаем компоненты из глобального контекста
|
|
39
|
+
from .handlers import get_global_var
|
|
40
|
+
bot = get_global_var('bot')
|
|
41
|
+
supabase_client = get_global_var('supabase_client')
|
|
42
|
+
openai_client = get_global_var('openai_client')
|
|
43
|
+
config = get_global_var('config')
|
|
44
|
+
prompt_loader = get_global_var('prompt_loader')
|
|
45
|
+
|
|
46
|
+
# Если session_id не указан, получаем активную сессию пользователя
|
|
47
|
+
if not session_id:
|
|
48
|
+
session_info = await supabase_client.get_active_session(user_id)
|
|
49
|
+
if not session_info:
|
|
50
|
+
return {
|
|
51
|
+
"status": "error",
|
|
52
|
+
"error": "Активная сессия не найдена",
|
|
53
|
+
"user_id": user_id
|
|
54
|
+
}
|
|
55
|
+
session_id = session_info['id']
|
|
56
|
+
|
|
57
|
+
# Загружаем системный промпт
|
|
58
|
+
try:
|
|
59
|
+
system_prompt = await prompt_loader.load_system_prompt()
|
|
60
|
+
logger.info(f"✅ Системный промпт загружен ({len(system_prompt)} символов)")
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.error(f"❌ Ошибка загрузки системного промпта: {e}")
|
|
63
|
+
return {
|
|
64
|
+
"status": "error",
|
|
65
|
+
"error": "Не удалось загрузить системный промпт",
|
|
66
|
+
"user_id": user_id
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Сохраняем сообщение пользователя в БД
|
|
70
|
+
await supabase_client.add_message(
|
|
71
|
+
session_id=session_id,
|
|
72
|
+
role='user',
|
|
73
|
+
content=message_text,
|
|
74
|
+
message_type='text'
|
|
75
|
+
)
|
|
76
|
+
logger.info(f"✅ Сообщение пользователя сохранено в БД")
|
|
77
|
+
|
|
78
|
+
# Получаем историю сообщений
|
|
79
|
+
chat_history = await supabase_client.get_chat_history(session_id, limit=config.MAX_CONTEXT_MESSAGES)
|
|
80
|
+
logger.info(f"📚 Загружена история: {len(chat_history)} сообщений")
|
|
81
|
+
|
|
82
|
+
# Добавляем текущее время
|
|
83
|
+
moscow_tz = pytz.timezone('Europe/Moscow')
|
|
84
|
+
current_time = datetime.now(moscow_tz)
|
|
85
|
+
time_info = current_time.strftime('%H:%M, %d.%m.%Y, %A')
|
|
86
|
+
|
|
87
|
+
# Модифицируем системный промпт, добавляя время
|
|
88
|
+
system_prompt_with_time = f"""
|
|
89
|
+
{system_prompt}
|
|
90
|
+
|
|
91
|
+
ТЕКУЩЕЕ ВРЕМЯ: {time_info} (московское время)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# Формируем контекст для OpenAI
|
|
95
|
+
messages = [{"role": "system", "content": system_prompt_with_time}]
|
|
96
|
+
|
|
97
|
+
for msg in chat_history[-config.MAX_CONTEXT_MESSAGES:]:
|
|
98
|
+
messages.append({
|
|
99
|
+
"role": msg['role'],
|
|
100
|
+
"content": msg['content']
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
# Добавляем финальные инструкции
|
|
104
|
+
final_instructions = await prompt_loader.load_final_instructions()
|
|
105
|
+
if final_instructions:
|
|
106
|
+
messages.append({"role": "system", "content": final_instructions})
|
|
107
|
+
logger.info(f"🎯 Добавлены финальные инструкции")
|
|
108
|
+
|
|
109
|
+
logger.info(f"📝 Контекст сформирован: {len(messages)} сообщений")
|
|
110
|
+
|
|
111
|
+
# Отправляем действие "печатает"
|
|
112
|
+
await bot.send_chat_action(user_id, "typing")
|
|
113
|
+
|
|
114
|
+
# Получаем ответ от ИИ
|
|
115
|
+
start_time = time.time()
|
|
116
|
+
ai_response = await openai_client.get_completion(messages)
|
|
117
|
+
processing_time = int((time.time() - start_time) * 1000)
|
|
118
|
+
|
|
119
|
+
logger.info(f"🤖 OpenAI ответил за {processing_time}мс")
|
|
120
|
+
|
|
121
|
+
# Обрабатываем ответ
|
|
122
|
+
tokens_used = 0
|
|
123
|
+
ai_metadata = {}
|
|
124
|
+
response_text = ""
|
|
125
|
+
|
|
126
|
+
if not ai_response or not ai_response.strip():
|
|
127
|
+
logger.warning(f"❌ OpenAI вернул пустой ответ!")
|
|
128
|
+
fallback_message = "Извините, произошла техническая ошибка. Попробуйте переформулировать вопрос."
|
|
129
|
+
ai_response = fallback_message
|
|
130
|
+
response_text = fallback_message
|
|
131
|
+
else:
|
|
132
|
+
tokens_used = openai_client.estimate_tokens(ai_response)
|
|
133
|
+
response_text, ai_metadata = parse_ai_response(ai_response)
|
|
134
|
+
|
|
135
|
+
if not ai_metadata:
|
|
136
|
+
response_text = ai_response
|
|
137
|
+
ai_metadata = {}
|
|
138
|
+
elif not response_text.strip():
|
|
139
|
+
response_text = ai_response
|
|
140
|
+
|
|
141
|
+
# Обновляем этап сессии и качество лида
|
|
142
|
+
if ai_metadata:
|
|
143
|
+
stage = ai_metadata.get('этап')
|
|
144
|
+
quality = ai_metadata.get('качество')
|
|
145
|
+
|
|
146
|
+
if stage or quality is not None:
|
|
147
|
+
await supabase_client.update_session_stage(session_id, stage, quality)
|
|
148
|
+
logger.info(f"✅ Этап и качество обновлены в БД")
|
|
149
|
+
|
|
150
|
+
# Обрабатываем события
|
|
151
|
+
events = ai_metadata.get('события', [])
|
|
152
|
+
if events:
|
|
153
|
+
logger.info(f"🔔 Обрабатываем {len(events)} событий")
|
|
154
|
+
await process_events(session_id, events, user_id)
|
|
155
|
+
|
|
156
|
+
# Сохраняем ответ ассистента
|
|
157
|
+
await supabase_client.add_message(
|
|
158
|
+
session_id=session_id,
|
|
159
|
+
role='assistant',
|
|
160
|
+
content=response_text,
|
|
161
|
+
message_type='text',
|
|
162
|
+
tokens_used=tokens_used,
|
|
163
|
+
processing_time_ms=processing_time,
|
|
164
|
+
ai_metadata=ai_metadata
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Определяем финальный ответ
|
|
168
|
+
if config.DEBUG_MODE:
|
|
169
|
+
final_response = ai_response
|
|
170
|
+
else:
|
|
171
|
+
final_response = response_text
|
|
172
|
+
|
|
173
|
+
# Отправляем ответ пользователю напрямую через бота
|
|
174
|
+
await bot.send_message(
|
|
175
|
+
chat_id=user_id,
|
|
176
|
+
text=final_response
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
"status": "success",
|
|
181
|
+
"user_id": user_id,
|
|
182
|
+
"response_text": response_text,
|
|
183
|
+
"tokens_used": tokens_used,
|
|
184
|
+
"processing_time_ms": processing_time,
|
|
185
|
+
"events_processed": len(events) if events else 0
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"❌ Ошибка в send_message_by_ai: {e}")
|
|
190
|
+
return {
|
|
191
|
+
"status": "error",
|
|
192
|
+
"error": str(e),
|
|
193
|
+
"user_id": user_id
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async def send_message_by_human(
|
|
197
|
+
user_id: int,
|
|
198
|
+
message_text: str,
|
|
199
|
+
session_id: Optional[str] = None
|
|
200
|
+
) -> Dict[str, Any]:
|
|
201
|
+
"""
|
|
202
|
+
Отправляет сообщение пользователю от имени человека (готовый текст)
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
user_id: ID пользователя в Telegram
|
|
206
|
+
message_text: Готовый текст сообщения
|
|
207
|
+
session_id: ID сессии (опционально, для сохранения в БД)
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Результат отправки
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
# Импортируем необходимые компоненты
|
|
214
|
+
from .handlers import get_global_var
|
|
215
|
+
bot = get_global_var('bot')
|
|
216
|
+
supabase_client = get_global_var('supabase_client')
|
|
217
|
+
|
|
218
|
+
# Отправляем сообщение пользователю
|
|
219
|
+
message = await bot.send_message(
|
|
220
|
+
chat_id=user_id,
|
|
221
|
+
text=message_text
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Если указана сессия, сохраняем сообщение в БД
|
|
225
|
+
if session_id:
|
|
226
|
+
await supabase_client.add_message(
|
|
227
|
+
session_id=session_id,
|
|
228
|
+
role='assistant',
|
|
229
|
+
content=message_text,
|
|
230
|
+
message_type='text',
|
|
231
|
+
metadata={'sent_by_human': True}
|
|
232
|
+
)
|
|
233
|
+
logger.info(f"💾 Сообщение от человека сохранено в БД")
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
"status": "success",
|
|
237
|
+
"user_id": user_id,
|
|
238
|
+
"message_id": message.message_id,
|
|
239
|
+
"message_text": message_text,
|
|
240
|
+
"saved_to_db": bool(session_id)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(f"❌ Ошибка в send_message_by_human: {e}")
|
|
245
|
+
return {
|
|
246
|
+
"status": "error",
|
|
247
|
+
"error": str(e),
|
|
248
|
+
"user_id": user_id
|
|
249
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Состояния FSM для бота
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from aiogram.fsm.state import State, StatesGroup
|
|
6
|
+
|
|
7
|
+
class UserStates(StatesGroup):
|
|
8
|
+
waiting_for_message = State()
|
|
9
|
+
admin_chat = State() # пользователь в диалоге с админом
|
|
10
|
+
|
|
11
|
+
class AdminStates(StatesGroup):
|
|
12
|
+
admin_mode = State()
|
|
13
|
+
in_conversation = State()
|
|
14
|
+
|