Nischal Subedi commited on
Commit
7e15221
·
1 Parent(s): 72aa01f
Files changed (1) hide show
  1. app.py +403 -647
app.py CHANGED
@@ -5,7 +5,10 @@ from functools import lru_cache
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,7 +36,7 @@ logging.basicConfig(
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,7 +60,7 @@ Statutes from context:
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,7 +75,7 @@ Answer:"""
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
 
@@ -169,7 +172,7 @@ Answer:"""
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,10 +192,10 @@ Answer:"""
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,6 +223,7 @@ Answer:"""
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,15 +241,12 @@ Answer:"""
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,785 +254,540 @@ Answer:"""
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)
 
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
  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
  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
  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
 
 
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
  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
  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
  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
 
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
+ # Improved Custom CSS for better UI design and HuggingFace compatibility
295
  custom_css = """
296
+ /* Import legible fonts */
297
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@600;700;800&display=swap');
298
+
299
+ /* Root variables for consistent theming */
300
  :root {
301
+ --primary-color: #2563eb;
302
+ --primary-hover: #1d4ed8;
303
+ --background-primary: #ffffff;
304
+ --background-secondary: #f8fafc;
305
+ --text-primary: #1e293b;
306
+ --text-secondary: #64748b;
307
+ --border-color: #e2e8f0;
308
+ --border-focus: #2563eb;
309
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
310
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
311
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
312
+ --error-bg: #fef2f2;
313
+ --error-border: #fecaca;
314
+ --error-text: #dc2626;
315
+ }
316
+
317
+ /* Dark mode variables */
 
 
 
 
 
318
  @media (prefers-color-scheme: dark) {
319
  :root {
320
+ --background-primary: #0f172a;
321
+ --background-secondary: #1e293b;
322
+ --text-primary: #f1f5f9;
323
+ --text-secondary: #94a3b8;
324
+ --border-color: #334155;
325
+ --error-bg: #1e1b1b;
326
+ --error-border: #451a1a;
327
+ --error-text: #f87171;
 
 
 
 
 
 
 
 
 
328
  }
329
  }
330
+ /* Base container improvements */
331
+ .gradio-container {
332
+ max-width: 1000px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  margin: 0 auto !important;
334
+ padding: 1rem !important;
335
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
336
+ }
337
+ /* Header styling */
338
+ .app-header-wrapper {
339
+ background: linear-gradient(135deg, var(--background-primary) 0%, var(--background-secondary) 100%) !important;
340
+ border: 2px solid var(--border-color) !important;
341
+ border-radius: 16px !important;
342
+ padding: 2rem !important;
343
+ margin-bottom: 1.5rem !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  text-align: center !important;
345
+ box-shadow: var(--shadow-md) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  }
347
+ .app-header-logo {
348
+ font-size: 3rem !important;
349
+ margin-bottom: 0.5rem !important;
350
+ display: block !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  }
352
+ .app-header-title {
353
+ font-family: 'Poppins', sans-serif !important;
354
+ font-size: 2.5rem !important;
355
+ font-weight: 700 !important;
356
+ color: var(--text-primary) !important;
357
+ margin: 0 0 0.5rem 0 !important;
358
+ line-height: 1.2 !important;
359
  }
360
+ .app-header-tagline {
361
+ font-size: 1.1rem !important;
362
+ color: var(--text-secondary) !important;
363
+ font-weight: 400 !important;
364
+ margin: 0 !important;
365
+ }
366
+ /* Main container with compact spacing */
367
+ .main-dashboard-container {
368
+ display: flex !important;
369
+ flex-direction: column !important;
370
+ gap: 1rem !important; /* Reduced gap for more compact look */
371
+ }
372
+ /* Card sections with better boundaries */
373
+ .dashboard-card-section {
374
+ background: var(--background-primary) !important;
375
+ border: 2px solid var(--border-color) !important;
376
+ border-radius: 12px !important;
377
+ padding: 1.5rem !important; /* Reduced padding for compactness */
378
+ box-shadow: var(--shadow-sm) !important;
379
+ transition: all 0.2s ease !important;
380
  }
381
+ .dashboard-card-section:hover {
382
+ box-shadow: var(--shadow-md) !important;
383
  }
384
+ /* Centered section titles with better typography */
385
+ .sub-section-title {
386
+ font-family: 'Poppins', sans-serif !important;
387
+ font-size: 1.5rem !important;
388
+ font-weight: 600 !important;
389
+ color: var(--text-primary) !important;
390
+ text-align: center !important;
391
+ margin: 0 0 1rem 0 !important;
392
+ padding-bottom: 0.5rem !important;
393
+ border-bottom: 2px solid var(--border-color) !important;
394
+ display: block !important;
395
  }
396
+ /* Improved input styling with clear boundaries */
397
+ .gradio-textbox, .gradio-dropdown {
398
+ margin-bottom: 0.75rem !important; /* Reduced margin for compactness */
399
  }
400
  .gradio-textbox textarea,
401
+ .gradio-textbox input,
402
+ .gradio-dropdown select {
403
+ background: var(--background-primary) !important;
404
+ border: 2px solid var(--border-color) !important;
405
+ border-radius: 8px !important;
406
+ padding: 0.75rem !important;
407
+ font-size: 0.95rem !important;
408
+ font-family: 'Inter', sans-serif !important;
409
  color: var(--text-primary) !important;
410
+ transition: all 0.2s ease !important;
411
+ box-shadow: var(--shadow-sm) !important;
 
 
 
 
 
412
  }
413
  .gradio-textbox textarea:focus,
414
+ .gradio-textbox input:focus,
415
+ .gradio-dropdown select:focus {
 
 
416
  outline: none !important;
417
+ border-color: var(--border-focus) !important;
418
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1) !important;
419
+ }
420
+ /* Placeholder text styling */
421
+ .gradio-textbox textarea::placeholder,
422
+ .gradio-textbox input::placeholder {
423
+ color: var(--text-secondary) !important;
424
+ opacity: 0.7 !important;
425
  }
426
+ /* Label styling for better readability */
427
+ .gradio-textbox label,
428
+ .gradio-dropdown label {
429
  font-weight: 500 !important;
430
  color: var(--text-primary) !important;
431
+ font-size: 0.9rem !important;
432
+ margin-bottom: 0.5rem !important;
433
  display: block !important;
434
  }
435
+ /* Info text styling */
436
+ .gradio-textbox .gr-form,
437
+ .gradio-dropdown .gr-form {
438
+ font-size: 0.85rem !important;
439
  color: var(--text-secondary) !important;
440
+ margin-top: 0.25rem !important;
441
+ }
442
+ /* Input row layout improvements */
443
+ .input-row {
444
+ display: flex !important;
445
+ gap: 1rem !important;
446
+ margin-bottom: 0.5rem !important; /* Reduced margin */
447
  }
448
+ .input-field {
449
+ flex: 1 !important;
450
+ }
451
+ /* Button styling improvements */
452
  .button-row {
453
+ display: flex !important;
454
+ gap: 1rem !important;
455
+ justify-content: flex-end !important;
456
+ margin-top: 1rem !important;
 
457
  }
458
  .gradio-button {
459
+ padding: 0.75rem 1.5rem !important;
460
+ border-radius: 8px !important;
 
461
  font-weight: 500 !important;
462
+ font-size: 0.9rem !important;
463
+ transition: all 0.2s ease !important;
464
+ cursor: pointer !important;
465
+ border: 2px solid transparent !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  }
467
  .gr-button-primary {
468
  background: var(--primary-color) !important;
469
+ color: white !important;
470
+ box-shadow: var(--shadow-sm) !important;
471
  }
472
+ .gr-button-primary:hover {
473
  background: var(--primary-hover) !important;
474
+ box-shadow: var(--shadow-md) !important;
475
+ transform: translateY(-1px) !important;
476
  }
477
  .gr-button-secondary {
478
+ background: transparent !important;
479
  color: var(--text-primary) !important;
480
+ border-color: var(--border-color) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  }
482
+ .gr-button-secondary:hover {
483
+ background: var(--background-secondary) !important;
484
+ border-color: var(--primary-color) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  }
486
+ /* Output styling with clear boundaries */
487
+ .output-content-wrapper {
488
+ background: var(--background-primary) !important;
489
+ border: 2px solid var(--border-color) !important;
490
+ border-radius: 8px !important;
491
+ padding: 1rem !important;
492
+ min-height: 100px !important;
493
+ font-size: 0.95rem !important;
494
+ line-height: 1.6 !important;
495
+ color: var(--text-primary) !important;
496
  }
497
+ .response-header {
498
+ font-size: 1.2rem !important;
499
+ font-weight: 600 !important;
500
+ color: var(--text-primary) !important;
501
+ margin-bottom: 0.75rem !important;
502
+ display: flex !important;
503
+ align-items: center !important;
504
+ gap: 0.5rem !important;
505
  }
506
+ .response-icon {
507
+ font-size: 1.3rem !important;
508
+ color: var(--primary-color) !important;
 
 
509
  }
510
+ .divider {
511
+ border: none !important;
512
+ border-top: 1px solid var(--border-color) !important;
513
+ margin: 0.75rem 0 !important;
514
+ }
515
+ /* Error message styling */
516
+ .error-message {
517
+ background: var(--error-bg) !important;
518
+ border: 2px solid var(--error-border) !important;
519
+ color: var(--error-text) !important;
520
+ padding: 1rem !important;
521
+ border-radius: 8px !important;
522
+ display: flex !important;
523
+ align-items: flex-start !important;
524
+ gap: 0.75rem !important;
525
+ font-size: 0.9rem !important;
526
+ }
527
+ .error-icon {
528
+ font-size: 1.2rem !important;
529
+ line-height: 1 !important;
530
+ margin-top: 0.1rem !important;
531
+ }
532
+ /* Placeholder styling */
533
+ .placeholder {
534
+ background: var(--background-secondary) !important;
535
+ border: 2px dashed var(--border-color) !important;
536
+ border-radius: 8px !important;
537
+ padding: 2rem 1rem !important;
538
+ text-align: center !important;
539
+ color: var(--text-secondary) !important;
540
+ font-style: italic !important;
541
+ }
542
+ /* Examples table styling */
543
+ .examples-section .gr-samples-table {
544
+ border: 2px solid var(--border-color) !important;
545
+ border-radius: 8px !important;
546
+ overflow: hidden !important;
547
+ margin-top: 1rem !important;
548
+ }
549
+ .examples-section .gr-samples-table th,
550
+ .examples-section .gr-samples-table td {
551
+ padding: 0.75rem !important;
552
+ border: none !important;
553
+ font-size: 0.9rem !important;
554
  }
555
+ .examples-section .gr-samples-table th {
556
+ background: var(--background-secondary) !important;
 
 
557
  font-weight: 600 !important;
558
  color: var(--text-primary) !important;
 
 
 
559
  }
560
+ .examples-section .gr-samples-table td {
561
+ background: var(--background-primary) !important;
 
 
 
 
 
 
 
 
 
 
 
 
562
  color: var(--text-primary) !important;
563
+ border-top: 1px solid var(--border-color) !important;
564
+ cursor: pointer !important;
 
 
 
 
 
 
 
565
  }
566
+ .examples-section .gr-samples-table tr:hover td {
567
+ background: var(--background-secondary) !important;
568
  }
569
+ /* Footer styling */
570
+ .app-footer-wrapper {
571
+ background: var(--background-secondary) !important;
572
+ border: 2px solid var(--border-color) !important;
573
+ border-radius: 12px !important;
574
+ padding: 1.5rem !important;
575
+ margin-top: 1.5rem !important;
576
  text-align: center !important;
577
+ }
578
+ .app-footer p {
579
+ margin: 0.5rem 0 !important;
580
+ font-size: 0.9rem !important;
581
  color: var(--text-secondary) !important;
 
582
  line-height: 1.5 !important;
583
  }
584
+ .app-footer a {
585
+ color: var(--primary-color) !important;
586
+ text-decoration: none !important;
587
+ font-weight: 500 !important;
 
 
 
 
588
  }
589
+ .app-footer a:hover {
590
+ text-decoration: underline !important;
 
591
  }
592
+ /* Hide Gradio default elements */
593
+ .gr-examples .gr-label,
594
+ .gr-examples .label-wrap,
595
+ .gr-examples .gr-accordion-header {
596
+ display: none !important;
 
 
 
 
 
 
 
 
 
 
 
597
  }
598
+ /* Responsive design */
 
 
599
  @media (max-width: 768px) {
600
+ .gradio-container {
601
+ padding: 0.5rem !important;
 
 
602
  }
603
+
604
+ .app-header-title {
605
+ font-size: 2rem !important;
606
  }
607
+
608
+ .app-header-tagline {
609
+ font-size: 1rem !important;
610
  }
611
+
 
612
  .input-row {
613
+ flex-direction: column !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  }
615
+
616
  .button-row {
617
+ flex-direction: column !important;
 
618
  }
619
+
620
+ .gradio-button {
621
+ width: 100% !important;
 
622
  }
623
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  """
625
 
