Spaces:
Running
Running
Waitress better than gunicorn
Browse files- Dockerfile +24 -18
- app.py +59 -71
- requirements.txt +1 -0
- templates/index.html +74 -41
Dockerfile
CHANGED
@@ -1,12 +1,26 @@
|
|
1 |
FROM python:3.9
|
2 |
|
3 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
RUN apt-get update && apt-get install -y \
|
5 |
python3 \
|
6 |
python3-pip \
|
7 |
python3-venv \
|
8 |
wget \
|
9 |
-
build-essential
|
|
|
10 |
|
11 |
# Install newer SQLite version
|
12 |
WORKDIR /tmp
|
@@ -20,25 +34,17 @@ RUN wget https://www.sqlite.org/2023/sqlite-autoconf-3410200.tar.gz \
|
|
20 |
# Update library path to use the new SQLite
|
21 |
ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
22 |
|
23 |
-
|
24 |
-
RUN useradd -m -u 1000 user
|
25 |
-
|
26 |
-
# Create necessary directories with proper permissions
|
27 |
-
RUN mkdir -p /app/chroma_store && chown -R user:user /app
|
28 |
|
29 |
-
#
|
30 |
USER user
|
31 |
-
ENV PATH="/home/user/.local/bin:$PATH"
|
32 |
-
|
33 |
-
# Set working directory
|
34 |
WORKDIR /app
|
35 |
|
36 |
-
# Copy
|
37 |
-
COPY --chown=user ./requirements.txt requirements.txt
|
38 |
-
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
39 |
-
|
40 |
-
# Copy the source code
|
41 |
COPY --chown=user . /app
|
42 |
|
43 |
-
#
|
44 |
-
|
|
|
|
|
|
|
|
1 |
FROM python:3.9
|
2 |
|
3 |
+
# Create a non-root user
|
4 |
+
RUN useradd -m -u 1000 user
|
5 |
+
USER user
|
6 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
7 |
+
|
8 |
+
# Set working directory
|
9 |
+
WORKDIR /app
|
10 |
+
|
11 |
+
# Copy and install Python dependencies
|
12 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
13 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
14 |
+
|
15 |
+
# Install system dependencies and build tools
|
16 |
+
USER root
|
17 |
RUN apt-get update && apt-get install -y \
|
18 |
python3 \
|
19 |
python3-pip \
|
20 |
python3-venv \
|
21 |
wget \
|
22 |
+
build-essential \
|
23 |
+
libsqlite3-dev
|
24 |
|
25 |
# Install newer SQLite version
|
26 |
WORKDIR /tmp
|
|
|
34 |
# Update library path to use the new SQLite
|
35 |
ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
36 |
|
37 |
+
RUN mkdir -p /app/static/chroma_store && chown -R user:user /app/static/chroma_store
|
|
|
|
|
|
|
|
|
38 |
|
39 |
+
# Go back to non-root user and app directory
|
40 |
USER user
|
|
|
|
|
|
|
41 |
WORKDIR /app
|
42 |
|
43 |
+
# Copy the app source code
|
|
|
|
|
|
|
|
|
44 |
COPY --chown=user . /app
|
45 |
|
46 |
+
# Expose port 7860 for the application
|
47 |
+
EXPOSE 7860
|
48 |
+
|
49 |
+
# Start the app using waitress
|
50 |
+
CMD ["waitress-serve", "--listen=0.0.0.0:7860", "app:app"]
|
app.py
CHANGED
@@ -1,9 +1,7 @@
|
|
1 |
-
from dotenv import load_dotenv
|
2 |
-
load_dotenv()
|
3 |
-
|
4 |
-
import os
|
5 |
from flask import Flask, request, jsonify, render_template
|
6 |
from flask_cors import CORS
|
|
|
|
|
7 |
|
8 |
from langchain_community.embeddings import HuggingFaceEmbeddings
|
9 |
from langchain_community.vectorstores import Chroma
|
@@ -11,85 +9,75 @@ from langchain_google_genai import ChatGoogleGenerativeAI
|
|
11 |
from langchain_core.prompts import PromptTemplate
|
12 |
from langchain.chains import RetrievalQA
|
13 |
|
14 |
-
|
|
|
|
|
|
|
|
|
15 |
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
|
16 |
if not GOOGLE_API_KEY:
|
17 |
raise ValueError("GOOGLE_API_KEY not found in environment variables.")
|
18 |
|
19 |
-
#
|
20 |
-
|
21 |
-
model="gemini-2.0-flash-lite",
|
22 |
-
google_api_key=GOOGLE_API_KEY,
|
23 |
-
convert_system_message_to_human=True
|
24 |
-
)
|
25 |
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
|
|
34 |
|
35 |
-
#
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
{question}
|
44 |
-
Answer:
|
45 |
-
""")
|
46 |
|
47 |
-
#
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
|
|
|
|
|
|
|
|
54 |
|
55 |
-
#
|
56 |
-
|
57 |
-
|
58 |
-
|
|
|
|
|
|
|
|
|
59 |
|
60 |
-
# === Step 7: Serve Frontend ===
|
61 |
@app.route("/")
|
62 |
-
def
|
63 |
-
return render_template("index.html")
|
64 |
|
65 |
-
@app.route(
|
66 |
-
def
|
67 |
-
|
68 |
-
|
|
|
|
|
69 |
|
70 |
-
|
71 |
-
@app.route('/api/chat', methods=['POST'])
|
72 |
-
def chat():
|
73 |
-
print("Received request to /api/chat")
|
74 |
try:
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
query = data.get('message', '').strip()
|
79 |
-
print(f"Query: {query}")
|
80 |
-
|
81 |
-
if not query:
|
82 |
-
print("No message provided")
|
83 |
-
return jsonify({"error": "No message provided."}), 400
|
84 |
-
|
85 |
-
response = qa_chain.run(query)
|
86 |
-
print(f"Generated response: {response}")
|
87 |
-
|
88 |
-
return jsonify({"response": response})
|
89 |
except Exception as e:
|
90 |
-
|
91 |
-
return jsonify({"response": f"Sorry, I encountered an error: {str(e)}"}), 500
|
92 |
|
93 |
-
|
94 |
-
|
95 |
-
app.run(host='0.0.0.0', port=7860, debug=False)
|
|
|
|
|
|
|
|
|
|
|
1 |
from flask import Flask, request, jsonify, render_template
|
2 |
from flask_cors import CORS
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
import os
|
5 |
|
6 |
from langchain_community.embeddings import HuggingFaceEmbeddings
|
7 |
from langchain_community.vectorstores import Chroma
|
|
|
9 |
from langchain_core.prompts import PromptTemplate
|
10 |
from langchain.chains import RetrievalQA
|
11 |
|
12 |
+
app = Flask(__name__)
|
13 |
+
CORS(app)
|
14 |
+
|
15 |
+
# Load environment variables
|
16 |
+
load_dotenv()
|
17 |
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
|
18 |
if not GOOGLE_API_KEY:
|
19 |
raise ValueError("GOOGLE_API_KEY not found in environment variables.")
|
20 |
|
21 |
+
# Lazy globals
|
22 |
+
qa_chain = None
|
|
|
|
|
|
|
|
|
23 |
|
24 |
+
def get_qa_chain():
|
25 |
+
global qa_chain
|
26 |
+
if qa_chain is None:
|
27 |
+
# Initialize LLM
|
28 |
+
llm = ChatGoogleGenerativeAI(
|
29 |
+
model="gemini-2.0-flash-lite",
|
30 |
+
google_api_key=GOOGLE_API_KEY,
|
31 |
+
convert_system_message_to_human=True
|
32 |
+
)
|
33 |
|
34 |
+
# Embeddings and vector store
|
35 |
+
embedding_model = HuggingFaceEmbeddings(model_name="BAAI/bge-large-en-v1.5")
|
36 |
+
vectordb = Chroma(
|
37 |
+
persist_directory="chroma_store",
|
38 |
+
embedding_function=embedding_model,
|
39 |
+
collection_name="pdf_search_chroma"
|
40 |
+
)
|
41 |
+
retriever = vectordb.as_retriever(search_kwargs={"k": 6})
|
|
|
|
|
|
|
42 |
|
43 |
+
# Prompt
|
44 |
+
prompt_template = PromptTemplate.from_template("""
|
45 |
+
You are an intelligent assistant for students asking about their university.
|
46 |
+
If answer is not defined or not clearly understood, ask for clarification.
|
47 |
+
Answer clearly and helpfully based on the retrieved context. Do not make up information or suggestions.
|
48 |
+
Context:
|
49 |
+
{context}
|
50 |
+
Question:
|
51 |
+
{question}
|
52 |
+
Answer:
|
53 |
+
""")
|
54 |
|
55 |
+
# Create chain
|
56 |
+
qa_chain = RetrievalQA.from_chain_type(
|
57 |
+
llm=llm,
|
58 |
+
chain_type="stuff",
|
59 |
+
retriever=retriever,
|
60 |
+
chain_type_kwargs={"prompt": prompt_template}
|
61 |
+
)
|
62 |
+
return qa_chain
|
63 |
|
|
|
64 |
@app.route("/")
|
65 |
+
def home():
|
66 |
+
return render_template("index.html")
|
67 |
|
68 |
+
@app.route("/get", methods=["POST"])
|
69 |
+
def get_response():
|
70 |
+
data = request.get_json()
|
71 |
+
query = data.get("message", "")
|
72 |
+
if not query:
|
73 |
+
return jsonify({"response": {"response": "No message received."}}), 400
|
74 |
|
75 |
+
chain = get_qa_chain()
|
|
|
|
|
|
|
76 |
try:
|
77 |
+
response = chain.run(query)
|
78 |
+
return jsonify({"response": {"response": response}})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
except Exception as e:
|
80 |
+
return jsonify({"response": {"response": f"Error: {str(e)}"}}), 500
|
|
|
81 |
|
82 |
+
if __name__ == "__main__":
|
83 |
+
app.run(debug=True)
|
|
requirements.txt
CHANGED
@@ -25,3 +25,4 @@ transformers==4.51.3
|
|
25 |
torch==2.6.0
|
26 |
uvicorn==0.34.1
|
27 |
gunicorn
|
|
|
|
25 |
torch==2.6.0
|
26 |
uvicorn==0.34.1
|
27 |
gunicorn
|
28 |
+
waitress==3.0.2
|
templates/index.html
CHANGED
@@ -19,7 +19,6 @@
|
|
19 |
<body class="bg-zinc-100 dark:bg-slate-950 text-black dark:text-white flex items-center justify-center min-h-screen">
|
20 |
<div
|
21 |
class="flex w-full w-full flex-col overflow-hidden border border-slate-800 dark:border-gray-700 shadow-lg rounded-lg h-[90vh] h-full">
|
22 |
-
|
23 |
<div class="flex flex-col flex-grow overflow-hidden">
|
24 |
<div id="chat-container" class="flex-grow overflow-y-auto p-3 space-y-3 bg-zinc-100 dark:bg-slate-950">
|
25 |
</div>
|
@@ -50,6 +49,8 @@
|
|
50 |
}
|
51 |
});
|
52 |
|
|
|
|
|
53 |
const chatContainer = document.getElementById("chat-container");
|
54 |
const chatInput = document.getElementById("chat-input");
|
55 |
const sendButton = document.getElementById("send-button");
|
@@ -70,15 +71,25 @@
|
|
70 |
bubble.className = `flex items-start gap-2 ${msg.isUser ? "flex-row-reverse" : "flex-row"}`;
|
71 |
const avatar = document.createElement("img");
|
72 |
avatar.className = "h-9 w-9 rounded-full object-cover";
|
73 |
-
|
74 |
-
|
|
|
|
|
75 |
avatar.alt = msg.isUser ? "User" : "Bot";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
const msgDiv = document.createElement("div");
|
77 |
msgDiv.className = `max-w-[75%] break-words rounded-md px-3 py-1 text-sm ${
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
msgDiv.innerText = msg.text;
|
83 |
bubble.appendChild(avatar);
|
84 |
bubble.appendChild(msgDiv);
|
@@ -102,44 +113,35 @@
|
|
102 |
}
|
103 |
|
104 |
async function simulateBotResponse(userInput) {
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
if (data.error) {
|
130 |
-
throw new Error(data.error);
|
131 |
}
|
132 |
-
|
133 |
-
return data;
|
134 |
-
} catch (error) {
|
135 |
-
console.error("Error in simulateBotResponse:", error);
|
136 |
-
throw error;
|
137 |
-
}
|
138 |
}
|
139 |
|
140 |
async function handleSendMessage() {
|
141 |
const text = chatInput.value.trim();
|
142 |
-
if (!text) return;
|
143 |
|
144 |
const userMessage = {
|
145 |
id: Date.now().toString(),
|
@@ -150,10 +152,24 @@
|
|
150 |
messages.push(userMessage);
|
151 |
chatInput.value = "";
|
152 |
isTyping = true;
|
|
|
|
|
153 |
renderMessages();
|
154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
155 |
try {
|
156 |
const res = await simulateBotResponse(text);
|
|
|
|
|
|
|
|
|
157 |
const botMessage = {
|
158 |
id: (Date.now() + 1).toString(),
|
159 |
text: res.response,
|
@@ -161,9 +177,12 @@
|
|
161 |
};
|
162 |
messages.push(botMessage);
|
163 |
} catch (error) {
|
|
|
|
|
|
|
164 |
messages.push({
|
165 |
id: Date.now().toString(),
|
166 |
-
text:
|
167 |
isUser: false,
|
168 |
});
|
169 |
}
|
@@ -172,6 +191,18 @@
|
|
172 |
renderMessages();
|
173 |
}
|
174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
sendButton.addEventListener("click", handleSendMessage);
|
176 |
chatInput.addEventListener("keydown", (e) => {
|
177 |
if (e.key === "Enter" && !e.shiftKey) {
|
@@ -180,7 +211,9 @@
|
|
180 |
}
|
181 |
});
|
182 |
|
|
|
183 |
renderMessages();
|
|
|
184 |
</script>
|
185 |
</body>
|
186 |
|
|
|
19 |
<body class="bg-zinc-100 dark:bg-slate-950 text-black dark:text-white flex items-center justify-center min-h-screen">
|
20 |
<div
|
21 |
class="flex w-full w-full flex-col overflow-hidden border border-slate-800 dark:border-gray-700 shadow-lg rounded-lg h-[90vh] h-full">
|
|
|
22 |
<div class="flex flex-col flex-grow overflow-hidden">
|
23 |
<div id="chat-container" class="flex-grow overflow-y-auto p-3 space-y-3 bg-zinc-100 dark:bg-slate-950">
|
24 |
</div>
|
|
|
49 |
}
|
50 |
});
|
51 |
|
52 |
+
const API_BASE_URL = window.location.origin;
|
53 |
+
|
54 |
const chatContainer = document.getElementById("chat-container");
|
55 |
const chatInput = document.getElementById("chat-input");
|
56 |
const sendButton = document.getElementById("send-button");
|
|
|
71 |
bubble.className = `flex items-start gap-2 ${msg.isUser ? "flex-row-reverse" : "flex-row"}`;
|
72 |
const avatar = document.createElement("img");
|
73 |
avatar.className = "h-9 w-9 rounded-full object-cover";
|
74 |
+
|
75 |
+
// Use complete URLs for images
|
76 |
+
const baseUrl = window.location.origin;
|
77 |
+
avatar.src = msg.isUser ? `${baseUrl}/static/profile.png` : `${baseUrl}/static/JUIT_icon.png`;
|
78 |
avatar.alt = msg.isUser ? "User" : "Bot";
|
79 |
+
|
80 |
+
avatar.onerror = function () {
|
81 |
+
// Fallback if image fails to load
|
82 |
+
this.src = msg.isUser ?
|
83 |
+
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='12' fill='%230ea5e9'/%3E%3C/svg%3E" :
|
84 |
+
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='12' fill='%23475569'/%3E%3C/svg%3E";
|
85 |
+
};
|
86 |
+
|
87 |
const msgDiv = document.createElement("div");
|
88 |
msgDiv.className = `max-w-[75%] break-words rounded-md px-3 py-1 text-sm ${
|
89 |
+
msg.isUser
|
90 |
+
? "bg-blue-600 text-white rounded-tr-none"
|
91 |
+
: "bg-gray-200 text-black dark:bg-gray-700 dark:text-white rounded-tl-none"
|
92 |
+
}`;
|
93 |
msgDiv.innerText = msg.text;
|
94 |
bubble.appendChild(avatar);
|
95 |
bubble.appendChild(msgDiv);
|
|
|
113 |
}
|
114 |
|
115 |
async function simulateBotResponse(userInput) {
|
116 |
+
try {
|
117 |
+
const response = await fetch(`${API_BASE_URL}/get`, {
|
118 |
+
method: "POST",
|
119 |
+
headers: {
|
120 |
+
"Content-Type": "application/json",
|
121 |
+
},
|
122 |
+
body: JSON.stringify({
|
123 |
+
message: userInput
|
124 |
+
}),
|
125 |
+
});
|
126 |
+
|
127 |
+
if (!response.ok) {
|
128 |
+
const errorText = await response.text();
|
129 |
+
throw new Error(`API error (${response.status}): ${errorText}`);
|
130 |
+
}
|
131 |
+
|
132 |
+
const data = await response.json();
|
133 |
+
if (data.error) throw new Error(data.error);
|
134 |
+
|
135 |
+
return data.response;
|
136 |
+
} catch (error) {
|
137 |
+
console.error("Error fetching response:", error);
|
138 |
+
throw error;
|
|
|
|
|
|
|
139 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
140 |
}
|
141 |
|
142 |
async function handleSendMessage() {
|
143 |
const text = chatInput.value.trim();
|
144 |
+
if (!text || isTyping) return;
|
145 |
|
146 |
const userMessage = {
|
147 |
id: Date.now().toString(),
|
|
|
152 |
messages.push(userMessage);
|
153 |
chatInput.value = "";
|
154 |
isTyping = true;
|
155 |
+
|
156 |
+
// Render messages, including typing animation
|
157 |
renderMessages();
|
158 |
|
159 |
+
// Add typing animation (". . .")
|
160 |
+
const typingMessage = {
|
161 |
+
id: "typing",
|
162 |
+
text: ". . .", // Use animated dots here
|
163 |
+
isUser: false,
|
164 |
+
};
|
165 |
+
messages.push(typingMessage);
|
166 |
+
|
167 |
try {
|
168 |
const res = await simulateBotResponse(text);
|
169 |
+
|
170 |
+
// Remove typing animation once response is received
|
171 |
+
messages.pop(); // Removes the ". . ." animation message
|
172 |
+
|
173 |
const botMessage = {
|
174 |
id: (Date.now() + 1).toString(),
|
175 |
text: res.response,
|
|
|
177 |
};
|
178 |
messages.push(botMessage);
|
179 |
} catch (error) {
|
180 |
+
// Remove typing animation if there's an error
|
181 |
+
messages.pop(); // Removes the ". . ." animation message
|
182 |
+
|
183 |
messages.push({
|
184 |
id: Date.now().toString(),
|
185 |
+
text: `Error: ${error.message || "Failed to get response. Please try again."}`,
|
186 |
isUser: false,
|
187 |
});
|
188 |
}
|
|
|
191 |
renderMessages();
|
192 |
}
|
193 |
|
194 |
+
// Check server health on page load
|
195 |
+
async function checkServerHealth() {
|
196 |
+
try {
|
197 |
+
const response = await fetch(`${API_BASE_URL}/health`);
|
198 |
+
if (!response.ok) {
|
199 |
+
console.warn("Server health check failed");
|
200 |
+
}
|
201 |
+
} catch (error) {
|
202 |
+
console.error("Server health check error:", error);
|
203 |
+
}
|
204 |
+
}
|
205 |
+
|
206 |
sendButton.addEventListener("click", handleSendMessage);
|
207 |
chatInput.addEventListener("keydown", (e) => {
|
208 |
if (e.key === "Enter" && !e.shiftKey) {
|
|
|
211 |
}
|
212 |
});
|
213 |
|
214 |
+
// Initialize
|
215 |
renderMessages();
|
216 |
+
checkServerHealth();
|
217 |
</script>
|
218 |
</body>
|
219 |
|