Benny-Tang commited on
Commit
0cbb7d0
·
verified ·
1 Parent(s): 4dc3a24

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -388
app.py CHANGED
@@ -1,16 +1,14 @@
1
  """
2
  Sloth — AI Workforce Intelligence Demo
3
- Hugging Face Spaces deployment
4
 
5
- Works in two modes:
6
- 1. STANDALONE — if no SLOTH_BACKEND_URL is set, uses built-in demo data (always live)
7
- 2. CONNECTED — if SLOTH_BACKEND_URL is set, calls your private backend (real engine)
8
  """
9
 
10
  import gradio as gr
11
  import requests
12
  import os
13
- import json
14
  import datetime
15
  import random
16
 
@@ -18,7 +16,6 @@ BACKEND_URL = os.environ.get("SLOTH_BACKEND_URL", "").strip()
18
  API_TOKEN = os.environ.get("SLOTH_API_TOKEN", "sloth-demo-token-2025")
19
  HEADERS = {"Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json"}
20
 
21
- # ── Built-in demo data (used when no backend is connected) ─────────────────
22
  DEMO_DB = {
23
  "malaysia": {
24
  "country": "Malaysia", "flag": "🇲🇾",
@@ -36,8 +33,8 @@ DEMO_DB = {
36
  "country": "Germany", "flag": "🇩🇪",
37
  "min_wage": {"amount": "EUR 12.41/hour", "effective": "2024-01-01", "status": "current"},
38
  "tax": {"corporate": "15% + solidarity", "personal_top": "45%", "last_updated": "2024-01-01"},
39
- "social_security": {"employer_total": "~20%", "employee_total": "~20%", "health": "14.6%"},
40
- "contractor_rules": "Strict bogus self-employment (Scheinselbständigkeit) laws. High misclassification risk.",
41
  "recent_changes": [
42
  {"date": "2024-01-01", "type": "Payroll", "summary": "Minimum wage increased to EUR 12.41/hour", "severity": "high"},
43
  {"date": "2024-02-15", "type": "Compliance", "summary": "New Works Council AI consultation requirements", "severity": "medium"},
@@ -49,10 +46,10 @@ DEMO_DB = {
49
  "min_wage": {"amount": "BRL 1,412/month", "effective": "2024-01-01", "status": "current"},
50
  "tax": {"corporate": "15-25%", "personal_top": "27.5%", "last_updated": "2024-01-01"},
51
  "social_security": {"inss_employer": "20%", "inss_employee": "7.5-14%", "fgts": "8%"},
52
- "contractor_rules": "CLT employment strongly preferred by courts. PJ contractor arrangements under scrutiny.",
53
  "recent_changes": [
54
  {"date": "2024-01-01", "type": "Payroll", "summary": "Minimum wage raised to BRL 1,412", "severity": "high"},
55
- {"date": "2024-02-01", "type": "Compliance", "summary": "Updated eSocial reporting v3.1 mandatory", "severity": "high"},
56
  {"date": "2024-03-10", "type": "Tax", "summary": "New Simples Nacional thresholds for 2024", "severity": "medium"},
57
  ],
58
  "expansion_score": 61, "eor_available": True,
@@ -62,7 +59,7 @@ DEMO_DB = {
62
  "min_wage": {"amount": "No statutory minimum wage", "effective": "N/A", "status": "info"},
63
  "tax": {"corporate": "17%", "personal_top": "24%", "last_updated": "2024-04-01"},
64
  "social_security": {"cpf_employer": "17%", "cpf_employee": "20%", "applies_to": "Citizens/PRs only"},
65
- "contractor_rules": "Clear contractor framework. Employment Act amended 2021 covers most workers.",
66
  "recent_changes": [
67
  {"date": "2024-04-01", "type": "Tax", "summary": "GST increased to 9% from 8%", "severity": "medium"},
68
  {"date": "2024-01-01", "type": "Compliance", "summary": "Flexible work arrangement guidelines effective", "severity": "low"},
@@ -74,7 +71,7 @@ DEMO_DB = {
74
  "min_wage": {"amount": "USD 7.25/hour federal (state rates vary)", "effective": "2009-07-24", "status": "outdated"},
75
  "tax": {"corporate": "21%", "personal_top": "37%", "last_updated": "2024-01-01"},
76
  "social_security": {"fica_employer": "7.65%", "fica_employee": "7.65%", "futa": "0.6%"},
77
- "contractor_rules": "ABC test or economic realities test varies by state. California AB5 most restrictive.",
78
  "recent_changes": [
79
  {"date": "2024-01-01", "type": "Compliance", "summary": "Corporate Transparency Act BOI reporting effective", "severity": "high"},
80
  {"date": "2024-04-23", "type": "Labor", "summary": "DOL new overtime threshold rule finalized", "severity": "high"},
@@ -83,13 +80,13 @@ DEMO_DB = {
83
  },
84
  "india": {
85
  "country": "India", "flag": "🇮🇳",
86
- "min_wage": {"amount": "INR 176423/day (varies by state & skill)", "effective": "2024-04-01", "status": "current"},
87
  "tax": {"corporate": "22% (domestic)", "personal_top": "30%", "last_updated": "2024-04-01"},
88
  "social_security": {"epf_employer": "12%", "epf_employee": "12%", "esi": "3.25% employer"},
89
- "contractor_rules": "Contract Labour Act regulates use of contractors. Principal employer liability applies.",
90
  "recent_changes": [
91
- {"date": "2024-04-01", "type": "Tax", "summary": "New TDS provisions for online gaming / crypto gains", "severity": "medium"},
92
- {"date": "2024-01-15", "type": "Compliance", "summary": "EPFO UAN autogeneration now mandatory for new hires", "severity": "medium"},
93
  ],
94
  "expansion_score": 68, "eor_available": True,
95
  },
@@ -98,10 +95,9 @@ DEMO_DB = {
98
  "min_wage": {"amount": "JPY 1,004/hour (national avg)", "effective": "2023-10-01", "status": "current"},
99
  "tax": {"corporate": "23.2%", "personal_top": "45%", "last_updated": "2024-01-01"},
100
  "social_security": {"health_employer": "5.0%", "pension_employer": "9.15%", "employment_insurance": "0.95%"},
101
- "contractor_rules": "Worker dispatch laws strictly regulate contractor use. Misclassification fines are heavy.",
102
  "recent_changes": [
103
  {"date": "2024-04-01", "type": "Labor", "summary": "Overtime disclosure rules for large firms effective", "severity": "medium"},
104
- {"date": "2024-01-01", "type": "Compliance", "summary": "Childcare leave promotion requirements strengthened", "severity": "low"},
105
  ],
106
  "expansion_score": 78, "eor_available": True,
107
  },
@@ -110,7 +106,7 @@ DEMO_DB = {
110
  "min_wage": {"amount": "AUD 23.23/hour", "effective": "2023-07-01", "status": "current"},
111
  "tax": {"corporate": "30% (25% for small business)", "personal_top": "45%", "last_updated": "2024-07-01"},
112
  "social_security": {"superannuation_employer": "11%", "medicare_levy": "2%"},
113
- "contractor_rules": "High misclassification risk post-2022 High Court rulings. Substance over form test applies.",
114
  "recent_changes": [
115
  {"date": "2024-07-01", "type": "Payroll", "summary": "Superannuation rate increases to 11.5%", "severity": "high"},
116
  {"date": "2024-01-01", "type": "Labor", "summary": "Right to disconnect legislation effective", "severity": "medium"},
@@ -135,13 +131,12 @@ RATING_ICON = {"High": "🟢", "Medium": "🟡", "Low": "🔴"}
135
 
136
 
137
  def query_standalone(query: str) -> dict:
138
- """Query the built-in demo database."""
139
  raw = query.lower()
140
  for key, data in DEMO_DB.items():
141
  if key in raw:
142
  return {
143
  "status": "success",
144
- "queried_at": datetime.datetime.utcnow().isoformat() + "Z",
145
  "source": "Sloth Intelligence Engine v1.0-MVP (Demo)",
146
  "country": data["country"], "flag": data["flag"],
147
  "summary": {
@@ -161,45 +156,31 @@ def query_standalone(query: str) -> dict:
161
  }
162
  return {
163
  "status": "no_match",
164
- "message": f"No data found for: '{query}'",
165
  "available_countries": [v["country"] for v in DEMO_DB.values()],
166
  }
167
 
168
 
169
- def call_backend(query: str) -> dict:
170
- """Call the connected private backend."""
171
- try:
172
- resp = requests.post(
173
- f"{BACKEND_URL}/query",
174
- headers=HEADERS,
175
- json={"query": query},
176
- timeout=15,
177
- )
178
- resp.raise_for_status()
179
- return resp.json()
180
- except requests.exceptions.ConnectionError:
181
- return {"status": "error", "message": "⚠ Backend unreachable. Falling back to demo data.", "fallback": True}
182
- except Exception as e:
183
- return {"status": "error", "message": f"⚠ Backend error: {str(e)}", "fallback": True}
184
-
185
-
186
  def get_data(query: str) -> dict:
187
  if BACKEND_URL:
188
- result = call_backend(query)
189
- if result.get("fallback"):
190
- return query_standalone(query)
191
- return result
 
 
 
 
 
192
  return query_standalone(query)
193
 
194
 
195
  def format_result(data: dict) -> tuple:
196
  if data.get("status") == "error":
197
  return data.get("message", "Unknown error"), "", "", ""
198
-
199
  if data.get("status") == "no_match":
200
  avail = ", ".join(data.get("available_countries", []))
201
- return f"**No match for that query.**\n\nCurrently monitoring: {avail}", "", "", ""
202
-
203
  if data.get("status") != "success":
204
  return "Unexpected response.", "", "", ""
205
 
@@ -208,421 +189,128 @@ def format_result(data: dict) -> tuple:
208
  ts = data.get("queried_at", "")[:19].replace("T", " ")
209
  alerts = data.get("alert_count", 0)
210
  sources = data.get("monitored_sources", 0)
211
- mode = " · Demo mode" if "Demo" in data.get("source", "") else ""
212
-
213
- header = f"# {flag} {country}\n*Queried {ts} UTC · {sources} sources monitored · {alerts} recent alerts{mode}*\n---"
214
-
215
- s = data.get("summary", {})
216
- mw = s.get("minimum_wage", {})
217
- tx = s.get("tax_rates", {})
218
- ss = s.get("social_security", {})
219
- cr = s.get("contractor_classification", "N/A")
220
- wage_status = {"current": "✅ Current", "outdated": "⚠ Outdated", "info": "ℹ Info"}.get(mw.get("status", ""), "")
221
-
222
- summary = (
223
- f"## 📋 Compliance Summary\n\n"
224
- f"**Minimum Wage** \n{mw.get('amount','N/A')} — effective {mw.get('effective','N/A')} {wage_status}\n\n"
225
- f"**Tax Rates** \n- Corporate: `{tx.get('corporate','N/A')}`\n- Personal (top): `{tx.get('personal_top','N/A')}`\n- Last updated: {tx.get('last_updated','N/A')}\n\n"
226
- f"**Social Security** \n" + "\n".join(f"- {k.replace('_',' ').title()}: `{v}`" for k, v in ss.items()) +
227
- f"\n\n**Contractor Classification** \n{cr}"
228
- )
229
-
230
- changes = data.get("recent_regulatory_changes", [])
231
- if changes:
232
- rows = "\n".join(
233
- f"| {SEVERITY_ICON.get(c.get('severity','low'),'⚪')} | {c.get('date','')} | {c.get('type','')} | {c.get('summary','')} |"
234
- for c in changes
235
- )
236
- changes_md = f"## ⚡ Recent Regulatory Changes\n\n| Sev | Date | Type | Summary |\n|---|---|---|---|\n{rows}"
237
- else:
238
- changes_md = "## ⚡ Recent Regulatory Changes\n\nNo recent changes detected."
239
-
240
- er = data.get("expansion_readiness", {})
241
- score = er.get("score", 0)
242
- rating = er.get("rating", "N/A")
243
- eor = "✅ Available" if er.get("eor_available") else "❌ Not available"
244
- bar = "█" * (score // 10) + "░" * (10 - score // 10)
245
- expansion = (
246
- f"## 🌍 Expansion Readiness\n\n"
247
- f"**Score: {score}/100** {RATING_ICON.get(rating,'⚪')} {rating}\n\n"
248
- f"`{bar}` {score}%\n\n**EOR:** {eor}"
249
- )
250
-
251
- return header, summary, changes_md, expansion
252
-
253
-
254
- def run_query(query: str):
255
- if not query.strip():
256
- return "Enter a country or compliance question above.", "", "", ""
257
- return format_result(get_data(query))
258
-
259
-
260
- CSS = """
261
- body, .gradio-container { font-family: 'Inter', system-ui, sans-serif !important; background: #F8FAFC !important; }
262
- #header-banner { background: linear-gradient(135deg, #0B1F3A 0%, #12294D 60%, #0D9488 100%); border-radius: 12px; padding: 28px 32px 20px; margin-bottom: 16px; }
263
- #header-banner h1 { font-size: 2.4rem; font-weight: 700; letter-spacing: 0.1em; color: white !important; margin: 0 0 4px; }
264
- #header-banner p { color: #5DCAA5; font-size: 1rem; margin: 0; }
265
- textarea, input[type=text] { border: 1.5px solid #CBD5E1 !important; border-radius: 8px !important; background: white !important; }
266
- textarea:focus, input[type=text]:focus { border-color: #0D9488 !important; box-shadow: 0 0 0 3px rgba(13,148,136,0.15) !important; }
267
- button.primary { background: #0D9488 !important; border: none !important; border-radius: 8px !important; font-weight: 600 !important; color: white !important; }
268
- button.primary:hover { background: #0F6E56 !important; }
269
- .result-panel { background: white; border: 1px solid #E2E8F0; border-radius: 10px; padding: 20px; margin-top: 12px; }
270
- .markdown-body h1 { color: #0B1F3A !important; font-size: 1.5rem !important; }
271
- .markdown-body h2 { color: #0D9488 !important; font-size: 1.15rem !important; border-bottom: 1px solid #E2E8F0; padding-bottom: 4px; }
272
- .markdown-body code { background: #F0FDFA !important; color: #0B1F3A !important; border-radius: 4px; padding: 1px 5px; }
273
- .markdown-body table { border-collapse: collapse; width: 100%; }
274
- .markdown-body td, .markdown-body th { border: 1px solid #E2E8F0 !important; padding: 6px 12px !important; font-size: 0.9rem; }
275
- .markdown-body th { background: #F8FAFC !important; }
276
- """
277
-
278
- HEADER_HTML = """
279
- <div id="header-banner">
280
- <h1>🦥 SLOTH</h1>
281
- <p>AI Workforce Intelligence · Real-time compliance monitoring across 150+ countries</p>
282
- </div>
283
- """
284
-
285
- with gr.Blocks(css=CSS, title="Sloth — AI Workforce Intelligence") as demo:
286
- gr.HTML(HEADER_HTML)
287
-
288
- with gr.Row():
289
- with gr.Column(scale=5):
290
- query_box = gr.Textbox(
291
- placeholder="Try: 'Malaysia payroll compliance' or 'Germany minimum wage changes'",
292
- label="Ask Sloth", lines=2, max_lines=4,
293
- )
294
- with gr.Column(scale=1, min_width=120):
295
- gr.Markdown(" ")
296
- submit_btn = gr.Button("Run Query →", variant="primary")
297
-
298
- gr.Markdown("<div style='color:#64748B;font-size:0.85rem;margin-bottom:6px'>Quick examples:</div>")
299
- gr.Examples(examples=EXAMPLE_QUERIES, inputs=query_box, label="")
300
-
301
- with gr.Row():
302
- header_out = gr.Markdown(label="", elem_classes=["result-panel"])
303
- with gr.Row():
304
- with gr.Column(scale=1):
305
- summary_out = gr.Markdown(label="", elem_classes=["result-panel"])
306
- with gr.Column(scale=1):
307
- changes_out = gr.Markdown(label="", elem_classes=["result-panel"])
308
- with gr.Row():
309
- expansion_out = gr.Markdown(label="", elem_classes=["result-panel"])
310
-
311
- gr.Markdown(
312
- "<br><center style='color:#94A3B8;font-size:0.8rem'>"
313
- "Sloth MVP · 8 countries in demo · Full 150-country coverage in production · "
314
- "<a href='mailto:hello@sloth.ai' style='color:#0D9488'>hello@sloth.ai</a>"
315
- "</center>"
316
- )
317
-
318
- submit_btn.click(fn=run_query, inputs=query_box, outputs=[header_out, summary_out, changes_out, expansion_out])
319
- query_box.submit(fn=run_query, inputs=query_box, outputs=[header_out, summary_out, changes_out, expansion_out])
320
 
321
- if __name__ == "__main__":
322
- demo.launch(server_name="0.0.0.0", server_port=7860)
323
-
324
- HEADERS = {
325
- "Authorization": f"Bearer {API_TOKEN}",
326
- "Content-Type": "application/json",
327
- }
328
-
329
- EXAMPLE_QUERIES = [
330
- "What are Malaysia's payroll compliance requirements?",
331
- "Show me Germany's minimum wage and recent changes",
332
- "Brazil contractor classification rules",
333
- "Singapore expansion readiness report",
334
- "United States overtime compliance 2024",
335
- "India EPF and social security rates",
336
- ]
337
-
338
- SEVERITY_ICON = {"high": "🔴", "medium": "🟡", "low": "🟢"}
339
- RATING_ICON = {"High": "🟢", "Medium": "🟡", "Low": "🔴"}
340
-
341
-
342
- # ── API call ───────────────────────────────────────────────────────────────
343
-
344
- def call_backend(query: str) -> dict:
345
- try:
346
- resp = requests.post(
347
- f"{BACKEND_URL}/query",
348
- headers=HEADERS,
349
- json={"query": query},
350
- timeout=15,
351
- )
352
- resp.raise_for_status()
353
- return resp.json()
354
- except requests.exceptions.ConnectionError:
355
- return {"status": "error", "message": "⚠ Backend offline. Start your local server and set SLOTH_BACKEND_URL."}
356
- except requests.exceptions.Timeout:
357
- return {"status": "error", "message": "⚠ Request timed out. Check your backend URL."}
358
- except Exception as e:
359
- return {"status": "error", "message": f"⚠ Error: {str(e)}"}
360
-
361
-
362
- # ── Result formatter ───────────────────────────────────────────────────────
363
-
364
- def format_result(data: dict) -> tuple[str, str, str, str]:
365
- """Returns (header_md, summary_md, changes_md, expansion_md)"""
366
-
367
- if data.get("status") == "error":
368
- err = data.get("message", "Unknown error")
369
- return err, "", "", ""
370
-
371
- if data.get("status") == "no_match":
372
- msg = data.get("message", "No data found.")
373
- avail = ", ".join(c.title() for c in data.get("available_countries", []))
374
- return f"**{msg}**\n\nAvailable: {avail}", "", "", ""
375
-
376
- if data.get("status") != "success":
377
- return "Unexpected response from backend.", "", "", ""
378
-
379
- flag = data.get("flag", "")
380
- country = data.get("country", "")
381
- ts = data.get("queried_at", "")[:19].replace("T", " ")
382
- alerts = data.get("alert_count", 0)
383
- sources = data.get("monitored_sources", 0)
384
-
385
- # ── Header ──
386
  header = (
387
  f"# {flag} {country}\n"
388
- f"*Queried {ts} UTC · {sources} sources monitored · {alerts} recent alerts*\n"
389
- f"---"
390
  )
391
 
392
- # ── Summary ──
393
- s = data.get("summary", {})
394
- mw = s.get("minimum_wage", {})
395
- tx = s.get("tax_rates", {})
396
- ss = s.get("social_security", {})
397
- cr = s.get("contractor_classification", "N/A")
398
-
399
- wage_status = {"current": "✅ Current", "outdated": "⚠ Outdated", "info": "ℹ Info"}.get(mw.get("status", ""), "")
400
 
401
  summary = (
402
- f"## 📋 Compliance Summary\n\n"
403
- f"**Minimum Wage** \n"
404
- f"{mw.get('amount','N/A')} — effective {mw.get('effective','N/A')} {wage_status}\n\n"
405
- f"**Tax Rates** \n"
406
- f"- Corporate: `{tx.get('corporate','N/A')}`\n"
407
- f"- Personal (top): `{tx.get('personal_top','N/A')}`\n"
408
  f"- Last updated: {tx.get('last_updated','N/A')}\n\n"
409
- f"**Social Security** \n"
410
- + "\n".join(f"- {k.replace('_',' ').title()}: `{v}`" for k, v in ss.items()) +
411
- f"\n\n**Contractor Classification** \n{cr}"
412
  )
413
 
414
- # ── Recent changes ──
415
  changes = data.get("recent_regulatory_changes", [])
416
  if changes:
417
  rows = "\n".join(
418
- f"| {SEVERITY_ICON.get(c.get('severity','low'), '⚪')} {c.get('date','')} "
419
  f"| {c.get('type','')} | {c.get('summary','')} |"
420
  for c in changes
421
  )
422
  changes_md = (
423
- f"## ⚡ Recent Regulatory Changes\n\n"
424
- f"| Severity | Date | Type | Summary |\n"
425
- f"|---|---|---|---|\n"
426
- f"{rows}"
427
  )
428
  else:
429
  changes_md = "## ⚡ Recent Regulatory Changes\n\nNo recent changes detected."
430
 
431
- # ── Expansion readiness ──
432
- er = data.get("expansion_readiness", {})
433
  score = er.get("score", 0)
434
  rating = er.get("rating", "N/A")
435
  eor = "✅ Available" if er.get("eor_available") else "❌ Not available"
436
  bar = "█" * (score // 10) + "░" * (10 - score // 10)
437
 
438
  expansion = (
439
- f"## 🌍 Expansion Readiness\n\n"
440
  f"**Score: {score}/100** {RATING_ICON.get(rating,'⚪')} {rating}\n\n"
441
  f"`{bar}` {score}%\n\n"
442
- f"**EOR (Employer of Record):** {eor}"
443
  )
444
-
445
  return header, summary, changes_md, expansion
446
 
447
 
448
- # ── Main query function ────────────────────────────────────────────────────
449
-
450
  def run_query(query: str):
451
  if not query.strip():
452
  return "Enter a country or compliance question above.", "", "", ""
 
453
 
454
- data = call_backend(query)
455
- return format_result(data)
456
-
457
-
458
- # ── CSS theme — navy/teal matching pitch deck ──────────────────────────────
459
 
460
  CSS = """
461
- :root {
462
- --navy: #0B1F3A;
463
- --teal: #0D9488;
464
- --light: #F0FDFA;
465
- }
466
-
467
- body, .gradio-container {
468
- font-family: 'Inter', system-ui, sans-serif !important;
469
- background: #F8FAFC !important;
470
- }
471
-
472
- .gradio-container > .main > .wrap {
473
- max-width: 900px;
474
- margin: 0 auto;
475
- }
476
-
477
- /* Header banner */
478
  #header-banner {
479
  background: linear-gradient(135deg, #0B1F3A 0%, #12294D 60%, #0D9488 100%);
480
- border-radius: 12px;
481
- padding: 28px 32px 20px;
482
- margin-bottom: 16px;
483
- color: white;
484
  }
485
-
486
  #header-banner h1 {
487
- font-size: 2.4rem;
488
- font-weight: 700;
489
- letter-spacing: 0.1em;
490
- color: white !important;
491
- margin: 0 0 4px;
492
- }
493
-
494
- #header-banner p {
495
- color: #5DCAA5;
496
- font-size: 1rem;
497
- margin: 0;
498
- }
499
-
500
- /* Search box */
501
- textarea, input[type=text] {
502
- border: 1.5px solid #CBD5E1 !important;
503
- border-radius: 8px !important;
504
- font-size: 1rem !important;
505
- background: white !important;
506
- }
507
-
508
- textarea:focus, input[type=text]:focus {
509
- border-color: #0D9488 !important;
510
- box-shadow: 0 0 0 3px rgba(13,148,136,0.15) !important;
511
- }
512
-
513
- /* Buttons */
514
- button.primary {
515
- background: #0D9488 !important;
516
- border: none !important;
517
- border-radius: 8px !important;
518
- font-weight: 600 !important;
519
- font-size: 1rem !important;
520
- color: white !important;
521
- padding: 10px 28px !important;
522
- cursor: pointer !important;
523
- transition: background 0.2s !important;
524
- }
525
-
526
- button.primary:hover {
527
- background: #0F6E56 !important;
528
- }
529
-
530
- button.secondary {
531
- background: transparent !important;
532
- border: 1.5px solid #CBD5E1 !important;
533
- border-radius: 8px !important;
534
- color: #64748B !important;
535
- font-size: 0.9rem !important;
536
- cursor: pointer !important;
537
  }
538
-
539
- /* Result panels */
540
  .result-panel {
541
- background: white;
542
- border: 1px solid #E2E8F0;
543
- border-radius: 10px;
544
- padding: 20px;
545
- margin-top: 12px;
546
- box-shadow: 0 1px 3px rgba(0,0,0,0.06);
547
  }
548
-
549
- /* Markdown output */
550
- .markdown-body h1 { color: #0B1F3A !important; font-size: 1.5rem !important; }
551
- .markdown-body h2 { color: #0D9488 !important; font-size: 1.15rem !important; border-bottom: 1px solid #E2E8F0; padding-bottom: 4px; }
552
- .markdown-body code { background: #F0FDFA !important; color: #0B1F3A !important; border-radius: 4px; padding: 1px 5px; }
553
- .markdown-body table { border-collapse: collapse; width: 100%; }
554
- .markdown-body td, .markdown-body th { border: 1px solid #E2E8F0 !important; padding: 6px 12px !important; font-size: 0.9rem; }
555
- .markdown-body th { background: #F8FAFC !important; }
556
-
557
- /* Example pills */
558
- .example-label { color: #64748B; font-size: 0.85rem; margin-bottom: 6px; }
559
  """
560
 
561
  HEADER_HTML = """
562
  <div id="header-banner">
563
  <h1>🦥 SLOTH</h1>
564
- <p>AI Workforce Intelligence · Real-time compliance monitoring across 150+ countries</p>
565
  </div>
566
  """
567
 
568
- # ── Build Gradio UI ────────────────────────────────────────────────────────
569
-
570
- with gr.Blocks(css=CSS, title="Sloth — AI Workforce Intelligence") as demo:
571
-
572
  gr.HTML(HEADER_HTML)
573
 
574
  with gr.Row():
575
  with gr.Column(scale=5):
576
  query_box = gr.Textbox(
577
  placeholder="Try: 'Malaysia payroll compliance' or 'Germany minimum wage changes'",
578
- label="Ask Sloth",
579
- lines=2,
580
- max_lines=4,
581
  )
582
- with gr.Column(scale=1, min_width=120):
583
- gr.Markdown(" ")
584
  submit_btn = gr.Button("Run Query →", variant="primary")
585
 
586
- # Example queries
587
- gr.Markdown("<div class='example-label'>Quick examples:</div>")
588
- gr.Examples(
589
- examples=EXAMPLE_QUERIES,
590
- inputs=query_box,
591
- label="",
592
- )
593
 
594
- # Output panels
595
  with gr.Row():
596
- header_out = gr.Markdown(label="", elem_classes=["result-panel"])
597
-
598
  with gr.Row():
599
  with gr.Column(scale=1):
600
- summary_out = gr.Markdown(label="", elem_classes=["result-panel"])
601
  with gr.Column(scale=1):
602
- changes_out = gr.Markdown(label="", elem_classes=["result-panel"])
603
-
604
  with gr.Row():
605
- expansion_out = gr.Markdown(label="", elem_classes=["result-panel"])
606
 
607
- # Footer
608
  gr.Markdown(
609
- "<br><center style='color:#94A3B8;font-size:0.8rem'>"
610
- "Sloth MVP · Data sourced from 5,000+ government portals, labor law sites, and talent platforms · "
611
- "<a href='https://sloth.ai' style='color:#0D9488'>sloth.ai</a>"
 
612
  "</center>"
613
  )
614
 
615
- # Wire up
616
- submit_btn.click(
617
- fn=run_query,
618
- inputs=query_box,
619
- outputs=[header_out, summary_out, changes_out, expansion_out],
620
- )
621
- query_box.submit(
622
- fn=run_query,
623
- inputs=query_box,
624
- outputs=[header_out, summary_out, changes_out, expansion_out],
625
- )
626
 
627
  if __name__ == "__main__":
628
- demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
1
  """
2
  Sloth — AI Workforce Intelligence Demo
3
+ Hugging Face Spaces · Gradio 6 compatible
4
 
5
+ Standalone mode: works immediately with built-in demo data (no backend needed)
6
+ Connected mode: set SLOTH_BACKEND_URL secret to proxy to your private engine
 
7
  """
8
 
9
  import gradio as gr
10
  import requests
11
  import os
 
12
  import datetime
13
  import random
14
 
 
16
  API_TOKEN = os.environ.get("SLOTH_API_TOKEN", "sloth-demo-token-2025")
17
  HEADERS = {"Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json"}
18
 
 
19
  DEMO_DB = {
20
  "malaysia": {
21
  "country": "Malaysia", "flag": "🇲🇾",
 
33
  "country": "Germany", "flag": "🇩🇪",
34
  "min_wage": {"amount": "EUR 12.41/hour", "effective": "2024-01-01", "status": "current"},
35
  "tax": {"corporate": "15% + solidarity", "personal_top": "45%", "last_updated": "2024-01-01"},
36
+ "social_security": {"employer_total": "~20%", "employee_total": "~20%", "health_insurance": "14.6%"},
37
+ "contractor_rules": "Strict Scheinselbständigkeit laws. High misclassification risk.",
38
  "recent_changes": [
39
  {"date": "2024-01-01", "type": "Payroll", "summary": "Minimum wage increased to EUR 12.41/hour", "severity": "high"},
40
  {"date": "2024-02-15", "type": "Compliance", "summary": "New Works Council AI consultation requirements", "severity": "medium"},
 
46
  "min_wage": {"amount": "BRL 1,412/month", "effective": "2024-01-01", "status": "current"},
47
  "tax": {"corporate": "15-25%", "personal_top": "27.5%", "last_updated": "2024-01-01"},
48
  "social_security": {"inss_employer": "20%", "inss_employee": "7.5-14%", "fgts": "8%"},
49
+ "contractor_rules": "CLT employment strongly preferred by courts. PJ arrangements under scrutiny.",
50
  "recent_changes": [
51
  {"date": "2024-01-01", "type": "Payroll", "summary": "Minimum wage raised to BRL 1,412", "severity": "high"},
52
+ {"date": "2024-02-01", "type": "Compliance", "summary": "eSocial reporting v3.1 now mandatory", "severity": "high"},
53
  {"date": "2024-03-10", "type": "Tax", "summary": "New Simples Nacional thresholds for 2024", "severity": "medium"},
54
  ],
55
  "expansion_score": 61, "eor_available": True,
 
59
  "min_wage": {"amount": "No statutory minimum wage", "effective": "N/A", "status": "info"},
60
  "tax": {"corporate": "17%", "personal_top": "24%", "last_updated": "2024-04-01"},
61
  "social_security": {"cpf_employer": "17%", "cpf_employee": "20%", "applies_to": "Citizens/PRs only"},
62
+ "contractor_rules": "Clear framework. Employment Act 2021 covers most workers.",
63
  "recent_changes": [
64
  {"date": "2024-04-01", "type": "Tax", "summary": "GST increased to 9% from 8%", "severity": "medium"},
65
  {"date": "2024-01-01", "type": "Compliance", "summary": "Flexible work arrangement guidelines effective", "severity": "low"},
 
71
  "min_wage": {"amount": "USD 7.25/hour federal (state rates vary)", "effective": "2009-07-24", "status": "outdated"},
72
  "tax": {"corporate": "21%", "personal_top": "37%", "last_updated": "2024-01-01"},
73
  "social_security": {"fica_employer": "7.65%", "fica_employee": "7.65%", "futa": "0.6%"},
74
+ "contractor_rules": "ABC test varies by state. California AB5 most restrictive.",
75
  "recent_changes": [
76
  {"date": "2024-01-01", "type": "Compliance", "summary": "Corporate Transparency Act BOI reporting effective", "severity": "high"},
77
  {"date": "2024-04-23", "type": "Labor", "summary": "DOL new overtime threshold rule finalized", "severity": "high"},
 
80
  },
81
  "india": {
82
  "country": "India", "flag": "🇮🇳",
83
+ "min_wage": {"amount": "INR 176-423/day (varies by state & skill)", "effective": "2024-04-01", "status": "current"},
84
  "tax": {"corporate": "22% (domestic)", "personal_top": "30%", "last_updated": "2024-04-01"},
85
  "social_security": {"epf_employer": "12%", "epf_employee": "12%", "esi": "3.25% employer"},
86
+ "contractor_rules": "Contract Labour Act regulates use. Principal employer liability applies.",
87
  "recent_changes": [
88
+ {"date": "2024-04-01", "type": "Tax", "summary": "New TDS provisions for online gaming / crypto", "severity": "medium"},
89
+ {"date": "2024-01-15", "type": "Compliance", "summary": "EPFO UAN autogeneration mandatory for new hires", "severity": "medium"},
90
  ],
91
  "expansion_score": 68, "eor_available": True,
92
  },
 
95
  "min_wage": {"amount": "JPY 1,004/hour (national avg)", "effective": "2023-10-01", "status": "current"},
96
  "tax": {"corporate": "23.2%", "personal_top": "45%", "last_updated": "2024-01-01"},
97
  "social_security": {"health_employer": "5.0%", "pension_employer": "9.15%", "employment_insurance": "0.95%"},
98
+ "contractor_rules": "Worker dispatch laws strictly regulate contractor use. Misclassification fines heavy.",
99
  "recent_changes": [
100
  {"date": "2024-04-01", "type": "Labor", "summary": "Overtime disclosure rules for large firms effective", "severity": "medium"},
 
101
  ],
102
  "expansion_score": 78, "eor_available": True,
103
  },
 
106
  "min_wage": {"amount": "AUD 23.23/hour", "effective": "2023-07-01", "status": "current"},
107
  "tax": {"corporate": "30% (25% for small business)", "personal_top": "45%", "last_updated": "2024-07-01"},
108
  "social_security": {"superannuation_employer": "11%", "medicare_levy": "2%"},
109
+ "contractor_rules": "High misclassification risk post-2022 High Court rulings. Substance over form test.",
110
  "recent_changes": [
111
  {"date": "2024-07-01", "type": "Payroll", "summary": "Superannuation rate increases to 11.5%", "severity": "high"},
112
  {"date": "2024-01-01", "type": "Labor", "summary": "Right to disconnect legislation effective", "severity": "medium"},
 
131
 
132
 
133
  def query_standalone(query: str) -> dict:
 
134
  raw = query.lower()
135
  for key, data in DEMO_DB.items():
136
  if key in raw:
137
  return {
138
  "status": "success",
139
+ "queried_at": datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z"),
140
  "source": "Sloth Intelligence Engine v1.0-MVP (Demo)",
141
  "country": data["country"], "flag": data["flag"],
142
  "summary": {
 
156
  }
157
  return {
158
  "status": "no_match",
159
+ "message": f"No data for: '{query}'",
160
  "available_countries": [v["country"] for v in DEMO_DB.values()],
161
  }
162
 
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  def get_data(query: str) -> dict:
165
  if BACKEND_URL:
166
+ try:
167
+ resp = requests.post(
168
+ f"{BACKEND_URL}/query", headers=HEADERS,
169
+ json={"query": query}, timeout=15,
170
+ )
171
+ resp.raise_for_status()
172
+ return resp.json()
173
+ except Exception:
174
+ pass
175
  return query_standalone(query)
176
 
177
 
178
  def format_result(data: dict) -> tuple:
179
  if data.get("status") == "error":
180
  return data.get("message", "Unknown error"), "", "", ""
 
181
  if data.get("status") == "no_match":
182
  avail = ", ".join(data.get("available_countries", []))
183
+ return f"**No match.** \nCurrently monitoring: {avail}", "", "", ""
 
184
  if data.get("status") != "success":
185
  return "Unexpected response.", "", "", ""
186
 
 
189
  ts = data.get("queried_at", "")[:19].replace("T", " ")
190
  alerts = data.get("alert_count", 0)
191
  sources = data.get("monitored_sources", 0)
192
+ mode = " · Demo" if "Demo" in data.get("source", "") else ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  header = (
195
  f"# {flag} {country}\n"
196
+ f"*Queried {ts} UTC · {sources} sources monitored · {alerts} recent alerts{mode}*\n\n---"
 
197
  )
198
 
199
+ s = data.get("summary", {})
200
+ mw = s.get("minimum_wage", {})
201
+ tx = s.get("tax_rates", {})
202
+ ss = s.get("social_security", {})
203
+ cr = s.get("contractor_classification", "N/A")
204
+ ws = {"current": "✅ Current", "outdated": "⚠ Outdated", "info": "ℹ Info"}.get(mw.get("status", ""), "")
 
 
205
 
206
  summary = (
207
+ "## 📋 Compliance Summary\n\n"
208
+ f"**Minimum Wage** \n{mw.get('amount','N/A')} — effective {mw.get('effective','N/A')} {ws}\n\n"
209
+ "**Tax Rates** \n"
210
+ f"- Corporate: `{tx.get('corporate','N/A')}` \n"
211
+ f"- Personal (top rate): `{tx.get('personal_top','N/A')}` \n"
 
212
  f"- Last updated: {tx.get('last_updated','N/A')}\n\n"
213
+ "**Social Security** \n"
214
+ + "\n".join(f"- {k.replace('_',' ').title()}: `{v}`" for k, v in ss.items())
215
+ + f"\n\n**Contractor Classification** \n{cr}"
216
  )
217
 
 
218
  changes = data.get("recent_regulatory_changes", [])
219
  if changes:
220
  rows = "\n".join(
221
+ f"| {SEVERITY_ICON.get(c.get('severity','low'),'⚪')} | {c.get('date','')} "
222
  f"| {c.get('type','')} | {c.get('summary','')} |"
223
  for c in changes
224
  )
225
  changes_md = (
226
+ "## ⚡ Recent Regulatory Changes\n\n"
227
+ "| Sev | Date | Type | Summary |\n|---|---|---|---|\n" + rows
 
 
228
  )
229
  else:
230
  changes_md = "## ⚡ Recent Regulatory Changes\n\nNo recent changes detected."
231
 
232
+ er = data.get("expansion_readiness", {})
 
233
  score = er.get("score", 0)
234
  rating = er.get("rating", "N/A")
235
  eor = "✅ Available" if er.get("eor_available") else "❌ Not available"
236
  bar = "█" * (score // 10) + "░" * (10 - score // 10)
237
 
238
  expansion = (
239
+ "## 🌍 Expansion Readiness\n\n"
240
  f"**Score: {score}/100** {RATING_ICON.get(rating,'⚪')} {rating}\n\n"
241
  f"`{bar}` {score}%\n\n"
242
+ f"**Employer of Record (EOR):** {eor}"
243
  )
 
244
  return header, summary, changes_md, expansion
245
 
246
 
 
 
247
  def run_query(query: str):
248
  if not query.strip():
249
  return "Enter a country or compliance question above.", "", "", ""
250
+ return format_result(get_data(query))
251
 
 
 
 
 
 
252
 
253
  CSS = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  #header-banner {
255
  background: linear-gradient(135deg, #0B1F3A 0%, #12294D 60%, #0D9488 100%);
256
+ border-radius: 12px; padding: 28px 32px 20px; margin-bottom: 16px;
 
 
 
257
  }
 
258
  #header-banner h1 {
259
+ font-size: 2.4rem; font-weight: 700; letter-spacing: 0.1em;
260
+ color: white !important; margin: 0 0 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  }
262
+ #header-banner p { color: #5DCAA5; font-size: 1rem; margin: 0; }
 
263
  .result-panel {
264
+ background: white; border: 1px solid #E2E8F0;
265
+ border-radius: 10px; padding: 20px; margin-top: 12px;
 
 
 
 
266
  }
 
 
 
 
 
 
 
 
 
 
 
267
  """
268
 
269
  HEADER_HTML = """
270
  <div id="header-banner">
271
  <h1>🦥 SLOTH</h1>
272
+ <p>AI Workforce Intelligence &nbsp;·&nbsp; Real-time compliance monitoring across 150+ countries</p>
273
  </div>
274
  """
275
 
276
+ with gr.Blocks(title="Sloth AI Workforce Intelligence") as demo:
 
 
 
277
  gr.HTML(HEADER_HTML)
278
 
279
  with gr.Row():
280
  with gr.Column(scale=5):
281
  query_box = gr.Textbox(
282
  placeholder="Try: 'Malaysia payroll compliance' or 'Germany minimum wage changes'",
283
+ label="Ask Sloth", lines=2, max_lines=4,
 
 
284
  )
285
+ with gr.Column(scale=1, min_width=130):
286
+ gr.Markdown("&nbsp;")
287
  submit_btn = gr.Button("Run Query →", variant="primary")
288
 
289
+ gr.Markdown("<span style='color:#64748B;font-size:0.85rem'>Quick examples — click to load:</span>")
290
+ gr.Examples(examples=EXAMPLE_QUERIES, inputs=query_box, label="")
 
 
 
 
 
291
 
 
292
  with gr.Row():
293
+ header_out = gr.Markdown(elem_classes=["result-panel"])
 
294
  with gr.Row():
295
  with gr.Column(scale=1):
296
+ summary_out = gr.Markdown(elem_classes=["result-panel"])
297
  with gr.Column(scale=1):
298
+ changes_out = gr.Markdown(elem_classes=["result-panel"])
 
299
  with gr.Row():
300
+ expansion_out = gr.Markdown(elem_classes=["result-panel"])
301
 
 
302
  gr.Markdown(
303
+ "<center style='color:#94A3B8;font-size:0.8rem;margin-top:16px'>"
304
+ "Sloth MVP &nbsp;·&nbsp; 8 countries in demo &nbsp;·&nbsp; "
305
+ "Full 150-country coverage in production &nbsp;·&nbsp; "
306
+ "<a href='mailto:hello@sloth.ai' style='color:#0D9488'>hello@sloth.ai</a>"
307
  "</center>"
308
  )
309
 
310
+ submit_btn.click(fn=run_query, inputs=query_box,
311
+ outputs=[header_out, summary_out, changes_out, expansion_out])
312
+ query_box.submit(fn=run_query, inputs=query_box,
313
+ outputs=[header_out, summary_out, changes_out, expansion_out])
 
 
 
 
 
 
 
314
 
315
  if __name__ == "__main__":
316
+ demo.launch(server_name="0.0.0.0", server_port=7860, css=CSS, ssr_mode=False)