import os import asyncio from pathlib import Path from langchain_community.embeddings import OllamaEmbeddings from langchain_chroma import Chroma from langchain_core.documents import Document from langchain_openai import ChatOpenAI from langchain.tools import tool from deepagents import create_deep_agent from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend from langchain_core.messages import HumanMessage # --------------------------- # 1. Embeddings & Chroma setup # --------------------------- # Using OllamaEmbeddings with nomic-embed-text as required by the "Исправить" section. # The embeddings are used for both loading the FAQ and for the search tool. embeddings = OllamaEmbeddings(model="nomic-embed-text") # Persistent Chroma collection for the FAQ knowledge base. vector_store = Chroma( collection_name="faq_collection", embedding_function=embeddings, persist_directory="./chroma_faq" ) # --------------------------- # 2. Load FAQ markdown files into Chroma # --------------------------- def load_faq_to_chroma(data_dir: str = "data"): """Load all .md files from *data_dir* into the persistent Chroma collection. Each file is split into documents with a simple line‑based splitter. """ data_path = Path(data_dir) if not data_path.exists(): raise FileNotFoundError(f"Data directory {data_dir} not found") docs = [] for md_file in data_path.glob("*.md"): text = md_file.read_text(encoding="utf-8") # Simple split by double newlines to create chunks for i, chunk in enumerate(text.split("\n\n")): docs.append(Document(page_content=chunk, metadata={"source": md_file.name, "chunk": i})) vector_store.add_documents(docs) vector_store.persist() # --------------------------- # 3. Tools # --------------------------- @tool def search_course_docs(query: str, k: int = 3) -> str: """Search the FAQ knowledge base for relevant information.""" docs = vector_store.similarity_search(query, k=k) if not docs: return "No relevant information found in the FAQ." return "\n\n---\n\n".join(f"**{doc.metadata.get('source')}** (chunk {doc.metadata.get('chunk')}):\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 perform an HTTP GET to an MCP server. Here we simply return a static JSON string based on the query. """ # Simple static mapping for demo purposes meta = { "schedule": "Monday 10:00-12:00, Wednesday 14:00-16:00", "instructor": "Dr. Ivanov", "credits": "3" } key = query.lower().strip() return meta.get(key, f"No metadata found for '{query}'.") # --------------------------- # 4. Agent setup with deepagents # --------------------------- # LLM via OpenRouter as per course requirement 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 = CompositeBackend([ LocalShellBackend(workspace_dir="./workspace"), FilesystemBackend(), ]) # System prompt instructs the agent to choose the appropriate tool and to label the source. system_prompt = ( "You are a helpful FAQ assistant.\n" "When a user asks a question about course materials, use the tool `search_course_docs`.\n" "When a user asks about schedule, instructor, or credits, use the tool `fetch_course_meta`.\n" "Do not call both tools unless necessary.\n" "In your final answer, prepend the source label: `source: chroma` or `source: mcp_meta`." ) agent = create_deep_agent( model=llm, tools=[search_course_docs, fetch_course_meta], backend=backend, system_prompt=system_prompt, ) # --------------------------- # 5. CLI # --------------------------- PRESET_QUESTIONS = [ "What topics are covered in the first lecture?", "Who is the instructor for this course?", "When is the next class?" ] async def run_agent(question: str, thread_id: str = "session-1"): result = await agent.ainvoke( {"messages": [HumanMessage(content=question)]}, {"configurable": {"thread_id": thread_id}}, ) # The last message is the agent's response return result["messages"][-1].content async def main(): # Ensure FAQ is loaded load_faq_to_chroma() print("--- FAQ Bot Demo ---\n") for i, q in enumerate(PRESET_QUESTIONS, 1): print(f"Q{i}: {q}") answer = await run_agent(q, thread_id=f"demo-{i}") print(f"A{i}: {answer}\n") # Interactive mode print("Enter your own questions (type 'exit' to quit):") while True: user_input = input("> ") if user_input.lower() in {"exit", "quit"}: break answer = await run_agent(user_input, thread_id="interactive") print(answer) if __name__ == "__main__": asyncio.run(main())