Adieee5 commited on
Commit
ab34076
·
1 Parent(s): 953a37d

Waitress better than gunicorn

Browse files
Files changed (4) hide show
  1. Dockerfile +24 -18
  2. app.py +59 -71
  3. requirements.txt +1 -0
  4. templates/index.html +74 -41
Dockerfile CHANGED
@@ -1,12 +1,26 @@
1
  FROM python:3.9
2
 
3
- # Install system dependencies and build tools first
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Create a non-root user
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
- # Switch to non-root user
30
  USER user
31
- ENV PATH="/home/user/.local/bin:$PATH"
32
-
33
- # Set working directory
34
  WORKDIR /app
35
 
36
- # Copy and install Python dependencies
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
- # Start the app
44
- CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app", "--preload"]
 
 
 
 
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
- # === Step 1: Load API Key ===
 
 
 
 
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
- # === Step 2: Initialize LLM (Gemini) ===
20
- llm = ChatGoogleGenerativeAI(
21
- model="gemini-2.0-flash-lite",
22
- google_api_key=GOOGLE_API_KEY,
23
- convert_system_message_to_human=True
24
- )
25
 
26
- # === Step 3: Load Chroma Vector Store ===
27
- embedding_model = HuggingFaceEmbeddings(model_name="BAAI/bge-large-en-v1.5")
28
- vectordb = Chroma(
29
- persist_directory="./chroma_store",
30
- embedding_function=embedding_model,
31
- collection_name="pdf_search_chroma"
32
- )
33
- retriever = vectordb.as_retriever(search_kwargs={"k": 6})
 
34
 
35
- # === Step 4: Custom Prompt Template ===
36
- prompt_template = PromptTemplate.from_template("""
37
- You are an intelligent assistant for students asking about their university.
38
- If answer is not defined or not clearly understood, ask for clarification.
39
- Answer clearly and helpfully based on the retrieved context. Do not make up information or suggestions.
40
- Context:
41
- {context}
42
- Question:
43
- {question}
44
- Answer:
45
- """)
46
 
47
- # === Step 5: Create Retrieval-QA Chain ===
48
- qa_chain = RetrievalQA.from_chain_type(
49
- llm=llm,
50
- chain_type="stuff",
51
- retriever=retriever,
52
- chain_type_kwargs={"prompt": prompt_template}
53
- )
 
 
 
 
54
 
55
- # === Step 6: Flask Setup ===
56
- app = Flask(__name__, static_folder="static", template_folder="templates")
57
- # Enable CORS for all routes
58
- CORS(app)
 
 
 
 
59
 
60
- # === Step 7: Serve Frontend ===
61
  @app.route("/")
62
- def index():
63
- return render_template("index.html") # Make sure chat.html exists
64
 
65
- @app.route('/api/test', methods=['GET', 'POST'])
66
- def test():
67
- print("Test endpoint reached")
68
- return jsonify({"status": "API is working!", "message": "Connection successful"})
 
 
69
 
70
- # === Step 8: API Route - Change to match what your frontend expects ===
71
- @app.route('/api/chat', methods=['POST'])
72
- def chat():
73
- print("Received request to /api/chat")
74
  try:
75
- data = request.json
76
- print(f"Request data: {data}")
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
- print(f"Error in chat endpoint: {str(e)}")
91
- return jsonify({"response": f"Sorry, I encountered an error: {str(e)}"}), 500
92
 
93
- # === Step 9: Run the App ===
94
- if __name__ == '__main__':
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
- avatar.src = msg.isUser ? "../static/profile.png" :
74
- "../static/JUIT_icon.png"; // adjust path if needed
 
 
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
- msg.isUser
79
- ? "bg-blue-600 text-white rounded-tr-none"
80
- : "bg-gray-200 text-black dark:bg-gray-700 dark:text-white rounded-tl-none"
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
- try {
106
- console.log("Sending request to /api/chat", userInput);
107
-
108
- const response = await fetch("/api/chat", { // Using relative URL
109
- method: "POST",
110
- headers: {
111
- "Content-Type": "application/json",
112
- },
113
- body: JSON.stringify({
114
- message: userInput
115
- }),
116
- });
117
-
118
- console.log("Response status:", response.status);
119
-
120
- if (!response.ok) {
121
- const errorText = await response.text();
122
- console.error("API error response:", errorText);
123
- throw new Error(`API error: ${response.status} - ${errorText}`);
124
- }
125
-
126
- const data = await response.json();
127
- console.log("API response data:", data);
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: "Error: " + error.message,
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