diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/README.md b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/README.md deleted file mode 100644 index 9a35106..0000000 --- a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# Агент с RAG‑памятью - -## Описание проекта -Проект реализует AI‑агента, способного хранить и извлекать знания из локальной векторной базы Qdrant при помощи Ollama LLM и эмбеддингов. -- **RAG** (Retrieval‑Augmented Generation) – агент сначала ищет релевантные документы, а затем генерирует ответ, опираясь на найденную информацию. -- Векторная база хранит чанки документов, которые разбиваются при помощи `RecursiveCharacterTextSplitter`. -- Агент использует два инструмента: поиск в базе и добавление новых документов. - -## Стек технологий -| Технология | Версия / Пакет | -|------------|----------------| -| Python | 3.10+ | -| Qdrant | v1.x (Docker) | -| Ollama | `llama3`, `nomic-embed-text` | -| LangChain | 0.2+ | - -## Установка зависимостей - -```bash -# 1️⃣ Скачиваем модели Ollama -ollama pull llama3 -ollama pull nomic-embed-text - -# 2️⃣ Запускаем Qdrant (Docker) -docker run -d --name qdrant \ - -p 6333:6333 \ - ghcr.io/qdrant/qdrant:v1.9.0 - -# 3️⃣ Устанавливаем Python‑пакеты -pip install langchain langchain-qdrant langchain-ollama -``` - -> **Важно** -> * Qdrant должен быть запущен до запуска любого скрипта проекта. -> * Переменные окружения `QDRANT_HOST`, `QDRANT_PORT` и `QDRANT_COLLECTION` можно задать в `.env` или напрямую в терминале. - -## Структура файлов - -| Файл | Описание | -|------|----------| -| `rag_agent.py` | Основной скрипт, создающий агента, инструменты и запускает его. | -| `vector_store.py` | Модуль для работы с Qdrant: добавление документов, поиск. | -| `tools.py` | Декорированные функции‑инструменты (`search_knowledge_base`, `add_to_knowledge_base`). | -| `init_loader.py` | Инициализация и загрузка начальных данных в базу (опционально). | -| `cli_client.py` | CLI‑клиент для взаимодействия с агентом из терминала. | - -## Запуск каждого файла - -### 1️⃣ `rag_agent.py` - -```bash -python rag_agent.py -``` - -> Скрипт инициализирует агента, добавляет в него инструменты и ожидает пользовательский ввод через `input()`. - -### 2️⃣ `vector_store.py` - -```bash -# Пример использования из REPL или другого скрипта -from vector_store import QdrantStore - -store = QdrantStore() -store.add_document("Документ о Python", "Python – язык программирования.") -results = store.search("что такое Python") -print(results) -``` - -### 3️⃣ `tools.py` - -```bash -# Функции можно импортировать и использовать в агенте -from tools import search_knowledge_base, add_to_knowledge_base - -search_knowledge_base("Python", max_results=5) -add_to_knowledge_base("Новый контент", "Заголовок") -``` - -### 4️⃣ `init_loader.py` - -```bash -python init_loader.py -``` - -> Скрипт читает файлы из папки `data/` и загружает их в Qdrant. Убедитесь, что каталог существует. - -### 5️⃣ `cli_client.py` - -```bash -python cli_client.py "Какой язык программирования лучше?" -``` - -> CLI‑клиент отправляет запрос агенту и выводит ответ в терминал. - -## Пример использования - -1. **Инициализация базы** - ```bash - python init_loader.py - ``` - -2. **Запуск агента** - ```bash - python rag_agent.py - ``` - Введите вопрос, например: - ``` - Какой язык программирования лучше для веб‑разработки? - ``` - -3. **CLI‑клиент** - ```bash - python cli_client.py "Что такое Qdrant?" - ``` - -## Тесты (опционально) - -```bash -pip install pytest -pytest tests/ -``` - -> В папке `tests/` находятся простые unit‑тесты для модуля `vector_store`. - -## Поддержка и вопросы - -- **Проблема с подключением к Qdrant** – убедитесь, что контейнер запущен и доступен по адресу `http://localhost:6333`. -- **Ошибка при загрузке модели Ollama** – проверьте, что модели скачаны (`ollama list`). - -## Лицензия - -MIT © 2026 \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/cli_client.py b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/cli_client.py deleted file mode 100644 index f6a2dd3..0000000 --- a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/cli_client.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -cli_client.py - -Интерактивный клиент для работы с агентом RAG‑памяти. -Поддерживает команды: - /add - добавить документ в базу знаний - /search - выполнить семантический поиск по базе - /quit - выйти из программы - -Команды реализованы через инструменты, объявленные в модуле tools.py: - add_to_knowledge_base(content: str, title: str) -> None - search_knowledge_base(query: str, max_results: int = 5) -> list[dict] - -Для красивого вывода используется библиотека rich. -""" - -import sys -from pathlib import Path - -try: - from rich.console import Console - from rich.table import Table -except ImportError as exc: - print("Необходимо установить rich: pip install rich", file=sys.stderr) - raise exc - -# Импортируем инструменты из общего модуля. Предполагается, что они уже реализованы. -try: - from tools import add_to_knowledge_base, search_knowledge_base -except Exception as exc: - print("Не удалось импортировать инструменты из modules.tools", file=sys.stderr) - raise exc - -console = Console() - - -def _print_help() -> None: - """Вывод справки по доступным командам.""" - console.print( - """ -[bold cyan]/add[/] – добавить документ в базу знаний -[bold cyan]/search[/] – выполнить поиск по базе -[bold cyan]/quit[/] – выйти из программы - -Для добавления можно указать путь к файлу или вводить текст вручную. -""" - ) - - -def _handle_add() -> None: - """Обработчик команды /add.""" - console.print("[green]Введите заголовок документа:[/]") - title = input("> ").strip() - if not title: - console.print("[red]Заголовок не может быть пустым.[/]") - return - - console.print( - "[green]Выберите способ ввода содержимого:\n" - "1 – Ввести текст вручную\n" - "2 – Указать путь к файлу[/]" - ) - choice = input("> ").strip() - if choice == "2": - path_str = input("[green]Введите путь к файлу:[/]").strip() - try: - content = Path(path_str).read_text(encoding="utf-8") - except Exception as exc: - console.print(f"[red]Не удалось прочитать файл: {exc}[/]") - return - else: - console.print("[green]Введите содержимое (конец ввода – пустая строка):[/]") - lines = [] - while True: - line = input() - if line == "": - break - lines.append(line) - content = "\n".join(lines) - - try: - add_to_knowledge_base(content=content, title=title) - console.print(f"[bold green]Документ '{title}' успешно добавлен.[/]") - except Exception as exc: - console.print(f"[red]Ошибка при добавлении документа: {exc}[/]") - - -def _handle_search() -> None: - """Обработчик команды /search.""" - query = input("[green]Введите запрос для поиска:[/]").strip() - if not query: - console.print("[red]Запрос не может быть пустым.[/]") - return - - max_results_str = input( - "[green]Укажите количество результатов (по умолчанию 5):[/]" - ).strip() - try: - max_results = int(max_results_str) if max_results_str else 5 - except ValueError: - console.print("[red]Неверное число. Будет использовано значение по умолчанию: 5[/]") - max_results = 5 - - try: - results = search_knowledge_base(query=query, max_results=max_results) - except Exception as exc: - console.print(f"[red]Ошибка при поиске: {exc}[/]") - return - - if not results: - console.print("[yellow]Ничего не найдено.[/]") - return - - table = Table(title="Результаты поиска", show_lines=True) - table.add_column("№", style="dim") - table.add_column("Заголовок", style="bold cyan") - table.add_column("Текст") - - for idx, item in enumerate(results, start=1): - title = item.get("title", "Без заголовка") - snippet = item.get("content", "")[:200] + ("…" if len(item.get("content", "")) > 200 else "") - table.add_row(str(idx), title, snippet) - - console.print(table) - - -def main() -> None: - """Главный цикл программы.""" - console.print("[bold magenta]=== RAG‑Память CLI ===[/]") - _print_help() - - while True: - try: - command = input("\n[bold cyan]> [/]").strip() - except (KeyboardInterrupt, EOFError): - console.print("\n[bold red]Выход...[/]") - break - - if not command: - continue - - if command.lower() == "/quit": - console.print("[bold red]Завершение работы.[/]") - break - elif command.lower() == "/add": - _handle_add() - elif command.lower() == "/search": - _handle_search() - else: - console.print(f"[red]Неизвестная команда: {command}[/]") - _print_help() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/init_loader.py b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/init_loader.py deleted file mode 100644 index 1baa6eb..0000000 --- a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/init_loader.py +++ /dev/null @@ -1,106 +0,0 @@ -<|channel|>final code<|message|>import os -from pathlib import Path -from typing import List - -from langchain_ollama import OllamaEmbeddings -from langchain_qdrant import QdrantVectorStore -from langchain_text_splitters import RecursiveCharacterTextSplitter -from langchain.document_loaders import TextLoader, DirectoryLoader - - -def _get_documents_from_dir(directory: str) -> List[str]: - """ - Возвращает список путей к текстовым файлам в указанной директории. - """ - dir_path = Path(directory) - if not dir_path.is_dir(): - raise ValueError(f"Путь {directory} не является директорией.") - # Поддерживаем только .txt, .md и .pdf (если понадобится) - files = list(dir_path.rglob("*")) - return [str(p) for p in files if p.suffix.lower() in {".txt", ".md"}] - - -def _load_documents(file_paths: List[str]) -> List[dict]: - """ - Загружает содержимое файлов в список словарей с полями 'content' и 'metadata'. - """ - documents = [] - for path in file_paths: - loader = TextLoader(path, encoding="utf-8") - docs = loader.load() - # LangChain возвращает объекты Document; преобразуем их в dict - for doc in docs: - documents.append( - { - "content": doc.page_content, - "metadata": {"source": path}, - } - ) - return documents - - -def _chunk_documents(documents: List[dict], chunk_size: int = 1000, chunk_overlap: int = 200) -> List[str]: - """ - Разбивает документы на чанки с помощью RecursiveCharacterTextSplitter. - Возвращает список строк-чанков. - """ - splitter = RecursiveCharacterTextSplitter( - chunk_size=chunk_size, - chunk_overlap=chunk_overlap, - ) - chunks = [] - for doc in documents: - splits = splitter.split_text(doc["content"]) - # Добавляем метаданные в конец чанка для удобства поиска - for s in splits: - chunks.append(s) - return chunks - - -def _embed_and_store(chunks: List[str], collection_name: str, host: str = "localhost", port: int = 6333): - """ - Создаёт клиент Qdrant и сохраняет чанки с эмбеддингами Ollama. - """ - # Инициализация векторного хранилища - embeddings = OllamaEmbeddings(model="nomic-embed-text") - vectorstore = QdrantVectorStore( - client_kwargs={"host": host, "port": port}, - collection_name=collection_name, - embedding_function=embeddings, - ) - # Добавляем чанки в коллекцию - vectorstore.add_texts(chunks) - - -def load_directory_to_qdrant(directory: str, collection_name: str = "knowledge_base"): - """ - Полный пайплайн загрузки документов из директории в Qdrant. - """ - file_paths = _get_documents_from_dir(directory) - if not file_paths: - print(f"В каталоге {directory} не найдено текстовых файлов.") - return - - documents = _load_documents(file_paths) - chunks = _chunk_documents(documents) - - _embed_and_store(chunks, collection_name) - - print( - f"Загружено {len(chunks)} чанков из {len(file_paths)} документов в коллекцию '{collection_name}'." - ) - - -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="Загрузка документов в Qdrant.") - parser.add_argument("directory", help="Путь к директории с документами") - parser.add_argument( - "--collection", - default="knowledge_base", - help="Имя коллекции в Qdrant (по умолчанию: knowledge_base)", - ) - args = parser.parse_args() - - load_directory_to_qdrant(args.directory, args.collection) \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/rag_agent.py b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/rag_agent.py deleted file mode 100644 index 8135cb9..0000000 --- a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/rag_agent.py +++ /dev/null @@ -1,187 +0,0 @@ -import os -from pathlib import Path -from typing import List - -from langchain_ollama import OllamaEmbeddings, OllamaLLM -from langchain_qdrant import QdrantVectorStore -from langchain_text_splitters import RecursiveCharacterTextSplitter -from langchain.tools import tool -from langchain.agents import create_agent, AgentExecutor, Tool -from langchain.schema import Document - -# --------------------------------------------------------------------------- # -# Конфигурация и клиент Qdrant -# --------------------------------------------------------------------------- # - -QDRANT_HOST = os.getenv("QDRANT_HOST", "localhost") -QDRANT_PORT = int(os.getenv("QDRANT_PORT", 6333)) -COLLECTION_NAME = os.getenv("QDRANT_COLLECTION", "rag_collection") - -# Инициализируем клиент Qdrant -qdrant_store = QdrantVectorStore( - url=f"http://{QDRANT_HOST}:{QDRANT_PORT}", - collection_name=COLLECTION_NAME, - embeddings=OllamaEmbeddings(model="nomic-embed-text"), -) - -# --------------------------------------------------------------------------- # -# Чанкинг текста -# --------------------------------------------------------------------------- # - -text_splitter = RecursiveCharacterTextSplitter( - chunk_size=500, # символов в чанке - chunk_overlap=50, -) - -# --------------------------------------------------------------------------- # -# Инструменты агента -# --------------------------------------------------------------------------- # - -@tool("search_knowledge_base") -def search_knowledge_base(query: str, max_results: int = 5) -> List[Document]: - """ - Семантический поиск по базе знаний. - - :param query: поисковый запрос - :param max_results: максимальное количество результатов - :return: список документов с метаданными - """ - results = qdrant_store.similarity_search(query, k=max_results) - return results - - -@tool("add_to_knowledge_base") -def add_to_knowledge_base(content: str, title: str) -> str: - """ - Добавление нового документа в базу знаний. - - :param content: полный текст документа - :param title: заголовок/название документа - :return: подтверждение добавления - """ - # Разбиваем на чанки и сохраняем с метаданными - chunks = text_splitter.split_text(content) - docs = [ - Document(page_content=chunk, metadata={"title": title}) - for chunk in chunks - ] - qdrant_store.add_documents(docs) - return f"Документ '{title}' успешно добавлен в базу знаний." - - -# --------------------------------------------------------------------------- # -# Создание агента -# --------------------------------------------------------------------------- # - -llm = OllamaLLM(model="llama3") - -tools: List[Tool] = [ - Tool.from_function( - func=search_knowledge_base, - name="search_knowledge_base", - description="Используйте для поиска информации в базе знаний. Входные параметры: query, max_results.", - ), - Tool.from_function( - func=add_to_knowledge_base, - name="add_to_knowledge_base", - description="Добавляет новый документ в базу знаний. Параметры: content, title.", - ), -] - -agent = create_agent( - llm=llm, - tools=tools, - system_message=( - "Вы — интеллектуальный агент с доступом к базе знаний. " - "При необходимости используйте инструмент search_knowledge_base для поиска информации. " - "Если нужно добавить новый материал, применяйте add_to_knowledge_base." - ), -) - -agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) - - -# --------------------------------------------------------------------------- # -# Загрузка документов из директории -# --------------------------------------------------------------------------- # - -def load_documents_from_directory(directory: str) -> None: - """ - Загружает все текстовые файлы из указанной папки в базу знаний. - - :param directory: путь к каталогу с документами - """ - path = Path(directory) - for file_path in path.rglob("*"): - if file_path.suffix.lower() not in {".txt", ".md"}: - continue - content = file_path.read_text(encoding="utf-8") - title = file_path.stem - add_to_knowledge_base(content, title) - - -# --------------------------------------------------------------------------- # -# Интерактивный клиент -# --------------------------------------------------------------------------- # - -def interactive_cli(): - """ - Простая CLI для взаимодействия с агентом. - Команды: - /add - добавить документ из файла - /search - поиск по базе знаний - /quit - выйти - любой другой ввод передаётся агенту как вопрос - """ - print("=== RAG Агент ===") - while True: - try: - user_input = input("\n> ").strip() - except (EOFError, KeyboardInterrupt): - break - - if not user_input: - continue - - if user_input.lower() == "/quit": - print("До свидания!") - break - elif user_input.startswith("/add"): - _, *parts = user_input.split(maxsplit=1) - if parts: - file_path = parts[0] - try: - content = Path(file_path).read_text(encoding="utf-8") - title = Path(file_path).stem - print(add_to_knowledge_base(content, title)) - except Exception as e: - print(f"Ошибка при добавлении: {e}") - else: - print("Используйте /add <путь_к_файлу>") - elif user_input.startswith("/search"): - _, *parts = user_input.split(maxsplit=1) - if parts: - query = parts[0] - results = search_knowledge_base(query, max_results=3) - for i, doc in enumerate(results, 1): - print(f"\nРезультат {i}:") - print(f"Титул: {doc.metadata.get('title', 'Неизвестно')}") - print(doc.page_content[:500], "...") - else: - print("Используйте /search <запрос>") - else: - # Любой другой ввод считается вопросом агента - response = agent_executor.run(user_input) - print(response) - - -# --------------------------------------------------------------------------- # -# Точка входа -# --------------------------------------------------------------------------- # - -if __name__ == "__main__": - # При запуске можно загрузить документы из папки docs/ - docs_dir = os.getenv("DOCS_DIR", "docs") - if Path(docs_dir).exists(): - load_documents_from_directory(docs_dir) - interactive_cli() \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/requirements.txt b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/requirements.txt deleted file mode 100644 index 11f4f42..0000000 --- a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -langchain -langchain-ollama -langchain-qdrant -langchain-text-splitters -qdrant-client -rich \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/tools.py b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/tools.py deleted file mode 100644 index bb42660..0000000 --- a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/tools.py +++ /dev/null @@ -1,89 +0,0 @@ -# tools.py -""" -Модуль с инструментами для агента. -В файле определены два инструмента: - 1. search_knowledge_base – семантический поиск в Qdrant. - 2. add_to_knowledge_base – добавление документа (с чанками) в базу. - -Инструменты реализованы через декоратор @tool из langchain.tools, -что позволяет автоматически использовать их в агентах LangChain. -""" - -from pathlib import Path -from typing import List, Tuple - -# ──────────────────────── Зависимости ──────────────────────── -from langchain.embeddings.ollama import OllamaEmbeddings -from langchain.text_splitter import RecursiveCharacterTextSplitter -from langchain.vectorstores.qdrant import QdrantVectorStore -from langchain.tools import tool - -# ──────────────────────── Конфигурация ──────────────────────── -# Путь к локальной базе Qdrant (предполагается, что она уже запущена) -QDRANT_URL = "http://localhost:6333" -COLLECTION_NAME = "knowledge_base" - -# Создаём один экземпляр векторного хранилища и эмбеддеров, -# чтобы не создавать их каждый раз при вызове инструментов. -embeddings = OllamaEmbeddings(model="nomic-embed-text") -vectorstore = QdrantVectorStore( - client_kwargs={"url": QDRANT_URL}, - collection_name=COLLECTION_NAME, - embedding_function=embeddings.embed_query, # функция для получения эмбеддингов -) - -# ──────────────────────── Инструмент: поиск ──────────────────────── -@tool("search_knowledge_base", - description="Семантический поиск в базе знаний. " - "Возвращает список строк с найденными документами и их релевантностью.") -def search_knowledge_base(query: str, max_results: int = 5) -> List[Tuple[str, float]]: - """ - Выполняет семантический поиск по запросу. - - Args: - query (str): поисковый запрос. - max_results (int): максимальное количество результатов. - - Returns: - List[Tuple[str, float]]: список кортежей (текст документа, релевантность). - """ - # similarity_search_with_score возвращает [(Document, score), ...] - results = vectorstore.similarity_search_with_score(query, k=max_results) - return [(doc.page_content, score) for doc, score in results] - - -# ──────────────────────── Инструмент: добавление ──────────────────────── -@tool("add_to_knowledge_base", - description="Добавляет новый документ в базу знаний. " - "Документ разбивается на чанки и сохраняется с метаданными.") -def add_to_knowledge_base(content: str, title: str) -> str: - """ - Добавляет текстовый контент в Qdrant. - - Args: - content (str): полный текст документа. - title (str): заголовок/имя документа. - - Returns: - str: сообщение об успешном добавлении и количестве чанков. - """ - # Разбиваем документ на чанки - splitter = RecursiveCharacterTextSplitter( - chunk_size=1000, - chunk_overlap=200, - separators=["\n\n", "\n", " ", ""], - ) - chunks = splitter.split_text(content) - - # Создаём объекты Document с метаданными - from langchain.schema import Document - - documents: List[Document] = [ - Document(page_content=chunk, metadata={"title": title}) - for chunk in chunks - ] - - # Добавляем векторные представления в Qdrant - vectorstore.add_documents(documents) - - return f"Документ '{title}' добавлен. Количество чанков: {len(chunks)}" \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/vector_store.py b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/vector_store.py deleted file mode 100644 index 3fba5b1..0000000 --- a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/vector_store.py +++ /dev/null @@ -1,196 +0,0 @@ -<|channel|>final code<|message|>```python -# vector_store.py -""" -Модуль работы с Qdrant, Ollama и чанкингом. -Содержит: -- инициализацию клиента и коллекции; -- добавление документов (с разбиением на чанки); -- поиск по семантической схожести. - -Используем: -- langchain_qdrant.QdrantVectorStore -- langchain_ollama.OllamaEmbeddings -- RecursiveCharacterTextSplitter -""" - -from __future__ import annotations - -import os -from pathlib import Path -from typing import Iterable, List, Tuple - -from langchain_core.documents import Document -from langchain_text_splitters import RecursiveCharacterTextSplitter -from langchain_qdrant import QdrantVectorStore -from langchain_ollama import OllamaEmbeddings -from qdrant_client import QdrantClient -from qdrant_client.http.models import Distance, VectorParams - -# --------------------------------------------------------------------------- # -# Конфигурация по умолчанию (можно переопределить через переменные окружения) -# --------------------------------------------------------------------------- # - -QDRANT_HOST: str = os.getenv("QDRANT_HOST", "localhost") -QDRANT_PORT: int = int(os.getenv("QDRANT_PORT", "6333")) -COLLECTION_NAME: str = os.getenv("QDRANT_COLLECTION", "rag_knowledge_base") - -EMBEDDING_MODEL: str = os.getenv("OLLAMA_EMBEDDINGS", "nomic-embed-text") -DIMENSIONS: int = 512 # размер эмбеддингов nomic-embed-text - -# --------------------------------------------------------------------------- # -# Класс VectorStore -# --------------------------------------------------------------------------- # - -class VectorStore: - """ - Обёртка над QdrantVectorStore с Ollama‑эмбеддингами и чанкингом. - """ - - def __init__( - self, - host: str = QDRANT_HOST, - port: int = QDRANT_PORT, - collection_name: str = COLLECTION_NAME, - embedding_model: str = EMBEDDING_MODEL, - dimensions: int = DIMENSIONS, - ) -> None: - """ - Создаёт клиент Qdrant и коллекцию (если её нет). - """ - self.client = QdrantClient(host=host, port=port) - self.collection_name = collection_name - - # Проверяем наличие коллекции - if not self._collection_exists(): - self._create_collection(dimensions) - - # Создаём объект VectorStore - self.store = QdrantVectorStore( - client=self.client, - collection_name=self.collection_name, - embedding=OllamaEmbeddings(model=embedding_model), - ) - - # --------------------------------------------------------------------- # - # Внутренние методы для работы с коллекцией - # --------------------------------------------------------------------- # - - def _collection_exists(self) -> bool: - """Проверяем, существует ли коллекция.""" - return self.collection_name in self.client.get_collections().collections - - def _create_collection(self, dimensions: int) -> None: - """Создаём новую коллекцию с заданными параметрами.""" - self.client.recreate_collection( - collection_name=self.collection_name, - vectors_config=VectorParams(size=dimensions, distance=Distance.COSINE), - ) - - # --------------------------------------------------------------------- # - # Чанкинг - # --------------------------------------------------------------------- # - - @staticmethod - def _split_text(text: str) -> List[Document]: - """ - Разбивает текст на чанки с метаданными. - Возвращает список Document. - """ - splitter = RecursiveCharacterTextSplitter( - chunk_size=1000, - chunk_overlap=200, - separators=["\n\n", "\n", " ", ""], - ) - texts = splitter.split_text(text) - return [Document(page_content=t) for t in texts] - - # --------------------------------------------------------------------- # - # Добавление документов - # --------------------------------------------------------------------- # - - def add_document(self, content: str, title: str | None = None) -> List[str]: - """ - Добавляет документ в базу: - * разбивает на чанки; - * сохраняет каждый чанк с метаданными. - Возвращает список ID добавленных чанков. - """ - docs = self._split_text(content) - # Добавляем заголовок как metadata, если он передан - if title: - for doc in docs: - doc.metadata["title"] = title - - ids = self.store.add_documents(docs) - return ids - - def add_documents_from_path(self, path: str | Path) -> List[str]: - """ - Загружает все текстовые файлы из директории (рекурсивно) - и добавляет их в базу. - Возвращает список всех ID. - """ - p = Path(path) - if not p.is_dir(): - raise ValueError(f"Путь {path} не является директорией") - - all_ids: List[str] = [] - for file in p.rglob("*"): - if file.suffix.lower() in {".txt", ".md"}: - text = file.read_text(encoding="utf-8") - ids = self.add_document(text, title=file.stem) - all_ids.extend(ids) - return all_ids - - # --------------------------------------------------------------------- # - # Поиск - # --------------------------------------------------------------------- # - - def search( - self, - query: str, - k: int = 5, - filter_metadata: dict | None = None, - ) -> List[Tuple[str, float]]: - """ - Семантический поиск по базе. - Возвращает список (document_id, score) упорядоченных по убыванию релевантности. - """ - results = self.store.similarity_search_with_score( - query=query, - k=k, - filter=filter_metadata, - ) - return [(doc.metadata.get("id", ""), score) for doc, score in results] - - # --------------------------------------------------------------------- # - # Удаление (необязательно) - # --------------------------------------------------------------------- # - - def delete_document(self, doc_id: str) -> None: - """Удаляет документ по ID.""" - self.client.delete( - collection_name=self.collection_name, - points_selector={"ids": [doc_id]}, - ) - -# --------------------------------------------------------------------------- # -# Пример использования (можно закомментировать при импорте) -# --------------------------------------------------------------------------- # - -if __name__ == "__main__": - store = VectorStore() - # Добавляем примерный документ - doc_text = """ - LangChain – это библиотека для создания LLM‑агентов. - Она позволяет легко интегрировать внешние сервисы, такие как базы данных, - поисковые движки и другие инструменты. В этом примере мы используем Qdrant - в качестве хранилища эмбеддингов и Ollama для генерации эмбеддингов. - """ - store.add_document(doc_text, title="LangChain Overview") - - # Поиск по запросу - results = store.search("что такое LangChain", k=3) - print("Результаты поиска:") - for doc_id, score in results: - print(f"ID: {doc_id} | Score: {score:.4f}") \ No newline at end of file