117 lines
6.5 KiB
Python
117 lines
6.5 KiB
Python
"""
|
||
# 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())
|