From 466e91231cf72cf036aee5811342fb56f9ed87f7 Mon Sep 17 00:00:00 2001 From: kuzakhmetovartur Date: Mon, 29 Jun 2026 12:19:57 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20solution=20for=20'=D0=9F=D0=BE=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BD=D1=8B=D0=B9=20=D1=8D=D0=BA=D0=B7=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD:=20FAQ-=D0=B1=D0=BE=D1=82=20=E2=80=94=20Chro?= =?UTF-8?q?maDB=20+=20=D0=BE=D0=B4=D0=B8=D0=BD=20MCP-tool'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 ++ README.md | 119 ++++++++++++++++++++++++++++++++++++++++++ data/course_meta.json | 27 ++++++++++ data/faq1.md | 24 +++++++++ data/faq2.md | 13 +++++ data/faq3.md | 12 +++++ requirements.txt | 6 +++ src/agent.py | 46 ++++++++++++++++ src/cli.py | 55 +++++++++++++++++++ src/main.py | 4 ++ src/tools.py | 85 ++++++++++++++++++++++++++++++ 11 files changed, 396 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 data/course_meta.json create mode 100644 data/faq1.md create mode 100644 data/faq2.md create mode 100644 data/faq3.md create mode 100644 requirements.txt create mode 100644 src/agent.py create mode 100644 src/cli.py create mode 100644 src/main.py create mode 100644 src/tools.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b16538b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +dist/ +build/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..9403a80 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# FAQ Bot – ChromaDB + MCP-style Tool + +This project implements a simple FAQ bot that answers questions about a machine learning course. +The bot uses: + +- **ChromaDB** to store and retrieve FAQ documents. +- **Ollama** embeddings (`nomic-embed-text`) for vectorization. +- **LangChain** to build an agent that routes queries to the appropriate tool. +- **MCP-style HTTP tool** (`fetch_course_meta`) that returns course metadata from a local JSON file. + +## Project Structure + +``` +. +├── chroma_faq/ # Persisted Chroma vector store +├── data/ +│ ├── faq1.md +│ ├── faq2.md +│ ├── faq3.md +│ └── course_meta.json +├── src/ +│ ├── __init__.py +│ ├── agent.py +│ ├── cli.py +│ ├── main.py +│ └── tools.py +├── requirements.txt +└── README.md +``` + +## Setup + +1. **Install Ollama** + Download and install Ollama from https://ollama.ai/. + Pull the required models: + + ```bash + ollama pull nomic-embed-text + ollama pull llama3 + ``` + +2. **Create a virtual environment** (optional but recommended): + + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +3. **Install Python dependencies**: + + ```bash + pip install -r requirements.txt + ``` + +## Running the Bot + +### Preset Questions + +Run the script without arguments to execute three preset questions (two for the FAQ tool, one for the metadata tool): + +```bash +python -m src.main +``` + +You should see output similar to: + +``` +Running preset questions: + +Q1: What is the deadline for Assignment 1? +A1: The deadline for Assignment 1 is August 31, 2026. source: chroma + +Q2: How many lectures are there in the course? +A2: There are 12 lectures in the course. source: chroma + +Q3: What is the course schedule for next week? +A3: The course schedule for next week is: +- 2026-09-01: Lecture 1 – Introduction to ML (Room 101) +- 2026-09-08: Lecture 2 – Data Preprocessing (Room 102) +- 2026-09-15: Lecture 3 – Linear Regression (Room 103) +source: mcp_meta +``` + +### Interactive Mode + +Start an interactive session: + +```bash +python -m src.main --interactive +``` + +You can type any question, and the bot will answer using the appropriate tool. Type `exit` or `Ctrl+C` to quit. + +## How It Works + +1. **Data Loading** + `src/tools.py` contains `load_faq_to_chroma()` which reads all `.md` files in `data/`, chunks them, embeds them with `nomic-embed-text`, and persists the vector store in `chroma_faq/`. + +2. **Tools** + - `search_course_docs(query, k)` – searches the Chroma vector store for relevant FAQ snippets. + - `fetch_course_meta(query)` – reads `data/course_meta.json` and returns schedule or instructor information based on the query. + +3. **Agent** + `src/agent.py` builds a LangChain agent that: + - Uses a system prompt to decide which tool to call. + - Adds a `source:` tag to the final answer indicating whether the answer came from the FAQ (`chroma`) or the metadata tool (`mcp_meta`). + +4. **CLI** + `src/cli.py` provides a simple command‑line interface to run preset questions or an interactive session. + +## Extending the Bot + +- **Add more FAQ documents** – Place additional `.md` files in `data/` and re‑run the script to rebuild the vector store. +- **Add more metadata** – Update `data/course_meta.json` or modify `fetch_course_meta` to call a real HTTP endpoint. +- **Change the LLM** – Replace `Ollama` with another LLM provider in `src/agent.py`. + +## License + +This project is provided as-is for educational purposes. Feel free to adapt and extend it for your own use cases. \ No newline at end of file diff --git a/data/course_meta.json b/data/course_meta.json new file mode 100644 index 0000000..0852ecf --- /dev/null +++ b/data/course_meta.json @@ -0,0 +1,27 @@ +{ + "schedule": [ + { + "date": "2026-09-01", + "lecture": "Lecture 1", + "topic": "Introduction to ML", + "location": "Room 101" + }, + { + "date": "2026-09-08", + "lecture": "Lecture 2", + "topic": "Data Preprocessing", + "location": "Room 102" + }, + { + "date": "2026-09-15", + "lecture": "Lecture 3", + "topic": "Linear Regression", + "location": "Room 103" + } + ], + "instructor": { + "name": "Dr. Jane Doe", + "email": "jane.doe@example.com", + "office": "Room 201" + } +} \ No newline at end of file diff --git a/data/faq1.md b/data/faq1.md new file mode 100644 index 0000000..fe70221 --- /dev/null +++ b/data/faq1.md @@ -0,0 +1,24 @@ +# Course Overview + +This course covers the fundamentals of machine learning, including supervised and unsupervised learning, neural networks, and reinforcement learning. The course is divided into 12 lectures, each lasting 90 minutes. + +## Assignment 1 + +The first assignment is due on **August 31, 2026**. It requires you to implement a simple linear regression model and evaluate its performance. + +## Lecture Schedule + +| Lecture | Topic | +|---------|-------| +| 1 | Introduction to ML | +| 2 | Data Preprocessing | +| 3 | Linear Regression | +| 4 | Logistic Regression | +| 5 | Decision Trees | +| 6 | Random Forests | +| 7 | Support Vector Machines | +| 8 | Neural Networks | +| 9 | Convolutional Neural Networks | +| 10 | Recurrent Neural Networks | +| 11 | Reinforcement Learning | +| 12 | Project Presentations | \ No newline at end of file diff --git a/data/faq2.md b/data/faq2.md new file mode 100644 index 0000000..ee9d4fe --- /dev/null +++ b/data/faq2.md @@ -0,0 +1,13 @@ +# Frequently Asked Questions + +**Q: How many lectures are there in the course?** +A: There are 12 lectures in total. + +**Q: What is the deadline for Assignment 2?** +A: Assignment 2 is due on **September 15, 2026**. + +**Q: Where can I find the lecture slides?** +A: All lecture slides are available in the course portal under the "Resources" section. + +**Q: Can I submit the assignment late?** +A: Late submissions are accepted with a penalty of 10% per day after the deadline. \ No newline at end of file diff --git a/data/faq3.md b/data/faq3.md new file mode 100644 index 0000000..8c84aaf --- /dev/null +++ b/data/faq3.md @@ -0,0 +1,12 @@ +# Course Materials + +- **Lecture Slides**: PDF files for each lecture. +- **Reading List**: A list of recommended books and papers. +- **Code Repository**: GitHub repository with starter code and solutions. +- **Discussion Forum**: For asking questions and collaborating with peers. + +**Q: Where is the code repository hosted?** +A: The code repository is hosted on GitHub at https://github.com/example/course-ml. + +**Q: How do I clone the repository?** +A: Use `git clone https://github.com/example/course-ml.git` in your terminal. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e9a7be5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +langchain==0.1.0 +langchain-chroma==0.1.0 +langchain-ollama==0.1.0 +chromadb==0.4.24 +httpx==0.27.0 +python-dotenv==1.0.1 \ No newline at end of file diff --git a/src/agent.py b/src/agent.py new file mode 100644 index 0000000..4376e79 --- /dev/null +++ b/src/agent.py @@ -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 \ No newline at end of file diff --git a/src/cli.py b/src/cli.py new file mode 100644 index 0000000..4536c6e --- /dev/null +++ b/src/cli.py @@ -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() \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..27d14a7 --- /dev/null +++ b/src/main.py @@ -0,0 +1,4 @@ +from .cli import main + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/tools.py b/src/tools.py new file mode 100644 index 0000000..18c61b0 --- /dev/null +++ b/src/tools.py @@ -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 \ No newline at end of file