KayO Codex commited on
Commit
34f7072
·
1 Parent(s): 3dbcb72

Switch default Qwen model and collapse Q&A snippets

Browse files
README.md CHANGED
@@ -10,7 +10,7 @@ app_file: app.py
10
  pinned: false
11
  short_description: Simplify complex legislation that affects you!
12
  models:
13
- - Qwen/Qwen3-32B
14
  ---
15
 
16
  # Legislation Explainer
@@ -29,13 +29,9 @@ It is created for the Hugging Face Build Small Hackathon under the `Backyard AI`
29
 
30
  - Track: `Backyard AI`
31
  - Real user: Ghanaian citizens and digital-policy stakeholders who need a clearer view of a bill's practical effects.
32
- - Small-model constraint: default provider is `Qwen/Qwen3-32B:cheapest` through the Hugging Face router, staying within the hackathon's `<= 32B` model cap. I could go lower than 32B, but since this deals with legal documents I want the best cognition possible.
33
  - Required surface: Gradio app, ready for Hugging Face Spaces through `app.py`.
34
- <<<<<<< HEAD
35
- - Submission assets still needed: Space link, short demo video, and social post.
36
- =======
37
  - GitHub repo: https://github.com/KayO-GH/legislation-explainer
38
- >>>>>>> hf
39
 
40
  ## What It Does
41
 
@@ -52,7 +48,7 @@ It is created for the Hugging Face Build Small Hackathon under the `Backyard AI`
52
 
53
  ## Model And Provider Notes
54
 
55
- The hackathon-safe default is Qwen3 32B through the Hugging Face router. Bring-your-own provider support is a proposed future expansion and is currently commented out so the hackathon build stays focused on one documented `<= 32B` model path.
56
 
57
  For future expansion, we will have a bring your own provider setting allowing users to connect to other models eg. from OpenAI, ANthropic, etc. if they so wish.
58
 
 
10
  pinned: false
11
  short_description: Simplify complex legislation that affects you!
12
  models:
13
+ - Qwen/Qwen3-14B
14
  ---
15
 
16
  # Legislation Explainer
 
29
 
30
  - Track: `Backyard AI`
31
  - Real user: Ghanaian citizens and digital-policy stakeholders who need a clearer view of a bill's practical effects.
32
+ - Small-model constraint: default provider is `Qwen/Qwen3-14B:cheapest` through the Hugging Face router, staying within the hackathon's `<= 32B` model cap while giving a better speed/cost tradeoff for this app.
33
  - Required surface: Gradio app, ready for Hugging Face Spaces through `app.py`.
 
 
 
34
  - GitHub repo: https://github.com/KayO-GH/legislation-explainer
 
35
 
36
  ## What It Does
37
 
 
48
 
49
  ## Model And Provider Notes
50
 
51
+ The hackathon-safe default is Qwen3 14B through the Hugging Face router. Bring-your-own provider support is a proposed future expansion and is currently commented out so the hackathon build stays focused on one documented `<= 32B` model path.
52
 
53
  For future expansion, we will have a bring your own provider setting allowing users to connect to other models eg. from OpenAI, ANthropic, etc. if they so wish.
54
 
app.py CHANGED
@@ -383,6 +383,25 @@ def _format_analysis(analysis: AnalysisResult | None) -> str:
383
  )
384
 
385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  def _format_chat_entry(answer_text: str, citations: list[dict[str, Any]], *, provenance: str = "analysis_based") -> str:
387
  prefix = ""
388
  if provenance == "analysis_based":
@@ -392,8 +411,7 @@ def _format_chat_entry(answer_text: str, citations: list[dict[str, Any]], *, pro
392
 
393
  if not citations:
394
  return prefix + answer_text
395
- citations_text = "\n".join(f"- [ref{item['ref_id']}] {item['snippet']}" for item in citations)
396
- return f"{prefix}{answer_text}\n\nSupporting snippets:\n{citations_text}"
397
 
398
 
399
  def _stream_chat_entry(
@@ -403,13 +421,21 @@ def _stream_chat_entry(
403
  *,
404
  provenance: str = "analysis_based",
405
  ):
406
- formatted_answer = _format_chat_entry(answer_text, citations, provenance=provenance)
 
 
 
 
 
 
407
  streamed_answer = ""
408
  for character in formatted_answer:
409
  streamed_answer += character
410
  yield base_history + [{"role": "assistant", "content": streamed_answer}]
411
  if CHAT_STREAM_DELAY_SECONDS:
412
  time.sleep(CHAT_STREAM_DELAY_SECONDS)
 
 
413
 
414
 
415
  def _deeper_answer_updates(visible: bool, label: str = "Run deeper full-document answer") -> gr.update:
 
383
  )
384
 
385
 
386
+ def _render_supporting_snippets(citations: list[dict[str, Any]]) -> str:
387
+ if not citations:
388
+ return ""
389
+
390
+ items = []
391
+ for item in citations:
392
+ ref_id = html.escape(str(item["ref_id"]))
393
+ snippet = html.escape(item["snippet"])
394
+ items.append(f"<li><strong>[ref{ref_id}]</strong> {snippet}</li>")
395
+
396
+ count = len(citations)
397
+ label = "snippet" if count == 1 else "snippets"
398
+ return (
399
+ f"<details><summary>Supporting {label} ({count})</summary>"
400
+ f"<ul>{''.join(items)}</ul>"
401
+ "</details>"
402
+ )
403
+
404
+
405
  def _format_chat_entry(answer_text: str, citations: list[dict[str, Any]], *, provenance: str = "analysis_based") -> str:
406
  prefix = ""
407
  if provenance == "analysis_based":
 
411
 
412
  if not citations:
413
  return prefix + answer_text
414
+ return f"{prefix}{answer_text}\n\n{_render_supporting_snippets(citations)}"
 
415
 
416
 
417
  def _stream_chat_entry(
 
421
  *,
422
  provenance: str = "analysis_based",
423
  ):
424
+ prefix = ""
425
+ if provenance == "analysis_based":
426
+ prefix = "_Based on the summary and analysis._\n\n"
427
+ elif provenance == "full_document":
428
+ prefix = "_Deeper full-document answer._\n\n"
429
+
430
+ formatted_answer = prefix + answer_text
431
  streamed_answer = ""
432
  for character in formatted_answer:
433
  streamed_answer += character
434
  yield base_history + [{"role": "assistant", "content": streamed_answer}]
435
  if CHAT_STREAM_DELAY_SECONDS:
436
  time.sleep(CHAT_STREAM_DELAY_SECONDS)
437
+ if citations:
438
+ yield base_history + [{"role": "assistant", "content": _format_chat_entry(answer_text, citations, provenance=provenance)}]
439
 
440
 
441
  def _deeper_answer_updates(visible: bool, label: str = "Run deeper full-document answer") -> gr.update:
assets/example_bills/cybersecurity-amendment-bill-2025/metadata.json CHANGED
@@ -1,10 +1,10 @@
1
  {
2
  "generated_at": "2026-06-01T11:48:29.371678+00:00",
3
  "provider": "qwen",
4
- "model": "Qwen/Qwen3-32B:cheapest",
5
  "source_url": "https://www.csa.gov.gh/resources/Cybersecurity%20%28Amendment%29%20Draft%20Bill%202025%20final%2015102025.pdf",
6
  "document_hash": "d879a4473b3631906b7c4e965195d53f4693a722ce7644e187ab76fca33ef763",
7
  "chunk_size": 350,
8
  "chunk_overlap": 60,
9
  "embedding_model": "sentence-transformers/all-MiniLM-L6-v2"
10
- }
 
1
  {
2
  "generated_at": "2026-06-01T11:48:29.371678+00:00",
3
  "provider": "qwen",
4
+ "model": "Qwen/Qwen3-14B:cheapest",
5
  "source_url": "https://www.csa.gov.gh/resources/Cybersecurity%20%28Amendment%29%20Draft%20Bill%202025%20final%2015102025.pdf",
6
  "document_hash": "d879a4473b3631906b7c4e965195d53f4693a722ce7644e187ab76fca33ef763",
7
  "chunk_size": 350,
8
  "chunk_overlap": 60,
9
  "embedding_model": "sentence-transformers/all-MiniLM-L6-v2"
10
+ }
config.py CHANGED
@@ -34,7 +34,7 @@ warnings.filterwarnings(
34
 
35
  SUPPORTED_PROVIDERS = ["qwen", "openai", "anthropic", "gemini", "cohere"]
36
  DEFAULT_PROVIDER: str = "qwen"
37
- DEFAULT_QWEN_MODEL = "Qwen/Qwen3-32B:cheapest"
38
  DEFAULT_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
39
  OPENAI_REASONING_EFFORT = "medium"
40
  ANTHROPIC_THINKING_BUDGET = 2048
@@ -90,7 +90,7 @@ PROVIDER_METADATA: list[ProviderConfig] = [
90
  ProviderConfig(
91
  name="qwen",
92
  key_prefix=None,
93
- display_name="Qwen3 32B",
94
  instructions=(
95
  "Use your Hugging Face token for the router-backed Qwen model. Leave blank to use HF_TOKEN from .env if configured."
96
  ),
 
34
 
35
  SUPPORTED_PROVIDERS = ["qwen", "openai", "anthropic", "gemini", "cohere"]
36
  DEFAULT_PROVIDER: str = "qwen"
37
+ DEFAULT_QWEN_MODEL = "Qwen/Qwen3-14B:cheapest"
38
  DEFAULT_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
39
  OPENAI_REASONING_EFFORT = "medium"
40
  ANTHROPIC_THINKING_BUDGET = 2048
 
90
  ProviderConfig(
91
  name="qwen",
92
  key_prefix=None,
93
+ display_name="Qwen3 14B",
94
  instructions=(
95
  "Use your Hugging Face token for the router-backed Qwen model. Leave blank to use HF_TOKEN from .env if configured."
96
  ),
tests/test_app.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app import _format_chat_entry, _stream_chat_entry
2
+
3
+
4
+ def test_format_chat_entry_wraps_supporting_snippets_in_details() -> None:
5
+ result = _format_chat_entry(
6
+ "Answer text",
7
+ [{"ref_id": 1, "snippet": "Quoted <clause> & detail"}],
8
+ )
9
+
10
+ assert "Answer text" in result
11
+ assert "<details><summary>Supporting snippet (1)</summary>" in result
12
+ assert "[ref1]" in result
13
+ assert "Quoted &lt;clause&gt; &amp; detail" in result
14
+
15
+
16
+ def test_stream_chat_entry_appends_collapsible_snippets_after_text_stream() -> None:
17
+ frames = list(
18
+ _stream_chat_entry(
19
+ [{"role": "user", "content": "Question"}],
20
+ "Answer text",
21
+ [{"ref_id": 1, "snippet": "Snippet text"}],
22
+ )
23
+ )
24
+
25
+ assert frames[-2][-1]["content"] == "_Based on the summary and analysis._\n\nAnswer text"
26
+ assert "<details><summary>Supporting snippet (1)</summary>" in frames[-1][-1]["content"]