""" # main.py – RAG‑агент с ChromaDB и веб‑поиском (Tavily) # Используем deepagents, Ollama (LLM и эмбеддинги) и ChromaDB. # Всё в одном файле для удобства. """ import os import asyncio from pathlib import Path # ────────────────────────────────────── DeepAgents ──────────────────────── from deepagents import create_deep_agent from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend from langchain_ollama import ChatOllama from langchain_ollama import OllamaEmbeddings from langchain.tools import tool from langchain_core.messages import HumanMessage # ────────────────────────────────────── ChromaDB ──────────────────────── from langchain_chroma import Chroma from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.documents import Document # ────────────────────────────────────── Tavily ──────────────────────── from langchain_tavily import TavilySearchResults # ────────────────────────────────────── Настройки ──────────────────────── OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") # не используется, но оставляем для совместимости TAVILY_API_KEY = os.getenv("TAVILY_API_KEY") if not TAVILY_API_KEY: raise RuntimeError("Требуется переменная окружения TAVILY_API_KEY") # Папки CHROMA_DIR = Path("./chroma_db") DOCS_DIR = Path("./documents") WORKSPACE_DIR = Path("./workspace") # ────────────────────────────────────── LLM и Embeddings ──────────────────────── llm = ChatOllama(model="llama3", temperature=0.0) embeddings = OllamaEmbeddings(model="nomic-embed-text") # ────────────────────────────────────── VectorStore ──────────────────────── vector_store = Chroma( collection_name="knowledge", persist_directory=str(CHROMA_DIR), embedding_function=embeddings, ) # ────────────────────────────────────── Загрузка документов ──────────────────────── def load_documents(directory: Path, store: Chroma, chunk_size: int = 1000, chunk_overlap: int = 200): """Читает .txt/.md, разбивает на чанки и добавляет в Chroma.""" if not directory.exists(): print(f"Документы не найдены в {directory}") return splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap) for file_path in directory.glob("**/*.*"): if file_path.suffix.lower() not in {".txt", ".md"}: continue text = file_path.read_text(encoding="utf-8") docs = splitter.split_text(text) documents = [Document(page_content=chunk, metadata={"source": str(file_path)}) for chunk in docs] store.add_documents(documents) print("Документы загружены в Chroma.") # Если база пустая – загрузим if not CHROMA_DIR.exists() or not any(CHROMA_DIR.iterdir()): load_documents(DOCS_DIR, vector_store) # ────────────────────────────────────── Инструменты ──────────────────────── @tool def search_local_kb(query: str, top_k: int = 3) -> str: """Семантический поиск в локальной базе знаний.""" docs = vector_store.similarity_search(query, k=top_k) if not docs: return "No results in local knowledge base." return "\n---\n".join(f"{d.metadata.get('source')}\n{d.page_content[:500]}" for d in docs) @tool def web_search(query: str) -> str: """Веб‑поиск через Tavily.""" tavily = TavilySearchResults(api_key=TAVILY_API_KEY, max_results=3) results = tavily.run(query) return "\n---\n".join(f"{r['title']}\n{r['content']}" for r in results) # ────────────────────────────────────── Backend ──────────────────────── backend = CompositeBackend([ LocalShellBackend(workspace_dir=str(WORKSPACE_DIR)), FilesystemBackend(), ]) # ────────────────────────────────────── Агент ──────────────────────── agent = create_deep_agent( model=llm, tools=[search_local_kb, web_search], backend=backend, system_prompt="Вы – RAG‑агент. При запросе о локальных документах используйте search_local_kb, для актуальных новостей – web_search. В ответе указывайте источник: chromadb или tavily.", ) # ────────────────────────────────────── Чат‑цикл ──────────────────────── async def chat_loop(): print("RAG‑агент готов. Введите запрос (exit для выхода).") while True: user_input = input("Запрос: ") if user_input.lower() in {"exit", "quit"}: print("Выход.") break result = await agent.ainvoke( {"messages": [HumanMessage(content=user_input)]}, {"configurable": {"thread_id": "session-1"}}, ) # Предполагаем, что агент возвращает сообщение с полем source в metadata response = result["messages"][-1].content print("\nОтвет:\n", response) print("\n---\n") if __name__ == "__main__": asyncio.run(chat_loop())