Add agent.py
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import os
|
||||
import json
|
||||
import httpx
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from langchain_ollama import ChatOllama, OllamaEmbeddings
|
||||
from langchain_text_splitter import RecursiveCharacterTextSplitter
|
||||
from langchain.vectorstores import Chroma
|
||||
from langchain.schema import Document
|
||||
from langchain import LLMChain, PromptTemplate
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Constants
|
||||
CHROMA_PATH = Path("chromadb")
|
||||
DATA_PATH = Path("data")
|
||||
|
||||
# ---------- Data Loader ----------
|
||||
|
||||
def load_md_files(data_dir: Path) -> list[Document]:
|
||||
docs = []
|
||||
for file_path in data_dir.rglob("*.md"):
|
||||
try:
|
||||
text = file_path.read_text(encoding="utf-8")
|
||||
except Exception as e:
|
||||
print(f"Error reading {file_path}: {e}")
|
||||
continue
|
||||
docs.append(Document(page_content=text, metadata={"source": str(file_path)}))
|
||||
return docs
|
||||
|
||||
|
||||
def chunk_documents(docs: list[Document], chunk_size: int = 1000, chunk_overlap: int = 200) -> list[Document]:
|
||||
splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
|
||||
all_chunks = []
|
||||
for doc in docs:
|
||||
chunks = splitter.split_text(doc.page_content)
|
||||
for i, chunk in enumerate(chunks):
|
||||
meta = dict(doc.metadata)
|
||||
meta["chunk_index"] = i
|
||||
all_chunks.append(Document(page_content=chunk, metadata=meta))
|
||||
return all_chunks
|
||||
|
||||
|
||||
def init_vectorstore() -> Chroma:
|
||||
# If vectorstore exists, load it; otherwise build and persist.
|
||||
if CHROMA_PATH.exists() and any(CHROMA_PATH.iterdir()):
|
||||
return Chroma(persist_directory=str(CHROMA_PATH), embedding_function=OllamaEmbeddings(model="nomic-embed-text"))
|
||||
# Build
|
||||
md_docs = load_md_files(DATA_PATH)
|
||||
if not md_docs:
|
||||
raise RuntimeError("No .md files found in data/")
|
||||
chunks = chunk_documents(md_docs)
|
||||
vectorstore = Chroma.from_documents(chunks, OllamaEmbeddings(model="nomic-embed-text"), persist_directory=str(CHROMA_PATH))
|
||||
return vectorstore
|
||||
|
||||
# ---------- MCP Tool ----------
|
||||
|
||||
def fetch_course_meta(query: str) -> str:
|
||||
"""MCP-style HTTP GET returning JSON.
|
||||
Returns JSON string or error message.
|
||||
"""
|
||||
url = "https://api.example.com/course_meta"
|
||||
params = {"query": query}
|
||||
try:
|
||||
response = httpx.get(url, params=params, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
# ---------- Agent Logic ----------
|
||||
SYSTEM_PROMPT = """
|
||||
You are a FAQ assistant. Use the provided tools to answer questions.
|
||||
- If the question is about course metadata, use the 'fetch_course_meta' tool.
|
||||
- For other questions, search the local FAQ database.
|
||||
Return the answer and a source field which is either 'chroma' or 'mcp_meta'.
|
||||
"""
|
||||
|
||||
# LLM
|
||||
llm = ChatOllama(model="llama3", base_url=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1"))
|
||||
|
||||
# Retrieval chain for chroma
|
||||
vectorstore = init_vectorstore()
|
||||
retriever = vectorstore.as_retriever(search_kwargs={"k":5})
|
||||
|
||||
# Prompt template for retrieval
|
||||
RETRIEVE_PROMPT = PromptTemplate(
|
||||
input_variables=["question"],
|
||||
template="Answer the question based on the following context. If no context is relevant, answer directly. Context:\n{context}\nQuestion: {question}",
|
||||
)
|
||||
|
||||
# Retrieval chain (LLM + retriever)
|
||||
retrieval_chain = LLMChain(llm=llm, prompt=RETRIEVE_PROMPT)
|
||||
|
||||
# Function to get answer from chroma
|
||||
def answer_from_chroma(question: str) -> tuple[str, str]:
|
||||
docs = retriever.get_relevant_documents(question)
|
||||
if not docs:
|
||||
# Fallback to MCP
|
||||
meta_json = fetch_course_meta(question)
|
||||
return (meta_json, "mcp_meta")
|
||||
context = "\n".join([doc.page_content for doc in docs])
|
||||
try:
|
||||
answer = retrieval_chain.run({"question": question, "context": context})
|
||||
except Exception as e:
|
||||
answer = f"Error generating answer: {e}"
|
||||
return (answer, "chroma")
|
||||
|
||||
# Main query function
|
||||
def query_agent(question: str) -> dict:
|
||||
# Simple rule: if question contains 'meta' or 'course', use MCP
|
||||
if any(word in question.lower() for word in ["meta", "course", "information", "details"]):
|
||||
meta_json = fetch_course_meta(question)
|
||||
return {"answer": meta_json, "source": "mcp_meta"}
|
||||
# Else try chroma
|
||||
answer, src = answer_from_chroma(question)
|
||||
return {"answer": answer, "source": src}
|
||||
|
||||
# ---------- CLI ----------
|
||||
PRESET_QUESTIONS = [
|
||||
"What is the grading policy?",
|
||||
"How do I submit assignments?",
|
||||
"Tell me about course meta for CS101",
|
||||
]
|
||||
|
||||
def main():
|
||||
print("=== FAQ Bot ===")
|
||||
print("Preset questions:")
|
||||
for i, q in enumerate(PRESET_QUESTIONS, 1):
|
||||
print(f"{i}. {q}")
|
||||
print("Enter 0 to exit or type a custom question.")
|
||||
while True:
|
||||
try:
|
||||
inp = input("\nYour choice (number or question): ")
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\nExiting.")
|
||||
break
|
||||
if inp.strip() == "0":
|
||||
print("Goodbye!")
|
||||
break
|
||||
if inp.strip().isdigit():
|
||||
idx = int(inp) - 1
|
||||
if 0 <= idx < len(PRESET_QUESTIONS):
|
||||
question = PRESET_QUESTIONS[idx]
|
||||
else:
|
||||
print("Invalid number.")
|
||||
continue
|
||||
else:
|
||||
question = inp.strip()
|
||||
if not question:
|
||||
print("Please enter a question.")
|
||||
continue
|
||||
result = query_agent(question)
|
||||
print(f"\nAnswer:\n{result['answer']}\nSource: {result['source']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user