Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files- Dockerfile +27 -0
- main.py +103 -0
- requirements.txt +8 -0
- static/styles.css +38 -0
- templates/chat.html +35 -0
- vectordb_utils.py +31 -0
Dockerfile
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Base image with Python
|
2 |
+
FROM python:3.10-slim
|
3 |
+
|
4 |
+
# Install system dependencies
|
5 |
+
RUN apt-get update && apt-get install -y \
|
6 |
+
git \
|
7 |
+
curl \
|
8 |
+
&& rm -rf /var/lib/apt/lists/*
|
9 |
+
|
10 |
+
# Set work directory
|
11 |
+
WORKDIR /app
|
12 |
+
|
13 |
+
# Copy application files
|
14 |
+
COPY . /app
|
15 |
+
|
16 |
+
# Install Python dependencies
|
17 |
+
RUN pip install --no-cache-dir --upgrade pip
|
18 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
19 |
+
|
20 |
+
# Download SentenceTransformer model (caches on build)
|
21 |
+
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')"
|
22 |
+
|
23 |
+
# Expose the app on port 7860 for Hugging Face Spaces
|
24 |
+
ENV PORT 7860
|
25 |
+
|
26 |
+
# Start FastAPI with uvicorn
|
27 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
main.py
ADDED
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# main.py
|
2 |
+
|
3 |
+
from fastapi import FastAPI, Request, Form
|
4 |
+
from fastapi.responses import HTMLResponse
|
5 |
+
from fastapi.staticfiles import StaticFiles
|
6 |
+
from fastapi.templating import Jinja2Templates
|
7 |
+
from dotenv import load_dotenv
|
8 |
+
|
9 |
+
# Load environment variables
|
10 |
+
load_dotenv()
|
11 |
+
from gliner import GLiNER
|
12 |
+
from groq import Groq
|
13 |
+
from vectordb_utils import search_vectordb, init_qdrant_collection
|
14 |
+
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
15 |
+
app = FastAPI()
|
16 |
+
templates = Jinja2Templates(directory="templates")
|
17 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
18 |
+
|
19 |
+
# Load models
|
20 |
+
gliner_model = GLiNER.from_pretrained("urchade/gliner_medium-v2.1")
|
21 |
+
groq_client = Groq(api_key=GROQ_API_KEY)
|
22 |
+
|
23 |
+
init_qdrant_collection()
|
24 |
+
|
25 |
+
def extract_entities(text):
|
26 |
+
labels = ["PRODUCT", "ISSUE", "PROBLEM", "SERVICE"]
|
27 |
+
return gliner_model.predict_entities(text, labels)
|
28 |
+
|
29 |
+
def validate_answer(user_query, retrieved_answer):
|
30 |
+
prompt = f"""
|
31 |
+
You are a validator assistant.
|
32 |
+
|
33 |
+
Given the user query and the answer retrieved from a knowledge base, decide if the answer is relevant and correctly addresses the query.
|
34 |
+
|
35 |
+
Respond ONLY with:
|
36 |
+
- "YES" if the answer is appropriate.
|
37 |
+
- "NO" if the answer is unrelated, inaccurate, or insufficient.
|
38 |
+
|
39 |
+
User Query:
|
40 |
+
{user_query}
|
41 |
+
|
42 |
+
Retrieved Answer:
|
43 |
+
{retrieved_answer}
|
44 |
+
|
45 |
+
Is the answer appropriate?
|
46 |
+
"""
|
47 |
+
completion = groq_client.chat.completions.create(
|
48 |
+
model="meta-llama/llama-4-scout-17b-16e-instruct",
|
49 |
+
messages=[{"role": "user", "content": prompt}],
|
50 |
+
temperature=0, max_completion_tokens=10, top_p=1
|
51 |
+
)
|
52 |
+
return completion.choices[0].message.content.strip()
|
53 |
+
|
54 |
+
def generate_response(user_query, validated_answer):
|
55 |
+
prompt = f"""
|
56 |
+
You are a customer support agent.
|
57 |
+
|
58 |
+
Using the following validated support answer, respond helpfully and politely to the user's query.
|
59 |
+
warning: there should not be [],<> tags in the response
|
60 |
+
|
61 |
+
User Query:
|
62 |
+
{user_query}
|
63 |
+
|
64 |
+
Support Answer:
|
65 |
+
{validated_answer}
|
66 |
+
|
67 |
+
Compose your response:
|
68 |
+
"""
|
69 |
+
completion = groq_client.chat.completions.create(
|
70 |
+
model="meta-llama/llama-4-scout-17b-16e-instruct",
|
71 |
+
messages=[{"role": "user", "content": prompt}],
|
72 |
+
temperature=0.8, max_completion_tokens=1000, top_p=1
|
73 |
+
)
|
74 |
+
return completion.choices[0].message.content.strip()
|
75 |
+
|
76 |
+
@app.get("/", response_class=HTMLResponse)
|
77 |
+
async def home(request: Request):
|
78 |
+
return templates.TemplateResponse("chat.html", {"request": request, "chat_history": []})
|
79 |
+
|
80 |
+
@app.post("/chat", response_class=HTMLResponse)
|
81 |
+
async def chat(request: Request, message: str = Form(...)):
|
82 |
+
entities = extract_entities(message)
|
83 |
+
entity_info = [(e['text'], e['label']) for e in entities]
|
84 |
+
|
85 |
+
results = search_vectordb(message)
|
86 |
+
if not results:
|
87 |
+
bot_reply = "Sorry, I couldn't find anything helpful."
|
88 |
+
else:
|
89 |
+
answer = results[0].payload["response"]
|
90 |
+
if validate_answer(message, answer) == "YES":
|
91 |
+
bot_reply = generate_response(message, answer)
|
92 |
+
else:
|
93 |
+
bot_reply = "Sorry, I couldn't find a suitable answer. Please contact support."
|
94 |
+
|
95 |
+
chat_history = [
|
96 |
+
{"sender": "User", "message": message},
|
97 |
+
{"sender": "Bot", "message": bot_reply}
|
98 |
+
]
|
99 |
+
return templates.TemplateResponse("chat.html", {
|
100 |
+
"request": request,
|
101 |
+
"chat_history": chat_history,
|
102 |
+
"entities": entity_info
|
103 |
+
})
|
requirements.txt
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi
|
2 |
+
uvicorn
|
3 |
+
jinja2
|
4 |
+
gliner
|
5 |
+
groq
|
6 |
+
qdrant-client
|
7 |
+
sentence-transformers
|
8 |
+
dotenv
|
static/styles.css
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
font-family: Arial, sans-serif;
|
3 |
+
background-color: #f5f7fa;
|
4 |
+
padding: 20px;
|
5 |
+
}
|
6 |
+
.chat-container {
|
7 |
+
max-width: 600px;
|
8 |
+
margin: auto;
|
9 |
+
background: white;
|
10 |
+
padding: 20px;
|
11 |
+
border-radius: 8px;
|
12 |
+
}
|
13 |
+
.chat-box {
|
14 |
+
border: 1px solid #ccc;
|
15 |
+
padding: 10px;
|
16 |
+
max-height: 300px;
|
17 |
+
overflow-y: auto;
|
18 |
+
margin-bottom: 10px;
|
19 |
+
}
|
20 |
+
.user {
|
21 |
+
text-align: right;
|
22 |
+
color: #0066cc;
|
23 |
+
}
|
24 |
+
.bot {
|
25 |
+
text-align: left;
|
26 |
+
color: #333;
|
27 |
+
}
|
28 |
+
form {
|
29 |
+
display: flex;
|
30 |
+
gap: 10px;
|
31 |
+
}
|
32 |
+
input[type="text"] {
|
33 |
+
flex-grow: 1;
|
34 |
+
padding: 8px;
|
35 |
+
}
|
36 |
+
.entities {
|
37 |
+
margin-top: 20px;
|
38 |
+
}
|
templates/chat.html
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html>
|
3 |
+
<head>
|
4 |
+
<title>Support Chatbot</title>
|
5 |
+
<link rel="stylesheet" href="/static/styles.css">
|
6 |
+
</head>
|
7 |
+
<body>
|
8 |
+
<div class="chat-container">
|
9 |
+
<h2>🧠 Customer Support Chatbot</h2>
|
10 |
+
<div class="chat-box">
|
11 |
+
{% for entry in chat_history %}
|
12 |
+
<div class="{{ 'user' if entry.sender == 'User' else 'bot' }}">
|
13 |
+
<strong>{{ entry.sender }}:</strong> {{ entry.message }}
|
14 |
+
</div>
|
15 |
+
{% endfor %}
|
16 |
+
</div>
|
17 |
+
|
18 |
+
<form method="post" action="/chat">
|
19 |
+
<input type="text" name="message" placeholder="Type your question..." required>
|
20 |
+
<button type="submit">Send</button>
|
21 |
+
</form>
|
22 |
+
|
23 |
+
{% if entities %}
|
24 |
+
<div class="entities">
|
25 |
+
<h4>🔍 Detected Entities:</h4>
|
26 |
+
<ul>
|
27 |
+
{% for text, label in entities %}
|
28 |
+
<li>{{ label }}: <em>{{ text }}</em></li>
|
29 |
+
{% endfor %}
|
30 |
+
</ul>
|
31 |
+
</div>
|
32 |
+
{% endif %}
|
33 |
+
</div>
|
34 |
+
</body>
|
35 |
+
</html>
|
vectordb_utils.py
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# vectordb_utils.py
|
2 |
+
|
3 |
+
from qdrant_client import QdrantClient
|
4 |
+
from qdrant_client.models import VectorParams, Distance, PointStruct
|
5 |
+
from sentence_transformers import SentenceTransformer
|
6 |
+
import uuid
|
7 |
+
|
8 |
+
encoder = SentenceTransformer("all-MiniLM-L6-v2")
|
9 |
+
qdrant = QdrantClient(":memory:")
|
10 |
+
collection_name = "customer_support_docsv1"
|
11 |
+
|
12 |
+
def init_qdrant_collection():
|
13 |
+
qdrant.recreate_collection(
|
14 |
+
collection_name=collection_name,
|
15 |
+
vectors_config=VectorParams(size=384, distance=Distance.COSINE)
|
16 |
+
)
|
17 |
+
|
18 |
+
def add_to_vectordb(query, response):
|
19 |
+
vector = encoder.encode(query).tolist()
|
20 |
+
qdrant.upload_points(
|
21 |
+
collection_name=collection_name,
|
22 |
+
points=[PointStruct(
|
23 |
+
id=str(uuid.uuid4()),
|
24 |
+
vector=vector,
|
25 |
+
payload={"query": query, "response": response}
|
26 |
+
)]
|
27 |
+
)
|
28 |
+
|
29 |
+
def search_vectordb(query, limit=3):
|
30 |
+
vector = encoder.encode(query).tolist()
|
31 |
+
return qdrant.search(collection_name=collection_name, query_vector=vector, limit=limit)
|