Files

117 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
# 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())