From b68eb03497200863d9a8752185c1b0acb84c5243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D1=80=D0=B8=D1=8F=20=D0=91=D0=B5=D1=80=D0=B4?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=D0=B0?= Date: Wed, 27 May 2026 07:54:07 +0000 Subject: [PATCH] revert 96911dcdb90236b92f051861ec42223de15d4d6c revert Delete directory 'solutions' --- .../README.md | 120 +++++++++++ .../requirements.txt | 5 + .../stream_agent.py | 46 ++++ .../README.md | 116 ++++++++++ .../memory_client.py | 58 +++++ .../memory_server.py | 115 ++++++++++ .../requirements.txt | 1 + .../README.md | 169 +++++++++++++++ .../chunker.py | 86 ++++++++ .../cli_client.py | 136 ++++++++++++ .../init_loader.py | 109 ++++++++++ .../qdrant_client.py | 175 +++++++++++++++ .../rag_agent.py | 203 ++++++++++++++++++ .../rag_tools.py | 41 ++++ .../requirements.txt | 6 + .../solution.py | 65 ++++++ 16 files changed, 1451 insertions(+) create mode 100644 solutions/699cc158d6d3a5544a3ed35b_Stream_режим_AI_агента/README.md create mode 100644 solutions/699cc158d6d3a5544a3ed35b_Stream_режим_AI_агента/requirements.txt create mode 100644 solutions/699cc158d6d3a5544a3ed35b_Stream_режим_AI_агента/stream_agent.py create mode 100644 solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/README.md create mode 100644 solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/memory_client.py create mode 100644 solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/memory_server.py create mode 100644 solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/requirements.txt create mode 100644 solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/README.md create mode 100644 solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/chunker.py create mode 100644 solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/cli_client.py create mode 100644 solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/init_loader.py create mode 100644 solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/qdrant_client.py create mode 100644 solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/rag_agent.py create mode 100644 solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/rag_tools.py create mode 100644 solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/requirements.txt create mode 100644 solutions/сырой текст задания → плоская карточка/solution.py diff --git a/solutions/699cc158d6d3a5544a3ed35b_Stream_режим_AI_агента/README.md b/solutions/699cc158d6d3a5544a3ed35b_Stream_режим_AI_агента/README.md new file mode 100644 index 0000000..5dec197 --- /dev/null +++ b/solutions/699cc158d6d3a5544a3ed35b_Stream_режим_AI_агента/README.md @@ -0,0 +1,120 @@ +# 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. \ No newline at end of file diff --git a/solutions/699cc158d6d3a5544a3ed35b_Stream_режим_AI_агента/requirements.txt b/solutions/699cc158d6d3a5544a3ed35b_Stream_режим_AI_агента/requirements.txt new file mode 100644 index 0000000..dace80d --- /dev/null +++ b/solutions/699cc158d6d3a5544a3ed35b_Stream_режим_AI_агента/requirements.txt @@ -0,0 +1,5 @@ +langchain-community +langchain-core +langgraph +openai +tavily \ No newline at end of file diff --git a/solutions/699cc158d6d3a5544a3ed35b_Stream_режим_AI_агента/stream_agent.py b/solutions/699cc158d6d3a5544a3ed35b_Stream_режим_AI_агента/stream_agent.py new file mode 100644 index 0000000..f93b6b2 --- /dev/null +++ b/solutions/699cc158d6d3a5544a3ed35b_Stream_режим_AI_агента/stream_agent.py @@ -0,0 +1,46 @@ +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() \ No newline at end of file diff --git a/solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/README.md b/solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/README.md new file mode 100644 index 0000000..fd28333 --- /dev/null +++ b/solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/README.md @@ -0,0 +1,116 @@ +# 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. \ No newline at end of file diff --git a/solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/memory_client.py b/solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/memory_client.py new file mode 100644 index 0000000..1bc4d7f --- /dev/null +++ b/solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/memory_client.py @@ -0,0 +1,58 @@ +# 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()) \ No newline at end of file diff --git a/solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/memory_server.py b/solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/memory_server.py new file mode 100644 index 0000000..df9c785 --- /dev/null +++ b/solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/memory_server.py @@ -0,0 +1,115 @@ +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") \ No newline at end of file diff --git a/solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/requirements.txt b/solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/requirements.txt new file mode 100644 index 0000000..35a7d0c --- /dev/null +++ b/solutions/69f8e929da860fb4533faa2a_MCP-сервер_для_управления_памятью_агента/requirements.txt @@ -0,0 +1 @@ +fastmcp \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/README.md b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/README.md new file mode 100644 index 0000000..82bd021 --- /dev/null +++ b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/README.md @@ -0,0 +1,169 @@ +# Агент с 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 + +--- \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/chunker.py b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/chunker.py new file mode 100644 index 0000000..71cc21c --- /dev/null +++ b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/chunker.py @@ -0,0 +1,86 @@ +# 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 [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") \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/cli_client.py b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/cli_client.py new file mode 100644 index 0000000..bc5e72f --- /dev/null +++ b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/cli_client.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +cli_client.py + +Интерактивный клиент для работы с агентом RAG. +Поддерживает команды: + /add <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() \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/init_loader.py b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/init_loader.py new file mode 100644 index 0000000..388ff25 --- /dev/null +++ b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/init_loader.py @@ -0,0 +1,109 @@ +#!/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() \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/qdrant_client.py b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/qdrant_client.py new file mode 100644 index 0000000..6c93f17 --- /dev/null +++ b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/qdrant_client.py @@ -0,0 +1,175 @@ +# 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]}...") \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/rag_agent.py b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/rag_agent.py new file mode 100644 index 0000000..4b4e841 --- /dev/null +++ b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/rag_agent.py @@ -0,0 +1,203 @@ +# 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"]) \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/rag_tools.py b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/rag_tools.py new file mode 100644 index 0000000..468a1ab --- /dev/null +++ b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/rag_tools.py @@ -0,0 +1,41 @@ +# 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}' успешно добавлен в базу знаний." \ No newline at end of file diff --git a/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/requirements.txt b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/requirements.txt new file mode 100644 index 0000000..ed9f7c2 --- /dev/null +++ b/solutions/6a02e23da6fe2e4ac16acf65_Агент_с_RAG_памятью/requirements.txt @@ -0,0 +1,6 @@ +langchain +langchain-ollama +langchain-qdrant +qdrant-client +rich +langchain-text-splitters \ No newline at end of file diff --git a/solutions/сырой текст задания → плоская карточка/solution.py b/solutions/сырой текст задания → плоская карточка/solution.py new file mode 100644 index 0000000..50b1bfd --- /dev/null +++ b/solutions/сырой текст задания → плоская карточка/solution.py @@ -0,0 +1,65 @@ +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() \ No newline at end of file