Files
task-6a1d75c5fd30e81cf3126ae7/main.py
T

179 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
# main.py
# FAQ bot using deepagents, ChromaDB and a single MCPstyle HTTP tool.
# The agent decides whether to query the local knowledge base or the
# external metadata service and annotates the answer with a `source` field.
"""
import os
import asyncio
import json
from pathlib import Path
from typing import List, Dict
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_core.messages import HumanMessage, SystemMessage
from langchain.tools import tool
from deepagents import create_deep_agent
from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
# Load API key from .env or environment variable
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
raise RuntimeError("OPENAI_API_KEY not set")
# LLM configuration OpenRouter
llm = ChatOpenAI(
model="openai/gpt-oss-20b:free",
base_url="https://openrouter.ai/api/v1",
api_key=OPENAI_API_KEY,
temperature=0.0,
)
# Embeddings OpenRouter
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
base_url="https://openrouter.ai/api/v1",
api_key=OPENAI_API_KEY,
)
# ---------------------------------------------------------------------------
# ChromaDB setup
# ---------------------------------------------------------------------------
CHROMA_PATH = Path("./chroma_faq")
CHROMA_COLLECTION = "faq_collection"
vector_store = Chroma(
collection_name=CHROMA_COLLECTION,
embedding_function=embeddings,
persist_directory=str(CHROMA_PATH),
)
# Load markdown files into Chroma if not already persisted
if not CHROMA_PATH.exists() or not list(CHROMA_PATH.iterdir()):
def load_faq_to_chroma(md_dir: str = "data"):
md_path = Path(md_dir)
docs: List[Document] = []
for file in md_path.glob("*.md"):
text = file.read_text(encoding="utf-8")
docs.append(Document(page_content=text, metadata={"source": file.name}))
vector_store.add_documents(docs)
vector_store.persist()
load_faq_to_chroma()
# ---------------------------------------------------------------------------
# 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(d.page_content for d in docs)
# MCPstyle tool simple HTTP GET to a local JSON file
# For the purpose of this assignment we use a static JSON file
# located at ./meta/course_meta.json
@tool
def fetch_course_meta(query: str) -> str:
"""Return metadata that matches the query from a local JSON file."""
meta_path = Path("./meta/course_meta.json")
if not meta_path.exists():
return "Metadata file not found."
data = json.loads(meta_path.read_text(encoding="utf-8"))
# Very naive matching: return any entry where the query is a substring
matches = [item for item in data if query.lower() in item.get("title", "").lower()]
if not matches:
return "No matching metadata found."
return json.dumps(matches, indent=2)
# ---------------------------------------------------------------------------
# Backend for deepagents
# ---------------------------------------------------------------------------
backend = CompositeBackend([
LocalShellBackend(workspace_dir="./workspace"),
FilesystemBackend(),
])
# ---------------------------------------------------------------------------
# Agent definition
# ---------------------------------------------------------------------------
# System prompt instructs the agent to choose the appropriate tool and
# to annotate the answer with a `source` field.
SYSTEM_PROMPT = (
"You are an FAQ assistant for a course.\n"
"If the user asks about course materials, use the `search_course_docs` tool.\n"
"If the user asks about schedule or metadata, use the `fetch_course_meta` tool.\n"
"Respond in JSON format with two fields: `answer` (string) and `source` (either `chroma` or `mcp_meta`).\n"
"Do not call both tools unless absolutely necessary."
)
agent = create_deep_agent(
model=llm,
tools=[search_course_docs, fetch_course_meta],
backend=backend,
system_prompt=SYSTEM_PROMPT,
)
# ---------------------------------------------------------------------------
# CLI helpers
# ---------------------------------------------------------------------------
PRESET_QUESTIONS = [
"What topics are covered in the first lecture?", # chroma
"How can I access the lecture slides?", # chroma
"What is the schedule for the next week?", # mcp_meta
]
async def run_interactive():
print("FAQ Bot type your question (or 'exit' to quit).\n")
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"}},
)
# The agent returns a list of messages; the last is the assistant
assistant_msg = result["messages"][-1].content
try:
data = json.loads(assistant_msg)
print(f"\nAnswer: {data['answer']}\nSource: {data['source']}\n")
except Exception:
print("\nUnexpected response format:\n", assistant_msg)
async def run_presets():
for q in PRESET_QUESTIONS:
print(f"\nQuestion: {q}")
result = await agent.ainvoke(
{"messages": [HumanMessage(content=q)]},
{"configurable": {"thread_id": "session-1"}},
)
assistant_msg = result["messages"][-1].content
try:
data = json.loads(assistant_msg)
print(f"Answer: {data['answer']}\nSource: {data['source']}")
except Exception:
print("Unexpected response format:\n", assistant_msg)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="FAQ Bot CLI")
parser.add_argument("--presets", action="store_true", help="Run preset questions")
args = parser.parse_args()
if args.presets:
asyncio.run(run_presets())
else:
asyncio.run(run_interactive())