yonilev commited on
Commit
ead4894
·
verified ·
1 Parent(s): 6b1f213

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +62 -110
app.py CHANGED
@@ -72,11 +72,7 @@ def _lazy_init():
72
  BASE_MODEL, device_map="cpu",
73
  torch_dtype=torch.float32, attn_implementation="eager")
74
  model = PeftModel.from_pretrained(base, MODEL_REPO)
75
- try:
76
- model = model.merge_and_unload() # flatten LoRA → base (fixes CPU/PEFT forward quirks)
77
- print("✅ CPU: fine-tuned adapter loaded + merged")
78
- except Exception as _me:
79
- print(f"⚠ merge_and_unload failed ({_me}); using PEFT-wrapped model")
80
  except Exception as e:
81
  print(f"⚠ adapter failed ({e}); base model (CPU)")
82
  model = AutoModelForCausalLM.from_pretrained(
@@ -139,9 +135,7 @@ def model_parse(raw_text):
139
  out = model.generate(**enc, max_new_tokens=128, do_sample=False,
140
  pad_token_id=tok.pad_token_id)
141
  decoded = tok.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True)
142
- parsed = _extract_json(decoded)
143
- print(f"🔎 PARSE in={raw_text!r} | json_ok={parsed is not None} | raw_out={decoded!r}", flush=True)
144
- return parsed
145
 
146
 
147
  # ═════════════════════════════════════════════════════════════════════════════
@@ -343,49 +337,50 @@ def render_recommendations(recs):
343
  # ═════════════════════════════════════════════════════════════════════════════
344
  DEMO_ISSUER = {"name": "מים שקטים", "tax_id": "962569844", "status": "authorized_dealer"}
345
 
346
- def _run_pipeline(raw_text, history, pending_parse, answers):
347
- _lazy_init()
348
- parse = model_parse(raw_text) if pending_parse is None else pending_parse
349
- questions = clarification_questions(parse, answers)
 
350
 
351
- if questions:
352
- q_md = "\n".join(f"{i+1}. {q}" for i, q in enumerate(questions))
353
- return (history + [{"role": "assistant",
354
- "content": f"יש לי כמה שאלות לפני שאפיק את המסמך:\n\n{q_md}"}],
355
- "", "", parse, answers)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
 
357
  final = apply_answers(parse, answers)
358
  try:
359
  completed = core.complete(DEMO_ISSUER, final, _STATE["rng"])
360
  except Exception as e:
361
- return (history + [{"role": "assistant", "content": f"שגיאה: {e}"}],
362
- "", "", None, {})
363
 
364
- return (history + [{"role": "assistant", "content": "✅ המסמך הופק בהצלחה"}],
 
365
  render_document(completed),
366
- render_recommendations(recommend(raw_text)),
367
- None, {})
368
-
369
-
370
- def on_submit(raw, history, pending, answers):
371
- if not raw.strip():
372
- return history, "", "", pending, answers
373
- return _run_pipeline(raw, history + [{"role": "user", "content": raw}], None, {})
374
-
375
-
376
- def on_answer(user_msg, history, pending, answers, raw_text):
377
- if not user_msg.strip():
378
- return history, "", "", pending, answers
379
- a, low = dict(answers), user_msg.strip()
380
- if any(w in low for w in ["חשבונית מס", "מס וקבלה", "מס/קבלה"]):
381
- a["doc_type"] = low
382
- elif "קבלה" in low:
383
- a["doc_type"] = "receipt"
384
- elif not a.get("client_name") and pending and not pending.get("client_name"):
385
- a["client_name"] = low
386
- return _run_pipeline(raw_text,
387
- history + [{"role": "user", "content": user_msg}],
388
- pending, a)
389
 
390
 
391
  # ═════════════════════════════════════════════════════════════════════════════
@@ -509,29 +504,21 @@ with gr.Blocks(title="Text2Receipt") as demo:
509
  </div>
