From 74e7fa3b76e35b1b2fa53ecf1f95fb477b728f23 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: Thu, 28 May 2026 09:54:46 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20solution.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- solution.py | 66 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/solution.py b/solution.py index d780008..acefe31 100644 --- a/solution.py +++ b/solution.py @@ -2,8 +2,11 @@ rag_agent.py — AI-агент с RAG-памятью на базе Ollama + Qdrant. Стек: langchain-ollama, langchain-qdrant, langgraph, qdrant-client, rich -Установка: pip install langchain langchain-core langchain-ollama langchain-qdrant - langchain-text-splitters langgraph qdrant-client rich +Установка: + ollama pull llama3 + ollama pull nomic-embed-text + pip install langchain langchain-core langchain-ollama langchain-qdrant \ + langchain-text-splitters langgraph qdrant-client rich """ import os @@ -15,7 +18,7 @@ from langchain_qdrant import QdrantVectorStore from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.tools import tool from langchain_core.documents import Document -from langgraph.prebuilt import create_react_agent +from langgraph.prebuilt import create_react_agent as create_agent # алиас from qdrant_client import QdrantClient as _QdrantClient from qdrant_client.http.models import Distance, VectorParams @@ -23,11 +26,11 @@ 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_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-эмбеддинги) @@ -65,7 +68,7 @@ class RagVectorStore: def search(self, query: str, k: int = 5) -> List[tuple]: # similarity_search_with_relevance_scores возвращает score в [0..1], - # где 1.0 = максимальная релевантность (исправлено с with_score) + # где 1.0 = максимальная релевантность. results = self.store.similarity_search_with_relevance_scores(query, k=k) return [(doc.page_content, score) for doc, score in results] @@ -144,7 +147,7 @@ def add_to_knowledge_base(content: str, title: str = "") -> str: # --------------------------------------------------------------------------- -# Агент (create_react_agent из langgraph — актуальный API v0.3+) +# Агент (create_agent — обёртка над create_react_agent из langgraph v0.3+) # --------------------------------------------------------------------------- llm = ChatOllama(model=LLM_MODEL, temperature=0.3) @@ -159,7 +162,7 @@ SYSTEM_PROMPT = """Ты — интеллектуальный AI-ассистен 5. Отвечай на том же языке, на котором задан вопрос. """ -agent = create_react_agent( +agent = create_agent( model=llm, tools=[search_knowledge_base, add_to_knowledge_base], prompt=SYSTEM_PROMPT, @@ -176,7 +179,7 @@ def run_agent(user_input: str) -> str: # --------------------------------------------------------------------------- -# Загрузка документов из директории +# Клиент инициализации: загрузка документов из директории # --------------------------------------------------------------------------- def load_documents_from_directory(directory: str) -> int: @@ -185,6 +188,9 @@ def load_documents_from_directory(directory: str) -> int: Возвращает количество загруженных файлов. """ path = Path(directory) + if not path.is_dir(): + raise ValueError(f"Директория не найдена: {directory}") + count = 0 for file_path in path.rglob("*"): if file_path.suffix.lower() in {".txt", ".md"}: @@ -198,7 +204,7 @@ def load_documents_from_directory(directory: str) -> int: # --------------------------------------------------------------------------- -# Интерактивный CLI-клиент +# Интерактивный тестовый CLI-клиент # --------------------------------------------------------------------------- if __name__ == "__main__": @@ -211,10 +217,11 @@ if __name__ == "__main__": console.print(Panel( "[bold]RAG-агент[/bold] · Qdrant + Ollama\n\n" - "[cyan]/add <текст>[/cyan] — добавить текст в базу знаний\n" - "[cyan]/add-file <путь>[/cyan] — загрузить файл в базу знаний\n" - "[cyan]/search <запрос>[/cyan] — прямой поиск (без агента)\n" - "[cyan]/quit[/cyan] — выйти", + "[cyan]/add <текст>[/cyan] — добавить текст в базу знаний\n" + "[cyan]/add-file <путь>[/cyan] — загрузить файл в базу знаний\n" + "[cyan]/load-dir <папка>[/cyan] — загрузить все .txt/.md из папки\n" + "[cyan]/search <запрос>[/cyan] — прямой поиск (без агента)\n" + "[cyan]/quit[/cyan] — выйти", border_style="dim", )) @@ -228,12 +235,12 @@ if __name__ == "__main__": if not user_input: continue - # /quit + # ── /quit ────────────────────────────────────────────────────────── if user_input.lower() in ("/quit", "/exit"): console.print("[yellow]Выход.[/yellow]") break - # /add-file <путь> + # ── /add-file <путь> ─────────────────────────────────────────────── elif user_input.lower().startswith("/add-file "): file_path = Path(user_input[10:].strip()) if file_path.is_file(): @@ -245,25 +252,32 @@ if __name__ == "__main__": else: console.print(f"[red]Файл не найден: {file_path}[/red]") - # /add <текст> ← исправлено: теперь принимает текст напрямую, - # а не только путь к файлу + # ── /load-dir <папка> ────────────────────────────────────────────── + elif user_input.lower().startswith("/load-dir "): + directory = user_input[10:].strip() + console.print(f"[dim]Загрузка из «{directory}»...[/dim]") + try: + n = load_documents_from_directory(directory) + console.print(f"[green]✓ Загружено файлов: {n}[/green]") + except ValueError as e: + console.print(f"[red]{e}[/red]") + + # ── /add <текст или путь> ────────────────────────────────────────── elif user_input.lower().startswith("/add "): raw = user_input[5:].strip() path = Path(raw) if path.is_file(): - # если передан путь к существующему файлу — загружаем файл text = path.read_text(encoding="utf-8", errors="ignore") result = add_to_knowledge_base.invoke( {"content": text, "title": path.name} ) else: - # иначе сохраняем как текст напрямую result = add_to_knowledge_base.invoke( {"content": raw, "title": "manual"} ) console.print(f"[green]✓ {result}[/green]") - # /search <запрос> + # ── /search <запрос> ─────────────────────────────────────────────── elif user_input.lower().startswith("/search "): query = user_input[8:].strip() console.print(Rule("Результаты поиска")) @@ -278,11 +292,11 @@ if __name__ == "__main__": border_style="dim", )) - # /search без аргумента + # ── /search без аргумента ────────────────────────────────────────── elif user_input.lower() == "/search": console.print("[yellow]Укажите запрос: /search <текст>[/yellow]") - # запрос к агенту + # ── запрос к агенту ──────────────────────────────────────────────── else: console.print("[dim]Агент думает...[/dim]") try: