Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .env +1 -0
- .gitattributes +2 -0
- 7817_1.csv +3 -0
- Dockerfile +21 -0
- README.md +34 -10
- app.py +56 -0
- chroma_db/73f185a9-e985-49ef-8c16-8c9acf109315/data_level0.bin +3 -0
- chroma_db/73f185a9-e985-49ef-8c16-8c9acf109315/header.bin +3 -0
- chroma_db/73f185a9-e985-49ef-8c16-8c9acf109315/index_metadata.pickle +3 -0
- chroma_db/73f185a9-e985-49ef-8c16-8c9acf109315/length.bin +3 -0
- chroma_db/73f185a9-e985-49ef-8c16-8c9acf109315/link_lists.bin +3 -0
- chroma_db/chroma.sqlite3 +3 -0
- main.py +45 -0
- preprocess.py +56 -0
- preprocessed_docs.json +0 -0
- rag_model.py +113 -0
- requirements.txt +13 -0
- static/css/style.css +253 -0
- static/js/main.js +75 -0
- templates/index.html +52 -0
.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
GOOGLE_API_KEY=AIzaSyAwFDQXJD3DWkCthwE1EQO9VC3ctrTcrPQ
|
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
7817_1.csv filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
chroma_db/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
|
7817_1.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:92cf56b1520f970d37d237d0e4f393efffbdab14ba15d52b14e8868f7d5eb3ba
|
| 3 |
+
size 18386219
|
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
COPY requirements.txt .
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
COPY . .
|
| 14 |
+
|
| 15 |
+
# Ensure the vector store and data are present
|
| 16 |
+
# preprocessed_docs.json and chroma_db/ should be part of the build
|
| 17 |
+
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
# We'll use gunicorn for the Space
|
| 21 |
+
CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
|
README.md
CHANGED
|
@@ -1,10 +1,34 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Retail Product Knowledge Assistant (RAG Model)
|
| 2 |
+
|
| 3 |
+
This project builds a Retrieval-Augmented Generation (RAG) model using retail product data (Kindle reviews and details). It uses a vector database to store product information and an LLM to answer questions naturally.
|
| 4 |
+
|
| 5 |
+
## Tech Stack
|
| 6 |
+
- **LLM**: Google Gemini (via `langchain-google-genai`)
|
| 7 |
+
- **Embeddings**: HuggingFace (`all-MiniLM-L6-v2`)
|
| 8 |
+
- **Vector Store**: ChromaDB
|
| 9 |
+
- **Framework**: LangChain
|
| 10 |
+
|
| 11 |
+
## Setup
|
| 12 |
+
1. **API Key**: Add your Google Gemini API Key to the `.env` file:
|
| 13 |
+
```env
|
| 14 |
+
GOOGLE_API_KEY=your_actual_key_here
|
| 15 |
+
```
|
| 16 |
+
2. **Build Knowledge Base**:
|
| 17 |
+
Run the following command to process the data and build the vector database:
|
| 18 |
+
```bash
|
| 19 |
+
python main.py
|
| 20 |
+
```
|
| 21 |
+
(It will automatically detect if the database needs to be built).
|
| 22 |
+
|
| 23 |
+
## Usage
|
| 24 |
+
Run `main.py` and ask questions about the products, such as:
|
| 25 |
+
- "Which Kindle model has the best resolution?"
|
| 26 |
+
- "What do users say about the battery life of the Paperwhite?"
|
| 27 |
+
- "Is the Kindle Voyage worth the extra money?"
|
| 28 |
+
|
| 29 |
+
## Files
|
| 30 |
+
- `7817_1.csv`: Raw product data.
|
| 31 |
+
- `preprocess.py`: Cleans and formats data into JSON.
|
| 32 |
+
- `rag_model.py`: Contains the logic for the RAG pipeline.
|
| 33 |
+
- `main.py`: Interactive CLI for user queries.
|
| 34 |
+
- `chroma_db/`: Directory where the vector store is persisted.
|
app.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify
|
| 2 |
+
from rag_model import get_qa_chain, build_rag_system, PERSIST_DIRECTORY, HuggingFaceEmbeddings, Chroma
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
import os
|
| 5 |
+
load_dotenv(override=True)
|
| 6 |
+
print(f"App: API Key loaded (starting with {os.getenv('GOOGLE_API_KEY', 'None')[:5]}...)")
|
| 7 |
+
|
| 8 |
+
app = Flask(__name__)
|
| 9 |
+
|
| 10 |
+
# Load or Build Vector Store
|
| 11 |
+
if not os.path.exists(PERSIST_DIRECTORY):
|
| 12 |
+
print("Vector store not found. Building...")
|
| 13 |
+
vectorstore = build_rag_system()
|
| 14 |
+
else:
|
| 15 |
+
print("Loading existing vector store...")
|
| 16 |
+
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
| 17 |
+
vectorstore = Chroma(persist_directory=PERSIST_DIRECTORY, embedding_function=embeddings)
|
| 18 |
+
|
| 19 |
+
qa_chain = get_qa_chain(vectorstore)
|
| 20 |
+
|
| 21 |
+
@app.route('/')
|
| 22 |
+
def index():
|
| 23 |
+
return render_template('index.html')
|
| 24 |
+
|
| 25 |
+
@app.route('/ask', methods=['POST'])
|
| 26 |
+
def ask():
|
| 27 |
+
data = request.json
|
| 28 |
+
query = data.get('query')
|
| 29 |
+
print(f"Processing query: {query}")
|
| 30 |
+
|
| 31 |
+
if not query:
|
| 32 |
+
return jsonify({"error": "No query provided"}), 400
|
| 33 |
+
|
| 34 |
+
if not qa_chain:
|
| 35 |
+
return jsonify({"error": "QA chain not initialized. Check GOOGLE_API_KEY."}), 500
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
result = qa_chain({"query": query})
|
| 39 |
+
answer = result['result']
|
| 40 |
+
sources = []
|
| 41 |
+
seen = set()
|
| 42 |
+
for doc in result['source_documents']:
|
| 43 |
+
source_name = doc.metadata.get('name', 'Unknown')
|
| 44 |
+
if source_name not in seen:
|
| 45 |
+
sources.append(source_name)
|
| 46 |
+
seen.add(source_name)
|
| 47 |
+
|
| 48 |
+
return jsonify({
|
| 49 |
+
"answer": answer,
|
| 50 |
+
"sources": sources[:3] # Return top 3 unique names
|
| 51 |
+
})
|
| 52 |
+
except Exception as e:
|
| 53 |
+
return jsonify({"error": str(e)}), 500
|
| 54 |
+
|
| 55 |
+
if __name__ == '__main__':
|
| 56 |
+
app.run(debug=True, port=5000)
|
chroma_db/73f185a9-e985-49ef-8c16-8c9acf109315/data_level0.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:97636bb963ce51b1990b3b4290dfe132e5277521604170865da20675fd900624
|
| 3 |
+
size 5328004
|
chroma_db/73f185a9-e985-49ef-8c16-8c9acf109315/header.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6194ad38681db82ca87feaaa099b3e8791f8ded2e54d1241e9719b8a6a18257d
|
| 3 |
+
size 100
|
chroma_db/73f185a9-e985-49ef-8c16-8c9acf109315/index_metadata.pickle
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:def0c5e5c125cba2afce37ddc75157a85026da9bc703e60fbada61868841bfcd
|
| 3 |
+
size 292608
|
chroma_db/73f185a9-e985-49ef-8c16-8c9acf109315/length.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:657bb227bf74711a4e2f80351342c7aa997ce5fa6cfc277b922045d277e649ca
|
| 3 |
+
size 12716
|
chroma_db/73f185a9-e985-49ef-8c16-8c9acf109315/link_lists.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:62e9fa8b0f27e850d27b6db438cf3b7fb6a3fbf0607eef1452e5afd77845972a
|
| 3 |
+
size 26996
|
chroma_db/chroma.sqlite3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f519a0f46127579cddc14f876ca632155b6ab254c22c1f746d7e14bdc677d66d
|
| 3 |
+
size 32440320
|
main.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rag_model import get_qa_chain, HuggingFaceEmbeddings, Chroma
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
def main():
|
| 5 |
+
PERSIST_DIRECTORY = "chroma_db"
|
| 6 |
+
|
| 7 |
+
if not os.path.exists(PERSIST_DIRECTORY):
|
| 8 |
+
print("Knowledge base not built. Building now...")
|
| 9 |
+
from rag_model import build_rag_system
|
| 10 |
+
vectorstore = build_rag_system()
|
| 11 |
+
else:
|
| 12 |
+
print("Loading existing knowledge base...")
|
| 13 |
+
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
| 14 |
+
vectorstore = Chroma(persist_directory=PERSIST_DIRECTORY, embedding_function=embeddings)
|
| 15 |
+
|
| 16 |
+
qa_chain = get_qa_chain(vectorstore)
|
| 17 |
+
|
| 18 |
+
if not qa_chain:
|
| 19 |
+
print("Failed to initialize QA chain. Check your GOOGLE_API_KEY in .env")
|
| 20 |
+
return
|
| 21 |
+
|
| 22 |
+
print("\n--- Retail Product Knowledge Assistant ---")
|
| 23 |
+
print("Type 'exit' to quit.")
|
| 24 |
+
|
| 25 |
+
while True:
|
| 26 |
+
query = input("\nAsk a question about our products: ")
|
| 27 |
+
if query.lower() == 'exit':
|
| 28 |
+
break
|
| 29 |
+
|
| 30 |
+
print("Thinking...")
|
| 31 |
+
try:
|
| 32 |
+
result = qa_chain({"query": query})
|
| 33 |
+
print(f"\nAnswer: {result['result']}")
|
| 34 |
+
print("\nSources (Top Reviews):")
|
| 35 |
+
seen_sources = set()
|
| 36 |
+
for doc in result["source_documents"]:
|
| 37 |
+
source_info = f"{doc.metadata['name']} (Brand: {doc.metadata['brand']})"
|
| 38 |
+
if source_info not in seen_sources:
|
| 39 |
+
print(f"- {source_info}")
|
| 40 |
+
seen_sources.add(source_info)
|
| 41 |
+
except Exception as e:
|
| 42 |
+
print(f"An error occurred: {e}")
|
| 43 |
+
|
| 44 |
+
if __name__ == "__main__":
|
| 45 |
+
main()
|
preprocess.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import json
|
| 3 |
+
|
| 4 |
+
def preprocess_data(file_path):
|
| 5 |
+
# Load the CSV
|
| 6 |
+
df = pd.read_csv(file_path)
|
| 7 |
+
|
| 8 |
+
# Select relevant columns
|
| 9 |
+
# name, brand, categories, reviews.text, reviews.title, reviews.rating
|
| 10 |
+
relevant_cols = ['name', 'brand', 'categories', 'reviews.text', 'reviews.title', 'reviews.rating']
|
| 11 |
+
|
| 12 |
+
# Drop rows with missing reviews
|
| 13 |
+
df = df.dropna(subset=['reviews.text', 'name'])
|
| 14 |
+
|
| 15 |
+
documents = []
|
| 16 |
+
for _, row in df.iterrows():
|
| 17 |
+
name = row['name']
|
| 18 |
+
brand = row.get('brand', 'Unknown')
|
| 19 |
+
categories = row.get('categories', 'N/A')
|
| 20 |
+
text = row['reviews.text']
|
| 21 |
+
title = row.get('reviews.title', '')
|
| 22 |
+
rating = row.get('reviews.rating', 'N/A')
|
| 23 |
+
|
| 24 |
+
# Parse price
|
| 25 |
+
price_str = "Price info not available"
|
| 26 |
+
prices_raw = row.get('prices')
|
| 27 |
+
if pd.notna(prices_raw):
|
| 28 |
+
try:
|
| 29 |
+
# The CSV might have escaped quotes
|
| 30 |
+
prices_data = json.loads(prices_raw.replace('""', '"'))
|
| 31 |
+
if isinstance(prices_data, list) and len(prices_data) > 0:
|
| 32 |
+
best_price = min([p.get('amountMin', float('inf')) for p in prices_data])
|
| 33 |
+
currency = prices_data[0].get('currency', 'USD')
|
| 34 |
+
if best_price != float('inf'):
|
| 35 |
+
price_str = f"{best_price} {currency}"
|
| 36 |
+
except:
|
| 37 |
+
pass
|
| 38 |
+
|
| 39 |
+
doc_content = f"Product: {name}\nBrand: {brand}\nCategories: {categories}\nPrice: {price_str}\nReview Title: {title}\nRating: {rating}\nReview Content: {text}"
|
| 40 |
+
|
| 41 |
+
metadata = {
|
| 42 |
+
"name": name,
|
| 43 |
+
"brand": brand,
|
| 44 |
+
"rating": str(rating),
|
| 45 |
+
"price": price_str
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
documents.append({"content": doc_content, "metadata": metadata})
|
| 49 |
+
|
| 50 |
+
return documents
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
docs = preprocess_data("7817_1.csv")
|
| 54 |
+
with open("preprocessed_docs.json", "w") as f:
|
| 55 |
+
json.dump(docs, f, indent=2)
|
| 56 |
+
print(f"Preprocessed {len(docs)} documents.")
|
preprocessed_docs.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
rag_model.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 5 |
+
from langchain_community.vectorstores import Chroma
|
| 6 |
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
| 7 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 8 |
+
from langchain_classic.chains import RetrievalQA
|
| 9 |
+
from langchain_core.prompts import PromptTemplate
|
| 10 |
+
from langchain_core.documents import Document
|
| 11 |
+
|
| 12 |
+
load_dotenv(override=True)
|
| 13 |
+
|
| 14 |
+
# Configuration
|
| 15 |
+
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
| 16 |
+
if GOOGLE_API_KEY:
|
| 17 |
+
print(f"RAG Model: API Key loaded (starting with {GOOGLE_API_KEY[:5]}...)")
|
| 18 |
+
else:
|
| 19 |
+
print("RAG Model: No API Key found in environment!")
|
| 20 |
+
DATA_FILE = "preprocessed_docs.json"
|
| 21 |
+
PERSIST_DIRECTORY = "chroma_db"
|
| 22 |
+
|
| 23 |
+
def build_rag_system():
|
| 24 |
+
if not os.path.exists(DATA_FILE):
|
| 25 |
+
print("Data file not found. Please run preprocess.py first.")
|
| 26 |
+
return None
|
| 27 |
+
|
| 28 |
+
with open(DATA_FILE, "r") as f:
|
| 29 |
+
docs_data = json.load(f)
|
| 30 |
+
|
| 31 |
+
print(f"Loading {len(docs_data)} documents...")
|
| 32 |
+
|
| 33 |
+
# Prepare documents for LangChain
|
| 34 |
+
documents = [Document(page_content=d["content"], metadata=d["metadata"]) for d in docs_data]
|
| 35 |
+
|
| 36 |
+
# Split documents into chunks (optional, but good practice)
|
| 37 |
+
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
|
| 38 |
+
chunks = text_splitter.split_documents(documents)
|
| 39 |
+
|
| 40 |
+
print(f"Created {len(chunks)} chunks.")
|
| 41 |
+
|
| 42 |
+
# Initialize Embeddings
|
| 43 |
+
print("Initializing embeddings (HuggingFace)...")
|
| 44 |
+
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
| 45 |
+
|
| 46 |
+
# Initialize Vector Store
|
| 47 |
+
print("Building vector store...")
|
| 48 |
+
vectorstore = Chroma.from_documents(
|
| 49 |
+
documents=chunks,
|
| 50 |
+
embedding=embeddings,
|
| 51 |
+
persist_directory=PERSIST_DIRECTORY
|
| 52 |
+
)
|
| 53 |
+
vectorstore.persist()
|
| 54 |
+
print("Vector store built and persisted.")
|
| 55 |
+
|
| 56 |
+
return vectorstore
|
| 57 |
+
|
| 58 |
+
def get_qa_chain(vectorstore):
|
| 59 |
+
if not GOOGLE_API_KEY:
|
| 60 |
+
print("Warning: GOOGLE_API_KEY not found. LLM functionality will not work.")
|
| 61 |
+
return None
|
| 62 |
+
|
| 63 |
+
llm = ChatGoogleGenerativeAI(model="gemini-flash-latest", google_api_key=GOOGLE_API_KEY)
|
| 64 |
+
|
| 65 |
+
# Custom Prompt
|
| 66 |
+
template = """You are a helpful and expert Retail Product Assistant.
|
| 67 |
+
|
| 68 |
+
Context (Product Details & Reviews):
|
| 69 |
+
{context}
|
| 70 |
+
|
| 71 |
+
Rules:
|
| 72 |
+
1. If the user says "hi", "hello" or greets you, greet them back warmly and mention 2-3 popular products from the context to get started.
|
| 73 |
+
2. Use the provided context to answer specific questions.
|
| 74 |
+
3. If the answer is not in the context, politely say you don't have that specific information.
|
| 75 |
+
4. Maintain a professional yet friendly tone.
|
| 76 |
+
5. Always use Markdown for formatting (bolding, lists, etc.) to make it easy to read.
|
| 77 |
+
6. Use bullet points if listing features or pros/cons.
|
| 78 |
+
7. IMPORTANT: Convert all prices from USD to Indian Rupees (INR) using an approximate exchange rate of 1 USD = ₹83 INR. Always display the price in INR (₹).
|
| 79 |
+
|
| 80 |
+
Question: {question}
|
| 81 |
+
Answer:"""
|
| 82 |
+
|
| 83 |
+
QA_CHAIN_PROMPT = PromptTemplate.from_template(template)
|
| 84 |
+
|
| 85 |
+
qa_chain = RetrievalQA.from_chain_type(
|
| 86 |
+
llm=llm,
|
| 87 |
+
chain_type="stuff",
|
| 88 |
+
retriever=vectorstore.as_retriever(),
|
| 89 |
+
return_source_documents=True,
|
| 90 |
+
chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
return qa_chain
|
| 94 |
+
|
| 95 |
+
if __name__ == "__main__":
|
| 96 |
+
# Check if vector store exists
|
| 97 |
+
if os.path.exists(PERSIST_DIRECTORY):
|
| 98 |
+
print("Vector store already exists. Loading...")
|
| 99 |
+
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
| 100 |
+
vectorstore = Chroma(persist_directory=PERSIST_DIRECTORY, embedding_function=embeddings)
|
| 101 |
+
else:
|
| 102 |
+
vectorstore = build_rag_system()
|
| 103 |
+
|
| 104 |
+
if vectorstore:
|
| 105 |
+
qa_chain = get_qa_chain(vectorstore)
|
| 106 |
+
if qa_chain:
|
| 107 |
+
query = "Which Kindle model has the best resolution according to reviews?"
|
| 108 |
+
print(f"\nQuestion: {query}")
|
| 109 |
+
result = qa_chain({"query": query})
|
| 110 |
+
print(f"\nAnswer: {result['result']}")
|
| 111 |
+
print("\nSources:")
|
| 112 |
+
for doc in result["source_documents"][:2]:
|
| 113 |
+
print(f"- {doc.metadata['name']} (Rating: {doc.metadata['rating']})")
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
python-dotenv
|
| 3 |
+
langchain
|
| 4 |
+
langchain-community
|
| 5 |
+
langchain-google-genai
|
| 6 |
+
langchain-text-splitters
|
| 7 |
+
langchain-classic
|
| 8 |
+
chromadb
|
| 9 |
+
sentence-transformers
|
| 10 |
+
pandas
|
| 11 |
+
google-generativeai
|
| 12 |
+
huggingface_hub
|
| 13 |
+
gunicorn
|
static/css/style.css
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--primary-color: #6366f1;
|
| 3 |
+
--primary-hover: #4f46e5;
|
| 4 |
+
--bg-dark: #0f172a;
|
| 5 |
+
--bg-card: rgba(30, 41, 59, 0.7);
|
| 6 |
+
--text-main: #f8fafc;
|
| 7 |
+
--text-muted: #94a3b8;
|
| 8 |
+
--glass-border: rgba(255, 255, 255, 0.1);
|
| 9 |
+
--accent-glow: rgba(99, 102, 241, 0.2);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
* {
|
| 13 |
+
margin: 0;
|
| 14 |
+
padding: 0;
|
| 15 |
+
box-sizing: border-box;
|
| 16 |
+
font-family: 'Inter', sans-serif;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
body {
|
| 20 |
+
background-color: var(--bg-dark);
|
| 21 |
+
background-image:
|
| 22 |
+
radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0, transparent 50%),
|
| 23 |
+
radial-gradient(at 100% 100%, rgba(168, 85, 247, 0.15) 0, transparent 50%);
|
| 24 |
+
color: var(--text-main);
|
| 25 |
+
min-height: 100vh;
|
| 26 |
+
display: flex;
|
| 27 |
+
flex-direction: column;
|
| 28 |
+
align-items: center;
|
| 29 |
+
padding: 2rem;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.container {
|
| 33 |
+
width: 100%;
|
| 34 |
+
max-width: 900px;
|
| 35 |
+
z-index: 1;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
header {
|
| 39 |
+
text-align: center;
|
| 40 |
+
margin-bottom: 3rem;
|
| 41 |
+
animation: fadeInDown 0.8s ease-out;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
h1 {
|
| 45 |
+
font-size: 2.5rem;
|
| 46 |
+
font-weight: 800;
|
| 47 |
+
background: linear-gradient(to right, #818cf8, #c084fc);
|
| 48 |
+
-webkit-background-clip: text;
|
| 49 |
+
-webkit-text-fill-color: transparent;
|
| 50 |
+
margin-bottom: 0.5rem;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
p.subtitle {
|
| 54 |
+
color: var(--text-muted);
|
| 55 |
+
font-size: 1.1rem;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.chat-card {
|
| 59 |
+
background: var(--bg-card);
|
| 60 |
+
backdrop-filter: blur(12px);
|
| 61 |
+
border: 1px solid var(--glass-border);
|
| 62 |
+
border-radius: 1.5rem;
|
| 63 |
+
padding: 2rem;
|
| 64 |
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
| 65 |
+
display: flex;
|
| 66 |
+
flex-direction: column;
|
| 67 |
+
gap: 1.5rem;
|
| 68 |
+
animation: fadeInUp 0.8s ease-out;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.chat-history {
|
| 72 |
+
height: 400px;
|
| 73 |
+
overflow-y: auto;
|
| 74 |
+
display: flex;
|
| 75 |
+
flex-direction: column;
|
| 76 |
+
gap: 1rem;
|
| 77 |
+
padding-right: 0.5rem;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.chat-history::-webkit-scrollbar {
|
| 81 |
+
width: 6px;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.chat-history::-webkit-scrollbar-thumb {
|
| 85 |
+
background: var(--glass-border);
|
| 86 |
+
border-radius: 10px;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.message {
|
| 90 |
+
padding: 1rem;
|
| 91 |
+
border-radius: 1rem;
|
| 92 |
+
max-width: 85%;
|
| 93 |
+
line-height: 1.5;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.user-message {
|
| 97 |
+
background: var(--primary-color);
|
| 98 |
+
align-self: flex-end;
|
| 99 |
+
border-bottom-right-radius: 0.2rem;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.bot-message {
|
| 103 |
+
background: rgba(51, 65, 85, 0.5);
|
| 104 |
+
align-self: flex-start;
|
| 105 |
+
border-bottom-left-radius: 0.2rem;
|
| 106 |
+
border: 1px solid var(--glass-border);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.bot-message p {
|
| 110 |
+
margin-bottom: 0.5rem;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.bot-message p:last-child {
|
| 114 |
+
margin-bottom: 0;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.bot-message ul,
|
| 118 |
+
.bot-message ol {
|
| 119 |
+
margin-left: 1.5rem;
|
| 120 |
+
margin-bottom: 0.5rem;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.bot-message li {
|
| 124 |
+
margin-bottom: 0.25rem;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.input-group {
|
| 128 |
+
display: flex;
|
| 129 |
+
gap: 0.75rem;
|
| 130 |
+
background: rgba(0, 0, 0, 0.2);
|
| 131 |
+
padding: 0.5rem;
|
| 132 |
+
border-radius: 1rem;
|
| 133 |
+
border: 1px solid var(--glass-border);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
input {
|
| 137 |
+
flex: 1;
|
| 138 |
+
background: transparent;
|
| 139 |
+
border: none;
|
| 140 |
+
color: var(--text-main);
|
| 141 |
+
padding: 0.75rem 1rem;
|
| 142 |
+
font-size: 1rem;
|
| 143 |
+
outline: none;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
button {
|
| 147 |
+
background: var(--primary-color);
|
| 148 |
+
color: white;
|
| 149 |
+
border: none;
|
| 150 |
+
padding: 0.75rem 1.5rem;
|
| 151 |
+
border-radius: 0.75rem;
|
| 152 |
+
font-weight: 600;
|
| 153 |
+
cursor: pointer;
|
| 154 |
+
transition: all 0.3s ease;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
button:hover {
|
| 158 |
+
background: var(--primary-hover);
|
| 159 |
+
transform: translateY(-2px);
|
| 160 |
+
box-shadow: 0 10px 15px -3px var(--accent-glow);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.sources {
|
| 164 |
+
margin-top: 0.5rem;
|
| 165 |
+
font-size: 0.8rem;
|
| 166 |
+
color: var(--text-muted);
|
| 167 |
+
border-top: 1px solid var(--glass-border);
|
| 168 |
+
padding-top: 0.5rem;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.source-tag {
|
| 172 |
+
display: inline-block;
|
| 173 |
+
background: rgba(99, 102, 241, 0.15);
|
| 174 |
+
padding: 0.2rem 0.6rem;
|
| 175 |
+
border-radius: 0.4rem;
|
| 176 |
+
margin-right: 0.5rem;
|
| 177 |
+
margin-top: 0.5rem;
|
| 178 |
+
border: 1px solid rgba(99, 102, 241, 0.2);
|
| 179 |
+
color: #a5b4fc;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.suggestions {
|
| 183 |
+
margin: 0.5rem 0;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.suggestions p {
|
| 187 |
+
font-size: 0.85rem;
|
| 188 |
+
color: var(--text-muted);
|
| 189 |
+
margin-bottom: 0.75rem;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.suggestion-chips {
|
| 193 |
+
display: flex;
|
| 194 |
+
flex-wrap: wrap;
|
| 195 |
+
gap: 0.5rem;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.chip {
|
| 199 |
+
background: rgba(255, 255, 255, 0.05);
|
| 200 |
+
border: 1px solid var(--glass-border);
|
| 201 |
+
color: var(--text-main);
|
| 202 |
+
padding: 0.4rem 0.8rem;
|
| 203 |
+
border-radius: 2rem;
|
| 204 |
+
font-size: 0.8rem;
|
| 205 |
+
cursor: pointer;
|
| 206 |
+
transition: all 0.2s ease;
|
| 207 |
+
width: auto;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.chip:hover {
|
| 211 |
+
background: rgba(99, 102, 241, 0.2);
|
| 212 |
+
border-color: var(--primary-color);
|
| 213 |
+
transform: translateY(-1px);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
@keyframes fadeInDown {
|
| 217 |
+
from {
|
| 218 |
+
opacity: 0;
|
| 219 |
+
transform: translateY(-20px);
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
to {
|
| 223 |
+
opacity: 1;
|
| 224 |
+
transform: translateY(0);
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
@keyframes fadeInUp {
|
| 229 |
+
from {
|
| 230 |
+
opacity: 0;
|
| 231 |
+
transform: translateY(20px);
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
to {
|
| 235 |
+
opacity: 1;
|
| 236 |
+
transform: translateY(0);
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.loading-dots:after {
|
| 241 |
+
content: '...';
|
| 242 |
+
display: inline-block;
|
| 243 |
+
width: 0;
|
| 244 |
+
animation: dots 1.5s steps(4, end) infinite;
|
| 245 |
+
overflow: hidden;
|
| 246 |
+
vertical-align: bottom;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
@keyframes dots {
|
| 250 |
+
to {
|
| 251 |
+
width: 1.25em;
|
| 252 |
+
}
|
| 253 |
+
}
|
static/js/main.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
const chatHistory = document.getElementById('chat-history');
|
| 3 |
+
const queryInput = document.getElementById('query-input');
|
| 4 |
+
const sendBtn = document.getElementById('send-btn');
|
| 5 |
+
|
| 6 |
+
const appendMessage = (content, isUser, sources = []) => {
|
| 7 |
+
const msgDiv = document.createElement('div');
|
| 8 |
+
msgDiv.className = `message ${isUser ? 'user-message' : 'bot-message'}`;
|
| 9 |
+
|
| 10 |
+
// Parse markdown if it's from the bot
|
| 11 |
+
const parsedContent = isUser ? content : marked.parse(content);
|
| 12 |
+
let html = `<div class="content">${parsedContent}</div>`;
|
| 13 |
+
|
| 14 |
+
if (!isUser && sources.length > 0) {
|
| 15 |
+
html += '<div class="sources"><strong>Sources:</strong><br>';
|
| 16 |
+
sources.forEach(src => {
|
| 17 |
+
html += `<span class="source-tag">${src}</span>`;
|
| 18 |
+
});
|
| 19 |
+
html += '</div>';
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
msgDiv.innerHTML = html;
|
| 23 |
+
chatHistory.appendChild(msgDiv);
|
| 24 |
+
chatHistory.scrollTop = chatHistory.scrollHeight;
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
const handleQuery = async (forcedQuery = null) => {
|
| 28 |
+
const query = (forcedQuery || queryInput.value).trim();
|
| 29 |
+
if (!query) return;
|
| 30 |
+
|
| 31 |
+
appendMessage(query, true);
|
| 32 |
+
queryInput.value = '';
|
| 33 |
+
|
| 34 |
+
// Add loading indicator
|
| 35 |
+
const loadingDiv = document.createElement('div');
|
| 36 |
+
loadingDiv.className = 'message bot-message loading-indicator';
|
| 37 |
+
loadingDiv.innerHTML = '<span class="loading-dots">Thinking</span>';
|
| 38 |
+
chatHistory.appendChild(loadingDiv);
|
| 39 |
+
chatHistory.scrollTop = chatHistory.scrollHeight;
|
| 40 |
+
|
| 41 |
+
try {
|
| 42 |
+
const response = await fetch('/ask', {
|
| 43 |
+
method: 'POST',
|
| 44 |
+
headers: { 'Content-Type': 'application/json' },
|
| 45 |
+
body: JSON.stringify({ query })
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
const data = await response.json();
|
| 49 |
+
|
| 50 |
+
// Remove loading
|
| 51 |
+
chatHistory.removeChild(loadingDiv);
|
| 52 |
+
|
| 53 |
+
if (data.answer) {
|
| 54 |
+
appendMessage(data.answer, false, data.sources);
|
| 55 |
+
} else {
|
| 56 |
+
appendMessage("Sorry, I encountered an error: " + (data.error || "Unknown error"), false);
|
| 57 |
+
}
|
| 58 |
+
} catch (error) {
|
| 59 |
+
chatHistory.removeChild(loadingDiv);
|
| 60 |
+
appendMessage("Connection error. Is the server running?", false);
|
| 61 |
+
}
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
// Suggestion chips
|
| 65 |
+
document.querySelectorAll('.chip').forEach(chip => {
|
| 66 |
+
chip.addEventListener('click', () => {
|
| 67 |
+
handleQuery(chip.getAttribute('data-query'));
|
| 68 |
+
});
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
sendBtn.addEventListener('click', () => handleQuery());
|
| 72 |
+
queryInput.addEventListener('keypress', (e) => {
|
| 73 |
+
if (e.key === 'Enter') handleQuery();
|
| 74 |
+
});
|
| 75 |
+
});
|
templates/index.html
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Retail Assistant | Product Knowledge RAG</title>
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
| 12 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 13 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 14 |
+
</head>
|
| 15 |
+
|
| 16 |
+
<body>
|
| 17 |
+
<div class="container">
|
| 18 |
+
<header>
|
| 19 |
+
<h1><i class="fas fa-robot"></i> Retail Genius</h1>
|
| 20 |
+
<p class="subtitle">AI-Powered Product Knowledge Assistant</p>
|
| 21 |
+
</header>
|
| 22 |
+
|
| 23 |
+
<main class="chat-card">
|
| 24 |
+
<div id="chat-history" class="chat-history">
|
| 25 |
+
<div class="message bot-message">
|
| 26 |
+
<i class="fas fa-hand-wave"></i> Hello! I'm your retail knowledge assistant. Ask me anything about
|
| 27 |
+
our products, pricing, or customer reviews!
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<div class="suggestions">
|
| 32 |
+
<p><i class="fas fa-lightbulb"></i> Try asking:</p>
|
| 33 |
+
<div class="suggestion-chips">
|
| 34 |
+
<button class="chip" data-query="Which Kindle has the best resolution?">Kindle Resolution</button>
|
| 35 |
+
<button class="chip" data-query="Compare Paperwhite vs Voyage battery life.">Paperwhite vs
|
| 36 |
+
Voyage</button>
|
| 37 |
+
<button class="chip" data-query="What do reviews say about the Fire TV?">Fire TV Reviews</button>
|
| 38 |
+
<button class="chip" data-query="Is there a Kindle with physical buttons?">Physical
|
| 39 |
+
Buttons?</button>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
<div class="input-group">
|
| 44 |
+
<input type="text" id="query-input" placeholder="Ask a question..." autocomplete="off">
|
| 45 |
+
<button id="send-btn"><i class="fas fa-paper-plane"></i></button>
|
| 46 |
+
</div>
|
| 47 |
+
</main>
|
| 48 |
+
</div>
|
| 49 |
+
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
| 50 |
+
</body>
|
| 51 |
+
|
| 52 |
+
</html>
|