Curious-PM commited on
Commit
23b42e6
·
verified ·
1 Parent(s): f21e85d

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +114 -85
app.py CHANGED
@@ -1,7 +1,8 @@
1
- """Junior Associate — contract review in IRAC format.
2
 
3
- Fine-tuned Qwen 2.5-3B-Instruct with a LoRA adapter trained on 80 hand-crafted
4
- contract-review memos. Hosted on Hugging Face Spaces with ZeroGPU.
 
5
  """
6
  import torch
7
  import spaces
@@ -28,33 +29,45 @@ model.eval()
28
  print("Model ready.")
29
 
30
 
31
- @spaces.GPU(duration=60)
32
- def generate_reply(question):
 
 
 
 
33
  if not question or not question.strip():
34
- return "_Type a contract question first._"
 
35
 
36
  msgs = [
37
  {"role": "system", "content": SYSTEM_PROMPT},
38
  {"role": "user", "content": question.strip()},
39
  ]
40
- # Two-step tokenization for safe unpacking with model.generate(**inputs)
41
  text = tokenizer.apply_chat_template(
42
  msgs, tokenize=False, add_generation_prompt=True
43
  )
44
  inputs = tokenizer(text, return_tensors="pt").to("cuda")
45
  model.to("cuda")
46
- with torch.no_grad():
47
- out = model.generate(
48
- **inputs,
49
- max_new_tokens=600,
50
- do_sample=False,
51
- pad_token_id=tokenizer.eos_token_id,
52
- )
53
- reply = tokenizer.decode(
54
- out[0][inputs.input_ids.shape[1]:],
55
- skip_special_tokens=True,
56
  )
57
- return reply
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
 
