3 Commits
Author SHA1 Message Date
Arxip222 b0bbc739f3 update swagger
continuous-integration/drone/push Build is passing
2025-12-24 18:45:38 +03:00
Arxip222 42fcc0eb16 Забыл батчить, теперь ок
continuous-integration/drone/push Build is passing
2025-12-24 16:17:50 +03:00
Arxip222 683f779c31 UTF 8 вместо абракадабры
continuous-integration/drone/push Build is passing
2025-12-24 15:55:49 +03:00
7 changed files with 1466 additions and 223 deletions
+1403 -212
View File
File diff suppressed because it is too large Load Diff
@@ -29,7 +29,7 @@ class RAGService:
self.splitter = splitter or TextSplitter() self.splitter = splitter or TextSplitter()
async def index_document(self, document: Document) -> list[DocumentChunk]: async def index_document(self, document: Document) -> list[DocumentChunk]:
chunks_text = self.splitter.split(document.content) chunks_text = self.splitter.split(document.content)
chunks: list[DocumentChunk] = [] chunks: list[DocumentChunk] = []
for idx, text in enumerate(chunks_text): for idx, text in enumerate(chunks_text):
chunks.append( chunks.append(
@@ -42,9 +42,18 @@ class RAGService:
) )
) )
embeddings = self.embedding_service.embed_texts([c.content for c in chunks]) EMBEDDING_BATCH_SIZE = 50
all_embeddings: list[list[float]] = []
for i in range(0, len(chunks), EMBEDDING_BATCH_SIZE):
batch_chunks = chunks[i:i + EMBEDDING_BATCH_SIZE]
batch_texts = [c.content for c in batch_chunks]
batch_embeddings = self.embedding_service.embed_texts(batch_texts)
all_embeddings.extend(batch_embeddings)
print(f"Created {len(all_embeddings)} embeddings, upserting to Qdrant...")
await self.vector_repository.upsert_chunks( await self.vector_repository.upsert_chunks(
chunks, embeddings, model_version=self.embedding_service.model_version() chunks, all_embeddings, model_version=self.embedding_service.model_version()
) )
return chunks return chunks
@@ -39,5 +39,10 @@ class TextSplitter:
def _split_sentences(self, text: str) -> Iterable[str]: def _split_sentences(self, text: str) -> Iterable[str]:
parts = re.split(r"(?<=[\.\?\!])\s+", text) parts = re.split(r"(?<=[\.\?\!])\s+", text)
if len(parts) == 1 and len(text) > self.chunk_size * 2:
chunk_text = []
for i in range(0, len(text), self.chunk_size):
chunk_text.append(text[i:i + self.chunk_size])
return chunk_text
return [p.strip() for p in parts if p.strip()] return [p.strip() for p in parts if p.strip()]
@@ -36,6 +36,8 @@ class QdrantVectorRepository(IVectorRepository):
embeddings: Sequence[list[float]], embeddings: Sequence[list[float]],
model_version: str, model_version: str,
) -> None: ) -> None:
BATCH_SIZE = 100
points = [] points = []
for chunk, vector in zip(chunks, embeddings): for chunk, vector in zip(chunks, embeddings):
points.append( points.append(
@@ -52,7 +54,13 @@ class QdrantVectorRepository(IVectorRepository):
}, },
) )
) )
self.client.upsert(collection_name=self.collection_name, points=points)
if len(points) >= BATCH_SIZE:
self.client.upsert(collection_name=self.collection_name, points=points)
points = []
if points:
self.client.upsert(collection_name=self.collection_name, points=points)
async def search( async def search(
self, self,
@@ -3,6 +3,7 @@ from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, C
from aiogram.filters import Command, StateFilter from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
import aiohttp import aiohttp
from urllib.parse import unquote
from tg_bot.config.settings import settings from tg_bot.config.settings import settings
from tg_bot.infrastructure.http_client import create_http_session from tg_bot.infrastructure.http_client import create_http_session
from tg_bot.infrastructure.telegram.states.collection_states import ( from tg_bot.infrastructure.telegram.states.collection_states import (
@@ -10,6 +11,18 @@ from tg_bot.infrastructure.telegram.states.collection_states import (
CollectionEditStates CollectionEditStates
) )
def decode_title(title: str) -> str:
if not title:
return "Без названия"
try:
decoded = unquote(title)
if decoded != title or '%' not in title:
return decoded
return title
except Exception:
return title
router = Router() router = Router()
@@ -243,7 +256,7 @@ async def cmd_search(message: Message):
response = f"<b>Результаты поиска:</b> \"{query}\"\n\n" response = f"<b>Результаты поиска:</b> \"{query}\"\n\n"
for i, doc in enumerate(results[:5], 1): for i, doc in enumerate(results[:5], 1):
title = doc.get("title", "Без названия") title = decode_title(doc.get("title", "Без названия"))
content = doc.get("content", "")[:200] content = doc.get("content", "")[:200]
response += f"{i}. <b>{title}</b>\n" response += f"{i}. <b>{title}</b>\n"
response += f" <i>{content}...</i>\n\n" response += f" <i>{content}...</i>\n\n"
@@ -378,7 +391,7 @@ async def show_collection_documents(callback: CallbackQuery):
for i, doc in enumerate(documents[:10], 1): for i, doc in enumerate(documents[:10], 1):
doc_id = doc.get("document_id") doc_id = doc.get("document_id")
title = doc.get("title", "Без названия") title = decode_title(doc.get("title", "Без названия"))
content_preview = doc.get("content", "")[:100] content_preview = doc.get("content", "")[:100]
response += f"{i}. <b>{title}</b>\n" response += f"{i}. <b>{title}</b>\n"
if content_preview: if content_preview:
@@ -6,6 +6,7 @@ from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, C
from aiogram.filters import StateFilter from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
import aiohttp import aiohttp
from urllib.parse import unquote
from tg_bot.config.settings import settings from tg_bot.config.settings import settings
from tg_bot.infrastructure.http_client import create_http_session from tg_bot.infrastructure.http_client import create_http_session
from tg_bot.infrastructure.telegram.states.collection_states import ( from tg_bot.infrastructure.telegram.states.collection_states import (
@@ -13,6 +14,18 @@ from tg_bot.infrastructure.telegram.states.collection_states import (
DocumentUploadStates DocumentUploadStates
) )
def decode_title(title: str) -> str:
"""Декодирует URL-encoded название документа"""
if not title:
return "Без названия"
try:
decoded = unquote(title)
if decoded != title or '%' not in title:
return decoded
return title
except Exception:
return title
router = Router() router = Router()
@@ -108,7 +121,7 @@ async def view_document(callback: CallbackQuery):
) )
return return
title = document.get("title", "Без названия") title = decode_title(document.get("title", "Без названия"))
content = document.get("content", "") content = document.get("content", "")
collection_id = document.get("collection_id") collection_id = document.get("collection_id")
@@ -184,7 +197,7 @@ async def edit_document_prompt(callback: CallbackQuery, state: FSMContext):
await callback.message.answer( await callback.message.answer(
"<b>Редактирование документа</b>\n\n" "<b>Редактирование документа</b>\n\n"
"Отправьте новое название документа или /skip чтобы оставить текущее.\n\n" "Отправьте новое название документа или /skip чтобы оставить текущее.\n\n"
f"Текущее название: <b>{document.get('title', 'Без названия')}</b>", f"Текущее название: <b>{decode_title(document.get('title', 'Без названия'))}</b>",
parse_mode="HTML" parse_mode="HTML"
) )
await callback.answer() await callback.answer()
@@ -361,7 +374,7 @@ async def process_upload_document(message: Message, state: FSMContext):
if result: if result:
await message.answer( await message.answer(
f"<b>✅ Документ загружен и добавлен в коллекцию</b>\n\n" f"<b>✅ Документ загружен и добавлен в коллекцию</b>\n\n"
f"<b>Название:</b> {result.get('title', filename)}\n\n" f"<b>Название:</b> {decode_title(result.get('title', filename))}\n\n"
f"📄 Документ сейчас индексируется. Вы получите уведомление, когда индексация завершится.\n\n", f"📄 Документ сейчас индексируется. Вы получите уведомление, когда индексация завершится.\n\n",
parse_mode="HTML" parse_mode="HTML"
) )
@@ -83,7 +83,9 @@ async def process_premium_question(message: Message, user: User, question_text:
title = source.get('title', 'Без названия') title = source.get('title', 'Без названия')
try: try:
from urllib.parse import unquote from urllib.parse import unquote
title = unquote(title) decoded = unquote(title)
if decoded != title or '%' in title:
title = decoded
except: except:
pass pass
response += f" {idx}. {title}\n" response += f" {idx}. {title}\n"
@@ -148,7 +150,9 @@ async def process_free_question(message: Message, user: User, question_text: str
title = source.get('title', 'Без названия') title = source.get('title', 'Без названия')
try: try:
from urllib.parse import unquote from urllib.parse import unquote
title = unquote(title) decoded = unquote(title)
if decoded != title or '%' in title:
title = decoded
except: except:
pass pass
response += f" {idx}. {title}\n" response += f" {idx}. {title}\n"