Delete directory 'solutions'
This commit is contained in:
@@ -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‑обёртку для GPT‑4o-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()
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
# MCP‑Memory 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 JSON‑backed 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 multi‑agent 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 it’s 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.
|
||||
-58
@@ -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())
|
||||
-115
@@ -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
@@ -1 +0,0 @@
|
||||
fastmcp
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user