60
  EXAMPLES = [
@@ -88,6 +101,8 @@ CSS = """
88
  --rule: #C8C8C8;
89
  --muted: #6B6B6B;
90
  --subtle: #A1A1AA;
 
 
91
  }
92
 
93
  * { box-sizing: border-box; }
@@ -99,20 +114,18 @@ body, .gradio-container {
99
  }
100
 
101
  .gradio-container {
102
- max-width: 920px !important;
103
  margin: 0 auto !important;
104
  padding: 56px 32px 80px !important;
105
  }
106
 
107
- /* Hide Gradio chrome */
108
  footer { display: none !important; }
109
  .show-api, .built-with { display: none !important; }
110
- button.lg.secondary[aria-label*="Use via API"] { display: none !important; }
111
 
112
  /* ── HEADER ───────────────────────────────────────────── */
113
  #header-block {
114
  text-align: center;
115
- margin-bottom: 48px;
116
  }
117
  #header-block .title-row {
118
  font-family: 'Inter', sans-serif;
@@ -135,7 +148,7 @@ button.lg.secondary[aria-label*="Use via API"] { display: none !important; }
135
  #header-block .subtitle {
136
  font-size: 16px;
137
  color: var(--muted);
138
- max-width: 560px;
139
  margin: 0 auto;
140
  line-height: 1.6;
141
  font-weight: 500;
@@ -165,7 +178,6 @@ button.lg.secondary[aria-label*="Use via API"] { display: none !important; }
165
  text-underline-offset: 2px;
166
  }
167
 
168
- /* ── SECTION LABELS ────────────────────────────────────── */
169
  .section-label {
170
  font-family: 'Inter', sans-serif;
171
  font-size: 11px;
@@ -173,7 +185,7 @@ button.lg.secondary[aria-label*="Use via API"] { display: none !important; }
173
  letter-spacing: 2px;
174
  color: var(--muted);
175
  text-transform: uppercase;
176
- margin: 32px 0 12px;
177
  }
178
 
179
  /* ── INPUT ─────────────────────────────────────────────── */
@@ -188,13 +200,7 @@ button.lg.secondary[aria-label*="Use via API"] { display: none !important; }
188
  color: var(--ink) !important;
189
  box-shadow: none !important;
190
  }
191
- textarea:focus {
192
- border-color: var(--ink) !important;
193
- outline: none !important;
194
- }
195
- .gr-textbox label, label[for] {
196
- display: none !important;
197
- }
198
 
199
  /* ── BUTTONS ────────────────────────────────────────────── */
200
  button.primary, .primary-button button {
@@ -209,17 +215,14 @@ button.primary, .primary-button button {
209
  padding: 14px 28px !important;
210
  cursor: pointer !important;
211
  width: 100% !important;
212
- transition: background 0.15s ease !important;
213
  margin-top: 16px !important;
 
214
  }
215
- button.primary:hover, .primary-button button:hover {
216
- background: #2A2A2A !important;
217
- }
218
 
219
- /* Example buttons — small cards */
220
  .examples-row {
221
  display: grid !important;
222
- grid-template-columns: 1fr 1fr !important;
223
  gap: 10px !important;
224
  margin-top: 8px !important;
225
  }
@@ -227,17 +230,16 @@ button.primary:hover, .primary-button button:hover {
227
  background: var(--paper) !important;
228
  border: 1px solid var(--hair) !important;
229
  border-radius: 8px !important;
230
- padding: 12px 16px !important;
231
  font-family: 'Inter', sans-serif !important;
232
- font-size: 13px !important;
233
  font-weight: 500 !important;
234
  color: var(--ink) !important;
235
  text-align: left !important;
236
  cursor: pointer !important;
237
  width: 100% !important;
238
- height: auto !important;
239
  min-height: 48px !important;
240
- line-height: 1.45 !important;
241
  white-space: normal !important;
242
  transition: all 0.15s ease !important;
243
  }
@@ -246,36 +248,54 @@ button.primary:hover, .primary-button button:hover {
246
  border-color: var(--rule) !important;
247
  }
248
 
249
- /* ── OUTPUT MEMO ────────────────────────────────────────── */
250
- #output-block {
 
 
251
  background: var(--paper);
252
  border: 1px solid var(--hair);
253
- border-left: 3px solid var(--ink);
254
- border-radius: 8px;
255
- padding: 28px 32px;
256
- margin-top: 12px;
257
- min-height: 100px;
258
  }
259
- #output-block .prose, #output-block * {
260
- font-family: 'Inter', sans-serif !important;
261
- font-size: 14.5px !important;
262
- line-height: 1.7 !important;
263
- color: var(--ink) !important;
 
 
264
  }
265
- #output-block strong {
266
- font-weight: 700 !important;
267
- color: var(--ink) !important;
 
 
 
 
 
 
 
 
 
 
268
  }
269
- #output-block em {
270
- color: var(--muted) !important;
271
- font-style: italic !important;
 
 
 
272
  }
273
- #output-block p { margin: 0 0 12px 0 !important; }
274
- #output-block ol, #output-block ul {
275
- padding-left: 22px !important;
276
- margin: 8px 0 12px 0 !important;
277
  }
278
- #output-block li { margin-bottom: 6px !important; }
 
 
 
 
279
 
280
  /* ── FOOTER ────────────────────────────────────────────── */
281
  #footer-block {
@@ -300,8 +320,9 @@ HEADER_HTML = """
300
  <div class="title-row">Demo 03 · Stay Curious · Fine-Tuning LLMs</div>
301
  <h1>Junior Associate</h1>
302
  <div class="subtitle">
303
- Qwen 2.5-3B fine-tuned on 80 hand-crafted contract-review memos.
304
- Every reply ships with the firm's IRAC structure and disclaimers automatically.
 
305
  </div>
306
  <div class="meta-pill">
307
  <span class="label">MODEL</span>
@@ -319,28 +340,25 @@ FOOTER_HTML = """
319
  </div>
320
  """
321
 
322
- INITIAL_OUTPUT = (
323
- "_The reply will appear here. First request takes ~10 seconds while the GPU warms up; "
324
- "subsequent requests are faster._"
325
- )
326
 
327
 
328
  with gr.Blocks(title="Junior Associate · Stay Curious", css=CSS, theme=gr.themes.Base()) as demo:
329
 
330
  gr.HTML(HEADER_HTML)
331
 
332
- gr.HTML('<div class="section-label">Ask the model a contract question</div>')
333
 
334
  question = gr.Textbox(
335
- placeholder="e.g. Their MSA caps liability at $1M. Is that reasonable?",
336
  lines=3,
337
  show_label=False,
338
  container=False,
339
  )
340
 
341
- submit = gr.Button("Ask the Junior Associate →", elem_classes="primary-button", variant="primary")
342
 
343
- gr.HTML('<div class="section-label">Try one of these</div>')
344
 
345
  with gr.Row(elem_classes="examples-row"):
346
  example_buttons = []
@@ -348,23 +366,34 @@ with gr.Blocks(title="Junior Associate · Stay Curious", css=CSS, theme=gr.theme
348
  btn = gr.Button(label, elem_classes="example-btn")
349
  example_buttons.append((btn, full_text))
350
 
351
- gr.HTML('<div class="section-label">The reply</div>')
352
-
353
- output = gr.Markdown(
354
- value=INITIAL_OUTPUT,
355
- elem_id="output-block",
356
- )
 
 
 
 
 
 
 
 
 
 
 
 
357
 
358
  gr.HTML(FOOTER_HTML)
359
 
360
  # Wire up
361
- submit.click(fn=generate_reply, inputs=question, outputs=output)
362
- question.submit(fn=generate_reply, inputs=question, outputs=output)
363
 
364
- # Example clicks: populate the textbox and immediately run inference
365
  for btn, full_text in example_buttons:
366
  btn.click(fn=lambda t=full_text: t, inputs=None, outputs=question).then(
367
- fn=generate_reply, inputs=question, outputs=output
368
  )
369
 
370
 
 
1
+ """Junior Associate — side-by-side comparison of base vs fine-tuned Qwen-3B.
2
 
3
+ Both sides receive the same input and same system prompt. The only difference
4
+ is whether the LoRA adapter is enabled. Hosted on Hugging Face Spaces with
5
+ ZeroGPU.
6
  """
7
  import torch
8
  import spaces
 
29
  print("Model ready.")
30
 
31
 
32
+ def _decode(out, input_len):
33
+ return tokenizer.decode(out[0][input_len:], skip_special_tokens=True)
34
+
35
+
36
+ @spaces.GPU(duration=120)
37
+ def generate_both(question):
38
  if not question or not question.strip():
39
+ placeholder = "_Type a contract question first._"
40
+ return placeholder, placeholder
41
 
42
  msgs = [
43
  {"role": "system", "content": SYSTEM_PROMPT},
44
  {"role": "user", "content": question.strip()},
45
  ]
 
46
  text = tokenizer.apply_chat_template(
47
  msgs, tokenize=False, add_generation_prompt=True
48
  )
49
  inputs = tokenizer(text, return_tensors="pt").to("cuda")
50
  model.to("cuda")
51
+ input_len = inputs.input_ids.shape[1]
52
+
53
+ gen_kwargs = dict(
54
+ max_new_tokens=600,
55
+ do_sample=False,
56
+ pad_token_id=tokenizer.eos_token_id,
 
 
 
 
57
  )
58
+
59
+ # LEFT — base model (adapter disabled)
60
+ with model.disable_adapter():
61
+ with torch.no_grad():
62
+ base_out = model.generate(**inputs, **gen_kwargs)
63
+ base_reply = _decode(base_out, input_len)
64
+
65
+ # RIGHT — fine-tuned (adapter on)
66
+ with torch.no_grad():
67
+ ft_out = model.generate(**inputs, **gen_kwargs)
68
+ ft_reply = _decode(ft_out, input_len)
69
+
70
+ return base_reply, ft_reply
71
 
72
 
73
  EXAMPLES = [
 
101
  --rule: #C8C8C8;
102
  --muted: #6B6B6B;
103
  --subtle: #A1A1AA;
104
+ --left-accent: #C8C8C8;
105
+ --right-accent: #0A0A0A;
106
  }
107
 
108
  * { box-sizing: border-box; }
 
114
  }
115
 
116
  .gradio-container {
117
+ max-width: 1280px !important;
118
  margin: 0 auto !important;
119
  padding: 56px 32px 80px !important;
120
  }
121
 
 
122
  footer { display: none !important; }
123
  .show-api, .built-with { display: none !important; }
 
124
 
125
  /* ── HEADER ───────────────────────────────────────────── */
126
  #header-block {
127
  text-align: center;
128
+ margin-bottom: 40px;
129
  }
130
  #header-block .title-row {
131
  font-family: 'Inter', sans-serif;
 
148
  #header-block .subtitle {
149
  font-size: 16px;
150
  color: var(--muted);
151
+ max-width: 720px;
152
  margin: 0 auto;
153
  line-height: 1.6;
154
  font-weight: 500;
 
178
  text-underline-offset: 2px;
179
  }
180
 
 
181
  .section-label {
182
  font-family: 'Inter', sans-serif;
183
  font-size: 11px;
 
185
  letter-spacing: 2px;
186
  color: var(--muted);
187
  text-transform: uppercase;
188
+ margin: 28px 0 10px;
189
  }
190
 
191
  /* ── INPUT ─────────────────────────────────────────────── */
 
200
  color: var(--ink) !important;
201
  box-shadow: none !important;
202
  }
203
+ textarea:focus { border-color: var(--ink) !important; outline: none !important; }
 
 
 
 
 
 
204
 
205
  /* ── BUTTONS ────────────────────────────────────────────── */
206
  button.primary, .primary-button button {
 
215
  padding: 14px 28px !important;
216
  cursor: pointer !important;
217
  width: 100% !important;
 
218
  margin-top: 16px !important;
219
+ transition: background 0.15s ease !important;
220
  }
221
+ button.primary:hover, .primary-button button:hover { background: #2A2A2A !important; }
 
 
222
 
 
223
  .examples-row {
224
  display: grid !important;
225
+ grid-template-columns: 1fr 1fr 1fr !important;
226
  gap: 10px !important;
227
  margin-top: 8px !important;
228
  }
 
230
  background: var(--paper) !important;
231
  border: 1px solid var(--hair) !important;
232
  border-radius: 8px !important;
233
+ padding: 12px 14px !important;
234
  font-family: 'Inter', sans-serif !important;
235
+ font-size: 12.5px !important;
236
  font-weight: 500 !important;
237
  color: var(--ink) !important;
238
  text-align: left !important;
239
  cursor: pointer !important;
240
  width: 100% !important;
 
241
  min-height: 48px !important;
242
+ line-height: 1.4 !important;
243
  white-space: normal !important;
244
  transition: all 0.15s ease !important;
245
  }
 
248
  border-color: var(--rule) !important;
249
  }
250
 
251
+ /* ── COMPARISON OUTPUT (TWO COLUMNS) ───────────────────── */
252
+ #comparison-row { gap: 18px !important; }
253
+
254
+ .lane {
255
  background: var(--paper);
256
  border: 1px solid var(--hair);
257
+ border-radius: 10px;
258
+ padding: 0;
259
+ overflow: hidden;
260
+ min-height: 200px;
 
261
  }
262
+ .lane.left-lane { border-top: 4px solid var(--left-accent); }
263
+ .lane.right-lane { border-top: 4px solid var(--right-accent); }
264
+
265
+ .lane-header {
266
+ padding: 16px 22px 12px 22px;
267
+ border-bottom: 1px solid var(--hair);
268
+ background: var(--off);
269
  }
270
+ .lane-header .lane-title {
271
+ font-family: 'Inter', sans-serif;
272
+ font-size: 16px;
273
+ font-weight: 800;
274
+ color: var(--ink);
275
+ letter-spacing: -0.3px;
276
+ margin: 0;
277
+ }
278
+ .lane-header .lane-meta {
279
+ font-family: 'JetBrains Mono', monospace;
280
+ font-size: 11px;
281
+ color: var(--muted);
282
+ margin-top: 4px;
283
  }
