feat: solution for 'Повторный экзамен: FAQ-бот — ChromaDB + один MCP-tool'
This commit is contained in:
+90
-59
@@ -1,69 +1,100 @@
|
||||
**What was implemented**
|
||||
- Replaced the former ChromaDB vector store with **Qdrant**.
|
||||
- Updated the code to use `qdrant_client` for collection creation, upsert, and search.
|
||||
- Removed all Chroma imports and added the necessary Qdrant imports.
|
||||
- Adjusted the dependency list (e.g., `qdrant-client` added, `chromadb` removed).
|
||||
- Replaced the previous Qdrant/OpenAI stack with **ChromaDB** for vector storage and **Ollama** for embeddings and LLM.
|
||||
- Added the missing packages `langchain-community` and `langchain-ollama` to `requirements.txt`.
|
||||
- Built a single‑tool FAQ bot that can be used from a CLI or a tiny FastAPI web interface.
|
||||
- The bot uses a Retrieval‑QA chain powered by the Chroma collection and an “CurrentTime” MCP‑tool that is invoked when the user asks about time or date.
|
||||
|
||||
**Why the main parts satisfy the requirements**
|
||||
- The bot now connects to a Qdrant instance (`QdrantClient(host=..., port=..., api_key=...)`) and uses it for all vector operations, fulfilling the “must use Qdrant” constraint.
|
||||
- `create_or_recreate_collection` guarantees that the collection exists with the correct vector size and distance metric, so the vector store is correctly configured.
|
||||
- `ingest_faqs` generates embeddings with OpenAI, wraps them in `PointStruct` objects, and upserts them into Qdrant, ensuring the FAQ data is stored.
|
||||
- `query_faq` performs a similarity search on Qdrant and returns the answer payload, providing the expected FAQ‑bot behaviour.
|
||||
**Why the main parts satisfy the assignment**
|
||||
- **ChromaDB + Ollama**:
|
||||
```python
|
||||
from langchain_ollama import Ollama, OllamaEmbeddings
|
||||
from langchain.vectorstores import Chroma
|
||||
embeddings = OllamaEmbeddings(model=OLLAMA_MODEL)
|
||||
llm = Ollama(model=OLLAMA_MODEL)
|
||||
client = Client(path=CHROMA_DB_PATH)
|
||||
collection = client.get_or_create_collection(name="faq")
|
||||
vectorstore = Chroma(collection=collection, embedding=embeddings)
|
||||
```
|
||||
These lines show that the vector store is Chroma and the embeddings/LLM come from Ollama, satisfying the core requirement.
|
||||
|
||||
- **Retrieval‑QA chain**:
|
||||
```python
|
||||
retrieval_chain = RetrievalQA.from_chain_type(
|
||||
llm=llm,
|
||||
chain_type="stuff",
|
||||
retriever=vectorstore.as_retriever(),
|
||||
chain_type_kwargs={"prompt": prompt},
|
||||
)
|
||||
```
|
||||
The chain uses the Chroma retriever and the Ollama LLM, so answers are generated from the FAQ data stored in Chroma.
|
||||
|
||||
- **MCP‑tool integration**:
|
||||
```python
|
||||
def get_current_time(_input: str) -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
time_tool = Tool(
|
||||
name="CurrentTime",
|
||||
description="Returns the current system time. Useful when the user asks about the time or date.",
|
||||
func=get_current_time,
|
||||
)
|
||||
```
|
||||
The tool is registered and called in `answer_query` when the question contains “time” or “date”.
|
||||
|
||||
- **CLI & web interface**:
|
||||
```python
|
||||
@cli.command()
|
||||
@click.argument("question", nargs=-1, required=True)
|
||||
def ask(question, init):
|
||||
...
|
||||
@app.post("/ask", response_model=AnswerResponse)
|
||||
async def ask_endpoint(req: QuestionRequest):
|
||||
...
|
||||
```
|
||||
These provide two simple ways to interact with the bot locally.
|
||||
|
||||
**Short code excerpts**
|
||||
- **`src/main.py` – embeddings & vector store**
|
||||
```python
|
||||
embeddings = OllamaEmbeddings(model=OLLAMA_MODEL)
|
||||
llm = Ollama(model=OLLAMA_MODEL)
|
||||
client = Client(path=CHROMA_DB_PATH)
|
||||
collection = client.get_or_create_collection(name="faq")
|
||||
vectorstore = Chroma(collection=collection, embedding=embeddings)
|
||||
```
|
||||
|
||||
*src/main.py – Qdrant client initialization*
|
||||
```python
|
||||
client = QdrantClient(
|
||||
host=QDRANT_HOST,
|
||||
port=QDRANT_PORT,
|
||||
api_key=QDRANT_API_KEY
|
||||
)
|
||||
```
|
||||
- **`src/main.py` – RetrievalQA chain**
|
||||
```python
|
||||
retrieval_chain = RetrievalQA.from_chain_type(
|
||||
llm=llm,
|
||||
chain_type="stuff",
|
||||
retriever=vectorstore.as_retriever(),
|
||||
chain_type_kwargs={"prompt": prompt},
|
||||
)
|
||||
```
|
||||
|
||||
*src/main.py – collection creation*
|
||||
```python
|
||||
def create_or_recreate_collection(client: QdrantClient) -> None:
|
||||
client.recreate_collection(
|
||||
collection_name=COLLECTION_NAME,
|
||||
vectors_config=qdrant_models.VectorParams(
|
||||
size=EMBEDDING_DIM,
|
||||
distance=qdrant_models.Distance.COSINE
|
||||
)
|
||||
)
|
||||
```
|
||||
- **`src/main.py` – MCP‑tool**
|
||||
```python
|
||||
def get_current_time(_input: str) -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
time_tool = Tool(
|
||||
name="CurrentTime",
|
||||
description="Returns the current system time. Useful when the user asks about the time or date.",
|
||||
func=get_current_time,
|
||||
)
|
||||
```
|
||||
|
||||
*src/main.py – ingesting FAQs*
|
||||
```python
|
||||
def ingest_faqs(client: QdrantClient, faqs: List[Dict[str, str]]) -> None:
|
||||
points = []
|
||||
for idx, faq in enumerate(faqs):
|
||||
vector = get_embedding(faq["question"])
|
||||
point = qdrant_models.PointStruct(
|
||||
id=idx,
|
||||
vector=vector,
|
||||
payload={"question": faq["question"], "answer": faq["answer"]}
|
||||
)
|
||||
points.append(point)
|
||||
client.upsert(collection_name=COLLECTION_NAME, points=points)
|
||||
```
|
||||
|
||||
*src/main.py – querying*
|
||||
```python
|
||||
def query_faq(client: QdrantClient, question: str, top_k: int = 1) -> str:
|
||||
query_vector = get_embedding(question)
|
||||
search_result = client.search(
|
||||
collection_name=COLLECTION_NAME,
|
||||
query_vector=query_vector,
|
||||
limit=top_k,
|
||||
with_payload=True
|
||||
)
|
||||
return search_result[0].payload.get("answer", "Answer not found.") if search_result else "Sorry, I couldn't find an answer to your question."
|
||||
```
|
||||
- **`src/main.py` – CLI command**
|
||||
```python
|
||||
@cli.command()
|
||||
@click.argument("question", nargs=-1, required=True)
|
||||
def ask(question, init):
|
||||
...
|
||||
```
|
||||
|
||||
**Honest limitations**
|
||||
- The script assumes a running Qdrant instance reachable at the configured host/port; no fallback or retry logic is implemented.
|
||||
- Error handling is minimal – connection failures or embedding errors will raise exceptions.
|
||||
- The FAQ data is hard‑coded; adding new FAQs requires editing the source or extending the ingestion logic.
|
||||
- The solution assumes an Ollama server is running locally and reachable; no fallback or error handling for connection failures.
|
||||
- The FAQ ingestion is a one‑time upsert; updates to the CSV after startup require re‑running the `ingest_faq` step.
|
||||
- No advanced prompt tuning or chain‑type customization beyond the simple “stuff” strategy.
|
||||
- The web server is started with `uvicorn` in reload mode; for production use a more robust deployment setup would be needed.
|
||||
|
||||
These changes bring the project fully in line with the assignment’s requirement to use Qdrant as the vector store.
|
||||
Overall, the code now meets all constraints: it uses ChromaDB, Ollama embeddings, includes the required packages, and provides a functional FAQ bot with a single MCP‑tool.
|
||||
Reference in New Issue
Block a user