Files
task-6a1d75c5fd30e81cf3126ae7/main.py
T

164 lines
6.0 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.
import os
import asyncio
import json
from pathlib import Path
from typing import List
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_core.messages import HumanMessage
from langchain.tools import tool
from deepagents import create_deep_agent
from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
raise RuntimeError("OPENAI_API_KEY not set in environment")
# LLM via 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 via OpenRouter
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
base_url="https://openrouter.ai/api/v1",
api_key=OPENAI_API_KEY,
)
# ---------------------------------------------------------------------------
# Chroma persistence
# ---------------------------------------------------------------------------
CHROMA_DIR = Path("./chroma_faq")
CHROMA_DIR.mkdir(parents=True, exist_ok=True)
vector_store = Chroma(
collection_name="faq_collection",
embedding_function=embeddings,
persist_directory=str(CHROMA_DIR),
)
# ---------------------------------------------------------------------------
# Utility: load markdown files into Chroma
# ---------------------------------------------------------------------------
@tool
def load_faq_to_chroma() -> str:
"""Load all .md files from data/ into the Chroma vector store.
This tool is idempotent it will overwrite existing collection.
"""
data_dir = Path("data")
if not data_dir.exists():
return "Data directory not found."
docs: List[Document] = []
for md_file in data_dir.glob("*.md"):
text = md_file.read_text(encoding="utf-8")
docs.append(Document(page_content=text, metadata={"source": md_file.name}))
if docs:
vector_store.delete_collection()
vector_store.add_documents(docs)
vector_store.persist()
return f"Loaded {len(docs)} documents into Chroma."
return "No markdown files found."
# ---------------------------------------------------------------------------
# Tool: search knowledge base
# ---------------------------------------------------------------------------
@tool
def search_course_docs(query: str, k: int = 3) -> str:
"""Search the FAQ collection for relevant passages.
Returns a string with the top k passages and a source tag.
"""
docs = vector_store.similarity_search(query, k=k)
if not docs:
return "No relevant information found in the FAQ."
results = "\n\n".join(f"{i+1}. {doc.page_content}" for i, doc in enumerate(docs))
return f"source: chroma\n{results}"
# ---------------------------------------------------------------------------
# MCP-style tool: fetch course metadata
# ---------------------------------------------------------------------------
# For simplicity we use a static JSON file in the repo. In production this
# would be an HTTP call to an MCP server.
METADATA_FILE = Path("course_meta.json")
@tool
def fetch_course_meta(query: str) -> str:
"""Return metadata that matches the query.
The function performs a simple keyword search in the static JSON.
"""
if not METADATA_FILE.exists():
return "Metadata file not found."
data = json.loads(METADATA_FILE.read_text(encoding="utf-8"))
matches = [item for item in data if query.lower() in item.get("title", "").lower()]
if not matches:
return "No metadata matches the query."
return f"source: mcp_meta\n" + json.dumps(matches, indent=2)
# ---------------------------------------------------------------------------
# Backend setup
# ---------------------------------------------------------------------------
backend = CompositeBackend([
LocalShellBackend(workspace_dir="./workspace"),
FilesystemBackend(),
])
# ---------------------------------------------------------------------------
# Agent definition
# ---------------------------------------------------------------------------
agent = create_deep_agent(
model=llm,
tools=[load_faq_to_chroma, search_course_docs, fetch_course_meta],
backend=backend,
system_prompt=(
"You are a helpful FAQ bot for a course. "
"Use the search_course_docs tool for questions about lecture materials. "
"Use fetch_course_meta for questions about schedule or metadata. "
"Do not call both tools unless necessary. "
"Always prefix your answer with the source tag (chroma or mcp_meta)."
),
)
# ---------------------------------------------------------------------------
# CLI helpers
# ---------------------------------------------------------------------------
PRESET_QUESTIONS = [
"What is the deadline for the final project?", # should hit metadata
"Explain the concept of polymorphism in OOP.", # should hit FAQ
"How many lectures are there in the first module?", # metadata
]
async def run_agent(question: str, thread_id: str = "session-1"):
result = await agent.ainvoke(
{"messages": [HumanMessage(content=question)]},
{"configurable": {"thread_id": thread_id}},
)
return result["messages"][-1].content
async def main():
# Ensure FAQ is loaded once
await agent.ainvoke(
{"messages": [HumanMessage(content="load_faq_to_chroma()")]},
{"configurable": {"thread_id": "init"}},
)
print("\n--- Preset questions ---")
for q in PRESET_QUESTIONS:
ans = await run_agent(q)
print(f"Q: {q}\nA: {ans}\n")
print("\n--- Interactive mode (type 'exit' to quit) ---")
while True:
user_input = input("> ")
if user_input.lower() in {"exit", "quit"}:
break
ans = await run_agent(user_input)
print(ans)
if __name__ == "__main__":
asyncio.run(main())