import os import asyncio from langchain_openai import ChatOpenAI, OpenAIEmbeddings 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 from langchain_core.messages import HumanMessage from tavily import TavilySearchResults # --------------------- # Configuration # --------------------- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") TAVILY_API_KEY = os.getenv("TAVILY_API_KEY") # --------------------- # Vector store utilities # --------------------- def create_vectorstore(persist_directory: str = "./chroma_db") -> Chroma: """Create or load a Chroma vector store with OpenAI embeddings.""" embeddings = OpenAIEmbeddings( model="text-embedding-3-small", base_url="https://openrouter.ai/api/v1", api_key=OPENAI_API_KEY, ) return Chroma( collection_name="knowledge", embedding_function=embeddings, persist_directory=persist_directory, ) def load_documents(directory: str, vectorstore: Chroma) -> None: """Load .txt and .md files from *directory* into *vectorstore* using chunking.""" splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200) docs = [] 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() # Create Document objects docs.extend( [Document(page_content=chunk, metadata={"source": path}) for chunk in splitter.split_text(text)] ) if docs: vectorstore.add_documents(docs) vectorstore.persist() # --------------------- # Tools # --------------------- vectorstore = create_vectorstore() # Ensure we have some data loaded – load from ./documents if collection empty if len(vectorstore.get_all_documents()) == 0: load_documents("./documents", vectorstore) @tool def search_local_kb(query: str, top_k: int = 3) -> str: """Search the local knowledge base for relevant passages.""" docs = vectorstore.similarity_search(query, k=top_k) if not docs: return "[Local KB] No relevant information found." result = "\n\n".join([f"{d.metadata.get('source', 'unknown')}\n{d.page_content}" for d in docs]) return f"[Local KB]\n{result}" @tool def web_search(query: str) -> str: """Perform a web search using Tavily and return top results.""" results = TavilySearchResults(query=query, max_results=3, api_key=TAVILY_API_KEY) if not results.results: return "[Web Search] No results found." snippets = [] for r in results.results: snippets.append(f"{r.get('title', 'No title')}\n{r.get('content', 'No content')}\nURL: {r.get('url', '')}") return f"[Web Search]\n\n".join(snippets) # --------------------- # Agent setup # --------------------- llm = ChatOpenAI( model="openai/gpt-oss-20b:free", base_url="https://openrouter.ai/api/v1", api_key=OPENAI_API_KEY, temperature=0.0, ) backend = CompositeBackend([ LocalShellBackend(workspace_dir="./workspace"), FilesystemBackend(), ]) system_prompt = ( "You are a helpful RAG agent. For questions about local documents, use the tool `search_local_kb`. " "For current news or facts that may not be in your local knowledge base, use `web_search`. " "Return the answer prefixed with either `[Local KB]` or `[Web Search]` to indicate the source." ) agent = create_deep_agent( model=llm, tools=[search_local_kb, web_search], backend=backend, system_prompt=system_prompt, ) # --------------------- # CLI loop # --------------------- async def main(): print("RAG Agent ready. Type your question (or 'exit' to quit).") while True: user_input = input("\nQuery: ") if user_input.lower() in {"exit", "quit", "q"}: print("Goodbye!") break # Invoke agent result = await agent.ainvoke( {"messages": [HumanMessage(content=user_input)]}, {"configurable": {"thread_id": "session-1"}}, ) # The agent returns a list of messages; get the last one reply = result["messages"][-1].content print(f"\n{reply}") if __name__ == "__main__": asyncio.run(main())