gowtham0992 Codex commited on
Commit
8c85b65
·
1 Parent(s): 5971541

Wire app to configurable model backend

Browse files

Co-authored-by: Codex <codex@openai.com>

Files changed (5) hide show
  1. app.py +107 -13
  2. eval/run_eval.py +8 -0
  3. jawbreaker/analyzers.py +49 -1
  4. requirements.txt +2 -1
  5. tests/test_schema.py +24 -0
app.py CHANGED
@@ -1,5 +1,10 @@
 
 
 
 
1
  import gradio as gr
2
 
 
3
  from jawbreaker.render import render_analysis_html, render_memory_html
4
  from jawbreaker.schema import ScamAnalysis
5
 
@@ -11,17 +16,115 @@ EXAMPLES = [
11
  ]
12
 
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  def analyze_message(message: str, memory: list[dict] | None) -> tuple[str, str, list[dict]]:
15
  memory = memory or []
16
- analysis = ScamAnalysis.from_heuristics(message, memory)
 
 
 
17
  return render_analysis_html(message, analysis), render_memory_html(analysis, memory), memory
18
 
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  def remember_current(message: str, memory: list[dict] | None) -> tuple[str, list[dict]]:
21
  memory = memory or []
22
- analysis = ScamAnalysis.from_heuristics(message, memory)
23
  if not message.strip():
24
  return "Paste a message first.", memory
 
 
 
 
25
 
26
  memory.append(
27
  {
@@ -36,15 +139,7 @@ def remember_current(message: str, memory: list[dict] | None) -> tuple[str, list
36
 
37
 
38
  def build_app() -> gr.Blocks:
39
- css = open("style.css", "r", encoding="utf-8").read()
40
- theme = gr.themes.Soft(
41
- primary_hue="red",
42
- secondary_hue="slate",
43
- neutral_hue="zinc",
44
- radius_size="sm",
45
- )
46
-
47
- with gr.Blocks(title="Jawbreaker", theme=theme, css=css) as demo:
48
  memory_state = gr.State([])
49
 
50
  gr.HTML(
@@ -100,5 +195,4 @@ def build_app() -> gr.Blocks:
100
 
101
 
102
  if __name__ == "__main__":
103
- build_app().launch()
104
-
 
1
+ import os
2
+ from functools import lru_cache
3
+ from pathlib import Path
4
+
5
  import gradio as gr
6
 
7
+ from jawbreaker.analyzers import build_llama_cpp_analyzer, heuristic_analyzer, prediction_to_analysis
8
  from jawbreaker.render import render_analysis_html, render_memory_html
9
  from jawbreaker.schema import ScamAnalysis
10
 
 
16
  ]
17
 
18
 
19
+ def app_theme() -> gr.Theme:
20
+ return gr.themes.Soft(
21
+ primary_hue="red",
22
+ secondary_hue="slate",
23
+ neutral_hue="zinc",
24
+ radius_size="sm",
25
+ )
26
+
27
+
28
+ def app_css() -> str:
29
+ return Path("style.css").read_text(encoding="utf-8")
30
+
31
+
32
+ def _env_int(name: str, default: int | None = None) -> int | None:
33
+ value = os.getenv(name)
34
+ if value is None or value == "":
35
+ return default
36
+ return int(value)
37
+
38
+
39
+ def _env_bool(name: str, default: bool | None = None) -> bool | None:
40
+ value = os.getenv(name)
41
+ if value is None or value == "":
42
+ return default
43
+ return value.strip().lower() in {"1", "true", "yes", "on"}
44
+
45
+
46
+ @lru_cache(maxsize=1)
47
+ def get_analyzer():
48
+ backend = os.getenv("JAWBREAKER_BACKEND", "heuristic").strip().lower()
49
+ if backend == "heuristic":
50
+ return heuristic_analyzer
51
+
52
+ if backend == "llama-cpp":
53
+ model_path = resolve_model_path()
54
+ return build_llama_cpp_analyzer(
55
+ model_path,
56
+ chat_format=os.getenv("JAWBREAKER_CHAT_FORMAT") or None,
57
+ n_ctx=_env_int("JAWBREAKER_N_CTX", 2048) or 2048,
58
+ n_threads=_env_int("JAWBREAKER_N_THREADS"),
59
+ n_gpu_layers=_env_int("JAWBREAKER_N_GPU_LAYERS", 0) or 0,
60
+ n_batch=_env_int("JAWBREAKER_N_BATCH", 512) or 512,
61
+ n_ubatch=_env_int("JAWBREAKER_N_UBATCH", 512) or 512,
62
+ offload_kqv=_env_bool("JAWBREAKER_OFFLOAD_KQV", True),
63
+ op_offload=_env_bool("JAWBREAKER_OP_OFFLOAD"),
64
+ max_tokens=_env_int("JAWBREAKER_MAX_TOKENS", 512) or 512,
65
+ temperature=float(os.getenv("JAWBREAKER_TEMPERATURE", "0")),
66
+ )
67
+
68
+ raise ValueError(f"Unsupported JAWBREAKER_BACKEND: {backend}")
69
+
70
+
71
+ def resolve_model_path() -> Path:
72
+ model_path = Path(os.getenv("JAWBREAKER_MODEL_PATH", "models/qwen3-4b-gguf/Qwen3-4B-Q4_K_M.gguf"))
73
+ if model_path.exists():
74
+ return model_path
75
+
76
+ repo_id = os.getenv("JAWBREAKER_MODEL_REPO", "Qwen/Qwen3-4B-GGUF")
77
+ filename = os.getenv("JAWBREAKER_MODEL_FILE", "Qwen3-4B-Q4_K_M.gguf")
78
+ cache_dir = os.getenv("JAWBREAKER_MODEL_CACHE")
79
+ try:
80
+ from huggingface_hub import hf_hub_download
81
+ except ImportError as exc:
82
+ raise RuntimeError("huggingface_hub is required to download the configured model file.") from exc
83
+
84
+ return Path(hf_hub_download(repo_id=repo_id, filename=filename, cache_dir=cache_dir))
85
+
86
+
87
+ def run_analysis(message: str, memory: list[dict] | None) -> ScamAnalysis:
88
+ memory = memory or []
89
+ prediction = get_analyzer()(message)
90
+ similar_memory = ScamAnalysis.from_heuristics(message, memory).similar_memory
91
+ return prediction_to_analysis(prediction, similar_memory=similar_memory)
92
+
93
+
94
  def analyze_message(message: str, memory: list[dict] | None) -> tuple[str, str, list[dict]]:
95
  memory = memory or []
96
+ try:
97
+ analysis = run_analysis(message, memory)
98
+ except Exception as exc:
99
+ analysis = analysis_error(exc)
100
  return render_analysis_html(message, analysis), render_memory_html(analysis, memory), memory
101
 
102
 
103
+ def analysis_error(exc: Exception) -> ScamAnalysis:
104
+ return ScamAnalysis(
105
+ risk_level="needs_check",
106
+ scam_type="analysis_error",
107
+ summary="Jawbreaker could not finish the model scan in this environment.",
108
+ tactics=["runtime error"],
109
+ safest_action="Do not click links or reply yet. Verify through an official app, website, or known phone number.",
110
+ trusted_person_message=f"Can you check this for me? Jawbreaker hit an analysis error: {exc}",
111
+ scam_dna={
112
+ "Impersonates": "Unknown",
113
+ "Pressure": "Unknown",
114
+ "Ask": "Unknown",
115
+ "Risk": "Could not analyze",
116
+ },
117
+ )
118
+
119
+
120
  def remember_current(message: str, memory: list[dict] | None) -> tuple[str, list[dict]]:
121
  memory = memory or []
 
122
  if not message.strip():
123
  return "Paste a message first.", memory
124
+ try:
125
+ analysis = run_analysis(message, memory)
126
+ except Exception as exc:
127
+ analysis = analysis_error(exc)
128
 
129
  memory.append(
130
  {
 
139
 
140
 
141
  def build_app() -> gr.Blocks:
142
+ with gr.Blocks(title="Jawbreaker") as demo:
 
 
 
 
 
 
 
 
143
  memory_state = gr.State([])
144
 
145
  gr.HTML(
 
195
 
196
 
197
  if __name__ == "__main__":
198
+ build_app().launch(theme=app_theme(), css=app_css())
 
eval/run_eval.py CHANGED
@@ -51,6 +51,10 @@ def parse_args() -> argparse.Namespace:
51
  parser.add_argument("--n-ctx", type=int, default=4096)
52
  parser.add_argument("--n-threads", type=int)
53
  parser.add_argument("--n-gpu-layers", type=int, default=0)
 
 
 
 
54
  parser.add_argument("--max-tokens", type=int, default=512)
55
  parser.add_argument("--temperature", type=float, default=0.0)
56
  return parser.parse_args()
@@ -131,6 +135,10 @@ def build_analyzer(args: argparse.Namespace):
131
  n_ctx=args.n_ctx,
132
  n_threads=args.n_threads,
133
  n_gpu_layers=args.n_gpu_layers,
 
 
 
 
134
  max_tokens=args.max_tokens,
135
  temperature=args.temperature,
136
  )
 
51
  parser.add_argument("--n-ctx", type=int, default=4096)
52
  parser.add_argument("--n-threads", type=int)
53
  parser.add_argument("--n-gpu-layers", type=int, default=0)
54
+ parser.add_argument("--n-batch", type=int, default=512)
55
+ parser.add_argument("--n-ubatch", type=int, default=512)
56
+ parser.add_argument("--offload-kqv", action=argparse.BooleanOptionalAction, default=True)
57
+ parser.add_argument("--op-offload", action=argparse.BooleanOptionalAction)
58
  parser.add_argument("--max-tokens", type=int, default=512)
59
  parser.add_argument("--temperature", type=float, default=0.0)
60
  return parser.parse_args()
 
135
  n_ctx=args.n_ctx,
136
  n_threads=args.n_threads,
137
  n_gpu_layers=args.n_gpu_layers,
138
+ n_batch=args.n_batch,
139
+ n_ubatch=args.n_ubatch,
140
+ offload_kqv=args.offload_kqv,
141
+ op_offload=args.op_offload,
142
  max_tokens=args.max_tokens,
143
  temperature=args.temperature,
144
  )
jawbreaker/analyzers.py CHANGED
@@ -30,6 +30,46 @@ def analysis_to_prediction(analysis: ScamAnalysis) -> Prediction:
30
  }
31
 
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  def heuristic_analyzer(message: str) -> Prediction:
34
  return analysis_to_prediction(ScamAnalysis.from_heuristics(message))
35
 
@@ -61,6 +101,10 @@ def build_llama_cpp_analyzer(
61
  n_ctx: int = 4096,
62
  n_threads: int | None = None,
63
  n_gpu_layers: int = 0,
 
 
 
 
64
  max_tokens: int = 512,
65
  temperature: float = 0.0,
66
  ) -> Analyzer:
@@ -75,8 +119,13 @@ def build_llama_cpp_analyzer(
75
  "model_path": str(model_path),
76
  "n_ctx": n_ctx,
77
  "n_gpu_layers": n_gpu_layers,
 
 
 
78
  "verbose": False,
79
  }
 
 
80
  if chat_format:
81
  kwargs["chat_format"] = chat_format
82
  if n_threads is not None:
@@ -132,4 +181,3 @@ def write_predictions(path: Path, rows: Iterable[dict], predictions: dict[str, P
132
  case_id = row["id"]
133
  lines.append(json.dumps({"id": case_id, "prediction": predictions[case_id]}, ensure_ascii=True))
134
  path.write_text("\n".join(lines) + "\n", encoding="utf-8")
135
-
 
30
  }
31
 
32
 
33
+ def prediction_to_analysis(prediction: Prediction, *, similar_memory: str = "") -> ScamAnalysis:
34
+ risk_level = prediction.get("risk_level")
35
+ if risk_level not in {"dangerous", "suspicious", "needs_check", "safe"}:
36
+ risk_level = "needs_check"
37
+
38
+ tactics = prediction.get("tactics", [])
39
+ if not isinstance(tactics, list):
40
+ tactics = []
41
+
42
+ scam_dna = prediction.get("scam_dna", {})
43
+ if not isinstance(scam_dna, dict):
44
+ scam_dna = {}
45
+
46
+ return ScamAnalysis(
47
+ risk_level=str(risk_level),
48
+ scam_type=str(prediction.get("scam_type", "unknown")),
49
+ summary=str(prediction.get("summary", "This message should be checked before anyone acts.")),
50
+ tactics=[str(tactic) for tactic in tactics],
51
+ safest_action=str(
52
+ prediction.get(
53
+ "safest_action",
54
+ "Do not click links or reply. Verify through an official app, website, or known phone number.",
55
+ )
56
+ ),
57
+ trusted_person_message=str(
58
+ prediction.get(
59
+ "trusted_person_message",
60
+ "Can you check this for me before I respond or click anything?",
61
+ )
62
+ ),
63
+ scam_dna={
64
+ "Impersonates": str(scam_dna.get("impersonates", "")),
65
+ "Pressure": str(scam_dna.get("pressure", "")),
66
+ "Ask": str(scam_dna.get("ask", "")),
67
+ "Risk": str(scam_dna.get("risk", "")),
68
+ },
69
+ similar_memory=similar_memory,
70
+ )
71
+
72
+
73
  def heuristic_analyzer(message: str) -> Prediction:
74
  return analysis_to_prediction(ScamAnalysis.from_heuristics(message))
75
 
 
101
  n_ctx: int = 4096,
102
  n_threads: int | None = None,
103
  n_gpu_layers: int = 0,
104
+ n_batch: int = 512,
105
+ n_ubatch: int = 512,
106
+ offload_kqv: bool = True,
107
+ op_offload: bool | None = None,
108
  max_tokens: int = 512,
109
  temperature: float = 0.0,
110
  ) -> Analyzer:
 
119
  "model_path": str(model_path),
120
  "n_ctx": n_ctx,
121
  "n_gpu_layers": n_gpu_layers,
122
+ "n_batch": n_batch,
123
+ "n_ubatch": n_ubatch,
124
+ "offload_kqv": offload_kqv,
125
  "verbose": False,
126
  }
127
+ if op_offload is not None:
128
+ kwargs["op_offload"] = op_offload
129
  if chat_format:
130
  kwargs["chat_format"] = chat_format
131
  if n_threads is not None:
 
181
  case_id = row["id"]
182
  lines.append(json.dumps({"id": case_id, "prediction": predictions[case_id]}, ensure_ascii=True))
183
  path.write_text("\n".join(lines) + "\n", encoding="utf-8")
 
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
  gradio==6.16.0
 
 
2
  pytest==8.4.2
3
-
 
1
  gradio==6.16.0
2
+ huggingface-hub==1.18.0
3
+ llama-cpp-python==0.3.26
4
  pytest==8.4.2
 
tests/test_schema.py CHANGED
@@ -1,4 +1,5 @@
1
  from jawbreaker.schema import ScamAnalysis
 
2
 
3
 
4
  def test_family_impersonation_is_dangerous() -> None:
@@ -19,3 +20,26 @@ def test_legitimate_fraud_alert_needs_check_not_dangerous() -> None:
19
  assert analysis.risk_level == "needs_check"
20
  assert analysis.scam_type == "possible_legitimate_alert"
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from jawbreaker.schema import ScamAnalysis
2
+ from jawbreaker.analyzers import prediction_to_analysis
3
 
4
 
5
  def test_family_impersonation_is_dangerous() -> None:
 
20
  assert analysis.risk_level == "needs_check"
21
  assert analysis.scam_type == "possible_legitimate_alert"
22
 
23
+
24
+ def test_prediction_to_analysis_normalizes_model_json() -> None:
25
+ analysis = prediction_to_analysis(
26
+ {
27
+ "risk_level": "not_valid",
28
+ "scam_type": "package_phishing",
29
+ "summary": "Fake delivery notice.",
30
+ "tactics": ["fake authority"],
31
+ "safest_action": "Open the official carrier website yourself.",
32
+ "trusted_person_message": "Can you check this?",
33
+ "scam_dna": {
34
+ "impersonates": "USPS",
35
+ "pressure": "Held package",
36
+ "ask": "Open link",
37
+ "risk": "credential theft",
38
+ },
39
+ },
40
+ similar_memory="This resembles a saved pattern.",
41
+ )
42
+
43
+ assert analysis.risk_level == "needs_check"
44
+ assert analysis.scam_dna["Impersonates"] == "USPS"
45
+ assert analysis.similar_memory == "This resembles a saved pattern."