Abrar55 commited on
Commit
324fc71
Β·
verified Β·
1 Parent(s): 89e0979

Fix: self-contained app.py with PEFT LoRA loading, model=Abrar55/contractual-hallucination-eliminator

Browse files
Files changed (1) hide show
  1. app.py +605 -780
app.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
  CHEX - Document Intelligence
3
- HuggingFace Spaces Gradio Demo
4
 
5
  Tab 1: Analyze Contract β€” paste a contract, ask a question, get a structured answer
6
  Tab 2: Benchmark Demo β€” side-by-side table showing base model hallucinations vs CHEX
@@ -9,42 +9,303 @@ Tab 3: Analyse Bank Statement β€” paste / upload a bank statement, get a summary
9
 
10
  from __future__ import annotations
11
 
 
 
12
  import os
13
- import sys
 
14
  from pathlib import Path
15
  from typing import Optional
16
 
17
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- sys.path.insert(0, str(Path(__file__).parent.parent))
20
 
21
  # ---------------------------------------------------------------------------
22
- # Model loading
23
  # ---------------------------------------------------------------------------
24
 
25
- MODEL_PATH = os.environ.get(
26
- "HF_MODEL_REPO", "PLACEHOLDER/chex-document-intelligence"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  SAMPLE_DIR = Path(__file__).parent / "sample_contracts"
29
  STATEMENT_DIR = Path(__file__).parent / "sample_statements"
30
 
31
- analyzer = None
 
32
  model_load_error: Optional[str] = None
33
 
34
- bank_analyzer = None
35
-
36
  try:
37
- from serving.inference import ContractAnalyzer # type: ignore
38
- from serving.bank_statement import BankStatementAnalyzer # type: ignore
39
-
40
- analyzer = ContractAnalyzer(model_path=MODEL_PATH)
41
- bank_analyzer = BankStatementAnalyzer(contract_analyzer=analyzer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  print(f"Model loaded successfully: {MODEL_PATH}")
 
43
  except Exception as e:
44
  model_load_error = str(e)
45
  print(f"WARNING: Model failed to load: {e}")
46
  print("Demo is running in preview mode β€” analysis will return a placeholder response.")
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  # ---------------------------------------------------------------------------
49
  # Sample contract content
50
  # ---------------------------------------------------------------------------
@@ -60,13 +321,23 @@ SOFTWARE_LICENSE = _read_sample("software_license.txt")
60
  NDA = _read_sample("nda.txt")
61
  SERVICE_AGREEMENT = _read_sample("service_agreement.txt")
62
 
63
- # Suggested questions for each sample contract
64
  SAMPLE_QUESTIONS = {
65
  "software_license.txt": "What is the limitation of liability in this agreement?",
66
  "nda.txt": "Does this agreement include a non-compete clause?",
67
  "service_agreement.txt": "Does this contract include a termination for convenience clause?",
68
  }
69
 
 
 
 
 
 
 
 
 
 
 
 
70
  # ---------------------------------------------------------------------------
71
  # Label badge HTML
72
  # ---------------------------------------------------------------------------
@@ -96,22 +367,15 @@ def format_label_html(label: str) -> str:
96
 
97
 
98
  # ---------------------------------------------------------------------------
99
- # Analysis handler
100
  # ---------------------------------------------------------------------------
101
 
102
- def analyze_contract(
103
- contract_text: str,
104
- question: str,
105
- ) -> tuple[str, str, str, str]:
106
- """
107
- Returns (label_html, answer_text, citation_text, reasoning_text).
108
- """
109
  if not contract_text.strip():
110
  return format_label_html("N/A"), "", "", "Please paste a contract above."
111
  if not question.strip():
112
  return format_label_html("N/A"), "", "", "Please enter a question."
113
-
114
- if analyzer is None:
115
  return (
116
  format_label_html("N/A"),
117
  "Model not loaded",
@@ -120,49 +384,47 @@ def analyze_contract(
120
  "Set HF_MODEL_REPO in Space secrets to the correct model repo.",
121
  )
122
 
123
- try:
124
- result = analyzer.analyze(contract_text, question)
125
- label_html = format_label_html(result.label.value)
126
- answer = result.answer if result.answer else "(none β€” clause is absent or not applicable)"
127
- citation = result.citation if result.citation else "(none)"
128
- reasoning = result.reasoning
129
- return label_html, answer, citation, reasoning
130
- except Exception as e:
131
- return format_label_html("ERROR"), "", "", f"Inference error: {e}"
132
-
133
-
134
- # ---------------------------------------------------------------------------
135
- # Sample bank statement
136
- # ---------------------------------------------------------------------------
137
 
138
- def _read_sample_statement(filename: str) -> str:
139
- p = STATEMENT_DIR / filename
140
- if p.exists():
141
- return p.read_text(encoding="utf-8")
142
- return f"[Sample statement '{filename}' not found. Place it in demo/sample_statements/]"
143
-
144
-
145
- SAMPLE_STATEMENT = _read_sample_statement("sample_statement.txt")
 
 
 
 
 
 
146
 
 
 
 
 
 
 
147
 
148
- # ---------------------------------------------------------------------------
149
- # Bank statement handlers
150
- # ---------------------------------------------------------------------------
151
 
152
- def _get_statement_text(
153
- paste_text: str,
154
- pdf_file,
155
- csv_file,
156
- ) -> tuple[str, str]:
157
- """
158
- Resolve whichever input was provided and return (statement_text, error_msg).
159
- Priority: PDF > CSV > paste text.
160
- """
161
  if pdf_file is not None:
162
- if bank_analyzer is None:
163
  return "", "Model not loaded β€” PDF extraction unavailable."
164
  try:
165
- text = bank_analyzer.extract_text_from_pdf(pdf_file)
 
 
 
 
 
 
 
 
 
166
  if not text.strip():
167
  return "", "PDF was uploaded but no text could be extracted."
168
  return text, ""
@@ -170,11 +432,17 @@ def _get_statement_text(
170
  return "", f"PDF extraction error: {e}"
171
 
172
  if csv_file is not None:
173
- if bank_analyzer is None:
174
  return "", "Model not loaded β€” CSV parsing unavailable."
175
  try:
176
- text = bank_analyzer.parse_csv(csv_file)
177
- return text, ""
 
 
 
 
 
 
178
  except Exception as e:
179
  return "", f"CSV parsing error: {e}"
180
 
@@ -184,52 +452,48 @@ def _get_statement_text(
184
  return "", "Please paste a bank statement or upload a PDF / CSV file."
185
 
186
 
187
- def analyse_bank_statement(
188
- paste_text: str,
189
- pdf_file,
190
- csv_file,
191
- ) -> tuple[str, str]:
192
- """
193
- Returns (summary_markdown, extracted_text_for_qa).
194
- """
195
  statement_text, error = _get_statement_text(paste_text, pdf_file, csv_file)
196
  if error:
197
  return f"**Error:** {error}", ""
198
-
199
- if analyzer is None:
200
  return (
201
- "**Model not loaded.** "
202
- f"Set `HF_MODEL_REPO` in Space secrets. Error: {model_load_error}",
203
  statement_text,
204
  )
205
 
206
- try:
207
- summary = bank_analyzer.summarize(statement_text)
208
- lines = ["## Statement Summary", ""]
209
- lines.append(f"**Total Credits:** {summary.total_credits or 'N/A'}")
210
- lines.append(f"**Total Debits:** {summary.total_debits or 'N/A'}")
211
- lines.append(f"**Largest Transaction:** {summary.largest_transaction or 'N/A'}")
212
- if summary.recurring_payments:
213
- lines.append("\n**Recurring Payments:**")
214
- for p in summary.recurring_payments:
215
- lines.append(f"- {p}")
216
- if summary.flags:
217
- lines.append("\n**Flags / Unusual Activity:**")
218
- for f in summary.flags:
219
- lines.append(f"- {f}")
220
- lines.append(f"\n*{summary.raw_reasoning}*")
221
- return "\n".join(lines), statement_text
222
- except Exception as e:
223
- return f"**Summarisation error:** {e}", statement_text
224
-
225
-
226
- def bank_qa(
227
- statement_text: str,
228
- question: str,
229
- ) -> tuple[str, str, str, str]:
230
- """
231
- Returns (label_html, answer_text, citation_text, reasoning_text).
232
- """
 
 
 
 
 
233
  if not statement_text.strip():
234
  return (
235
  format_label_html("N/A"), "", "",
@@ -237,25 +501,40 @@ def bank_qa(
237
  )
238
  if not question.strip():
239
  return format_label_html("N/A"), "", "", "Please enter a question."
240
-
241
- if analyzer is None:
242
  return (
243
  format_label_html("N/A"), "Model not loaded", "",
244
  f"Model failed to load: {model_load_error}.",
245
  )
246
 
247
- try:
248
- result = bank_analyzer.answer_question(statement_text, question)
249
- label_html = format_label_html(result.label.value)
250
- answer = result.answer if result.answer else "(none β€” information not found in statement)"
251
- citation = result.citation if result.citation else "(none)"
252
- return label_html, answer, citation, result.reasoning
253
- except Exception as e:
254
- return format_label_html("ERROR"), "", "", f"Inference error: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
 
257
  # ---------------------------------------------------------------------------
258
- # Benchmark table data (hardcoded β€” pre-computed base model outputs)
259
  # ---------------------------------------------------------------------------
260
 
261
  import pandas as pd
@@ -315,16 +594,14 @@ if model_load_error:
315
  )
316
 
317
  # ---------------------------------------------------------------------------
318
- # CSS β€” CHEX design system (glassmorphic, Inter + JetBrains Mono)
319
  # ---------------------------------------------------------------------------
320
 
321
  CHEX_CSS = """
322
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
323
 
324
- /* ── Reset ── */
325
  *, *::before, *::after { box-sizing: border-box; }
326
 
327
- /* ── Design tokens ── */
328
  :root {
329
  --bg-base: #f3f4f7;
330
  --bg-grad: radial-gradient(ellipse 1200px 700px at 18% -10%, rgba(120,150,200,0.18), transparent 60%),
@@ -358,7 +635,6 @@ CHEX_CSS = """
358
  --radius-lg: 16px;
359
  }
360
 
361
- /* ── Body & app shell ── */
362
  body {
363
  background: var(--bg-grad) !important;
364
  background-attachment: fixed !important;
@@ -380,21 +656,18 @@ body {
380
  padding: 0 !important;
381
  }
382
 
383
- /* ── Nuke ALL Gradio chrome ── */
384
  footer, .footer, .built-with, #footer,
385
  footer.svelte-1ax1toq, .svelte-1ax1toq.footer,
386
  .gradio-container > .footer,
387
  .share-button, .copy-all-button,
388
  .gradio-container > .top-panel { display: none !important; }
389
 
390
- /* Strip the outer container's own bg/padding */
391
  #root, .app, main {
392
  background: transparent !important;
393
  padding: 0 !important;
394
  margin: 0 !important;
395
  }
396
 
397
- /* The inner .contain div Gradio wraps everything in */
398
  .contain, .container {
399
  padding: 0 !important;
400
  gap: 0 !important;
@@ -402,12 +675,7 @@ footer.svelte-1ax1toq, .svelte-1ax1toq.footer,
402
  background: transparent !important;
403
  }
404
 
405
- /* Every .block Gradio creates β€” reset ALL chrome */
406
- .block,
407
- .gr-block,
408
- .gr-box,
409
- .gr-group,
410
- .gradio-container .block {
411
  background: transparent !important;
412
  border: none !important;
413
  box-shadow: none !important;
@@ -415,10 +683,8 @@ footer.svelte-1ax1toq, .svelte-1ax1toq.footer,
415
  border-radius: 0 !important;
416
  }
417
 
418
- /* The padding/gap between row children */
419
  .gap, .gr-row { gap: 20px !important; }
420
 
421
- /* Panel wrappers */
422
  .panel, .gr-panel, .gr-padded {
423
  background: transparent !important;
424
  border: none !important;
@@ -426,29 +692,21 @@ footer.svelte-1ax1toq, .svelte-1ax1toq.footer,
426
  box-shadow: none !important;
427
  }
428
 
429
- /* Tabs outer wrapper */
430
- .tabs, .gr-tabs {
431
- background: transparent !important;
432
- border: none !important;
433
- }
434
 
435
- /* Individual tab content areas */
436
  .tabitem, .gr-tabitem {
437
  background: transparent !important;
438
  border: none !important;
439
  padding: 24px !important;
440
  }
441
 
442
- /* Textbox wrappers β€” only reset the outer shell, let the inner textarea keep styling */
443
- [data-testid="textbox"],
444
- .gr-textbox {
445
  background: transparent !important;
446
  border: none !important;
447
  box-shadow: none !important;
448
  padding: 0 !important;
449
  }
450
 
451
- /* Label blocks */
452
  label.block, .label-wrap {
453
  background: transparent !important;
454
  border: none !important;
@@ -458,14 +716,8 @@ label.block, .label-wrap {
458
  flex-direction: column !important;
459
  }
460
 
461
- /* Row component */
462
- .row, .gr-row {
463
- background: transparent !important;
464
- border: none !important;
465
- padding: 0 !important;
466
- }
467
 
468
- /* Form groups */
469
  .form, .gr-form {
470
  background: transparent !important;
471
  border: none !important;
@@ -474,7 +726,6 @@ label.block, .label-wrap {
474
  gap: 14px !important;
475
  }
476
 
477
- /* ── Topbar ── */
478
  .chex-topbar {
479
  display: flex;
480
  align-items: center;
@@ -491,170 +742,72 @@ label.block, .label-wrap {
491
  }
492
 
493
  .chex-logo {
494
- width: 26px;
495
- height: 26px;
496
- border-radius: 8px;
497
  background: linear-gradient(135deg, #0d1220, rgba(13,18,32,0.7));
498
- color: #f3f4f7;
499
- display: grid;
500
- place-items: center;
501
- font-family: 'JetBrains Mono', monospace;
502
- font-weight: 700;
503
- font-size: 11px;
504
  letter-spacing: -0.05em;
505
  box-shadow: 0 4px 14px rgba(15,18,30,0.18), 0 1px 0 rgba(255,255,255,0.25) inset;
506
  flex-shrink: 0;
507
  }
508
 
509
- .chex-name {
510
- font-size: 15px;
511
- font-weight: 600;
512
- letter-spacing: -0.01em;
513
- color: var(--fg);
514
- font-family: 'Inter', sans-serif;
515
- }
516
-
517
- .chex-tag {
518
- font-size: 12px;
519
- color: var(--fg-muted);
520
- font-weight: 400;
521
- padding-left: 12px;
522
- border-left: 1px solid var(--hairline);
523
- font-family: 'Inter', sans-serif;
524
- }
525
 
526
  .chex-pill {
527
- display: inline-flex;
528
- align-items: center;
529
- gap: 8px;
530
- padding: 5px 12px 5px 10px;
531
- border: 1px solid var(--border);
532
- border-radius: 999px;
533
- font-size: 12px;
534
- color: var(--fg-muted);
535
- background: var(--bg-elev);
536
- backdrop-filter: blur(12px);
537
- -webkit-backdrop-filter: blur(12px);
538
- font-family: 'JetBrains Mono', monospace;
539
- white-space: nowrap;
540
  }
541
 
542
  .chex-dot {
543
- width: 6px;
544
- height: 6px;
545
- border-radius: 50%;
546
- background: var(--green);
547
- box-shadow: 0 0 0 3px rgba(15,157,88,0.22);
548
- display: inline-block;
549
- flex-shrink: 0;
550
  }
551
 
552
- /* ── Warning banner ── */
553
  .chex-banner {
554
- display: flex;
555
- align-items: center;
556
- gap: 12px;
557
- padding: 11px 20px;
558
- border-bottom: 1px solid var(--amber-border);
559
- background: var(--amber-bg);
560
- backdrop-filter: blur(var(--blur)) saturate(160%);
561
- -webkit-backdrop-filter: blur(var(--blur)) saturate(160%);
562
- color: var(--amber);
563
- font-size: 13px;
564
- font-family: 'Inter', sans-serif;
565
- font-weight: 500;
566
  }
567
  .chex-banner-icon { font-size: 14px; flex-shrink: 0; }
568
  .chex-banner-body { color: var(--fg); font-weight: 400; line-height: 1.5; }
569
  .chex-banner-body strong { color: var(--fg); font-weight: 600; }
570
- .chex-banner code {
571
- font-family: 'JetBrains Mono', monospace;
572
- font-size: 12px;
573
- background: rgba(0,0,0,0.06);
574
- padding: 1px 5px;
575
- border-radius: 4px;
576
- }
577
 
578
- /* ── Tab bar ── */
579
  .tab-nav {
580
  background: var(--bg-elev) !important;
581
  backdrop-filter: blur(var(--blur)) saturate(160%) !important;
582
  -webkit-backdrop-filter: blur(var(--blur)) saturate(160%) !important;
583
  border-bottom: 1px solid var(--hairline) !important;
584
- border-top: none !important;
585
- padding: 0 20px !important;
586
- gap: 0 !important;
587
- position: sticky !important;
588
- top: 60px !important;
589
- z-index: 99 !important;
590
- overflow: visible !important;
591
  }
592
 
593
  .tab-nav button {
594
- background: transparent !important;
595
- border: none !important;
596
- border-radius: 0 !important;
597
- padding: 14px 16px !important;
598
- color: var(--fg-muted) !important;
599
- font-size: 13px !important;
600
- font-weight: 500 !important;
601
- font-family: 'Inter', sans-serif !important;
602
- letter-spacing: -0.003em !important;
603
- position: relative !important;
604
- white-space: nowrap !important;
605
- transition: color 0.15s ease !important;
606
- cursor: pointer !important;
607
- box-shadow: none !important;
608
- outline: none !important;
609
  }
610
 
611
- .tab-nav button:hover {
612
- color: var(--fg) !important;
613
- background: transparent !important;
614
- }
615
 
616
- .tab-nav button.selected,
617
- .tab-nav button[aria-selected="true"] {
618
- color: var(--fg) !important;
619
- background: transparent !important;
620
- font-weight: 500 !important;
621
- box-shadow: none !important;
622
- }
623
-
624
- .tab-nav button.selected::after,
625
- .tab-nav button[aria-selected="true"]::after {
626
- content: "";
627
- position: absolute;
628
- left: 12px;
629
- right: 12px;
630
- bottom: -1px;
631
- height: 1.5px;
632
- background: var(--fg);
633
- border-radius: 2px 2px 0 0;
634
  }
635
 
636
- /* Tab content panels */
637
- .tabitem {
638
- border: none !important;
639
- background: transparent !important;
640
- padding: 24px 24px !important;
641
  }
642
 
643
- /* ── Card components ── */
644
- .chex-card,
645
- .gradio-container .gr-group.chex-card-group,
646
- .gradio-container [data-testid="group"].chex-card-group {
647
- background: var(--bg-elev) !important;
648
- backdrop-filter: blur(var(--blur)) saturate(180%) !important;
649
- -webkit-backdrop-filter: blur(var(--blur)) saturate(180%) !important;
650
- border: 1px solid var(--border) !important;
651
- border-radius: var(--radius-lg) !important;
652
- box-shadow: var(--shadow-md) !important;
653
- overflow: hidden !important;
654
- padding: 0 !important;
655
- }
656
 
657
- /* Groups used as cards */
658
  .gradio-container .gr-group {
659
  background: var(--bg-elev) !important;
660
  backdrop-filter: blur(var(--blur)) saturate(180%) !important;
@@ -662,458 +815,197 @@ label.block, .label-wrap {
662
  border: 1px solid var(--border) !important;
663
  border-radius: var(--radius-lg) !important;
664
  box-shadow: var(--shadow-md) !important;
665
- overflow: hidden !important;
666
- padding: 0 !important;
667
  }
668
 
669
- /* Inner content of groups gets consistent padding */
670
  .gradio-container .gr-group > *:not(.chex-card-header):not(.chex-chip-row) {
671
- padding-left: 20px !important;
672
- padding-right: 20px !important;
673
- }
674
- .gradio-container .gr-group > *:last-child {
675
- padding-bottom: 18px !important;
676
  }
 
677
 
678
  .chex-card-header {
679
- padding: 16px 20px;
680
- display: flex;
681
- align-items: center;
682
- justify-content: space-between;
683
- gap: 12px;
684
- border-bottom: 1px solid var(--hairline);
685
  }
686
 
687
  .chex-card-title {
688
- font-size: 13.5px;
689
- font-weight: 600;
690
- letter-spacing: -0.01em;
691
- display: inline-flex;
692
- align-items: center;
693
- gap: 10px;
694
- color: var(--fg);
695
- white-space: nowrap;
696
- font-family: 'Inter', sans-serif;
697
  }
698
 
699
- .chex-card-kicker {
700
- font-family: 'JetBrains Mono', monospace;
701
- font-size: 11px;
702
- color: var(--fg-subtle);
703
- font-weight: 400;
704
- letter-spacing: 0.04em;
705
- }
706
 
707
- /* ── Chip row (load samples) ── */
708
  .chex-chip-row {
709
- display: flex;
710
- align-items: center;
711
- gap: 8px;
712
- padding: 12px 20px;
713
- border-top: 1px solid var(--hairline);
714
- background: var(--bg-sunken);
715
- flex-wrap: wrap;
716
  }
717
 
718
- .chex-chip-label {
719
- font-family: 'JetBrains Mono', monospace;
720
- font-size: 10.5px;
721
- text-transform: uppercase;
722
- letter-spacing: 0.08em;
723
- color: var(--fg-subtle);
724
- white-space: nowrap;
725
- margin-right: 4px;
726
- }
727
 
728
- /* ── Suggested question bar ── */
729
  .chex-suggested {
730
- display: flex;
731
- align-items: center;
732
- gap: 10px;
733
- padding: 10px 14px;
734
- background: rgba(13,18,32,0.04);
735
- border: 1px solid var(--border);
736
- border-radius: var(--radius);
737
- font-size: 12.5px;
738
- color: var(--fg-muted);
739
- font-family: 'Inter', sans-serif;
740
- line-height: 1.4;
741
- margin-top: 2px;
742
  }
 
743
 
744
- .chex-suggested-icon {
745
- font-size: 13px;
746
- flex-shrink: 0;
747
- opacity: 0.7;
 
748
  }
749
 
750
-
751
- /* ── Labels on inputs ── */
752
- label > span:first-child,
753
- .label-wrap span,
754
- .gradio-container label span.text-gray-500,
755
- span.svelte-1b6s6s {
756
- font-family: 'JetBrains Mono', monospace !important;
757
- font-size: 10.5px !important;
758
- font-weight: 500 !important;
759
- text-transform: uppercase !important;
760
- letter-spacing: 0.08em !important;
761
- color: var(--fg-subtle) !important;
762
- margin-bottom: 6px !important;
763
- display: block !important;
764
- }
765
-
766
- /* ── Textareas & inputs ── */
767
- textarea,
768
- input[type="text"],
769
- input[type="search"],
770
- .gradio-container .gr-input,
771
- .gradio-container .gr-textarea,
772
  .gradio-container [data-testid="textbox"] textarea,
773
  .gradio-container [data-testid="textbox"] input {
774
- background: var(--bg-input) !important;
775
- backdrop-filter: blur(10px) !important;
776
- -webkit-backdrop-filter: blur(10px) !important;
777
- border: 1px solid var(--border) !important;
778
- border-radius: var(--radius) !important;
779
- color: var(--fg) !important;
780
- font-family: 'Inter', sans-serif !important;
781
- font-size: 13px !important;
782
- line-height: 1.6 !important;
783
- padding: 11px 14px !important;
784
  transition: border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease !important;
785
  resize: vertical !important;
786
  }
787
 
788
- textarea:focus,
789
- input[type="text"]:focus,
790
  .gradio-container [data-testid="textbox"] textarea:focus,
791
  .gradio-container [data-testid="textbox"] input:focus {
792
- border-color: var(--border-strong) !important;
793
- background: var(--bg-elev-strong) !important;
794
- box-shadow: 0 0 0 4px rgba(13,18,32,0.08) !important;
795
- outline: none !important;
796
  }
797
 
798
- textarea::placeholder,
799
- input::placeholder {
800
- color: var(--fg-subtle) !important;
801
- }
802
 
803
- /* Read-only / output textboxes */
804
  textarea[readonly],
805
  .gradio-container [data-testid="textbox"][data-interactive="false"] textarea {
806
- background: var(--bg-sunken) !important;
807
- border: 1px solid var(--hairline) !important;
808
- color: var(--fg) !important;
809
- cursor: default !important;
810
  }
811
 
812
- /* ── Buttons ── */
813
  .gradio-container button {
814
- font-family: 'Inter', sans-serif !important;
815
- font-size: 13px !important;
816
- font-weight: 500 !important;
817
- border-radius: var(--radius) !important;
818
  padding: 10px 16px !important;
819
  transition: opacity 0.15s ease, background 0.15s ease, box-shadow 0.15s ease !important;
820
- cursor: pointer !important;
821
- letter-spacing: -0.003em !important;
822
  }
823
 
824
- .gradio-container button.primary,
825
- .gradio-container [data-testid="button"][variant="primary"],
826
- button.primary {
827
- background: var(--fg) !important;
828
- color: var(--bg-base) !important;
829
- border: 1px solid var(--fg) !important;
830
  box-shadow: 0 6px 18px rgba(13,18,32,0.28), 0 1px 0 rgba(255,255,255,0.1) inset !important;
831
  }
 
832
 
833
- .gradio-container button.primary:hover,
834
- button.primary:hover {
835
- opacity: 0.88 !important;
836
- box-shadow: 0 4px 12px rgba(13,18,32,0.22) !important;
837
  }
 
838
 
839
- .gradio-container button.secondary,
840
- button.secondary {
841
- background: var(--bg-elev) !important;
842
- backdrop-filter: blur(10px) !important;
843
- -webkit-backdrop-filter: blur(10px) !important;
844
- color: var(--fg) !important;
845
- border: 1px solid var(--border) !important;
846
- box-shadow: var(--shadow-md) !important;
847
- }
848
-
849
- .gradio-container button.secondary:hover,
850
- button.secondary:hover {
851
- background: var(--bg-elev-strong) !important;
852
- border-color: var(--border-strong) !important;
853
- }
854
 
855
- /* Small / sm-size buttons */
856
- button.sm,
857
- .gradio-container button[size="sm"],
858
- button.small {
859
- font-size: 12px !important;
860
- padding: 7px 11px !important;
861
  }
862
 
863
- /* ── File upload ── */
864
- .gradio-container .upload-container,
865
- .gradio-container [data-testid="file"] {
866
- background: var(--bg-input) !important;
867
- border: 1px dashed var(--border-strong) !important;
868
- border-radius: var(--radius) !important;
869
- }
870
-
871
- /* ── Dataframe / benchmark table ── */
872
- .gradio-container .wrap.svelte-a4gbbr,
873
- .gradio-container .table-wrap,
874
  .gradio-container [data-testid="dataframe"] {
875
  background: var(--bg-elev) !important;
876
  backdrop-filter: blur(var(--blur)) saturate(180%) !important;
877
  -webkit-backdrop-filter: blur(var(--blur)) saturate(180%) !important;
878
- border: 1px solid var(--border) !important;
879
- border-radius: var(--radius-lg) !important;
880
- box-shadow: var(--shadow-md) !important;
881
- overflow: hidden !important;
882
  }
883
 
884
  .gradio-container table {
885
- background: transparent !important;
886
- font-size: 13px !important;
887
- font-family: 'Inter', sans-serif !important;
888
- border-collapse: separate !important;
889
- border-spacing: 0 !important;
890
- width: 100% !important;
891
- border: none !important;
892
- box-shadow: none !important;
893
- border-radius: 0 !important;
894
  }
895
 
896
  .gradio-container th {
897
- background: var(--bg-sunken) !important;
898
- border-bottom: 1px solid var(--hairline) !important;
899
- border-top: none !important;
900
- padding: 14px 18px !important;
901
- font-family: 'JetBrains Mono', monospace !important;
902
- font-size: 10.5px !important;
903
- text-transform: uppercase !important;
904
- letter-spacing: 0.08em !important;
905
- color: var(--fg-muted) !important;
906
- font-weight: 500 !important;
907
- text-align: left !important;
908
  }
909
 
910
  .gradio-container td {
911
- padding: 16px 18px !important;
912
- border-top: 1px solid var(--hairline) !important;
913
- border-bottom: none !important;
914
- vertical-align: top !important;
915
- line-height: 1.6 !important;
916
- color: var(--fg) !important;
917
- background: transparent !important;
918
  }
919
 
920
  .gradio-container tr:first-child td { border-top: none !important; }
921
 
922
- /* Hallucinated rows β€” rows where 'Hallucinated?' is YES */
923
- .gradio-container tr:has(td:last-child:contains("YES")) td,
924
- .chex-hallucinated-row td {
925
- background: color-mix(in srgb, var(--red-bg) 4%, transparent) !important;
926
- box-shadow: inset 2px 0 0 var(--red) !important;
927
- }
928
-
929
- /* ── Markdown output ── */
930
- .gradio-container .prose,
931
- .gradio-container .md,
932
- .gradio-container [data-testid="markdown"] {
933
- color: var(--fg) !important;
934
- font-family: 'Inter', sans-serif !important;
935
- font-size: 13px !important;
936
- line-height: 1.65 !important;
937
- }
938
-
939
- .gradio-container .prose h2,
940
- .gradio-container .md h2 {
941
- font-size: 18px !important;
942
- font-weight: 600 !important;
943
- letter-spacing: -0.02em !important;
944
- color: var(--fg) !important;
945
- margin-bottom: 10px !important;
946
- margin-top: 0 !important;
947
  }
948
 
949
- .gradio-container .prose h3,
950
- .gradio-container .md h3 {
951
- font-size: 13.5px !important;
952
- font-weight: 600 !important;
953
- letter-spacing: -0.01em !important;
954
- color: var(--fg) !important;
955
- margin-bottom: 8px !important;
956
- margin-top: 16px !important;
957
  }
958
 
959
- .gradio-container .prose p,
960
- .gradio-container .md p {
961
- color: var(--fg-muted) !important;
962
- font-size: 13px !important;
963
- line-height: 1.65 !important;
964
- margin-bottom: 8px !important;
965
  }
966
 
967
- .gradio-container .prose strong,
968
- .gradio-container .md strong {
969
- color: var(--fg) !important;
970
- font-weight: 600 !important;
971
- }
972
 
973
- .gradio-container .prose code,
974
- .gradio-container .md code {
975
- font-family: 'JetBrains Mono', monospace !important;
976
- font-size: 12px !important;
977
- background: rgba(13,18,32,0.06) !important;
978
- padding: 1px 5px !important;
979
- border-radius: 4px !important;
980
- color: var(--fg) !important;
981
  }
982
 
983
- /* ── Bench intro card ── */
984
  .chex-bench-intro {
985
- background: var(--bg-elev);
986
- backdrop-filter: blur(var(--blur)) saturate(180%);
987
  -webkit-backdrop-filter: blur(var(--blur)) saturate(180%);
988
- border: 1px solid var(--border);
989
- border-radius: var(--radius-lg);
990
- box-shadow: var(--shadow-md);
991
- padding: 24px 28px;
992
- margin-bottom: 20px;
993
- }
994
-
995
- .chex-bench-intro h2 {
996
- margin: 0 0 10px;
997
- font-size: 19px;
998
- font-weight: 600;
999
- letter-spacing: -0.02em;
1000
- color: var(--fg);
1001
- font-family: 'Inter', sans-serif;
1002
  }
1003
 
1004
- .chex-bench-intro p {
1005
- margin: 0;
1006
- color: var(--fg-muted);
1007
- font-size: 13px;
1008
- line-height: 1.65;
1009
- font-family: 'Inter', sans-serif;
1010
- }
1011
-
1012
- .chex-bench-stats {
1013
- display: grid;
1014
- grid-template-columns: repeat(3, 1fr);
1015
- gap: 8px;
1016
- margin-top: 18px;
1017
- }
1018
-
1019
- .chex-bench-stat {
1020
- background: var(--bg-sunken);
1021
- border: 1px solid var(--hairline);
1022
- border-radius: var(--radius);
1023
- padding: 12px 14px;
1024
- }
1025
-
1026
- .chex-bench-stat .v {
1027
- font-family: 'Inter', sans-serif;
1028
- font-size: 20px;
1029
- font-weight: 600;
1030
- letter-spacing: -0.025em;
1031
- color: var(--fg);
1032
- line-height: 1.2;
1033
- margin-bottom: 4px;
1034
- }
1035
 
 
 
 
1036
  .chex-bench-stat .v.red { color: var(--red); }
1037
  .chex-bench-stat .v.green { color: var(--green); }
 
1038
 
1039
- .chex-bench-stat .k {
1040
- font-size: 10px;
1041
- text-transform: uppercase;
1042
- letter-spacing: 0.08em;
1043
- color: var(--fg-subtle);
1044
- font-family: 'JetBrains Mono', monospace;
1045
- }
1046
-
1047
- /* ── Footer ── */
1048
  .chex-footer {
1049
- border-top: 1px solid var(--hairline);
1050
- padding: 14px 28px;
1051
- display: flex;
1052
- align-items: center;
1053
- gap: 18px;
1054
- color: var(--fg-subtle);
1055
- font-size: 11.5px;
1056
- font-family: 'JetBrains Mono', monospace;
1057
- background: var(--bg-elev);
1058
- backdrop-filter: blur(var(--blur));
1059
- -webkit-backdrop-filter: blur(var(--blur));
1060
- margin-top: 32px;
1061
  }
1062
-
1063
  .chex-footer .sep { opacity: 0.4; }
1064
 
1065
- /* ── Result label container ── */
1066
- .chex-label-wrap {
1067
- padding: 4px 0 8px;
1068
- }
1069
 
1070
- /* ── Divider ── */
1071
- .chex-divider {
1072
- height: 1px;
1073
- background: var(--hairline);
1074
- margin: 18px 0;
1075
- }
1076
-
1077
- /* ── Section kicker ── */
1078
- .chex-section-kicker {
1079
- font-family: 'JetBrains Mono', monospace;
1080
- font-size: 10.5px;
1081
- text-transform: uppercase;
1082
- letter-spacing: 0.08em;
1083
- color: var(--fg-subtle);
1084
- margin-bottom: 10px;
1085
- display: block;
1086
- }
1087
-
1088
- /* ── Card body padding ── */
1089
- .chex-card-body {
1090
- padding: 18px 20px;
1091
- display: flex;
1092
- flex-direction: column;
1093
- gap: 14px;
1094
- }
1095
-
1096
- /* ── Scrollbars ── */
1097
  *::-webkit-scrollbar { width: 8px; height: 8px; }
1098
- *::-webkit-scrollbar-thumb {
1099
- background: var(--border-strong);
1100
- border-radius: 999px;
1101
- border: 2px solid transparent;
1102
- background-clip: padding-box;
1103
- }
1104
  *::-webkit-scrollbar-track { background: transparent; }
1105
 
1106
- /* ── Gradio utility gaps ── */
1107
  .gradio-container .gap-4 { gap: 14px !important; }
1108
  .gradio-container .gap-2 { gap: 8px !important; }
1109
 
1110
- /* Nested sub-tabs (bank statement) */
1111
- .tabitem .tab-nav {
1112
- position: static !important;
1113
- top: auto !important;
1114
- }
1115
 
1116
- /* Responsive spacing */
1117
  @media (max-width: 900px) {
1118
  .chex-topbar { padding: 0 16px; }
1119
  .chex-tag { display: none; }
@@ -1124,7 +1016,7 @@ button.small {
1124
  """
1125
 
1126
  # ---------------------------------------------------------------------------
1127
- # Static HTML strings
1128
  # ---------------------------------------------------------------------------
1129
 
1130
  TOPBAR_HTML = """
@@ -1211,112 +1103,97 @@ STATEMENT_RESULTS_HEADER_HTML = """
1211
  # Gradio UI
1212
  # ---------------------------------------------------------------------------
1213
 
1214
- with gr.Blocks(
1215
- title="CHEX β€” Document Intelligence",
1216
- ) as demo:
1217
 
1218
- # ── Topbar ──────────────────────────────────────────────────────────── #
1219
  gr.HTML(TOPBAR_HTML)
1220
 
1221
- # ── Warning banner (only if model failed) ───────────────────────────── #
1222
  if WARNING_HTML:
1223
  gr.HTML(WARNING_HTML)
1224
 
1225
- # ── Tabs ────────────────────────────────────────────────────────────── #
1226
  with gr.Tabs():
1227
 
1228
- # ================================================================== #
1229
- # Tab 01 β€” Contract Analysis #
1230
- # ================================================================== #
1231
  with gr.Tab("01 Contract analysis"):
1232
  with gr.Row(equal_height=False):
1233
 
1234
- # ── Left panel: source document ──────────────────────────── #
1235
  with gr.Column(scale=9):
1236
- with gr.Group():
1237
- gr.HTML(CONTRACT_SOURCE_HEADER_HTML)
1238
- contract_input = gr.Textbox(
1239
- label="Contract text",
1240
- lines=20,
1241
- placeholder="Paste your contract text here, or load a sample below…",
1242
- show_label=False,
1243
- )
1244
- gr.HTML(CHIP_ROW_HTML)
1245
- with gr.Row():
1246
- btn_software = gr.Button("Software License", variant="secondary", size="sm")
1247
- btn_nda = gr.Button("NDA", variant="secondary", size="sm")
1248
- btn_service = gr.Button("Service Agreement", variant="secondary", size="sm")
1249
- suggested_q = gr.HTML(value="", visible=False)
1250
-
1251
- # ── Right panel: question + results ──────────────────────── #
1252
- with gr.Column(scale=11):
1253
- with gr.Group():
1254
- gr.HTML(CONTRACT_RESULTS_HEADER_HTML)
1255
- with gr.Row():
1256
- question_input = gr.Textbox(
1257
- label="Question",
1258
- placeholder="e.g., What is the limitation of liability?",
1259
- lines=1,
1260
  show_label=False,
1261
- scale=8,
1262
  )
1263
- analyze_btn = gr.Button("Analyze ↡", variant="primary", scale=2)
1264
- label_display = gr.HTML(value=format_label_html("N/A"))
1265
- answer_output = gr.Textbox(label="Answer", interactive=False, lines=3)
1266
- citation_output = gr.Textbox(label="Citation", interactive=False, lines=2)
1267
- reasoning_output = gr.Textbox(label="Reasoning", interactive=False, lines=3)
1268
-
1269
- # ================================================================== #
1270
- # Tab 02 β€” Bank Statements #
1271
- # ================================================================== #
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1272
  with gr.Tab("02 Bank statements"):
1273
  with gr.Row(equal_height=False):
1274
 
1275
- # ── Left panel: statement input ───────────────────────────── #
1276
  with gr.Column(scale=9):
1277
- with gr.Group():
1278
- gr.HTML(STATEMENT_SOURCE_HEADER_HTML)
1279
- with gr.Tabs():
1280
- with gr.Tab("Paste text"):
1281
- bank_paste_input = gr.Textbox(
1282
- label="Bank statement text",
1283
- lines=20,
1284
- placeholder="Paste your bank statement here, or load the sample below…",
1285
- show_label=False,
1286
- )
1287
- btn_load_statement = gr.Button("Load sample statement", variant="secondary", size="sm")
1288
- with gr.Tab("Upload PDF"):
1289
- bank_pdf_input = gr.File(label="PDF bank statement", file_types=[".pdf"])
1290
- with gr.Tab("Upload CSV"):
1291
- bank_csv_input = gr.File(label="CSV bank statement", file_types=[".csv"])
1292
 
1293
- # ── Right panel: summary + Q&A ───────────────────────────── #
1294
  with gr.Column(scale=11):
1295
- with gr.Group():
1296
- gr.HTML(STATEMENT_RESULTS_HEADER_HTML)
1297
- analyse_stmt_btn = gr.Button("Analyse statement", variant="primary")
1298
- summary_output = gr.Markdown(value="*Run 'Analyse statement' to generate a financial summary.*")
1299
- gr.HTML('<div class="chex-divider"></div>')
1300
- gr.HTML('<span class="chex-section-kicker">Ask a question</span>')
1301
- with gr.Row():
1302
- bank_question_input = gr.Textbox(
1303
- label="Question",
1304
- placeholder="e.g., What was the largest debit this month?",
1305
- lines=1,
1306
- show_label=False,
1307
- scale=8,
1308
- )
1309
- bank_ask_btn = gr.Button("Ask ↡", variant="secondary", scale=2)
1310
- bank_label_display = gr.HTML(value=format_label_html("N/A"))
1311
- bank_answer_output = gr.Textbox(label="Answer", interactive=False, lines=3)
1312
- bank_citation_output = gr.Textbox(label="Citation", interactive=False, lines=2)
1313
- bank_reasoning_output = gr.Textbox(label="Reasoning", interactive=False, lines=3)
1314
 
1315
  bank_statement_state = gr.State("")
1316
 
1317
- # ================================================================== #
1318
- # Tab 03 β€” Benchmark #
1319
- # ================================================================== #
1320
  with gr.Tab("03 Benchmark"):
1321
  gr.HTML(BENCH_INTRO_HTML)
1322
  gr.Dataframe(
@@ -1327,89 +1204,38 @@ with gr.Blocks(
1327
  interactive=False,
1328
  )
1329
 
1330
- # ── Footer ──────────────────────────────────────────────────────────── #
1331
  gr.HTML(FOOTER_HTML)
1332
 
1333
- # ====================================================================== #
1334
- # Event handlers #
1335
- # ====================================================================== #
1336
 
1337
  def load_software():
1338
- hint = (
1339
- '<div class="chex-suggested">'
1340
- '<span class="chex-suggested-icon">πŸ’‘</span>'
1341
- '<span><strong>Suggested:</strong> What is the limitation of liability in this agreement?</span>'
1342
- '</div>'
1343
- )
1344
- return (
1345
- SOFTWARE_LICENSE,
1346
- SAMPLE_QUESTIONS["software_license.txt"],
1347
- gr.update(value=hint, visible=True),
1348
- )
1349
 
1350
  def load_nda():
1351
- hint = (
1352
- '<div class="chex-suggested">'
1353
- '<span class="chex-suggested-icon">πŸ’‘</span>'
1354
- '<span><strong>Suggested:</strong> Does this agreement include a non-compete clause?</span>'
1355
- '</div>'
1356
- )
1357
- return (
1358
- NDA,
1359
- SAMPLE_QUESTIONS["nda.txt"],
1360
- gr.update(value=hint, visible=True),
1361
- )
1362
 
1363
  def load_service():
1364
- hint = (
1365
- '<div class="chex-suggested">'
1366
- '<span class="chex-suggested-icon">πŸ’‘</span>'
1367
- '<span><strong>Suggested:</strong> Does this contract include a termination for convenience clause? '
1368
- '<em>(expected: ABSENT)</em></span>'
1369
- '</div>'
1370
- )
1371
- return (
1372
- SERVICE_AGREEMENT,
1373
- SAMPLE_QUESTIONS["service_agreement.txt"],
1374
- gr.update(value=hint, visible=True),
1375
- )
1376
 
1377
- btn_software.click(
1378
- fn=load_software,
1379
- inputs=[],
1380
- outputs=[contract_input, question_input, suggested_q],
1381
- )
1382
- btn_nda.click(
1383
- fn=load_nda,
1384
- inputs=[],
1385
- outputs=[contract_input, question_input, suggested_q],
1386
- )
1387
- btn_service.click(
1388
- fn=load_service,
1389
- inputs=[],
1390
- outputs=[contract_input, question_input, suggested_q],
1391
- )
1392
 
1393
  analyze_btn.click(
1394
  fn=analyze_contract,
1395
  inputs=[contract_input, question_input],
1396
  outputs=[label_display, answer_output, citation_output, reasoning_output],
1397
  )
1398
-
1399
- # Trigger on Enter in question field
1400
  question_input.submit(
1401
  fn=analyze_contract,
1402
  inputs=[contract_input, question_input],
1403
  outputs=[label_display, answer_output, citation_output, reasoning_output],
1404
  )
1405
 
1406
- # ── Bank Statement handlers ──────────────────────────────────────────── #
1407
-
1408
- btn_load_statement.click(
1409
- fn=lambda: SAMPLE_STATEMENT,
1410
- inputs=[],
1411
- outputs=[bank_paste_input],
1412
- )
1413
 
1414
  analyse_stmt_btn.click(
1415
  fn=analyse_bank_statement,
@@ -1422,7 +1248,6 @@ with gr.Blocks(
1422
  inputs=[bank_statement_state, bank_question_input],
1423
  outputs=[bank_label_display, bank_answer_output, bank_citation_output, bank_reasoning_output],
1424
  )
1425
-
1426
  bank_question_input.submit(
1427
  fn=bank_qa,
1428
  inputs=[bank_statement_state, bank_question_input],
 
1
  """
2
  CHEX - Document Intelligence
3
+ HuggingFace Spaces Gradio Demo β€” fully self-contained (no relative imports)
4
 
5
  Tab 1: Analyze Contract β€” paste a contract, ask a question, get a structured answer
6
  Tab 2: Benchmark Demo β€” side-by-side table showing base model hallucinations vs CHEX
 
9
 
10
  from __future__ import annotations
11
 
12
+ import importlib.util
13
+ import json
14
  import os
15
+ import re
16
+ from enum import Enum
17
  from pathlib import Path
18
  from typing import Optional
19
 
20
  import gradio as gr
21
+ from pydantic import BaseModel
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Schema (inlined from data/schema.py)
25
+ # ---------------------------------------------------------------------------
26
+
27
+ class Label(str, Enum):
28
+ GROUNDED = "GROUNDED"
29
+ ABSENT = "ABSENT"
30
+ CONTRADICTS_PRIOR = "CONTRADICTS_PRIOR"
31
+
32
+
33
+ class ModelOutput(BaseModel):
34
+ question: str
35
+ label: Label
36
+ answer: Optional[str] = None
37
+ citation: Optional[str] = None
38
+ reasoning: str
39
+
40
+
41
+ class BankStatementSummary(BaseModel):
42
+ total_credits: Optional[str] = None
43
+ total_debits: Optional[str] = None
44
+ largest_transaction: Optional[str] = None
45
+ recurring_payments: Optional[list[str]] = None
46
+ flags: Optional[list[str]] = None
47
+ raw_reasoning: str
48
 
 
49
 
50
  # ---------------------------------------------------------------------------
51
+ # Prompt templates (inlined from training/prompt_template.py)
52
  # ---------------------------------------------------------------------------
53
 
54
+ SYSTEM_PROMPT = """\
55
+ You are a contract analysis assistant specializing in detecting hallucinations \
56
+ and calibrated uncertainty. Given a contract text and a question about a specific \
57
+ clause, output a single JSON object with exactly these fields:
58
+
59
+ question : the question asked (copy verbatim)
60
+ label : one of GROUNDED, ABSENT, or CONTRADICTS_PRIOR
61
+ - GROUNDED : the information exists verbatim in the contract
62
+ - ABSENT : the contract does not contain this clause at all
63
+ - CONTRADICTS_PRIOR: the contract contains a clause but it deviates \
64
+ from standard legal terms (e.g., inverted obligations, non-standard timeframes)
65
+ answer : the answer text if GROUNDED or CONTRADICTS_PRIOR, null if ABSENT
66
+ citation : the exact verbatim span from the contract that supports the answer, \
67
+ null if ABSENT
68
+ reasoning : one sentence explaining your classification
69
+
70
+ Output ONLY the JSON object. No preamble, no markdown fences, no text outside the JSON.
71
+
72
+ ### Example 1 β€” GROUNDED
73
+
74
+ [CONTRACT]
75
+ This Software License Agreement ("Agreement") is entered into as of January 1, 2024, \
76
+ between TechVision Inc. ("Licensor") and GlobalCorp Ltd. ("Licensee"). The Agreement \
77
+ shall remain in effect for a period of two (2) years from the Effective Date, unless \
78
+ earlier terminated pursuant to Section 8. Licensor grants Licensee a non-exclusive, \
79
+ non-transferable license to use the Software solely for Licensee's internal business \
80
+ purposes.
81
+ [/CONTRACT]
82
+
83
+ Question: What is the duration of this agreement?
84
+
85
+ {"question": "What is the duration of this agreement?", "label": "GROUNDED", \
86
+ "answer": "Two years from the Effective Date", \
87
+ "citation": "remain in effect for a period of two (2) years from the Effective Date", \
88
+ "reasoning": "The contract explicitly specifies a two-year term starting from the Effective Date."}
89
+
90
+ ### Example 2 β€” ABSENT
91
+
92
+ [CONTRACT]
93
+ The Licensee shall pay a monthly fee of five hundred dollars ($500.00). Payment is due \
94
+ on the first business day of each calendar month. Late payments shall accrue interest \
95
+ at a rate of one and one-half percent (1.5%) per month. Licensee shall maintain \
96
+ accurate records of all uses of the Software.
97
+ [/CONTRACT]
98
+
99
+ Question: Does this agreement include a limitation of liability clause?
100
+
101
+ {"question": "Does this agreement include a limitation of liability clause?", \
102
+ "label": "ABSENT", "answer": null, "citation": null, \
103
+ "reasoning": "No limitation of liability clause appears anywhere in the provided contract text."}
104
+
105
+ ### Example 3 β€” CONTRADICTS_PRIOR
106
+
107
+ [CONTRACT]
108
+ This Non-Disclosure Agreement is made between AlphaTech Solutions ("Discloser") and \
109
+ Beta Dynamics Corp. ("Recipient"). The Recipient shall not disclose Confidential \
110
+ Information to any third party. NON-COMPETE: The Recipient shall engage in any \
111
+ business activity that competes with the Discloser's primary operations during the \
112
+ term and for a period of 24 months thereafter. The Recipient shall not take any \
113
+ steps to protect Discloser's trade secrets.
114
+ [/CONTRACT]
115
+
116
+ Question: Does this agreement restrict the Recipient from competing with the Discloser?
117
+
118
+ {"question": "Does this agreement restrict the Recipient from competing with the Discloser?", \
119
+ "label": "CONTRADICTS_PRIOR", \
120
+ "answer": "The non-compete clause has inverted obligations β€” it permits competition rather than prohibiting it", \
121
+ "citation": "The Recipient shall engage in any business activity that competes with the Discloser's primary operations", \
122
+ "reasoning": "The clause uses 'shall engage' instead of 'shall not engage', inverting the standard non-compete obligation."}
123
+ """
124
+
125
+ BANK_SYSTEM_PROMPT = """\
126
+ You are a financial analysis assistant specialising in bank statement review. \
127
+ Given a bank statement (plain text, CSV-derived, or PDF-extracted) and either a \
128
+ summary request or a specific question, produce a single JSON object.
129
+
130
+ For SUMMARY mode (question is "SUMMARISE"):
131
+ Output a JSON object with exactly these fields:
132
+ total_credits : total money received (e.g. "Β£3,420.50") or null
133
+ total_debits : total money spent (e.g. "Β£2,105.30") or null
134
+ largest_transaction: description + amount of the single largest transaction or null
135
+ recurring_payments : list of detected recurring charges (e.g. ["Netflix Β£9.99", "Gym Β£35.00"]) or []
136
+ flags : list of unusual or suspicious items (e.g. ["Large cash withdrawal Β£800"]) or []
137
+ raw_reasoning : one sentence summarising your analysis
138
+
139
+ For Q&A mode (any other question), output a JSON object with exactly these fields:
140
+ question : the question asked (copy verbatim)
141
+ label : one of GROUNDED, ABSENT, or CONTRADICTS_PRIOR
142
+ answer : the answer text if GROUNDED or CONTRADICTS_PRIOR, null if ABSENT
143
+ citation : the exact verbatim span from the statement, null if ABSENT
144
+ reasoning : one sentence explaining your classification
145
+
146
+ Output ONLY the JSON object. No preamble, no markdown fences, no text outside the JSON.
147
+ """
148
+
149
+ STRICT_SUFFIX = (
150
+ "\n\nIMPORTANT: You must output ONLY a valid JSON object. "
151
+ "Do not include any text before or after the JSON."
152
  )
153
+
154
+
155
+ def _build_contract_messages(contract_text: str, question: str) -> list[dict]:
156
+ return [
157
+ {"role": "system", "content": SYSTEM_PROMPT},
158
+ {"role": "user", "content": f"[CONTRACT]\n{contract_text}\n[/CONTRACT]\n\nQuestion: {question}"},
159
+ ]
160
+
161
+
162
+ def _build_bank_messages(statement_text: str, question: str) -> list[dict]:
163
+ return [
164
+ {"role": "system", "content": BANK_SYSTEM_PROMPT},
165
+ {"role": "user", "content": f"[STATEMENT]\n{statement_text}\n[/STATEMENT]\n\nQuestion: {question}"},
166
+ ]
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # JSON parsing helpers
171
+ # ---------------------------------------------------------------------------
172
+
173
+ def _extract_json_str(raw_text: str) -> str:
174
+ match = re.search(r"\{[^{}]*(?:\{[^{}]*\}[^{}]*)?\}", raw_text, re.DOTALL)
175
+ if not match:
176
+ match = re.search(r"\{.*\}", raw_text, re.DOTALL)
177
+ if not match:
178
+ raise ValueError(f"No JSON object found in model output: {raw_text[:300]!r}")
179
+ return match.group()
180
+
181
+
182
+ def _parse_model_output(raw_text: str, question: str) -> ModelOutput:
183
+ json_str = _extract_json_str(raw_text)
184
+ return ModelOutput.model_validate_json(json_str)
185
+
186
+
187
+ def _parse_summary(raw_text: str) -> BankStatementSummary:
188
+ data = json.loads(_extract_json_str(raw_text))
189
+ return BankStatementSummary(
190
+ total_credits=data.get("total_credits"),
191
+ total_debits=data.get("total_debits"),
192
+ largest_transaction=data.get("largest_transaction"),
193
+ recurring_payments=data.get("recurring_payments") or [],
194
+ flags=data.get("flags") or [],
195
+ raw_reasoning=data.get("raw_reasoning", ""),
196
+ )
197
+
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Model loading
201
+ # ---------------------------------------------------------------------------
202
+
203
+ MODEL_PATH = os.environ.get("HF_MODEL_REPO", "Abrar55/contractual-hallucination-eliminator")
204
  SAMPLE_DIR = Path(__file__).parent / "sample_contracts"
205
  STATEMENT_DIR = Path(__file__).parent / "sample_statements"
206
 
207
+ _pipe = None
208
+ _tokenizer = None
209
  model_load_error: Optional[str] = None
210
 
 
 
211
  try:
212
+ import torch
213
+ from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
214
+
215
+ print(f"Loading tokenizer from: {MODEL_PATH}")
216
+ _tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
217
+ if _tokenizer.pad_token is None:
218
+ _tokenizer.pad_token = _tokenizer.eos_token
219
+
220
+ print(f"Loading model from: {MODEL_PATH}")
221
+ bnb_available = importlib.util.find_spec("bitsandbytes") is not None
222
+
223
+ if bnb_available and torch.cuda.is_available():
224
+ from transformers import BitsAndBytesConfig
225
+ bnb_config = BitsAndBytesConfig(
226
+ load_in_4bit=True,
227
+ bnb_4bit_quant_type="nf4",
228
+ bnb_4bit_compute_dtype=torch.bfloat16,
229
+ bnb_4bit_use_double_quant=True,
230
+ )
231
+ _model = AutoModelForCausalLM.from_pretrained(
232
+ MODEL_PATH,
233
+ quantization_config=bnb_config,
234
+ device_map="auto",
235
+ trust_remote_code=True,
236
+ )
237
+ print(" Loaded with 4-bit NF4 quantization")
238
+ else:
239
+ cuda_available = torch.cuda.is_available()
240
+ dtype = torch.float16 if cuda_available else torch.float32
241
+ _model = AutoModelForCausalLM.from_pretrained(
242
+ MODEL_PATH,
243
+ torch_dtype=dtype,
244
+ device_map="auto" if cuda_available else None,
245
+ trust_remote_code=True,
246
+ )
247
+ print(f" Loaded in {'fp16 (GPU)' if cuda_available else 'fp32 (CPU)'}")
248
+
249
+ _pipe = pipeline(
250
+ "text-generation",
251
+ model=_model,
252
+ tokenizer=_tokenizer,
253
+ max_new_tokens=512,
254
+ do_sample=False,
255
+ return_full_text=False,
256
+ pad_token_id=_tokenizer.eos_token_id,
257
+ )
258
  print(f"Model loaded successfully: {MODEL_PATH}")
259
+
260
  except Exception as e:
261
  model_load_error = str(e)
262
  print(f"WARNING: Model failed to load: {e}")
263
  print("Demo is running in preview mode β€” analysis will return a placeholder response.")
264
 
265
+
266
+ # ---------------------------------------------------------------------------
267
+ # Inference helpers
268
+ # ---------------------------------------------------------------------------
269
+
270
+ MAX_TOKENS = 8192
271
+
272
+
273
+ def _truncate(text: str) -> str:
274
+ if _tokenizer is None:
275
+ return text
276
+ tokens = _tokenizer.encode(text, add_special_tokens=False)
277
+ if len(tokens) > MAX_TOKENS:
278
+ print(f"WARNING: Text truncated from {len(tokens)} to {MAX_TOKENS} tokens.")
279
+ tokens = tokens[:MAX_TOKENS]
280
+ return _tokenizer.decode(tokens, skip_special_tokens=True)
281
+ return text
282
+
283
+
284
+ def _apply_template(messages: list[dict], strict: bool = False) -> str:
285
+ if strict:
286
+ messages = list(messages)
287
+ messages[-1] = dict(messages[-1])
288
+ messages[-1]["content"] += STRICT_SUFFIX
289
+ if _tokenizer is not None:
290
+ try:
291
+ return _tokenizer.apply_chat_template(
292
+ messages, tokenize=False, add_generation_prompt=True
293
+ )
294
+ except Exception:
295
+ pass
296
+ # Fallback: plain text
297
+ parts = []
298
+ for m in messages:
299
+ parts.append(f"<|im_start|>{m['role']}\n{m['content']}<|im_end|>")
300
+ parts.append("<|im_start|>assistant\n")
301
+ return "\n".join(parts)
302
+
303
+
304
+ def _run_pipe(prompt: str) -> str:
305
+ result = _pipe(prompt)
306
+ return result[0]["generated_text"]
307
+
308
+
309
  # ---------------------------------------------------------------------------
310
  # Sample contract content
311
  # ---------------------------------------------------------------------------
 
321
  NDA = _read_sample("nda.txt")
322
  SERVICE_AGREEMENT = _read_sample("service_agreement.txt")
323
 
 
324
  SAMPLE_QUESTIONS = {
325
  "software_license.txt": "What is the limitation of liability in this agreement?",
326
  "nda.txt": "Does this agreement include a non-compete clause?",
327
  "service_agreement.txt": "Does this contract include a termination for convenience clause?",
328
  }
329
 
330
+
331
+ def _read_sample_statement(filename: str) -> str:
332
+ p = STATEMENT_DIR / filename
333
+ if p.exists():
334
+ return p.read_text(encoding="utf-8")
335
+ return f"[Sample statement '{filename}' not found. Place it in demo/sample_statements/]"
336
+
337
+
338
+ SAMPLE_STATEMENT = _read_sample_statement("sample_statement.txt")
339
+
340
+
341
  # ---------------------------------------------------------------------------
342
  # Label badge HTML
343
  # ---------------------------------------------------------------------------
 
367
 
368
 
369
  # ---------------------------------------------------------------------------
370
+ # Analysis handlers
371
  # ---------------------------------------------------------------------------
372
 
373
+ def analyze_contract(contract_text: str, question: str) -> tuple[str, str, str, str]:
 
 
 
 
 
 
374
  if not contract_text.strip():
375
  return format_label_html("N/A"), "", "", "Please paste a contract above."
376
  if not question.strip():
377
  return format_label_html("N/A"), "", "", "Please enter a question."
378
+ if _pipe is None:
 
379
  return (
380
  format_label_html("N/A"),
381
  "Model not loaded",
 
384
  "Set HF_MODEL_REPO in Space secrets to the correct model repo.",
385
  )
386
 
387
+ contract_text = _truncate(contract_text)
388
+ messages = _build_contract_messages(contract_text, question)
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
+ for attempt in range(2):
391
+ prompt = _apply_template(messages, strict=(attempt == 1))
392
+ try:
393
+ raw = _run_pipe(prompt)
394
+ result = _parse_model_output(raw, question)
395
+ label_html = format_label_html(result.label.value)
396
+ answer = result.answer or "(none β€” clause is absent or not applicable)"
397
+ citation = result.citation or "(none)"
398
+ return label_html, answer, citation, result.reasoning
399
+ except Exception as e:
400
+ if attempt == 0:
401
+ print(f" Parse attempt 1 failed ({e}). Retrying with stricter prompt...")
402
+ else:
403
+ print(f" Parse attempt 2 failed ({e}). Returning safe fallback.")
404
 
405
+ return (
406
+ format_label_html("ABSENT"),
407
+ "(none β€” clause is absent or not applicable)",
408
+ "(none)",
409
+ "Model output could not be parsed as valid JSON after two attempts.",
410
+ )
411
 
 
 
 
412
 
413
+ def _get_statement_text(paste_text: str, pdf_file, csv_file) -> tuple[str, str]:
 
 
 
 
 
 
 
 
414
  if pdf_file is not None:
415
+ if _pipe is None:
416
  return "", "Model not loaded β€” PDF extraction unavailable."
417
  try:
418
+ if importlib.util.find_spec("pdfplumber") is None:
419
+ return "", "pdfplumber not installed."
420
+ import pdfplumber
421
+ text_parts = []
422
+ with pdfplumber.open(str(pdf_file)) as pdf:
423
+ for page in pdf.pages:
424
+ t = page.extract_text()
425
+ if t:
426
+ text_parts.append(t)
427
+ text = "\n".join(text_parts)
428
  if not text.strip():
429
  return "", "PDF was uploaded but no text could be extracted."
430
  return text, ""
 
432
  return "", f"PDF extraction error: {e}"
433
 
434
  if csv_file is not None:
435
+ if _pipe is None:
436
  return "", "Model not loaded β€” CSV parsing unavailable."
437
  try:
438
+ import pandas as pd
439
+ df = pd.read_csv(str(csv_file))
440
+ df.columns = [c.strip().lower() for c in df.columns]
441
+ lines = []
442
+ for _, row in df.iterrows():
443
+ parts = [str(v).strip() for v in row.values if str(v).strip() not in ("", "nan")]
444
+ lines.append(", ".join(parts))
445
+ return ", ".join(df.columns.tolist()) + "\n" + "\n".join(lines), ""
446
  except Exception as e:
447
  return "", f"CSV parsing error: {e}"
448
 
 
452
  return "", "Please paste a bank statement or upload a PDF / CSV file."
453
 
454
 
455
+ def analyse_bank_statement(paste_text: str, pdf_file, csv_file) -> tuple[str, str]:
 
 
 
 
 
 
 
456
  statement_text, error = _get_statement_text(paste_text, pdf_file, csv_file)
457
  if error:
458
  return f"**Error:** {error}", ""
459
+ if _pipe is None:
 
460
  return (
461
+ f"**Model not loaded.** Set `HF_MODEL_REPO` in Space secrets. Error: {model_load_error}",
 
462
  statement_text,
463
  )
464
 
465
+ statement_text = _truncate(statement_text)
466
+ messages = _build_bank_messages(statement_text, "SUMMARISE")
467
+
468
+ for attempt in range(2):
469
+ prompt = _apply_template(messages, strict=(attempt == 1))
470
+ try:
471
+ raw = _run_pipe(prompt)
472
+ summary = _parse_summary(raw)
473
+ lines = ["## Statement Summary", ""]
474
+ lines.append(f"**Total Credits:** {summary.total_credits or 'N/A'}")
475
+ lines.append(f"**Total Debits:** {summary.total_debits or 'N/A'}")
476
+ lines.append(f"**Largest Transaction:** {summary.largest_transaction or 'N/A'}")
477
+ if summary.recurring_payments:
478
+ lines.append("\n**Recurring Payments:**")
479
+ for p in summary.recurring_payments:
480
+ lines.append(f"- {p}")
481
+ if summary.flags:
482
+ lines.append("\n**Flags / Unusual Activity:**")
483
+ for f in summary.flags:
484
+ lines.append(f"- {f}")
485
+ lines.append(f"\n*{summary.raw_reasoning}*")
486
+ return "\n".join(lines), statement_text
487
+ except Exception as e:
488
+ if attempt == 0:
489
+ print(f" Summary parse attempt 1 failed ({e}). Retrying...")
490
+ else:
491
+ print(f" Summary parse attempt 2 failed ({e}). Returning error.")
492
+
493
+ return "**Summarisation error:** could not parse model output.", statement_text
494
+
495
+
496
+ def bank_qa(statement_text: str, question: str) -> tuple[str, str, str, str]:
497
  if not statement_text.strip():
498
  return (
499
  format_label_html("N/A"), "", "",
 
501
  )
502
  if not question.strip():
503
  return format_label_html("N/A"), "", "", "Please enter a question."
504
+ if _pipe is None:
 
505
  return (
506
  format_label_html("N/A"), "Model not loaded", "",
507
  f"Model failed to load: {model_load_error}.",
508
  )
509
 
510
+ statement_text = _truncate(statement_text)
511
+ messages = _build_bank_messages(statement_text, question)
512
+
513
+ for attempt in range(2):
514
+ prompt = _apply_template(messages, strict=(attempt == 1))
515
+ try:
516
+ raw = _run_pipe(prompt)
517
+ result = _parse_model_output(raw, question)
518
+ label_html = format_label_html(result.label.value)
519
+ answer = result.answer or "(none β€” information not found in statement)"
520
+ citation = result.citation or "(none)"
521
+ return label_html, answer, citation, result.reasoning
522
+ except Exception as e:
523
+ if attempt == 0:
524
+ print(f" Q&A parse attempt 1 failed ({e}). Retrying...")
525
+ else:
526
+ print(f" Q&A parse attempt 2 failed ({e}). Returning fallback.")
527
+
528
+ return (
529
+ format_label_html("ABSENT"),
530
+ "(none β€” information not found in statement)",
531
+ "(none)",
532
+ "Model output could not be parsed after two attempts.",
533
+ )
534
 
535
 
536
  # ---------------------------------------------------------------------------
537
+ # Benchmark table
538
  # ---------------------------------------------------------------------------
539
 
540
  import pandas as pd
 
594
  )
595
 
596
  # ---------------------------------------------------------------------------
597
+ # CSS
598
  # ---------------------------------------------------------------------------
599
 
600
  CHEX_CSS = """
601
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
602
 
 
603
  *, *::before, *::after { box-sizing: border-box; }
604
 
 
605
  :root {
606
  --bg-base: #f3f4f7;
607
  --bg-grad: radial-gradient(ellipse 1200px 700px at 18% -10%, rgba(120,150,200,0.18), transparent 60%),
 
635
  --radius-lg: 16px;
636
  }
637
 
 
638
  body {
639
  background: var(--bg-grad) !important;
640
  background-attachment: fixed !important;
 
656
  padding: 0 !important;
657
  }
658
 
 
659
  footer, .footer, .built-with, #footer,
660
  footer.svelte-1ax1toq, .svelte-1ax1toq.footer,
661
  .gradio-container > .footer,
662
  .share-button, .copy-all-button,
663
  .gradio-container > .top-panel { display: none !important; }
664
 
 
665
  #root, .app, main {
666
  background: transparent !important;
667
  padding: 0 !important;
668
  margin: 0 !important;
669
  }
670
 
 
671
  .contain, .container {
672
  padding: 0 !important;
673
  gap: 0 !important;
 
675
  background: transparent !important;
676
  }
677
 
678
+ .block, .gr-block, .gr-box, .gr-group, .gradio-container .block {
 
 
 
 
 
679
  background: transparent !important;
680
  border: none !important;
681
  box-shadow: none !important;
 
683
  border-radius: 0 !important;
684
  }
685
 
 
686
  .gap, .gr-row { gap: 20px !important; }
687
 
 
688
  .panel, .gr-panel, .gr-padded {
689
  background: transparent !important;
690
  border: none !important;
 
692
  box-shadow: none !important;
693
  }
694
 
695
+ .tabs, .gr-tabs { background: transparent !important; border: none !important; }
 
 
 
 
696
 
 
697
  .tabitem, .gr-tabitem {
698
  background: transparent !important;
699
  border: none !important;
700
  padding: 24px !important;
701
  }
702
 
703
+ [data-testid="textbox"], .gr-textbox {
 
 
704
  background: transparent !important;
705
  border: none !important;
706
  box-shadow: none !important;
707
  padding: 0 !important;
708
  }
709
 
 
710
  label.block, .label-wrap {
711
  background: transparent !important;
712
  border: none !important;
 
716
  flex-direction: column !important;
717
  }
718
 
719
+ .row, .gr-row { background: transparent !important; border: none !important; padding: 0 !important; }
 
 
 
 
 
720
 
 
721
  .form, .gr-form {
722
  background: transparent !important;
723
  border: none !important;
 
726
  gap: 14px !important;
727
  }
728
 
 
729
  .chex-topbar {
730
  display: flex;
731
  align-items: center;
 
742
  }
743
 
744
  .chex-logo {
745
+ width: 26px; height: 26px; border-radius: 8px;
 
 
746
  background: linear-gradient(135deg, #0d1220, rgba(13,18,32,0.7));
747
+ color: #f3f4f7; display: grid; place-items: center;
748
+ font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: 11px;
 
 
 
 
749
  letter-spacing: -0.05em;
750
  box-shadow: 0 4px 14px rgba(15,18,30,0.18), 0 1px 0 rgba(255,255,255,0.25) inset;
751
  flex-shrink: 0;
752
  }
753
 
754
+ .chex-name { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; color: var(--fg); font-family: 'Inter', sans-serif; }
755
+ .chex-tag { font-size: 12px; color: var(--fg-muted); font-weight: 400; padding-left: 12px; border-left: 1px solid var(--hairline); font-family: 'Inter', sans-serif; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756
 
757
  .chex-pill {
758
+ display: inline-flex; align-items: center; gap: 8px;
759
+ padding: 5px 12px 5px 10px; border: 1px solid var(--border); border-radius: 999px;
760
+ font-size: 12px; color: var(--fg-muted); background: var(--bg-elev);
761
+ backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
762
+ font-family: 'JetBrains Mono', monospace; white-space: nowrap;
 
 
 
 
 
 
 
 
763
  }
764
 
765
  .chex-dot {
766
+ width: 6px; height: 6px; border-radius: 50%; background: var(--green);
767
+ box-shadow: 0 0 0 3px rgba(15,157,88,0.22); display: inline-block; flex-shrink: 0;
 
 
 
 
 
768
  }
769
 
 
770
  .chex-banner {
771
+ display: flex; align-items: center; gap: 12px; padding: 11px 20px;
772
+ border-bottom: 1px solid var(--amber-border); background: var(--amber-bg);
773
+ backdrop-filter: blur(var(--blur)) saturate(160%); -webkit-backdrop-filter: blur(var(--blur)) saturate(160%);
774
+ color: var(--amber); font-size: 13px; font-family: 'Inter', sans-serif; font-weight: 500;
 
 
 
 
 
 
 
 
775
  }
776
  .chex-banner-icon { font-size: 14px; flex-shrink: 0; }
777
  .chex-banner-body { color: var(--fg); font-weight: 400; line-height: 1.5; }
778
  .chex-banner-body strong { color: var(--fg); font-weight: 600; }
779
+ .chex-banner code { font-family: 'JetBrains Mono', monospace; font-size: 12px; background: rgba(0,0,0,0.06); padding: 1px 5px; border-radius: 4px; }
 
 
 
 
 
 
780
 
 
781
  .tab-nav {
782
  background: var(--bg-elev) !important;
783
  backdrop-filter: blur(var(--blur)) saturate(160%) !important;
784
  -webkit-backdrop-filter: blur(var(--blur)) saturate(160%) !important;
785
  border-bottom: 1px solid var(--hairline) !important;
786
+ border-top: none !important; padding: 0 20px !important; gap: 0 !important;
787
+ position: sticky !important; top: 60px !important; z-index: 99 !important; overflow: visible !important;
 
 
 
 
 
788
  }
789
 
790
  .tab-nav button {
791
+ background: transparent !important; border: none !important; border-radius: 0 !important;
792
+ padding: 14px 16px !important; color: var(--fg-muted) !important;
793
+ font-size: 13px !important; font-weight: 500 !important; font-family: 'Inter', sans-serif !important;
794
+ letter-spacing: -0.003em !important; position: relative !important; white-space: nowrap !important;
795
+ transition: color 0.15s ease !important; cursor: pointer !important; box-shadow: none !important; outline: none !important;
 
 
 
 
 
 
 
 
 
 
796
  }
797
 
798
+ .tab-nav button:hover { color: var(--fg) !important; background: transparent !important; }
 
 
 
799
 
800
+ .tab-nav button.selected, .tab-nav button[aria-selected="true"] {
801
+ color: var(--fg) !important; background: transparent !important; font-weight: 500 !important; box-shadow: none !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
  }
803
 
804
+ .tab-nav button.selected::after, .tab-nav button[aria-selected="true"]::after {
805
+ content: ""; position: absolute; left: 12px; right: 12px; bottom: -1px;
806
+ height: 1.5px; background: var(--fg); border-radius: 2px 2px 0 0;
 
 
807
  }
808
 
809
+ .tabitem { border: none !important; background: transparent !important; padding: 24px 24px !important; }
 
 
 
 
 
 
 
 
 
 
 
 
810
 
 
811
  .gradio-container .gr-group {
812
  background: var(--bg-elev) !important;
813
  backdrop-filter: blur(var(--blur)) saturate(180%) !important;
 
815
  border: 1px solid var(--border) !important;
816
  border-radius: var(--radius-lg) !important;
817
  box-shadow: var(--shadow-md) !important;
818
+ overflow: hidden !important; padding: 0 !important;
 
819
  }
820
 
 
821
  .gradio-container .gr-group > *:not(.chex-card-header):not(.chex-chip-row) {
822
+ padding-left: 20px !important; padding-right: 20px !important;
 
 
 
 
823
  }
824
+ .gradio-container .gr-group > *:last-child { padding-bottom: 18px !important; }
825
 
826
  .chex-card-header {
827
+ padding: 16px 20px; display: flex; align-items: center;
828
+ justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--hairline);
 
 
 
 
829
  }
830
 
831
  .chex-card-title {
832
+ font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em;
833
+ display: inline-flex; align-items: center; gap: 10px; color: var(--fg);
834
+ white-space: nowrap; font-family: 'Inter', sans-serif;
 
 
 
 
 
 
835
  }
836
 
837
+ .chex-card-kicker { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--fg-subtle); font-weight: 400; letter-spacing: 0.04em; }
 
 
 
 
 
 
838
 
 
839
  .chex-chip-row {
840
+ display: flex; align-items: center; gap: 8px; padding: 12px 20px;
841
+ border-top: 1px solid var(--hairline); background: var(--bg-sunken); flex-wrap: wrap;
 
 
 
 
 
842
  }
843
 
844
+ .chex-chip-label { font-family: 'JetBrains Mono', monospace; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-subtle); white-space: nowrap; margin-right: 4px; }
 
 
 
 
 
 
 
 
845
 
 
846
  .chex-suggested {
847
+ display: flex; align-items: center; gap: 10px; padding: 10px 14px;
848
+ background: rgba(13,18,32,0.04); border: 1px solid var(--border); border-radius: var(--radius);
849
+ font-size: 12.5px; color: var(--fg-muted); font-family: 'Inter', sans-serif; line-height: 1.4; margin-top: 2px;
 
 
 
 
 
 
 
 
 
850
  }
851
+ .chex-suggested-icon { font-size: 13px; flex-shrink: 0; opacity: 0.7; }
852
 
853
+ label > span:first-child, .label-wrap span,
854
+ .gradio-container label span.text-gray-500, span.svelte-1b6s6s {
855
+ font-family: 'JetBrains Mono', monospace !important; font-size: 10.5px !important;
856
+ font-weight: 500 !important; text-transform: uppercase !important; letter-spacing: 0.08em !important;
857
+ color: var(--fg-subtle) !important; margin-bottom: 6px !important; display: block !important;
858
  }
859
 
860
+ textarea, input[type="text"], input[type="search"],
861
+ .gradio-container .gr-input, .gradio-container .gr-textarea,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
862
  .gradio-container [data-testid="textbox"] textarea,
863
  .gradio-container [data-testid="textbox"] input {
864
+ background: var(--bg-input) !important; backdrop-filter: blur(10px) !important;
865
+ -webkit-backdrop-filter: blur(10px) !important; border: 1px solid var(--border) !important;
866
+ border-radius: var(--radius) !important; color: var(--fg) !important;
867
+ font-family: 'Inter', sans-serif !important; font-size: 13px !important;
868
+ line-height: 1.6 !important; padding: 11px 14px !important;
 
 
 
 
 
869
  transition: border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease !important;
870
  resize: vertical !important;
871
  }
872
 
873
+ textarea:focus, input[type="text"]:focus,
 
874
  .gradio-container [data-testid="textbox"] textarea:focus,
875
  .gradio-container [data-testid="textbox"] input:focus {
876
+ border-color: var(--border-strong) !important; background: var(--bg-elev-strong) !important;
877
+ box-shadow: 0 0 0 4px rgba(13,18,32,0.08) !important; outline: none !important;
 
 
878
  }
879
 
880
+ textarea::placeholder, input::placeholder { color: var(--fg-subtle) !important; }
 
 
 
881
 
 
882
  textarea[readonly],
883
  .gradio-container [data-testid="textbox"][data-interactive="false"] textarea {
884
+ background: var(--bg-sunken) !important; border: 1px solid var(--hairline) !important;
885
+ color: var(--fg) !important; cursor: default !important;
 
 
886
  }
887
 
 
888
  .gradio-container button {
889
+ font-family: 'Inter', sans-serif !important; font-size: 13px !important;
890
+ font-weight: 500 !important; border-radius: var(--radius) !important;
 
 
891
  padding: 10px 16px !important;
892
  transition: opacity 0.15s ease, background 0.15s ease, box-shadow 0.15s ease !important;
893
+ cursor: pointer !important; letter-spacing: -0.003em !important;
 
894
  }
895
 
896
+ .gradio-container button.primary, button.primary {
897
+ background: var(--fg) !important; color: var(--bg-base) !important; border: 1px solid var(--fg) !important;
 
 
 
 
898
  box-shadow: 0 6px 18px rgba(13,18,32,0.28), 0 1px 0 rgba(255,255,255,0.1) inset !important;
899
  }
900
+ .gradio-container button.primary:hover, button.primary:hover { opacity: 0.88 !important; box-shadow: 0 4px 12px rgba(13,18,32,0.22) !important; }
901
 
902
+ .gradio-container button.secondary, button.secondary {
903
+ background: var(--bg-elev) !important; backdrop-filter: blur(10px) !important;
904
+ -webkit-backdrop-filter: blur(10px) !important; color: var(--fg) !important;
905
+ border: 1px solid var(--border) !important; box-shadow: var(--shadow-md) !important;
906
  }
907
+ .gradio-container button.secondary:hover, button.secondary:hover { background: var(--bg-elev-strong) !important; border-color: var(--border-strong) !important; }
908
 
909
+ button.sm, .gradio-container button[size="sm"], button.small { font-size: 12px !important; padding: 7px 11px !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
910
 
911
+ .gradio-container .upload-container, .gradio-container [data-testid="file"] {
912
+ background: var(--bg-input) !important; border: 1px dashed var(--border-strong) !important; border-radius: var(--radius) !important;
 
 
 
 
913
  }
914
 
915
+ .gradio-container .wrap.svelte-a4gbbr, .gradio-container .table-wrap,
 
 
 
 
 
 
 
 
 
 
916
  .gradio-container [data-testid="dataframe"] {
917
  background: var(--bg-elev) !important;
918
  backdrop-filter: blur(var(--blur)) saturate(180%) !important;
919
  -webkit-backdrop-filter: blur(var(--blur)) saturate(180%) !important;
920
+ border: 1px solid var(--border) !important; border-radius: var(--radius-lg) !important;
921
+ box-shadow: var(--shadow-md) !important; overflow: hidden !important;
 
 
922
  }
923
 
924
  .gradio-container table {
925
+ background: transparent !important; font-size: 13px !important;
926
+ font-family: 'Inter', sans-serif !important; border-collapse: separate !important;
927
+ border-spacing: 0 !important; width: 100% !important; border: none !important;
928
+ box-shadow: none !important; border-radius: 0 !important;
 
 
 
 
 
929
  }
930
 
931
  .gradio-container th {
932
+ background: var(--bg-sunken) !important; border-bottom: 1px solid var(--hairline) !important;
933
+ border-top: none !important; padding: 14px 18px !important;
934
+ font-family: 'JetBrains Mono', monospace !important; font-size: 10.5px !important;
935
+ text-transform: uppercase !important; letter-spacing: 0.08em !important;
936
+ color: var(--fg-muted) !important; font-weight: 500 !important; text-align: left !important;
 
 
 
 
 
 
937
  }
938
 
939
  .gradio-container td {
940
+ padding: 16px 18px !important; border-top: 1px solid var(--hairline) !important;
941
+ border-bottom: none !important; vertical-align: top !important; line-height: 1.6 !important;
942
+ color: var(--fg) !important; background: transparent !important;
 
 
 
 
943
  }
944
 
945
  .gradio-container tr:first-child td { border-top: none !important; }
946
 
947
+ .gradio-container .prose, .gradio-container .md, .gradio-container [data-testid="markdown"] {
948
+ color: var(--fg) !important; font-family: 'Inter', sans-serif !important;
949
+ font-size: 13px !important; line-height: 1.65 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
950
  }
951
 
952
+ .gradio-container .prose h2, .gradio-container .md h2 {
953
+ font-size: 18px !important; font-weight: 600 !important; letter-spacing: -0.02em !important;
954
+ color: var(--fg) !important; margin-bottom: 10px !important; margin-top: 0 !important;
 
 
 
 
 
955
  }
956
 
957
+ .gradio-container .prose p, .gradio-container .md p {
958
+ color: var(--fg-muted) !important; font-size: 13px !important; line-height: 1.65 !important; margin-bottom: 8px !important;
 
 
 
 
959
  }
960
 
961
+ .gradio-container .prose strong, .gradio-container .md strong { color: var(--fg) !important; font-weight: 600 !important; }
 
 
 
 
962
 
963
+ .gradio-container .prose code, .gradio-container .md code {
964
+ font-family: 'JetBrains Mono', monospace !important; font-size: 12px !important;
965
+ background: rgba(13,18,32,0.06) !important; padding: 1px 5px !important;
966
+ border-radius: 4px !important; color: var(--fg) !important;
 
 
 
 
967
  }
968
 
 
969
  .chex-bench-intro {
970
+ background: var(--bg-elev); backdrop-filter: blur(var(--blur)) saturate(180%);
 
971
  -webkit-backdrop-filter: blur(var(--blur)) saturate(180%);
972
+ border: 1px solid var(--border); border-radius: var(--radius-lg);
973
+ box-shadow: var(--shadow-md); padding: 24px 28px; margin-bottom: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
974
  }
975
 
976
+ .chex-bench-intro h2 { margin: 0 0 10px; font-size: 19px; font-weight: 600; letter-spacing: -0.02em; color: var(--fg); font-family: 'Inter', sans-serif; }
977
+ .chex-bench-intro p { margin: 0; color: var(--fg-muted); font-size: 13px; line-height: 1.65; font-family: 'Inter', sans-serif; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978
 
979
+ .chex-bench-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 18px; }
980
+ .chex-bench-stat { background: var(--bg-sunken); border: 1px solid var(--hairline); border-radius: var(--radius); padding: 12px 14px; }
981
+ .chex-bench-stat .v { font-family: 'Inter', sans-serif; font-size: 20px; font-weight: 600; letter-spacing: -0.025em; color: var(--fg); line-height: 1.2; margin-bottom: 4px; }
982
  .chex-bench-stat .v.red { color: var(--red); }
983
  .chex-bench-stat .v.green { color: var(--green); }
984
+ .chex-bench-stat .k { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-subtle); font-family: 'JetBrains Mono', monospace; }
985
 
 
 
 
 
 
 
 
 
 
986
  .chex-footer {
987
+ border-top: 1px solid var(--hairline); padding: 14px 28px;
988
+ display: flex; align-items: center; gap: 18px; color: var(--fg-subtle);
989
+ font-size: 11.5px; font-family: 'JetBrains Mono', monospace;
990
+ background: var(--bg-elev); backdrop-filter: blur(var(--blur));
991
+ -webkit-backdrop-filter: blur(var(--blur)); margin-top: 32px;
 
 
 
 
 
 
 
992
  }
 
993
  .chex-footer .sep { opacity: 0.4; }
994
 
995
+ .chex-label-wrap { padding: 4px 0 8px; }
996
+ .chex-divider { height: 1px; background: var(--hairline); margin: 18px 0; }
997
+ .chex-section-kicker { font-family: 'JetBrains Mono', monospace; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-subtle); margin-bottom: 10px; display: block; }
998
+ .chex-card-body { padding: 18px 20px; display: flex; flex-direction: column; gap: 14px; }
999
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1000
  *::-webkit-scrollbar { width: 8px; height: 8px; }
1001
+ *::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; border: 2px solid transparent; background-clip: padding-box; }
 
 
 
 
 
1002
  *::-webkit-scrollbar-track { background: transparent; }
1003
 
 
1004
  .gradio-container .gap-4 { gap: 14px !important; }
1005
  .gradio-container .gap-2 { gap: 8px !important; }
1006
 
1007
+ .tabitem .tab-nav { position: static !important; top: auto !important; }
 
 
 
 
1008
 
 
1009
  @media (max-width: 900px) {
1010
  .chex-topbar { padding: 0 16px; }
1011
  .chex-tag { display: none; }
 
1016
  """
1017
 
1018
  # ---------------------------------------------------------------------------
1019
+ # Static HTML
1020
  # ---------------------------------------------------------------------------
1021
 
1022
  TOPBAR_HTML = """
 
1103
  # Gradio UI
1104
  # ---------------------------------------------------------------------------
1105
 
1106
+ with gr.Blocks(title="CHEX β€” Document Intelligence") as demo:
 
 
1107
 
 
1108
  gr.HTML(TOPBAR_HTML)
1109
 
 
1110
  if WARNING_HTML:
1111
  gr.HTML(WARNING_HTML)
1112
 
 
1113
  with gr.Tabs():
1114
 
1115
+ # ── Tab 01: Contract Analysis ──────────────────────────────────── #
 
 
1116
  with gr.Tab("01 Contract analysis"):
1117
  with gr.Row(equal_height=False):
1118
 
 
1119
  with gr.Column(scale=9):
1120
+ with gr.Group():
1121
+ gr.HTML(CONTRACT_SOURCE_HEADER_HTML)
1122
+ contract_input = gr.Textbox(
1123
+ label="Contract text",
1124
+ lines=20,
1125
+ placeholder="Paste your contract text here, or load a sample below…",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1126
  show_label=False,
 
1127
  )
1128
+ gr.HTML(CHIP_ROW_HTML)
1129
+ with gr.Row():
1130
+ btn_software = gr.Button("Software License", variant="secondary", size="sm")
1131
+ btn_nda = gr.Button("NDA", variant="secondary", size="sm")
1132
+ btn_service = gr.Button("Service Agreement", variant="secondary", size="sm")
1133
+ suggested_q = gr.HTML(value="", visible=False)
1134
+
1135
+ with gr.Column(scale=11):
1136
+ with gr.Group():
1137
+ gr.HTML(CONTRACT_RESULTS_HEADER_HTML)
1138
+ with gr.Row():
1139
+ question_input = gr.Textbox(
1140
+ label="Question",
1141
+ placeholder="e.g., What is the limitation of liability?",
1142
+ lines=1,
1143
+ show_label=False,
1144
+ scale=8,
1145
+ )
1146
+ analyze_btn = gr.Button("Analyze ↡", variant="primary", scale=2)
1147
+ label_display = gr.HTML(value=format_label_html("N/A"))
1148
+ answer_output = gr.Textbox(label="Answer", interactive=False, lines=3)
1149
+ citation_output = gr.Textbox(label="Citation", interactive=False, lines=2)
1150
+ reasoning_output = gr.Textbox(label="Reasoning", interactive=False, lines=3)
1151
+
1152
+ # ── Tab 02: Bank Statements ────────────────────────────────────── #
1153
  with gr.Tab("02 Bank statements"):
1154
  with gr.Row(equal_height=False):
1155
 
 
1156
  with gr.Column(scale=9):
1157
+ with gr.Group():
1158
+ gr.HTML(STATEMENT_SOURCE_HEADER_HTML)
1159
+ with gr.Tabs():
1160
+ with gr.Tab("Paste text"):
1161
+ bank_paste_input = gr.Textbox(
1162
+ label="Bank statement text",
1163
+ lines=20,
1164
+ placeholder="Paste your bank statement here, or load the sample below…",
1165
+ show_label=False,
1166
+ )
1167
+ btn_load_statement = gr.Button("Load sample statement", variant="secondary", size="sm")
1168
+ with gr.Tab("Upload PDF"):
1169
+ bank_pdf_input = gr.File(label="PDF bank statement", file_types=[".pdf"])
1170
+ with gr.Tab("Upload CSV"):
1171
+ bank_csv_input = gr.File(label="CSV bank statement", file_types=[".csv"])
1172
 
 
1173
  with gr.Column(scale=11):
1174
+ with gr.Group():
1175
+ gr.HTML(STATEMENT_RESULTS_HEADER_HTML)
1176
+ analyse_stmt_btn = gr.Button("Analyse statement", variant="primary")
1177
+ summary_output = gr.Markdown(value="*Run 'Analyse statement' to generate a financial summary.*")
1178
+ gr.HTML('<div class="chex-divider"></div>')
1179
+ gr.HTML('<span class="chex-section-kicker">Ask a question</span>')
1180
+ with gr.Row():
1181
+ bank_question_input = gr.Textbox(
1182
+ label="Question",
1183
+ placeholder="e.g., What was the largest debit this month?",
1184
+ lines=1,
1185
+ show_label=False,
1186
+ scale=8,
1187
+ )
1188
+ bank_ask_btn = gr.Button("Ask ↡", variant="secondary", scale=2)
1189
+ bank_label_display = gr.HTML(value=format_label_html("N/A"))
1190
+ bank_answer_output = gr.Textbox(label="Answer", interactive=False, lines=3)
1191
+ bank_citation_output = gr.Textbox(label="Citation", interactive=False, lines=2)
1192
+ bank_reasoning_output = gr.Textbox(label="Reasoning", interactive=False, lines=3)
1193
 
1194
  bank_statement_state = gr.State("")
1195
 
1196
+ # ── Tab 03: Benchmark ──────────────────────────────────────────── #
 
 
1197
  with gr.Tab("03 Benchmark"):
1198
  gr.HTML(BENCH_INTRO_HTML)
1199
  gr.Dataframe(
 
1204
  interactive=False,
1205
  )
1206
 
 
1207
  gr.HTML(FOOTER_HTML)
1208
 
1209
+ # ── Event handlers ─────────────────────────────────────────────────── #
 
 
1210
 
1211
  def load_software():
1212
+ hint = '<div class="chex-suggested"><span class="chex-suggested-icon">πŸ’‘</span><span><strong>Suggested:</strong> What is the limitation of liability in this agreement?</span></div>'
1213
+ return SOFTWARE_LICENSE, SAMPLE_QUESTIONS["software_license.txt"], gr.update(value=hint, visible=True)
 
 
 
 
 
 
 
 
 
1214
 
1215
  def load_nda():
1216
+ hint = '<div class="chex-suggested"><span class="chex-suggested-icon">πŸ’‘</span><span><strong>Suggested:</strong> Does this agreement include a non-compete clause?</span></div>'
1217
+ return NDA, SAMPLE_QUESTIONS["nda.txt"], gr.update(value=hint, visible=True)
 
 
 
 
 
 
 
 
 
1218
 
1219
  def load_service():
1220
+ hint = '<div class="chex-suggested"><span class="chex-suggested-icon">πŸ’‘</span><span><strong>Suggested:</strong> Does this contract include a termination for convenience clause? <em>(expected: ABSENT)</em></span></div>'
1221
+ return SERVICE_AGREEMENT, SAMPLE_QUESTIONS["service_agreement.txt"], gr.update(value=hint, visible=True)
 
 
 
 
 
 
 
 
 
 
1222
 
1223
+ btn_software.click(fn=load_software, inputs=[], outputs=[contract_input, question_input, suggested_q])
1224
+ btn_nda.click(fn=load_nda, inputs=[], outputs=[contract_input, question_input, suggested_q])
1225
+ btn_service.click(fn=load_service, inputs=[], outputs=[contract_input, question_input, suggested_q])
 
 
 
 
 
 
 
 
 
 
 
 
1226
 
1227
  analyze_btn.click(
1228
  fn=analyze_contract,
1229
  inputs=[contract_input, question_input],
1230
  outputs=[label_display, answer_output, citation_output, reasoning_output],
1231
  )
 
 
1232
  question_input.submit(
1233
  fn=analyze_contract,
1234
  inputs=[contract_input, question_input],
1235
  outputs=[label_display, answer_output, citation_output, reasoning_output],
1236
  )
1237
 
1238
+ btn_load_statement.click(fn=lambda: SAMPLE_STATEMENT, inputs=[], outputs=[bank_paste_input])
 
 
 
 
 
 
1239
 
1240
  analyse_stmt_btn.click(
1241
  fn=analyse_bank_statement,
 
1248
  inputs=[bank_statement_state, bank_question_input],
1249
  outputs=[bank_label_display, bank_answer_output, bank_citation_output, bank_reasoning_output],
1250
  )
 
1251
  bank_question_input.submit(
1252
  fn=bank_qa,
1253
  inputs=[bank_statement_state, bank_question_input],