510
  </div>""")
511
 
512
- st_parse = gr.State(None)
513
- st_answers = gr.State({})
514
-
515
- chatbot = gr.Chatbot(
516
- label="", height=320, elem_classes=["chatbot-area"], rtl=True,
517
- placeholder="<div dir='rtl' style='color:#1e293b;text-align:center;"
518
- "padding:48px 0;font-size:14px;'>הקלד הערה ולחץ שלח</div>",
519
  )
520
 
521
- answer_input = gr.Textbox(
522
- label="תשובה לסוכן ",
523
- placeholder="ענה לשאלת הסוכן ולחץ Enter…",
524
- lines=1, elem_classes=["answer-box"],
525
  )
526
 
527
- with gr.Row(equal_height=True):
528
- note_input = gr.Textbox(
529
- label="", lines=2, scale=5, elem_classes=["note-box"],
530
- placeholder='לדוגמה: קיבלתי 1,200 ש"ח ממשה כהן על ייעוץ עסקי',
531
- )
532
- with gr.Column(scale=1, min_width=100):
533
- submit_btn = gr.Button("שלח ⚡", elem_classes=["btn-send"])
534
- clear_btn = gr.Button("נקה 🗑", elem_classes=["btn-clear"])
535
 
536
  gr.HTML('<div dir="rtl" style="font-size:10px;color:#1e293b;margin:14px 0 6px;'
537
  'letter-spacing:.07em;text-transform:uppercase;">דוגמאות מהירות</div>')
@@ -543,29 +530,17 @@ with gr.Blocks(title="Text2Receipt") as demo:
543
  doc_output = gr.HTML()
544
  recs_output = gr.HTML()
545
 
 
546
  submit_btn.click(
547
- fn=on_submit,
548
- inputs=[note_input, chatbot, st_parse, st_answers],
549
- outputs=[chatbot, doc_output, recs_output, st_parse, st_answers],
550
- )
551
- answer_input.submit(
552
- fn=on_answer,
553
- inputs=[answer_input, chatbot, st_parse, st_answers, note_input],
554
- outputs=[chatbot, doc_output, recs_output, st_parse, st_answers],
555
  )
556
  clear_btn.click(
557
- fn=lambda: ([], "", "", None, {}, ""),
558
- outputs=[chatbot, doc_output, recs_output, st_parse, st_answers, note_input],
559
  )
560
 
561
- # ── Hidden API endpoint for the external Discord relay bot ──
562
- # Exposes the single-shot pipeline as /receipt_api (no UI, callable via gradio_client).
563
- _api_in = gr.Textbox(visible=False)
564
- _api_out = gr.Textbox(visible=False)
565
- # lambda defers the name lookup to call-time (_discord_pipeline is defined below in §8)
566
- _api_in.submit(fn=lambda note: _discord_pipeline(note),
567
- inputs=_api_in, outputs=_api_out, api_name="receipt_api")
568
-
569
  # ═════════════════════════════════════════════════════════════════════════════
570
  # 8. Discord bot — runs as a background thread inside the Space (+5% bonus)
571
  # ═════════════════════════════════════════════════════════════════════════════
@@ -674,7 +649,12 @@ def _start_discord_bot():
674
  )
675
 
676
  def _run():
677
- asyncio.run(bot.start(token))
 
 
 
 
 
678
 
679
  threading.Thread(target=_run, daemon=True).start()
680
  print("🤖 Discord bot thread started.")
@@ -682,35 +662,7 @@ def _start_discord_bot():
682
  # ═════════════════════════════════════════════════════════════════════════════
683
  # 9. Launch
684
  # ═════════════════════════════════════════════════════════════════════════════
685
- # ── TEMP diagnostics — remove once model + egress are confirmed ──
686
- def _probe_discord():
687
- import socket, urllib.request
688
- try:
689
- s = socket.create_connection(("discord.com", 443), timeout=8); s.close()
690
- print("🩺 PROBE tcp discord.com:443 → OK (TCP reachable)", flush=True)
691
- except Exception as e:
692
- print(f"🩺 PROBE tcp discord.com:443 → FAIL {type(e).__name__}: {e}", flush=True)
693
- return
694
- try:
695
- req = urllib.request.Request("https://discord.com/api/v10/gateway",
696
- headers={"User-Agent": "probe"})
697
- with urllib.request.urlopen(req, timeout=8) as r:
698
- print(f"🩺 PROBE https discord.com → OK status={r.status} (EGRESS OPEN)", flush=True)
699
- except Exception as e:
700
- print(f"🩺 PROBE https discord.com → FAIL {type(e).__name__}: {e} (EGRESS BLOCKED)", flush=True)
701
-
702
- def _startup_selftest():
703
- try:
704
- _lazy_init()
705
- for t in ["קיבלתי 500 שקל ממשה על ייעוץ",
706
- 'קמפיין רב-ערוצי למעגל בע"מ, 17699₪, מזומן']:
707
- model_parse(t)
708
- except Exception as e:
709
- print(f"🔎 SELFTEST error: {e}", flush=True)
710
-
711
- _probe_discord()
712
- _startup_selftest()
713
- # _start_discord_bot() # disabled until egress is confirmed (avoids crash spam in logs)
714
 
715
  if __name__ == "__main__":
716
  demo.launch(css=CSS)
 
72
  BASE_MODEL, device_map="cpu",
73
  torch_dtype=torch.float32, attn_implementation="eager")
74
  model = PeftModel.from_pretrained(base, MODEL_REPO)
75
+ print("✅ CPU: fine-tuned adapter loaded")
 
 
 
 
76
  except Exception as e:
77
  print(f"⚠ adapter failed ({e}); base model (CPU)")
78
  model = AutoModelForCausalLM.from_pretrained(
 
135
  out = model.generate(**enc, max_new_tokens=128, do_sample=False,
136
  pad_token_id=tok.pad_token_id)
137
  decoded = tok.decode(out[0, enc["input_ids"].shape[1]:], skip_special_tokens=True)
138
+ return _extract_json(decoded)
 
 
139
 
140
 
141
  # ═════════════════════════════════════════════════════════════════════════════
 
337
  # ═════════════════════════════════════════════════════════════════════════════
338
  DEMO_ISSUER = {"name": "מים שקטים", "tax_id": "962569844", "status": "authorized_dealer"}
339
 
340
+ _DOC_CHOICE_MAP = {
341
+ "קבלה": "receipt",
342
+ "חשבונית מס": "tax_invoice",
343
+ "חשבונית מס וקבלה": "tax_invoice_receipt",
344
+ }
345
 
346
+ def generate(note, doc_choice):
347
+ """Single-shot, stateless pipeline:
348
+ note (Textbox) + doc_choice (Radio) → (status_md, doc_html, recs_html).
349
+ No gr.State, no second handler the input value cannot be lost.
350
+ """
351
+ note = (note or "").strip()
352
+ if not note:
353
+ return "✏️ הקלד הערת הכנסה ולחץ הפק מסמך.", "", ""
354
+
355
+ _lazy_init()
356
+ parse = model_parse(note)
357
+
358
+ if parse is None or _missing_fields(parse) == ["parse_failed"]:
359
+ return ("🤔 לא הצלחתי לחלץ פרטים. נסח מחדש — לדוגמה: "
360
+ "'קיבלתי 500₪ ממשה על ייעוץ'.", "", "")
361
+
362
+ # ---- agentic clarification: only the doc-type can be genuinely ambiguous ----
363
+ answers = {}
364
+ if doc_choice and doc_choice in _DOC_CHOICE_MAP:
365
+ answers["doc_type"] = doc_choice # user picked explicitly
366
+ elif not parse.get("doc_type"):
367
+ # model didn't determine it and user left it on auto → ask
368
+ hint = ""
369
+ if _needs_allocation_clarification(parse):
370
+ hint = " (סכום גבוה ללקוח עסקי — לרוב חשבונית מס וקבלה)"
371
+ return (f"❓ צריך פרט אחד נוסף: **בחר סוג מסמך** מהרשימה למעלה{hint} "
372
+ "ולחץ הפק מסמך שוב.", "", "")
373
 
374
  final = apply_answers(parse, answers)
375
  try:
376
  completed = core.complete(DEMO_ISSUER, final, _STATE["rng"])
377
  except Exception as e:
378
+ return (f"⚠️ שגיאה בעיבוד: {e}", "", "")
 
379
 
380
+ client = final.get("client_name", "")
381
+ return ("✅ המסמך הופק בהצלחה.",
382
  render_document(completed),
383
+ render_recommendations(recommend(note)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
 
386
  # ═════════════════════════════════════════════════════════════════════════════
 
504
  </div>
505
  </div>""")
