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,449 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, List, Tuple, Any
|
|
6
|
+
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
from langchain_community.vectorstores import SupabaseVectorStore
|
|
9
|
+
from langchain_openai import OpenAIEmbeddings
|
|
10
|
+
from langchain_core.documents import Document
|
|
11
|
+
|
|
12
|
+
from project_root_finder import root
|
|
13
|
+
from supabase import Client, create_client
|
|
14
|
+
|
|
15
|
+
PROJECT_ROOT = root
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VectorStore(SupabaseVectorStore):
|
|
20
|
+
"""Векторное хранилище с автоматической загрузкой настроек бота."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, bot_id: str, table_name: str, query_name: str = None) -> None:
|
|
23
|
+
self.bot_id = bot_id
|
|
24
|
+
self.table_name = table_name
|
|
25
|
+
self.query_name = query_name
|
|
26
|
+
self.supabase_client: Optional[Client] = None
|
|
27
|
+
self.url: Optional[str] = None
|
|
28
|
+
self.key: Optional[str] = None
|
|
29
|
+
self.openai_api_key: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
self._load_env_config()
|
|
32
|
+
self.supabase_client = create_client(self.url, self.key)
|
|
33
|
+
|
|
34
|
+
embedding_model = "text-embedding-3-small"
|
|
35
|
+
embedding = OpenAIEmbeddings(api_key=self.openai_api_key, model=embedding_model)
|
|
36
|
+
self.embedding = embedding
|
|
37
|
+
|
|
38
|
+
# Если query_name не указан, используем стандартное имя функции
|
|
39
|
+
if not query_name:
|
|
40
|
+
query_name = f"match_{table_name}"
|
|
41
|
+
|
|
42
|
+
self.query_name = query_name
|
|
43
|
+
|
|
44
|
+
super().__init__(client=self.supabase_client, embedding=embedding, table_name=table_name, query_name=query_name)
|
|
45
|
+
|
|
46
|
+
# Проверяем наличие таблицы и функции при инициализации
|
|
47
|
+
self._check_table_and_function()
|
|
48
|
+
|
|
49
|
+
# ======================================================================
|
|
50
|
+
# Методы загрузки ENV (оставлены для доступа к настройкам из .env)
|
|
51
|
+
# ======================================================================
|
|
52
|
+
def _load_env_config(self) -> None:
|
|
53
|
+
"""Загружает конфигурацию из .env файла."""
|
|
54
|
+
try:
|
|
55
|
+
env_path = self._find_env_file()
|
|
56
|
+
|
|
57
|
+
if not env_path or not env_path.exists():
|
|
58
|
+
raise FileNotFoundError(f".env файл не найден: {env_path}")
|
|
59
|
+
|
|
60
|
+
load_dotenv(env_path)
|
|
61
|
+
|
|
62
|
+
self.url = os.getenv("SUPABASE_URL")
|
|
63
|
+
self.key = os.getenv("SUPABASE_KEY")
|
|
64
|
+
self.openai_api_key = os.getenv("OPENAI_API_KEY")
|
|
65
|
+
|
|
66
|
+
missing_vars = []
|
|
67
|
+
if not self.url:
|
|
68
|
+
missing_vars.append("SUPABASE_URL")
|
|
69
|
+
if not self.key:
|
|
70
|
+
missing_vars.append("SUPABASE_KEY")
|
|
71
|
+
if not self.openai_api_key:
|
|
72
|
+
missing_vars.append("OPENAI_API_KEY")
|
|
73
|
+
|
|
74
|
+
if missing_vars:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Отсутствуют обязательные переменные в .env: {', '.join(missing_vars)}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
logger.info("✅ Настройки Supabase загружены из %s", env_path)
|
|
80
|
+
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
logger.error("❌ Ошибка загрузки конфигурации Supabase: %s", exc)
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
def _find_env_file(self) -> Optional[Path]:
|
|
86
|
+
"""Автоматически находит .env файл для указанного бота."""
|
|
87
|
+
bot_env_path = PROJECT_ROOT / "bots" / self.bot_id / ".env"
|
|
88
|
+
|
|
89
|
+
if bot_env_path.exists():
|
|
90
|
+
logger.info("🔍 Найден .env файл для бота %s: %s", self.bot_id, bot_env_path)
|
|
91
|
+
return bot_env_path
|
|
92
|
+
|
|
93
|
+
logger.error("❌ .env файл не найден для бота %s", self.bot_id)
|
|
94
|
+
logger.error(" Искали в: %s", bot_env_path)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def _check_table_and_function(self) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Проверяет наличие таблицы и функции при инициализации класса.
|
|
100
|
+
Если таблицы нет - генерирует SQL файл и выбрасывает исключение, останавливающее код.
|
|
101
|
+
Если функции нет - генерирует SQL файл и выводит инструкции.
|
|
102
|
+
"""
|
|
103
|
+
# Проверяем наличие таблицы
|
|
104
|
+
try:
|
|
105
|
+
# Пробуем выполнить простой запрос к таблице
|
|
106
|
+
self.supabase_client.table(self.table_name).select("id").limit(1).execute()
|
|
107
|
+
logger.info("✅ Таблица '%s' найдена в Supabase", self.table_name)
|
|
108
|
+
except Exception as table_error:
|
|
109
|
+
# Если есть ошибка при запросе к таблице - значит таблицы нет или она неправильная
|
|
110
|
+
# Генерируем SQL с таблицей и функцией и выбрасываем исключение
|
|
111
|
+
self._handle_table_not_found_error(table_error, self.table_name)
|
|
112
|
+
# Исключение уже выброшено в _handle_table_not_found_error
|
|
113
|
+
|
|
114
|
+
# Если таблица есть, проверяем функцию через попытку простого поиска
|
|
115
|
+
# Используем минимальный запрос, который вызовет функцию
|
|
116
|
+
try:
|
|
117
|
+
# Пробуем выполнить простой поиск с минимальными параметрами
|
|
118
|
+
# Это вызовет функцию, если она существует
|
|
119
|
+
test_query = "test"
|
|
120
|
+
# Пробуем вызвать через родительский класс - это вызовет функцию
|
|
121
|
+
super().similarity_search(query=test_query, k=1)
|
|
122
|
+
logger.info("✅ Функция '%s' найдена в Supabase", self.query_name)
|
|
123
|
+
except Exception as function_error:
|
|
124
|
+
# Если есть ошибка при запросе к функции - значит функции нет или она неправильная
|
|
125
|
+
# Генерируем SQL файл для создания функции
|
|
126
|
+
self._handle_function_not_found_error(function_error, self.query_name)
|
|
127
|
+
|
|
128
|
+
def _is_table_not_found_error(self, error: Exception) -> bool:
|
|
129
|
+
"""Проверяет, является ли ошибка ошибкой отсутствия таблицы в Supabase."""
|
|
130
|
+
error_str = str(error).lower()
|
|
131
|
+
# Проверяем, что это ошибка таблицы (relation), а не функции
|
|
132
|
+
# Если в ошибке есть "relation" и "does not exist", но нет "function" - это таблица
|
|
133
|
+
has_relation_error = "relation" in error_str and "does not exist" in error_str
|
|
134
|
+
has_function_error = "function" in error_str
|
|
135
|
+
|
|
136
|
+
# Если ошибка содержит название таблицы - точно таблица
|
|
137
|
+
if has_relation_error and self.table_name.lower() in error_str and not has_function_error:
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
# Если это ошибка relation, но не функция, и мы пытаемся работать с таблицей - вероятно таблица
|
|
141
|
+
if has_relation_error and not has_function_error:
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def _is_function_not_found_error(self, error: Exception) -> bool:
|
|
147
|
+
"""Проверяет, является ли ошибка ошибкой отсутствия функции в Supabase."""
|
|
148
|
+
error_str = str(error).lower()
|
|
149
|
+
return (
|
|
150
|
+
"function" in error_str and "does not exist" in error_str
|
|
151
|
+
) or (
|
|
152
|
+
"function" in error_str and "not found" in error_str
|
|
153
|
+
) or (
|
|
154
|
+
"relation" in error_str and "does not exist" in error_str and "function" in error_str
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def _generate_sql_file_for_table_and_function(self, table_name: str) -> Path:
|
|
158
|
+
"""
|
|
159
|
+
Генерирует SQL файл для создания таблицы и функции в Supabase.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
table_name: Название таблицы
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Path к созданному SQL файлу
|
|
166
|
+
"""
|
|
167
|
+
from .templates.create_table_and_function_template import generate_table_and_function_sql
|
|
168
|
+
|
|
169
|
+
# Получаем размерность embedding из модели
|
|
170
|
+
# text-embedding-3-small имеет размерность 1536
|
|
171
|
+
embedding_dim = 1536
|
|
172
|
+
|
|
173
|
+
# Генерируем SQL
|
|
174
|
+
sql_content = generate_table_and_function_sql(
|
|
175
|
+
table_name=table_name,
|
|
176
|
+
embedding_dim=embedding_dim,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Создаем директорию для SQL файлов, если её нет
|
|
180
|
+
sql_dir = PROJECT_ROOT / "sql_functions"
|
|
181
|
+
sql_dir.mkdir(exist_ok=True)
|
|
182
|
+
|
|
183
|
+
# Создаем файл с именем таблицы
|
|
184
|
+
sql_file = sql_dir / f"create_{table_name}_table_and_function.sql"
|
|
185
|
+
|
|
186
|
+
# Записываем SQL в файл
|
|
187
|
+
sql_file.write_text(sql_content, encoding="utf-8")
|
|
188
|
+
|
|
189
|
+
logger.info("📝 SQL файл создан: %s", sql_file)
|
|
190
|
+
|
|
191
|
+
return sql_file
|
|
192
|
+
|
|
193
|
+
def _generate_sql_file_for_function(self, function_name: str) -> Path:
|
|
194
|
+
"""
|
|
195
|
+
Генерирует SQL файл для создания функции в Supabase.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
function_name: Название функции (например, "match_services")
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Path к созданному SQL файлу
|
|
202
|
+
"""
|
|
203
|
+
from .templates.match_services_template import generate_match_services_sql
|
|
204
|
+
|
|
205
|
+
# Определяем название таблицы из имени функции
|
|
206
|
+
# Если функция match_services, то таблица services
|
|
207
|
+
if function_name.startswith("match_"):
|
|
208
|
+
table_name = function_name.replace("match_", "")
|
|
209
|
+
else:
|
|
210
|
+
table_name = self.table_name
|
|
211
|
+
|
|
212
|
+
# Генерируем SQL
|
|
213
|
+
sql_content = generate_match_services_sql(
|
|
214
|
+
table_name=table_name,
|
|
215
|
+
function_name=function_name,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Создаем директорию для SQL файлов, если её нет
|
|
219
|
+
sql_dir = PROJECT_ROOT / "sql_functions"
|
|
220
|
+
sql_dir.mkdir(exist_ok=True)
|
|
221
|
+
|
|
222
|
+
# Создаем файл с именем функции
|
|
223
|
+
sql_file = sql_dir / f"{function_name}.sql"
|
|
224
|
+
|
|
225
|
+
# Записываем SQL в файл
|
|
226
|
+
sql_file.write_text(sql_content, encoding="utf-8")
|
|
227
|
+
|
|
228
|
+
logger.info("📝 SQL файл создан: %s", sql_file)
|
|
229
|
+
|
|
230
|
+
return sql_file
|
|
231
|
+
|
|
232
|
+
def _handle_table_not_found_error(self, error: Exception, table_name: str) -> None:
|
|
233
|
+
"""
|
|
234
|
+
Обрабатывает ошибку отсутствия таблицы: генерирует SQL файл с таблицей и функцией.
|
|
235
|
+
Выбрасывает исключение, которое останавливает выполнение кода.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
error: Исключение с ошибкой
|
|
239
|
+
table_name: Название отсутствующей таблицы
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
RuntimeError: Всегда выбрасывается после генерации SQL файла
|
|
243
|
+
"""
|
|
244
|
+
logger.error("❌ Таблица %s не найдена в Supabase: %s", table_name, error)
|
|
245
|
+
|
|
246
|
+
# Генерируем SQL файл с таблицей и функцией
|
|
247
|
+
sql_file = self._generate_sql_file_for_table_and_function(table_name)
|
|
248
|
+
|
|
249
|
+
function_name = f"match_{table_name}"
|
|
250
|
+
|
|
251
|
+
# Выводим понятное сообщение пользователю
|
|
252
|
+
print("\n" + "=" * 80)
|
|
253
|
+
print("⚠️ ОШИБКА: Таблица не найдена в Supabase")
|
|
254
|
+
print("=" * 80)
|
|
255
|
+
print(f"\nТаблица '{table_name}' отсутствует в вашей базе данных Supabase.")
|
|
256
|
+
print(f"Скорее всего, также отсутствует функция '{function_name}'.")
|
|
257
|
+
print(f"\n✅ SQL файл для создания таблицы и функции был автоматически сгенерирован:")
|
|
258
|
+
print(f" 📁 {sql_file}")
|
|
259
|
+
print(f"\n📋 ЧТО НУЖНО СДЕЛАТЬ:")
|
|
260
|
+
print(f" 1. Откройте файл: {sql_file}")
|
|
261
|
+
print(f" 2. Скопируйте содержимое SQL файла")
|
|
262
|
+
print(f" 3. Выполните SQL команду в Supabase:")
|
|
263
|
+
print(f" - Откройте Supabase Dashboard")
|
|
264
|
+
print(f" - Перейдите в SQL Editor")
|
|
265
|
+
print(f" - Вставьте SQL код из файла")
|
|
266
|
+
print(f" - Нажмите 'Run' для выполнения")
|
|
267
|
+
print(f"\nSQL файл создаст таблицу '{table_name}' и функцию '{function_name}'.")
|
|
268
|
+
print(f"После создания таблицы и функции в Supabase, повторите операцию.")
|
|
269
|
+
print("=" * 80 + "\n")
|
|
270
|
+
|
|
271
|
+
# Выбрасываем исключение, которое останавливает выполнение кода
|
|
272
|
+
raise RuntimeError(
|
|
273
|
+
f"Таблица '{table_name}' не найдена в Supabase. "
|
|
274
|
+
f"SQL файл для создания таблицы и функции сохранен в: {sql_file}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def _handle_function_not_found_error(self, error: Exception, function_name: str) -> None:
|
|
278
|
+
"""
|
|
279
|
+
Обрабатывает ошибку отсутствия функции: генерирует SQL файл и выводит инструкции.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
error: Исключение с ошибкой
|
|
283
|
+
function_name: Название отсутствующей функции
|
|
284
|
+
"""
|
|
285
|
+
logger.error("❌ Функция %s не найдена в Supabase: %s", function_name, error)
|
|
286
|
+
|
|
287
|
+
# Генерируем SQL файл
|
|
288
|
+
sql_file = self._generate_sql_file_for_function(function_name)
|
|
289
|
+
|
|
290
|
+
# Выводим понятное сообщение пользователю
|
|
291
|
+
print("\n" + "=" * 80)
|
|
292
|
+
print("⚠️ ОШИБКА: Функция не найдена в Supabase")
|
|
293
|
+
print("=" * 80)
|
|
294
|
+
print(f"\nФункция '{function_name}' отсутствует в вашей базе данных Supabase.")
|
|
295
|
+
print(f"\n✅ SQL файл для создания функции был автоматически сгенерирован:")
|
|
296
|
+
print(f" 📁 {sql_file}")
|
|
297
|
+
print(f"\n📋 ЧТО НУЖНО СДЕЛАТЬ:")
|
|
298
|
+
print(f" 1. Откройте файл: {sql_file}")
|
|
299
|
+
print(f" 2. Скопируйте содержимое SQL файла")
|
|
300
|
+
print(f" 3. Выполните SQL команду в Supabase:")
|
|
301
|
+
print(f" - Откройте Supabase Dashboard")
|
|
302
|
+
print(f" - Перейдите в SQL Editor")
|
|
303
|
+
print(f" - Вставьте SQL код из файла")
|
|
304
|
+
print(f" - Нажмите 'Run' для выполнения")
|
|
305
|
+
print(f"\nПосле создания функции в Supabase, повторите операцию.")
|
|
306
|
+
print("=" * 80 + "\n")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def as_retriever(self, **kwargs):
|
|
310
|
+
"""
|
|
311
|
+
Returns a retriever for the vector store.
|
|
312
|
+
"""
|
|
313
|
+
kwargs.setdefault("search_kwargs", {})
|
|
314
|
+
search_kwargs = kwargs["search_kwargs"]
|
|
315
|
+
filter_payload = search_kwargs.get("filter", {})
|
|
316
|
+
|
|
317
|
+
# Обеспечиваем, что filter_payload - это словарь
|
|
318
|
+
if not isinstance(filter_payload, dict):
|
|
319
|
+
filter_payload = {}
|
|
320
|
+
|
|
321
|
+
# Добавляем bot_id в фильтр
|
|
322
|
+
filter_payload = filter_payload.copy() # Копируем, чтобы не модифицировать оригинал
|
|
323
|
+
filter_payload["bot_id"] = self.bot_id
|
|
324
|
+
|
|
325
|
+
search_kwargs["filter"] = filter_payload
|
|
326
|
+
kwargs["search_kwargs"] = search_kwargs
|
|
327
|
+
|
|
328
|
+
logger.info("JSONB фильтр для поиска: %s", filter_payload)
|
|
329
|
+
|
|
330
|
+
return super().as_retriever(**kwargs)
|
|
331
|
+
|
|
332
|
+
def similarity_search(
|
|
333
|
+
self,
|
|
334
|
+
query: str,
|
|
335
|
+
k: int = 4,
|
|
336
|
+
filter: Optional[dict] = None,
|
|
337
|
+
score: float = 0.6,
|
|
338
|
+
**kwargs: Any,
|
|
339
|
+
) -> List[Document]:
|
|
340
|
+
"""
|
|
341
|
+
Поиск похожих документов с фильтрацией по минимальному score.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
query: Поисковый запрос
|
|
345
|
+
k: Количество результатов для получения
|
|
346
|
+
filter: Фильтр по metadata
|
|
347
|
+
score: Минимальный score релевантности (по умолчанию 0.6)
|
|
348
|
+
**kwargs: Дополнительные аргументы
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Список документов с score >= указанного значения
|
|
352
|
+
"""
|
|
353
|
+
# Подготавливаем фильтр
|
|
354
|
+
if filter is None:
|
|
355
|
+
filter = {}
|
|
356
|
+
filter = filter.copy()
|
|
357
|
+
filter["bot_id"] = self.bot_id
|
|
358
|
+
|
|
359
|
+
logger.info("Запрос: %s, score: %.2f", query, score)
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
# Вызываем метод родительского класса для получения результатов с scores
|
|
363
|
+
results_with_scores: List[Tuple[Document, float]] = (
|
|
364
|
+
super().similarity_search_with_relevance_scores(
|
|
365
|
+
query, k=k, filter=filter, **kwargs
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Фильтруем по score
|
|
370
|
+
filtered_results = [
|
|
371
|
+
(doc, doc_score)
|
|
372
|
+
for doc, doc_score in results_with_scores
|
|
373
|
+
if doc_score >= score
|
|
374
|
+
]
|
|
375
|
+
|
|
376
|
+
# Возвращаем только документы (без scores)
|
|
377
|
+
return [doc for doc, _ in filtered_results]
|
|
378
|
+
|
|
379
|
+
except NotImplementedError:
|
|
380
|
+
# Fallback на обычный поиск, если метод не поддерживается
|
|
381
|
+
return super().similarity_search(query, k=k, filter=filter, **kwargs)
|
|
382
|
+
except Exception as exc:
|
|
383
|
+
# Проверяем, является ли это ошибкой отсутствия таблицы
|
|
384
|
+
if self._is_table_not_found_error(exc):
|
|
385
|
+
self._handle_table_not_found_error(exc, self.table_name)
|
|
386
|
+
# Проверяем, является ли это ошибкой отсутствия функции
|
|
387
|
+
elif self._is_function_not_found_error(exc):
|
|
388
|
+
self._handle_function_not_found_error(exc, self.query_name)
|
|
389
|
+
logger.error("❌ Ошибка при поиске: %s", exc)
|
|
390
|
+
raise
|
|
391
|
+
|
|
392
|
+
async def asimilarity_search(
|
|
393
|
+
self,
|
|
394
|
+
query: str,
|
|
395
|
+
k: int = 4,
|
|
396
|
+
filter: Optional[dict] = None,
|
|
397
|
+
score: float = 0.6,
|
|
398
|
+
**kwargs: Any,
|
|
399
|
+
) -> List[Document]:
|
|
400
|
+
"""
|
|
401
|
+
Асинхронный поиск похожих документов с фильтрацией по минимальному score.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
query: Поисковый запрос
|
|
405
|
+
k: Количество результатов для получения
|
|
406
|
+
filter: Фильтр по metadata
|
|
407
|
+
score: Минимальный score релевантности (по умолчанию 0.6)
|
|
408
|
+
**kwargs: Дополнительные аргументы
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Список документов с score >= указанного значения
|
|
412
|
+
"""
|
|
413
|
+
# Подготавливаем фильтр
|
|
414
|
+
if filter is None:
|
|
415
|
+
filter = {}
|
|
416
|
+
filter = filter.copy()
|
|
417
|
+
filter["bot_id"] = self.bot_id
|
|
418
|
+
|
|
419
|
+
logger.info("Запрос: %s, score: %.2f", query, score)
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
# Вызываем синхронный метод в отдельном потоке, так как async версии нет
|
|
423
|
+
results_with_scores: List[Tuple[Document, float]] = await asyncio.to_thread(
|
|
424
|
+
super().similarity_search_with_relevance_scores,
|
|
425
|
+
query, k=k, filter=filter, **kwargs
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Фильтруем по score
|
|
429
|
+
filtered_results = [
|
|
430
|
+
(doc, doc_score)
|
|
431
|
+
for doc, doc_score in results_with_scores
|
|
432
|
+
if doc_score >= score
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
# Возвращаем только документы (без scores)
|
|
436
|
+
return [doc for doc, _ in filtered_results]
|
|
437
|
+
|
|
438
|
+
except NotImplementedError:
|
|
439
|
+
# Fallback на обычный async поиск, если метод не поддерживается
|
|
440
|
+
return await super().asimilarity_search(query, k=k, filter=filter, **kwargs)
|
|
441
|
+
except Exception as exc:
|
|
442
|
+
# Проверяем, является ли это ошибкой отсутствия таблицы
|
|
443
|
+
if self._is_table_not_found_error(exc):
|
|
444
|
+
self._handle_table_not_found_error(exc, self.table_name)
|
|
445
|
+
# Проверяем, является ли это ошибкой отсутствия функции
|
|
446
|
+
elif self._is_function_not_found_error(exc):
|
|
447
|
+
self._handle_function_not_found_error(exc, self.query_name)
|
|
448
|
+
logger.error("❌ Ошибка при асинхронном поиске: %s", exc)
|
|
449
|
+
raise
|