pebaryan commited on
Commit
00ce06b
Β·
1 Parent(s): 475ac07

feat: add ethics guardrails, verifier, beranda + kredit tabs

Browse files

- Added Nilai-nilai Demokrasi & HAM to all 4 agent system prompts
- Created ethics.py verifier (post-processing guardrail using SMALL LLM)
- Added ethics_verify call to all 4 Gradio agent wrappers
- Added Beranda tab with welcome, quick guide, and values framework
- Added Kredit tab with Taufik Basari quote and credentials
- Inpsired by feedback from @taufikbasari (DPR RI 2019-2024)

.gitignore CHANGED
@@ -19,3 +19,4 @@ output/
19
 
20
  # Claude Code per-user settings (project-level settings.json is fine to commit)
21
  .claude/settings.local.json
 
 
19
 
20
  # Claude Code per-user settings (project-level settings.json is fine to commit)
21
  .claude/settings.local.json
22
+ video/
app.py CHANGED
@@ -22,6 +22,7 @@ import gradio as gr
22
  from legawa.agents import analis_ruu, peneliti, penyusun, surat
23
  from legawa.tools.cache import CachingPasalClient
24
  from legawa.tools.pasal import PasalClient
 
25
 
26
  # ── Default HF Inference API config (zero-config demo) ──────────────────
27
  # Uses huggingface_hub's InferenceClient (works reliably on HF Spaces).
@@ -166,8 +167,10 @@ def agent_analyze(
166
  try:
167
  progress(0.3, desc="Menganalisis RUU...")
168
  result = analis_ruu.analyze(pool, pasal, source)
 
 
169
  progress(1.0, desc="Selesai!")
170
- return result.output
171
  except Exception as e:
172
  return f"**Error:** {e}"
173
  finally:
@@ -195,6 +198,8 @@ def agent_research(
195
  progress(0.2, desc="Ekspansi query...")
196
  progress(0.5, desc="Mencari peraturan...")
197
  output = peneliti.research(pool, pasal, topic)
 
 
198
  progress(1.0, desc="Selesai!")
199
  return output
200
  except Exception as e:
@@ -230,6 +235,8 @@ def agent_draft(
230
  with_research=with_research,
231
  extra_instructions=extra_instructions or None,
232
  )
 
 
233
  progress(1.0, desc="Selesai!")
234
  return output
235
  except Exception as e:
@@ -262,8 +269,11 @@ def agent_surat(
262
  pool, pasal, surat_text,
263
  verify_law=verify_law,
264
  )
 
 
 
265
  progress(1.0, desc="Selesai!")
266
- return surat.format_report(result)
267
  except Exception as e:
268
  return f"**Error:** {e}"
269
  finally:
@@ -368,7 +378,37 @@ def build_app() -> gr.Blocks:
368
  )
369
 
370
  with gr.Tabs():
371
- # ─── Tab 1: Analisis RUU ──────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  with gr.TabItem("πŸ“„ Analisis RUU"):
373
  gr.Markdown(
374
  "Upload atau tempel teks RUU untuk dianalisis pasal-per-pasal."
@@ -586,6 +626,32 @@ def build_app() -> gr.Blocks:
586
  outputs=[big_url, big_key, small_url, small_key, pasal_token, health_out],
587
  )
588
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  gr.Markdown(
590
  f"---\n"
591
  f"**Legawa** β€” *small models, big adventure* πŸ•οΈ | "
 
22
  from legawa.agents import analis_ruu, peneliti, penyusun, surat
23
  from legawa.tools.cache import CachingPasalClient
24
  from legawa.tools.pasal import PasalClient
25
+ from legawa.tools.ethics import ethics_verify
26
 
27
  # ── Default HF Inference API config (zero-config demo) ──────────────────
28
  # Uses huggingface_hub's InferenceClient (works reliably on HF Spaces).
 
167
  try:
168
  progress(0.3, desc="Menganalisis RUU...")
169
  result = analis_ruu.analyze(pool, pasal, source)
170
+ progress(0.8, desc="Verifikasi etika & HAM...")
171
+ output = ethics_verify(result.output, pool.small)
172
  progress(1.0, desc="Selesai!")
173
+ return output
174
  except Exception as e:
175
  return f"**Error:** {e}"
176
  finally:
 
198
  progress(0.2, desc="Ekspansi query...")
199
  progress(0.5, desc="Mencari peraturan...")
200
  output = peneliti.research(pool, pasal, topic)
201
+ progress(0.8, desc="Verifikasi etika & HAM...")
202
+ output = ethics_verify(output, pool.small)
203
  progress(1.0, desc="Selesai!")
204
  return output
205
  except Exception as e:
 
235
  with_research=with_research,
236
  extra_instructions=extra_instructions or None,
237
  )
238
+ progress(0.8, desc="Verifikasi etika & HAM...")
239
+ output = ethics_verify(output, pool.small)
240
  progress(1.0, desc="Selesai!")
241
  return output
242
  except Exception as e:
 
269
  pool, pasal, surat_text,
270
  verify_law=verify_law,
271
  )
