Huzaifa Ali commited on
Commit
12321f4
·
0 Parent(s):

Clean deployment version without venv

Browse files
Files changed (8) hide show
  1. .dockerignore +29 -0
  2. .gitattributes +36 -0
  3. .gitignore +0 -0
  4. Dockerfile +52 -0
  5. README.md +10 -0
  6. app.py +222 -0
  7. custom_prompt.py +24 -0
  8. 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