decodingdatascience commited on
Commit
88a94a3
·
verified ·
1 Parent(s): fbd4d3f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +715 -163
app.py CHANGED
@@ -1,62 +1,120 @@
1
- # DDS Enterprise HR Chatbot + Telegram Bot
2
- # Hugging Face Spaces app.py
 
 
3
 
4
- import logging
5
  import os
6
  import sys
7
- import threading
8
  import time
9
-
10
- import gradio as gr
11
  import requests
 
 
12
  from pinecone import Pinecone, ServerlessSpec
13
 
14
  from llama_index.core import (
15
- Settings,
16
  SimpleDirectoryReader,
17
  StorageContext,
18
- VectorStoreIndex,
19
  )
20
- from llama_index.embeddings.openai import OpenAIEmbedding
21
- from llama_index.llms.openai import OpenAI
22
- from llama_index.readers.file import PDFReader
23
  from llama_index.vector_stores.pinecone import PineconeVectorStore
 
 
 
 
 
 
 
 
24
 
 
 
 
 
 
 
25
 
26
- # ---------------------------------------------------------
27
- # 1. Logging
28
- # ---------------------------------------------------------
29
- logging.basicConfig(stream=sys.stdout, level=logging.INFO)
30
- logger = logging.getLogger("dds-hr-enterprise")
31
 
 
 
 
32
 
33
- # ---------------------------------------------------------
34
- # 2. Secrets from Hugging Face
35
- # ---------------------------------------------------------
36
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
37
  PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
38
  TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
39
 
 
40
  if not OPENAI_API_KEY:
41
- raise ValueError("OPENAI_API_KEY is missing from Hugging Face Secrets.")
 
 
42
 
43
  if not PINECONE_API_KEY:
44
- raise ValueError("PINECONE_API_KEY is missing from Hugging Face Secrets.")
 
 
45
 
46
  if not TELEGRAM_BOT_TOKEN:
47
  logger.warning(
48
- "TELEGRAM_BOT_TOKEN is missing. Web chatbot will work, "
49
- "but Telegram integration will stay disabled."
50
  )
51
 
52
 
53
- # ---------------------------------------------------------
54
- # 3. LlamaIndex / OpenAI settings
55
- # ---------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  Settings.llm = OpenAI(
57
  model="gpt-4o-mini",
58
  temperature=0.2,
59
  api_key=OPENAI_API_KEY,
 
60
  )
61
 
62
  Settings.embed_model = OpenAIEmbedding(
@@ -68,42 +126,32 @@ Settings.chunk_size = 600
68
  Settings.chunk_overlap = 200
69
 
70
 
71
- # ---------------------------------------------------------
72
- # 4. System prompt
73
- # ---------------------------------------------------------
74
- SYSTEM_PROMPT = """
75
- You are AYesha, the Decoding Data Science (DDS) Enterprise HR Chatbot.
76
-
77
- Answer questions exclusively using the latest DDS HR Handbook content supplied
78
- to the retrieval system.
79
-
80
- Rules:
81
- - Only answer questions directly related to DDS HR policies in the handbook.
82
- - Do not answer general questions unrelated to DDS HR.
83
- - Do not provide confidential information such as salary details.
84
- - If the answer is not supported by the handbook, do not guess.
85
- - For confidential, unsupported, or out-of-scope requests, respond:
86
- "I’m sorry, I can only answer questions about the latest DDS HR policies.
87
- For confidential or other queries, please email
88
- connect@decodingdatascience.com."
89
- - Keep responses concise, professional, and easy to understand.
90
- - Base the final answer only on retrieved handbook information.
91
- """
92
 
 
93
 
94
- # ---------------------------------------------------------
95
- # 5. Pinecone
96
- # ---------------------------------------------------------
97
- pc = Pinecone(api_key=PINECONE_API_KEY)
98
 
99
  INDEX_NAME = "quickstart"
100
  DIMENSION = 1536
101
 
102
- existing_indexes = [idx["name"] for idx in pc.list_indexes()]
103
- index_was_created = INDEX_NAME not in existing_indexes
104
 
105
- if index_was_created:
106
- logger.info("Creating Pinecone index '%s'...", INDEX_NAME)
 
 
 
 
 
 
 
 
 
 
107
  pc.create_index(
108
  name=INDEX_NAME,
109
  dimension=DIMENSION,
@@ -114,249 +162,753 @@ if index_was_created:
114
  ),
115
  )
116
 
117
- # Give Pinecone a moment to make the new index ready.
118
- time.sleep(5)
 
 
 
 
119
  else:
 
120
  logger.info(
121
- "Using existing Pinecone index '%s' — NOT deleting/rebuilding it.",
122
- INDEX_NAME,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  )
124
 
125
- pinecone_index = pc.Index(INDEX_NAME)
126
- vector_store = PineconeVectorStore(pinecone_index=pinecone_index)
 
 
 
 
 
127
 
 
 
 
128
 
129
- # ---------------------------------------------------------
130
- # 6. Build vectors once, or reconnect to existing vectors
131
- # ---------------------------------------------------------
132
- if index_was_created:
133
- logger.info("Loading PDFs from Data/ and creating embeddings...")
134
 
135
  documents = SimpleDirectoryReader(
136
  input_dir="Data",
137
  required_exts=[".pdf"],
138
- file_extractor={".pdf": PDFReader()},
 
 
139
  ).load_data()
140
 
 
141
  if not documents:
142
- raise ValueError("No PDF documents were loaded from the 'Data' folder.")
 
 
 
 
 
 
 
 
 
143
 
144
  storage_context = StorageContext.from_defaults(
145
  vector_store=vector_store
146
  )
147
 
 
 
 
 
 
 
148
  index = VectorStoreIndex.from_documents(
149
  documents,
150
  storage_context=storage_context,
151
  )
152
 
153
- logger.info("DDS HR handbook indexed successfully.")
 
 
 
 
154
 
155
  else:
156
- # Reuse the vectors already stored in Pinecone.
 
 
 
 
 
157
  index = VectorStoreIndex.from_vector_store(
158
  vector_store=vector_store
159
  )
160
 
161
 
162
- # ---------------------------------------------------------
163
- # 7. Query engine shared by BOTH Gradio and Telegram
164
- # ---------------------------------------------------------
 
165
  query_engine = index.as_query_engine(
166
- system_prompt=SYSTEM_PROMPT,
167
- similarity_top_k=4,
168
  )
169
 
170
 
171
- def query_doc(prompt: str) -> str:
172
- """Single HR answering function used by web app and Telegram."""
173
- if not prompt or not prompt.strip():
174
- return "Please enter an HR policy question."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
  try:
177
- response = query_engine.query(prompt.strip())
178
- return str(response)
179
 
180
- except Exception as exc:
181
- logger.exception("Query error")
182
- return "Sorry, I couldn't process your question right now. Please try again."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
 
 
 
 
 
 
 
184
 
185
- # ---------------------------------------------------------
186
- # 8. Telegram integration
187
- # ---------------------------------------------------------
188
- def telegram_api(method: str) -> str:
189
- return f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/{method}"
190
 
 
 
 
 
 
 
 
 
191
 
192
- def telegram_send_message(chat_id: int, text: str) -> None:
193
- """Send answer back to Telegram. Split long answers if required."""
194
  if not TELEGRAM_BOT_TOKEN:
195
  return
196
 
 
197
  text = str(text)
198
 
199
- # Telegram messages have a size limit; 4000 gives us a safe margin.
200
- for start in range(0, len(text), 4000):
201
- chunk = text[start:start + 4000]
202
 
203
- response = requests.post(
204
- telegram_api("sendMessage"),
205
- json={
206
- "chat_id": chat_id,
207
- "text": chunk,
208
- },
209
- timeout=20,
 
 
 
210
  )
211
- response.raise_for_status()
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
- def telegram_typing(chat_id: int) -> None:
215
  try:
 
216
  requests.post(
217
- telegram_api("sendChatAction"),
 
 
 
 
218
  json={
219
  "chat_id": chat_id,
220
  "action": "typing",
221
  },
222
- timeout=10,
 
223
  )
 
 
224
  except Exception:
225
- # Typing indicator is optional, so ignore failures.
226
  pass
227
 
228
 
229
- def telegram_polling_loop() -> None:
230
- """Receive Telegram messages using long polling."""
 
 
 
 
 
 
 
 
231
  if not TELEGRAM_BOT_TOKEN:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  return
233
 
234
- logger.info("Starting DDS HR Telegram bot...")
235
 