506
 
507
+ note_input = gr.Textbox(
508
+ label="הערת הכנסה", lines=2, elem_classes=["note-box"],
509
+ placeholder='לדוגמה: קיבלתי 1,200 ש"ח ממשה כהן על ייעוץ עסקי',
 
 
 
 
510
  )
511
 
512
+ doc_choice = gr.Radio(
513
+ ["זיהוי אוטומטי", "קבלה", "חשבונית מס", "חשבונית מס וקבלה"],
514
+ value="זיהוי אוטומטי", label="סוג מסמך",
 
515
  )
516
 
517
+ with gr.Row():
518
+ submit_btn = gr.Button("הפק מסמך ⚡", variant="primary", elem_classes=["btn-send"])
519
+ clear_btn = gr.Button("נקה 🗑", elem_classes=["btn-clear"])
520
+
521
+ status_output = gr.Markdown("", elem_classes=["status-area"])
 
 
 
522
 
523
  gr.HTML('<div dir="rtl" style="font-size:10px;color:#1e293b;margin:14px 0 6px;'
524
  'letter-spacing:.07em;text-transform:uppercase;">דוגמאות מהירות</div>')
 
530
  doc_output = gr.HTML()
531
  recs_output = gr.HTML()
532
 
533
+ # ── single, stateless handler — value cannot be lost ──
534
  submit_btn.click(
535
+ fn=generate,
536
+ inputs=[note_input, doc_choice],
537
+ outputs=[status_output, doc_output, recs_output],
 
 
 
 
 
538
  )
539
  clear_btn.click(
540
+ fn=lambda: ("", "זיהוי אוטומטי", "", "", ""),
541
+ outputs=[note_input, doc_choice, status_output, doc_output, recs_output],
542
  )
543
 
 
 
 
 
 
 
 
 
544
  # ═════════════════════════════════════════════════════════════════════════════
545
  # 8. Discord bot — runs as a background thread inside the Space (+5% bonus)
546
  # ═════════════════════════════════════════════════════════════════════════════
 
649
  )
650
 
651
  def _run():
652
+ try:
653
+ asyncio.run(bot.start(token))
654
+ except Exception as e:
655
+ print(f"ℹ Discord bot could not connect "
656
+ f"(expected on HF Spaces — outbound to discord.com is blocked): "
657
+ f"{type(e).__name__}. UI is unaffected.", flush=True)
658
 
659
  threading.Thread(target=_run, daemon=True).start()
660
  print("🤖 Discord bot thread started.")
 
662
  # ═════════════════════════════════════════════════════════════════════════════
663
  # 9. Launch
664
  # ═════════════════════════════════════════════════════════════════════════════
665
+ _start_discord_bot()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
 
667
  if __name__ == "__main__":
668
  demo.launch(css=CSS)