Delete directory 'solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью'
This commit is contained in:
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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 <path> - добавить документ из файла
|
||||
/search <q> - поиск по базе знаний
|
||||
/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()
|
||||
@@ -1,6 +0,0 @@
|
||||
langchain
|
||||
langchain-ollama
|
||||
langchain-qdrant
|
||||
langchain-text-splitters
|
||||
qdrant-client
|
||||
rich
|
||||
@@ -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)}"
|
||||
@@ -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}")
|
||||
Reference in New Issue
Block a user