626
+ with gr.Blocks(theme="soft", css=custom_css, title="Landlord-Tenant Rights Assistant") as demo:
 
627
  # Header Section
628
+ with gr.Group(elem_classes="app-header-wrapper"):
629
  gr.Markdown(
630
  """
631
+ <div class="app-header">
632
+ <span class="app-header-logo">⚖️</span>
633
+ <h1 class="app-header-title">Landlord-Tenant Rights Assistant</h1>
634
+ <p class="app-header-tagline">Empowering You with State-Specific Legal Insights</p>
635
+ </div>
 
 
 
 
636
  """
 
 
 
 
 
637
  )
638
 
639
+ # Main Dashboard Container
640
+ with gr.Column(elem_classes="main-dashboard-container"):
 
641
 
642
+ # Introduction and Disclaimer Card
643
+ with gr.Group(elem_classes="dashboard-card-section"):
644
+ gr.Markdown("<h3 class='sub-section-title'>Welcome & Disclaimer</h3>")
645
+ gr.Markdown(
646
+ """
647
+ Navigate landlord-tenant laws with ease. This assistant provides detailed, state-specific answers grounded in legal authority.
648
+
649
+ **Disclaimer:** 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.
650
+ """
651
  )
652
 
653
+ # OpenAI API Key Input Card
654
+ with gr.Group(elem_classes="dashboard-card-section"):
655
+ gr.Markdown("<h3 class='sub-section-title'>OpenAI API Key</h3>")
656
+ api_key_input = gr.Textbox(
657
+ label="API Key",
658
+ type="password",
659
+ placeholder="Enter your API key (e.g., sk-...)",
660
+ info="Required to process your query. Get one free from OpenAI.",
661
+ lines=1,
662
+ elem_classes=["input-field-group"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
663
  )
664
 
665
+ # Query Input and State Selection Card
666
+ with gr.Group(elem_classes="dashboard-card-section"):
667
+ gr.Markdown("<h3 class='sub-section-title'>Ask Your Question</h3>")
668
+ with gr.Row(elem_classes="input-row"):
669
+ with gr.Column(elem_classes="input-field", scale=3):
670
+ query_input = gr.Textbox(
671
+ label="Your Question",
672
+ placeholder="E.g., What are the rules for security deposit returns in my state?",
673
+ lines=4,
674
+ max_lines=8,
675
+ elem_classes=["input-field-group"]
676
+ )
677
+ with gr.Column(elem_classes="input-field", scale=1):
678
+ state_input = gr.Dropdown(
679
+ label="Select State",
680
+ choices=dropdown_choices,
681
+ value=initial_value,
682
+ allow_custom_value=False,
683
+ elem_classes=["input-field-group"]
684
+ )
685
+ with gr.Row(elem_classes="button-row"):
686
+ clear_button = gr.Button("Clear", variant="secondary", elem_classes=["gr-button-secondary"])
687
+ submit_button = gr.Button("Submit Query", variant="primary", elem_classes=["gr-button-primary"])
688
+
689
+ # Output Display Card
690
+ with gr.Group(elem_classes="dashboard-card-section"):
691
+ gr.Markdown("<h3 class='sub-section-title'>Legal Assistant's Response</h3>")
692
  output = gr.Markdown(
693
+ value="<div class='placeholder'>The answer will appear here after submitting your query.</div>",
 
694
  elem_classes="output-content-wrapper"
695
  )
696
 
697
+ # Example Questions Section
698
+ with gr.Group(elem_classes="dashboard-card-section examples-section"):
699
+ gr.Markdown("<h3 class='sub-section-title'>Example Questions</h3>")
700
+ if example_queries:
701
+ gr.Examples(
702
+ examples=example_queries,
703
+ inputs=[query_input, state_input],
704
+ examples_per_page=5,
705
+ label=""
706
+ )
707
+ else:
708
+ gr.Markdown("<div class='placeholder'>Sample questions could not be loaded.</div>")
 
 
709
 
710
  # Footer Section
711
+ with gr.Group(elem_classes="app-footer-wrapper"):
712
  gr.Markdown(
713
  """
714
+ 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.
715
+ Developed by **Nischal Subedi**. Connect on [LinkedIn](https://www.linkedin.com/in/nischal1/) or explore insights at [Substack](https://datascientistinsights.substack.com/).
716
+ """
 
717
  )
718
 
719
+ # Event Listeners
720
  submit_button.click(
721
  fn=query_interface_wrapper,
722
  inputs=[api_key_input, query_input, state_input],
723
  outputs=output,
724
  api_name="submit_query"
725
  )
726
+
727
  clear_button.click(
728
  fn=lambda: (
729
+ "",
730
+ "",
731
+ initial_value,
732
  "<div class='placeholder'>Inputs cleared. Ready for your next question.</div>"
733
  ),
734
  inputs=[],
735
  outputs=[api_key_input, query_input, state_input, output]
736
  )
737
 
738
+ return demo
 
739
 
740
+ # --- Main Execution Block (remains untouched from original logic, just fixed the exit) ---
741
  if __name__ == "__main__":
742
  logging.info("Starting Landlord-Tenant Rights Bot application...")
743
  try:
744
  SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
745
+ DEFAULT_PDF_PATH = os.path.join(SCRIPT_DIR, "tenant-landlord.pdf")
746
+ DEFAULT_DB_PATH = os.path.join(SCRIPT_DIR, "chroma_db")
747
 
748
  PDF_PATH = os.getenv("PDF_PATH", DEFAULT_PDF_PATH)
749
  VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", DEFAULT_DB_PATH)
750
 
751
  os.makedirs(os.path.dirname(VECTOR_DB_PATH), exist_ok=True)
 
 
 
 
752
 
753
+ logging.info(f"Attempting to load PDF from: {PDF_PATH}")
754
  if not os.path.exists(PDF_PATH):
755
  logging.error(f"FATAL: PDF file not found at the specified path: {PDF_PATH}")
756
+ 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")
757
+ exit(1) # This exit is correct: if file not found
758
+
759
+ if not os.access(PDF_PATH, os.R_OK):
760
+ logging.error(f"FATAL: PDF file at '{PDF_PATH}' exists but is not readable. Check file permissions.")
761
+ 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")
762
+ exit(1) # This exit is correct: if file exists but is not readable
763
+
764
+ logging.info(f"PDF file '{os.path.basename(PDF_PATH)}' found and is readable.")
765
 
 
766
  vector_db_instance = VectorDatabase(persist_directory=VECTOR_DB_PATH)
 
767
  rag = RAGSystem(vector_db=vector_db_instance)
768
 
769
+ rag.load_pdf(PDF_PATH)
 
 
 
 
770
 
 
 
 
 
 
 
 
771
  app_interface = rag.gradio_interface()
772
+ SERVER_PORT = int(os.getenv("PORT", 7860)) # Use PORT env var if on Spaces/Cloud, else 7860
773
 
 
774
  logging.info(f"Launching Gradio app on http://0.0.0.0:{SERVER_PORT}")
775
+ print(f"\n--- Gradio App Running ---\nAccess at: http://localhost:{SERVER_PORT} or your public Spaces URL\n--------------------------\n")
776
+ app_interface.launch(server_name="0.0.0.0", server_port=SERVER_PORT, share=False) # share=False is typical for Spaces
 
 
 
 
 
 
777
 
778
+ except ModuleNotFoundError as e:
779
+ if "vector_db" in str(e):
780
+ 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)
781
+ 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")
782
+ else:
783
+ logging.error(f"Application startup failed due to a missing module: {str(e)}", exc_info=True)
784
+ print(f"\n--- FATAL STARTUP ERROR - MISSING MODULE ---\n{str(e)}\nPlease ensure all dependencies are installed.\nCheck logs for more details.\n---------------------------\n")
 
 
 
 
 
785
  exit(1)
786
+ except FileNotFoundError as e:
787
+ logging.error(f"Application startup failed due to a missing file: {str(e)}", exc_info=True)
788
+ 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")
 
 
 
789
  exit(1)
790
  except Exception as e:
791
+ logging.error(f"Application startup failed: {str(e)}", exc_info=True)
792
+ print(f"\n--- FATAL STARTUP ERROR ---\n{str(e)}\nCheck logs for more details.\n---------------------------\n")
 
 
 
793
  exit(1)