import os import asyncio from pathlib import Path from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_chroma import Chroma from langchain_core.documents import Document from langchain.tools import tool from deepagents import create_deep_agent from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend from langchain_core.messages import HumanMessage # --------------------- Configuration --------------------- BASE_DIR = Path(__file__).parent DATA_DIR = BASE_DIR / "data" CHROMA_DIR = BASE_DIR / "chroma_faq" MOCK_META_FILE = BASE_DIR / "course_meta.json" # --------------------- LLM and Embeddings --------------------- llm = ChatOpenAI( model="openai/gpt-oss-20b:free", base_url="https://openrouter.ai/api/v1", api_key=os.getenv("OPENAI_API_KEY"), temperature=0.0, ) embeddings = OpenAIEmbeddings( model="text-embedding-3-small", base_url="https://openrouter.ai/api/v1", api_key=os.getenv("OPENAI_API_KEY"), ) # --------------------- Chroma Vector Store --------------------- vector_store = Chroma( collection_name="faq_collection", embedding_function=embeddings, persist_directory=str(CHROMA_DIR), ) # --------------------- Tools --------------------- @tool def search_course_docs(query: str, k: int = 3) -> str: """Search the local FAQ collection for relevant passages.""" docs = vector_store.similarity_search(query, k=k) if not docs: return "No relevant information found in the course materials." return "\n\n---\n\n".join(f"**{doc.metadata.get('title', 'Document')}**\n{doc.page_content}" for doc in docs) @tool def fetch_course_meta(query: str) -> str: """Mock MCP-style tool that returns course metadata. In production this would be an HTTP call to an MCP server. Here we read a local JSON file for simplicity. """ import json if not MOCK_META_FILE.exists(): return "Metadata source not available." with open(MOCK_META_FILE, "r", encoding="utf-8") as f: data = json.load(f) # Simple keyword search in the metadata results = [f"{k}: {v}" for k, v in data.items() if query.lower() in k.lower() or query.lower() in str(v).lower()] return "\n".join(results) if results else "No metadata matches your query." # --------------------- Backend --------------------- backend = CompositeBackend([ LocalShellBackend(workspace_dir="./workspace"), FilesystemBackend(), ]) # --------------------- Agent --------------------- agent = create_deep_agent( model=llm, tools=[search_course_docs, fetch_course_meta], backend=backend, system_prompt=( "You are a helpful FAQ bot for the course.\n" "Use the search_course_docs tool for questions about lecture materials.\n" "Use the fetch_course_meta tool for questions about schedule or metadata.\n" "Do not call both tools unless absolutely necessary.\n" "In your final answer, prefix the source with 'source: chroma' or 'source: mcp_meta'." ), ) # --------------------- Data Loading --------------------- async def load_faq_to_chroma(): """Load all .md files from data/ into the Chroma collection.""" if not DATA_DIR.exists(): print("Data directory not found.") return docs = [] for md_file in DATA_DIR.glob("*.md"): text = md_file.read_text(encoding="utf-8") docs.append(Document(page_content=text, metadata={"title": md_file.stem})) if docs: vector_store.add_documents(docs) vector_store.persist() print(f"Loaded {len(docs)} documents into Chroma.") else: print("No markdown files found in data/.") # --------------------- CLI --------------------- PRESET_QUESTIONS = [ "What is the deadline for the final project?", # chroma "Explain the concept of tokenization in NLP.", # chroma "When is the next lecture scheduled?", # mcp_meta ] async def run_cli(): await load_faq_to_chroma() print("\n--- FAQ Bot CLI ---\n") for i, q in enumerate(PRESET_QUESTIONS, 1): print(f"{i}. {q}") print("\nEnter your own question (or 'exit' to quit):") while True: user_input = input("> ") if user_input.lower() in {"exit", "quit"}: break result = await agent.ainvoke( {"messages": [HumanMessage(content=user_input)]}, {"configurable": {"thread_id": "session-1"}}, ) print(result["messages"][-1].content) if __name__ == "__main__": asyncio.run(run_cli())