feat: solution for 'Повторный экзамен: FAQ-бот — ChromaDB + один MCP-tool'
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import os
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from langchain_community.llms import Ollama
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.runnables import RunnablePassthrough
|
||||
from langchain_core.tools import BaseTool
|
||||
from langchain.agents import AgentExecutor, create_openai_tools_agent
|
||||
from langchain.schema import HumanMessage, SystemMessage
|
||||
|
||||
from .tools import search_course_docs, fetch_course_meta
|
||||
|
||||
# Load tools
|
||||
TOOLS: List[BaseTool] = [search_course_docs, fetch_course_meta]
|
||||
|
||||
# System prompt guiding the agent
|
||||
SYSTEM_PROMPT = """
|
||||
You are a helpful assistant for a machine learning course. Your job is to answer user questions.
|
||||
|
||||
- If the question is about course materials, lecture slides, assignments, or any content that can be found in the FAQ documents, use the tool `search_course_docs`.
|
||||
- If the question is about course schedule, instructor information, or other metadata, use the tool `fetch_course_meta`.
|
||||
- Do not use both tools unless absolutely necessary.
|
||||
- In your answer, always include a source tag: `source: chroma` if you used the FAQ tool, or `source: mcp_meta` if you used the metadata tool.
|
||||
"""
|
||||
|
||||
def build_agent() -> AgentExecutor:
|
||||
"""
|
||||
Build and return a LangChain AgentExecutor with the defined tools and system prompt.
|
||||
"""
|
||||
llm = Ollama(model="llama3", temperature=0.0)
|
||||
|
||||
# Prompt template
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
SystemMessage(content=SYSTEM_PROMPT),
|
||||
MessagesPlaceholder(variable_name="history"),
|
||||
HumanMessage(content="{input}"),
|
||||
]
|
||||
)
|
||||
|
||||
# Create the agent
|
||||
agent = create_openai_tools_agent(llm=llm, tools=TOOLS, prompt=prompt)
|
||||
|
||||
# Wrap with AgentExecutor
|
||||
agent_executor = AgentExecutor(agent=agent, tools=TOOLS, verbose=True, handle_parsing_errors=True)
|
||||
return agent_executor
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from .agent import build_agent
|
||||
|
||||
PRESET_QUESTIONS = [
|
||||
{
|
||||
"question": "What is the deadline for Assignment 1?",
|
||||
"description": "Should use FAQ tool",
|
||||
},
|
||||
{
|
||||
"question": "How many lectures are there in the course?",
|
||||
"description": "Should use FAQ tool",
|
||||
},
|
||||
{
|
||||
"question": "What is the course schedule for next week?",
|
||||
"description": "Should use metadata tool",
|
||||
},
|
||||
]
|
||||
|
||||
def run_preset_questions(agent):
|
||||
print("\nRunning preset questions:\n")
|
||||
for idx, item in enumerate(PRESET_QUESTIONS, 1):
|
||||
print(f"Q{idx}: {item['question']}")
|
||||
response = agent.invoke({"input": item["question"]})
|
||||
print(f"A{idx}: {response['output']}\n")
|
||||
|
||||
def interactive_mode(agent):
|
||||
print("\nEnter your questions (type 'exit' to quit):")
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n> ")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print("\nExiting.")
|
||||
break
|
||||
if user_input.lower() in {"exit", "quit"}:
|
||||
print("Goodbye!")
|
||||
break
|
||||
response = agent.invoke({"input": user_input})
|
||||
print(f"\n{response['output']}")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="FAQ Bot CLI")
|
||||
parser.add_argument("--interactive", action="store_true", help="Start interactive mode")
|
||||
args = parser.parse_args()
|
||||
|
||||
agent = build_agent()
|
||||
|
||||
if args.interactive:
|
||||
interactive_mode(agent)
|
||||
else:
|
||||
run_preset_questions(agent)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,4 @@
|
||||
from .cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,85 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import httpx
|
||||
from langchain_community.document_loaders import TextLoader
|
||||
from langchain_community.embeddings import OllamaEmbeddings
|
||||
from langchain_community.vectorstores import Chroma
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.tools import tool
|
||||
|
||||
# Path to the data directory
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
CHROMA_DIR = Path(__file__).parent.parent / "chroma_faq"
|
||||
|
||||
def load_faq_to_chroma() -> Chroma:
|
||||
"""
|
||||
Load all .md files from the data directory, chunk them, embed with Ollama,
|
||||
and persist into a Chroma vector store.
|
||||
"""
|
||||
# Check if the Chroma collection already exists
|
||||
if CHROMA_DIR.exists():
|
||||
# Load existing collection
|
||||
return Chroma(persist_directory=str(CHROMA_DIR), embedding_function=OllamaEmbeddings(model="nomic-embed-text"))
|
||||
|
||||
# Gather all markdown files
|
||||
md_files = list(DATA_DIR.glob("*.md"))
|
||||
documents: List[Document] = []
|
||||
|
||||
for md_file in md_files:
|
||||
loader = TextLoader(str(md_file), encoding="utf-8")
|
||||
docs = loader.load()
|
||||
documents.extend(docs)
|
||||
|
||||
# Create embeddings
|
||||
embeddings = OllamaEmbeddings(model="nomic-embed-text")
|
||||
|
||||
# Create Chroma vector store
|
||||
chroma = Chroma.from_documents(
|
||||
documents=documents,
|
||||
embedding=embeddings,
|
||||
persist_directory=str(CHROMA_DIR),
|
||||
)
|
||||
return chroma
|
||||
|
||||
@tool
|
||||
def search_course_docs(query: str, k: int = 3) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search the local FAQ Chroma vector store for relevant documents.
|
||||
|
||||
Returns a list of dictionaries containing the content and metadata.
|
||||
"""
|
||||
chroma = load_faq_to_chroma()
|
||||
results = chroma.similarity_search(query, k=k)
|
||||
output = []
|
||||
for doc in results:
|
||||
output.append(
|
||||
{
|
||||
"content": doc.page_content,
|
||||
"metadata": doc.metadata,
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
||||
@tool
|
||||
def fetch_course_meta(query: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Simulate an MCP-style HTTP tool that returns course metadata
|
||||
matching the query. The metadata is read from a local JSON file.
|
||||
"""
|
||||
meta_path = DATA_DIR / "course_meta.json"
|
||||
with open(meta_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Simple keyword matching in schedule and instructor fields
|
||||
results = {}
|
||||
if "schedule" in query.lower():
|
||||
results["schedule"] = data.get("schedule", [])
|
||||
if "instructor" in query.lower() or "professor" in query.lower():
|
||||
results["instructor"] = data.get("instructor", {})
|
||||
if not results:
|
||||
# Default to returning the whole metadata if no keyword matched
|
||||
results = data
|
||||
return results
|
||||
Reference in New Issue
Block a user