272
+ output = surat.format_report(result)
273
+ progress(0.8, desc="Verifikasi etika & HAM...")
274
+ output = ethics_verify(output, pool.small)
275
  progress(1.0, desc="Selesai!")
276
+ return output
277
  except Exception as e:
278
  return f"**Error:** {e}"
279
  finally:
 
378
  )
379
 
380
  with gr.Tabs():
381
+ # ─── Tab 1: Beranda β€” Welcome + Quick Guide ────────────────
382
+ with gr.TabItem("🏠 Beranda"):
383
+ gr.Markdown(
384
+ "# πŸ›οΈ Selamat Datang di Legawa\n\n"
385
+ "**Asisten multi-agen untuk legislator Indonesia (DPR/DPRD).**\n\n"
386
+ "Legawa membantu Anda menganalisis RUU, mencari peraturan terkait, "
387
+ "menyusun naskah, dan membalas surat konstituen β€” semuanya dalam "
388
+ "hitungan menit.\n\n"
389
+ "---\n"
390
+ )
391
+ gr.Markdown(
392
+ "### πŸš€ Panduan Cepat\n\n"
393
+ "1. **πŸ“„ Analisis RUU** β€” Tempel teks RUU atau upload PDF, klik Analisis\n"
394
+ "2. **πŸ” Riset Hukum** β€” Cari peraturan Indonesia berdasarkan topik\n"
395
+ "3. **✍️ Draf Dokumen** β€” Buat pidato, naskah akademik, atau memo kebijakan\n"
396
+ "4. **πŸ“¬ Surat Konstituen** β€” Triase dan balas surat/email konstituen\n"
397
+ "5. **βš™οΈ Pengaturan** β€” Atur koneksi LLM dan token API\n\n"
398
+ "---\n"
399
+ )
400
+ gr.Markdown(
401
+ "### βš–οΈ Nilai-nilai Demokrasi & HAM\n\n"
402
+ "Setiap output Legawa diperiksa terhadap 4 pilar:\n"
403
+ "- **Kedaulatan Rakyat** β€” apakah keputusan berpihak pada rakyat?\n"
404
+ "- **Prinsip Demokrasi** β€” apakah checks and balances terjaga?\n"
405
+ "- **Hak Asasi Manusia** β€” apakah HAM dilindungi?\n"
406
+ "- **Etika Politik** β€” apakah ada do's and don'ts untuk legislator?\n\n"
407
+ "*Inisiatif ini terinspirasi dari masukan Taufik Basari, S.H., S.Hum., LL.M., "
408
+ "anggota DPR RI 2019–2024.*\n"
409
+ )
410
+
411
+ # ─── Tab 2: Analisis RUU ──────────────────────────────────
412
  with gr.TabItem("πŸ“„ Analisis RUU"):
413
  gr.Markdown(
414
  "Upload atau tempel teks RUU untuk dianalisis pasal-per-pasal."
 
626
  outputs=[big_url, big_key, small_url, small_key, pasal_token, health_out],
627
  )
628
 
