import os import asyncio from pathlib import Path from typing import List from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_core.documents import Document from langchain_core.messages import HumanMessage from langchain.tools import tool from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_qdrant import QdrantVectorStore from deepagents import create_deep_agent from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- QDRANT_URL = os.getenv("QDRANT_URL", "http://localhost:6333") QDRANT_COLLECTION = "knowledge_base" EMBEDDING_MODEL = "text-embedding-3-small" LLM_MODEL = "openai/gpt-oss-20b:free" BASE_URL = "https://openrouter.ai/api/v1" API_KEY = os.getenv("OPENAI_API_KEY") # --------------------------------------------------------------------------- # Embeddings and Vector Store # --------------------------------------------------------------------------- embeddings = OpenAIEmbeddings( model=EMBEDDING_MODEL, base_url=BASE_URL, api_key=API_KEY, ) vector_store = QdrantVectorStore( url=QDRANT_URL, collection_name=QDRANT_COLLECTION, embedding_function=embeddings, ) # --------------------------------------------------------------------------- # Text splitter # --------------------------------------------------------------------------- text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200) # --------------------------------------------------------------------------- # Tools # --------------------------------------------------------------------------- @tool def search_knowledge_base(query: str, max_results: int = 3) -> str: """Semantic search in the knowledge base.""" docs: List[Document] = vector_store.similarity_search(query, k=max_results) if not docs: return "No relevant documents found." return "\n\n---\n\n".join(doc.page_content for doc in docs) @tool def add_to_knowledge_base(content: str, title: str = "untitled") -> str: """Add a new document to the knowledge base.""" # Split content into chunks chunks = text_splitter.split_text(content) docs = [Document(page_content=chunk, metadata={"title": title}) for chunk in chunks] vector_store.add_documents(docs) return f"Added {len(docs)} chunks for title '{title}'." # --------------------------------------------------------------------------- # Backend setup # --------------------------------------------------------------------------- backend = CompositeBackend([ LocalShellBackend(workspace_dir="./workspace"), FilesystemBackend(), ]) # --------------------------------------------------------------------------- # LLM # --------------------------------------------------------------------------- llm = ChatOpenAI( model=LLM_MODEL, base_url=BASE_URL, api_key=API_KEY, temperature=0.0, ) # --------------------------------------------------------------------------- # Agent # --------------------------------------------------------------------------- agent = create_deep_agent( model=llm, tools=[search_knowledge_base, add_to_knowledge_base], backend=backend, system_prompt="You are a helpful assistant with access to a knowledge base. Use the provided tools to search and add information.", ) # --------------------------------------------------------------------------- # Document loader for initialization # --------------------------------------------------------------------------- async def load_documents_from_dir(directory: str): """Load all text files from a directory into the vector store.""" dir_path = Path(directory) if not dir_path.is_dir(): print(f"Directory {directory} does not exist.") return for file_path in dir_path.rglob("*.txt"): content = file_path.read_text(encoding="utf-8") title = file_path.stem await agent.ainvoke( {"messages": [HumanMessage(content=f"/add {title}")], "content": content}, {"configurable": {"thread_id": f"init-{file_path.name}"}}, ) print("Initialization complete.") # --------------------------------------------------------------------------- # Interactive CLI # --------------------------------------------------------------------------- async def interactive_loop(): print("Welcome to the RAG agent. Commands: /add , /search <query>, /quit") while True: user_input = input("> ") if user_input.strip() == "/quit": print("Goodbye!") break if user_input.startswith("/add "): parts = user_input.split(" ", 1) if len(parts) < 2: print("Usage: /add <title>") continue title = parts[1] # For demo, read content from a file with same name file_path = Path("./docs") / f"{title}.txt" if not file_path.exists(): print(f"File {file_path} not found.") continue content = file_path.read_text(encoding="utf-8") response = await agent.ainvoke( {"messages": [HumanMessage(content=f"/add {title}")], "content": content}, {"configurable": {"thread_id": f"add-{title}"}}, ) print(response["messages"][-1].content) elif user_input.startswith("/search "): query = user_input[len("/search "):] response = await agent.ainvoke( {"messages": [HumanMessage(content=f"/search {query}")]}, {"configurable": {"thread_id": f"search-{query}"}}, ) print(response["messages"][-1].content) else: # Regular message to agent response = await agent.ainvoke( {"messages": [HumanMessage(content=user_input)]}, {"configurable": {"thread_id": "interactive"}}, ) print(response["messages"][-1].content) # --------------------------------------------------------------------------- # Main entry point # --------------------------------------------------------------------------- async def main(): # Optional: load initial documents # await load_documents_from_dir("./initial_docs") await interactive_loop() if __name__ == "__main__": asyncio.run(main())