Spaces:
Running
Running
Huzaifa Ali
commited on
Commit
·
12321f4
0
Parent(s):
Clean deployment version without venv
Browse files- .dockerignore +29 -0
- .gitattributes +36 -0
- .gitignore +0 -0
- Dockerfile +52 -0
- README.md +10 -0
- app.py +222 -0
- custom_prompt.py +24 -0
- requirements.txt +7 -0
.dockerignore
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
__pycache__
|
2 |
+
*.pyc
|
3 |
+
*.pyo
|
4 |
+
*.pyd
|
5 |
+
.Python
|
6 |
+
env
|
7 |
+
pip-log.txt
|
8 |
+
pip-delete-this-directory.txt
|
9 |
+
.tox
|
10 |
+
.coverage
|
11 |
+
.coverage.*
|
12 |
+
.cache
|
13 |
+
nosetests.xml
|
14 |
+
coverage.xml
|
15 |
+
*.cover
|
16 |
+
*.log
|
17 |
+
.git
|
18 |
+
.mypy_cache
|
19 |
+
.pytest_cache
|
20 |
+
.hypothesis
|
21 |
+
|
22 |
+
.DS_Store
|
23 |
+
.vscode
|
24 |
+
README.md
|
25 |
+
.env
|
26 |
+
.gitignore
|
27 |
+
*.md
|
28 |
+
venv/
|
29 |
+
.venv/
|
.gitattributes
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
+
*.xz 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 |
+
index.faiss filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
Binary file (1.05 kB). View file
|
|
Dockerfile
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use Python 3.9 slim image
|
2 |
+
FROM python:3.9-slim
|
3 |
+
|
4 |
+
# Set working directory
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Install system dependencies
|
8 |
+
RUN apt-get update && apt-get install -y \
|
9 |
+
build-essential \
|
10 |
+
curl \
|
11 |
+
software-properties-common \
|
12 |
+
git \
|
13 |
+
&& rm -rf /var/lib/apt/lists/*
|
14 |
+
|
15 |
+
# Copy requirements first for better caching
|
16 |
+
COPY requirements.txt .
|
17 |
+
|
18 |
+
# Install Python dependencies
|
19 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
20 |
+
|
21 |
+
# Copy all application files
|
22 |
+
COPY . .
|
23 |
+
|
24 |
+
# Create a non-root user
|
25 |
+
RUN useradd -m -u 1000 user
|
26 |
+
USER user
|
27 |
+
|
28 |
+
# Set environment variables
|
29 |
+
ENV HOME=/home/user \
|
30 |
+
PATH=/home/user/.local/bin:$PATH \
|
31 |
+
PYTHONPATH=$HOME/app \
|
32 |
+
PYTHONUNBUFFERED=1 \
|
33 |
+
GRADIO_ALLOW_FLAGGING=never \
|
34 |
+
GRADIO_NUM_PORTS=1 \
|
35 |
+
GRADIO_SERVER_NAME=0.0.0.0 \
|
36 |
+
GRADIO_THEME=huggingface \
|
37 |
+
SYSTEM=spaces
|
38 |
+
|
39 |
+
# Change to user's home directory
|
40 |
+
WORKDIR $HOME/app
|
41 |
+
|
42 |
+
# Copy files with correct ownership
|
43 |
+
COPY --chown=user . $HOME/app
|
44 |
+
|
45 |
+
# Expose port 7860 (Hugging Face Spaces default)
|
46 |
+
EXPOSE 7860
|
47 |
+
|
48 |
+
# Health check
|
49 |
+
HEALTHCHECK CMD curl --fail http://localhost:7860/_health || exit 1
|
50 |
+
|
51 |
+
# Command to run the application
|
52 |
+
CMD ["python", "app.py"]
|
README.md
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: CCET Chat Assistance
|
3 |
+
emoji: 🏃
|
4 |
+
colorFrom: pink
|
5 |
+
colorTo: indigo
|
6 |
+
sdk: docker
|
7 |
+
pinned: false
|
8 |
+
---
|
9 |
+
|
10 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from flask import Flask, request, jsonify, render_template
|
3 |
+
import google.generativeai as genai
|
4 |
+
from langchain_google_genai import GoogleGenerativeAIEmbeddings, ChatGoogleGenerativeAI
|
5 |
+
from langchain.vectorstores import FAISS
|
6 |
+
from langchain.chains import ConversationalRetrievalChain
|
7 |
+
from langchain.memory import ConversationBufferMemory
|
8 |
+
from dotenv import load_dotenv
|
9 |
+
import logging
|
10 |
+
import re
|
11 |
+
from custom_prompt import get_custom_prompt
|
12 |
+
|
13 |
+
# Configure logging
|
14 |
+
logging.basicConfig(level=logging.INFO,
|
15 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
16 |
+
logger = logging.getLogger(__name__)
|
17 |
+
|
18 |
+
# Load environment variables
|
19 |
+
load_dotenv()
|
20 |
+
|
21 |
+
app = Flask(__name__)
|
22 |
+
|
23 |
+
# Initialize the environment - Check multiple possible env var names
|
24 |
+
GOOGLE_API_KEY = (os.getenv("GOOGLE_API_KEY") or
|
25 |
+
os.getenv("GEMINI_API_KEY") or
|
26 |
+
os.getenv("GOOGLE_GEMINI_API_KEY"))
|
27 |
+
|
28 |
+
if not GOOGLE_API_KEY or GOOGLE_API_KEY == "your_api_key_here":
|
29 |
+
logger.error("No valid GOOGLE_API_KEY found in environment variables")
|
30 |
+
print("⚠️ Please set your Gemini API key in the environment variables")
|
31 |
+
print("Supported env var names: GOOGLE_API_KEY, GEMINI_API_KEY, GOOGLE_GEMINI_API_KEY")
|
32 |
+
else:
|
33 |
+
genai.configure(api_key=GOOGLE_API_KEY)
|
34 |
+
logger.info("API key configured successfully")
|
35 |
+
|
36 |
+
# Global variables for the chain and memory
|
37 |
+
qa_chain = None
|
38 |
+
memory = None
|
39 |
+
|
40 |
+
def initialize_chatbot():
|
41 |
+
global qa_chain, memory
|
42 |
+
|
43 |
+
logger.info("Initializing chatbot...")
|
44 |
+
|
45 |
+
# Initialize embeddings
|
46 |
+
try:
|
47 |
+
embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
|
48 |
+
logger.info("Embeddings initialized")
|
49 |
+
except Exception as e:
|
50 |
+
logger.error(f"Error initializing embeddings: {str(e)}")
|
51 |
+
return False
|
52 |
+
|
53 |
+
# Load the vector store
|
54 |
+
try:
|
55 |
+
vector_store = FAISS.load_local("faiss_index", embeddings, allow_dangerous_deserialization=True)
|
56 |
+
logger.info("Vector store loaded successfully!")
|
57 |
+
except Exception as e:
|
58 |
+
logger.error(f"Error loading vector store: {str(e)}")
|
59 |
+
print(f"⚠️ Error loading vector store: {str(e)}")
|
60 |
+
print("Make sure your 'faiss_index' folder is in the same directory as this script.")
|
61 |
+
return False
|
62 |
+
|
63 |
+
# Create memory
|
64 |
+
memory = ConversationBufferMemory(
|
65 |
+
memory_key="chat_history",
|
66 |
+
return_messages=True,
|
67 |
+
output_key="answer"
|
68 |
+
)
|
69 |
+
logger.info("Conversation memory initialized")
|
70 |
+
|
71 |
+
# Initialize the language model
|
72 |
+
try:
|
73 |
+
llm = ChatGoogleGenerativeAI(
|
74 |
+
model="gemini-2.0-flash",
|
75 |
+
temperature=0.2,
|
76 |
+
top_p=0.85,
|
77 |
+
google_api_key=GOOGLE_API_KEY
|
78 |
+
)
|
79 |
+
logger.info("Language model initialized")
|
80 |
+
except Exception as e:
|
81 |
+
logger.error(f"Error initializing language model: {str(e)}")
|
82 |
+
return False
|
83 |
+
|
84 |
+
# Create the conversation chain with the custom prompt
|
85 |
+
try:
|
86 |
+
retriever = vector_store.as_retriever(search_kwargs={"k": 3})
|
87 |
+
|
88 |
+
qa_chain = ConversationalRetrievalChain.from_llm(
|
89 |
+
llm=llm,
|
90 |
+
retriever=retriever,
|
91 |
+
memory=memory,
|
92 |
+
verbose=True,
|
93 |
+
return_source_documents=False, # Set to False to hide source documents
|
94 |
+
combine_docs_chain_kwargs={"prompt": get_custom_prompt()},
|
95 |
+
)
|
96 |
+
logger.info("QA chain created successfully")
|
97 |
+
except Exception as e:
|
98 |
+
logger.error(f"Error creating QA chain: {str(e)}")
|
99 |
+
return False
|
100 |
+
|
101 |
+
return True
|
102 |
+
|
103 |
+
# Function to format links as HTML anchor tags
|
104 |
+
def format_links_as_html(text):
|
105 |
+
# Detect markdown style links [text](url)
|
106 |
+
markdown_pattern = r'\[(.*?)\]\((https?://[^\s\)]+)\)'
|
107 |
+
if re.search(markdown_pattern, text):
|
108 |
+
text = re.sub(markdown_pattern, r'<a href="\2" target="_blank">\1</a>', text)
|
109 |
+
return text
|
110 |
+
|
111 |
+
# Handle URLs in square brackets [url]
|
112 |
+
bracket_pattern = r'\[(https?://[^\s\]]+)\]'
|
113 |
+
if re.search(bracket_pattern, text):
|
114 |
+
text = re.sub(bracket_pattern, r'<a href="\1" target="_blank">\1</a>', text)
|
115 |
+
return text
|
116 |
+
|
117 |
+
# Regular URL pattern
|
118 |
+
url_pattern = r'(https?://[^\s\]]+)'
|
119 |
+
|
120 |
+
# Find all URLs in the text
|
121 |
+
urls = re.findall(url_pattern, text)
|
122 |
+
|
123 |
+
# If there are multiple URLs, keep only the first one
|
124 |
+
if len(urls) > 1:
|
125 |
+
for url in urls[1:]:
|
126 |
+
text = text.replace(url, "")
|
127 |
+
|
128 |
+
# Replace the remaining URL with an HTML anchor tag
|
129 |
+
if urls:
|
130 |
+
text = re.sub(url_pattern, r'<a href="\1" target="_blank">\1</a>', text, count=1)
|
131 |
+
|
132 |
+
return text
|
133 |
+
|
134 |
+
# Function to properly escape asterisks for markdown rendering
|
135 |
+
def escape_markdown(text):
|
136 |
+
# Replace single asterisks not intended for markdown with escaped versions
|
137 |
+
# This regex looks for asterisks that aren't part of markdown patterns
|
138 |
+
return re.sub(r'(?<!\*)\*(?!\*)', r'\*', text)
|
139 |
+
|
140 |
+
# Function to format markdown and handle asterisks with proper line breaks
|
141 |
+
def format_markdown_with_breaks(text):
|
142 |
+
# First remove escaped asterisks (\*) and replace with just asterisks (*)
|
143 |
+
text = text.replace('\\*', '*')
|
144 |
+
|
145 |
+
# Handle bold text (convert **text** to <strong>text</strong>)
|
146 |
+
text = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', text)
|
147 |
+
|
148 |
+
# Now split text by lines for processing asterisk line breaks
|
149 |
+
lines = text.split('\n')
|
150 |
+
formatted_lines = []
|
151 |
+
|
152 |
+
for i, line in enumerate(lines):
|
153 |
+
# If line starts with asterisk (possibly after whitespace), add a line break before it
|
154 |
+
# except for the first line
|
155 |
+
if line.strip().startswith('*'):
|
156 |
+
# Extract content after the asterisk
|
157 |
+
content = line.strip()[1:].strip()
|
158 |
+
|
159 |
+
# Add line break (except for the first line)
|
160 |
+
if i == 0 or len(formatted_lines) == 0:
|
161 |
+
formatted_lines.append(f"• {content}")
|
162 |
+
else:
|
163 |
+
formatted_lines.append(f"<br>• {content}")
|
164 |
+
else:
|
165 |
+
formatted_lines.append(line)
|
166 |
+
|
167 |
+
return '\n'.join(formatted_lines)
|
168 |
+
|
169 |
+
@app.route('/')
|
170 |
+
def home():
|
171 |
+
return render_template('index.html')
|
172 |
+
|
173 |
+
@app.route('/_health')
|
174 |
+
def health_check():
|
175 |
+
"""Health check endpoint for Docker"""
|
176 |
+
return jsonify({"status": "healthy"}), 200
|
177 |
+
|
178 |
+
@app.route('/api/chat', methods=['POST'])
|
179 |
+
def chat():
|
180 |
+
global qa_chain
|
181 |
+
|
182 |
+
# Initialize on first request if not already done
|
183 |
+
if qa_chain is None:
|
184 |
+
success = initialize_chatbot()
|
185 |
+
if not success:
|
186 |
+
return jsonify({"error": "Failed to initialize chatbot. Check server logs for details."}), 500
|
187 |
+
|
188 |
+
data = request.json
|
189 |
+
user_message = data.get('message', '')
|
190 |
+
|
191 |
+
if not user_message:
|
192 |
+
return jsonify({"error": "No message provided"}), 400
|
193 |
+
|
194 |
+
try:
|
195 |
+
logger.info(f"Processing user query: {user_message}")
|
196 |
+
|
197 |
+
# Process the query through the QA chain
|
198 |
+
result = qa_chain({"question": user_message})
|
199 |
+
|
200 |
+
# Extract the answer
|
201 |
+
answer = result.get("answer", "I'm sorry, I couldn't generate a response.")
|
202 |
+
|
203 |
+
# Format the answer (escape markdown, format links, and handle numbered lists)
|
204 |
+
answer = escape_markdown(answer)
|
205 |
+
answer = format_links_as_html(answer)
|
206 |
+
answer = format_markdown_with_breaks(answer)
|
207 |
+
|
208 |
+
logger.info("Query processed successfully")
|
209 |
+
|
210 |
+
return jsonify({
|
211 |
+
"answer": answer,
|
212 |
+
# No sources included in the response
|
213 |
+
})
|
214 |
+
|
215 |
+
except Exception as e:
|
216 |
+
logger.error(f"Error processing request: {str(e)}")
|
217 |
+
return jsonify({"error": f"Error processing request: {str(e)}"}), 500
|
218 |
+
|
219 |
+
if __name__ == '__main__':
|
220 |
+
# For Docker deployment, bind to all interfaces and use port 7860
|
221 |
+
port = int(os.environ.get('PORT', 7860))
|
222 |
+
app.run(host='0.0.0.0', port=port, debug=False)
|
custom_prompt.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain.prompts import PromptTemplate
|
2 |
+
|
3 |
+
def get_custom_prompt():
|
4 |
+
# Create a system template that defines the chatbot's behavior
|
5 |
+
system_template = """You are a helpful AI assistant for our college.
|
6 |
+
Your job is to provide accurate, helpful information about our college based on the data provided.
|
7 |
+
|
8 |
+
When answering questions, use the following information as context:
|
9 |
+
{context}
|
10 |
+
|
11 |
+
Chat History: {chat_history}
|
12 |
+
|
13 |
+
Use the above context to answer the user's question. If you don't know the answer based on the provided context,
|
14 |
+
say so clearly rather than making up information. If the answer is not in the context,
|
15 |
+
you can provide general information about colleges but make it clear that it's not specific to this college.
|
16 |
+
|
17 |
+
Be conversational, friendly, and professional.
|
18 |
+
Question: {question}
|
19 |
+
Answer:"""
|
20 |
+
|
21 |
+
return PromptTemplate(
|
22 |
+
input_variables=["context", "chat_history", "question"],
|
23 |
+
template=system_template
|
24 |
+
)
|
requirements.txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
flask
|
2 |
+
google-generativeai
|
3 |
+
langchain
|
4 |
+
langchain-google-genai
|
5 |
+
faiss-cpu
|
6 |
+
python-dotenv
|
7 |
+
gunicorn
|