Nischal Subedi commited on
Commit
72aa01f
·
1 Parent(s): 9300bef
Files changed (1) hide show
  1. app.py +693 -394
app.py CHANGED
@@ -5,10 +5,7 @@ from functools import lru_cache
5
  import re
6
 
7
  import gradio as gr
8
- import gradio.themes as themes # Import gradio.themes
9
-
10
  try:
11
- # Assuming vector_db.py exists in the same directory or is installed
12
  from vector_db import VectorDatabase
13
  except ImportError:
14
  print("Error: Could not import VectorDatabase from vector_db.py.")
@@ -36,7 +33,7 @@ logging.basicConfig(
36
  format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
37
  )
38
 
39
- # --- RAGSystem Class (Processing Logic - kept intact as requested) ---
40
  class RAGSystem:
41
  def __init__(self, vector_db: Optional[VectorDatabase] = None):
42
  logging.info("Initializing RAGSystem")
@@ -60,7 +57,7 @@ Statutes from context:
60
  Context information:
61
  --- START CONTEXT ---
62
  {context}
63
- --- END CONCONTEXT ---
64
  Answer:"""
65
  self.prompt_template = PromptTemplate(
66
  input_variables=["query", "context", "state", "statutes"],
@@ -75,7 +72,7 @@ Answer:"""
75
  for statute in statutes:
76
  statute = statute.strip()
77
  if '§' in statute and any(char.isdigit() for char in statute):
78
- if not re.match(r'^\([\w\.]+\)$', statute) and 'http' not in statute:
79
  if len(statute) > 5:
80
  valid_statutes.append(statute)
81
 
@@ -83,7 +80,7 @@ Answer:"""
83
  seen = set()
84
  unique_statutes = [s for s in valid_statutes if not (s.rstrip('.,;') in seen or seen.add(s.rstrip('.,;')))]
85
  logging.info(f"Extracted {len(unique_statutes)} unique statutes.")
86
- return "\n".join(f"- {s}" for unique_statutes)
87
 
88
  logging.info("No statutes found matching the pattern in the context.")
89
  return "No specific statutes found in the provided context."
@@ -172,7 +169,7 @@ Answer:"""
172
 
173
  if not answer_text:
174
  logging.warning("LLM returned an empty answer.")
175
- answer_text = "<div class='error-message'><span class='error-icon'>⚠️</span>The AI model returned an empty response. This might be due to the query, context limitations, or temporary issues. Please try rephrasing your question or try again later.</div>"
176
  else:
177
  logging.info("LLM generated answer successfully.")
178
 
@@ -192,10 +189,10 @@ Answer:"""
192
  error_message = "Error: The request was too long for the AI model. This can happen with very complex questions or extensive retrieved context."
193
  details = "Try simplifying your question or asking about a more specific aspect."
194
  elif "timeout" in str(e).lower():
195
- error_message = "Error: The request to the AI model timed out. The service might be busy."
196
- details = "Please try again in a few moments."
197
 
198
- formatted_error = f"<div class='error-message'><span class='error-icon'>❌</span>{error_message}</div>"
199
  if details:
200
  formatted_error += f"<div class='error-details'>{details}</div>"
201
 
@@ -223,7 +220,6 @@ Answer:"""
223
  raise FileNotFoundError(f"PDF file not found: {pdf_path}")
224
  try:
225
  logging.info(f"Attempting to load/verify data from PDF: {pdf_path}")
226
- # Assuming process_and_load_pdf is part of VectorDatabase and correctly implemented
227
  num_states_processed = self.vector_db.process_and_load_pdf(pdf_path)
228
  doc_count = self.vector_db.document_collection.count()
229
  state_count = self.vector_db.state_collection.count()
@@ -241,12 +237,15 @@ Answer:"""
241
  logging.error(f"Failed to load or process PDF '{pdf_path}': {str(e)}", exc_info=True)
242
  raise RuntimeError(f"Failed to process PDF '{pdf_path}': {e}") from e
243
 
244
- # --- GRADIO INTERFACE (Refactored to use earneleh/paris theme) ---
245
  def gradio_interface(self):
 
246
  def query_interface_wrapper(api_key: str, query: str, state: str) -> str:
247
- # Basic client-side validation for immediate feedback (redundant but good UX)
 
 
248
  if not api_key or not api_key.strip() or not api_key.startswith("sk-"):
249
- return "<div class='error-message'><span class='error-icon'>⚠️</span>Please provide a valid OpenAI API key (starting with 'sk-'). <a href='https://platform.openai.com/api-keys' target='_blank'>Get one free from OpenAI</a>.</div>"
250
  if not state or state == "Select a state..." or "Error" in state:
251
  return "<div class='error-message'><span class='error-icon'>⚠️</span>Please select a valid state from the dropdown.</div>"
252
  if not query or not query.strip():
@@ -254,485 +253,785 @@ Answer:"""
254
 
255
  # Call the core processing logic
256
  result = self.process_query(query=query, state=state, openai_api_key=api_key)
257
- answer = result.get("answer", "<div class='error-message'><span class='error-icon'>⚠️</span>An unexpected error occurred.</div>")
258
 
259
- # Check if the answer already contains an error message (from deeper within process_query)
260
- if "<div class='error-message'>" in answer:
261
- return answer # Return the pre-formatted error message directly
 
 
 
 
 
 
 
 
 
 
262
  else:
263
- # Format the successful response with the new UI structure
264
- formatted_response = f"<div class='response-header'><span class='response-icon'>📜</span>Response for {state}</div><hr class='divider'>{answer}"
265
- return formatted_response
266
 
 
 
 
267
  try:
268
  available_states_list = self.get_states()
269
- dropdown_choices = ["Select a state..."] + (available_states_list if available_states_list and "Error" not in available_states_list[0] else ["Error: States unavailable"])
270
- initial_value = dropdown_choices[0]
271
- except Exception: # Catch-all for safety
 
 
 
 
 
 
272
  dropdown_choices = ["Error: Critical failure loading states"]
273
  initial_value = dropdown_choices[0]
274
 
275
- # Define example queries, filtering based on available states
276
  example_queries_base = [
277
  ["What are the rules for security deposit returns?", "California"],
278
  ["Can a landlord enter my apartment without notice?", "New York"],
279
  ["My landlord hasn't made necessary repairs. What can I do?", "Texas"],
280
- ["How much notice must a landlord give to raise rent?", "Florida"],
281
- ["What is an implied warranty of habitability?", "Illinois"]
 
282
  ]
283
  example_queries = []
284
- if available_states_list and "Error" not in available_states_list[0] and len(available_states_list) > 0:
285
  loaded_states_set = set(available_states_list)
286
- # Filter for examples whose state is in the loaded states
287
  example_queries = [ex for ex in example_queries_base if ex[1] in loaded_states_set]
288
- # Add a generic example if no specific state examples match or if list is empty
289
- if not example_queries:
290
- example_queries.append(["What basic rights do tenants have?", available_states_list[0] if available_states_list else "California"])
291
- else: # Fallback if states list is problematic
292
- example_queries.append(["What basic rights do tenants have?", "California"])
293
 
