Delete directory 'solutions'

This commit is contained in:
2026-05-27 07:53:02 +00:00
parent 0646c27980
commit 96911dcdb9
16 changed files with 0 additions and 1451 deletions
@@ -1,120 +0,0 @@
# Stream‑режим AI‑агента
**Stream‑режим AI‑агента** – проект, демонстрирующий работу LangGraph‑агента в режиме потоковой генерации ответов.
В отличие от обычного вызова `invoke()`, который возвращает результат только после завершения работы, здесь используется `stream()` и вывод токен за токеном прямо в консоль.
---
## 📦 Установка зависимостей
```bash
# 1️⃣ Клонируйте репозиторий
git clone https://github.com/your-username/stream-agent.git
cd stream-agent
# 2️⃣ Создайте виртуальное окружение (рекомендуется)
python -m venv .venv
source .venv/bin/activate # Windows: .\.venv\Scripts\activate
# 3️⃣ Установите зависимости из requirements.txt
pip install -r requirements.txt
```
`requirements.txt` содержит:
```text
langchain-core>=0.2.0
langgraph>=0.1.0
langchain-community>=0.2.0
tavily-python>=0.1.0
openai>=1.0.0
python-dotenv>=1.0.0 # для загрузки переменных окружения
```
> **Важно**:
> - Установите ключ OpenAI в переменную окружения `OPENAI_API_KEY`.
> - Если используете Tavily, добавьте `TAVILY_API_KEY` (необязательно, если вы не планируете использовать поиск).
```bash
export OPENAI_API_KEY="sk-..."
# export TAVILY_API_KEY="..." # при необходимости
```
---
## 🚀 Запуск
### 1️⃣ Основной скрипт `stream_agent.py`
```bash
python stream_agent.py
```
Скрипт автоматически:
1. Создаёт LLM‑обёртку для GPT4o-mini.
2. Инициализирует инструмент поиска Tavily.
3. Формирует LangGraph‑агента и запускает его в режиме `stream()`.
4. Выводит токены ответа по мере их генерации, а также события об использовании инструментов.
### 2️⃣ Тестовый скрипт (если есть)
Если вы добавили файл `test_stream_agent.py`, запустите:
```bash
pytest test_stream_agent.py
```
---
## 📄 Пример использования
```python
# stream_agent.py
import os
from langchain_community.tools.tavily_search import TavilySearchResults
from langgraph.prebuilt import create_agent_executor
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
llm = client.chat.completions.create
search_tool = TavilySearchResults(max_results=3)
agent = create_agent_executor(
llm=llm,
tools=[search_tool],
agent_type="openai-functions",
)
# Запускаем в режиме стрима
stream = agent.stream(
{"messages": [{"role": "human", "content": "Расскажи о последних новостях по искусственному интеллекту"}]},
stream_mode=["messages", "updates"],
)
for chunk_type, data in stream:
if chunk_type == "messages":
print(data["content"], end="", flush=True)
elif chunk_type == "updates":
# Можно логировать события использования инструментов
pass
print("\n--- Конец ответа ---")
```
При запуске вы увидите вывод токен за токеном, а также сообщения о вызове инструмента поиска.
---
## 📚 Что дальше?
- **Добавить обработку ошибок**: если LLM не отвечает или инструмент падает.
- **Интегрировать с UI**: например, в Streamlit для веб‑интерфейса.
- **Расширить набор инструментов**: добавить поиск в Wikipedia, генерацию изображений и т.д.
---
## 📄 Лицензия
MIT © 2026.
@@ -1,5 +0,0 @@
langchain-community
langchain-core
langgraph
openai
tavily
@@ -1,46 +0,0 @@
import os
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import HumanMessage
from langgraph.prebuilt import create_react_agent
llm = ChatOpenAI(model="gpt-4o-mini")
search_tool = TavilySearchResults(max_results=3)
agent = create_react_agent(llm, tools=[search_tool])
def format_message(message) -> str:
if message.content:
return message.content
return f"{message.tool_calls[0]['name']}({message.tool_calls[0]['args']})"
current_step = 1
def format_chunk_message(chunk):
global current_step
message, meta = chunk
step_num = meta.get("langgraph_step", 0)
if step_num != current_step:
current_step = step_num
print("\n --- --- --- \n")
if message.content:
print(message.content, end="", flush=False)
if __name__ == "__main__":
user_query = input("Введите ваш вопрос: ")
messages = [HumanMessage(content=user_query)]
stream = agent.stream(
{"messages": messages},
stream_mode=["messages", "updates"]
)
for chunk in stream:
chunk_type, chunk_data = chunk
if chunk_type == "messages":
format_chunk_message(chunk_data)
elif chunk_type == "updates":
if chunk_data.get("model", None):
last_msg = chunk_data["model"]["messages"][-1]
print(format_message(last_msg))
print()
@@ -1,116 +0,0 @@
# MCPMemory Server
A lightweight **Model Context Protocol (MCP)** server that exposes a simple key/value memory store for agents and other clients.
The project contains two scripts:
| File | Purpose |
|------|---------|
| `memory_server.py` | Runs the MCP server, exposing *set*, *get* and *delete* operations on a JSONbacked storage. |
| `memory_client.py` | Demonstrates how to connect to the server and use its tools from a client script. |
> **Why MCP?**
> MCP is a lightweight protocol for exchanging structured data between agents, services or CLI utilities. By running this server as a separate process we enable distributed multiagent systems to share state without tight coupling.
---
## 📦 Installation
```bash
# Create and activate a virtual environment (optional but recommended)
python -m venv .venv
source .venv/bin/activate # Windows: .\.venv\Scripts\activate
# Install the required packages
pip install fastmcp pydantic python-dotenv
```
> **Tip:**
> `fastmcp` is a minimal framework for building MCP servers and clients.
> `pydantic` is used internally by `fastmcp` for data validation.
---
## 🚀 Running the Server
```bash
python memory_server.py
```
The server starts on the default port **8000** (you can change it in the script).
It will create a file called `memory_data.json` in the current directory to persist data between restarts.
### Available Tools
| Tool | Parameters | Description |
|------|------------|-------------|
| `set` | `key: str`, `value: Any` | Stores a value under the given key. |
| `get` | `key: str` | Retrieves the value for the key (or `null`). |
| `delete` | `key: str` | Removes the key from storage. |
---
## 🧪 Running the Client
```bash
python memory_client.py
```
The client script demonstrates:
1. Setting a value (`foo = "bar"`).
2. Getting that value back.
3. Deleting the key and verifying its gone.
You can also use the client interactively by editing `memory_client.py` or by sending raw MCP messages via another tool (e.g., `curl`, Postman, or a custom agent).
---
## 📄 Example Usage
```python
# memory_client.py snippet
from fastmcp import FastMCPClient
client = FastMCPClient("Memory-Server", host="localhost", port=8000)
# Set a key/value pair
client.call_tool("set", {"key": "greeting", "value": "Hello, world!"})
# Retrieve the value
response = client.call_tool("get", {"key": "greeting"})
print(response) # Output: Hello, world!
# Delete the key
client.call_tool("delete", {"key": "greeting"})
# Verify deletion
assert client.call_tool("get", {"key": "greeting"}) is None
```
Feel free to integrate this server into your own agent framework or use it as a standalone memory service.
---
## 📁 Project Structure
```
.
├── memory_server.py # MCP server implementation
├── memory_client.py # Example client script
└── memory_data.json # (generated) persistent storage
```
---
## 🔧 Customization
- **Port** change the `port` argument in `FastMCP("Memory-Server", port=8000)` inside `memory_server.py`.
- **Storage Path** modify `self.storage_path = Path("./memory_data.json")` to point elsewhere.
- **Additional Tools** add new methods decorated with `@self.mcp.tool(...)` following the pattern in the script.
---
## 📜 License
This project is released under the MIT license. Feel free to fork, extend or use it in your own projects.
@@ -1,58 +0,0 @@
# memory_client.py
"""
Клиент для тестирования MCP‑сервера памяти.
Подключается к серверу через stdio и демонстрирует работу инструментов:
- save_with_namespace
- get_by_namespace
- list_keys
"""
import asyncio
from fastmcp import Client
async def main() -> None:
"""
Основная асинхронная функция, которая:
1. Создаёт клиент для сервера memory_server.py.
2. Подключается к серверу.
3. Выполняет серию вызовов инструментов и выводит результаты.
4. Закрывает соединение.
"""
# Инициализируем клиента, указывая путь к исполняемому скрипту сервера
client = Client("python memory_server.py")
# Подключаемся к серверу (стандартный ввод/вывод)
await client.connect()
try:
# Сохраняем данные в namespace "default"
result = await client.call_tool(
"save_with_namespace",
{"key": "username", "value": "Алексей", "namespace": "default"},
)
print(f"Сохранено: {result}")
# Читаем все ключи из namespace "default"
result = await client.call_tool(
"get_by_namespace",
{"namespace": "default"},
)
print("Данные namespace 'default':")
for item in result:
print(f" {item['key']}: {item['value']}")
# Ищем ключи, содержащие подстроку 'name'
keys = await client.call_tool(
"list_keys",
{"pattern": "*name"},
)
print("Ключи с 'name':", keys)
finally:
# Завершаем соединение
await client.close()
if __name__ == "__main__":
asyncio.run(main())
@@ -1,115 +0,0 @@
import json
from datetime import datetime
from pathlib import Path
from typing import Any, Optional
from fastmcp import FastMCP
class MemoryServer:
def __init__(self):
self.mcp = FastMCP("Memory-Server")
self.storage_path = Path("./memory_data.json")
# ------------------------------------------------------------------
# Вспомогательные методы для работы с файлом
# ------------------------------------------------------------------
def _load_memory(self) -> dict[str, Any]:
"""Загружает память из JSON-файла."""
if not self.storage_path.exists():
return {}
with open(self.storage_path, "r", encoding="utf-8") as f:
try:
data = json.load(f)
if isinstance(data, dict):
return data
except json.JSONDecodeError:
pass
return {}
def _save_memory(self, data: dict[str, Any]) -> None:
"""Сохраняет память в JSON-файл."""
self.storage_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.storage_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# ------------------------------------------------------------------
# Инструменты без namespace
# ------------------------------------------------------------------
@self.mcp.tool()
def save(key: str, value: Any) -> bool:
"""Сохраняет значение по ключу в память сервера."""
if not isinstance(key, str) or not key.strip():
return False
data = self._load_memory()
data[key] = {"value": value, "timestamp": datetime.utcnow().isoformat()}
self._save_memory(data)
return True
@self.mcp.tool()
def get(key: str) -> Optional[dict]:
"""Возвращает значение по ключу с метаданными."""
if not isinstance(key, str):
return None
data = self._load_memory()
entry = data.get(key)
if entry is None:
return None
return {"key": key, "value": entry["value"], "timestamp": entry["timestamp"]}
@self.mcp.tool()
def delete(key: str) -> bool:
"""Удаляет ключ из памяти сервера."""
if not isinstance(key, str):
return False
data = self._load_memory()
if key in data:
del data[key]
self._save_memory(data)
return True
return False
@self.mcp.tool()
def list_keys(pattern: str = "*") -> list[str]:
"""Возвращает список всех ключей с поддержкой wildcard-паттерна."""
if not isinstance(pattern, str):
pattern = "*"
data = self._load_memory()
import fnmatch
return [k for k in data.keys() if fnmatch.fnmatch(k, pattern)]
# ------------------------------------------------------------------
# Инструменты с namespace
# ------------------------------------------------------------------
@self.mcp.tool()
def save_with_namespace(key: str, value: Any, namespace: str = "default") -> bool:
"""Сохраняет значение с указанием пространства имён."""
if not isinstance(namespace, str) or not namespace.strip():
return False
full_key = f"{namespace}:{key}"
return self.save(full_key, value)
@self.mcp.tool()
def get_by_namespace(namespace: str = "default") -> list[dict]:
"""Возвращает все ключи из указанного namespace."""
if not isinstance(namespace, str) or not namespace.strip():
return []
data = self._load_memory()
prefix = f"{namespace}:"
result = []
for k, v in data.items():
if k.startswith(prefix):
key_part = k[len(prefix) :]
result.append(
{"key": key_part, "value": v["value"], "timestamp": v["timestamp"]}
)
return result
# ----------------------------------------------------------------------
# Запуск сервера
# ----------------------------------------------------------------------
if __name__ == "__main__":
server = MemoryServer()
server.mcp.run(transport="stdio", show_banner=False, log_level="ERROR")
@@ -1,169 +0,0 @@
# Агент с RAG‑памятью
## Описание проекта
Проект реализует **AI‑агента**, способного хранить и извлекать знания из локальной векторной базы Qdrant при помощи Ollama (LLM + эмбеддинги).
Основные возможности:
- Семантический поиск по базе знаний (`search_knowledge_base`).
- Добавление новых документов в базу (`add_to_knowledge_base`).
- Автоматическое разбиение больших текстов на чанки.
- Интеграция с LangChain для создания агента, использующего RAG‑память.
## Стек технологий
| Технология | Версия | Зачем |
|------------|--------|-------|
| Python | 3.10+ | Язык разработки |
| Qdrant | - | Векторная база данных |
| Ollama | - | Локальные LLM и эмбеддинги (`llama3`, `nomic-embed-text`) |
| LangChain | - | Фреймворк для агентов, RAG и инструментов |
## Установка зависимостей
```bash
# 1️⃣ Установить Ollama (если ещё не установлен)
# https://ollama.com/download
# Загрузить модели
ollama pull llama3
ollama pull nomic-embed-text
# 2️⃣ Создать виртуальное окружение и установить Python‑пакеты
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install langchain langchain-qdrant langchain-ollama
```
> **Важно**
> Qdrant должен быть запущен локально (по умолчанию `http://localhost:6333`).
> Если у вас нет Docker‑образа, можно установить его через:
> ```bash
> docker run -p 6333:6333 qdrant/qdrant
> ```
## Структура проекта
| Файл | Описание |
|------|----------|
| `rag_agent.py` | Основная логика агента и RAG‑инструментов. |
| `qdrant_client.py` | Обёртка над QdrantVectorStore (инициализация, добавление, поиск). |
| `chunker.py` | Модуль разбиения текста на чанки. |
| `init_loader.py` | Скрипт для первичной загрузки документов в базу. |
| `cli_client.py` | CLI‑интерфейс для взаимодействия с агентом. |
## Как запустить каждый файл
### 1️⃣ `rag_agent.py`
Файл содержит самих инструментов и пример их использования.
```bash
python rag_agent.py
```
> Вы увидите вывод о работе инструментов: добавление документа, поиск по запросу и ответ агента.
---
### 2️⃣ `qdrant_client.py`
Пример работы с клиентом Qdrant (добавление/поиск).
```bash
python qdrant_client.py
```
---
### 3️⃣ `chunker.py`
Показывает, как разбивается текст на чанки.
```bash
python chunker.py
```
---
### 4️⃣ `init_loader.py`
Загружает все файлы из папки `data/` в базу знаний.
Создайте каталог `data/`, поместите туда `.txt` или `.pdf` файлы, затем:
```bash
python init_loader.py
```
---
### 5️⃣ `cli_client.py`
CLI‑интерфейс для общения с агентом.
```bash
python cli_client.py
```
> Введите любой вопрос, и агент ответит, используя RAG‑память.
## Пример использования
1. **Инициализируйте базу знаний** (если ещё не сделано):
```bash
python init_loader.py
```
2. **Запустите CLI‑клиент**:
```bash
python cli_client.py
```
3. **Взаимодействуйте с агентом**:
```
> Какой был первый президент России?
Ответ: Первый президент Российской Федерации — Борис Ельцин.
```
4. **Добавьте новый документ вручную** (через `rag_agent.py` или любой другой скрипт):
```python
from rag_agent import add_to_knowledge_base
content = "Python – язык программирования, созданный Гвидо ван Россумом."
title = "О Python"
add_to_knowledge_base(content=content, title=title)
```
5. **Проверьте поиск**:
```python
from rag_agent import search_knowledge_base
results = search_knowledge_base(query="что такое python", max_results=3)
for r in results:
print(r.page_content[:200])
```
## Тесты и отладка
```bash
# Установить pytest (если нужен)
pip install pytest
# Запустить тесты (если есть)
pytest tests/
```
> В проекте пока нет автоматических тестов, но вы можете добавить их в папку `tests/`.
## FAQ
| Вопрос | Ответ |
|--------|-------|
| **Как изменить модель Ollama?** | Измените параметр `model` в `OllamaEmbeddings` и `OllamaLLM`. |
| **Можно ли использовать удалённый Qdrant?** | Да, укажите нужный URL при создании `KnowledgeBase`. |
| **Что делать, если база пустая?** | Запустите `init_loader.py` или добавьте документы через `add_to_knowledge_base`. |
## Лицензия
MIT © 2026
---
@@ -1,86 +0,0 @@
# chunker.py
"""
Модуль для разбиения больших текстов на небольшие части (чанки)
с помощью RecursiveCharacterTextSplitter из LangChain.
Используется в процессе загрузки документов в Qdrant:
1. Читаем исходный файл.
2. Разбиваем его на чанки, чтобы каждый чанк не превышал заданную длину.
3. Возвращаем список строк (чанков) и метаданные для каждого чанка
(например, номер чанка и оригинальный заголовок).
"""
from pathlib import Path
from typing import List, Tuple
from langchain_text_splitters import RecursiveCharacterTextSplitter
def split_document(
file_path: str | Path,
chunk_size: int = 1000,
chunk_overlap: int = 200,
) -> List[Tuple[str, dict]]:
"""
Разбивает содержимое файла на чанки.
Parameters
----------
file_path : str | Path
Путь к текстовому файлу (может быть .txt, .md и т.п.).
chunk_size : int, default=1000
Максимальная длина одного чанка в символах.
chunk_overlap : int, default=200
Количество пересекающихся символов между соседними чанками.
Returns
-------
List[Tuple[str, dict]]
Список кортежей (чанк, метаданные). Метаданные включают:
- `source`: путь к файлу,
- `chunk_index`: порядковый номер чанка в документе.
"""
# Читаем файл
path = Path(file_path)
if not path.is_file():
raise FileNotFoundError(f"Файл не найден: {file_path}")
text = path.read_text(encoding="utf-8")
# Создаём splitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
)
# Получаем чанки
chunks = splitter.split_text(text)
# Формируем список с метаданными
result: List[Tuple[str, dict]] = []
for idx, chunk in enumerate(chunks):
metadata = {
"source": str(path.resolve()),
"chunk_index": idx,
}
result.append((chunk, metadata))
return result
# Пример использования (можно закомментировать при импорте)
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python chunker.py <file_path> [chunk_size] [chunk_overlap]")
sys.exit(1)
file = sys.argv[1]
size = int(sys.argv[2]) if len(sys.argv) > 2 else 1000
overlap = int(sys.argv[3]) if len(sys.argv) > 3 else 200
for chunk, meta in split_document(file, size, overlap):
print(f"--- Chunk {meta['chunk_index']} ({len(chunk)} chars) ---")
print(chunk[:200] + "...\n")
@@ -1,136 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
cli_client.py
Интерактивный клиент для работы с агентом RAG.
Поддерживает команды:
/add <title> <content> – добавить документ в базу знаний
/search <query> – выполнить семантический поиск
/quit – выйти из программы
"""
import sys
from typing import List
# Rich для красивого вывода
try:
from rich.console import Console
from rich.table import Table
except ImportError: # pragma: no cover
print("Необходимо установить пакет 'rich'")
sys.exit(1)
# Инструменты агента (предполагается, что они реализованы в другом модуле)
try:
from rag_tools import search_knowledge_base, add_to_knowledge_base
except Exception as exc: # pragma: no cover
print(f"Ошибка при импорте инструментов: {exc}")
sys.exit(1)
console = Console()
def _parse_command(line: str) -> List[str]:
"""
Разбирает строку ввода пользователя.
Возвращает список токенов, разделённых пробелами,
но сохраняет аргументы в кавычках как один элемент.
"""
import shlex
try:
return shlex.split(line)
except ValueError as exc: # pragma: no cover
console.print(f"[red]Ошибка разбора команды:[/red] {exc}")
return []
def _handle_add(tokens: List[str]) -> None:
"""
Обработчик команды /add.
Ожидает два аргумента: title и content (в кавычках).
"""
if len(tokens) < 3:
console.print("[red]Неверное количество аргументов. Используйте:/add \"title\" \"content\"[/red]")
return
title = tokens[1]
content = " ".join(tokens[2:])
try:
add_to_knowledge_base(content=content, title=title)
console.print(f"[green]Документ '{title}' успешно добавлен.[/green]")
except Exception as exc: # pragma: no cover
console.print(f"[red]Ошибка при добавлении:[/red] {exc}")
def _handle_search(tokens: List[str]) -> None:
"""
Обработчик команды /search.
Ожидает один аргумент – запрос (в кавычках).
"""
if len(tokens) < 2:
console.print("[red]Неверное количество аргументов. Используйте:/search \"query\"[/red]")
return
query = " ".join(tokens[1:])
try:
results = search_knowledge_base(query=query, max_results=5)
if not results:
console.print("[yellow]Результаты не найдены.[/yellow]")
return
table = Table(title="Поиск", show_lines=True)
table.add_column("Номер", style="cyan")
table.add_column("Заголовок", style="magenta")
table.add_column("Содержание")
for idx, res in enumerate(results, start=1):
# Предполагается, что результат – словарь с ключами title и content
title = res.get("title", "Без заголовка")
content = res.get("content", "")
snippet = (content[:200] + "...") if len(content) > 200 else content
table.add_row(str(idx), title, snippet)
console.print(table)
except Exception as exc: # pragma: no cover
console.print(f"[red]Ошибка при поиске:[/red] {exc}")
def main() -> None:
"""
Основной цикл CLI.
"""
console.print("[bold green]Добро пожаловать в RAG-агент![/bold green]")
console.print("Доступные команды:")
console.print("/add \"title\" \"content\" – добавить документ")
console.print("/search \"query\" – выполнить поиск")
console.print("/quit – выйти из программы")
while True:
try:
line = console.input("\n[bold blue]Введите команду[/bold blue]> ")
except (KeyboardInterrupt, EOFError): # pragma: no cover
console.print("\n[bold red]Выход...[/bold red]")
break
if not line.strip():
continue
tokens = _parse_command(line)
if not tokens:
continue
command = tokens[0].lower()
if command == "/add":
_handle_add(tokens)
elif command == "/search":
_handle_search(tokens)
elif command in {"/quit", "exit"}:
console.print("[bold red]Выход...[/bold red]")
break
else:
console.print(f"[red]Неизвестная команда: {command}[/red]")
if __name__ == "__main__":
main()
@@ -1,109 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
init_loader.py
Скрипт для загрузки файлов из указанной директории в локальное Qdrant‑хранилище.
Использует:
* langchain.document_loaders.FileSystemLoader чтение всех файлов
* RecursiveCharacterTextSplitter разбиение на чанки
* OllamaEmbeddings (nomic-embed-text) – генерация эмбеддингов
* QdrantVectorStore – сохранение векторных представлений
Параметры:
--data-dir Путь к директории с исходными документами
--collection Название коллекции в Qdrant (по умолчанию: rag_collection)
"""
import argparse
import os
from pathlib import Path
from typing import List, Dict
# LangChain imports
from langchain.document_loaders import DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings.ollama import OllamaEmbeddings
from langchain.vectorstores.qdrant import QdrantVectorStore
from langchain.schema import Document
def load_documents(data_dir: Path) -> List[Document]:
"""
Загружает все файлы из указанной директории в список объектов Document.
Поддерживаются форматы *.txt, *.md, *.pdf (если установлен pdfminer).
"""
loader = DirectoryLoader(
str(data_dir),
glob="**/*",
suffixes=[".txt", ".md", ".pdf"],
show_progress=True,
)
return loader.load()
def chunk_documents(docs: List[Document], chunk_size: int = 1000, overlap: int = 200) -> List[Document]:
"""
Разбивает каждый документ на чанки фиксированного размера.
"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=overlap,
separators=["\n\n", "\n", " ", ""],
)
return splitter.split_documents(docs)
def embed_and_store(chunks: List[Document], collection_name: str) -> None:
"""
Генерирует эмбеддинги для чанков и сохраняет их в Qdrant.
"""
# Инициализируем Ollama embeddings
embedding = OllamaEmbeddings(model="nomic-embed-text")
# Создаём (или подключаемся к) коллекцию Qdrant
vector_store = QdrantVectorStore(
collection_name=collection_name,
embedding=embedding,
url="http://localhost:6333",
)
# Добавляем документы в хранилище
vector_store.add_documents(chunks)
def main() -> None:
parser = argparse.ArgumentParser(description="Загрузка документов в Qdrant.")
parser.add_argument(
"--data-dir",
type=str,
required=True,
help="Путь к директории с документами.",
)
parser.add_argument(
"--collection",
type=str,
default="rag_collection",
help="Имя коллекции в Qdrant.",
)
args = parser.parse_args()
data_dir = Path(args.data_dir).expanduser().resolve()
if not data_dir.is_dir():
raise FileNotFoundError(f"Каталог {data_dir} не найден.")
print(f"[INFO] Загружаем документы из: {data_dir}")
docs = load_documents(data_dir)
print(f"[INFO] Найдено {len(docs)} документов. Разбиваем на чанки...")
chunks = chunk_documents(docs)
print(f"[INFO] Получено {len(chunks)} чанков. Генерируем эмбеддинги и сохраняем в Qdrant...")
embed_and_store(chunks, args.collection)
print("[SUCCESS] Загрузка завершена.")
if __name__ == "__main__":
main()
@@ -1,175 +0,0 @@
# qdrant_client.py
"""
Модуль инициализации клиента Qdrant и простых операций с ним.
Используется в агенте для RAG‑памяти.
Поддержка:
- Создание/получение коллекции
- Добавление документов (с чанками) с эмбеддингами Ollama
- Семантический поиск по запросу
Требуемые зависимости:
pip install langchain langchain-qdrant langchain-ollama qdrant-client
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Iterable, List, Tuple
from langchain_ollama import OllamaEmbeddings
from langchain_qdrant import QdrantVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from qdrant_client import QdrantClient as _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")
EMBEDDING_MODEL: str = os.getenv("OLLAMA_EMBEDDING_MODEL", "nomic-embed-text")
TEXT_SPLITTER_PARAMS = {
"chunk_size": 1000,
"chunk_overlap": 200,
}
# --------------------------------------------------------------------------- #
# Класс клиента Qdrant
# --------------------------------------------------------------------------- #
class QdrantClient:
"""
Обёртка над qdrant_client и langchain_qdrant для упрощения работы с коллекцией.
"""
def __init__(self, host: str = QDRANT_HOST, port: int = QDRANT_PORT,
collection_name: str = COLLECTION_NAME) -> None:
self.host = host
self.port = port
self.collection_name = collection_name
# Инициализируем низкоуровневый клиент
self.client = _QdrantClient(host=self.host, port=self.port)
# Создаём коллекцию при необходимости
if not self._collection_exists():
self._create_collection()
# Обёртка LangChain для упрощённого добавления/поиска
self.vector_store = QdrantVectorStore(
client=self.client,
collection_name=self.collection_name,
embedding=OllamaEmbeddings(model=EMBEDDING_MODEL),
)
# ----------------------------------------------------------------------- #
# Внутренние вспомогательные методы
# ----------------------------------------------------------------------- #
def _collection_exists(self) -> bool:
"""Проверяем наличие коллекции."""
try:
self.client.get_collection(name=self.collection_name)
return True
except Exception:
return False
def _create_collection(self) -> None:
"""Создаём коллекцию с параметрами по умолчанию."""
vector_params = VectorParams(size=384, distance=Distance.COSINE)
self.client.create_collection(
collection_name=self.collection_name,
vectors_config=vector_params
)
# ----------------------------------------------------------------------- #
# Публичные методы
# ----------------------------------------------------------------------- #
def add_documents(self, documents: Iterable[str], titles: Iterable[str]) -> None:
"""
Добавляет документы в коллекцию после разбиения на чанки.
:param documents: Итерируемый список текстов.
:param titles: Заголовки документов (один к одному с documents).
"""
splitter = RecursiveCharacterTextSplitter(**TEXT_SPLITTER_PARAMS)
all_chunks: List[Tuple[str, str]] = []
for doc_text, title in zip(documents, titles):
chunks = splitter.split_text(doc_text)
# Для каждого чанка сохраняем заголовок как метаданные
all_chunks.extend([(chunk, title) for chunk in chunks])
texts, metas = zip(*all_chunks)
self.vector_store.add_texts(
texts=texts,
metadatas=[{"title": meta} for meta in metas]
)
def search(self, query: str, limit: int = 5) -> List[dict]:
"""
Семантический поиск по запросу.
:param query: Текст запроса.
:param limit: Количество результатов.
:return: Список словарей с полями 'text', 'score' и метаданными.
"""
results = self.vector_store.similarity_search_with_score(query, k=limit)
return [
{
"text": r[0].page_content,
"score": r[1],
"metadata": r[0].metadata
}
for r in results
]
# ----------------------------------------------------------------------- #
# Утилиты для загрузки файлов из директории
# ----------------------------------------------------------------------- #
def load_from_directory(self, directory: str | Path) -> None:
"""
Загружает все текстовые файлы из указанной папки в коллекцию.
:param directory: Путь к каталогу.
"""
dir_path = Path(directory)
if not dir_path.is_dir():
raise ValueError(f"Папка {directory} не существует")
docs = []
titles = []
for file in dir_path.glob("**/*.txt"):
text = file.read_text(encoding="utf-8")
docs.append(text)
titles.append(file.stem)
self.add_documents(docs, titles)
# --------------------------------------------------------------------------- #
# Пример использования (можно удалить при импорте в другие модули)
# --------------------------------------------------------------------------- #
if __name__ == "__main__":
client = QdrantClient()
# Загрузка из папки ./data
client.load_from_directory("./data")
# Поиск по запросу
query_text = "Какой смысл жизни?"
results = client.search(query_text, limit=3)
for i, res in enumerate(results, 1):
print(f"\nРезультат {i}:")
print(f"Score: {res['score']:.4f}")
print(f"Title: {res['metadata'].get('title')}")
print(f"Text snippet: {res['text'][:200]}...")
@@ -1,203 +0,0 @@
# rag_agent.py
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_react_agent, AgentExecutor
from langchain.prompts import PromptTemplate
from langchain.schema import Document
from qdrant_client import QdrantClient as _QdrantClient
from qdrant_client.http.models import Distance, VectorParams
# ---------------------------------------------------------------------------
# Конфигурация
# ---------------------------------------------------------------------------
QDRANT_HOST = os.getenv("QDRANT_HOST", "localhost")
QDRANT_PORT = int(os.getenv("QDRANT_PORT", "6333"))
COLLECTION_NAME = os.getenv("QDRANT_COLLECTION", "rag_collection")
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "nomic-embed-text")
LLM_MODEL = os.getenv("LLM_MODEL", "llama3")
# ---------------------------------------------------------------------------
# Векторное хранилище (Qdrant + Ollama-эмбеддинги)
# ---------------------------------------------------------------------------
class RagVectorStore:
"""Обёртка над QdrantVectorStore с эмбеддингами через Ollama."""
def __init__(self, collection_name: str = COLLECTION_NAME):
self.embeddings = OllamaEmbeddings(model=EMBEDDING_MODEL)
# Низкоуровневый клиент для управления коллекцией
self._client = _QdrantClient(host=QDRANT_HOST, port=QDRANT_PORT)
# Создаём коллекцию, если её ещё нет
if not self._collection_exists(collection_name):
self._client.create_collection(
collection_name=collection_name,
vectors_config=VectorParams(size=768, distance=Distance.COSINE),
)
self.store = QdrantVectorStore(
client=self._client,
collection_name=collection_name,
embedding=self.embeddings,
)
def _collection_exists(self, name: str) -> bool:
try:
self._client.get_collection(name)
return True
except Exception:
return False
def add_documents(self, docs: List[Document]) -> None:
self.store.add_documents(docs)
def search(self, query: str, k: int = 5) -> List[tuple]:
results = self.store.similarity_search_with_score(query, k=k)
return [(doc.page_content, score) for doc, score in results]
# ---------------------------------------------------------------------------
# Чанкинг
# ---------------------------------------------------------------------------
def chunk_document(text: str, title: str) -> List[Document]:
"""Разбивает текст на чанки с метаданными."""
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", " ", ""],
)
return [
Document(page_content=chunk, metadata={"title": title})
for chunk in splitter.split_text(text)
]
# ---------------------------------------------------------------------------
# Инициализация хранилища (глобальный синглтон для инструментов)
# ---------------------------------------------------------------------------
rag_store = RagVectorStore()
# ---------------------------------------------------------------------------
# LangChain-инструменты агента (@tool)
# ---------------------------------------------------------------------------
@tool
def search_knowledge_base(query: str, max_results: int = 5) -> List[str]:
"""
Семантический поиск в базе знаний.
Args:
query: поисковый запрос.
max_results: максимальное количество результатов.
Returns:
Список найденных фрагментов с оценкой релевантности.
"""
results = rag_store.search(query, k=max_results)
return [f"[score={score:.4f}] {text}" for text, score in results]
@tool
def add_to_knowledge_base(content: str, title: str) -> str:
"""
Добавляет новый документ в базу знаний.
Args:
content: полный текст документа.
title: заголовок документа.
Returns:
Сообщение об успехе.
"""
docs = chunk_document(content, title)
rag_store.add_documents(docs)
return f"Документ '{title}' успешно добавлен в базу знаний."
# ---------------------------------------------------------------------------
# Агент на базе LangChain (create_react_agent)
# ---------------------------------------------------------------------------
llm = OllamaLLM(model=LLM_MODEL)
tools = [search_knowledge_base, add_to_knowledge_base]
prompt = PromptTemplate.from_template(
"Вы — интеллектуальный агент с доступом к локальной базе знаний.\n"
"При необходимости используйте инструменты search_knowledge_base и add_to_knowledge_base.\n\n"
"Доступные инструменты:\n{tools}\n\n"
"Формат ответа (строго соблюдайте):\n"
"Question: вопрос пользователя\n"
"Thought: что нужно сделать\n"
"Action: инструмент из [{tool_names}]\n"
"Action Input: входные данные для инструмента\n"
"Observation: результат инструмента\n"
"... (можно повторять Thought/Action/Observation)\n"
"Thought: теперь я знаю ответ\n"
"Final Answer: итоговый ответ пользователю\n\n"
"Begin!\n\n"
"Question: {input}\n"
"Thought: {agent_scratchpad}"
)
agent = create_react_agent(llm=llm, tools=tools, prompt=prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# ---------------------------------------------------------------------------
# Загрузка документов из директории (для init_loader.py)
# ---------------------------------------------------------------------------
def load_documents_from_directory(directory: str) -> None:
"""Загружает все .txt и .md файлы из папки в базу знаний."""
path = Path(directory)
for file_path in path.rglob("*"):
if file_path.suffix.lower() in {".txt", ".md"}:
text = file_path.read_text(encoding="utf-8")
add_to_knowledge_base.invoke({"content": text, "title": file_path.stem})
print(f"Загружен: {file_path.name}")
# ---------------------------------------------------------------------------
# Запуск напрямую — интерактивный CLI
# ---------------------------------------------------------------------------
if __name__ == "__main__":
print("=== RAG Агент ===")
print("Команды: /add <path>, /search <query>, /quit")
while True:
try:
user_input = input("> ").strip()
except (EOFError, KeyboardInterrupt):
break
if not user_input:
continue
if user_input.lower() == "/quit":
print("Выход.")
break
elif user_input.startswith("/add "):
file_path = Path(user_input[5:].strip())
if file_path.is_file():
text = file_path.read_text(encoding="utf-8")
print(add_to_knowledge_base.invoke({"content": text, "title": file_path.stem}))
else:
print(f"Файл не найден: {file_path}")
elif user_input.startswith("/search "):
query = user_input[8:].strip()
for r in search_knowledge_base.invoke({"query": query, "max_results": 3}):
print(r)
else:
response = agent_executor.invoke({"input": user_input})
print(response["output"])
@@ -1,41 +0,0 @@
# rag_tools.py
"""
Инструменты для LangChain-агента. Используют QdrantClient из qdrant_client.py.
"""
from langchain.tools import tool
from qdrant_client import QdrantClient
_store = QdrantClient()
@tool
def search_knowledge_base(query: str, max_results: int = 5) -> list:
"""
Семантический поиск в базе знаний.
Args:
query: поисковый запрос.
max_results: максимальное количество результатов.
Returns:
Список найденных фрагментов с оценкой релевантности.
"""
results = _store.search(query, limit=max_results)
return [
{"title": r["metadata"].get("title", ""), "content": r["text"], "score": r["score"]}
for r in results
]
@tool
def add_to_knowledge_base(content: str, title: str) -> str:
"""
Добавляет новый документ в базу знаний.
Args:
content: полный текст документа.
title: заголовок документа.
Returns:
Сообщение об успехе.
"""
_store.add_documents([content], [title])
return f"Документ '{title}' успешно добавлен в базу знаний."
@@ -1,6 +0,0 @@
langchain
langchain-ollama
langchain-qdrant
qdrant-client
rich
langchain-text-splitters
@@ -1,65 +0,0 @@
from typing import List
from pydantic import BaseModel, Field
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from langchain_openai import ChatOpenAI
class AssignmentCard(BaseModel):
title: str = Field(..., description="Название задания одной фразой")
subject: str = Field(..., description="Учебная дисциплина или тема")
deadline_hint: str | None = Field(
None,
description="Срок выполнения в произвольной форме, если упомянут",
)
deliverable_type: str = Field(
...,
description="Что нужно сдать: отчёт, код, презентация и т.п.",
)
grading_hints: List[str] | None = Field(
default_factory=list,
description="Критерии оценки, которые явно названы в тексте",
)
def main() -> None:
output_parser = PydanticOutputParser(pydantic_object=AssignmentCard)
template = PromptTemplate(
template=(
"Прочитай описание учебного задания и заполни все поля карточки.\n\n"
"Описание:\n{raw_text}\n\n"
"Верни ответ строго в JSON без лишних пояснений.\n"
"{format_instructions}"
),
input_variables=["raw_text"],
partial_variables={
"format_instructions": output_parser.get_format_instructions()
},
)
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
chain = template | model | output_parser
sample = (
"Сдайте к пятнице мини-отчёт по LangChain: 2 страницы, упор на агентов. "
"Оценка: за полноту и за пример кода."
)
card: AssignmentCard = chain.invoke({"raw_text": sample})
print("Валидированный объект:")
print(card.model_dump())
print("\nСводка:")
print(
f"Задание «{card.title}» по теме «{card.subject}». "
f"Сдать: {card.deliverable_type}. "
f"Дедлайн: {card.deadline_hint or 'не указан'}. "
f"Оценивается: {', '.join(card.grading_hints) if card.grading_hints else 'критерии не указаны'}."
)
if __name__ == "__main__":
main()