Обновить solution.py

This commit is contained in:
2026-05-28 09:54:46 +00:00
parent 2712fb2801
commit 74e7fa3b76
+40 -26
View File
@@ -2,8 +2,11 @@
rag_agent.py — AI-агент с RAG-памятью на базе Ollama + Qdrant. rag_agent.py — AI-агент с RAG-памятью на базе Ollama + Qdrant.
Стек: langchain-ollama, langchain-qdrant, langgraph, qdrant-client, rich Стек: 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 import os
@@ -15,7 +18,7 @@ from langchain_qdrant import QdrantVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.tools import tool from langchain_core.tools import tool
from langchain_core.documents import Document 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 import QdrantClient as _QdrantClient
from qdrant_client.http.models import Distance, VectorParams 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_HOST = os.getenv("QDRANT_HOST", "localhost")
QDRANT_PORT = int(os.getenv("QDRANT_PORT", "6333")) QDRANT_PORT = int(os.getenv("QDRANT_PORT", "6333"))
COLLECTION_NAME = os.getenv("QDRANT_COLLECTION", "rag_collection") COLLECTION_NAME = os.getenv("QDRANT_COLLECTION", "rag_collection")
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "nomic-embed-text") EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "nomic-embed-text")
LLM_MODEL = os.getenv("LLM_MODEL", "llama3") LLM_MODEL = os.getenv("LLM_MODEL", "llama3")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Векторное хранилище (Qdrant + Ollama-эмбеддинги) # Векторное хранилище (Qdrant + Ollama-эмбеддинги)
@@ -65,7 +68,7 @@ class RagVectorStore:
def search(self, query: str, k: int = 5) -> List[tuple]: def search(self, query: str, k: int = 5) -> List[tuple]:
# similarity_search_with_relevance_scores возвращает score в [0..1], # 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) results = self.store.similarity_search_with_relevance_scores(query, k=k)
return [(doc.page_content, score) for doc, score in results] 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) llm = ChatOllama(model=LLM_MODEL, temperature=0.3)
@@ -159,7 +162,7 @@ SYSTEM_PROMPT = """Ты — интеллектуальный AI-ассистен
5. Отвечай на том же языке, на котором задан вопрос. 5. Отвечай на том же языке, на котором задан вопрос.
""" """
agent = create_react_agent( agent = create_agent(
model=llm, model=llm,
tools=[search_knowledge_base, add_to_knowledge_base], tools=[search_knowledge_base, add_to_knowledge_base],
prompt=SYSTEM_PROMPT, prompt=SYSTEM_PROMPT,
@@ -176,7 +179,7 @@ def run_agent(user_input: str) -> str:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Загрузка документов из директории # Клиент инициализации: загрузка документов из директории
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def load_documents_from_directory(directory: str) -> int: def load_documents_from_directory(directory: str) -> int:
@@ -185,6 +188,9 @@ def load_documents_from_directory(directory: str) -> int:
Возвращает количество загруженных файлов. Возвращает количество загруженных файлов.
""" """
path = Path(directory) path = Path(directory)
if not path.is_dir():
raise ValueError(f"Директория не найдена: {directory}")
count = 0 count = 0
for file_path in path.rglob("*"): for file_path in path.rglob("*"):
if file_path.suffix.lower() in {".txt", ".md"}: 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__": if __name__ == "__main__":
@@ -211,10 +217,11 @@ if __name__ == "__main__":
console.print(Panel( console.print(Panel(
"[bold]RAG-агент[/bold] · Qdrant + Ollama\n\n" "[bold]RAG-агент[/bold] · Qdrant + Ollama\n\n"
"[cyan]/add <текст>[/cyan] — добавить текст в базу знаний\n" "[cyan]/add <текст>[/cyan] — добавить текст в базу знаний\n"
"[cyan]/add-file <путь>[/cyan] — загрузить файл в базу знаний\n" "[cyan]/add-file <путь>[/cyan] — загрузить файл в базу знаний\n"
"[cyan]/search <запрос>[/cyan] — прямой поиск (без агента)\n" "[cyan]/load-dir <папка>[/cyan] загрузить все .txt/.md из папки\n"
"[cyan]/quit[/cyan] — выйти", "[cyan]/search <запрос>[/cyan] — прямой поиск (без агента)\n"
"[cyan]/quit[/cyan] — выйти",
border_style="dim", border_style="dim",
)) ))
@@ -228,12 +235,12 @@ if __name__ == "__main__":
if not user_input: if not user_input:
continue continue
# /quit # ── /quit ──────────────────────────────────────────────────────────
if user_input.lower() in ("/quit", "/exit"): if user_input.lower() in ("/quit", "/exit"):
console.print("[yellow]Выход.[/yellow]") console.print("[yellow]Выход.[/yellow]")
break break
# /add-file <путь> # ── /add-file <путь> ───────────────────────────────────────────────
elif user_input.lower().startswith("/add-file "): elif user_input.lower().startswith("/add-file "):
file_path = Path(user_input[10:].strip()) file_path = Path(user_input[10:].strip())
if file_path.is_file(): if file_path.is_file():
@@ -245,25 +252,32 @@ if __name__ == "__main__":
else: else:
console.print(f"[red]Файл не найден: {file_path}[/red]") 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 "): elif user_input.lower().startswith("/add "):
raw = user_input[5:].strip() raw = user_input[5:].strip()
path = Path(raw) path = Path(raw)
if path.is_file(): if path.is_file():
# если передан путь к существующему файлу — загружаем файл
text = path.read_text(encoding="utf-8", errors="ignore") text = path.read_text(encoding="utf-8", errors="ignore")
result = add_to_knowledge_base.invoke( result = add_to_knowledge_base.invoke(
{"content": text, "title": path.name} {"content": text, "title": path.name}
) )
else: else:
# иначе сохраняем как текст напрямую
result = add_to_knowledge_base.invoke( result = add_to_knowledge_base.invoke(
{"content": raw, "title": "manual"} {"content": raw, "title": "manual"}
) )
console.print(f"[green]✓ {result}[/green]") console.print(f"[green]✓ {result}[/green]")
# /search <запрос> # ── /search <запрос> ───────────────────────────────────────────────
elif user_input.lower().startswith("/search "): elif user_input.lower().startswith("/search "):
query = user_input[8:].strip() query = user_input[8:].strip()
console.print(Rule("Результаты поиска")) console.print(Rule("Результаты поиска"))
@@ -278,11 +292,11 @@ if __name__ == "__main__":
border_style="dim", border_style="dim",
)) ))
# /search без аргумента # ── /search без аргумента ──────────────────────────────────────────
elif user_input.lower() == "/search": elif user_input.lower() == "/search":
console.print("[yellow]Укажите запрос: /search <текст>[/yellow]") console.print("[yellow]Укажите запрос: /search <текст>[/yellow]")
# запрос к агенту # ── запрос к агенту ────────────────────────────────────────────────
else: else:
console.print("[dim]Агент думает...[/dim]") console.print("[dim]Агент думает...[/dim]")
try: try: