Futuresony commited on
Commit
9c0f969
·
verified ·
1 Parent(s): 601eccf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +639 -26
app.py CHANGED
@@ -1,624 +1,1237 @@
1
  # app.py - Combined Script
2
 
 
 
3
  # Combined Imports
 
4
  import os
 
5
  import gradio as gr
 
6
  from huggingface_hub import InferenceClient
 
7
  import torch
 
8
  import re
 
9
  import warnings
 
10
  import time
 
11
  import json
 
12
  from transformers import AutoTokenizer, AutoModelForCausalLM, GenerationConfig, BitsAndBytesConfig
 
13
  from sentence_transformers import SentenceTransformer, util, CrossEncoder
 
14
  import gspread
 
15
  # from google.colab import auth
 
16
  from google.auth import default
 
17
  from tqdm import tqdm
 
18
  from ddgs import DDGS # Updated import
 
19
  import spacy
 
20
  from datetime import date, timedelta, datetime # Import datetime
 
21
  from dateutil.relativedelta import relativedelta # Corrected typo
 
22
  import traceback # Import traceback
 
23
  import base64 # Import base64
 
24
  import dateparser # Import dateparser
 
25
  from dateparser.search import search_dates
 
26
  import pytz # Import pytz for timezone handling
27
 
 
 
28
  # Suppress warnings
 
29
  warnings.filterwarnings("ignore", category=UserWarning)
30
 
 
 
31
  # Define global variables and load secrets
 
32
  HF_TOKEN = os.getenv("HF_TOKEN")
 
33
  # Add a print statement to check if HF_TOKEN is loaded
 
34
  print(f"HF_TOKEN loaded: {'*' * len(HF_TOKEN) if HF_TOKEN else 'None'}")
35
 
 
 
36
  SHEET_ID = "19ipxC2vHYhpXCefpxpIkpeYdI43a1Ku2kYwecgUULIw"
 
37
  GOOGLE_BASE64_CREDENTIALS = os.getenv("GOOGLE_BASE64_CREDENTIALS")
38
 
 
 
39
  # Initialize InferenceClient
 
40
  client = InferenceClient("google/gemma-2-9b-it", token=HF_TOKEN)
41
- # client = InferenceClient("Futuresony/FuturesonyAi-V1.005082025", token=None) # Corrected token usage
 
 
 
 
42
 
43
 
44
  # Load spacy model for sentence splitting
 
45
  nlp = None
 
46
  try:
 
47
  nlp = spacy.load("en_core_web_sm")
 
48
  print("SpaCy model 'en_core_web_sm' loaded.")
 
49
  except OSError:
 
50
  print("SpaCy model 'en_core_web_sm' not found. Downloading...")
 
51
  try:
 
52
  os.system("python -m spacy download en_core_web_sm")
 
53
  nlp = spacy.load("en_core_web_sm")
 
54
  print("SpaCy model 'en_core_web_sm' downloaded and loaded.")
 
55
  except Exception as e:
 
56
  print(f"Failed to download or load SpaCy model: {e}")
57
 
58
 
 
 
 
59
  # Load SentenceTransformer for RAG/business info retrieval and semantic detection
 
60
  embedder = None
 
61
  try:
 
62
  print("Attempting to load Sentence Transformer (sentence-transformers/paraphrase-MiniLM-L6-v2)...")
 
63
  # Use the model provided by the user for semantic detection as well
 
64
  embedder = SentenceTransformer("sentence-transformers/paraphrase-MiniLM-L6-v2") # Or 'all-MiniLM-L6-v2' if preferred
 
65
  print("Sentence Transformer loaded.")
 
66
  except Exception as e:
 
67
  print(f"Error loading Sentence Transformer: {e}")
68
 
69
 
 
 
 
70
  # Load a Cross-Encoder model for re-ranking retrieved documents
 
71
  reranker = None
 
72
  try:
 
73
  print("Attempting to load Cross-Encoder Reranker (cross-encoder/ms-marco-MiniLM-L6-v2)...")
 
74
  reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L6-v2')
 
75
  print("Cross-Encoder Reranker loaded.")
 
76
  except Exception as e:
 
77
  print(f"Error loading Cross-Encoder Reranker: {e}")
 
78
  print("Please ensure the model identifier 'cross-encoder/ms-marco-MiniLM-L6-v2' is correct and accessible on Hugging Face Hub.")
 
79
  print(traceback.format_exc())
 
80
  reranker = None
81
 
82
 
 
 
 
83
  # Google Sheets Authentication
 
84
  gc = None # Global variable for gspread client
 
85
  def authenticate_google_sheets():
 
86
  """Authenticates with Google Sheets using base64 encoded credentials."""
 
87
  global gc
 
88
  print("Authenticating Google Account...")
 
89
  if not GOOGLE_BASE64_CREDENTIALS:
 
90
  print("Error: GOOGLE_BASE64_CREDENTIALS secret not found.")
 
91
  return False
92
 
 
 
93
  try:
 
94
  # Decode the base64 credentials
 
95
  credentials_json = base64.b64decode(GOOGLE_BASE64_CREDENTIALS).decode('utf-8')
 
96
  credentials = json.loads(credentials_json)
97
 
 
 
98
  # Authenticate using service account from dictionary
 
99
  gc = gspread.service_account_from_dict(credentials)
 
100
  print("Google Sheets authentication successful via service account.")
 
101
  return True
 
102
  except Exception as e:
 
103
  print(f"Google Sheets authentication failed: {e}")
 
104
  print("Please ensure your GOOGLE_BASE64_CREDENTIALS secret is correctly set and contains valid service account credentials.")
 
105
  print(traceback.format_exc())
 
106
  return False
107
 
 
 
108
  # Google Sheets Data Loading and Embedding
 
109
  data = [] # Global variable to store loaded data
 
110
  descriptions_for_embedding = []
 
111
  embeddings = torch.tensor([])
 
112
  business_info_available = False # Flag to indicate if business info was loaded successfully
113
 
 
 
114
  def load_business_info():
 
115
  """Loads business information from Google Sheet and creates embeddings."""
 
116
  global data, descriptions_for_embedding, embeddings, business_info_available
 
117
  business_info_available = False # Reset flag
118
 
 
 
119
  if gc is None:
 
120
  print("Skipping Google Sheet loading: Google Sheets client not authenticated.")
 
121
  return
122
 
 
 
123
  if not SHEET_ID:
 
124
  print("Error: SHEET_ID not set.")
 
125
  return
126
 
 
 
127
  try:
 
128
  sheet = gc.open_by_key(SHEET_ID).sheet1
 
129
  print(f"Successfully opened Google Sheet with ID: {SHEET_ID}")
 
130
  data_records = sheet.get_all_records()
131
 
 
 
132
  if not data_records:
 
133
  print(f"Warning: No data records found in Google Sheet with ID: {SHEET_ID}")
 
134
  data = []
 
135
  descriptions_for_embedding = []
 
136
  else:
 
137
  # Filter out rows missing 'Service' or 'Description'
 
138
  filtered_data = [row for row in data_records if row.get('Service') and row.get('Description')]
 
139
  if not filtered_data:
 
140
  print("Warning: Filtered data is empty after checking for 'Service' and 'Description'.")
 
141
  data = []
 
142
  descriptions_for_embedding = []
 
143
  else:
 
144
  data = filtered_data
 
145
  # Use BOTH Service and Description for embedding
 
146
  descriptions_for_embedding = [f"Service: {row['Service']}. Description: {row['Description']}" for row in data]
147
 
 
 
148
  # Only encode if descriptions_for_embedding are found and embedder is available
 
149
  if descriptions_for_embedding and embedder is not None:
 
150
  print("Encoding descriptions...")
 
151
  try:
 
152
  embeddings = embedder.encode(descriptions_for_embedding, convert_to_tensor=True)
 
153
  print("Encoding complete.")
 
154
  business_info_available = True
 
155
  except Exception as e:
 
156
  print(f"Error during description encoding: {e}")
 
157
  embeddings = torch.tensor([])
 
158
  business_info_available = False
 
159
  else:
 
160
  print("Skipping encoding descriptions: No descriptions found or embedder not available.")
 
161
  embeddings = torch.tensor([])
 
162
  business_info_available = False
163
 
 
 
164
  print(f"Loaded {len(descriptions_for_embedding)} entries from Google Sheet for embedding/RAG.")
 
165
  if not business_info_available:
 
166
  print("Business information retrieval (RAG) is NOT available.")
167
 
 
 
168
  except gspread.exceptions.SpreadsheetNotFound:
 
169
  print(f"Error: Google Sheet with ID '{SHEET_ID}' not found.")
 
170
  print("Please check the SHEET_ID and ensure your authenticated Google Account has access to this sheet.")
 
171
  business_info_available = False
 
172
  except Exception as e:
 
173
  print(f"An error occurred while accessing the Google Sheet: {e}")
 
174
  print(traceback.format_exc())
 
175
  business_info_available = False
176
 
 
 
177
  # Business Info Retrieval (RAG)
 
178
  def retrieve_business_info(query: str, top_n: int = 3) -> list:
 
179
  """
 
180
  Retrieves relevant business information from loaded data based on a query.
 
181
  Args:
 
182
  query: The user's query string.
 
183
  top_n: The number of top relevant entries to retrieve.
 
184
  Returns:
 
185
  A list of dictionaries, where each dictionary is a relevant row from the
 
186
  Google Sheet data. Returns an empty list if RAG is not available or
 
187
  no relevant information is found.
 
188
  """
 
189
  global data
 
190
  if not business_info_available or embedder is None or not descriptions_for_embedding or not data:
 
191
  print("Business information retrieval is not available or data is empty.")
 
192
  return []
193
 
 
 
194
  try:
 
195
  query_embedding = embedder.encode(query, convert_to_tensor=True)
 
196
  cosine_scores = util.cos_sim(query_embedding, embeddings)[0]
 
197
  top_results_indices = torch.topk(cosine_scores, k=min(top_n, len(data)))[1].tolist()
 
198
  top_results = [data[i] for i in top_results_indices]
199
 
 
 
200
  if reranker is not None and top_results:
 
201
  print("Re-ranking top results...")
 
202
  rerank_pairs = [(query, descriptions_for_embedding[i]) for i in top_results_indices]
 
203
  rerank_scores = reranker.predict(rerank_pairs)
 
204
  reranked_indices = sorted(range(len(rerank_scores)), key=lambda i: rerank_scores[i], reverse=True)
 
205
  reranked_results = [top_results[i] for i in reranked_indices]
 
206
  print("Re-ranking complete.")
 
207
  return reranked_results
 
208
  else:
 
209
  return top_results
210
 
 
 
211
  except Exception as e:
 
212
  print(f"Error during business information retrieval: {e}")
 
213
  print(traceback.format_exc())
 
214
  return []
215
 
 
 
216
  # Function to perform DuckDuckGo Search and return results with URLs
 
217
  def perform_duckduckgo_search(query: str, max_results: int = 5): # Reduced max_results for multi-part queries
 
218
  """
 
219
  Performs a search using DuckDuckGo and returns a list of dictionaries.
 
220
  Includes a delay to avoid rate limits.
 
221
  Returns an empty list and prints an error if search fails.
 
222
  """
 
223
  print(f"Executing Tool: perform_duckduckgo_search with query='{query}')")
 
224
  search_results_list = []
 
225
  try:
 
226
  time.sleep(1)
227
 
 
 
228
  with DDGS() as ddgs:
 
229
  search_query = query.strip()
230
 
 
 
231
  if not search_query or len(search_query.split()) < 2:
 
232
  print(f"Skipping search for short query: '{search_query}'")
 
233
  return []
234
 
 
 
235
  print(f"Sending search query to DuckDuckGo: '{search_query}'")
 
236
  results_generator = ddgs.text(search_query, max_results=max_results)
 
237
  results_found = False
 
238
  for r in results_generator:
 
239
  search_results_list.append(r)
 
240
  results_found = True
241
 
 
 
242
  print(f"Raw results from DuckDuckGo: {search_results_list}")
243
 
 
 
244
  if not results_found and max_results > 0:
 
245
  print(f"DuckDuckGo search for '{search_query}' returned no results.")
 
246
  elif results_found:
 
247
  print(f"DuckDuckGo search for '{search_query}' completed. Found {len(search_results_list)} results.")
248
 
 
 
249
  except Exception as e:
 
250
  print(f"Error during Duckduckgo search for '{search_query if 'search_query' in locals() else query}': {e}")
 
251
  print(traceback.format_exc())
 
252
  return []
253
 
 
 
254
  return search_results_list
255
 
 
 
256
  # Define the new semantic date/time detection and calculation function using dateparser
 
257
  def perform_date_calculation(query: str) -> str or None:
 
258
  """
 
259
  Analyzes query for date/time information using dateparser.
 
260
  If dateparser finds a date, it returns a human-friendly response string.
 
261
  Otherwise, it returns None.
 
262
  It is designed to handle multiple languages and provide the time for East Africa (Tanzania).
 
263
  """
 
264
  print(f"Executing Tool: perform_date_calculation with query='{query}') using dateparser.search_dates")
265
 
 
 
266
  try:
 
267
  eafrica_tz = pytz.timezone('Africa/Dar_es_Salaam')
 
268
  now = datetime.now(eafrica_tz)
 
269
  except pytz.UnknownTimeZoneError:
 
270
  print("Error: Unknown timezone 'Africa/Dar_es_Salaam'. Using default system time.")
 
271
  now = datetime.now()
272
 
 
 
273
  try:
 
274
  # Try parsing with Swahili first, then English
 
275
  found = search_dates(
 
276
  query,
 
277
  settings={
 
278
  "PREFER_DATES_FROM": "future",
 
279
  "RELATIVE_BASE": now
 
280
  },
 
281
  languages=['sw', 'en'] # Prioritize Swahili
 
282
  )
283
 
 
 
284
  if not found:
 
285
  print("dateparser.search_dates could not parse any date/time.")
 
286
  return None
287
 
 
 
288
  text_snippet, parsed = found[0]
 
289
  print(f"dateparser.search_dates found: text='{text_snippet}', parsed='{parsed}'")
290
 
 
 
291
  is_swahili = any(swahili_phrase in query.lower() for swahili_phrase in ['tarehe', 'siku', 'saa', 'muda', 'leo', 'kesho', 'jana', 'ngapi', 'gani', 'mwezi', 'mwaka'])
292
 
 
 
293
  # Handle timezone information
 
294
  if now.tzinfo is not None and parsed.tzinfo is None:
 
295
  parsed = now.tzinfo.localize(parsed)
 
296
  elif now.tzinfo is None and parsed.tzinfo is not None:
 
297
  parsed = parsed.replace(tzinfo=None)
298
 
 
 
299
  # Check if the parsed date is today and time is close to now or midnight
 
300
  if parsed.date() == now.date():
 
301
  # Consider it "now" if within a small time window or if no specific time was parsed (midnight)
 
302
  if abs((parsed - now).total_seconds()) < 60 or parsed.time() == datetime.min.time():
 
303
  print("Query parsed to today's date and time is close to 'now' or midnight, returning current time/date.")
 
304
  if is_swahili:
 
305
  return f"Kwa saa za Afrika Mashariki (Tanzania), tarehe ya leo ni {now.strftime('%A, %d %B %Y')} na saa ni {now.strftime('%H:%M:%S')}."
 
306
  else:
 
307
  return f"In East Africa (Tanzania), the current date is {now.strftime('%A, %d %B %Y')} and the time is {now.strftime('%H:%M:%S')}."
 
308
  else:
 
309
  print(f"Query parsed to a specific time today: {parsed.strftime('%H:%M:%S')}")
 
310
  if is_swahili:
 
311
  return f"Hiyo inafanyika leo, {parsed.strftime('%A, %d %B %Y')}, saa {parsed.strftime('%H:%M:%S')} saa za Afrika Mashariki."
 
312
  else:
 
313
  return f"That falls on today, {parsed.strftime('%A, %d %B %Y')}, at {parsed.strftime('%H:%M:%S')} East Africa Time."
 
314
  else:
 
315
  print(f"Query parsed to a specific date: {parsed.strftime('%A, %d %B %Y')} at {parsed.strftime('%H:%M:%S')}")
 
316
  time_str = parsed.strftime('%H:%M:%S')
 
317
  date_str = parsed.strftime('%A, %d %B %Y')
 
318
  if parsed.tzinfo:
 
319
  tz_name = parsed.tzinfo.tzname(parsed) or 'UTC'
 
320
  if is_swahili:
 
321
  return f"Hiyo inafanyika tarehe {date_str} saa {time_str} {tz_name}."
 
322
  else:
 
323
  return f"That falls on {date_str} at {time_str} {tz_name}."
 
324
  else:
 
325
  if is_swahili:
 
326
  return f"Hiyo inafanyika tarehe {date_str} saa {time_str}."
 
327
  else:
 
328
  return f"That falls on {date_str} at {time_str}."
329
 
 
 
330
  except Exception as e:
 
331
  print(f"Error during dateparser.search_dates execution: {e}")
 
332
  print(traceback.format_exc())
 
333
  return f"An error occurred while parsing date/time: {e}"
334
 
 
 
335
  # Function to determine if a query requires a tool or can be answered directly
 
336
  def determine_tool_usage(query: str) -> str:
 
337
  """
 
338
  Analyzes the query to determine if a specific tool is needed.
 
339
  Returns the name of the tool ('duckduckgo_search', 'business_info_retrieval',
 
340
  'date_calculation') or 'none' if no specific tool is clearly indicated.
 
341
  Prioritizes business information retrieval, then specific tools based on keywords
 
342
  and LLM judgment.
 
343
  """
 
344
  query_lower = query.lower()
345
 
 
 
346
  # 1. Prioritize Business Info Retrieval if RAG is available
 
347
  if business_info_available:
 
348
  messages_business_check = [{"role": "user", "content": f"Does the following query ask about a specific person, service, offering, or description that is likely to be found *only* within a specific business's internal knowledge base, and not general knowledge? For example, questions about 'Salum' or 'Jackson Kisanga' are likely business-related, while questions about 'the current president of the USA' or 'who won the Ballon d'Or' are general knowledge. Answer only 'yes' or 'no'. Query: {query}"}]
 
349
  try:
 
350
  business_check_response = client.chat_completion(
 
351
  messages=messages_business_check,
 
352
  max_tokens=10,
 
353
  temperature=0.1
 
354
  ).choices[0].message.content.strip().lower()
 
355
  # Ensure the response explicitly contains "yes" and is not just a substring match
 
356
  if business_check_response == "yes":
 
357
  print(f"Detected as specific business info query based on LLM check: '{query}'")
 
358
  return "business_info_retrieval"
 
359
  else:
 
360
  print(f"LLM check indicates not a specific business info query: '{query}'")
 
361
  except Exception as e:
 
362
  print(f"Error during LLM call for business info check for query '{query}': {e}")
 
363
  print(traceback.format_exc())
 
364
  print(f"Proceeding without business info check for query '{query}' due to error.")
365
 
366
 
 
 
 
367
  # 2. Check for Date Calculation
 
368
  date_time_check_result = perform_date_calculation(query)
 
369
  if date_time_check_result is not None:
 
370
  print(f"Detected as date/time calculation query based on dateparser result for: '{query}'")
 
371
  return "date_calculation"
372
 
 
 
373
  # 3. Use LLM to determine if DuckDuckGo search is needed
 
374
  messages_tool_determination_search = [{"role": "user", "content": f"Does the following query require searching the web for current or general knowledge information (e.g., news, facts, definitions, current events)? Respond ONLY with 'duckduckgo_search' or 'none'. Query: {query}"}]
 
375
  try:
 
376
  search_determination_response = client.chat_completion(
 
377
  messages=messages_tool_determination_search,
 
378
  max_tokens=20,
 
379
  temperature=0.1,
 
380
  top_p=0.9
 
381
  ).choices[0].message.content or ""
 
382
  response_lower = search_determination_response.strip().lower()
383
 
 
 
384
  if "duckduckgo_search" in response_lower:
 
385
  print(f"Model-determined tool for '{query}': 'duckduckgo_search'")
 
386
  return "duckduckgo_search"
 
387
  else:
 
388
  print(f"Model-determined tool for '{query}': 'none' (for search)")
389
 
 
 
390
  except Exception as e:
 
391
  print(f"Error during LLM call for search tool determination for query '{query}': {e}")
 
392
  print(traceback.format_exc())
 
393
  print(f"Proceeding without search tool check for query '{query}' due to error.")
394
 
395
 
 
 
 
396
  # 4. If none of the specific tools are determined, default to 'none'
 
397
  print(f"No specific tool determined for '{query}'. Defaulting to 'none'.")
 
398
  return "none"
399
 
400
 
 
401
  # Function to generate text using the LLM, incorporating tool results if available
 
402
  def generate_text(prompt: str, tool_results: dict = None) -> str:
 
403
  """
 
404
  Generates text using the configured LLM, optionally incorporating tool results.
 
405
  Args:
 
406
  prompt: The initial prompt for the LLM.
 
407
  tool_results: A dictionary containing results from executed tools.
 
408
  Keys are tool names, values are their outputs.
 
409
  Returns:
 
410
  The generated text from the LLM.
 
411
  """
 
412
  full_prompt_builder = [prompt]
413
 
 
 
414
  if tool_results and any(tool_results.values()):
 
415
  full_prompt_builder.append("\n\nTool Results:\n")
 
416
  for question, results in tool_results.items(): # Iterate through results per question
 
417
  if results:
 
418
  full_prompt_builder.append(f"--- Results for: {question} ---\n") # Add question context
 
419
  if isinstance(results, list):
 
420
  for i, result in enumerate(results):
 
421
  # Check if the result is from business info retrieval
 
422
  if isinstance(result, dict) and 'Service' in result and 'Description' in result:
 
423
  full_prompt_builder.append(f"Business Info {i+1}:\nService: {result.get('Service', 'N/A')}\nDescription: {result.get('Description', 'N/A')}\n\n")
 
424
  elif isinstance(result, dict) and 'url' in result: # Check if the result is from DuckDuckGo
 
425
  full_prompt_builder.append(f"Search Result {i+1}:\nTitle: {result.get('title', 'N/A')}\nURL: {result.get('url', 'N/A')}\nSnippet: {result.get('body', 'N/A')}\n\n")
 
426
  else:
 
427
  full_prompt_builder.append(f"{result}\n\n") # Handle other list items
 
428
  elif isinstance(results, dict):
 
429
  for key, value in results.items():
 
430
  full_prompt_builder.append(f"{key}: {value}\n")
 
431
  full_prompt_builder.append("\n")
 
432
  else:
 
433
  full_prompt_builder.append(f"{results}\n\n") # Handle single string results (like date calculation)
434
 
 
 
435
  full_prompt_builder.append("Based on the provided tool results, answer the user's original query. If a question was answered by a tool, use the tool's result directly in your response.")
 
436
  print("Added tool results and instruction to final prompt.")
 
437
  else:
 
438
  print("No tool results to add to final prompt.")
439
 
 
 
440
  full_prompt = "".join(full_prompt_builder)
441
 
 
 
442
  print(f"Sending prompt to LLM:\n---\n{full_prompt}\n---")
443
 
 
 
444
  generation_config = {
 
445
  "temperature": 0.7,
 
446
  "max_new_tokens": 500,
 
447
  "top_p": 0.95,
 
448
  "top_k": 50,
 
449
  "do_sample": True,
 
450
  }
451
 
 
 
452
  try:
 
453
  response = client.chat_completion(
 
454
  messages=[
 
455
  {"role": "user", "content": full_prompt}
 
456
  ],
 
457
  max_tokens=generation_config.get("max_new_tokens", 512),
 
458
  temperature=generation_config.get("temperature", 0.7),
 
459
  top_p=generation_config.get("top_p", 0.95)
 
460
  ).choices[0].message.content or ""
461
 
 
 
462
  print("LLM generation successful using chat_completion.")
 
463
  return response
 
464
  except Exception as e:
 
465
  print(f"Error during final LLM generation: {e}")
 
466
  print(traceback.format_exc())
 
467
  return "An error occurred while generating the final response."
468
 
 
 
469
  # Main chat function with query breakdown and tool execution per question
470
- def chat(query: str, history: list):
 
 
471
  """
 
472
  Processes user queries by breaking down multi-part queries, determining and
 
473
  executing appropriate tools for each question, and synthesizing results
 
474
  using the LLM. Prioritizes business information retrieval.
475
- Also updates the chat history.
476
  """
 
477
  print(f"Received query: {query}")
478
 
 
 
479
  # Step 1: Query Breakdown
 
480
  print("\n--- Breaking down query ---")
 
481
  prompt_for_question_breakdown = f"""
 
482
  Analyze the following query and list each distinct question found within it.
 
483
  Present each question on a new line, starting with a hyphen.
 
484
  Query: {query}
 
485
  """
 
486
  try:
 
487
  messages_question_breakdown = [{"role": "user", "content": prompt_for_question_breakdown}]
 
488
  question_breakdown_response = client.chat_completion(
 
489
  messages=messages_question_breakdown,
 
490
  max_tokens=100,
 
491
  temperature=0.1,
 
492
  top_p=0.9
 
493
  ).choices[0].message.content or ""
 
494
  individual_questions = [line.strip() for line in question_breakdown_response.split('\n') if line.strip()]
 
495
  cleaned_questions = [re.sub(r'^[-*]?\s*', '', q) for q in individual_questions]
 
496
  print("Individual questions identified:")
 
497
  for q in cleaned_questions:
 
498
  print(f"- {q}")
 
499
  except Exception as e:
 
500
  print(f"Error during LLM call for question breakdown: {e}")
 
501
  print(traceback.format_exc())
 
502
  cleaned_questions = [query] # Fallback to treating the whole query as one question
503
 
 
 
504
  # Step 2: Tool Determination per Question
 
505
  print("\n--- Determining tools per question ---")
 
506
  determined_tools = {}
 
507
  for question in cleaned_questions:
 
508
  print(f"\nAnalyzing question for tool determination: '{question}'")
 
509
  determined_tools[question] = determine_tool_usage(question)
 
510
  print(f"Determined tool for '{question}': '{determined_tools[question]}'")
511
 
 
 
512
  print("\nSummary of determined tools per question:")
 
513
  for question, tool in determined_tools.items():
 
514
  print(f"'{question}': '{tool}'")
515
 
 
 
516
  # Step 3: Execute Tools and Step 4: Synthesize Results
 
517
  print("\n--- Executing tools and collecting results ---")
 
518
  tool_results = {}
 
519
  for question, tool in determined_tools.items():
 
520
  print(f"\nExecuting tool '{tool}' for question: '{question}'")
 
521
  result = None
522
 
 
 
523
  if tool == "date_calculation":
 
524
  result = perform_date_calculation(question)
 
525
  elif tool == "duckduckgo_search":
 
526
  result = perform_duckduckgo_search(question)
 
527
  elif tool == "business_info_retrieval":
 
528
  result = retrieve_business_info(question)
 
529
  elif tool == "none":
 
530
  # If tool is 'none', the LLM will answer this part using its internal knowledge
 
531
  # in the final response generation step. We don't need a specific tool result here.
 
532
  print(f"Skipping tool execution for question: '{question}' as tool is 'none'. LLM will handle.")
 
533
  result = None # Set result to None so it's not included in tool_results for 'none' tool
534
 
 
 
535
  # Only store results if they are not None (i.e., tool was executed and returned something)
 
536
  if result is not None:
 
537
  tool_results[question] = result
538
 
539
 
 
 
 
540
  print("\n--- Collected Tool Results ---")
 
541
  if tool_results:
 
542
  for question, result in tool_results.items():
 
543
  print(f"\nQuestion: {question}")
 
544
  print(f"Result: {result}")
 
545
  else:
 
546
  print("No tool results were collected.")
 
547
  print("\n--------------------------")
548
 
549
 
 
 
 
550
  # Step 5: Final Response Generation
 
551
  print("\n--- Generating final response ---")
 
552
  # The generate_text function already handles incorporating tool results if provided
 
553
  final_response = generate_text(query, tool_results)
554
 
 
 
555
  print("\n--- Final Response from LLM ---")
 
556
  print(final_response)
 
557
  print("\n----------------------------")
558
 
559
- # Update chat history
560
- history.append((query, final_response))
561
 
562
- return "", history # Return empty string for textbox and updated history
 
 
 
 
 
 
 
 
 
563
 
564
  # Keep the Gradio interface setup as is for now
 
565
  if __name__ == "__main__":
 
566
  # Authenticate Google Sheets when the script starts
 
567
  authenticate_google_sheets()
 
568
  # Load business info after authentication
 
569
  load_business_info()
570
 
 
 
571
  # Check if spacy model, embedder, and reranker loaded correctly
 
572
  if nlp is None:
 
573
  print("Warning: SpaCy model not loaded. Sentence splitting may not work correctly.")
 
574
  if embedder is None:
 
575
  print("Warning: Sentence Transformer (embedder) not loaded. RAG will not be available.")
 
576
  if reranker is None:
 
577
  print("Warning: Cross-Encoder Reranker not loaded. Re-ranking of RAG results will not be performed.")
 
578
  if not business_info_available:
 
579
  print("Warning: Business information (Google Sheet data) not loaded successfully. "
 
580
  "RAG will not be available. Please ensure the GOOGLE_BASE64_CREDENTIALS secret is set correctly.")
581
 
 
 
582
  print("Launching Gradio Interface...")
583
 
 
 
584
  import gradio as gr
585
 
586
- css = """
587
- .gradio-container {
588
- max-width: 800px;
589
- margin: auto;
590
- }
591
- .gradio-container .gr-image {
592
- max-width: 100px; /* Adjust as needed */
593
- height: auto;
594
- }
595
- """
596
 
597
- with gr.Blocks(theme="soft", css=css) as demo:
 
 
598
  gr.Markdown(
 
599
  """
 
600
  # LLM with Tools (DuckDuckGo Search, Date Calculation, Business Info RAG)
 
601
  Ask me anything! I can perform web searches, calculate dates, and retrieve business information.
 
602
  """
 
603
  )
604
 
605
- chatbot = gr.Chatbot(avatar_images=[None, None]) # Explicitly set avatar_images to None
 
606
  with gr.Row():
607
- msg = gr.Textbox(label="Query", placeholder="Enter your query here...", lines=2, scale=4)
608
- submit_button = gr.Button("Send", scale=1)
609
- clear = gr.Button("Clear")
610
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
 
612
- # Update the chat function call to include history
613
- msg.submit(chat, [msg, chatbot], [msg, chatbot])
614
- submit_button.click(chat, [msg, chatbot], [msg, chatbot])
615
- clear.click(lambda: None, None, chatbot, queue=False)
616
 
617
 
618
  try:
 
619
  demo.launch(debug=True)
 
620
  except Exception as e:
 
621
  print(f"Error launching Gradio interface: {e}")
 
622
  print(traceback.format_exc())
623
- print("Please check the console output for more details on the error.")
624
 
 
 
1
  # app.py - Combined Script
2
 
3
+
4
+
5
  # Combined Imports
6
+
7
  import os
8
+
9
  import gradio as gr
10
+
11
  from huggingface_hub import InferenceClient
12
+
13
  import torch
14
+
15
  import re
16
+
17
  import warnings
18
+
19
  import time
20
+
21
  import json
22
+
23
  from transformers import AutoTokenizer, AutoModelForCausalLM, GenerationConfig, BitsAndBytesConfig
24
+
25
  from sentence_transformers import SentenceTransformer, util, CrossEncoder
26
+
27
  import gspread
28
+
29
  # from google.colab import auth
30
+
31
  from google.auth import default
32
+
33
  from tqdm import tqdm
34
+
35
  from ddgs import DDGS # Updated import
36
+
37
  import spacy
38
+
39
  from datetime import date, timedelta, datetime # Import datetime
40
+
41
  from dateutil.relativedelta import relativedelta # Corrected typo
42
+
43
  import traceback # Import traceback
44
+
45
  import base64 # Import base64
46
+
47
  import dateparser # Import dateparser
48
+
49
  from dateparser.search import search_dates
50
+
51
  import pytz # Import pytz for timezone handling
52
 
53
+
54
+
55
  # Suppress warnings
56
+
57
  warnings.filterwarnings("ignore", category=UserWarning)
58
 
59
+
60
+
61
  # Define global variables and load secrets
62
+
63
  HF_TOKEN = os.getenv("HF_TOKEN")
64
+
65
  # Add a print statement to check if HF_TOKEN is loaded
66
+
67
  print(f"HF_TOKEN loaded: {'*' * len(HF_TOKEN) if HF_TOKEN else 'None'}")
68
 
69
+
70
+
71
  SHEET_ID = "19ipxC2vHYhpXCefpxpIkpeYdI43a1Ku2kYwecgUULIw"
72
+
73
  GOOGLE_BASE64_CREDENTIALS = os.getenv("GOOGLE_BASE64_CREDENTIALS")
74
 
75
+
76
+
77
  # Initialize InferenceClient
78
+
79
  client = InferenceClient("google/gemma-2-9b-it", token=HF_TOKEN)
80
+
81
+ # client = InferenceClient("Futuresony/FuturesonyAi-V1.005082025", token=HF_TOKEN)
82
+
83
+
84
+
85
 
86
 
87
  # Load spacy model for sentence splitting
88
+
89
  nlp = None
90
+
91
  try:
92
+
93
  nlp = spacy.load("en_core_web_sm")
94
+
95
  print("SpaCy model 'en_core_web_sm' loaded.")
96
+
97
  except OSError:
98
+
99
  print("SpaCy model 'en_core_web_sm' not found. Downloading...")
100
+
101
  try:
102
+
103
  os.system("python -m spacy download en_core_web_sm")
104
+
105
  nlp = spacy.load("en_core_web_sm")
106
+
107
  print("SpaCy model 'en_core_web_sm' downloaded and loaded.")
108
+
109
  except Exception as e:
110
+
111
  print(f"Failed to download or load SpaCy model: {e}")
112
 
113
 
114
+
115
+
116
+
117
  # Load SentenceTransformer for RAG/business info retrieval and semantic detection
118
+
119
  embedder = None
120
+
121
  try:
122
+
123
  print("Attempting to load Sentence Transformer (sentence-transformers/paraphrase-MiniLM-L6-v2)...")
124
+
125
  # Use the model provided by the user for semantic detection as well
126
+
127
  embedder = SentenceTransformer("sentence-transformers/paraphrase-MiniLM-L6-v2") # Or 'all-MiniLM-L6-v2' if preferred
128
+
129
  print("Sentence Transformer loaded.")
130
+
131
  except Exception as e:
132
+
133
  print(f"Error loading Sentence Transformer: {e}")
134
 
135
 
136
+
137
+
138
+
139
  # Load a Cross-Encoder model for re-ranking retrieved documents
140
+
141
  reranker = None
142
+
143
  try:
144
+
145
  print("Attempting to load Cross-Encoder Reranker (cross-encoder/ms-marco-MiniLM-L6-v2)...")
146
+
147
  reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L6-v2')
148
+
149
  print("Cross-Encoder Reranker loaded.")
150
+
151
  except Exception as e:
152
+
153
  print(f"Error loading Cross-Encoder Reranker: {e}")
154
+
155
  print("Please ensure the model identifier 'cross-encoder/ms-marco-MiniLM-L6-v2' is correct and accessible on Hugging Face Hub.")
156
+
157
  print(traceback.format_exc())
158
+
159
  reranker = None
160
 
161
 
162
+
163
+
164
+
165
  # Google Sheets Authentication
166
+
167
  gc = None # Global variable for gspread client
168
+
169
  def authenticate_google_sheets():
170
+
171
  """Authenticates with Google Sheets using base64 encoded credentials."""
172
+
173
  global gc
174
+
175
  print("Authenticating Google Account...")
176
+
177
  if not GOOGLE_BASE64_CREDENTIALS:
178
+
179
  print("Error: GOOGLE_BASE64_CREDENTIALS secret not found.")
180
+
181
  return False
182
 
183
+
184
+
185
  try:
186
+
187
  # Decode the base64 credentials
188
+
189
  credentials_json = base64.b64decode(GOOGLE_BASE64_CREDENTIALS).decode('utf-8')
190
+
191
  credentials = json.loads(credentials_json)
192
 
193
+
194
+
195
  # Authenticate using service account from dictionary
196
+
197
  gc = gspread.service_account_from_dict(credentials)
198
+
199
  print("Google Sheets authentication successful via service account.")
200
+
201
  return True
202
+
203
  except Exception as e:
204
+
205
  print(f"Google Sheets authentication failed: {e}")
206
+
207
  print("Please ensure your GOOGLE_BASE64_CREDENTIALS secret is correctly set and contains valid service account credentials.")
208
+
209
  print(traceback.format_exc())
210
+
211
  return False
212
 
213
+
214
+
215
  # Google Sheets Data Loading and Embedding
216
+
217
  data = [] # Global variable to store loaded data
218
+
219
  descriptions_for_embedding = []
220
+
221
  embeddings = torch.tensor([])
222
+
223
  business_info_available = False # Flag to indicate if business info was loaded successfully
224
 
225
+
226
+
227
  def load_business_info():
228
+
229
  """Loads business information from Google Sheet and creates embeddings."""
230
+
231
  global data, descriptions_for_embedding, embeddings, business_info_available
232
+
233
  business_info_available = False # Reset flag
234
 
235
+
236
+
237
  if gc is None:
238
+
239
  print("Skipping Google Sheet loading: Google Sheets client not authenticated.")
240
+
241
  return
242
 
243
+
244
+
245
  if not SHEET_ID:
246
+
247
  print("Error: SHEET_ID not set.")
248
+
249
  return
250
 
251
+
252
+
253
  try:
254
+
255
  sheet = gc.open_by_key(SHEET_ID).sheet1
256
+
257
  print(f"Successfully opened Google Sheet with ID: {SHEET_ID}")
258
+
259
  data_records = sheet.get_all_records()
260
 
261
+
262
+
263
  if not data_records:
264
+
265
  print(f"Warning: No data records found in Google Sheet with ID: {SHEET_ID}")
266
+
267
  data = []
268
+
269
  descriptions_for_embedding = []
270
+
271
  else:
272
+
273
  # Filter out rows missing 'Service' or 'Description'
274
+
275
  filtered_data = [row for row in data_records if row.get('Service') and row.get('Description')]
276
+
277
  if not filtered_data:
278
+
279
  print("Warning: Filtered data is empty after checking for 'Service' and 'Description'.")
280
+
281
  data = []
282
+
283
  descriptions_for_embedding = []
284
+
285
  else:
286
+
287
  data = filtered_data
288
+
289
  # Use BOTH Service and Description for embedding
290
+
291
  descriptions_for_embedding = [f"Service: {row['Service']}. Description: {row['Description']}" for row in data]
292
 
293
+
294
+
295
  # Only encode if descriptions_for_embedding are found and embedder is available
296
+
297
  if descriptions_for_embedding and embedder is not None:
298
+
299
  print("Encoding descriptions...")
300
+
301
  try:
302
+
303
  embeddings = embedder.encode(descriptions_for_embedding, convert_to_tensor=True)
304
+
305
  print("Encoding complete.")
306
+
307
  business_info_available = True
308
+
309
  except Exception as e:
310
+
311
  print(f"Error during description encoding: {e}")
312
+
313
  embeddings = torch.tensor([])
314
+
315
  business_info_available = False
316
+
317
  else:
318
+
319
  print("Skipping encoding descriptions: No descriptions found or embedder not available.")
320
+
321
  embeddings = torch.tensor([])
322
+
323
  business_info_available = False
324
 
325
+
326
+
327
  print(f"Loaded {len(descriptions_for_embedding)} entries from Google Sheet for embedding/RAG.")
328
+
329
  if not business_info_available:
330
+
331
  print("Business information retrieval (RAG) is NOT available.")
332
 
333
+
334
+
335
  except gspread.exceptions.SpreadsheetNotFound:
336
+
337
  print(f"Error: Google Sheet with ID '{SHEET_ID}' not found.")
338
+
339
  print("Please check the SHEET_ID and ensure your authenticated Google Account has access to this sheet.")
340
+
341
  business_info_available = False
342
+
343
  except Exception as e:
344
+
345
  print(f"An error occurred while accessing the Google Sheet: {e}")
346
+
347
  print(traceback.format_exc())
348
+
349
  business_info_available = False
350
 
351
+
352
+
353
  # Business Info Retrieval (RAG)
354
+
355
  def retrieve_business_info(query: str, top_n: int = 3) -> list:
356
+
357
  """
358
+
359
  Retrieves relevant business information from loaded data based on a query.
360
+
361
  Args:
362
+
363
  query: The user's query string.
364
+
365
  top_n: The number of top relevant entries to retrieve.
366
+
367
  Returns:
368
+
369
  A list of dictionaries, where each dictionary is a relevant row from the
370
+
371
  Google Sheet data. Returns an empty list if RAG is not available or
372
+
373
  no relevant information is found.
374
+
375
  """
376
+
377
  global data
378
+
379
  if not business_info_available or embedder is None or not descriptions_for_embedding or not data:
380
+
381
  print("Business information retrieval is not available or data is empty.")
382
+
383
  return []
384
 
385
+
386
+
387
  try:
388
+
389
  query_embedding = embedder.encode(query, convert_to_tensor=True)
390
+
391
  cosine_scores = util.cos_sim(query_embedding, embeddings)[0]
392
+
393
  top_results_indices = torch.topk(cosine_scores, k=min(top_n, len(data)))[1].tolist()
394
+
395
  top_results = [data[i] for i in top_results_indices]
396
 
397
+
398
+
399
  if reranker is not None and top_results:
400
+
401
  print("Re-ranking top results...")
402
+
403
  rerank_pairs = [(query, descriptions_for_embedding[i]) for i in top_results_indices]
404
+
405
  rerank_scores = reranker.predict(rerank_pairs)
406
+
407
  reranked_indices = sorted(range(len(rerank_scores)), key=lambda i: rerank_scores[i], reverse=True)
408
+
409
  reranked_results = [top_results[i] for i in reranked_indices]
410
+
411
  print("Re-ranking complete.")
412
+
413
  return reranked_results
414
+
415
  else:
416
+
417
  return top_results
418
 
419
+
420
+
421
  except Exception as e:
422
+
423
  print(f"Error during business information retrieval: {e}")
424
+
425
  print(traceback.format_exc())
426
+
427
  return []
428
 
429
+
430
+
431
  # Function to perform DuckDuckGo Search and return results with URLs
432
+
433
  def perform_duckduckgo_search(query: str, max_results: int = 5): # Reduced max_results for multi-part queries
434
+
435
  """
436
+
437
  Performs a search using DuckDuckGo and returns a list of dictionaries.
438
+
439
  Includes a delay to avoid rate limits.
440
+
441
  Returns an empty list and prints an error if search fails.
442
+
443
  """
444
+
445
  print(f"Executing Tool: perform_duckduckgo_search with query='{query}')")
446
+
447
  search_results_list = []
448
+
449
  try:
450
+
451
  time.sleep(1)
452
 
453
+
454
+
455
  with DDGS() as ddgs:
456
+
457
  search_query = query.strip()
458
 
459
+
460
+
461
  if not search_query or len(search_query.split()) < 2:
462
+
463
  print(f"Skipping search for short query: '{search_query}'")
464
+
465
  return []
466
 
467
+
468
+
469
  print(f"Sending search query to DuckDuckGo: '{search_query}'")
470
+
471
  results_generator = ddgs.text(search_query, max_results=max_results)
472
+
473
  results_found = False
474
+
475
  for r in results_generator:
476
+
477
  search_results_list.append(r)
478
+
479
  results_found = True
480
 
481
+
482
+
483
  print(f"Raw results from DuckDuckGo: {search_results_list}")
484
 
485
+
486
+
487
  if not results_found and max_results > 0:
488
+
489
  print(f"DuckDuckGo search for '{search_query}' returned no results.")
490
+
491
  elif results_found:
492
+
493
  print(f"DuckDuckGo search for '{search_query}' completed. Found {len(search_results_list)} results.")
494
 
495
+
496
+
497
  except Exception as e:
498
+
499
  print(f"Error during Duckduckgo search for '{search_query if 'search_query' in locals() else query}': {e}")
500
+
501
  print(traceback.format_exc())
502
+
503
  return []
504
 
505
+
506
+
507
  return search_results_list
508
 
509
+
510
+
511
  # Define the new semantic date/time detection and calculation function using dateparser
512
+
513
  def perform_date_calculation(query: str) -> str or None:
514
+
515
  """
516
+
517
  Analyzes query for date/time information using dateparser.
518
+
519
  If dateparser finds a date, it returns a human-friendly response string.
520
+
521
  Otherwise, it returns None.
522
+
523
  It is designed to handle multiple languages and provide the time for East Africa (Tanzania).
524
+
525
  """
526
+
527
  print(f"Executing Tool: perform_date_calculation with query='{query}') using dateparser.search_dates")
528
 
529
+
530
+
531
  try:
532
+
533
  eafrica_tz = pytz.timezone('Africa/Dar_es_Salaam')
534
+
535
  now = datetime.now(eafrica_tz)
536
+
537
  except pytz.UnknownTimeZoneError:
538
+
539
  print("Error: Unknown timezone 'Africa/Dar_es_Salaam'. Using default system time.")
540
+
541
  now = datetime.now()
542
 
543
+
544
+
545
  try:
546
+
547
  # Try parsing with Swahili first, then English
548
+
549
  found = search_dates(
550
+
551
  query,
552
+
553
  settings={
554
+
555
  "PREFER_DATES_FROM": "future",
556
+
557
  "RELATIVE_BASE": now
558
+
559
  },
560
+
561
  languages=['sw', 'en'] # Prioritize Swahili
562
+
563
  )
564
 
565
+
566
+
567
  if not found:
568
+
569
  print("dateparser.search_dates could not parse any date/time.")
570
+
571
  return None
572
 
573
+
574
+
575
  text_snippet, parsed = found[0]
576
+
577
  print(f"dateparser.search_dates found: text='{text_snippet}', parsed='{parsed}'")
578
 
579
+
580
+
581
  is_swahili = any(swahili_phrase in query.lower() for swahili_phrase in ['tarehe', 'siku', 'saa', 'muda', 'leo', 'kesho', 'jana', 'ngapi', 'gani', 'mwezi', 'mwaka'])
582
 
583
+
584
+
585
  # Handle timezone information
586
+
587
  if now.tzinfo is not None and parsed.tzinfo is None:
588
+
589
  parsed = now.tzinfo.localize(parsed)
590
+
591
  elif now.tzinfo is None and parsed.tzinfo is not None:
592
+
593
  parsed = parsed.replace(tzinfo=None)
594
 
595
+
596
+
597
  # Check if the parsed date is today and time is close to now or midnight
598
+
599
  if parsed.date() == now.date():
600
+
601
  # Consider it "now" if within a small time window or if no specific time was parsed (midnight)
602
+
603
  if abs((parsed - now).total_seconds()) < 60 or parsed.time() == datetime.min.time():
604
+
605
  print("Query parsed to today's date and time is close to 'now' or midnight, returning current time/date.")
606
+
607
  if is_swahili:
608
+
609
  return f"Kwa saa za Afrika Mashariki (Tanzania), tarehe ya leo ni {now.strftime('%A, %d %B %Y')} na saa ni {now.strftime('%H:%M:%S')}."
610
+
611
  else:
612
+
613
  return f"In East Africa (Tanzania), the current date is {now.strftime('%A, %d %B %Y')} and the time is {now.strftime('%H:%M:%S')}."
614
+
615
  else:
616
+
617
  print(f"Query parsed to a specific time today: {parsed.strftime('%H:%M:%S')}")
618
+
619
  if is_swahili:
620
+
621
  return f"Hiyo inafanyika leo, {parsed.strftime('%A, %d %B %Y')}, saa {parsed.strftime('%H:%M:%S')} saa za Afrika Mashariki."
622
+
623
  else:
624
+
625
  return f"That falls on today, {parsed.strftime('%A, %d %B %Y')}, at {parsed.strftime('%H:%M:%S')} East Africa Time."
626
+
627
  else:
628
+
629
  print(f"Query parsed to a specific date: {parsed.strftime('%A, %d %B %Y')} at {parsed.strftime('%H:%M:%S')}")
630
+
631
  time_str = parsed.strftime('%H:%M:%S')
632
+
633
  date_str = parsed.strftime('%A, %d %B %Y')
634
+
635
  if parsed.tzinfo:
636
+
637
  tz_name = parsed.tzinfo.tzname(parsed) or 'UTC'
638
+
639
  if is_swahili:
640
+
641
  return f"Hiyo inafanyika tarehe {date_str} saa {time_str} {tz_name}."
642
+
643
  else:
644
+
645
  return f"That falls on {date_str} at {time_str} {tz_name}."
646
+
647
  else:
648
+
649
  if is_swahili:
650
+
651
  return f"Hiyo inafanyika tarehe {date_str} saa {time_str}."
652
+
653
  else:
654
+
655
  return f"That falls on {date_str} at {time_str}."
656
 
657
+
658
+
659
  except Exception as e:
660
+
661
  print(f"Error during dateparser.search_dates execution: {e}")
662
+
663
  print(traceback.format_exc())
664
+
665
  return f"An error occurred while parsing date/time: {e}"
666
 
667
+
668
+
669
  # Function to determine if a query requires a tool or can be answered directly
670
+
671
  def determine_tool_usage(query: str) -> str:
672
+
673
  """
674
+
675
  Analyzes the query to determine if a specific tool is needed.
676
+
677
  Returns the name of the tool ('duckduckgo_search', 'business_info_retrieval',
678
+
679
  'date_calculation') or 'none' if no specific tool is clearly indicated.
680
+
681
  Prioritizes business information retrieval, then specific tools based on keywords
682
+
683
  and LLM judgment.
684
+
685
  """
686
+
687
  query_lower = query.lower()
688
 
689
+
690
+
691
  # 1. Prioritize Business Info Retrieval if RAG is available
692
+
693
  if business_info_available:
694
+
695
  messages_business_check = [{"role": "user", "content": f"Does the following query ask about a specific person, service, offering, or description that is likely to be found *only* within a specific business's internal knowledge base, and not general knowledge? For example, questions about 'Salum' or 'Jackson Kisanga' are likely business-related, while questions about 'the current president of the USA' or 'who won the Ballon d'Or' are general knowledge. Answer only 'yes' or 'no'. Query: {query}"}]
696
+
697
  try:
698
+
699
  business_check_response = client.chat_completion(
700
+
701
  messages=messages_business_check,
702
+
703
  max_tokens=10,
704
+
705
  temperature=0.1
706
+
707
  ).choices[0].message.content.strip().lower()
708
+
709
  # Ensure the response explicitly contains "yes" and is not just a substring match
710
+
711
  if business_check_response == "yes":
712
+
713
  print(f"Detected as specific business info query based on LLM check: '{query}'")
714
+
715
  return "business_info_retrieval"
716
+
717
  else:
718
+
719
  print(f"LLM check indicates not a specific business info query: '{query}'")
720
+
721
  except Exception as e:
722
+
723
  print(f"Error during LLM call for business info check for query '{query}': {e}")
724
+
725
  print(traceback.format_exc())
726
+
727
  print(f"Proceeding without business info check for query '{query}' due to error.")
728
 
729
 
730
+
731
+
732
+
733
  # 2. Check for Date Calculation
734
+
735
  date_time_check_result = perform_date_calculation(query)
736
+
737
  if date_time_check_result is not None:
738
+
739
  print(f"Detected as date/time calculation query based on dateparser result for: '{query}'")
740
+
741
  return "date_calculation"
742
 
743
+
744
+
745
  # 3. Use LLM to determine if DuckDuckGo search is needed
746
+
747
  messages_tool_determination_search = [{"role": "user", "content": f"Does the following query require searching the web for current or general knowledge information (e.g., news, facts, definitions, current events)? Respond ONLY with 'duckduckgo_search' or 'none'. Query: {query}"}]
748
+
749
  try:
750
+
751
  search_determination_response = client.chat_completion(
752
+
753
  messages=messages_tool_determination_search,
754
+
755
  max_tokens=20,
756
+
757
  temperature=0.1,
758
+
759
  top_p=0.9
760
+
761
  ).choices[0].message.content or ""
762
+
763
  response_lower = search_determination_response.strip().lower()
764
 
765
+
766
+
767
  if "duckduckgo_search" in response_lower:
768
+
769
  print(f"Model-determined tool for '{query}': 'duckduckgo_search'")
770
+
771
  return "duckduckgo_search"
772
+
773
  else:
774
+
775
  print(f"Model-determined tool for '{query}': 'none' (for search)")
776
 
777
+
778
+
779
  except Exception as e:
780
+
781
  print(f"Error during LLM call for search tool determination for query '{query}': {e}")
782
+
783
  print(traceback.format_exc())
784
+
785
  print(f"Proceeding without search tool check for query '{query}' due to error.")
786
 
787
 
788
+
789
+
790
+
791
  # 4. If none of the specific tools are determined, default to 'none'
792
+
793
  print(f"No specific tool determined for '{query}'. Defaulting to 'none'.")
794
+
795
  return "none"
796
 
797
 
798
+
799
  # Function to generate text using the LLM, incorporating tool results if available
800
+
801
  def generate_text(prompt: str, tool_results: dict = None) -> str:
802
+
803
  """
804
+
805
  Generates text using the configured LLM, optionally incorporating tool results.
806
+
807
  Args:
808
+
809
  prompt: The initial prompt for the LLM.
810
+
811
  tool_results: A dictionary containing results from executed tools.
812
+
813
  Keys are tool names, values are their outputs.
814
+
815
  Returns:
816
+
817
  The generated text from the LLM.
818
+
819
  """
820
+
821
  full_prompt_builder = [prompt]
822
 
823
+
824
+
825
  if tool_results and any(tool_results.values()):
826
+
827
  full_prompt_builder.append("\n\nTool Results:\n")
828
+
829
  for question, results in tool_results.items(): # Iterate through results per question
830
+
831
  if results:
832
+
833
  full_prompt_builder.append(f"--- Results for: {question} ---\n") # Add question context
834
+
835
  if isinstance(results, list):
836
+
837
  for i, result in enumerate(results):
838
+
839
  # Check if the result is from business info retrieval
840
+
841
  if isinstance(result, dict) and 'Service' in result and 'Description' in result:
842
+
843
  full_prompt_builder.append(f"Business Info {i+1}:\nService: {result.get('Service', 'N/A')}\nDescription: {result.get('Description', 'N/A')}\n\n")
844
+
845
  elif isinstance(result, dict) and 'url' in result: # Check if the result is from DuckDuckGo
846
+
847
  full_prompt_builder.append(f"Search Result {i+1}:\nTitle: {result.get('title', 'N/A')}\nURL: {result.get('url', 'N/A')}\nSnippet: {result.get('body', 'N/A')}\n\n")
848
+
849
  else:
850
+
851
  full_prompt_builder.append(f"{result}\n\n") # Handle other list items
852
+
853
  elif isinstance(results, dict):
854
+
855
  for key, value in results.items():
856
+
857
  full_prompt_builder.append(f"{key}: {value}\n")
858
+
859
  full_prompt_builder.append("\n")
860
+
861
  else:
862
+
863
  full_prompt_builder.append(f"{results}\n\n") # Handle single string results (like date calculation)
864
 
865
+
866
+
867
  full_prompt_builder.append("Based on the provided tool results, answer the user's original query. If a question was answered by a tool, use the tool's result directly in your response.")
868
+
869
  print("Added tool results and instruction to final prompt.")
870
+
871
  else:
872
+
873
  print("No tool results to add to final prompt.")
874
 
875
+
876
+
877
  full_prompt = "".join(full_prompt_builder)
878
 
879
+
880
+
881
  print(f"Sending prompt to LLM:\n---\n{full_prompt}\n---")
882
 
883
+
884
+
885
  generation_config = {
886
+
887
  "temperature": 0.7,
888
+
889
  "max_new_tokens": 500,
890
+
891
  "top_p": 0.95,
892
+
893
  "top_k": 50,
894
+
895
  "do_sample": True,
896
+
897
  }
898
 
899
+
900
+
901
  try:
902
+
903
  response = client.chat_completion(
904
+
905
  messages=[
906
+
907
  {"role": "user", "content": full_prompt}
908
+
909
  ],
910
+
911
  max_tokens=generation_config.get("max_new_tokens", 512),
912
+
913
  temperature=generation_config.get("temperature", 0.7),
914
+
915
  top_p=generation_config.get("top_p", 0.95)
916
+
917
  ).choices[0].message.content or ""
918
 
919
+
920
+
921
  print("LLM generation successful using chat_completion.")
922
+
923
  return response
924
+
925
  except Exception as e:
926
+
927
  print(f"Error during final LLM generation: {e}")
928
+
929
  print(traceback.format_exc())
930
+
931
  return "An error occurred while generating the final response."