294
-
295
- # --- Minimal Custom CSS for structural/behavioral overrides not covered by the theme ---
296
- # This CSS focuses on animations, specific layout tweaks, and hiding unwanted Gradio default elements.
297
  custom_css = """
298
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:wght@700;800;900&display=swap');
299
- /* Animation for header */
300
- @keyframes fadeInSlideDown { from { opacity: 0; transform: translateY(-40px); } to { opacity: 1; transform: translateY(0); } }
301
- /* General layout adjustments to Gradio's base container */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  .gradio-container > .flex.flex-col {
303
- max-width: 1120px;
304
  margin: 0 auto !important;
305
- padding: 0 !important; /* Remove default Gradio padding */
306
- gap: 0 !important; /* Remove default Gradio gap */
 
 
 
 
 
 
 
 
 
 
 
307
  }
308
- /* Header specific styling */
309
- .app-header-wrapper {
310
- padding: 4.5rem 3.5rem !important;
 
 
 
 
 
 
311
  text-align: center !important;
312
- border-bottom-left-radius: 24px;
313
- border-bottom-right-radius: 24px;
314
  position: relative;
315
  overflow: hidden;
316
- z-index: 10;
317
- margin-bottom: 2rem; /* Reduced margin */
318
- border: 3px solid var(--border-color-primary) !important; /* Thicker border */
319
- border-top: none;
320
- max-width: 1120px;
321
- margin-left: auto;
322
- margin-right: auto;
323
- width: 100%;
324
- /* Enhanced background */
325
- background-image: linear-gradient(135deg, var(--background-fill-primary) 0%, var(--background-fill-secondary) 100%);
326
- box-shadow: 0 10px 30px rgba(0,0,0,0.4); /* More pronounced shadow */
327
- }
328
- .app-header { display: flex; flex-direction: column; align-items: center; position: relative; z-index: 1; }
329
- .app-header-logo {
330
- font-size: 5.5rem; margin-bottom: 0.8rem; line-height: 1;
331
- filter: drop-shadow(0 0 15px var(--primary-500));
332
- transform: translateY(-40px); opacity: 0;
333
- animation: fadeInSlideDown 1.5s ease-out forwards; animation-delay: 0.3s;
334
- }
335
- .app-header-title {
336
- font-family: 'Playfair Display', serif !important;
337
- font-size: 4.2rem; font-weight: 900;
338
- margin: 0 0 0.8rem 0; letter-spacing: -0.07em;
339
- text-shadow: 0 8px 16px rgba(0,0,0,0.5);
340
- transform: translateY(-40px); opacity: 0;
341
- animation: fadeInSlideDown 1.5s ease-out forwards; animation-delay: 0.6s;
342
- }
343
- .app-header-tagline {
344
- font-family: 'Inter', sans-serif !important;
345
- font-size: 1.6rem; font-weight: 300;
346
- opacity: 0.9; max-width: 900px;
347
- transform: translateY(-40px); opacity: 0;
348
- animation: fadeInSlideDown 1.5s ease-out forwards; animation-delay: 0.9s;
349
- }
350
- /* Main dashboard container */
351
- .main-dashboard-container {
352
- border-radius: 24px;
353
- border: 3px solid var(--border-color-primary) !important; /* Thicker border */
354
- padding: 3.5rem !important;
355
- margin: 0 auto 0.6rem auto; /* Reduced margin-bottom */
356
- z-index: 1; position: relative;
357
- display: flex; flex-direction: column; gap: 2rem; /* Reduced gap between boxes */
358
- max-width: 1120px;
359
- }
360
- /* Card sections within the dashboard */
361
- .dashboard-card-section {
362
- border-radius: 12px;
363
- border: 3px solid var(--border-color-primary); /* Thicker border */
364
- padding: 2.5rem;
365
- box-shadow: inset 0 0 10px rgba(0,0,0,0.2);
366
- display: flex; flex-direction: column; gap: 1.5rem;
367
- }
368
- /* Light mode specific inner shadow for card sections */
369
- @media (prefers-color-scheme: light) {
370
- .dashboard-card-section { box-shadow: inset 0 0 8px rgba(0,0,0,0.05); }
371
- }
372
- /* Section titles */
373
- .sub-section-title {
374
- font-family: 'Playfair Display', serif !important;
375
- font-size: 3.0rem !important; /* Increased font size */
376
- font-weight: 800 !important;
377
- text-align: center !important; /* Already centered, kept */
378
- margin-top: 1.5rem !important;
379
- margin-bottom: 0.8rem !important;
380
- display: block !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  width: 100% !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  }
383
- /* Text styling for custom markdown within cards */
384
- .dashboard-card-section p, .output-content-wrapper p {
385
- font-size: 1.15rem; line-height: 1.8;
386
- margin-bottom: 1.2rem;
387
- }
388
- .dashboard-card-section a, .output-content-wrapper a {
389
- text-decoration: none; font-weight: 500;
390
- }
391
- .dashboard-card-section a:hover, .output-content-wrapper a:hover { text-decoration: underline; }
392
- .dashboard-card-section strong, .output-content-wrapper strong { font-weight: 700; }
393
- /* Input layout */
394
- .input-field-group { margin-bottom: 1rem; }
395
- .input-row { display: flex; gap: 1.8rem; flex-wrap: wrap; margin-bottom: 1rem; }
396
- .input-field { flex: 1; }
397
- /* Button layout */
398
- .button-row { display: flex; gap: 2rem; margin-top: 2rem; flex-wrap: wrap; justify-content: flex-end; }
399
  .gradio-button {
400
- border-radius: 12px !important;
401
- padding: 1.2rem 2.8rem !important;
402
- font-size: 1.15rem !important;
403
- font-weight: 600 !important;
404
- border: 1px solid transparent !important;
405
- box-shadow: 0 6px 20px rgba(0,0,0,0.35);
406
- transition: all 0.4s cubic-bezier(0.0, 0.0, 0.2, 1);
 
 
 
 
 
 
 
 
407
  }
408
- .gradio-button:hover:not(:disabled) { transform: translateY(-6px); box-shadow: 0 12px 28px rgba(0,0,0,0.45) !important; }
409
- .gradio-button:active:not(:disabled) { transform: translateY(-3px); }
410
  .gradio-button:disabled {
411
- box-shadow: none !important;
 
412
  cursor: not-allowed;
 
413
  }
414
- .gr-button-secondary { /* Override for secondary button specific look */
415
- background: transparent !important;
416
- border: 2px solid var(--neutral-500) !important; /* Used neutral-500 for clearer border */
 
 
 
 
 
 
 
 
417
  box-shadow: none !important;
418
  }
419
  .gr-button-secondary:hover:not(:disabled) {
420
- background: var(--button-secondary-background-hover) !important;
421
- border-color: var(--primary-500) !important;
422
- }
423
- @media (prefers-color-scheme: light) {
424
- .gradio-button { box-shadow: 0 6px 20px rgba(0,0,0,0.1); }
425
- .gradio-button:hover:not(:disabled) { box-shadow: 0 12px 28px rgba(0,0,0,0.2) !important; }
426
  }
427
- /* Output card and placeholders */
428
- .output-card { padding: 0 !important; margin-top: 0 !important; margin-bottom: 0 !important; }
429
  .output-card .response-header {
430
- font-size: 1.8rem; font-weight: 700;
431
- margin: 0 0 1rem 0; display: flex; align-items: center; gap: 1.2rem;
432
- }
433
- .output-card .response-icon { font-size: 2rem; color: var(--primary-500); }
434
- .output-card .divider { border: none; border-top: 1px solid var(--border-color-primary); margin: 1.5rem 0 1.8rem 0; }
435
- .output-card .output-content-wrapper { font-size: 1.15rem; line-height: 1.8; }
436
- .output-card .output-content-wrapper p { margin-bottom: 1rem; }
437
- .output-card .output-content-wrapper ul, .output-card .output-content-wrapper ol { margin-left: 2.2rem; margin-bottom: 1.2rem; padding-left: 0; list-style-type: disc; }
438
- .output-card .output-content-wrapper ol { list-style-type: decimal; }
439
- .output-card .output-content-wrapper li { margin-bottom: 0.8rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  .output-card .error-message {
441
- padding: 1.5rem 2rem; margin-top: 1.5rem; font-size: 1.1rem;
442
- border-radius: 12px;
443
- background: var(--error-background-fill) !important;
444
- color: var(--error-text-color) !important;
445
- border: 2px solid var(--error-border-color) !important;
446
- display: flex; align-items: flex-start; gap: 1.5em;
447
- }
448
- .output-card .error-message .error-icon { font-size: 1.8rem; line-height: 1; padding-top: 0.1em; }
449
- .output-card .error-details { font-size: 0.95rem; margin-top: 0.8rem; opacity: 0.9; word-break: break-word; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  .output-card .placeholder {
451
- padding: 2.5rem 2rem; font-size: 1.2rem; border-radius: 12px;
452
- border: 3px dashed var(--border-color-primary);
453
- text-align: center; opacity: 0.8;
 
 
 
454
  }
455
- /* Examples table styling */
456
- .examples-section table.gr-samples-table {
457
- border-radius: 12px !important;
458
- border: 3px solid var(--border-color-primary) !important; /* Thicker border */
 
 
 
 
 
 
 
 
 
 
 
459
  overflow: hidden;
460
- box-shadow: inset 0 0 10px rgba(0,0,0,0.2);
461
  }
462
- @media (prefers-color-scheme: light) {
463
- .examples-section table.gr-samples-table { box-shadow: inset 0 0 8px rgba(0,0,0,0.05); }
 
 
 
 
 
 
464
  }
465
- .examples-section table.gr-samples-table th, .examples-section table.gr-samples-table td { padding: 1rem 1.2rem !important; font-size: 1.05rem !important; border: none !important; }
466
- .examples-section table.gr-samples-table th {
467
- background: var(--block-background-fill) !important;
468
- font-weight: 600 !important; text-align: left;
469
  }
470
- .examples-section table.gr-samples-table td {
471
- border-top: 1px solid var(--border-color-primary) !important;
472
  cursor: pointer;
 
473
  }
474
- .examples-section table.gr-samples-table tr:hover td { background: var(--hover-color) !important; }
475
- .examples-section table.gr-samples-table tr:first-child td { border-top: none !important; }
476
- /* Footer styling */
477
- .app-footer-wrapper {
478
- border-top: 3px solid var(--border-color-primary) !important; /* Thicker border */
479
- margin-top: 0.5rem;
480
- padding-top: 2.5rem;
481
- padding-bottom: 2.5rem;
482
- border-top-left-radius: 24px;
483
- border-top-right-radius: 24px;
484
- box-shadow: inset 0 8px 15px rgba(0,0,0,0.2);
485
- max-width: 1120px;
486
- margin-left: auto;
487
- margin-right: auto;
488
- width: 100%;
489
- display: flex !important;
490
- flex-direction: column !important;
491
- align-items: flex-start !important;
492
- }
493
- .app-footer {
494
- padding: 0 3.5rem !important;
495
- display: flex;
496
- flex-direction: column;
497
- align-items: stretch;
498
- width: 100%;
499
  }
500
- .app-footer p {
501
- font-size: 1.05rem !important;
502
- text-align: left !important;
503
- margin-bottom: 1rem;
504
- padding-left: 2rem; /* Added indent */
505
- }
506
- .app-footer a { font-weight: 500; }
507
- /* Hide default Gradio example labels and related elements for cleaner presentation */
508
- .gr-examples .gr-label, .gr-examples button.gr-button-filter, .gr-examples .label-wrap,
509
- .gr-examples div[data-testid*="label-text"], .gr-examples span[data-testid*="label-text"],
510
- .gr-examples div[class*="label"], .gr-examples .gr-example-label,
511
- .gr-examples .gr-box.gr-component.gradio-example > div:first-child:has(> span[data-testid]),
512
- .gr-examples .gr-box.gr-component.gradio-example > div:first-child > span,
513
- .gr-examples .gr-accordion-header, .gr-examples .gr-accordion-title, .gr-examples .gr-accordion-toggle-icon,
514
- .gr-examples .gr-accordion-header button, .gr-examples .gr-button.gr-button-filter,
515
- .gr-examples .gr-button.gr-button-primary.gr-button-filter,
516
- .gr-examples .gr-examples-header, .gr-examples .gr-examples-header > * {
517
- display: none !important; visibility: hidden !important; width: 0 !important; height: 0 !important;
518
- overflow: hidden !important; margin: 0 !important; padding: 0 !important; border: 0 !important;
519
- font-size: 0 !important; line-height: 0 !important; position: absolute !important;
520
- pointer-events: none !important;
521
- }
522
- /* Responsive Adjustments */
523
- @media (max-width: 1024px) {
524
- .gradio-container > .flex.flex-col { max-width: 960px; padding: 0 1.5rem !important; }
525
- .app-header-title { font-size: 3.8rem; } .app-header-tagline { font-size: 1.5rem; }
526
- .app-header-wrapper { padding: 2.5rem 3.5rem !important; margin-bottom: 1.8rem; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; }
527
- .main-dashboard-container { padding: 2.5rem !important; margin-bottom: 0.6rem; border-radius: 12px; gap: 1.8rem; } /* Adjusted gap */
528
- .dashboard-card-section { padding: 1.8rem; border-radius: 8px; }
529
- .sub-section-title { font-size: 2.5rem !important; margin-bottom: 0.7rem !important; } /* Adjusted font size */
530
- .input-row { gap: 1.5rem; } .input-field { min-width: 280px; }
531
- .gradio-textbox textarea { min-height: 160px; } .output-card .response-header { font-size: 1.7rem; }
532
- .examples-section { padding-top: 1.2rem; }
533
- .examples-section table.gr-samples-table th, .examples-section table.gr-samples-table td { padding: 0.9rem 1.1rem !important; }
534
- .app-footer-wrapper { margin-top: 0.6rem; border-top-left-radius: 12px; border-top-right-radius: 12px; }
535
- .app-footer { padding: 0 1.5rem !important; }
536
- .app-footer p { padding-left: 1.5rem; } /* Adjusted indent for smaller screens */
 
 
 
 
537
  }
 
 
 
538
  @media (max-width: 768px) {
539
- .gradio-container > .flex.flex-col { padding: 0 1rem !important; }
540
- .app-header-wrapper { padding: 1.8rem 2.5rem !important; margin-bottom: 1.5rem; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; }
541
- .app-header-logo { font-size: 4.5rem; margin-bottom: 0.6rem; } .app-header-title { font-size: 3.2rem; letter-spacing: -0.06em; }
542
- .app-header-tagline { font-size: 1.3rem; }
543
- .main-dashboard-container { padding: 1.8rem !important; margin-bottom: 0.5rem; border-radius: 12px; gap: 1.5rem; } /* Adjusted gap */
544
- .dashboard-card-section { padding: 1.5rem; border-radius: 8px; }
545
- .sub-section-title { font-size: 2.0rem !important; margin-top: 1rem !important; margin-bottom: 0.6rem !important; } /* Adjusted font size */
546
- .input-row { flex-direction: column; gap: 1rem; } .input-field { min-width: 100%; }
547
- .gradio-textbox textarea { min-height: 140px; } .button-row { justify-content: stretch; gap: 1rem; }
548
- .gradio-button { width: 100%; padding: 1.1rem 2rem !important; font-size: 1.1rem !important; }
549
- .output-card .response-header { font-size: 1.5rem; } .output-card .response-icon { font-size: 1.7rem; }
550
- .output-card .placeholder { padding: 2.5rem 1.5rem; font-size: 1.1rem; }
551
- .examples-section { padding-top: 1.2rem; }
552
- .examples-section table.gr-samples-table th, .examples-section table.gr-samples-table td { padding: 0.9rem 1.1rem !important; font-size: 1.0rem !important; }
553
- .app-footer-wrapper { margin-top: 0.6rem; border-top-left-radius: 12px; border-top-right-radius: 12px; padding-top: 2rem; padding-bottom: 2rem; }
554
- .app-footer { padding: 0 1rem !important; }
555
- .app-footer p { padding-left: 1rem; } /* Adjusted indent for smaller screens */
 
 
 
 
 
 
556
  }
557
  @media (max-width: 480px) {
558
- .gradio-container > .flex.flex-col { padding: 0 0.8rem !important; }
559
- .app-header-wrapper { padding: 1.2rem 1rem !important; margin-bottom: 1.2rem; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; }
560
- .app-header-logo { font-size: 3.8rem; margin-bottom: 0.5rem; } .app-header-title { font-size: 2.8rem; }
561
- .app-header-tagline { font-size: 1.1rem; }
562
- .main-dashboard-container { padding: 1.2rem !important; margin-bottom: 0.4rem; border-radius: 8px; gap: 1.2rem; } /* Adjusted gap */
563
- .dashboard-card-section { padding: 1rem; border-radius: 4px; }
564
- .sub-section-title { font-size: 1.8rem !important; margin-top: 0.8rem !important; margin-bottom: 0.5rem !important; } /* Adjusted font size */
565
- .gradio-textbox textarea, .gradio-dropdown select, .gradio-textbox input[type=password] { font-size: 1.05rem !important; padding: 1rem 1.2rem !important; }
566
- .gradio-textbox textarea { min-height: 120px; }
567
- .gradio-button { padding: 1rem 1.5rem !important; font-size: 1.0rem !important; }
568
- .output-card .response-header { font-size: 1.4rem; } .output-card .response-icon { font-size: 1.5rem; }
569
- .output-card .placeholder { padding: 2rem 1rem; font-size: 1.05rem; }
570
- .examples-section { padding-top: 0.8rem; }
571
- .examples-section table.gr-samples-table th, .examples-section table.gr-samples-table td { padding: 0.6rem 0.8rem !important; font-size: 0.95rem !important; }
572
- .app-footer-wrapper { margin-top: 0.4rem; border-top-left-radius: 8px; border-top-right-radius: 8px; padding-top: 1.5rem; padding-bottom: 1.5rem; }
573
- .app-footer { padding: 0 0.8rem !important; }
574
- .app-footer p { padding-left: 0.5rem; } /* Adjusted indent for smallest screens */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
  }
576
  """
577
 
578
- with gr.Blocks(theme="earneleh/paris", css=custom_css, title="Landlord-Tenant Rights Assistant") as demo:
579
- # --- Header Section ---
580
- with gr.Group(elem_classes="app-header-wrapper"):
 
581
  gr.Markdown(
582
  """
583
- <div class="app-header">
584
- <span class="app-header-logo">⚖️</span>
585
- <h1 class="app-header-title">Landlord-Tenant Rights Assistant</h1>
586
- <p class="app-header-tagline">Empowering You with State-Specific Legal Insights</p>
587
- </div>
 
 
 
 
588
  """
 
 
 
 
 
589
  )
590
 
591
- # --- Main Dashboard Console Container ---
592
- with gr.Column(elem_classes="main-dashboard-container"):
 
593
 
594
- # --- Section 1: Introduction and Disclaimer Card ---
595
- with gr.Group(elem_classes="dashboard-card-section"):
596
- gr.Markdown("<h3 class='sub-section-title'>Welcome & Disclaimer</h3>")
597
- gr.Markdown(
598
- """
599
- <p>Navigate landlord-tenant laws with ease. This assistant provides detailed, state-specific answers grounded in legal authority.</p>
600
- <p><strong>Disclaimer:</strong> This tool is for informational purposes only and does not constitute legal advice. For specific legal guidance, always consult a licensed attorney in your jurisdiction.</p>
601
- """
602
  )
603
 