236
- # getUpdates and webhooks cannot be used at the same time.
237
- # Remove an old webhook if one exists.
 
 
 
238
  try:
 
239
  requests.post(
240
- telegram_api("deleteWebhook"),
241
- json={"drop_pending_updates": True},
242
- timeout=20,
 
 
 
 
 
 
 
 
 
 
 
 
243
  )
244
- except Exception as exc:
245
- logger.warning("Could not clear Telegram webhook: %s", exc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
  offset = None
248
 
 
249
  while True:
 
250
  try:
 
251
  params = {
252
- "timeout": 45,
253
- "allowed_updates": ["message"],
254
  }
255
 
 
256
  if offset is not None:
 
257
  params["offset"] = offset
258
 
 
 
259
  response = requests.get(
260
- telegram_api("getUpdates"),
 
 
 
 
261
  params=params,
262
- timeout=55,
 
263
  )
 
 
264
  response.raise_for_status()
265
 
266
- payload = response.json()
267
 
268
- if not payload.get("ok"):
269
- logger.warning("Telegram API response: %s", payload)
270
- time.sleep(3)
 
 
 
 
 
 
 
 
271
  continue
272
 
273
- for update in payload.get("result", []):
274
- offset = update["update_id"] + 1
275
 
276
- message = update.get("message")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  if not message:
 
278
  continue
279
 
280
- chat_id = message.get("chat", {}).get("id")
281
- text = (message.get("text") or "").strip()
282
 
283
- if not chat_id or not text:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  continue
285
 
286
- logger.info("Telegram question received from chat_id=%s", chat_id)
287
 
288
- # Telegram commands
289
- if text.startswith("/start"):
290
- telegram_send_message(
 
291
  chat_id,
292
- "Welcome to AYesha — DDS Enterprise HR Chatbot.\n\n"
293
- "Ask me a question about the latest DDS HR policies.",
 
 
294
  )
 
295
  continue
296
 
297
- if text.startswith("/help"):
298
- telegram_send_message(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  chat_id,
300
- "Ask a normal HR question, for example:\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  "• What is the annual leave policy?\n"
302
  "• What is the probation policy?\n"
303
- "• How do I request leave?",
 
304
  )
 
305
  continue
306
 
 
 
 
 
 
307
  if text.startswith("/"):
308
- telegram_send_message(
 
 
309
  chat_id,
310
- "Please type your HR policy question as a normal message.",
 
 
 
311
  )
 
312
  continue
313
 
314
- telegram_typing(chat_id)
315
 
316
- answer = query_doc(text)
317
- telegram_send_message(chat_id, answer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
 
319
- except Exception as exc:
320
- logger.exception("Telegram polling error: %s", exc)
321
  time.sleep(5)
322
 
323
 
324
- def start_telegram_bot() -> None:
325
- """Run Telegram polling without blocking the Gradio web application."""
 
 
 
 
326
  if not TELEGRAM_BOT_TOKEN:
327
- logger.warning("Telegram bot not started: TELEGRAM_BOT_TOKEN missing.")
 
 
 
 
328
  return
329
 
330
- bot_thread = threading.Thread(
 
 
331
  target=telegram_polling_loop,
 
332
  daemon=True,
333
- name="telegram-bot",
 
 
 
 
 
 
 
 
 
 
334
  )
335
- bot_thread.start()
336
- logger.info("Telegram bot background thread started.")
337
 
338
 
339
- # Start Telegram before Gradio blocks the main thread.
 
 
 
340
  start_telegram_bot()
341
 
342
 
343
- # ---------------------------------------------------------
344
- # 9. Gradio web app
345
- # ---------------------------------------------------------
 
346
  demo = gr.Interface(
 
347
  fn=query_doc,
 
348
  inputs=gr.Textbox(
 
349
  label="Ask a question about DDS HR policies",
350
- placeholder="Example: What is the annual leave policy?",
 
 
 
 
351
  ),
352
- outputs=gr.Textbox(label="Answer"),
 
 
 
 
353
  title="DDS Enterprise HR Chatbot",
354
- description="Ask questions based on the latest DDS HR Handbook.",
 
 
 
 
 
355
  )
356
 
357
 
 
 
 
 
358
  if __name__ == "__main__":
 
359
  demo.launch(
 
360
  server_name="0.0.0.0",
361
- server_port=7860,
362
- )
 
 
 
1
+ # ============================================================
2
+ # DDS ENTERPRISE HR CHATBOT
3
+ # Hugging Face + LlamaIndex + Pinecone + OpenAI + Telegram
4
+ # ============================================================
5
 
 
6
  import os
7
  import sys
 
8
  import time
9
+ import logging
10
+ import threading
11
  import requests
12
+ import gradio as gr
13
+
14
  from pinecone import Pinecone, ServerlessSpec
15
 
16
  from llama_index.core import (
17
+ VectorStoreIndex,
18
  SimpleDirectoryReader,
19
  StorageContext,
20
+ Settings,
21
  )
22
+
 
 
23
  from llama_index.vector_stores.pinecone import PineconeVectorStore
24
+ from llama_index.readers.file import PDFReader
25
+ from llama_index.llms.openai import OpenAI
26
+ from llama_index.embeddings.openai import OpenAIEmbedding
27
+
28
+
29
+ # ============================================================
30
+ # 1. LOGGING
31
+ # ============================================================
32
 
33
+ logging.basicConfig(
34
+ stream=sys.stdout,
35
+ level=logging.INFO
36
+ )
37
+
38
+ logger = logging.getLogger("DDS-HR")
39
 
 
 
 
 
 
40
 
41
+ # ============================================================
42
+ # 2. LOAD HUGGING FACE SECRETS
43
+ # ============================================================
44
 
 
 
 
45
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
46
  PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
47
  TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
48
 
49
+
50
  if not OPENAI_API_KEY:
51
+ raise ValueError(
52
+ "OPENAI_API_KEY is missing from Hugging Face Secrets"
53
+ )
54
 
55
  if not PINECONE_API_KEY:
56
+ raise ValueError(
57
+ "PINECONE_API_KEY is missing from Hugging Face Secrets"
58
+ )
59
 
60
  if not TELEGRAM_BOT_TOKEN:
61
  logger.warning(
62
+ "TELEGRAM_BOT_TOKEN not found. "
63
+ "Web chatbot will work but Telegram will not."
64
  )
65
 
66
 
67
+ # ============================================================
68
+ # 3. SYSTEM PROMPT
69
+ # ============================================================
70
+
71
+ SYSTEM_PROMPT = """
72
+ You are AYesha, the Decoding Data Science (DDS)
73
+ Enterprise HR Chatbot.
74
+
75
+ You answer questions ONLY using information contained
76
+ in the latest DDS HR Handbook retrieved for you.
77
+
78
+ RULES:
79
+
80
+ 1. Only answer questions related to DDS HR policies.
81
+
82
+ 2. Base answers only on retrieved DDS HR Handbook content.
83
+
84
+ 3. Never invent HR policies.
85
+
86
+ 4. Never answer confidential questions such as salary
87
+ information or private employee information.
88
+
89
+ 5. If the handbook does not contain the answer, say:
90
+
91
+ "I'm sorry, I can only answer questions about the latest
92
+ DDS HR policies. For confidential or other queries,
93
+ please email connect@decodingdatascience.com."
94
+
95
+ 6. If the question is unrelated to HR, say:
96
+
97
+ "I'm sorry, I only answer DDS HR policy questions
98
+ as outlined in the handbook."
99
+
100
+ 7. Keep answers concise, professional and easy to understand.
101
+
102
+ 8. Internally verify whether the question is within scope
103
+ before answering. Do not expose internal reasoning.
104
+
105
+ Always prioritize the DDS HR Handbook as the source of truth.
106
+ """
107
+
108
+
109
+ # ============================================================
110
+ # 4. LLAMAINDEX + OPENAI SETTINGS
111
+ # ============================================================
112
+
113
  Settings.llm = OpenAI(
114
  model="gpt-4o-mini",
115
  temperature=0.2,
116
  api_key=OPENAI_API_KEY,
117
+ system_prompt=SYSTEM_PROMPT,
118
  )
119
 
120
  Settings.embed_model = OpenAIEmbedding(
 
126
  Settings.chunk_overlap = 200
127
 
128
 
129
+ # ============================================================
130
+ # 5. CONNECT TO PINECONE
131
+ # ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
+ logger.info("Connecting to Pinecone...")
134
 
135
+ pc = Pinecone(
136
+ api_key=PINECONE_API_KEY
137
+ )
 
138
 
139
  INDEX_NAME = "quickstart"
140
  DIMENSION = 1536
141
 
 
 
142
 
143
+ # ============================================================
144
+ # 6. CREATE INDEX ONLY IF IT DOES NOT EXIST
145
+ # ============================================================
146
+
147
+ existing_indexes = pc.list_indexes().names()
148
+
149
+ if INDEX_NAME not in existing_indexes:
150
+
151
+ logger.info(
152
+ "Pinecone index does not exist. Creating it..."
153
+ )
154
+
155
  pc.create_index(
156
  name=INDEX_NAME,
157
  dimension=DIMENSION,
 
162
  ),
163
  )
164
 
165
+ logger.info(
166
+ "Waiting for Pinecone index..."
167
+ )
168
+
169
+ time.sleep(10)
170
+
171
  else:
172
+
173
  logger.info(
174
+ "Existing Pinecone index found. "
175
+ "NOT deleting it."
176
+ )
177
+
178
+
179
+ # ============================================================
180
+ # 7. CONNECT TO INDEX
181
+ # ============================================================
182
+
183
+ pinecone_index = pc.Index(
184
+ INDEX_NAME
185
+ )
186
+
187
+ vector_store = PineconeVectorStore(
188
+ pinecone_index=pinecone_index
189
+ )
190
+
191
+
192
+ # ============================================================
193
+ # 8. CHECK WHETHER DATA IS ALREADY INDEXED
194
+ # ============================================================
195
+
196
+ try:
197
+
198
+ stats = pinecone_index.describe_index_stats()
199
+
200
+ try:
201
+ vector_count = stats["total_vector_count"]
202
+ except Exception:
203
+ vector_count = getattr(
204
+ stats,
205
+ "total_vector_count",
206
+ 0
207
+ )
208
+
209
+ except Exception as e:
210
+
211
+ logger.warning(
212
+ f"Could not read Pinecone stats: {e}"
213
  )
214
 
215
+ vector_count = 0
216
+
217
+
218
+ logger.info(
219
+ f"Pinecone vector count: {vector_count}"
220
+ )
221
+
222
 
223
+ # ============================================================
224
+ # 9. INDEX HR PDF IF DATABASE IS EMPTY
225
+ # ============================================================
226
 
227
+ if vector_count == 0:
228
+
229
+ logger.info(
230
+ "Pinecone is empty. Loading PDFs from Data folder..."
231
+ )
232
 
233
  documents = SimpleDirectoryReader(
234
  input_dir="Data",
235
  required_exts=[".pdf"],
236
+ file_extractor={
237
+ ".pdf": PDFReader()
238
+ }
239
  ).load_data()
240
 
241
+
242
  if not documents:
243
+
244
+ raise ValueError(
245
+ "No PDF documents found inside Data folder"
246
+ )
247
+
248
+
249
+ logger.info(
250
+ f"Loaded {len(documents)} document sections"
251
+ )
252
+
253
 
254
  storage_context = StorageContext.from_defaults(
255
  vector_store=vector_store
256
  )
257
 
258
+
259
+ logger.info(
260
+ "Creating embeddings and storing them in Pinecone..."
261
+ )
262
+
263
+
264
  index = VectorStoreIndex.from_documents(
265
  documents,
266
  storage_context=storage_context,
267
  )
268
 
269
+
270
+ logger.info(
271
+ "DDS HR Handbook indexed successfully."
272
+ )
273
+
274
 
275
  else:
276
+
277
+ logger.info(
278
+ "Using existing Pinecone vectors."
279
+ )
280
+
281
+
282
  index = VectorStoreIndex.from_vector_store(
283
  vector_store=vector_store
284
  )
285
 
286
 
287
+ # ============================================================
288
+ # 10. QUERY ENGINE
289
+ # ============================================================
290
+
291
  query_engine = index.as_query_engine(
292
+ similarity_top_k=4
 
293
  )
294
 
295
 
296
+ # ============================================================
297
+ # 11. ONE FUNCTION USED BY WEB + TELEGRAM
298
+ # ============================================================
299
+
300
+ def query_doc(question):
301
+
302
+ if not question:
303
+ return "Please enter an HR question."
304
+
305
+
306
+ question = question.strip()
307
+
308
+
309
+ if not question:
310
+ return "Please enter an HR question."
311
+
312
+
313
+ logger.info(
314
+ f"HR question received: {question}"
315
+ )
316
+
317
+
318
+ try:
319
+
320
+ response = query_engine.query(
321
+ question
322
+ )
323
+
324
+ answer = str(response)
325
+
326
+ logger.info(
327
+ "Answer generated successfully."
328
+ )
329
+
330
+ return answer
331
+
332
+
333
+ except Exception as e:
334
+
335
+ logger.exception(
336
+ f"RAG ERROR: {e}"
337
+ )
338
+
339
+ return (
340
+ "Sorry, I couldn't process your question "
341
+ "right now. Please try again."
342
+ )
343
+
344
+
345
+ # ============================================================
346
+ # 12. TELEGRAM FUNCTIONS
347
+ # ============================================================
348
+
349
+ def telegram_url(method):
350
+
351
+ return (
352
+ f"https://api.telegram.org/"
353
+ f"bot{TELEGRAM_BOT_TOKEN}/"
354
+ f"{method}"
355
+ )
356
+
357
+
358
+ # ============================================================
359
+ # 13. TEST TELEGRAM CONNECTION
360
+ # ============================================================
361
+
362
+ def test_telegram_connection():
363
+
364
+ if not TELEGRAM_BOT_TOKEN:
365
+ return False
366
+
367
 
368
  try:
 
 
369
 
370
+ response = requests.get(
371
+ telegram_url("getMe"),
372
+ timeout=10
373
+ )
374
+
375
+ response.raise_for_status()
376
+
377
+ data = response.json()
378
+
379
+
380
+ if data.get("ok"):
381
+
382
+ bot = data["result"]
383
+
384
+ logger.info(
385
+ "===================================="
386
+ )
387
+
388
+ logger.info(
389
+ "TELEGRAM CONNECTED"
390
+ )
391
+
392
+ logger.info(
393
+ f"Bot Name: {bot.get('first_name')}"
394
+ )
395
+
396
+ logger.info(
397
+ f"Bot Username: @{bot.get('username')}"
398
+ )
399
+
400
+ logger.info(
401
+ "===================================="
402
+ )
403
+
404
+ return True
405
+
406
+
407
+ logger.error(
408
+ f"Telegram rejected token: {data}"
409
+ )
410
+
411
+ return False
412
+
413
 
414
+ except Exception as e:
415
+
416
+ logger.exception(
417
+ f"Telegram connection error: {e}"
418
+ )
419
+
420
+ return False
421
 
 
 
 
 
 
422
 
423
+ # ============================================================
424
+ # 14. SEND TELEGRAM MESSAGE
425
+ # ============================================================
426
+
427
+ def send_telegram_message(
428
+ chat_id,
429
+ text
430
+ ):
431
 
 
 
432
  if not TELEGRAM_BOT_TOKEN:
433
  return
434
 
435
+
436
  text = str(text)
437
 
 
 
 
438
 
439
+ # Telegram messages have a length limit,
440
+ # so split long answers.
441
+ chunks = [
442
+
443
+ text[i:i + 4000]
444
+
445
+ for i in range(
446
+ 0,
447
+ len(text),
448
+ 4000
449
  )
 
450
 
451
+ ]
452
+
453
+
454
+ for chunk in chunks:
455
+
456
+ try:
457
+
458
+ response = requests.post(
459
+
460
+ telegram_url(
461
+ "sendMessage"
462
+ ),
463
+
464
+ json={
465
+ "chat_id": chat_id,
466
+ "text": chunk,
467
+ },
468
+
469
+ timeout=15
470
+ )
471
+
472
+
473
+ response.raise_for_status()
474
+
475
+
476
+ except Exception as e:
477
+
478
+ logger.exception(
479
+ f"Telegram send error: {e}"
480
+ )
481
+
482
+
483
+ # ============================================================
484
+ # 15. SHOW TYPING...
485
+ # ============================================================
486
+
487
+ def telegram_typing(chat_id):
488
 
 
489
  try:
490
+
491
  requests.post(
492
+
493
+ telegram_url(
494
+ "sendChatAction"
495
+ ),
496
+
497
  json={
498
  "chat_id": chat_id,
499
  "action": "typing",
500
  },
501
+
502
+ timeout=5
503
  )
504
+
505
+
506
  except Exception:
507
+
508
  pass
509
 
510
 
511
+ # ============================================================
512
+ # 16. TELEGRAM POLLING
513
+ #
514
+ # IMPORTANT:
515
+ # We deliberately use SHORT POLLING for this demo.
516
+ # This avoids the 55-second Hugging Face timeout you saw.
517
+ # ============================================================
518
+
519
+ def telegram_polling_loop():
520
+
521
  if not TELEGRAM_BOT_TOKEN:
522
+
523
+ logger.warning(
524
+ "Telegram disabled because "
525
+ "TELEGRAM_BOT_TOKEN is missing."
526
+ )
527
+
528
+ return
529
+
530
+
531
+ # ----------------------------------------------
532
+ # Verify token first
533
+ # ----------------------------------------------
534
+
535
+ if not test_telegram_connection():
536
+
537
+ logger.error(
538
+ "Telegram connection failed."
539
+ )
540
+
541
  return
542
 
 
543
 
544
+ # ----------------------------------------------
545
+ # Delete old webhook
546
+ # getUpdates will not work with a webhook active.
547
+ # ----------------------------------------------
548
+
549
  try:
550
+
551
  requests.post(
552
+
553
+ telegram_url(
554
+ "deleteWebhook"
555
+ ),
556
+
557
+ json={
558
+ "drop_pending_updates": True
559
+ },
560
+
561
+ timeout=10
562
+ )
563
+
564
+
565
+ logger.info(
566
+ "Old Telegram webhook cleared."
567
  )
568
+
569
+
570
+ except Exception as e:
571
+
572
+ logger.warning(
573
+ f"Could not clear webhook: {e}"
574
+ )
575
+
576
+
577
+ logger.info(
578
+ "===================================="
579
+ )
580
+
581
+ logger.info(
582
+ "DDS HR TELEGRAM BOT STARTED"
583
+ )
584
+
585
+ logger.info(
586
+ "Waiting for Telegram messages..."
587
+ )
588
+
589
+ logger.info(
590
+ "===================================="
591
+ )
592
+
593
 
594
  offset = None
595
 
596
+
597
  while True:
598
+
599
  try:
600
+
601
  params = {
602
+ "timeout": 0
 
603
  }
604
 
605
+
606
  if offset is not None:
607
+
608
  params["offset"] = offset
609
 
610
+
611
+ # SHORT POLLING
612
  response = requests.get(
613
+
614
+ telegram_url(
615
+ "getUpdates"
616
+ ),
617
+
618
  params=params,
619
+
620
+ timeout=10
621
  )
622
+
623
+
624
  response.raise_for_status()
625
 
 
626
 
627
+ data = response.json()
628
+
629
+
630
+ if not data.get("ok"):
631
+
632
+ logger.warning(
633
+ f"Telegram API error: {data}"
634
+ )
635
+
636
+ time.sleep(2)
637
+
638
  continue
639
 
 
 
640
 
641
+ updates = data.get(
642
+ "result",
643
+ []
644
+ )
645
+
646
+
647
+ for update in updates:
648
+
649
+ offset = (
650
+ update["update_id"]
651
+ + 1
652
+ )
653
+
654
+
655
+ message = update.get(
656
+ "message"
657
+ )
658
+
659
+
660
  if not message:
661
+
662
  continue
663
 
 
 
664
 
665
+ chat = message.get(
666
+ "chat",
667
+ {}
668
+ )
669
+
670
+
671
+ chat_id = chat.get(
672
+ "id"
673
+ )
674
+
675
+
676
+ text = (
677
+ message.get(
678
+ "text"
679
+ )
680
+ or ""
681
+ ).strip()
682
+
683
+
684
+ if not chat_id:
685
+
686
  continue
687
 
 
688
 
689
+ if not text:
690
+
691
+ send_telegram_message(
692
+
693
  chat_id,
694
+
695
+ "Please send me a text "
696
+ "question about DDS HR policies."
697
+
698
  )
699
+
700
  continue
701
 
702
+
703
+ logger.info(
704
+ f"Telegram message received: {text}"
705
+ )
706
+
707
+
708
+ # ==================================
709
+ # /start
710
+ # ==================================
711
+
712
+ if text.startswith(
713
+ "/start"
714
+ ):
715
+
716
+ send_telegram_message(
717
+
718
  chat_id,
719
+
720
+ "Welcome to AYesha — "
721
+ "DDS Enterprise HR Chatbot.\n\n"
722
+ "Ask me a question about the "
723
+ "latest DDS HR policies.\n\n"
724
+ "For example:\n"
725
+ "What is the annual leave policy?"
726
+
727
+ )
728
+
729
+ continue
730
+
731
+
732
+ # ==================================
733
+ # /help
734
+ # ==================================
735
+
736
+ if text.startswith(
737
+ "/help"
738
+ ):
739
+
740
+ send_telegram_message(
741
+
742
+ chat_id,
743
+
744
+ "Ask me questions about DDS HR "
745
+ "policies.\n\n"
746
+ "Examples:\n"
747
  "• What is the annual leave policy?\n"
748
  "• What is the probation policy?\n"
749
+ "• How do I request leave?"
750
+
751
  )
752
+
753
  continue
754
 
755
+
756
+ # ==================================
757
+ # OTHER COMMAND
758
+ # ==================================
759
+
760
  if text.startswith("/"):
761
+
762
+ send_telegram_message(
763
+
764
  chat_id,
765
+
766
+ "Please type your HR question "
767
+ "as a normal message."
768
+
769
  )
770
+
771
  continue
772
 
 
773
 
774
+ # ==================================
775
+ # NORMAL HR QUESTION
776
+ # ==================================
777
+
778
+ telegram_typing(
779
+ chat_id
780
+ )
781
+
782
+
783
+ answer = query_doc(
784
+ text
785
+ )
786
+
787
+
788
+ send_telegram_message(
789
+
790
+ chat_id,
791
+ answer
792
+
793
+ )
794
+
795
+
796
+ # Prevent aggressive API calling
797
+ time.sleep(1)
798
+
799
+
800
+ except requests.exceptions.Timeout:
801
+
802
+ # Network timeout is not fatal.
803
+ # Simply try again.
804
+ logger.warning(
805
+ "Telegram temporary timeout. Retrying..."
806
+ )
807
+
808
+ time.sleep(2)
809
+
810
+
811
+ except requests.exceptions.RequestException as e:
812
+
813
+ logger.warning(
814
+ f"Telegram network error: {e}"
815
+ )
816
+
817
+ time.sleep(5)
818
+
819
+
820
+ except Exception as e:
821
+
822
+ logger.exception(
823
+ f"Telegram unexpected error: {e}"
824
+ )
825
 
 
 
826
  time.sleep(5)
827
 
828
 
829
+ # ============================================================
830
+ # 17. START TELEGRAM IN BACKGROUND
831
+ # ============================================================
832
+
833
+ def start_telegram_bot():
834
+
835
  if not TELEGRAM_BOT_TOKEN:
836
+
837
+ logger.warning(
838
+ "Telegram bot NOT started."
839
+ )
840
+
841
  return
842
 
843
+
844
+ thread = threading.Thread(
845
+
846
  target=telegram_polling_loop,
847
+
848
  daemon=True,
849
+
850
+ name="DDS-Telegram-Bot"
851
+
852
+ )
853
+
854
+
855
+ thread.start()
856
+
857
+
858
+ logger.info(
859
+ "Telegram background thread started."
860
  )
 
 
861
 
862
 
863
+ # ============================================================
864
+ # 18. START TELEGRAM
865
+ # ============================================================
866
+
867
  start_telegram_bot()
868
 
869
 
870
+ # ============================================================
871
+ # 19. GRADIO WEB APP
872
+ # ============================================================
873
+
874
  demo = gr.Interface(
875
+
876
  fn=query_doc,
877
+
878
  inputs=gr.Textbox(
879
+
880
  label="Ask a question about DDS HR policies",
881
+
882
+ placeholder=(
883
+ "Example: What is the annual leave policy?"
884
+ )
885
+
886
  ),
887
+
888
+ outputs=gr.Textbox(
889
+ label="Answer"
890
+ ),
891
+
892
  title="DDS Enterprise HR Chatbot",
893
+
894
+ description=(
895
+ "Ask questions based on the latest "
896
+ "DDS HR Handbook."
897
+ )
898
+
899
  )
900
 
901
 
902
+ # ============================================================
903
+ # 20. RUN HUGGING FACE APP
904
+ # ============================================================
905
+
906
  if __name__ == "__main__":
907
+
908
  demo.launch(
909
+
910
  server_name="0.0.0.0",
911
+
912
+ server_port=7860
913
+
914
+ )