629
+ # ─── Tab 6: Kredit β€” Attribution ──────────────────────────
630
+ with gr.TabItem("πŸ‘€ Kredit"):
631
+ gr.Markdown(
632
+ "### πŸ—£οΈ Masukan dari Legislator\n\n"
633
+ "Fitur **Nilai-nilai Demokrasi & HAM** dikembangkan berdasarkan "
634
+ "masukan dari:\n\n"
635
+ "**Taufik Basari, S.H., S.Hum., LL.M.**\n"
636
+ "*Anggota Dewan Perwakilan Rakyat Republik Indonesia*\n"
637
+ "*Masa jabatan: 1 Oktober 2019 – 30 September 2024*\n\n"
638
+ "> *\"AI agent nya mesti dilatih utk kasih do's and don'ts, "
639
+ "konsep kedaulatan rakyat, prinsip demokrasi dan HAM serta "
640
+ "mengingatkan pentingnya political ethics di setiap jawaban "
641
+ "yg diberikan. Jd kalau mau pake bahan dari AI, legislator "
642
+ "tsb harus sertakan jg nilai2 itu.\"\n"
643
+ "> β€” Taufik Basari, 29 Mei 2026*\n\n"
644
+ "---\n"
645
+ "[πŸ”— X/Twitter](https://x.com/taufikbasari) | "
646
+ "[Wikipedia](https://id.wikipedia.org/wiki/Taufik_Basari)\n\n"
647
+ "---\n"
648
+ "### πŸ›οΈ Legawa\n\n"
649
+ "*Small models, big adventure* πŸ•οΈ\n\n"
650
+ "Dibangun untuk [Build Small Hackathon](https://huggingface.co/build-small-hackathon) "
651
+ "oleh [@pebaryan](https://x.com/pebaryan).\n\n"
652
+ "Kode terbuka di [GitHub](https://github.com/pebaryan/Legawa).\n\n"
653
+ )
654
+
655
  gr.Markdown(
656
  f"---\n"
657
  f"**Legawa** β€” *small models, big adventure* πŸ•οΈ | "
src/legawa/agents/analis_ruu.py CHANGED
@@ -48,6 +48,13 @@ Aturan:
48
  - Gunakan tool secara agresif. Cari dahulu, sintesis kemudian. Lakukan setidaknya 3 pencarian
49
  untuk RUU bertema substansial.
50
  - Output akhir dalam format Markdown dengan heading yang jelas.
 
 
 
 
 
 
 
51
  """
52
 
53
 
 
48
  - Gunakan tool secara agresif. Cari dahulu, sintesis kemudian. Lakukan setidaknya 3 pencarian
49
  untuk RUU bertema substansial.
50
  - Output akhir dalam format Markdown dengan heading yang jelas.
51
+
52
+ Nilai-nilai Demokrasi & HAM β€” WAJIB dipertimbangkan dalam setiap analisis:
53
+ 1. **Kedaulatan Rakyat** β€” apakah RUU ini memperkuat atau melemahkan partisipasi publik?
54
+ 2. **Prinsip Demokrasi** β€” apakah mekanisme checks and balances terjaga?
55
+ 3. **Hak Asasi Manusia** β€” apakah ada pasal yang berpotensi melanggar HAM?
56
+ 4. **Etika Politik** β€” apa do's and don'ts yang perlu diingatkan ke legislator?
57
+ Sertakan poin-poin ini di bagian **Catatan untuk Legislator**.
58
  """
59
 
60
 
src/legawa/agents/peneliti.py CHANGED
@@ -57,6 +57,13 @@ Aturan:
57
  - Setiap klaim tentang isi peraturan harus berasal dari hasil pencarian; cantumkan frbr_uri.
58
  - Jangan mengarang nomor UU atau pasal yang tidak ada di hasil.
59
  - Bila hasil tidak cukup, katakan secara eksplisit di bagian "Celah".
 
 
 
 
 
 
 
60
  """
61
 
62
 
 
57
  - Setiap klaim tentang isi peraturan harus berasal dari hasil pencarian; cantumkan frbr_uri.
58
  - Jangan mengarang nomor UU atau pasal yang tidak ada di hasil.
59
  - Bila hasil tidak cukup, katakan secara eksplisit di bagian "Celah".
60
+
61
+ Nilai-nilai Demokrasi & HAM β€” WAJIB dipertimbangkan dalam setiap rekomendasi:
62
+ 1. **Kedaulatan Rakyat** β€” apakah peraturan ini berpihak pada kepentingan publik?
63
+ 2. **Prinsip Demokrasi** β€” apakah ada potensi abuse of power?
64
+ 3. **Hak Asasi Manusia** β€” identifikasi dampak HAM dari peraturan terkait.
65
+ 4. **Etika Politik** β€” berikan do's and don'ts untuk legislator.
66
+ Sertakan di bagian **Rekomendasi Tindak Lanjut**.
67
  """
68
 
69
 
src/legawa/agents/penyusun.py CHANGED
@@ -90,6 +90,12 @@ Aturan umum:
90
  - Jangan mengarang fakta hukum atau angka statistik. Jika tidak yakin, gunakan placeholder
91
  bersudut kurung siku (mis. [DATA BPS DAPIL]).
92
  - Output dalam Markdown.
 
 
 
 
 
 
93
  """
94
 
95
 
 
90
  - Jangan mengarang fakta hukum atau angka statistik. Jika tidak yakin, gunakan placeholder
91
  bersudut kurung siku (mis. [DATA BPS DAPIL]).
92
  - Output dalam Markdown.
93
+
94
+ Nilai-nilai Demokrasi & HAM β€” pastikan naskah mencerminkan:
95
+ 1. **Kedaulatan Rakyat** β€” apakah bahasa dan substansi berpihak pada kepentingan publik?
96
+ 2. **Prinsip Demokrasi** β€” hindari narasi yang melemahkan checks and balances.
97
+ 3. **Hak Asasi Manusia** β€” identifikasi dan lindungi hak-hak yang terdampak.
98
+ 4. **Etika Politik** β€” berikan do's and don'ts dalam rekomendasi kebijakan.
99
  """
100
 
101
 
src/legawa/agents/surat.py CHANGED
@@ -104,6 +104,12 @@ Pedoman:
104
  - Tanda tangan: "Hormat kami,\\n[NAMA ANGGOTA LEGISLATIF]\\n[FRAKSI/DAERAH PEMILIHAN]"
105
  - Panjang 250–450 kata. Output Markdown.
106
 
 
 
 
 
 
 
107
  {KOMISI_DPR_2024_2029}
108
  """
109
 
 
104
  - Tanda tangan: "Hormat kami,\\n[NAMA ANGGOTA LEGISLATIF]\\n[FRAKSI/DAERAH PEMILIHAN]"
105
  - Panjang 250–450 kata. Output Markdown.
106
 
107
+ Nilai-nilai Demokrasi & HAM β€” pastikan balasan mencerminkan:
108
+ 1. **Kedaulatan Rakyat** β€” hormati konstituen sebagai pemilik kedaulatan.
109
+ 2. **Prinsip Demokrasi** β€” tegaskan peran legislator sebagai wakil rakyat.
110
+ 3. **Hak Asasi Manusia** β€” tanggapi keluhan HAM dengan serius dan spesifik.
111
+ 4. **Etika Politik** β€” jangan buat janji berlebihan; beri ekspektasi realistis.
112
+
113
  {KOMISI_DPR_2024_2029}
114
  """
115
 
src/legawa/tools/ethics.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Ethics verifier β€” post-processing guardrails for democratic values & HAM.
2
+
3
+ Checks agent output against 4 democratic values (people's sovereignty, democracy,
4
+ human rights, political ethics) and appends a considerations section if missing.
5
+
6
+ Inspired by feedback from Taufik Basari, S.H., S.Hum., LL.M.
7
+ (DPR RI 2019–2024).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ VERIFIER_PROMPT = """\
15
+ Periksa apakah analisis berikut sudah mempertimbangkan 4 nilai demokrasi & HAM ini:
16
+ 1. **Kedaulatan Rakyat** β€” partisipasi publik, kepentingan rakyat
17
+ 2. **Prinsip Demokrasi** β€” checks and balances, abuse of power
18
+ 3. **Hak Asasi Manusia** β€” dampak HAM, hak-hak terdampak
19
+ 4. **Etika Politik** β€” do's and don'ts, transparansi, akuntabilitas
20
+
21
+ Jika KEEMPAT nilai sudah dibahas di dalam teks, balas hanya dengan: OK
22
+ Jika ADA yang belum dibahas, hasilkan bagian "### βš–οΈ Pertimbangan Etika & HAM"
23
+ dalam Bahasa Indonesia yang mencakup nilai-nilai yang kurang. Output markdown."""
24
+
25
+
26
+ def ethics_verify(
27
+ output: str,
28
+ llm: Any,
29
+ *,
30
+ temperature: float = 0.2,
31
+ max_tokens: int = 512,
32
+ ) -> str:
33
+ """Post-process agent output: append ethics considerations if missing.
34
+
35
+ Args:
36
+ output: The agent's raw markdown output.
37
+ llm: An object with a .chat() method (HFLLM or legawa LLM).
38
+
39
+ Returns:
40
+ Original output, possibly with an appended ethics section.
41
+ """
42
+ if not output.strip():
43
+ return output
44
+
45
+ try:
46
+ resp = llm.chat(
47
+ messages=[
48
+ {
49
+ "role": "system",
50
+ "content": VERIFIER_PROMPT,
51
+ },
52
+ {
53
+ "role": "user",
54
+ "content": (
55
+ "Berikut adalah output analisis yang sudah dihasilkan.\n"
56
+ "Periksa nilai-nilai demokrasi & HAM:\n\n"
57
+ f"{output[:4000]}"
58
+ ),
59
+ },
60
+ ],
61
+ temperature=temperature,
62
+ max_tokens=max_tokens,
63
+ )
64
+ text = (resp or "").strip()
65
+ if not text or text.upper().startswith("OK"):
66
+ return output # all values already covered
67
+
68
+ # Append the ethics section
69
+ return output.rstrip() + "\n\n---\n" + text
70
+
71
+ except Exception:
72
+ # Fail-safe: return original output on any error
73
+ return output
video/plan.md ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Legawa Explainer Video β€” Plan
2
+
3
+ ## Narrative Arc
4
+ Problem β†’ Solution β†’ Architecture β†’ Value
5
+
6
+ ## Scenes
7
+
8
+ ### Scene 1: The Problem (15s)
9
+ - Tall stack of papers labeled "RUU" piles up
10
+ - A stressed legislator figure with question mark
11
+ - Text: "Ratusan RUU setiap tahun... berhari-hari riset per dokumen"
12
+ - Exit: papers fade, revealing Legawa logo
13
+
14
+ ### Scene 2: What Legawa Does (25s)
15
+ - 4 agent cards appear one by one:
16
+ - πŸ“„ Analisis RUU β€” pasal-per-pasal analysis
17
+ - πŸ” Riset Hukum β€” search pasal.id
18
+ - ✍️ Draf Dokumen β€” draft speeches/policy memos
19
+ - πŸ“¬ Surat Konstituen β€” triage constituent letters
20
+ - Each card slides in with a brief label
21
+ - Final title: "Multi-Agent AI untuk Legislator"
22
+
23
+ ### Scene 3: How It Works (30s)
24
+ - Flow diagram from left to right:
25
+ 1. Input (RUU text / PDF)
26
+ 2. BIG LLM (Qwen3.5-27B) β€” synthesis & analysis
27
+ 3. SMALL LLM (Qwen3.5-9B) β€” classification & extraction
28
+ 4. pasal.id β€” legal database search
29
+ 5. Output β€” analysis with citations
30
+ - Animated arrows connect each box
31
+ - Final text appears: "Hasil lengkap dalam hitungan menit"
32
+
33
+ ### Scene 4: Free & Open (15s)
34
+ - Two tokens float in: HF Token + pasal.id Token
35
+ - Text: "Bawa token sendiri. Buka di GitHub."
36
+ - GitHub link + HF Space link at bottom
37
+ - Closing: "Legawa β€” small models, big adventure πŸ•οΈ"
38
+
39
+ ## Color Palette (Warm Academic)
40
+ | Role | Color | Hex |
41
+ |------|-------|-----|
42
+ | Background | Dark navy | `#1a1a2e` |
43
+ | Title text | Warm white | `#eaeaea` |
44
+ | Primary accent | Coral | `#ff6b6b` |
45
+ | Secondary | Gold | `#ffd93d` |
46
+ | Accent | Mint | `#6bcb77` |
47
+ | Muted | Grey | `#888888` |
48
+
49
+ ## Technical Notes
50
+ - No LaTeX available β€” use Text() with monospace font
51
+ - Resolution: 1280x720 (manim -qm)
52
+ - FPS: 30
53
+ - No audio/voiceover β€” text-based explainer with captions
54
+ - Each scene independently renderable
video/script.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Legawa Explainer Video β€” Manim CE script.
2
+
3
+ Scenes:
4
+ 1. The Problem β€” stack of RUU documents
5
+ 2. What Legawa Does β€” 4 agent cards
6
+ 3. How It Works β€” flow diagram
7
+ 4. Free & Open β€” tokens + links
8
+
9
+ Rendered with: manim -qm script.py Scene1_Problem Scene2_Agents Scene3_Flow Scene4_Closing
10
+ """
11
+
12
+ from manim import *
13
+
14
+ # ── Palette (Warm Academic) ──────────────────────────────────────────────
15
+ BG = "#1a1a2e"
16
+ TITLE = "#eaeaea"
17
+ BODY = "#cccccc"
18
+ PRIMARY = "#ff6b6b"
19
+ SECONDARY = "#ffd93d"
20
+ ACCENT = "#6bcb77"
21
+ MUTED = "#666666"
22
+ MONO = "monospace"
23
+
24
+ # ── Helpers ──────────────────────────────────────────────────────────────
25
+
26
+ def T(text, size=36, color=TITLE, weight=NORMAL):
27
+ """Shorthand for monospace Text."""
28
+ return Text(text, font_size=size, font=MONO, color=color, weight=weight)
29
+
30
+
31
+ def card_box(width=2.2, height=1.2, color=PRIMARY):
32
+ """Rounded rect for agent cards."""
33
+ return RoundedRectangle(
34
+ width=width, height=height,
35
+ corner_radius=0.15,
36
+ stroke_color=color, stroke_width=2.5,
37
+ fill_color=color, fill_opacity=0.12,
38
+ )
39
+
40
+
41
+ # ═══════════════════════════════════════════════════════════════════════════
42
+ # Scene 1 β€” The Problem
43
+ # ═══════════════════════════════════════════════════════════════════════════
44
+
45
+ class Scene1_Problem(Scene):
46
+ def construct(self):
47
+ self.camera.background_color = BG
48
+
49
+ # Papers piling up
50
+ papers = VGroup()
51
+ labels = ["RUU", "RUU", "RUU", "RUU", "RUU", "RUU"]
52
+ colors = [PRIMARY, SECONDARY, ACCENT, PRIMARY, SECONDARY, ACCENT]
53
+ for i, (label, c) in enumerate(zip(labels[:6], colors[:6])):
54
+ w, h = 2.6 - i * 0.15, 3.2 - i * 0.15
55
+ x_shift = i * 0.06
56
+ y_shift = i * 0.08 - 1.0
57
+ paper = Rectangle(
58
+ width=w, height=h,
59
+ stroke_color=c, stroke_width=2,
60
+ fill_color=c, fill_opacity=0.08,
61
+ ).shift(RIGHT * x_shift + UP * y_shift)
62
+ text = T(label, size=18, color=c).move_to(paper)
63
+ papers.add(paper, text)
64
+
65
+ self.play(
66
+ LaggedStart(*[FadeIn(p, shift=UP * 0.3, scale=0.9) for p in papers],
67
+ lag_ratio=0.12),
68
+ run_time=2.5,
69
+ )
70
+ self.wait(0.5)
71
+
72
+ # Stressed legislator text
73
+ stress = T("?", size=72, color=PRIMARY, weight=BOLD).shift(RIGHT * 3.2 + UP * 1.0)
74
+ self.play(Write(stress), run_time=0.5)
75
+ self.wait(0.3)
76
+
77
+ # Problem text above the stack
78
+ problem_text = T(
79
+ "Ratusan RUU setiap tahun...\nberhari-hari riset per dokumen",
80
+ size=26, color=BODY,
81
+ ).to_edge(UP, buff=0.5)
82
+ self.play(Write(problem_text), run_time=1.2)
83
+ self.wait(1.5)
84
+
85
+ # Fade out to Legawa logo
86
+ logo = T("πŸ›οΈ Legawa", size=56, color=SECONDARY, weight=BOLD)
87
+ subtitle = T("Multi-Agent AI untuk Legislator", size=24, color=MUTED).next_to(logo, DOWN)
88
+
89
+ self.play(
90
+ FadeOut(VGroup(*papers, stress, problem_text)),
91
+ run_time=0.8,
92
+ )
93
+ self.play(Write(logo), run_time=1.0)
94
+ self.play(Write(subtitle), run_time=0.8)
95
+ self.wait(1.5)
96
+ self.play(FadeOut(VGroup(logo, subtitle)), run_time=0.5)
97
+
98
+
99
+ # ═══════════════════════════════════════════════════════════════════════════
100
+ # Scene 2 β€” What Legawa Does
101
+ # ═══════════════════════════════════════════════════════════════════════════
102
+
103
+ class Scene2_Agents(Scene):
104
+ def construct(self):
105
+ self.camera.background_color = BG
106
+
107
+ title = T("Empat Agen dalam Satu Asisten", size=40, color=TITLE, weight=BOLD)
108
+ title.to_edge(UP, buff=0.5)
109
+ self.play(Write(title), run_time=0.8)
110
+ self.wait(0.3)
111
+
112
+ # 4 agent cards in a 2x2 grid
113
+ agents = [
114
+ ("πŸ“„ Analisis\nRUU", PRIMARY, "Analisis pasal-per-pasal\nlengkap sitasi"),
115
+ ("πŸ” Riset\nHukum", ACCENT, "Cari peraturan di\npasal.id"),
116
+ ("✍️ Draf\nDokumen", SECONDARY, "Pidato, naskah akademik,\nmemo kebijakan"),
117
+ ("πŸ“¬ Surat\nKonstituen", PRIMARY, "Triase & draft\nbalasan"),
118
+ ]
119
+
120
+ positions = [
121
+ LEFT * 3 + UP * 0.5,
122
+ RIGHT * 3 + UP * 0.5,
123
+ LEFT * 3 + DOWN * 1.8,
124
+ RIGHT * 3 + DOWN * 1.8,
125
+ ]
126
+
127
+ cards = VGroup()
128
+ for (emoji_name, color, desc), pos in zip(agents, positions):
129
+ box = RoundedRectangle(
130
+ width=4.5, height=2.2,
131
+ corner_radius=0.15,
132
+ stroke_color=color, stroke_width=2.5,
133
+ fill_color=color, fill_opacity=0.1,
134
+ ).move_to(pos)
135
+
136
+ name_text = T(emoji_name, size=30, color=color, weight=BOLD)
137
+ name_text.move_to(box).shift(UP * 0.3)
138
+
139
+ desc_text = T(desc, size=18, color=BODY)
140
+ desc_text.next_to(name_text, DOWN, buff=0.3)
141
+
142
+ cards.add(VGroup(box, name_text, desc_text))
143
+
144
+ # Animate in one by one
145
+ for card in cards:
146
+ self.play(FadeIn(card, shift=UP * 0.3, scale=0.95), run_time=0.6)
147
+ self.wait(0.15)
148
+
149
+ self.wait(1.0)
150
+
151
+ # Tagline
152
+ tag = T("Small models, big adventure πŸ•οΈ", size=24, color=SECONDARY)
153
+ tag.to_edge(DOWN, buff=0.4)
154
+ self.play(Write(tag), run_time=0.8)
155
+ self.wait(1.5)
156
+
157
+ self.play(FadeOut(VGroup(title, cards, tag)), run_time=0.5)
158
+
159
+
160
+ # ═══════════════════════════════════════════════════════════════════════════
161
+ # Scene 3 β€” How It Works (Flow Diagram)
162
+ # ═══════════════════════════════════════════════════════════════════════════
163
+
164
+ class Scene3_Flow(Scene):
165
+ def construct(self):
166
+ self.camera.background_color = BG
167
+
168
+ title = T("Bagaimana Cara Kerjanya?", size=36, color=TITLE, weight=BOLD)
169
+ title.to_edge(UP, buff=0.4)
170
+ self.play(Write(title), run_time=0.6)
171
+ self.wait(0.2)
172
+
173
+ def make_box(text_lines, color, w=2.2, h=1.0):
174
+ """Helper: box with centered text."""
175
+ box = RoundedRectangle(
176
+ width=w, height=h,
177
+ corner_radius=0.12,
178
+ stroke_color=color, stroke_width=2,
179
+ fill_color=color, fill_opacity=0.1,
180
+ )
181
+ txt = T(text_lines, size=18, color=TITLE)
182
+ txt.move_to(box)
183
+ return VGroup(box, txt)
184
+
185
+ def arrow_between(left_obj, right_obj, color=MUTED):
186
+ """Arrow from right edge of left_obj to left edge of right_obj."""
187
+ start = left_obj.get_right() + RIGHT * 0.15
188
+ end = right_obj.get_left() + LEFT * 0.15
189
+ arr = Arrow(
190
+ start, end,
191
+ stroke_width=2.5,
192
+ stroke_color=color,
193
+ max_tip_length_to_length_ratio=0.08,
194
+ )
195
+ return arr
196
+
197
+ # Row 1: Input
198
+ input_box = make_box("πŸ“„\nInput RUU", ACCENT, w=2.0, h=1.2)
199
+ input_box.shift(LEFT * 5.5 + UP * 0.5)
200
+
201
+ # Row 1: BIG LLM
202
+ big_box = make_box("🧠 BIG\nQwen3.5-27B", PRIMARY, w=2.2, h=1.2)
203
+ big_box.shift(LEFT * 2.0 + UP * 0.5)
204
+
205
+ # Row 1: pasal.id
206
+ pasal_box = make_box("πŸ“œ\npasal.id", SECONDARY, w=2.0, h=1.2)
207
+ pasal_box.shift(RIGHT * 1.2 + UP * 0.5)
208
+
209
+ # Row 2: SMALL LLM
210
+ small_box = make_box("🧠 SMALL\nQwen3.5-9B", PRIMARY, w=2.2, h=1.2)
211
+ small_box.shift(LEFT * 1.0 + DOWN * 1.3)
212
+
213
+ # Row 2: Output
214
+ output_box = make_box("βœ…\nHasil Analisis", ACCENT, w=2.0, h=1.2)
215
+ output_box.shift(RIGHT * 3.5 + DOWN * 1.3)
216
+
217
+ # Animate in
218
+ self.play(FadeIn(input_box, shift=RIGHT * 0.5), run_time=0.6)
219
+ self.wait(0.15)
220
+ self.play(FadeIn(big_box, shift=RIGHT * 0.5), run_time=0.6)
221
+ self.wait(0.15)
222
+
223
+ # Arrow: input β†’ BIG
224
+ arr1 = arrow_between(input_box, big_box)
225
+ self.play(Create(arr1), run_time=0.3)
226
+
227
+ self.play(FadeIn(pasal_box, shift=RIGHT * 0.5), run_time=0.6)
228
+ arr2 = arrow_between(big_box, pasal_box)
229
+ self.play(Create(arr2), run_time=0.3)
230
+ self.wait(0.3)
231
+
232
+ self.play(FadeIn(small_box, shift=UP * 0.5), run_time=0.6)
233
+ # Arrow: BIG β†’ SMALL (down-left)
234
+ arr3 = Arrow(
235
+ big_box.get_bottom() + DOWN * 0.15,
236
+ small_box.get_top() + UP * 0.15,
237
+ stroke_width=2.5, stroke_color=MUTED,
238
+ max_tip_length_to_length_ratio=0.08,
239
+ )
240
+ self.play(Create(arr3), run_time=0.3)
241
+
242
+ # Arrow: SMALL β†’ Output (right)
243
+ arr4 = Arrow(
244
+ small_box.get_right() + RIGHT * 0.15,
245
+ output_box.get_left() + LEFT * 0.15,
246
+ stroke_width=2.5, stroke_color=MUTED,
247
+ max_tip_length_to_length_ratio=0.08,
248
+ )
249
+ self.play(FadeIn(output_box, shift=RIGHT * 0.5), run_time=0.6)
250
+ self.play(Create(arr4), run_time=0.3)
251
+
252
+ self.wait(0.5)
253
+
254
+ # Arrow: pasal.id β†’ SMALL (feed back)
255
+ arr5 = Arrow(
256
+ pasal_box.get_bottom() + DOWN * 0.15,
257
+ small_box.get_right() + RIGHT * 0.5,
258
+ stroke_width=2, stroke_color=SECONDARY,
259
+ max_tip_length_to_length_ratio=0.08,
260
+ )
261
+ # Curve it by adding a path
262
+ self.play(Create(arr5), run_time=0.4)
263
+
264
+ # Value text
265
+ value = T("Hasil lengkap dalam hitungan menit ⚑", size=28, color=SECONDARY, weight=BOLD)
266
+ value.to_edge(DOWN, buff=0.5)
267
+ self.play(Write(value), run_time=0.8)
268
+ self.wait(2.0)
269
+
270
+ self.play(FadeOut(VGroup(
271
+ title, input_box, big_box, pasal_box, small_box, output_box,
272
+ arr1, arr2, arr3, arr4, arr5, value,
273
+ )), run_time=0.5)
274
+
275
+
276
+ # ═══════════════════════════════════════════════════════════════════════════
277
+ # Scene 4 β€” Free & Open (Closing)
278
+ # ═══════════════════════════════════════════════════════════════════════════
279
+
280
+ class Scene4_Closing(Scene):
281
+ def construct(self):
282
+ self.camera.background_color = BG
283
+
284
+ # Token icons β€” spread wider
285
+ hf_token = T("πŸ”‘ HF Token", size=32, color=PRIMARY, weight=BOLD)
286
+ pasal_token = T("πŸ“œ pasal.id Token", size=32, color=SECONDARY, weight=BOLD)
287
+
288
+ hf_token.shift(LEFT * 3.5 + UP * 0.8)
289
+ pasal_token.shift(RIGHT * 3.5 + UP * 0.8)
290
+
291
+ self.play(FadeIn(hf_token, shift=UP * 0.5), run_time=0.6)
292
+ self.wait(0.2)
293
+ self.play(FadeIn(pasal_token, shift=UP * 0.5), run_time=0.6)
294
+ self.wait(0.3)
295
+
296
+ # Tagline β€” centered below both tokens
297
+ byo = T("Bawa token sendiri. Buka di GitHub.", size=26, color=BODY)
298
+ byo.shift(DOWN * 0.5)
299
+ self.play(Write(byo), run_time=0.8)
300
+ self.wait(0.3)
301
+
302
+ # Links
303
+ gh = T("github.com/pebaryan/Legawa", size=20, color=ACCENT)
304
+ hf = T("build-small-hackathon/legawa", size=20, color=ACCENT)
305
+ gh.to_edge(DOWN, buff=0.8)
306
+ hf.next_to(gh, DOWN, buff=0.2)
307
+
308
+ self.play(
309
+ Write(gh), Write(hf),
310
+ run_time=0.6,
311
+ )
312
+ self.wait(0.5)
313
+
314
+ # Final brand
315
+ final = T("πŸ›οΈ Legawa β€” small models, big adventure πŸ•οΈ", size=34, color=SECONDARY, weight=BOLD)
316
+ final.to_edge(UP, buff=1.5)
317
+ self.play(Write(final), run_time=1.0)
318
+ self.wait(2.0)
319
+
320
+ # Clean exit
321
+ self.play(*[FadeOut(mob) for mob in self.mobjects], run_time=0.5)