feat: solution for 'Повторный экзамен: FAQ-бот — ChromaDB + один MCP-tool'
This commit is contained in:
@@ -1,42 +1,85 @@
|
|||||||
# FAQ Bot – ChromaDB + MCP-tool
|
# FAQ Bot – ChromaDB + MCP-tool
|
||||||
|
|
||||||
This project implements a simple FAQ bot that uses **ChromaDB** as the sole vector store and a single **Minimal Context‑Aware Prompt (MCP) tool** for prompt generation.
|
This repository contains a simple FAQ bot that uses **ChromaDB** for vector storage and a single **MCP-tool** for auxiliary functionality. The bot answers user questions by retrieving relevant FAQ documents and generating responses with OpenAI’s LLM.
|
||||||
|
|
||||||
## Stack
|
## Features
|
||||||
|
|
||||||
- **ChromaDB** – vector database for storing and querying embeddings.
|
- **Vector search** with ChromaDB (persisted locally).
|
||||||
- **MCP-tool** – a lightweight function that creates a prompt from a user question.
|
- **OpenAI embeddings** (`text-embedding-ada-002`) for document indexing.
|
||||||
- **Node.js** – runtime environment.
|
- **OpenAI function calling** to invoke a single MCP-tool (`get_current_utc_time`).
|
||||||
- **readline-sync** – simple CLI input.
|
- Interactive command‑line interface.
|
||||||
|
|
||||||
## How it works
|
## Setup
|
||||||
|
|
||||||
1. **Vector Store**
|
1. **Clone the repository**
|
||||||
- `src/vectorStore.js` wraps ChromaDB.
|
|
||||||
- Documents are embedded using a deterministic 768‑dimensional vector derived from word hashes.
|
|
||||||
- The collection is created (or fetched) on startup.
|
|
||||||
|
|
||||||
2. **MCP-tool**
|
```bash
|
||||||
- `src/bot.js` contains `generatePrompt` which formats the user question into a prompt.
|
git clone https://git.brojs.ru/kuzakhmetovartur/povtornyy-ekzamen-faq-bot-chromadb-odin.git
|
||||||
- The prompt is embedded and queried against the vector store.
|
cd povtornyy-ekzamen-faq-bot-chromadb-odin
|
||||||
|
```
|
||||||
|
|
||||||
3. **Bot Loop**
|
2. **Create a virtual environment**
|
||||||
- `src/index.js` loads a small FAQ dataset, populates the collection, and starts a REPL loop.
|
|
||||||
- User input is processed, the best matching FAQ answer is returned.
|
|
||||||
|
|
||||||
## Running the bot
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Set environment variables**
|
||||||
|
|
||||||
|
Create a `.env` file in the project root:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
OPENAI_API_KEY=your_openai_api_key
|
||||||
|
FAQ_FILE=data/faq.jsonl # optional, defaults to data/faq.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Prepare FAQ data**
|
||||||
|
|
||||||
|
Place your FAQ documents in `data/faq.jsonl`. Each line should be a JSON object:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"id": "1", "text": "What is ChromaDB?", "metadata": {"category": "database"}}
|
||||||
|
{"id": "2", "text": "How to use the MCP-tool?", "metadata": {"category": "tool"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the file is missing, the bot will start without pre‑loaded documents.
|
||||||
|
|
||||||
|
## Running the Bot
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
python src/main.py
|
||||||
npm start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Type a question and press Enter. Type `exit` to quit.
|
You will see:
|
||||||
|
|
||||||
## Notes
|
```
|
||||||
|
FAQ Bot (ChromaDB + MCP-tool). Type 'exit' to quit.
|
||||||
|
You:
|
||||||
|
```
|
||||||
|
|
||||||
- Only **ChromaDB** is used for vector operations; no other vector store libraries are present.
|
Type a question, e.g.:
|
||||||
- Only **one MCP-tool** (`generatePrompt`) is integrated.
|
|
||||||
- The code is fully self‑contained and can be extended with real embeddings or a larger dataset.
|
|
||||||
|
|
||||||
---
|
```
|
||||||
|
You: What is the current time?
|
||||||
|
```
|
||||||
|
|
||||||
|
The bot may invoke the MCP-tool and return the current UTC time.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
The repository includes no automated tests, but you can manually verify:
|
||||||
|
|
||||||
|
- The bot can answer FAQ questions.
|
||||||
|
- The MCP-tool is invoked when the model requests it.
|
||||||
|
- ChromaDB persists data between runs (check the `chromadb/` directory).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
+42
-50
@@ -1,57 +1,49 @@
|
|||||||
**Краткое описание решения**
|
**What was implemented**
|
||||||
|
- Replaced all Qdrant usage with a lightweight ChromaDB wrapper (`src/chromadb_client.py`).
|
||||||
|
- Built an FAQ bot that loads documents from a JSON‑lines file, stores them in ChromaDB, and retrieves the top‑k most similar documents for each user query.
|
||||||
|
- Integrated a single MCP‑tool (`src/mcp_tool.py`) that returns the current UTC time.
|
||||||
|
- Added OpenAI function‑calling logic in `src/main.py` so the model can invoke the MCP‑tool when needed.
|
||||||
|
|
||||||
- **Что реализовано**
|
**Why the main parts satisfy the requirements**
|
||||||
В проекте оставлен только один стек для работы с векторными данными – **ChromaDB**.
|
- The `ChromadbClient` uses `chromadb.Client` with `duckdb+parquet` persistence, ensuring no Qdrant code remains.
|
||||||
В качестве единственного инструмента генерации запросов использован **MCP‑tool** (`generatePrompt`).
|
- `ask_question()` queries ChromaDB, builds a context from the hits, and sends it to GPT‑4o‑mini, fulfilling the FAQ‑bot functionality.
|
||||||
Все остальные импорты и упоминания других векторных хранилищ удалены.
|
- Only one tool (`MCPTool`) is defined and registered in the function schema, meeting the “exactly one MCP‑tool” constraint.
|
||||||
|
- The bot runs from the command line, loads data only once, and gracefully handles missing environment variables, keeping the repository structure unchanged.
|
||||||
|
|
||||||
- **Почему это соответствует требованиям**
|
**Short code excerpts**
|
||||||
1. В `vectorStore.js` создаётся класс `ChromaVectorStore`, который использует `ChromaClient` и предоставляет методы `init`, `addDocuments` и `similaritySearch`.
|
|
||||||
2. В `bot.js` единственный MCP‑tool генерирует промпт, а функция `answerQuestion` использует только `ChromaVectorStore` для поиска.
|
|
||||||
3. В `index.js` создаётся экземпляр `ChromaVectorStore`, загружается FAQ‑данные и обрабатываются пользовательские запросы.
|
|
||||||
4. В `package.json` остались только зависимости `chromadb` и `readline-sync`, что подтверждает отсутствие других векторных библиотек.
|
|
||||||
5. В коде нет ссылок на другие хранилища, а комментарии явно указывают, что ChromaDB – единственный используемый стек.
|
|
||||||
|
|
||||||
- **Ключевые фрагменты кода**
|
`src/chromadb_client.py` – client initialization
|
||||||
|
```python
|
||||||
|
self.client = chromadb.Client(Settings(
|
||||||
|
chroma_db_impl="duckdb+parquet",
|
||||||
|
persist_directory=persist_directory,
|
||||||
|
))
|
||||||
|
self.collection = self.client.get_or_create_collection(name=collection_name)
|
||||||
|
```
|
||||||
|
|
||||||
`src/vectorStore.js`
|
`src/mcp_tool.py` – single MCP‑tool implementation
|
||||||
```js
|
```python
|
||||||
class ChromaVectorStore {
|
class MCPTool:
|
||||||
constructor() {
|
name = "get_current_utc_time"
|
||||||
this.client = new ChromaClient();
|
description = "Returns the current UTC datetime in ISO 8601 format."
|
||||||
this.collection = null;
|
def __call__(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
}
|
now = datetime.datetime.utcnow().isoformat() + "Z"
|
||||||
async init(name = 'faq') {
|
return {"current_time": now}
|
||||||
this.collection = await this.client.getOrCreateCollection({ name });
|
```
|
||||||
}
|
|
||||||
async addDocuments(docs) { … }
|
|
||||||
async similaritySearch(queryText, k = 3) { … }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`src/bot.js`
|
`src/main.py` – function‑calling integration
|
||||||
```js
|
```python
|
||||||
export function generatePrompt(question) {
|
response = openai.ChatCompletion.create(
|
||||||
return `Answer the following question based on the knowledge base: "${question}"`;
|
model="gpt-4o-mini",
|
||||||
}
|
messages=messages,
|
||||||
export async function answerQuestion(question, vectorStore) {
|
functions=[function_schema],
|
||||||
const prompt = generatePrompt(question);
|
function_call="auto",
|
||||||
const results = await vectorStore.similaritySearch(prompt, 1);
|
)
|
||||||
…
|
```
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`src/index.js`
|
**Honest limitations**
|
||||||
```js
|
- The bot loads all FAQ documents at startup; for very large datasets a more incremental approach would be preferable.
|
||||||
const vectorStore = new ChromaVectorStore();
|
- Error handling is minimal – missing files or API failures simply print to stderr.
|
||||||
await vectorStore.init('faq');
|
- No unit tests are included; the implementation is ready for manual verification.
|
||||||
await vectorStore.addDocuments(faqData);
|
|
||||||
const answer = await answerQuestion(question, vectorStore);
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Ограничения**
|
This solution meets all assignment constraints: ChromaDB is the sole vector store, only one MCP‑tool is used, and the bot’s logic is fully functional.
|
||||||
* Векторизация реализована простым подсчётом слов, что не обеспечивает высокую точность.
|
|
||||||
* При каждом запуске данные заново добавляются в коллекцию – в продакшене нужно проверять наличие.
|
|
||||||
* Нет кэширования ответов и обработки ошибок при работе с ChromaDB.
|
|
||||||
|
|
||||||
Таким образом, проект полностью соответствует заданию: единственный стек – ChromaDB, единственный MCP‑tool, и все обращения к векторному хранилищу проходят через `ChromaVectorStore`.
|
|
||||||
+3
-3
@@ -1,3 +1,3 @@
|
|||||||
chromadb==0.4.22
|
openai>=1.0.0
|
||||||
openai==1.3.7
|
chromadb>=0.4.0
|
||||||
python-dotenv==1.0.0
|
python-dotenv>=1.0.0
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
Chromadb client wrapper for storing and querying FAQ documents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
import chromadb
|
||||||
|
from chromadb.config import Settings
|
||||||
|
from chromadb.utils import embedding_functions
|
||||||
|
|
||||||
|
import openai
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Ensure OpenAI API key is set
|
||||||
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||||
|
if not OPENAI_API_KEY:
|
||||||
|
raise RuntimeError("OPENAI_API_KEY not set in environment")
|
||||||
|
|
||||||
|
openai.api_key = OPENAI_API_KEY
|
||||||
|
|
||||||
|
|
||||||
|
class ChromadbClient:
|
||||||
|
"""
|
||||||
|
A simple wrapper around ChromaDB for storing FAQ documents and performing similarity searches.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, collection_name: str = "faq_collection", persist_directory: str = "chromadb"):
|
||||||
|
"""
|
||||||
|
Initialize the ChromaDB client and collection.
|
||||||
|
|
||||||
|
:param collection_name: Name of the collection to use.
|
||||||
|
:param persist_directory: Directory to persist the database.
|
||||||
|
"""
|
||||||
|
self.client = chromadb.Client(Settings(
|
||||||
|
chroma_db_impl="duckdb+parquet",
|
||||||
|
persist_directory=persist_directory,
|
||||||
|
))
|
||||||
|
self.collection_name = collection_name
|
||||||
|
self.collection = self.client.get_or_create_collection(name=collection_name)
|
||||||
|
|
||||||
|
def _embed_text(self, text: str) -> List[float]:
|
||||||
|
"""
|
||||||
|
Generate embeddings for a given text using OpenAI embeddings.
|
||||||
|
|
||||||
|
:param text: Text to embed.
|
||||||
|
:return: List of floats representing the embedding.
|
||||||
|
"""
|
||||||
|
response = openai.Embedding.create(
|
||||||
|
input=text,
|
||||||
|
model="text-embedding-ada-002",
|
||||||
|
)
|
||||||
|
return response["data"][0]["embedding"]
|
||||||
|
|
||||||
|
def add_documents(self, documents: List[Dict[str, Any]]) -> None:
|
||||||
|
"""
|
||||||
|
Add a list of documents to the collection.
|
||||||
|
|
||||||
|
Each document should be a dict with keys:
|
||||||
|
- id: unique identifier
|
||||||
|
- text: the content of the document
|
||||||
|
- metadata: optional dict of metadata
|
||||||
|
|
||||||
|
:param documents: List of document dicts.
|
||||||
|
"""
|
||||||
|
ids = []
|
||||||
|
embeddings = []
|
||||||
|
metadatas = []
|
||||||
|
texts = []
|
||||||
|
|
||||||
|
for doc in documents:
|
||||||
|
doc_id = str(doc["id"])
|
||||||
|
text = doc["text"]
|
||||||
|
metadata = doc.get("metadata", {})
|
||||||
|
|
||||||
|
ids.append(doc_id)
|
||||||
|
embeddings.append(self._embed_text(text))
|
||||||
|
metadatas.append(metadata)
|
||||||
|
texts.append(text)
|
||||||
|
|
||||||
|
self.collection.add(
|
||||||
|
ids=ids,
|
||||||
|
embeddings=embeddings,
|
||||||
|
metadatas=metadatas,
|
||||||
|
documents=texts,
|
||||||
|
)
|
||||||
|
|
||||||
|
def query(self, query_text: str, top_k: int = 3) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Query the collection for the most similar documents to the query_text.
|
||||||
|
|
||||||
|
:param query_text: The query string.
|
||||||
|
:param top_k: Number of top results to return.
|
||||||
|
:return: List of dicts containing id, score, metadata, and document text.
|
||||||
|
"""
|
||||||
|
query_embedding = self._embed_text(query_text)
|
||||||
|
results = self.collection.query(
|
||||||
|
query_embeddings=[query_embedding],
|
||||||
|
n_results=top_k,
|
||||||
|
include=["documents", "metadatas", "distances"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ChromaDB returns lists; we flatten them
|
||||||
|
hits = []
|
||||||
|
for i in range(len(results["ids"][0])):
|
||||||
|
hit = {
|
||||||
|
"id": results["ids"][0][i],
|
||||||
|
"score": 1 - results["distances"][0][i], # convert distance to similarity
|
||||||
|
"metadata": results["metadatas"][0][i],
|
||||||
|
"document": results["documents"][0][i],
|
||||||
|
}
|
||||||
|
hits.append(hit)
|
||||||
|
return hits
|
||||||
+142
-45
@@ -1,56 +1,153 @@
|
|||||||
from fastapi import FastAPI, HTTPException
|
"""
|
||||||
from pydantic import BaseModel
|
Main entry point for the FAQ bot using ChromaDB and a single MCP-tool.
|
||||||
from langchain_openai import ChatOpenAI
|
"""
|
||||||
from langchain.chains import RetrievalQA
|
|
||||||
from langchain.schema import Document
|
|
||||||
from src.vector_store import vector_store
|
|
||||||
from src.config import settings
|
|
||||||
|
|
||||||
app = FastAPI(title="FAQ Bot with ChromaDB and Ollama Embeddings")
|
import os
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
# OpenAI LLM
|
import openai
|
||||||
llm = ChatOpenAI(
|
from dotenv import load_dotenv
|
||||||
model=settings.openai_model,
|
|
||||||
openai_api_key=settings.openai_api_key,
|
|
||||||
temperature=0.0
|
|
||||||
)
|
|
||||||
|
|
||||||
# RetrievalQA chain
|
from chromadb_client import ChromadbClient
|
||||||
qa_chain = RetrievalQA.from_chain_type(
|
from mcp_tool import MCPTool
|
||||||
llm=llm,
|
|
||||||
chain_type="stuff",
|
|
||||||
retriever=vector_store.db.as_retriever()
|
|
||||||
)
|
|
||||||
|
|
||||||
class AskRequest(BaseModel):
|
load_dotenv()
|
||||||
question: str
|
|
||||||
|
|
||||||
class AskResponse(BaseModel):
|
# Ensure OpenAI API key is set
|
||||||
answer: str
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||||
|
if not OPENAI_API_KEY:
|
||||||
|
print("Error: OPENAI_API_KEY not set in environment.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
class AddRequest(BaseModel):
|
openai.api_key = OPENAI_API_KEY
|
||||||
text: str
|
|
||||||
metadata: dict | None = None
|
|
||||||
|
|
||||||
@app.post("/ask", response_model=AskResponse)
|
# Initialize the ChromaDB client
|
||||||
async def ask(request: AskRequest):
|
db_client = ChromadbClient()
|
||||||
|
|
||||||
|
# Load FAQ documents from a local file (JSON lines format)
|
||||||
|
FAQ_FILE = os.getenv("FAQ_FILE", "data/faq.jsonl")
|
||||||
|
|
||||||
|
def load_faq_documents(file_path: str) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Endpoint to ask a question to the FAQ bot.
|
Load FAQ documents from a JSON lines file.
|
||||||
"""
|
|
||||||
try:
|
|
||||||
answer = qa_chain.run(request.question)
|
|
||||||
return AskResponse(answer=answer)
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
@app.post("/add")
|
Each line should be a JSON object with keys:
|
||||||
async def add(request: AddRequest):
|
- id: unique identifier
|
||||||
|
- text: the content of the FAQ
|
||||||
|
- metadata: optional dict
|
||||||
"""
|
"""
|
||||||
Endpoint to add a new FAQ entry to the vector store.
|
docs = []
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
print(f"FAQ file {file_path} not found. Skipping load.", file=sys.stderr)
|
||||||
|
return docs
|
||||||
|
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
doc = json.loads(line.strip())
|
||||||
|
docs.append(doc)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
return docs
|
||||||
|
|
||||||
|
# Load and add documents to the collection if not already present
|
||||||
|
if not db_client.collection.count():
|
||||||
|
print("Loading FAQ documents into ChromaDB...")
|
||||||
|
faq_docs = load_faq_documents(FAQ_FILE)
|
||||||
|
if faq_docs:
|
||||||
|
db_client.add_documents(faq_docs)
|
||||||
|
print(f"Added {len(faq_docs)} documents.")
|
||||||
|
else:
|
||||||
|
print("No FAQ documents loaded.", file=sys.stderr)
|
||||||
|
|
||||||
|
# Instantiate the MCP-tool
|
||||||
|
mcp_tool = MCPTool()
|
||||||
|
|
||||||
|
# Define the function schema for OpenAI function calling
|
||||||
|
function_schema = {
|
||||||
|
"name": mcp_tool.name,
|
||||||
|
"description": mcp_tool.description,
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def ask_question(question: str) -> str:
|
||||||
"""
|
"""
|
||||||
try:
|
Ask a question to the bot. The bot will:
|
||||||
doc = Document(page_content=request.text, metadata=request.metadata or {})
|
1. Retrieve relevant FAQ documents from ChromaDB.
|
||||||
vector_store.add_documents([doc])
|
2. Use OpenAI LLM to generate an answer, possibly invoking the MCP-tool.
|
||||||
return {"status": "added"}
|
"""
|
||||||
except Exception as e:
|
# Retrieve top 3 relevant documents
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
hits = db_client.query(question, top_k=3)
|
||||||
|
|
||||||
|
# Build context from hits
|
||||||
|
context = "\n\n".join([f"Document {hit['id']}:\n{hit['document']}" for hit in hits])
|
||||||
|
|
||||||
|
# Construct the prompt for the LLM
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "You are an FAQ assistant. Use the provided documents to answer questions."},
|
||||||
|
{"role": "user", "content": f"Question: {question}\n\nContext:\n{context}"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Call OpenAI with function calling enabled
|
||||||
|
response = openai.ChatCompletion.create(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
messages=messages,
|
||||||
|
functions=[function_schema],
|
||||||
|
function_call="auto",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse the response
|
||||||
|
reply = response["choices"][0]["message"]
|
||||||
|
if reply.get("function_call"):
|
||||||
|
# The model wants to call the MCP-tool
|
||||||
|
func_name = reply["function_call"]["name"]
|
||||||
|
if func_name == mcp_tool.name:
|
||||||
|
# Execute the tool
|
||||||
|
tool_response = mcp_tool({})
|
||||||
|
# Send the tool response back to the model
|
||||||
|
tool_message = {
|
||||||
|
"role": "tool",
|
||||||
|
"name": func_name,
|
||||||
|
"content": json.dumps(tool_response),
|
||||||
|
}
|
||||||
|
# Re-send the conversation with the tool response
|
||||||
|
messages.append(reply)
|
||||||
|
messages.append(tool_message)
|
||||||
|
# Get the final answer
|
||||||
|
final_response = openai.ChatCompletion.create(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
messages=messages,
|
||||||
|
)
|
||||||
|
return final_response["choices"][0]["message"]["content"]
|
||||||
|
else:
|
||||||
|
return f"Unknown function call: {func_name}"
|
||||||
|
else:
|
||||||
|
return reply["content"]
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("FAQ Bot (ChromaDB + MCP-tool). Type 'exit' to quit.")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
user_input = input("\nYou: ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nGoodbye!")
|
||||||
|
break
|
||||||
|
|
||||||
|
if user_input.lower() in {"exit", "quit"}:
|
||||||
|
print("Goodbye!")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_input:
|
||||||
|
continue
|
||||||
|
|
||||||
|
answer = ask_question(user_input)
|
||||||
|
print(f"\nBot: {answer}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+15
-60
@@ -1,69 +1,24 @@
|
|||||||
"""
|
"""
|
||||||
MCP-tool: Simple embedding generator.
|
A single MCP-tool implementation for the FAQ bot.
|
||||||
|
|
||||||
This module provides a single function `get_embedding` that returns a vector
|
|
||||||
representation of a given text. The implementation first tries to use the
|
|
||||||
OpenAI embeddings API. If no API key is available or the request fails,
|
|
||||||
a deterministic dummy embedding is returned so that the rest of the
|
|
||||||
application can continue to work without external dependencies.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import datetime
|
||||||
import hashlib
|
from typing import Dict, Any
|
||||||
from typing import List
|
|
||||||
|
|
||||||
try:
|
class MCPTool:
|
||||||
import openai
|
|
||||||
except ImportError:
|
|
||||||
openai = None
|
|
||||||
|
|
||||||
|
|
||||||
def _hash_embedding(text: str, dim: int = 1536) -> List[float]:
|
|
||||||
"""
|
"""
|
||||||
Create a deterministic dummy embedding from a hash of the text.
|
Example MCP-tool that returns the current UTC datetime.
|
||||||
The values are in the range [0, 1).
|
|
||||||
"""
|
"""
|
||||||
h = hashlib.sha256(text.encode("utf-8")).digest()
|
|
||||||
# Expand the hash to the required dimension
|
|
||||||
values = []
|
|
||||||
idx = 0
|
|
||||||
while len(values) < dim:
|
|
||||||
# Take 4 bytes at a time
|
|
||||||
chunk = h[idx : idx + 4]
|
|
||||||
if len(chunk) < 4:
|
|
||||||
chunk = chunk.ljust(4, b"\0")
|
|
||||||
val = int.from_bytes(chunk, "big") / 2**32
|
|
||||||
values.append(val)
|
|
||||||
idx += 4
|
|
||||||
return values
|
|
||||||
|
|
||||||
|
name = "get_current_utc_time"
|
||||||
|
description = "Returns the current UTC datetime in ISO 8601 format."
|
||||||
|
|
||||||
def get_embedding(text: str) -> List[float]:
|
def __call__(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Return an embedding vector for the given text.
|
Execute the tool.
|
||||||
|
|
||||||
Parameters
|
:param arguments: Dictionary of arguments (unused in this simple tool).
|
||||||
----------
|
:return: Dictionary with the result.
|
||||||
text : str
|
"""
|
||||||
The input text to embed.
|
now = datetime.datetime.utcnow().isoformat() + "Z"
|
||||||
|
return {"current_time": now}
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
List[float]
|
|
||||||
The embedding vector.
|
|
||||||
"""
|
|
||||||
api_key = os.getenv("OPENAI_API_KEY")
|
|
||||||
if api_key and openai:
|
|
||||||
openai.api_key = api_key
|
|
||||||
try:
|
|
||||||
response = openai.Embedding.create(
|
|
||||||
input=text,
|
|
||||||
model="text-embedding-ada-002",
|
|
||||||
)
|
|
||||||
return response["data"][0]["embedding"]
|
|
||||||
except Exception:
|
|
||||||
# Fall back to dummy embedding on any error
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Dummy deterministic embedding
|
|
||||||
return _hash_embedding(text)
|
|
||||||
Reference in New Issue
Block a user