105 lines
3.7 KiB
Python
105 lines
3.7 KiB
Python
import os
|
|
import asyncio
|
|
from dotenv import load_dotenv
|
|
from langchain_ollama import ChatOllama
|
|
from langchain_ollama import OllamaEmbeddings
|
|
from langchain_chroma import Chroma
|
|
from langchain_core.documents import Document
|
|
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
|
from langchain.tools import tool
|
|
from deepagents import create_deep_agent
|
|
from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
# ---------- Vector Store ----------
|
|
|
|
def create_vectorstore(persist_directory="./chroma_db"):
|
|
"""Create a Chroma vector store with Ollama embeddings."""
|
|
embeddings = OllamaEmbeddings(model="nomic-embed-text")
|
|
return Chroma(persist_directory=persist_directory, embedding_function=embeddings)
|
|
|
|
|
|
def load_documents(directory, vectorstore):
|
|
"""Load .txt and .md files from *directory*, chunk them, and add to *vectorstore*.
|
|
The function preserves the file name in metadata for later reference.
|
|
"""
|
|
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
|
|
for root, _, files in os.walk(directory):
|
|
for fname in files:
|
|
if fname.lower().endswith(('.txt', '.md')):
|
|
path = os.path.join(root, fname)
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
text = f.read()
|
|
docs = splitter.split_text(text)
|
|
documents = [Document(page_content=chunk, metadata={"source": fname}) for chunk in docs]
|
|
vectorstore.add_documents(documents)
|
|
|
|
# ---------- Tools ----------
|
|
|
|
vectorstore = create_vectorstore()
|
|
|
|
@tool
|
|
def search_local_kb(query: str, top_k: int = 3) -> str:
|
|
"""Semantic search in the local knowledge base (ChromaDB)."""
|
|
retriever = vectorstore.as_retriever(search_kwargs={"k": top_k})
|
|
docs = retriever.get_relevant_documents(query)
|
|
if not docs:
|
|
return "No relevant local knowledge found."
|
|
return "\n---\n".join([f"{doc.metadata.get('source', 'unknown')}\n{doc.page_content}" for doc in docs])
|
|
|
|
@tool
|
|
def web_search(query: str) -> str:
|
|
"""Web search using Tavily."""
|
|
from tavily import TavilyClient
|
|
client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
|
|
results = client.search(query, max_results=3)
|
|
if not results:
|
|
return "No web results found."
|
|
return "\n---\n".join([f"{r['title']}\n{r['content']}" for r in results])
|
|
|
|
# ---------- Agent ----------
|
|
|
|
llm = ChatOllama(model="llama3", temperature=0.0)
|
|
|
|
backend = CompositeBackend([
|
|
LocalShellBackend(workspace_dir="./workspace"),
|
|
FilesystemBackend(),
|
|
])
|
|
|
|
system_prompt = (
|
|
"You are an AI assistant with access to two tools: "
|
|
"search_local_kb for local knowledge and web_search for up-to-date information. "
|
|
"When answering a user query, first decide which tool is appropriate. "
|
|
"If the answer can be derived from the local documents, use search_local_kb; "
|
|
"otherwise use web_search. "
|
|
"Always indicate the source of the information in your response: "
|
|
"[Local KB] or [Web Search]."
|
|
)
|
|
|
|
agent = create_deep_agent(
|
|
model=llm,
|
|
tools=[search_local_kb, web_search],
|
|
backend=backend,
|
|
system_prompt=system_prompt,
|
|
)
|
|
|
|
async def main():
|
|
print("RAG Agent ready. Type 'exit' to quit.")
|
|
while True:
|
|
user_input = input("\nЗапрос: ")
|
|
if user_input.strip().lower() == "exit":
|
|
print("Goodbye!")
|
|
break
|
|
result = await agent.ainvoke(
|
|
{"messages": [{"role": "user", "content": user_input}]},
|
|
{"configurable": {"thread_id": "session-1"}},
|
|
)
|
|
# The last message is the assistant's reply
|
|
reply = result["messages"][-1].content
|
|
print(f"\n{reply}")
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|