import os import asyncio import json 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 langchain_text_splitter import RecursiveCharacterTextSplitter from deepagents import create_deep_agent from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend from langchain_core.messages import HumanMessage # ---------- LLM ---------- 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, ) # ---------- Backend ---------- backend = CompositeBackend( [ LocalShellBackend(workspace_dir="./workspace"), FilesystemBackend(), ] ) # ---------- Embeddings ---------- embeddings = OpenAIEmbeddings( model="text-embedding-3-small", base_url="https://openrouter.ai/api/v1", api_key=os.getenv("OPENAI_API_KEY"), ) # ---------- Vector Store ---------- vector_store = Chroma( collection_name="faq", embedding_function=embeddings, persist_directory="./chroma_faq", ) # ---------- Load FAQ into Chroma ---------- def load_faq_to_chroma() -> None: """Read .md files from data/ and add them to the Chroma collection.""" data_dir = Path("data") if not data_dir.exists(): return splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50) docs = [] for md_file in data_dir.glob("*.md"): content = md_file.read_text(encoding="utf-8") chunks = splitter.split_text(content) for i, chunk in enumerate(chunks): docs.append( Document( page_content=chunk, metadata={"source": md_file.name, "chunk": i}, ) ) if docs: vector_store.add_documents(docs) vector_store.persist() # ---------- Tools ---------- @tool def search_course_docs(query: str, k: int = 3) -> str: """Search the knowledge base for relevant information.""" docs = vector_store.similarity_search(query, k=k) return "\n\n".join(d.page_content for d in docs) if docs else "No results found." # Load meta data once META_PATH = Path("meta.json") if META_PATH.exists(): META_DATA = json.loads(META_PATH.read_text(encoding="utf-8")) else: META_DATA = {} @tool def fetch_course_meta(query: str) -> str: """Return course metadata (schedule, etc.).""" # Simple lookup: return the whole meta if query matches a key for key, value in META_DATA.items(): if key.lower() in query.lower(): return f"{key}: {value}" # Fallback: return all metadata return json.dumps(META_DATA, indent=2) # ---------- Agent ---------- system_prompt = ( "You are a helpful FAQ bot. Use search_course_docs for questions about course materials. " "Use fetch_course_meta for questions about schedule or metadata. " "Do not call both tools unless necessary. " "In your answer, indicate source: chroma or mcp_meta." ) agent = create_deep_agent( model=llm, tools=[search_course_docs, fetch_course_meta], backend=backend, system_prompt=system_prompt, ) # ---------- CLI ---------- PRESET_QUESTIONS = [ "What topics are covered in the introductory module?", "Explain the advanced algorithm discussed in chapter 3.", "What is the schedule for the next semester?", ] async def run_preset(): for q in PRESET_QUESTIONS: result = await agent.ainvoke( {"messages": [HumanMessage(content=q)]}, {"configurable": {"thread_id": "session-1"}}, ) print("\nQuestion:", q) print("Answer:", result["messages"][-1].content) async def interactive_loop(): print("\nEnter your question (type '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("Answer:", result["messages"][-1].content) async def main(): load_faq_to_chroma() await run_preset() await interactive_loop() if __name__ == "__main__": asyncio.run(main())