604
- # --- Section 2: OpenAI API Key Input Card ---
605
- with gr.Group(elem_classes="dashboard-card-section"):
606
- gr.Markdown("<h3 class='sub-section-title'>OpenAI API Key</h3>")
607
- api_key_input = gr.Textbox(
608
- label="OpenAI API Key", # Keep label for theme compatibility
609
- type="password", placeholder="Enter your API key (e.g., sk-...)",
610
- info="Required to process your query. Securely used per request, not stored. <a href='https://platform.openai.com/api-keys' target='_blank'>Get one free from OpenAI</a>.", lines=1,
611
- elem_classes=["input-field-group"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  )
613
 
614
- # --- Section 3: Query Input and State Selection Card ---
615
- with gr.Group(elem_classes="dashboard-card-section"):
616
- gr.Markdown("<h3 class='sub-section-title'>Ask Your Question</h3>")
617
- with gr.Row(elem_classes="input-row"):
618
- with gr.Column(elem_classes="input-field", scale=3):
619
- query_input = gr.Textbox(
620
- label="Question", placeholder="E.g., What are the rules for security deposit returns in my state?",
621
- lines=5, max_lines=10,
622
- elem_classes=["input-field-group"]
623
- )
624
- with gr.Column(elem_classes="input-field", scale=1):
625
- state_input = gr.Dropdown(
626
- label="Select State", choices=dropdown_choices, value=initial_value,
627
- allow_custom_value=False,
628
- elem_classes=["input-field-group"]
629
- )
630
- with gr.Row(elem_classes="button-row"):
631
- clear_button = gr.Button("Clear", variant="secondary", elem_classes=["gr-button-secondary"])
632
- submit_button = gr.Button("Submit Query", variant="primary", elem_classes=["gr-button-primary"])
633
-
634
- # --- Section 4: Output Display Card ---
635
- with gr.Group(elem_classes="dashboard-card-section"):
636
- gr.Markdown("<h3 class='sub-section-title'>Legal Assistant's Response</h3>")
637
  output = gr.Markdown(
638
- value="<div class='placeholder output-card'>The answer will appear here after submitting your query.</div>",
639
- elem_classes="output-content-wrapper output-card"
 
640
  )
641
 
642
- # --- Section 5: Example Questions Section ---
643
- with gr.Group(elem_classes="dashboard-card-section examples-section"):
644
- gr.Markdown("<h3 class='sub-section-title'>Example Questions to Ask</h3>")
645
- if example_queries:
646
- gr.Examples(
647
- examples=example_queries, inputs=[query_input, state_input],
648
- examples_per_page=5,
649
- label="" # Hide default Examples label
650
- )
651
- else:
652
- gr.Markdown("<div class='placeholder'>Sample questions could not be loaded.</div>")
 
 
 
653
 
654
- # --- Footer Section ---
655
- with gr.Group(elem_classes="app-footer-wrapper"):
656
  gr.Markdown(
657
  """
658
- <div class="app-footer">
659
- <p>This tool is for informational purposes only and does not constitute legal advice. For legal guidance, always consult with a licensed attorney in your jurisdiction.</p>
660
- <p>Developed by <strong>Nischal Subedi</strong>.
661
- Connect on <a href="https://www.linkedin.com/in/nischal1/" target='_blank'>LinkedIn</a>
662
- or explore insights at <a href="https://datascientistinsights.substack.com/" target='_blank'>Substack</a>.</p>
663
- </div>
664
- """
665
  )
666
 
667
- # --- Event Listeners (Logic remains identical) ---
668
  submit_button.click(
669
- fn=query_interface_wrapper, inputs=[api_key_input, query_input, state_input], outputs=output, api_name="submit_query"
 
 
 
670
  )
 
671
  clear_button.click(
672
  fn=lambda: (
673
- "",
674
- "",
675
- initial_value,
676
- "<div class='placeholder output-card'>Inputs cleared. Ready for your next question.</div>"
677
  ),
678
- inputs=[], outputs=[api_key_input, query_input, state_input, output]
 
679
  )
680
- logging.info("Gradio interface created with earneleh/paris theme and refined custom CSS.")
681
- return demo
682
 
683
- # --- Main Execution Block (remains untouched from original logic, just fixed the exit) ---
 
 
 
684
  if __name__ == "__main__":
685
  logging.info("Starting Landlord-Tenant Rights Bot application...")
686
  try:
687
  SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
688
- DEFAULT_PDF_PATH = os.path.join(SCRIPT_DIR, "tenant-landlord.pdf")
689
- DEFAULT_DB_PATH = os.path.join(SCRIPT_DIR, "chroma_db")
690
 
691
  PDF_PATH = os.getenv("PDF_PATH", DEFAULT_PDF_PATH)
692
  VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", DEFAULT_DB_PATH)
693
 
694
  os.makedirs(os.path.dirname(VECTOR_DB_PATH), exist_ok=True)
 
 
 
 
695
 
696
- logging.info(f"Attempting to load PDF from: {PDF_PATH}")
697
  if not os.path.exists(PDF_PATH):
698
  logging.error(f"FATAL: PDF file not found at the specified path: {PDF_PATH}")
699
- print(f"\n--- CONFIGURATION ERROR ---\nPDF file ('{os.path.basename(PDF_PATH)}') not found at: {PDF_PATH}.\nPlease ensure it exists or set 'PDF_PATH' environment variable.\n---------------------------\n")
700
- exit(1) # This exit is correct: if file not found
701
-
702
- # This `if` block checks for readability *only if the file exists*.
703
- # The erroneous `exit(1)` that was likely at line 701 has been removed.
704
- if not os.access(PDF_PATH, os.R_OK):
705
- logging.error(f"FATAL: PDF file at '{PDF_PATH}' exists but is not readable. Check file permissions.")
706
- print(f"\n--- PERMISSION ERROR ---\nPDF file ('{os.path.basename(PDF_PATH)}') found but not readable at: {PDF_PATH}\nPlease check file permissions (e.g., using 'chmod +r' in terminal).\n---------------------------\n")
707
- exit(1) # This exit is correct: if file exists but is not readable
708
-
709
- logging.info(f"PDF file '{os.path.basename(PDF_PATH)}' found and is readable.")
710
 
 
711
  vector_db_instance = VectorDatabase(persist_directory=VECTOR_DB_PATH)
 
712
  rag = RAGSystem(vector_db=vector_db_instance)
713
 
714
- rag.load_pdf(PDF_PATH)
 
 
 
 
 
 
 
 
 
 
715
 
 
716
  app_interface = rag.gradio_interface()
717
- SERVER_PORT = int(os.getenv("PORT", 7860)) # Use PORT env var if on Spaces/Cloud, else 7860
718
 
 
719
  logging.info(f"Launching Gradio app on http://0.0.0.0:{SERVER_PORT}")
720
- print(f"\n--- Gradio App Running ---\nAccess at: http://localhost:{SERVER_PORT} or your public Spaces URL\n--------------------------\n")
721
- app_interface.launch(server_name="0.0.0.0", server_port=SERVER_PORT, share=False) # share=False is typical for Spaces
 
 
 
 
 
 
722
 
723
- except ModuleNotFoundError as e:
724
- if "vector_db" in str(e):
725
- logging.error(f"FATAL: Could not import VectorDatabase. Ensure 'vector_db.py' is in the same directory and 'chromadb', 'langchain', 'pypdf', 'sentence-transformers' are installed.", exc_info=True)
726
- print(f"\n--- MISSING DEPENDENCY OR FILE ---\nCould not find/import 'vector_db.py' or one of its dependencies.\nError: {e}\nPlease ensure 'vector_db.py' is present and all required packages (chromadb, langchain, pypdf, sentence-transformers, etc.) are in your requirements.txt and installed.\n---------------------------\n")
727
- else:
728
- logging.error(f"Application startup failed due to a missing module: {str(e)}", exc_info=True)
729
- print(f"\n--- FATAL STARTUP ERROR - MISSING MODULE ---\n{str(e)}\nPlease ensure all dependencies are installed.\nCheck logs for more details.\n---------------------------\n")
 
 
 
 
 
730
  exit(1)
731
- except FileNotFoundError as e:
732
- logging.error(f"Application startup failed due to a missing file: {str(e)}", exc_info=True)
733
- print(f"\n--- FATAL STARTUP ERROR - FILE NOT FOUND ---\n{str(e)}\nPlease ensure the file exists at the specified path.\nCheck logs for more details.\n---------------------------\n")
 
 
 
734
  exit(1)
735
  except Exception as e:
736
- logging.error(f"Application startup failed: {str(e)}", exc_info=True)
737
- print(f"\n--- FATAL STARTUP ERROR ---\n{str(e)}\nCheck logs for more details.\n---------------------------\n")
 
 
 
738
  exit(1)
 
5
  import re
6
 
7
  import gradio as gr
 
 
8
  try:
 
9
  from vector_db import VectorDatabase
10
  except ImportError:
11
  print("Error: Could not import VectorDatabase from vector_db.py.")
 
33
  format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
34
  )
35
 
36
+ # --- RAGSystem Class ---
37
  class RAGSystem:
38
  def __init__(self, vector_db: Optional[VectorDatabase] = None):
39
  logging.info("Initializing RAGSystem")
 
57
  Context information:
58
  --- START CONTEXT ---
59
  {context}
60
+ --- END CONTEXT ---
61
  Answer:"""
62
  self.prompt_template = PromptTemplate(
63
  input_variables=["query", "context", "state", "statutes"],
 
72
  for statute in statutes:
73
  statute = statute.strip()
74
  if '§' in statute and any(char.isdigit() for char in statute):
75
+ if not re.match(r'^\([\w\.]+\)$', statute) and 'http' not in statute:
76
  if len(statute) > 5:
77
  valid_statutes.append(statute)
78
 
 
80
  seen = set()
81
  unique_statutes = [s for s in valid_statutes if not (s.rstrip('.,;') in seen or seen.add(s.rstrip('.,;')))]
82
  logging.info(f"Extracted {len(unique_statutes)} unique statutes.")
83
+ return "\n".join(f"- {s}" for s in unique_statutes)
84
 
85
  logging.info("No statutes found matching the pattern in the context.")
86
  return "No specific statutes found in the provided context."
 
169
 
170
  if not answer_text:
171
  logging.warning("LLM returned an empty answer.")
172
+ answer_text = "<div class='error-message'>The AI model returned an empty response. This might be due to the query, context limitations, or temporary issues. Please try rephrasing your question or try again later.</div>"
173
  else:
174
  logging.info("LLM generated answer successfully.")
175
 
 
189
  error_message = "Error: The request was too long for the AI model. This can happen with very complex questions or extensive retrieved context."
190
  details = "Try simplifying your question or asking about a more specific aspect."
191
  elif "timeout" in str(e).lower():
192
+ error_message = "Error: The request to the AI model timed out. The service might be busy."
193
+ details = "Please try again in a few moments."
194
 
195
+ formatted_error = f"<div class='error-message'>{error_message}</div>"
196
  if details:
197
  formatted_error += f"<div class='error-details'>{details}</div>"
198
 
 
220
  raise FileNotFoundError(f"PDF file not found: {pdf_path}")
221
  try:
222
  logging.info(f"Attempting to load/verify data from PDF: {pdf_path}")
 
223
  num_states_processed = self.vector_db.process_and_load_pdf(pdf_path)
224
  doc_count = self.vector_db.document_collection.count()
225
  state_count = self.vector_db.state_collection.count()
 
237
  logging.error(f"Failed to load or process PDF '{pdf_path}': {str(e)}", exc_info=True)
238
  raise RuntimeError(f"Failed to process PDF '{pdf_path}': {e}") from e
239
 
240
+ # --- GRADIO INTERFACE ---
241
  def gradio_interface(self):
242
+ # Wrapper function for the Gradio interface logic
243
  def query_interface_wrapper(api_key: str, query: str, state: str) -> str:
244
+ logging.info(f"Gradio interface received query: '{query[:50]}...', state: '{state}'")
245
+
246
+ # Re-validate inputs robustly
247
  if not api_key or not api_key.strip() or not api_key.startswith("sk-"):
248
+ return "<div class='error-message'><span class='error-icon'>⚠️</span>Please provide a valid OpenAI API key (starting with 'sk-'). <a href='https://platform.openai.com/api-keys' target='_blank'>Get one here</a>.</div>"
249
  if not state or state == "Select a state..." or "Error" in state:
250
  return "<div class='error-message'><span class='error-icon'>⚠️</span>Please select a valid state from the dropdown.</div>"
251
  if not query or not query.strip():
 
253
 
254
  # Call the core processing logic
255
  result = self.process_query(query=query, state=state, openai_api_key=api_key)
 
256
 
257
+ # Format the response for display
258
+ answer = result.get("answer", "<div class='error-message'><span class='error-icon'>⚠️</span>An unexpected error occurred, and no answer was generated. Please check the logs or try again.</div>")
259
+
260
+ # Add a header *only* if the answer is not an error message itself
261
+ if not "<div class='error-message'>" in answer:
262
+ formatted_response = f"<div class='response-header'><span class='response-icon'>📜</span>Response for {state}</div><hr class='divider'>{answer}"
263
+ else:
264
+ formatted_response = answer # Pass through error messages directly
265
+
266
+ # Log context length for debugging (optional)
267
+ context_used = result.get("context_used", "N/A")
268
+ if isinstance(context_used, str) and "N/A" not in context_used:
269
+ logging.debug(f"Context length used for query: {len(context_used)} characters.")
270
  else:
271
+ logging.debug(f"No context was used or available for this query ({context_used}).")
 
 
272
 
273
+ return formatted_response
274
+
275
+ # --- Get Available States for Dropdown ---
276
  try:
277
  available_states_list = self.get_states()
278
+ if not available_states_list or "Error" in available_states_list[0]:
279
+ dropdown_choices = ["Error: Could not load states"]
280
+ initial_value = dropdown_choices[0]
281
+ logging.error("Could not load states for dropdown. UI will show error.")
282
+ else:
283
+ dropdown_choices = ["Select a state..."] + available_states_list
284
+ initial_value = dropdown_choices[0]
285
+ except Exception as e:
286
+ logging.error(f"Unexpected critical error getting states: {e}", exc_info=True)
287
  dropdown_choices = ["Error: Critical failure loading states"]
288
  initial_value = dropdown_choices[0]
289
 
290
+ # --- Prepare Example Queries ---
291
  example_queries_base = [
292
  ["What are the rules for security deposit returns?", "California"],
293
  ["Can a landlord enter my apartment without notice?", "New York"],
294
  ["My landlord hasn't made necessary repairs. What can I do?", "Texas"],
295
+ ["What are the limits on rent increases in my state?", "Florida"],
296
+ ["Is my lease automatically renewed if I don't move out?", "Illinois"],
297
+ ["What happens if I break my lease early?", "Washington"]
298
  ]
299
  example_queries = []
300
+ if available_states_list and "Error" not in available_states_list[0]:
301
  loaded_states_set = set(available_states_list)
 
302
  example_queries = [ex for ex in example_queries_base if ex[1] in loaded_states_set]
303
+ if not example_queries:
304
+ fallback_state = available_states_list[0] if available_states_list and "Error" not in available_states_list[0] else "California"
305
+ example_queries.append(["What basic rights do tenants have?", fallback_state])
 
 
306
 
307
+ # --- Refined Custom CSS ---
 
 
308
  custom_css = """
309
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
310
+
311
+ /* --- CSS Variables for Theme Consistency --- */
312
+ :root {
313
+ --primary-color: #2563EB; /* Tailwind Blue 600 */
314
+ --primary-hover: #1D4ED8; /* Tailwind Blue 700 */
315
+ --secondary-color: #4B5563; /* Gray 600 */
316
+ --secondary-hover: #374151; /* Gray 700 */
317
+ --text-primary: #111827; /* Gray 900 */
318
+ --text-secondary: #6B7280; /* Gray 500 */
319
+ --background: #F3F4F6; /* Gray 100 */
320
+ --card-background: #FFFFFF; /* White */
321
+ --border-color: #D1D5DB; /* Gray 300 */
322
+ --shadow: 0 2px 4px rgba(0, 0, 0, 0.04), 0 4px 8px rgba(0, 0, 0, 0.06); /* Softer, layered shadow */
323
+ --error-bg: #FEF2F2; /* Red 50 */
324
+ --error-border: #FECACA; /* Red 300 */
325
+ --error-accent: #EF4444; /* Red 500 */
326
+ --error-text: #B91C1C; /* Red 700 */
327
+ --success-bg: #F0FDF4; /* Green 50 */
328
+ --success-border: #A7F3D0; /* Green 300 */
329
+ --success-text: #15803D; /* Green 700 */
330
+ --divider: #E5E7EB; /* Gray 200 */
331
+ --focus-ring: rgba(37, 99, 235, 0.3); /* Based on new primary */
332
+ }
333
+
334
+ /* Dark Mode Variables */
335
+ @media (prefers-color-scheme: dark) {
336
+ :root {
337
+ --primary-color: #3B82F6; /* Tailwind Blue 500 (lighter for dark mode primary) */
338
+ --primary-hover: #60A5FA; /* Tailwind Blue 400 */
339
+ --text-primary: #F3F4F6; /* Gray 100 */
340
+ --text-secondary: #9CA3AF; /* Gray 400 */
341
+ --background: #111827; /* Gray 900 (very dark) */
342
+ --card-background: #1F2937; /* Gray 800 (main content card bg) */
343
+ --border-color: #4B5563; /* Gray 600 */
344
+ --shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.2);
345
+ --error-bg: #450A0A; /* Darker Red */
346
+ --error-border: #7F1D1D; /* Darker Red */
347
+ --error-accent: #F87171; /* Lighter Red for accent */
348
+ --error-text: #FECACA; /* Lighter Red for text */
349
+ --success-bg: #064E3B; /* Darker Green */
350
+ --success-border: #15803D; /* Darker Green */
351
+ --success-text: #A7F3D0; /* Lighter Green */
352
+ --divider: #374151; /* Gray 700 */
353
+ --focus-ring: rgba(59, 130, 246, 0.4);
354
+ }
355
+ }
356
+
357
+ /* --- Base & Body --- */
358
+ body, .gradio-container {
359
+ font-family: 'Poppins', -apple-system, BlinkMacSystemFont, sans-serif !important;
360
+ background: var(--background) !important;
361
+ color: var(--text-primary) !important;
362
+ margin: 0;
363
+ padding: 0;
364
+ min-height: 100vh;
365
+ font-size: 15px; /* Slightly smaller base font */
366
+ line-height: 1.6; /* Increased line height for readability */
367
+ -webkit-font-smoothing: antialiased;
368
+ -moz-osx-font-smoothing: grayscale;
369
+ }
370
+ * {
371
+ box-sizing: border-box;
372
+ }
373
+
374
+ /* --- Main Content Container --- */
375
  .gradio-container > .flex.flex-col {
376
+ max-width: 960px; /* Slightly narrower for focus */
377
  margin: 0 auto !important;
378
+ padding: 2.5rem 1.5rem !important; /* Adjusted padding */
379
+ gap: 2rem !important; /* Adjusted gap */
380
+ background: transparent !important;
381
+ }
382
+
383
+ /* --- Card Styling --- */
384
+ .card-style {
385
+ background: var(--card-background) !important;
386
+ border: 1px solid var(--border-color) !important;
387
+ border-radius: 12px !important; /* More modern radius */
388
+ padding: 1.75rem !important; /* Adjusted padding */
389
+ box-shadow: var(--shadow) !important;
390
+ transition: transform 0.2s ease, background 0.2s ease, border 0.2s ease;
391
  }