284
+
285
+ .lane-body, .lane-body * {
286
+ font-family: 'Inter', sans-serif !important;
287
+ font-size: 13.5px !important;
288
+ line-height: 1.65 !important;
289
+ color: var(--ink) !important;
290
  }
291
+ .lane-body {
292
+ padding: 20px 24px 24px 24px !important;
 
 
293
  }
294
+ .lane-body strong { font-weight: 700 !important; }
295
+ .lane-body em { color: var(--muted) !important; font-style: italic !important; }
296
+ .lane-body p { margin: 0 0 10px 0 !important; }
297
+ .lane-body ol, .lane-body ul { padding-left: 22px !important; margin: 6px 0 10px 0 !important; }
298
+ .lane-body li { margin-bottom: 4px !important; }
299
 
300
  /* ── FOOTER ────────────────────────────────────────────── */
301
  #footer-block {
 
320
  <div class="title-row">Demo 03 · Stay Curious · Fine-Tuning LLMs</div>
321
  <h1>Junior Associate</h1>
322
  <div class="subtitle">
323
+ Same model. Same system prompt. Same question.
324
+ The only difference is a 30&nbsp;MB LoRA adapter trained on 80 contract-review memos.
325
+ Watch what the adapter does to the reply.
326
  </div>
327
  <div class="meta-pill">
328
  <span class="label">MODEL</span>
 
340
  </div>
341
  """
342
 
343
+ PLACEHOLDER = "_The reply will appear here. First request takes ~15s while the GPU warms up._"
 
 
 
344
 
345
 
346
  with gr.Blocks(title="Junior Associate · Stay Curious", css=CSS, theme=gr.themes.Base()) as demo:
347
 
348
  gr.HTML(HEADER_HTML)
349
 
350
+ gr.HTML('<div class="section-label">Ask a contract question</div>')
351
 
352
  question = gr.Textbox(
353
+ placeholder="e.g. Our SaaS vendor wants perpetual ML training rights on our customer data. Is that normal?",
354
  lines=3,
355
  show_label=False,
356
  container=False,
357
  )
358
 
359
+ submit = gr.Button("Compare base vs fine-tuned →", elem_classes="primary-button", variant="primary")
360
 
361
+ gr.HTML('<div class="section-label">Or try one of these</div>')
362
 
363
  with gr.Row(elem_classes="examples-row"):
364
  example_buttons = []
 
366
  btn = gr.Button(label, elem_classes="example-btn")
367
  example_buttons.append((btn, full_text))
368
 
369
+ with gr.Row(elem_id="comparison-row"):
370
+ with gr.Column(elem_classes="lane left-lane"):
371
+ gr.HTML(
372
+ '<div class="lane-header">'
373
+ ' <div class="lane-title">Base Qwen 2.5-3B</div>'
374
+ ' <div class="lane-meta">no fine-tuning · prompt only</div>'
375
+ '</div>'
376
+ )
377
+ base_output = gr.Markdown(value=PLACEHOLDER, elem_classes="lane-body")
378
+
379
+ with gr.Column(elem_classes="lane right-lane"):
380
+ gr.HTML(
381
+ '<div class="lane-header">'
382
+ ' <div class="lane-title">+ LoRA adapter (fine-tuned)</div>'
383
+ ' <div class="lane-meta">trained on 80 contract-review memos</div>'
384
+ '</div>'
385
+ )
386
+ ft_output = gr.Markdown(value=PLACEHOLDER, elem_classes="lane-body")
387
 
388
  gr.HTML(FOOTER_HTML)
389
 
390
  # Wire up
391
+ submit.click(fn=generate_both, inputs=question, outputs=[base_output, ft_output])
392
+ question.submit(fn=generate_both, inputs=question, outputs=[base_output, ft_output])
393
 
 
394
  for btn, full_text in example_buttons:
395
  btn.click(fn=lambda t=full_text: t, inputs=None, outputs=question).then(
396
+ fn=generate_both, inputs=question, outputs=[base_output, ft_output]
397
  )
398
 
399