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,299 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List, Optional, Dict, Any
|
|
4
|
+
from ..handlers.handlers import get_global_var
|
|
5
|
+
|
|
6
|
+
from langchain.messages import SystemMessage
|
|
7
|
+
from langchain_openai import ChatOpenAI
|
|
8
|
+
from langchain_core.output_parsers import StrOutputParser
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
class MemoryManager:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.supabase_client = get_global_var("supabase_client")
|
|
15
|
+
config = get_global_var("config")
|
|
16
|
+
|
|
17
|
+
self.max_memory_messages = config.MAX_CONTEXT_MESSAGES
|
|
18
|
+
self.min_memory_messages = config.HISTORY_MIN_MESSAGES if config.HISTORY_MIN_MESSAGES else 4
|
|
19
|
+
self.token_limit = config.HISTORY_MAX_TOKENS if config.HISTORY_MAX_TOKENS else 5000
|
|
20
|
+
|
|
21
|
+
self.chat_model = ChatOpenAI(model="gpt-5-mini", api_key=config.OPENAI_API_KEY) | StrOutputParser()
|
|
22
|
+
|
|
23
|
+
# Словарь для отслеживания активных фоновых задач суммаризации по session_id
|
|
24
|
+
self._active_summarization_tasks: Dict[str, asyncio.Task] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def get_memory_messages(self, session_id: str) -> List[Dict[str, Any]]:
|
|
28
|
+
"""Возвращает историю сообщений в формате OpenAI (список словарей с role и content)"""
|
|
29
|
+
chat_messages: List[Dict[str, Any]] = []
|
|
30
|
+
|
|
31
|
+
logger.debug(f"[MemoryManager] Запрос истории для сессии {session_id}")
|
|
32
|
+
session_info = await self.supabase_client.get_session_info(session_id)
|
|
33
|
+
messages_len = session_info.get('messages_len', self.min_memory_messages)
|
|
34
|
+
logger.info(
|
|
35
|
+
f"[MemoryManager] Текущий messages_len={messages_len}, min={self.min_memory_messages}, max={self.max_memory_messages}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
stored_summary = session_info.get('summary', '')
|
|
39
|
+
if stored_summary:
|
|
40
|
+
logger.info("[MemoryManager] Найдена сохраненная суммаризация — добавляем в контекст")
|
|
41
|
+
formatted_summary = self._format_summary(stored_summary)
|
|
42
|
+
logger.debug(f"[MemoryManager] Форматированная суммаризация: {formatted_summary[:200]}...")
|
|
43
|
+
chat_messages.append({"role": "system", "content": formatted_summary})
|
|
44
|
+
else:
|
|
45
|
+
logger.info("[MemoryManager] Суммаризация не найдена в сессии")
|
|
46
|
+
|
|
47
|
+
messages = await self.supabase_client.get_chat_history(session_id, limit=messages_len)
|
|
48
|
+
logger.info(f"[MemoryManager] Получено {len(messages)} сообщений из истории (limit={messages_len})")
|
|
49
|
+
|
|
50
|
+
added_count = 0
|
|
51
|
+
for msg in messages:
|
|
52
|
+
if msg['role'] in ('user', 'assistant'):
|
|
53
|
+
chat_messages.append({"role": msg['role'], "content": msg['content']})
|
|
54
|
+
added_count += 1
|
|
55
|
+
else:
|
|
56
|
+
logger.debug(f"[MemoryManager] Пропущено сообщение с ролью: {msg['role']}")
|
|
57
|
+
|
|
58
|
+
logger.info(f"[MemoryManager] Добавлено {added_count} сообщений из истории в контекст")
|
|
59
|
+
|
|
60
|
+
total_tokens = self._count_tokens(chat_messages)
|
|
61
|
+
logger.info(
|
|
62
|
+
f"[MemoryManager] Подготовлено сообщений: {len(chat_messages)}, оценка токенов: {total_tokens}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Проверяем, нужно ли обрезать (исключаем суммаризацию из подсчета для проверки лимита сообщений)
|
|
66
|
+
has_summary = chat_messages and chat_messages[0].get('role') == 'system' and self._is_summary_message(chat_messages[0])
|
|
67
|
+
effective_messages_count = len(chat_messages) - (1 if has_summary else 0)
|
|
68
|
+
|
|
69
|
+
# Если нужно обрезать, делаем быструю обрезку без суммаризации и запускаем фоновую задачу
|
|
70
|
+
if total_tokens > self.token_limit or effective_messages_count > self.max_memory_messages - 1:
|
|
71
|
+
logger.warning(
|
|
72
|
+
f"[MemoryManager] История превышает лимиты (tokens>{self.token_limit} или messages>{effective_messages_count}>{self.max_memory_messages - 1}). Делаем быструю обрезку и запускаем фоновую суммаризацию."
|
|
73
|
+
)
|
|
74
|
+
# Быстрая обрезка: просто берем последние сообщения
|
|
75
|
+
history_tail_size = max(self.min_memory_messages - 1, 0)
|
|
76
|
+
if has_summary:
|
|
77
|
+
# Сохраняем суммаризацию и берем хвост истории
|
|
78
|
+
summary_msg = chat_messages[0]
|
|
79
|
+
history_messages = chat_messages[1:]
|
|
80
|
+
history_tail = history_messages[-history_tail_size:] if history_tail_size else []
|
|
81
|
+
chat_messages = [summary_msg] + history_tail
|
|
82
|
+
else:
|
|
83
|
+
# Берем только хвост истории
|
|
84
|
+
chat_messages = chat_messages[-history_tail_size:] if history_tail_size else []
|
|
85
|
+
|
|
86
|
+
logger.info(f"[MemoryManager] Быстрая обрезка выполнена: {len(chat_messages)} сообщений")
|
|
87
|
+
|
|
88
|
+
# Очищаем завершенные задачи из словаря (предотвращаем утечки памяти)
|
|
89
|
+
completed_sessions = [
|
|
90
|
+
sid for sid, task in self._active_summarization_tasks.items()
|
|
91
|
+
if task.done()
|
|
92
|
+
]
|
|
93
|
+
for sid in completed_sessions:
|
|
94
|
+
del self._active_summarization_tasks[sid]
|
|
95
|
+
logger.debug(f"[MemoryManager] Удалена завершенная задача для сессии {sid}")
|
|
96
|
+
|
|
97
|
+
# Проверяем, не запущена ли уже фоновая суммаризация для этой сессии
|
|
98
|
+
if session_id in self._active_summarization_tasks:
|
|
99
|
+
existing_task = self._active_summarization_tasks[session_id]
|
|
100
|
+
if not existing_task.done():
|
|
101
|
+
logger.info(
|
|
102
|
+
f"[MemoryManager] Фоновая суммаризация уже выполняется для сессии {session_id}, "
|
|
103
|
+
f"пропускаем запуск новой. Существующая задача обработает все сообщения из БД."
|
|
104
|
+
)
|
|
105
|
+
# Не запускаем новую задачу, существующая уже обработает все сообщения из БД
|
|
106
|
+
else:
|
|
107
|
+
# Задача завершена (не должна быть здесь после очистки, но на всякий случай)
|
|
108
|
+
del self._active_summarization_tasks[session_id]
|
|
109
|
+
task = asyncio.create_task(self._background_summarize(session_id))
|
|
110
|
+
self._active_summarization_tasks[session_id] = task
|
|
111
|
+
logger.info(f"[MemoryManager] Запущена новая фоновая задача суммаризации для сессии {session_id}")
|
|
112
|
+
else:
|
|
113
|
+
# Нет активной задачи, запускаем новую
|
|
114
|
+
task = asyncio.create_task(self._background_summarize(session_id))
|
|
115
|
+
self._active_summarization_tasks[session_id] = task
|
|
116
|
+
logger.info(f"[MemoryManager] Запущена фоновая задача суммаризации для сессии {session_id}")
|
|
117
|
+
|
|
118
|
+
# Извлекаем суммаризацию из финального списка сообщений
|
|
119
|
+
summary_for_storage = self._extract_summary(chat_messages)
|
|
120
|
+
messages_len = self._calculate_messages_len(chat_messages)
|
|
121
|
+
|
|
122
|
+
# Логируем финальное состояние перед возвратом
|
|
123
|
+
logger.info(
|
|
124
|
+
f"[MemoryManager] Финальное состояние: {len(chat_messages)} сообщений, messages_len={messages_len}, summary={'есть (' + str(len(summary_for_storage)) + ' символов)' if summary_for_storage else 'нет'}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Логируем первые несколько сообщений для отладки
|
|
128
|
+
for idx, msg in enumerate(chat_messages[:3]):
|
|
129
|
+
role = msg.get('role', 'unknown')
|
|
130
|
+
content_preview = (msg.get('content', '') or '')[:100].replace("\n", " ")
|
|
131
|
+
logger.info(f"[MemoryManager] Сообщение #{idx + 1}: {role} - {content_preview}...")
|
|
132
|
+
|
|
133
|
+
# Обновляем сессию без ожидания (быстрое обновление)
|
|
134
|
+
await self.supabase_client.update_session(
|
|
135
|
+
session_id, {"messages_len": messages_len, "summary": summary_for_storage}
|
|
136
|
+
)
|
|
137
|
+
logger.info(f"[MemoryManager] Сессия {session_id} обновлена в БД")
|
|
138
|
+
|
|
139
|
+
return chat_messages
|
|
140
|
+
|
|
141
|
+
async def _background_summarize(self, session_id: str):
|
|
142
|
+
"""
|
|
143
|
+
Фоновая задача для создания суммаризации истории диалога.
|
|
144
|
+
Выполняется асинхронно, не блокирует основной поток.
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
logger.info(f"[MemoryManager] 🔄 Начало фоновой суммаризации для сессии {session_id}")
|
|
148
|
+
|
|
149
|
+
# Получаем полную историю из БД для суммаризации
|
|
150
|
+
session_info = await self.supabase_client.get_session_info(session_id)
|
|
151
|
+
messages_len = session_info.get('messages_len', self.min_memory_messages)
|
|
152
|
+
|
|
153
|
+
# Получаем больше сообщений для суммаризации (берем больше, чем обычно)
|
|
154
|
+
full_messages = await self.supabase_client.get_chat_history(session_id, limit=messages_len * 2)
|
|
155
|
+
|
|
156
|
+
# Формируем список сообщений для суммаризации
|
|
157
|
+
messages_for_summary: List[Dict[str, Any]] = []
|
|
158
|
+
existing_summary = session_info.get('summary', '')
|
|
159
|
+
|
|
160
|
+
if existing_summary:
|
|
161
|
+
formatted_summary = self._format_summary(existing_summary)
|
|
162
|
+
messages_for_summary.append({"role": "system", "content": formatted_summary})
|
|
163
|
+
|
|
164
|
+
for msg in full_messages:
|
|
165
|
+
if msg['role'] in ('user', 'assistant'):
|
|
166
|
+
messages_for_summary.append({"role": msg['role'], "content": msg['content']})
|
|
167
|
+
|
|
168
|
+
logger.info(f"[MemoryManager] 📚 Получено {len(messages_for_summary)} сообщений для суммаризации")
|
|
169
|
+
|
|
170
|
+
# Создаем суммаризацию
|
|
171
|
+
trimmed_messages = await self._trim_messages(messages_for_summary, existing_summary)
|
|
172
|
+
|
|
173
|
+
# Извлекаем новую суммаризацию
|
|
174
|
+
new_summary = self._extract_summary(trimmed_messages)
|
|
175
|
+
new_messages_len = self._calculate_messages_len(trimmed_messages)
|
|
176
|
+
|
|
177
|
+
# Сохраняем результат в БД
|
|
178
|
+
await self.supabase_client.update_session(
|
|
179
|
+
session_id, {"messages_len": new_messages_len, "summary": new_summary}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
logger.info(
|
|
183
|
+
f"[MemoryManager] ✅ Фоновая суммаризация завершена для сессии {session_id}. "
|
|
184
|
+
f"Новая суммаризация: {len(new_summary)} символов, messages_len={new_messages_len}"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(f"[MemoryManager] ❌ Ошибка в фоновой суммаризации для сессии {session_id}: {e}")
|
|
189
|
+
logger.exception("Полный стек ошибки:")
|
|
190
|
+
finally:
|
|
191
|
+
# Удаляем задачу из словаря активных задач после завершения (успешного или с ошибкой)
|
|
192
|
+
if session_id in self._active_summarization_tasks:
|
|
193
|
+
del self._active_summarization_tasks[session_id]
|
|
194
|
+
logger.debug(f"[MemoryManager] Задача суммаризации для сессии {session_id} удалена из активных")
|
|
195
|
+
|
|
196
|
+
async def _trim_messages(
|
|
197
|
+
self,
|
|
198
|
+
messages: List[Dict[str, Any]],
|
|
199
|
+
summary: Optional[str] = None,
|
|
200
|
+
) -> List[Dict[str, Any]]:
|
|
201
|
+
"""Обрезает сообщения, создавая суммаризацию. Работает со словарями OpenAI."""
|
|
202
|
+
existing_summary = summary or self._extract_summary(messages)
|
|
203
|
+
logger.info(f"[MemoryManager] Извлечена существующая суммаризация: {len(existing_summary) if existing_summary else 0} символов")
|
|
204
|
+
|
|
205
|
+
messages_history: List[Dict[str, Any]] = (
|
|
206
|
+
messages[1:] if messages and messages[0].get('role') == 'system' and self._is_summary_message(messages[0]) else messages
|
|
207
|
+
)
|
|
208
|
+
logger.info(f"[MemoryManager] История для суммаризации: {len(messages_history)} сообщений (исходно было {len(messages)})")
|
|
209
|
+
|
|
210
|
+
summary_prompt = SystemMessage(
|
|
211
|
+
content=f"""
|
|
212
|
+
Ты — ассистент для суммаризации диалогов.
|
|
213
|
+
Твоя задача — объединить уже существующую суммаризацию с новыми сообщениями из истории.
|
|
214
|
+
|
|
215
|
+
Прошлая суммаризация: (может быть пустой)
|
|
216
|
+
{existing_summary} (если ее не было не пиши об этом и если есть тоже писать не надо)
|
|
217
|
+
|
|
218
|
+
Новые сообщения будут добавлены после этого system message.
|
|
219
|
+
Сохраняй все ключевые мысли и действия, избегай потерь информации, делай связную компактную суммаризацию.
|
|
220
|
+
|
|
221
|
+
ВАЖНО:
|
|
222
|
+
- не надо ничего советовать, не надо ничего объяснять - твоя задача только суммаризация
|
|
223
|
+
|
|
224
|
+
Возвращай только суммаризацию, без других комментариев.
|
|
225
|
+
Максимальный размер суммаризации - 500 токенов.
|
|
226
|
+
"""
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Конвертируем словари OpenAI в LangChain сообщения для вызова модели
|
|
230
|
+
from ..handlers.handlers import openai_messages_to_langchain, langchain_messages_to_openai
|
|
231
|
+
langchain_history = openai_messages_to_langchain(messages_history)
|
|
232
|
+
prompt_messages = [summary_prompt] + langchain_history
|
|
233
|
+
|
|
234
|
+
logger.info(f"[MemoryManager] Отправляем {len(messages_history)} сообщений на суммаризацию")
|
|
235
|
+
new_summary_text = await self.chat_model.ainvoke(prompt_messages)
|
|
236
|
+
logger.info(f"[MemoryManager] Получена новая суммаризация: {len(new_summary_text)} символов")
|
|
237
|
+
|
|
238
|
+
formatted_summary = self._format_summary(new_summary_text)
|
|
239
|
+
new_summary = {"role": "system", "content": formatted_summary}
|
|
240
|
+
logger.info(f"[MemoryManager] Форматированная суммаризация: {len(formatted_summary)} символов")
|
|
241
|
+
|
|
242
|
+
history_tail_size = max(self.min_memory_messages - 1, 0)
|
|
243
|
+
history_tail = messages_history[-history_tail_size:] if history_tail_size else []
|
|
244
|
+
logger.info(
|
|
245
|
+
f"[MemoryManager] Новая суммаризация создана. Возвращаем {1 + len(history_tail)} сообщений (summary + хвост)."
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return [new_summary] + history_tail
|
|
249
|
+
|
|
250
|
+
def _calculate_messages_len(
|
|
251
|
+
self, messages: List[Dict[str, Any]]
|
|
252
|
+
) -> int:
|
|
253
|
+
"""Вычисляет messages_len для словарей OpenAI"""
|
|
254
|
+
effective_len = len(messages)
|
|
255
|
+
|
|
256
|
+
if messages and messages[0].get('role') == 'system' and self._is_summary_message(messages[0]):
|
|
257
|
+
effective_len -= 1
|
|
258
|
+
|
|
259
|
+
result = max(effective_len, 0) + 2
|
|
260
|
+
logger.debug(
|
|
261
|
+
f"[MemoryManager] Расчет messages_len: исходно={len(messages)}, эффективно={effective_len}, итог={result}"
|
|
262
|
+
)
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
def _count_tokens(self, messages: List[Dict[str, Any]]) -> int:
|
|
266
|
+
"""Подсчитывает токены для словарей OpenAI"""
|
|
267
|
+
tokens = 0
|
|
268
|
+
for msg in messages:
|
|
269
|
+
content = msg.get('content', '')
|
|
270
|
+
tokens += max(1, len(content) // 4)
|
|
271
|
+
logger.debug(f"[MemoryManager] Оценка токенов для {len(messages)} сообщений: {tokens}")
|
|
272
|
+
return tokens
|
|
273
|
+
|
|
274
|
+
def _format_summary(self, summary: str) -> str:
|
|
275
|
+
header = "## Суммаризация истории диалога (предыдущие сообщения до текущего момента):"
|
|
276
|
+
summary_body = summary.strip()
|
|
277
|
+
if not summary_body:
|
|
278
|
+
return header
|
|
279
|
+
return f"{header}\n{summary_body}"
|
|
280
|
+
|
|
281
|
+
def _is_summary_message(self, message: Dict[str, Any]) -> bool:
|
|
282
|
+
"""Проверяет, является ли сообщение суммаризацией (для словаря OpenAI)."""
|
|
283
|
+
if message.get('role') != 'system':
|
|
284
|
+
return False
|
|
285
|
+
content = message.get('content', '').strip()
|
|
286
|
+
return content.startswith("## Суммаризация истории диалога")
|
|
287
|
+
|
|
288
|
+
def _extract_summary(self, messages: List[Dict[str, Any]]) -> str:
|
|
289
|
+
"""Извлекает суммаризацию из первого сообщения (для словарей OpenAI)."""
|
|
290
|
+
if not messages or messages[0].get('role') != 'system' or not self._is_summary_message(messages[0]):
|
|
291
|
+
return ""
|
|
292
|
+
|
|
293
|
+
content = messages[0].get('content', '').strip()
|
|
294
|
+
header = "## Суммаризация истории диалога (предыдущие сообщения до текущего момента):"
|
|
295
|
+
if content.startswith(header):
|
|
296
|
+
summary_text = content[len(header):].lstrip()
|
|
297
|
+
# Возвращаем пустую строку, если суммаризация содержит только заголовок
|
|
298
|
+
return summary_text if summary_text else ""
|
|
299
|
+
return content
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Модуль для работы со статической памятью бота из текстовых файлов.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Dict, List
|
|
8
|
+
|
|
9
|
+
from project_root_finder import root
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
PROJECT_ROOT = root
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_bot_id_from_globals() -> Optional[str]:
|
|
17
|
+
"""
|
|
18
|
+
Пытается получить bot_id из глобальных переменных.
|
|
19
|
+
Сначала пробует supabase_client, затем config.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
bot_id если найден, None в противном случае
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
from ..handlers.handlers import get_global_var
|
|
26
|
+
|
|
27
|
+
# Пробуем получить из supabase_client
|
|
28
|
+
supabase_client = get_global_var("supabase_client")
|
|
29
|
+
if supabase_client and hasattr(supabase_client, "bot_id") and supabase_client.bot_id:
|
|
30
|
+
logger.debug(f"[StaticMemoryManager] bot_id получен из supabase_client: {supabase_client.bot_id}")
|
|
31
|
+
return supabase_client.bot_id
|
|
32
|
+
|
|
33
|
+
# Пробуем получить из config
|
|
34
|
+
config = get_global_var("config")
|
|
35
|
+
if config and hasattr(config, "BOT_ID") and config.BOT_ID:
|
|
36
|
+
logger.debug(f"[StaticMemoryManager] bot_id получен из config: {config.BOT_ID}")
|
|
37
|
+
return config.BOT_ID
|
|
38
|
+
|
|
39
|
+
logger.warning("[StaticMemoryManager] bot_id не найден в глобальных переменных")
|
|
40
|
+
return None
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.debug(f"[StaticMemoryManager] Ошибка при получении bot_id из глобальных переменных: {e}")
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class StaticMemoryManager:
|
|
47
|
+
"""
|
|
48
|
+
Класс для работы со статической памятью бота из текстовых файлов.
|
|
49
|
+
|
|
50
|
+
Автоматически ищет папку 'memory' в конфигурации бота (bots/{bot_id}/memory)
|
|
51
|
+
и предоставляет удобный интерфейс для чтения текстовых файлов.
|
|
52
|
+
|
|
53
|
+
Пример использования:
|
|
54
|
+
# С автоматическим определением bot_id из глобальных переменных
|
|
55
|
+
memory = StaticMemoryManager() # bot_id будет взят из supabase_client или config
|
|
56
|
+
|
|
57
|
+
# Или с явным указанием bot_id
|
|
58
|
+
memory = StaticMemoryManager("mdclinica")
|
|
59
|
+
|
|
60
|
+
# Чтение файлов
|
|
61
|
+
actions_info = memory.get("actions") # Читает файл actions.txt
|
|
62
|
+
promotions = memory.get("promotions") # Читает файл promotions.txt
|
|
63
|
+
all_files = memory.list_all() # Список всех доступных файлов
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, bot_id: Optional[str] = None):
|
|
67
|
+
"""
|
|
68
|
+
Инициализация менеджера статической памяти.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
bot_id: Идентификатор бота. Если не указан, будет попытка получить из глобальных переменных
|
|
72
|
+
(supabase_client или config через get_global_var)
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ValueError: Если bot_id не указан и не найден в глобальных переменных
|
|
76
|
+
"""
|
|
77
|
+
# Если bot_id не указан, пытаемся получить из глобальных переменных
|
|
78
|
+
if bot_id is None:
|
|
79
|
+
bot_id = _get_bot_id_from_globals()
|
|
80
|
+
if bot_id is None:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
"bot_id не указан и не найден в глобальных переменных. "
|
|
83
|
+
"Укажите bot_id явно или убедитесь, что supabase_client или config инициализированы."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
self.bot_id = bot_id
|
|
87
|
+
self.memory_dir = PROJECT_ROOT / "bots" / bot_id / "memory"
|
|
88
|
+
self._cache: Dict[str, str] = {}
|
|
89
|
+
|
|
90
|
+
if not self.memory_dir.exists():
|
|
91
|
+
logger.warning(
|
|
92
|
+
f"[StaticMemoryManager] Папка memory не найдена для бота {bot_id}: {self.memory_dir}"
|
|
93
|
+
)
|
|
94
|
+
logger.info(f"[StaticMemoryManager] Создайте папку {self.memory_dir} для использования статической памяти")
|
|
95
|
+
else:
|
|
96
|
+
logger.info(f"[StaticMemoryManager] Инициализирован для бота {bot_id}, папка: {self.memory_dir}")
|
|
97
|
+
|
|
98
|
+
def get(self, name: str, use_cache: bool = True) -> Optional[str]:
|
|
99
|
+
"""
|
|
100
|
+
Читает содержимое текстового файла по имени.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
name: Имя файла без расширения (например, "actions" для файла actions.txt)
|
|
104
|
+
use_cache: Использовать ли кэш для повторных запросов (по умолчанию True)
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Содержимое файла как строка, или None если файл не найден
|
|
108
|
+
|
|
109
|
+
Examples:
|
|
110
|
+
>>> memory = StaticMemoryManager() # или StaticMemoryManager("mdclinica")
|
|
111
|
+
>>> actions = memory.get("actions") # Читает bots/{bot_id}/memory/actions.txt
|
|
112
|
+
"""
|
|
113
|
+
# Проверяем кэш
|
|
114
|
+
if use_cache and name in self._cache:
|
|
115
|
+
logger.debug(f"[StaticMemoryManager] Возвращаем из кэша: {name}")
|
|
116
|
+
return self._cache[name]
|
|
117
|
+
|
|
118
|
+
# Проверяем существование папки
|
|
119
|
+
if not self.memory_dir.exists():
|
|
120
|
+
logger.warning(f"[StaticMemoryManager] Папка memory не существует: {self.memory_dir}")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
# Формируем путь к файлу
|
|
124
|
+
file_path = self.memory_dir / f"{name}.txt"
|
|
125
|
+
|
|
126
|
+
if not file_path.exists():
|
|
127
|
+
logger.warning(f"[StaticMemoryManager] Файл не найден: {file_path}")
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
content = file_path.read_text(encoding="utf-8")
|
|
132
|
+
logger.debug(f"[StaticMemoryManager] Прочитан файл {name}: {len(content)} символов")
|
|
133
|
+
|
|
134
|
+
# Сохраняем в кэш
|
|
135
|
+
if use_cache:
|
|
136
|
+
self._cache[name] = content
|
|
137
|
+
|
|
138
|
+
return content
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"[StaticMemoryManager] Ошибка при чтении файла {file_path}: {e}")
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def list_all(self) -> List[str]:
|
|
144
|
+
"""
|
|
145
|
+
Возвращает список всех доступных файлов памяти (без расширения .txt).
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Список имен файлов без расширения
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
>>> memory = StaticMemoryManager() # или StaticMemoryManager("mdclinica")
|
|
152
|
+
>>> files = memory.list_all() # ["actions", "promotions", "faq"]
|
|
153
|
+
"""
|
|
154
|
+
if not self.memory_dir.exists():
|
|
155
|
+
logger.warning(f"[StaticMemoryManager] Папка memory не существует: {self.memory_dir}")
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
files = []
|
|
159
|
+
for file_path in self.memory_dir.glob("*.txt"):
|
|
160
|
+
files.append(file_path.stem)
|
|
161
|
+
|
|
162
|
+
logger.debug(f"[StaticMemoryManager] Найдено файлов: {len(files)}")
|
|
163
|
+
return sorted(files)
|
|
164
|
+
|
|
165
|
+
def exists(self, name: str) -> bool:
|
|
166
|
+
"""
|
|
167
|
+
Проверяет, существует ли файл с указанным именем.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
name: Имя файла без расширения
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True если файл существует, False в противном случае
|
|
174
|
+
"""
|
|
175
|
+
if not self.memory_dir.exists():
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
file_path = self.memory_dir / f"{name}.txt"
|
|
179
|
+
return file_path.exists()
|
|
180
|
+
|
|
181
|
+
def clear_cache(self):
|
|
182
|
+
"""
|
|
183
|
+
Очищает кэш загруженных файлов.
|
|
184
|
+
Полезно, если файлы были изменены и нужно перезагрузить их.
|
|
185
|
+
"""
|
|
186
|
+
self._cache.clear()
|
|
187
|
+
logger.debug("[StaticMemoryManager] Кэш очищен")
|
|
188
|
+
|
|
189
|
+
def reload(self, name: str) -> Optional[str]:
|
|
190
|
+
"""
|
|
191
|
+
Принудительно перезагружает файл из диска, игнорируя кэш.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
name: Имя файла без расширения
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Содержимое файла как строка, или None если файл не найден
|
|
198
|
+
"""
|
|
199
|
+
# Удаляем из кэша если есть
|
|
200
|
+
if name in self._cache:
|
|
201
|
+
del self._cache[name]
|
|
202
|
+
|
|
203
|
+
# Читаем заново
|
|
204
|
+
return self.get(name, use_cache=True)
|
|
205
|
+
|
|
206
|
+
def get_memory_dir(self) -> Path:
|
|
207
|
+
"""
|
|
208
|
+
Возвращает путь к папке memory.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Path объект с путем к папке memory
|
|
212
|
+
"""
|
|
213
|
+
return self.memory_dir
|
|
214
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message модули smart_bot_factory
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from ..core.message_sender import (send_message, send_message_by_ai,
|
|
6
|
+
send_message_by_human,
|
|
7
|
+
send_message_to_users_by_stage)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_bot():
|
|
11
|
+
"""
|
|
12
|
+
Получает aiogram Bot из глобального контекста
|
|
13
|
+
|
|
14
|
+
Доступен после вызова bot_builder.start()
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Bot: aiogram Bot объект
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
RuntimeError: Если bot еще не инициализирован
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
from smart_bot_factory.message import get_bot
|
|
24
|
+
|
|
25
|
+
@event_router.event_handler("booking")
|
|
26
|
+
async def handle_booking(user_id: int, event_data: str):
|
|
27
|
+
bot = get_bot()
|
|
28
|
+
|
|
29
|
+
# Получаем информацию о пользователе из Telegram
|
|
30
|
+
telegram_user = await bot.get_chat(user_id)
|
|
31
|
+
name = telegram_user.first_name or 'Клиент'
|
|
32
|
+
|
|
33
|
+
# Используем любые методы aiogram Bot
|
|
34
|
+
await bot.send_message(user_id, f"Привет, {name}!")
|
|
35
|
+
await bot.send_photo(user_id, photo=...)
|
|
36
|
+
"""
|
|
37
|
+
from ..handlers.handlers import get_global_var
|
|
38
|
+
|
|
39
|
+
bot = get_global_var("bot")
|
|
40
|
+
if not bot:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
"Bot еще не инициализирован. "
|
|
43
|
+
"Убедитесь что bot_builder.start() уже вызван. "
|
|
44
|
+
"Функция get_bot() доступна только внутри обработчиков событий, "
|
|
45
|
+
"которые выполняются во время работы бота."
|
|
46
|
+
)
|
|
47
|
+
return bot
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"send_message_by_human",
|
|
52
|
+
"send_message_by_ai",
|
|
53
|
+
"send_message_to_users_by_stage",
|
|
54
|
+
"send_message", # Чистая отправка с файлами и кнопками
|
|
55
|
+
"get_bot", # Доступ к aiogram Bot
|
|
56
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
|
|
3
|
+
from langchain.tools import tool as langchain_tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def rag(*tool_args: Any, **tool_kwargs: Any):
|
|
7
|
+
"""
|
|
8
|
+
Wrapper around langchain.tools.tool to keep rag-related tooling imports localized.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
@rag
|
|
12
|
+
def my_tool(...):
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
@rag(return_direct=True)
|
|
16
|
+
def my_other_tool(...):
|
|
17
|
+
...
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
if tool_args and callable(tool_args[0]) and not tool_kwargs:
|
|
21
|
+
# Called without params: @rag
|
|
22
|
+
func = tool_args[0]
|
|
23
|
+
return langchain_tool(func)
|
|
24
|
+
|
|
25
|
+
def decorator(func: Callable[..., Any]):
|
|
26
|
+
return langchain_tool(*tool_args, **tool_kwargs)(func)
|
|
27
|
+
|
|
28
|
+
return decorator
|
|
29
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
from langchain_core.tools import BaseTool
|
|
7
|
+
|
|
8
|
+
from .decorators import rag
|
|
9
|
+
from ..utils.tool_router import ToolRouter
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RagRouter(ToolRouter):
|
|
15
|
+
"""
|
|
16
|
+
Упрощенный роутер для регистрации RAG-инструментов.
|
|
17
|
+
|
|
18
|
+
Позволяет описывать инструменты через декоратор `@rag_router.tool`
|
|
19
|
+
и потом одним вызовом подключать их к `BotBuilder`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def tool(self, *tool_args: Any, **tool_kwargs: Any) -> Callable[[Callable[..., Any]], BaseTool]:
|
|
23
|
+
"""
|
|
24
|
+
Декоратор для регистрации функции как RAG-инструмента.
|
|
25
|
+
|
|
26
|
+
Пример:
|
|
27
|
+
rag_router = RagRouter("mdclinica_rag")
|
|
28
|
+
|
|
29
|
+
@rag_router.tool
|
|
30
|
+
async def get_service(...):
|
|
31
|
+
...
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
if tool_args and callable(tool_args[0]) and not tool_kwargs:
|
|
35
|
+
func = tool_args[0]
|
|
36
|
+
tool_obj = rag(func)
|
|
37
|
+
self.add_tool(tool_obj)
|
|
38
|
+
return tool_obj
|
|
39
|
+
|
|
40
|
+
def decorator(func: Callable[..., Any]) -> BaseTool:
|
|
41
|
+
tool_obj = rag(*tool_args, **tool_kwargs)(func)
|
|
42
|
+
self.add_tool(tool_obj)
|
|
43
|
+
return tool_obj
|
|
44
|
+
|
|
45
|
+
return decorator
|
|
46
|
+
|
|
47
|
+
def register_to(self, bot_builder) -> None:
|
|
48
|
+
tools = self.get_tools()
|
|
49
|
+
if not tools:
|
|
50
|
+
logger.warning("⚠️ RagRouter %s не содержит инструментов для регистрации", self.name)
|
|
51
|
+
return
|
|
52
|
+
bot_builder.register_rag(self)
|
|
53
|
+
logger.info("✅ RagRouter %s: зарегистрировано %d инструментов", self.name, len(tools))
|
|
54
|
+
|