392
+ .card-style:hover {
393
+ transform: translateY(-2px);
394
+ }
395
+
396
+ /* --- Header Section --- */
397
+ .header-section {
398
+ background: var(--primary-color) !important; /* Solid primary color */
399
+ border-radius: 12px !important;
400
+ padding: 2.5rem 2rem !important;
401
  text-align: center !important;
402
+ color: #FFFFFF !important;
403
+ box-shadow: var(--shadow) !important;
404
  position: relative;
405
  overflow: hidden;
406
+ }
407
+ .header-section::before { /* Subtle background pattern */
408
+ content: '';
409
+ position: absolute;
410
+ top: 0; left: 0; width: 100%; height: 100%;
411
+ background-image: radial-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1.2px);
412
+ background-size: 8px 8px;
413
+ opacity: 0.5;
414
+ pointer-events: none;
415
+ }
416
+ .header-logo {
417
+ font-size: 2.5rem; /* Adjusted size */
418
+ margin-bottom: 0.75rem;
419
+ }
420
+ .header-title {
421
+ font-size: 2rem; /* Adjusted size */
422
+ font-weight: 600; /* Adjusted weight */
423
+ margin: 0 0 0.5rem 0;
424
+ }
425
+ .header-tagline {
426
+ font-size: 1.1rem; /* Adjusted size */
427
+ font-weight: 400; /* Adjusted weight */
428
+ opacity: 0.85;
429
+ }
430
+
431
+ /* --- Introduction Section --- */
432
+ .intro-card h3 { /* Title like "Know Your Rights" */
433
+ font-size: 1.5rem; /* Adjusted relative to new base */
434
+ font-weight: 600;
435
+ color: var(--primary-color);
436
+ margin: 0 0 1rem 0;
437
+ padding-bottom: 0.5rem;
438
+ border-bottom: 2px solid var(--primary-color);
439
+ display: inline-block;
440
+ }
441
+ .intro-card p {
442
+ font-size: 0.95rem; /* Adjusted relative to new base */
443
+ line-height: 1.7;
444
+ color: var(--text-secondary);
445
+ margin: 0 0 0.75rem 0;
446
+ }
447
+ .intro-card a {
448
+ color: var(--primary-color);
449
+ text-decoration: none;
450
+ font-weight: 500;
451
+ transition: color 0.2s ease;
452
+ }
453
+ .intro-card a:hover {
454
+ color: var(--primary-hover);
455
+ text-decoration: underline;
456
+ }
457
+ .intro-card strong {
458
+ font-weight: 600;
459
+ color: var(--text-primary);
460
+ }
461
+
462
+ /* --- Input Form Section --- */
463
+ .input-form-card h3 { /* Title like "Ask Your Question" */
464
+ font-size: 1.4rem; /* Adjusted */
465
+ font-weight: 600;
466
+ color: var(--text-primary);
467
+ margin: 0 0 1.25rem 0;
468
+ padding-bottom: 0.5rem;
469
+ border-bottom: 1px solid var(--divider); /* Thinner divider */
470
+ }
471
+ .input-field-group {
472
+ margin-bottom: 1.25rem;
473
+ }
474
+ .input-row {
475
+ display: flex;
476
+ gap: 1.25rem;
477
+ flex-wrap: wrap;
478
+ margin-bottom: 1.25rem;
479
+ }
480
+ .input-field {
481
+ flex: 1;
482
+ min-width: 200px; /* Adjusted min-width */
483
+ }
484
+ .gradio-textbox textarea,
485
+ .gradio-dropdown select,
486
+ .gradio-textbox input[type=password] {
487
+ border: 1px solid var(--border-color) !important;
488
+ border-radius: 8px !important; /* Sharper radius */
489
+ padding: 0.8rem 1rem !important; /* Adjusted padding */
490
+ font-size: 0.95rem !important; /* Adjusted font size */
491
+ background: var(--card-background) !important; /* Use card for consistency, can be var(--background) for contrast */
492
+ color: var(--text-primary) !important;
493
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
494
  width: 100% !important;
495
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.03);
496
+ }
497
+ .gradio-textbox textarea {
498
+ min-height: 110px; /* Adjusted height */
499
+ resize: vertical;
500
+ }
501
+ .gradio-textbox textarea:focus,
502
+ .gradio-dropdown select:focus,
503
+ .gradio-textbox input[type=password]:focus {
504
+ border-color: var(--primary-color) !important;
505
+ box-shadow: 0 0 0 3px var(--focus-ring) !important; /* Slightly smaller focus ring */
506
+ outline: none !important;
507
+ /* background: var(--background) !important; */ /* Keep card background on focus or change to main for contrast */
508
+ }
509
+ .gradio-input-label,
510
+ .gradio-output-label {
511
+ font-size: 0.9rem !important; /* Adjusted */
512
+ font-weight: 500 !important;
513
+ color: var(--text-primary) !important;
514
+ margin-bottom: 0.4rem !important;
515
+ display: block !important;
516
+ }
517
+ .gradio-input-info {
518
+ font-size: 0.8rem !important; /* Adjusted */
519
+ color: var(--text-secondary) !important;
520
+ margin-top: 0.3rem;
521
+ font-style: italic;
522
+ }
523
+ /* Buttons */
524
+ .button-row {
525
+ display: flex;
526
+ gap: 0.75rem; /* Tighter gap */
527
+ margin-top: 1.25rem;
528
+ flex-wrap: wrap;
529
+ justify-content: flex-end;
530
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  .gradio-button {
532
+ border-radius: 8px !important; /* Sharper radius */
533
+ padding: 0.75rem 1.5rem !important; /* Adjusted padding */
534
+ font-size: 0.95rem !important; /* Adjusted font size */
535
+ font-weight: 500 !important;
536
+ border: none !important;
537
+ cursor: pointer;
538
+ transition: background-color 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease;
539
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important; /* More subtle shadow */
540
+ }
541
+ .gradio-button:hover:not(:disabled) {
542
+ transform: translateY(-1px);
543
+ box-shadow: 0 2px 4px rgba(0,0,0,0.07) !important;
544
+ }
545
+ .gradio-button:active:not(:disabled) {
546
+ transform: scale(0.98) translateY(0);
547
  }
 
 
548
  .gradio-button:disabled {
549
+ background: var(--border-color) !important;
550
+ color: var(--text-secondary) !important;
551
  cursor: not-allowed;
552
+ box-shadow: none !important;
553
  }
554
+ .gr-button-primary {
555
+ background: var(--primary-color) !important;
556
+ color: #FFFFFF !important;
557
+ }
558
+ .gr-button-primary:hover:not(:disabled) {
559
+ background: var(--primary-hover) !important;
560
+ }
561
+ .gr-button-secondary {
562
+ background: var(--card-background) !important; /* Changed from transparent */
563
+ color: var(--text-primary) !important;
564
+ border: 1px solid var(--border-color) !important;
565
  box-shadow: none !important;
566
  }
567
  .gr-button-secondary:hover:not(:disabled) {
568
+ background: var(--background) !important; /* Use main bg for hover */
569
+ border-color: var(--secondary-hover) !important;
 
 
 
 
570
  }
571
+
572
+ /* --- Output Section --- */
573
  .output-card .response-header {
574
+ font-size: 1.3rem; /* Adjusted */
575
+ font-weight: 600;
576
+ color: var(--text-primary);
577
+ margin: 0 0 0.75rem 0;
578
+ display: flex;
579
+ align-items: center;
580
+ gap: 0.5rem;
581
+ }
582
+ .output-card .response-icon {
583
+ font-size: 1.5rem; /* Adjusted */
584
+ }
585
+ .output-card .divider {
586
+ border: none;
587
+ border-top: 1px solid var(--divider);
588
+ margin: 0.75rem 0 1.25rem 0; /* Adjusted margins */
589
+ }
590
+ .output-card .output-content-wrapper {
591
+ font-size: 0.95rem; /* Adjusted */
592
+ line-height: 1.7; /* Adjusted */
593
+ color: var(--text-primary);
594
+ }
595
+ .output-card .output-content-wrapper p {
596
+ margin-bottom: 0.85rem;
597
+ }
598
+ .output-card .output-content-wrapper ul,
599
+ .output-card .output-content-wrapper ol {
600
+ margin-left: 1.25rem;
601
+ margin-bottom: 0.85rem;
602
+ padding-left: 0.85rem;
603
+ }
604
+ .output-card .output-content-wrapper li {
605
+ margin-bottom: 0.4rem;
606
+ }
607
+ .output-card .output-content-wrapper strong,
608
+ .output-card .output-content-wrapper b {
609
+ font-weight: 600;
610
+ color: var(--text-primary);
611
+ }
612
+ .output-card .output-content-wrapper a {
613
+ color: var(--primary-color);
614
+ text-decoration: none;
615
+ font-weight: 500;
616
+ }
617
+ .output-card .output-content-wrapper a:hover {
618
+ color: var(--primary-hover);
619
+ text-decoration: underline;
620
+ }
621
+ /* Error and Success Messages */
622
+ .output-card .error-message,
623
+ .output-card .success-message {
624
+ display: flex;
625
+ align-items: flex-start;
626
+ gap: 0.6rem;
627
+ border-radius: 8px; /* Match other radii */
628
+ padding: 0.85rem 1.25rem; /* Adjusted padding */
629
+ margin-top: 0.5rem;
630
+ font-weight: 500;
631
+ line-height: 1.5;
632
+ }
633
  .output-card .error-message {
634
+ background: var(--error-bg);
635
+ border: 1px solid var(--error-border);
636
+ border-left: 3px solid var(--error-accent); /* Thinner accent line */
637
+ color: var(--error-text);
638
+ }
639
+ .output-card .success-message {
640
+ background: var(--success-bg);
641
+ border: 1px solid var(--success-border);
642
+ color: var(--success-text);
643
+ border-left: 3px solid var(--success-text);
644
+ }
645
+ .output-card .error-icon,
646
+ .output-card .success-icon {
647
+ font-size: 1.1rem; /* Adjusted */
648
+ line-height: 1.5;
649
+ margin-top: 2px; /* Align icon better with text */
650
+ }
651
+ .output-card .error-details {
652
+ font-size: 0.85rem; /* Adjusted */
653
+ color: var(--error-text); /* Ensure correct text color for dark mode if needed */
654
+ margin-top: 0.4rem;
655
+ font-style: italic;
656
+ }
657
+ /* Placeholder text */
658
  .output-card .placeholder {
659
+ color: var(--text-secondary);
660
+ font-style: italic;
661
+ text-align: center;
662
+ padding: 1.5rem 1rem; /* Adjusted padding */
663
+ display: block;
664
+ font-size: 1rem; /* Adjusted */
665
  }
666
+
667
+ /* --- Examples Section --- */
668
+ .examples-card .gr-examples-header {
669
+ font-size: 1.3rem !important; /* Adjusted */
670
+ font-weight: 600 !important;
671
+ color: var(--text-primary) !important;
672
+ margin: 0 0 1.25rem 0 !important;
673
+ padding-bottom: 0.5rem !important;
674
+ border-bottom: 1px solid var(--divider) !important; /* Thinner divider */
675
+ }
676
+ .examples-card .gr-examples-table {
677
+ border-collapse: collapse !important;
678
+ width: 100% !important;
679
+ background: var(--card-background) !important;
680
+ border-radius: 8px !important; /* Match other radii */
681
  overflow: hidden;
682
+ border: 1px solid var(--border-color) !important; /* Add outer border to table */
683
  }
684
+ .examples-card .gr-examples-table th,
685
+ .examples-card .gr-examples-table td {
686
+ text-align: left !important;
687
+ padding: 0.75rem 1rem !important; /* Adjusted padding */
688
+ border: 1px solid var(--border-color) !important;
689
+ font-size: 0.9rem !important; /* Adjusted font size */
690
+ color: var(--text-primary) !important;
691
+ background: transparent !important;
692
  }
693
+ .examples-card .gr-examples-table th {
694
+ font-weight: 500 !important;
695
+ background: var(--background) !important; /* Use main bg for header */
 
696
  }
697
+ .examples-card .gr-examples-table tr {
 
698
  cursor: pointer;
699
+ transition: background 0.2s ease;
700
  }
701
+ .examples-card .gr-examples-table tr:hover td {
702
+ background: var(--background) !important; /* Use main bg for hover */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
703
  }
704
+
705
+ /* --- Footer Section --- */
706
+ .footer-section {
707
+ background: transparent !important;
708
+ border-top: 1px solid var(--divider) !important;
709
+ padding: 1.5rem 1rem !important; /* Adjusted padding */
710
+ margin-top: 1.5rem !important; /* Adjusted margin */
711
+ text-align: center !important;
712
+ color: var(--text-secondary) !important;
713
+ font-size: 0.85rem !important; /* Adjusted font size */
714
+ line-height: 1.5 !important;
715
+ }
716
+ .footer-section strong {
717
+ color: var(--text-primary);
718
+ font-weight: 500;
719
+ }
720
+ .footer-section a {
721
+ color: var(--primary-color);
722
+ text-decoration: none;
723
+ font-weight: 500;
724
+ }
725
+ .footer-section a:hover {
726
+ color: var(--primary-hover);
727
+ text-decoration: underline;
728
+ }
729
+
730
+ /* --- Accessibility & Focus --- */
731
+ :focus-visible { /* Standard focus visibility */
732
+ outline: 2px solid var(--primary-color) !important;
733
+ outline-offset: 2px;
734
+ }
735
+ /* Remove custom box-shadow focus for inputs/selects if :focus-visible is preferred */
736
+ .gradio-textbox textarea:focus,
737
+ .gradio-dropdown select:focus,
738
+ .gradio-textbox input[type=password]:focus {
739
+ border-color: var(--primary-color) !important;
740
+ box-shadow: 0 0 0 3px var(--focus-ring) !important; /* Keep this for consistent focus */
741
+ outline: none !important;
742
+ }
743
+ .gradio-button span:focus { /* Remove Gradio's default focus on button text */
744
+ outline: none !important;
745
  }
746
+
747
+
748
+ /* --- Responsive Adjustments --- */
749
  @media (max-width: 768px) {
750
+ body { font-size: 14px; }
751
+ .gradio-container > .flex.flex-col {
752
+ padding: 2rem 1rem !important;
753
+ gap: 1.5rem !important;
754
+ }
755
+ .card-style {
756
+ padding: 1.5rem !important;
757
+ border-radius: 10px !important;
758
+ }
759
+ .header-section {
760
+ padding: 2rem 1.5rem !important;
761
+ border-radius: 10px !important;
762
+ }
763
+ .header-title { font-size: 1.8rem; }
764
+ .header-tagline { font-size: 1rem; }
765
+ .input-row {
766
+ flex-direction: column;
767
+ gap: 1rem;
768
+ }
769
+ .button-row { justify-content: center; }
770
+ .intro-card h3, .input-form-card h3, .output-card .response-header, .examples-card .gr-examples-header {
771
+ font-size: 1.2rem !important;
772
+ }
773
  }
774
  @media (max-width: 480px) {
775
+ body { font-size: 14px; } /* Keep 14px or adjust if too small */
776
+ .gradio-container > .flex.flex-col {
777
+ padding: 1.25rem 0.75rem !important;
778
+ gap: 1.25rem !important;
779
+ }
780
+ .card-style {
781
+ padding: 1rem !important;
782
+ border-radius: 8px !important;
783
+ }
784
+ .header-section {
785
+ padding: 1.5rem 1rem !important;
786
+ border-radius: 8px !important;
787
+ }
788
+ .header-logo { font-size: 2rem; }
789
+ .header-title { font-size: 1.5rem; }
790
+ .header-tagline { font-size: 0.9rem; }
791
+
792
+ .intro-card h3, .input-form-card h3, .output-card .response-header, .examples-card .gr-examples-header {
793
+ font-size: 1.1rem !important;
794
+ }
795
+ .gradio-textbox textarea,
796
+ .gradio-dropdown select,
797
+ .gradio-textbox input[type=password] {
798
+ font-size: 0.9rem !important;
799
+ padding: 0.7rem 0.9rem !important;
800
+ }
801
+ .gradio-button {
802
+ width: 100%;
803
+ padding: 0.7rem 1.25rem !important;
804
+ font-size: 0.9rem !important;
805
+ }
806
+ .button-row {
807
+ flex-direction: column;
808
+ gap: 0.5rem;
809
+ }
810
+ .examples-card .gr-examples-table th,
811
+ .examples-card .gr-examples-table td {
812
+ padding: 0.5rem 0.7rem !important;
813
+ font-size: 0.85rem !important;
814
+ }
815
+ }
816
+
817
+ /* --- Gradio Overrides --- */
818
+ .gradio-container > .flex {
819
+ gap: 2rem !important; /* Match main gap */
820
+ }
821
+ .gradio-markdown > *:first-child { margin-top: 0; }
822
+ .gradio-markdown > *:last-child { margin-bottom: 0; }
823
+ .gradio-dropdown,
824
+ .gradio-textbox { /* Remove Gradio default borders/padding around components */
825
+ border: none !important;
826
+ padding: 0 !important;
827
+ background: transparent !important;
828
  }
829
  """
830
 
831
+ # --- Gradio Blocks Layout ---
832
+ with gr.Blocks(theme=None, css=custom_css, title="Landlord-Tenant Rights Assistant") as demo:
833
+ # Header Section
834
+ with gr.Group(elem_classes="header-section"):
835
  gr.Markdown(
836
  """
837
+ <span class="header-logo">⚖️</span>
838
+ <h1 class="header-title">Landlord-Tenant Rights Assistant</h1>
839
+ <p class="header-tagline">Empowering You with State-Specific Legal Insights</p>
840
+ """, elem_id="app-title"
841
+ )
842
+
843
+ # Introduction Section
844
+ with gr.Group(elem_classes="card-style intro-card"):
845
+ gr.Markdown(
846
  """
847
+ <h3>Know Your Rights</h3>
848
+ <p>Navigate landlord-tenant laws with ease. Enter your <strong>OpenAI API key</strong>, select your state, and ask your question to get detailed, state-specific answers.</p>
849
+ <p>Don't have an API key? <a href='https://platform.openai.com/api-keys' target='_blank'>Get one free from OpenAI</a>.</p>
850
+ <p><strong>Disclaimer:</strong> This tool provides information only, not legal advice. For legal guidance, consult a licensed attorney.</p>
851
+ """, elem_id="app-description"
852
  )
853
 
854
+ # Input Form Section
855
+ with gr.Group(elem_classes="card-style input-form-card"):
856
+ gr.Markdown("<h3>Ask Your Question</h3>", elem_id="form-heading")
857
 
858
+ with gr.Column(elem_classes="input-field-group"):
859
+ api_key_input = gr.Textbox(
860
+ label="OpenAI API Key",
861
+ type="password",
862
+ placeholder="Enter your API key (e.g., sk-...)",
863
+ info="Required to process your query. Securely used per request, not stored.",
864
+ elem_id="api-key-input",
865
+ lines=1
866
  )
867
 
868
+ with gr.Row(elem_classes="input-row"):
869
+ with gr.Column(elem_classes="input-field"):
870
+ query_input = gr.Textbox(
871
+ label="Your Question",
872
+ placeholder="E.g., What are the rules for security deposit returns in my state?",
873
+ lines=4,
874
+ max_lines=8,
875
+ elem_id="query-input"
876
+ )
877
+ with gr.Column(elem_classes="input-field"):
878
+ state_input = gr.Dropdown(
879
+ label="Select State",
880
+ choices=dropdown_choices,
881
+ value=initial_value,
882
+ allow_custom_value=False,
883
+ elem_id="state-dropdown"
884
+ )
885
+
886
+ with gr.Row(elem_classes="button-row"):
887
+ clear_button = gr.Button(
888
+ "Clear",
889
+ variant="secondary",
890
+ elem_id="clear-button",
891
+ elem_classes=["gr-button-secondary"]
892
+ )
893
+ submit_button = gr.Button(
894
+ "Submit Query",
895
+ variant="primary",
896
+ elem_id="submit-button",
897
+ elem_classes=["gr-button-primary"]
898
  )
899
 
900
+ # Output Section
901
+ with gr.Group(elem_classes="card-style output-card"):
902
+ with gr.Column():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
903
  output = gr.Markdown(
904
+ value="<div class='placeholder'>Your answer will appear here after submitting your query.</div>",
905
+ elem_id="output-content",
906
+ elem_classes="output-content-wrapper"
907
  )
908
 
909
+ # Example Questions Section
910
+ if example_queries:
911
+ with gr.Group(elem_classes="card-style examples-card"):
912
+ gr.Examples(
913
+ examples=example_queries,
914
+ inputs=[query_input, state_input],
915
+ label="Explore Sample Questions",
916
+ examples_per_page=6
917
+ )
918
+ else:
919
+ with gr.Group(elem_classes="card-style examples-card"):
920
+ gr.Markdown(
921
+ "<div class='placeholder'>Sample questions could not be loaded. Please ensure states are available.</div>"
922
+ )
923
 
924
+ # Footer Section
925
+ with gr.Group(elem_classes="footer-section"):
926
  gr.Markdown(
927
  """
928
+ **Disclaimer**: This tool is for informational purposes only and does not constitute legal advice.
929
+ <br><br>
930
+ Developed by **Nischal Subedi**. Connect on <a href="https://www.linkedin.com/in/nischal1/" target="_blank">LinkedIn</a> or explore insights at <a href="https://datascientistinsights.substack.com/" target="_blank">Substack</a>.
931
+ """, elem_id="app-footer"
 
 
 
932
  )
933
 
934
+ # --- Event Listeners ---
935
  submit_button.click(
936
+ fn=query_interface_wrapper,
937
+ inputs=[api_key_input, query_input, state_input],
938
+ outputs=output,
939
+ api_name="submit_query"
940
  )
941
+
942
  clear_button.click(
943
  fn=lambda: (
944
+ "", "", initial_value,
945
+ "<div class='placeholder'>Inputs cleared. Ready for your next question.</div>"
 
 
946
  ),
947
+ inputs=[],
948
+ outputs=[api_key_input, query_input, state_input, output]
949
  )
 
 
950
 
951
+ logging.info("Refined Gradio interface created successfully.")
952
+ return demo
953
+
954
+ # --- Main Execution Block ---
955
  if __name__ == "__main__":
956
  logging.info("Starting Landlord-Tenant Rights Bot application...")
957
  try:
958
  SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
959
+ DEFAULT_PDF_PATH = os.path.join(SCRIPT_DIR, "data/tenant-landlord.pdf")
960
+ DEFAULT_DB_PATH = os.path.join(SCRIPT_DIR, "data/chroma_db")
961
 
962
  PDF_PATH = os.getenv("PDF_PATH", DEFAULT_PDF_PATH)
963
  VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", DEFAULT_DB_PATH)
964
 
965
  os.makedirs(os.path.dirname(VECTOR_DB_PATH), exist_ok=True)
966
+ os.makedirs(os.path.dirname(PDF_PATH), exist_ok=True)
967
+
968
+ logging.info(f"Using PDF path: {PDF_PATH}")
969
+ logging.info(f"Using Vector DB path: {VECTOR_DB_PATH}")
970
 
 
971
  if not os.path.exists(PDF_PATH):
972
  logging.error(f"FATAL: PDF file not found at the specified path: {PDF_PATH}")
973
+ print(f"\n--- CONFIGURATION ERROR ---")
974
+ print(f"The required PDF file ('{os.path.basename(PDF_PATH)}') was not found at:")
975
+ print(f" {PDF_PATH}")
976
+ print(f"Please ensure the file exists or set 'PDF_PATH' environment variable.")
977
+ print(f"---------------------------\n")
978
+ exit(1)
 
 
 
 
 
979
 
980
+ logging.info("Initializing Vector Database...")
981
  vector_db_instance = VectorDatabase(persist_directory=VECTOR_DB_PATH)
982
+ logging.info("Initializing RAG System...")
983
  rag = RAGSystem(vector_db=vector_db_instance)
984
 
985
+ logging.info(f"Loading/Verifying data from PDF: {PDF_PATH}")
986
+ states_loaded_count = rag.load_pdf(PDF_PATH)
987
+ doc_count = vector_db_instance.document_collection.count() if vector_db_instance.document_collection else 0
988
+ state_count = vector_db_instance.state_collection.count() if vector_db_instance.state_collection else 0
989
+ total_items = doc_count + state_count
990
+
991
+ if total_items > 0:
992
+ logging.info(f"Data loading/verification complete. Vector DB contains {total_items} items. Found {states_loaded_count} distinct states.")
993
+ else:
994
+ logging.warning("Potential issue: PDF processed but Vector DB appears empty. Check PDF content/format and logs.")
995
+ print("\nWarning: No data loaded from PDF or found in DB. Application might not function correctly.\n")
996
 
997
+ logging.info("Setting up Gradio interface...")
998
  app_interface = rag.gradio_interface()
 
999
 
1000
+ SERVER_PORT = 7860
1001
  logging.info(f"Launching Gradio app on http://0.0.0.0:{SERVER_PORT}")
1002
+ print("\n--- Gradio App Running ---")
1003
+ print(f"Access the interface in your browser at: http://localhost:{SERVER_PORT} or http://<your-ip-address>:{SERVER_PORT}")
1004
+ print("--------------------------\n")
1005
+ app_interface.launch(
1006
+ server_name="0.0.0.0",
1007
+ server_port=SERVER_PORT,
1008
+ share=True # Set to False if you don't want public sharing links
1009
+ )
1010
 
1011
+ except FileNotFoundError as fnf_error:
1012
+ logging.error(f"Initialization failed due to a missing file: {str(fnf_error)}", exc_info=True)
1013
+ print(f"\n--- STARTUP ERROR: File Not Found ---")
1014
+ print(f"{str(fnf_error)}")
1015
+ print(f"---------------------------------------\n")
1016
+ exit(1)
1017
+ except ImportError as import_error:
1018
+ logging.error(f"Import error: {str(import_error)}. Check dependencies.", exc_info=True)
1019
+ print(f"\n--- STARTUP ERROR: Missing Dependency ---")
1020
+ print(f"Import Error: {str(import_error)}")
1021
+ print(f"Please ensure required libraries are installed (e.g., pip install -r requirements.txt).")
1022
+ print(f"-----------------------------------------\n")
1023
  exit(1)
1024
+ except RuntimeError as runtime_error:
1025
+ logging.error(f"A runtime error occurred during setup: {str(runtime_error)}", exc_info=True)
1026
+ print(f"\n--- STARTUP ERROR: Runtime Problem ---")
1027
+ print(f"Runtime Error: {str(runtime_error)}")
1028
+ print(f"Check logs for details, often related to data loading or DB setup.")
1029
+ print(f"--------------------------------------\n")
1030
  exit(1)
1031
  except Exception as e:
1032
+ logging.error(f"An unexpected error occurred during application startup: {str(e)}", exc_info=True)
1033
+ print(f"\n--- FATAL STARTUP ERROR ---")
1034
+ print(f"An unexpected error stopped the application: {str(e)}")
1035
+ print(f"Check logs for detailed traceback.")
1036
+ print(f"---------------------------\n")
1037
  exit(1)