932
 
933
+
934
+
935
  # Main chat function with query breakdown and tool execution per question
936
+
937
+ def chat(query: str):
938
+
939
  """
940
+
941
  Processes user queries by breaking down multi-part queries, determining and
942
+
943
  executing appropriate tools for each question, and synthesizing results
944
+
945
  using the LLM. Prioritizes business information retrieval.
946
+
947
  """
948
+
949
  print(f"Received query: {query}")
950
 
951
+
952
+
953
  # Step 1: Query Breakdown
954
+
955
  print("\n--- Breaking down query ---")
956
+
957
  prompt_for_question_breakdown = f"""
958
+
959
  Analyze the following query and list each distinct question found within it.
960
+
961
  Present each question on a new line, starting with a hyphen.
962
+
963
  Query: {query}
964
+
965
  """
966
+
967
  try:
968
+
969
  messages_question_breakdown = [{"role": "user", "content": prompt_for_question_breakdown}]
970
+
971
  question_breakdown_response = client.chat_completion(
972
+
973
  messages=messages_question_breakdown,
974
+
975
  max_tokens=100,
976
+
977
  temperature=0.1,
978
+
979
  top_p=0.9
980
+
981
  ).choices[0].message.content or ""
982
+
983
  individual_questions = [line.strip() for line in question_breakdown_response.split('\n') if line.strip()]
984
+
985
  cleaned_questions = [re.sub(r'^[-*]?\s*', '', q) for q in individual_questions]
986
+
987
  print("Individual questions identified:")
988
+
989
  for q in cleaned_questions:
990
+
991
  print(f"- {q}")
992
+
993
  except Exception as e:
994
+
995
  print(f"Error during LLM call for question breakdown: {e}")
996
+
997
  print(traceback.format_exc())
998
+
999
  cleaned_questions = [query] # Fallback to treating the whole query as one question
1000
 
1001
+
1002
+
1003
  # Step 2: Tool Determination per Question
1004
+
1005
  print("\n--- Determining tools per question ---")
1006
+
1007
  determined_tools = {}
1008
+
1009
  for question in cleaned_questions:
1010
+
1011
  print(f"\nAnalyzing question for tool determination: '{question}'")
1012
+
1013
  determined_tools[question] = determine_tool_usage(question)
1014
+
1015
  print(f"Determined tool for '{question}': '{determined_tools[question]}'")
1016
 
1017
+
1018
+
1019
  print("\nSummary of determined tools per question:")
1020
+
1021
  for question, tool in determined_tools.items():
1022
+
1023
  print(f"'{question}': '{tool}'")
1024
 
1025
+
1026
+
1027
  # Step 3: Execute Tools and Step 4: Synthesize Results
1028
+
1029
  print("\n--- Executing tools and collecting results ---")
1030
+
1031
  tool_results = {}
1032
+
1033
  for question, tool in determined_tools.items():
1034
+
1035
  print(f"\nExecuting tool '{tool}' for question: '{question}'")
1036
+
1037
  result = None
1038
 
1039
+
1040
+
1041
  if tool == "date_calculation":
1042
+
1043
  result = perform_date_calculation(question)
1044
+
1045
  elif tool == "duckduckgo_search":
1046
+
1047
  result = perform_duckduckgo_search(question)
1048
+
1049
  elif tool == "business_info_retrieval":
1050
+
1051
  result = retrieve_business_info(question)
1052
+
1053
  elif tool == "none":
1054
+
1055
  # If tool is 'none', the LLM will answer this part using its internal knowledge
1056
+
1057
  # in the final response generation step. We don't need a specific tool result here.
1058
+
1059
  print(f"Skipping tool execution for question: '{question}' as tool is 'none'. LLM will handle.")
1060
+
1061
  result = None # Set result to None so it's not included in tool_results for 'none' tool
1062
 
1063
+
1064
+
1065
  # Only store results if they are not None (i.e., tool was executed and returned something)
1066
+
1067
  if result is not None:
1068
+
1069
  tool_results[question] = result
1070
 
1071
 
1072
+
1073
+
1074
+
1075
  print("\n--- Collected Tool Results ---")
1076
+
1077
  if tool_results:
1078
+
1079
  for question, result in tool_results.items():
1080
+
1081
  print(f"\nQuestion: {question}")
1082
+
1083
  print(f"Result: {result}")
1084
+
1085
  else:
1086
+
1087
  print("No tool results were collected.")
1088
+
1089
  print("\n--------------------------")
1090
 
1091
 
1092
+
1093
+
1094
+
1095
  # Step 5: Final Response Generation
1096
+
1097
  print("\n--- Generating final response ---")
1098
+
1099
  # The generate_text function already handles incorporating tool results if provided
1100
+
1101
  final_response = generate_text(query, tool_results)
1102
 
1103
+
1104
+
1105
  print("\n--- Final Response from LLM ---")
1106
+
1107
  print(final_response)
1108
+
1109
  print("\n----------------------------")
1110
 
 
 
1111
 
1112
+
1113
+ return final_response
1114
+
1115
+
1116
+
1117
+
1118
+
1119
+
1120
+
1121
+
1122
 
1123
  # Keep the Gradio interface setup as is for now
1124
+
1125
  if __name__ == "__main__":
1126
+
1127
  # Authenticate Google Sheets when the script starts
1128
+
1129
  authenticate_google_sheets()
1130
+
1131
  # Load business info after authentication
1132
+
1133
  load_business_info()
1134
 
1135
+
1136
+
1137
  # Check if spacy model, embedder, and reranker loaded correctly
1138
+
1139
  if nlp is None:
1140
+
1141
  print("Warning: SpaCy model not loaded. Sentence splitting may not work correctly.")
1142
+
1143
  if embedder is None:
1144
+
1145
  print("Warning: Sentence Transformer (embedder) not loaded. RAG will not be available.")
1146
+
1147
  if reranker is None:
1148
+
1149
  print("Warning: Cross-Encoder Reranker not loaded. Re-ranking of RAG results will not be performed.")
1150
+
1151
  if not business_info_available:
1152
+
1153
  print("Warning: Business information (Google Sheet data) not loaded successfully. "
1154
+
1155
  "RAG will not be available. Please ensure the GOOGLE_BASE64_CREDENTIALS secret is set correctly.")
1156
 
1157
+
1158
+
1159
  print("Launching Gradio Interface...")
1160
 
1161
+
1162
+
1163
  import gradio as gr
1164
 
 
 
 
 
 
 
 
 
 
 
1165
 
1166
+
1167
+ with gr.Blocks(theme="soft") as demo:
1168
+
1169
  gr.Markdown(
1170
+
1171
  """
1172
+
1173
  # LLM with Tools (DuckDuckGo Search, Date Calculation, Business Info RAG)
1174
+
1175
  Ask me anything! I can perform web searches, calculate dates, and retrieve business information.
1176
+
1177
  """
1178
+
1179
  )
1180
 
1181
+
1182
+
1183
  with gr.Row():
 
 
 
1184
 
1185
+ with gr.Column(scale=3):
1186
+
1187
+ query = gr.Textbox(
1188
+
1189
+ label="Query",
1190
+
1191
+ placeholder="Enter your query here....",
1192
+
1193
+ lines=3,
1194
+
1195
+ interactive=True
1196
+
1197
+ )
1198
+
1199
+ submit_btn = gr.Button("Submit")
1200
+
1201
+ clear_btn = gr.Button("Clear")
1202
+
1203
+
1204
+
1205
+ with gr.Column(scale=3):
1206
+
1207
+ output = gr.Textbox(
1208
+
1209
+ label="Output",
1210
+
1211
+ lines=8,
1212
+
1213
+ interactive=False
1214
+
1215
+ )
1216
+
1217
+
1218
+
1219
+ # Button actions
1220
+
1221
+ submit_btn.click(fn=chat, inputs=query, outputs=output)
1222
+
1223
+ clear_btn.click(fn=lambda: "", inputs=None, outputs=output)
1224
 
 
 
 
 
1225
 
1226
 
1227
  try:
1228
+
1229
  demo.launch(debug=True)
1230
+
1231
  except Exception as e:
1232
+
1233
  print(f"Error launching Gradio interface: {e}")
1234
+
1235
  print(traceback.format_exc())
 
1236
 
1237
+ print("Please check the console output for more details on the error.")