data-morph-gemma 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data_morph_gemma-0.1.0.dist-info/METADATA +177 -0
  2. data_morph_gemma-0.1.0.dist-info/RECORD +39 -0
  3. data_morph_gemma-0.1.0.dist-info/WHEEL +4 -0
  4. data_morph_gemma-0.1.0.dist-info/entry_points.txt +2 -0
  5. data_morph_gemma-0.1.0.dist-info/licenses/LICENSE +25 -0
  6. datamorph/__init__.py +19 -0
  7. datamorph/cli.py +84 -0
  8. datamorph/convert.py +146 -0
  9. datamorph/data/__init__.py +1 -0
  10. datamorph/data/collect.py +221 -0
  11. datamorph/data/envelope.py +20 -0
  12. datamorph/data/generators/__init__.py +1 -0
  13. datamorph/data/generators/base.py +48 -0
  14. datamorph/data/generators/uc1_csv_to_json.py +64 -0
  15. datamorph/data/generators/uc2_json_to_csv.py +59 -0
  16. datamorph/data/generators/uc3_txt_log_to_csv.py +64 -0
  17. datamorph/data/generators/uc4_csv_to_txt_report.py +62 -0
  18. datamorph/data/generators/uc5_schema_migration.py +49 -0
  19. datamorph/data/sandbox.py +95 -0
  20. datamorph/data/teacher_script.py +114 -0
  21. datamorph/evaluation/__init__.py +0 -0
  22. datamorph/evaluation/metrics.py +264 -0
  23. datamorph/evaluation/output_cleanup.py +116 -0
  24. datamorph/evaluation/runner.py +218 -0
  25. datamorph/evaluation/teacher.py +193 -0
  26. datamorph/extractor/__init__.py +15 -0
  27. datamorph/extractor/base.py +26 -0
  28. datamorph/extractor/csv_extractor.py +515 -0
  29. datamorph/extractor/json_extractor.py +447 -0
  30. datamorph/extractor/json_walker.py +217 -0
  31. datamorph/extractor/sampler.py +68 -0
  32. datamorph/extractor/txt_extractor.py +199 -0
  33. datamorph/extractor/warning_rules.py +473 -0
  34. datamorph/features/__init__.py +1 -0
  35. datamorph/features/format_pairs.py +57 -0
  36. datamorph/model.py +63 -0
  37. datamorph/models/__init__.py +0 -0
  38. datamorph/models/gemma_mlx.py +163 -0
  39. datamorph/models/gemma_script_teacher.py +100 -0
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ _LOCAL_PATH = Path(__file__).resolve().parents[2] / "models" / "gemma-4-e2b-it-bf16"
9
+ # Base model dir; override via GEMMA_MLX_MODEL to point at e.g. a quantized build.
10
+ MODEL_ID = os.environ.get("GEMMA_MLX_MODEL", str(_LOCAL_PATH))
11
+
12
+ # When the model is a stripped text-only build (gemma4_text), load it through
13
+ # mlx_lm instead of mlx_vlm — there is no vision/audio tower to wrap. Set
14
+ # GEMMA_TEXT_ONLY=1 to select this path (used by the W7 text-only student).
15
+ TEXT_ONLY = os.environ.get("GEMMA_TEXT_ONLY", "").lower() in ("1", "true", "yes")
16
+
17
+ # Model selection lives in _state so it can be changed at runtime (via use_model)
18
+ # without re-importing the module. The env vars seed the defaults, so existing
19
+ # scripts/notebooks that set GEMMA_MLX_MODEL / GEMMA_TEXT_ONLY keep working.
20
+ _state: dict = {
21
+ "model": None,
22
+ "processor": None,
23
+ "load_sec": None,
24
+ "adapter": None,
25
+ "model_id": MODEL_ID,
26
+ "text_only": TEXT_ONLY,
27
+ }
28
+
29
+
30
+ def use_adapter(adapter_path: str | None) -> None:
31
+ """Select a LoRA adapter directory for inference (None = base model).
32
+
33
+ The directory must contain ``adapters.safetensors`` + ``adapter_config.json``.
34
+ Changing the selection forces the model to reload on the next ``generate`` call.
35
+ """
36
+ if adapter_path != _state["adapter"]:
37
+ _state["adapter"] = adapter_path
38
+ _state["model"] = None # force reload with the new adapter (or base)
39
+
40
+
41
+ def use_model(model_id: str, *, text_only: bool = True) -> None:
42
+ """Select which model to load (local dir path or HF id).
43
+
44
+ Mirrors ``use_adapter``: changing the selection forces a reload on the next
45
+ ``generate`` call. ``text_only=True`` loads via mlx_lm (the stripped W7
46
+ student); ``False`` uses mlx_vlm (the multimodal base).
47
+ """
48
+ if model_id != _state["model_id"] or text_only != _state["text_only"]:
49
+ _state["model_id"] = model_id
50
+ _state["text_only"] = text_only
51
+ _state["model"] = None # force reload with the new model
52
+
53
+
54
+ @dataclass
55
+ class GenerationResult:
56
+ text: str
57
+ n_prompt_tokens: int
58
+ n_generated_tokens: int
59
+ elapsed_sec: float
60
+ tokens_per_sec: float
61
+ model_id: str
62
+ truncated: bool
63
+
64
+
65
+ def _ensure_loaded() -> None:
66
+ if _state["model"] is not None:
67
+ return
68
+ t0 = time.time()
69
+ model_id = _state["model_id"]
70
+ text_only = _state["text_only"]
71
+ if text_only:
72
+ import mlx_lm # lazy import
73
+
74
+ loaded = mlx_lm.load(model_id, adapter_path=_state["adapter"])
75
+ model, processor = loaded[0], loaded[1]
76
+ else:
77
+ from mlx_vlm import load # lazy import
78
+
79
+ model, processor = load(model_id, adapter_path=_state["adapter"])
80
+ _state["model"] = model
81
+ _state["processor"] = processor
82
+ _state["load_sec"] = round(time.time() - t0, 2)
83
+ tag = f" + adapter {_state['adapter']}" if _state["adapter"] else ""
84
+ backend = "mlx_lm/text" if text_only else "mlx_vlm"
85
+ print(f"[gemma_mlx] loaded {model_id}{tag} via {backend} in {_state['load_sec']}s")
86
+
87
+
88
+ def _generate_text_only(messages: list[dict], max_tokens: int) -> GenerationResult:
89
+ """Greedy generation via mlx_lm on the stripped text-only student."""
90
+ import mlx_lm # lazy
91
+ from mlx_lm.sample_utils import make_sampler # lazy
92
+
93
+ model = _state["model"]
94
+ tok = _state["processor"]
95
+ prompt = tok.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
96
+
97
+ t0 = time.time()
98
+ text = mlx_lm.generate(
99
+ model,
100
+ tok,
101
+ str(prompt),
102
+ max_tokens=max_tokens,
103
+ sampler=make_sampler(temp=0.0), # greedy, matches the temperature=0.0 VLM path
104
+ verbose=False,
105
+ )
106
+ elapsed = time.time() - t0
107
+
108
+ n_prompt = len(tok.encode(str(prompt)))
109
+ n_gen = len(tok.encode(text)) if text else 0
110
+ tps = round(n_gen / elapsed, 2) if elapsed > 0 else 0.0
111
+ return GenerationResult(
112
+ text=text,
113
+ n_prompt_tokens=n_prompt,
114
+ n_generated_tokens=n_gen,
115
+ elapsed_sec=round(elapsed, 2),
116
+ tokens_per_sec=tps,
117
+ model_id=_state["model_id"],
118
+ truncated=n_gen >= max_tokens,
119
+ )
120
+
121
+
122
+ def generate(messages: list[dict], max_tokens: int = 4096) -> GenerationResult:
123
+ """Run greedy generation on the given chat messages (text-only)."""
124
+ _ensure_loaded()
125
+ if _state["text_only"]:
126
+ return _generate_text_only(messages, max_tokens)
127
+
128
+ from mlx_vlm import generate as vlm_generate # lazy
129
+ from mlx_vlm.prompt_utils import apply_chat_template # lazy
130
+
131
+ model = _state["model"]
132
+ processor = _state["processor"]
133
+
134
+ prompt = apply_chat_template(processor, model.config, messages, num_images=0)
135
+
136
+ t0 = time.time()
137
+ result = vlm_generate(
138
+ model=model,
139
+ processor=processor,
140
+ prompt=str(prompt),
141
+ max_tokens=max_tokens,
142
+ temperature=0.0,
143
+ verbose=False,
144
+ )
145
+ elapsed = time.time() - t0
146
+
147
+ n_prompt = int(getattr(result, "prompt_tokens", 0) or 0)
148
+ n_gen = int(getattr(result, "generation_tokens", 0) or 0)
149
+ if elapsed > 0:
150
+ tps = float(getattr(result, "generation_tps", n_gen / elapsed) or 0.0)
151
+ else:
152
+ tps = 0.0
153
+ truncated = n_gen >= max_tokens
154
+
155
+ return GenerationResult(
156
+ text=result.text,
157
+ n_prompt_tokens=n_prompt,
158
+ n_generated_tokens=n_gen,
159
+ elapsed_sec=round(elapsed, 2),
160
+ tokens_per_sec=round(tps, 2),
161
+ model_id=_state["model_id"],
162
+ truncated=truncated,
163
+ )
@@ -0,0 +1,100 @@
1
+ """Gemma script-generation 'teacher' — the un-fine-tuned student on the new pipeline.
2
+
3
+ Drop-in replacement for ``datamorph.data.teacher_script.call_script_teacher`` (same
4
+ ``ScriptResult`` contract and ``(envelope, instruction, output_format, *, feedback)``
5
+ signature), but the script author is the local Gemma model via MLX instead of Opus.
6
+
7
+ Used to establish the **zero-shot base-student baseline** on the Stage-3..5 pipeline
8
+ (envelope -> script -> sandbox -> metrics), before any LoRA fine-tuning. The base
9
+ model has not learned the skill, so — like the W2 Gemma baseline — the skill text is
10
+ folded into the prompt (Gemma's chat template has no system role and it cannot Read
11
+ files the way the Opus CLI can).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from datamorph.data.teacher_script import ScriptResult, parse_teacher_output
21
+
22
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
23
+ SKILL_REL_PATH = "skills/script_generation_teacher.md"
24
+
25
+ _SKILL_CACHE: dict[str, str] = {}
26
+
27
+
28
+ def _skill_text() -> str:
29
+ if "text" not in _SKILL_CACHE:
30
+ _SKILL_CACHE["text"] = (PROJECT_ROOT / SKILL_REL_PATH).read_text(encoding="utf-8")
31
+ return _SKILL_CACHE["text"]
32
+
33
+
34
+ def build_gemma_prompt(
35
+ envelope: dict[str, Any],
36
+ instruction: str,
37
+ output_format: str,
38
+ feedback: str | None = None,
39
+ ) -> str:
40
+ """Skill text + the inference-style task (envelope + instruction + output contract).
41
+
42
+ The task half mirrors ``format_pairs.to_chat_record`` (what the fine-tuned model
43
+ will see at inference); the skill text is prepended so the *un-trained* base model
44
+ has the instructions in-context.
45
+ """
46
+ env_json = json.dumps(envelope, indent=2, default=str)
47
+ fb = (
48
+ f"\n\nYour previous attempt failed: {feedback}\nWrite a corrected response.\n"
49
+ if feedback
50
+ else ""
51
+ )
52
+ return (
53
+ f"{_skill_text()}\n\n---\n\n"
54
+ f"Metadata envelope:\n```json\n{env_json}\n```\n\n"
55
+ f"Task: {instruction}\n"
56
+ f"Target output format: {output_format.upper()}.\n"
57
+ f"Write a Python conversion script that reads the input file path from "
58
+ f"sys.argv[1] and writes the converted output to sys.argv[2], using only the "
59
+ f"Python standard library and pandas. Respond with exactly an "
60
+ f"<analysis>...</analysis> block followed by a <script>...</script> block. "
61
+ f"No prose outside the tags."
62
+ f"{fb}"
63
+ )
64
+
65
+
66
+ def call_gemma_script_teacher(
67
+ envelope: dict[str, Any],
68
+ instruction: str,
69
+ output_format: str,
70
+ *,
71
+ feedback: str | None = None,
72
+ max_tokens: int = 4096,
73
+ ) -> ScriptResult:
74
+ """Generate <analysis> + <script> with the local Gemma model; parse into a ScriptResult."""
75
+ from datamorph.models.gemma_mlx import generate as mlx_generate # lazy: keep MLX import optional
76
+
77
+ prompt = build_gemma_prompt(envelope, instruction, output_format, feedback)
78
+ try:
79
+ gen = mlx_generate([{"role": "user", "content": prompt}], max_tokens=max_tokens)
80
+ except Exception as e: # MLX / generation failure -> looks like "no script"
81
+ return ScriptResult("", "", "", -1, f"gemma generate raised: {e!r}", {})
82
+
83
+ analysis, script = parse_teacher_output(gen.text)
84
+ return ScriptResult(
85
+ analysis=analysis,
86
+ script=script,
87
+ raw_output=gen.text,
88
+ returncode=0,
89
+ stderr="",
90
+ raw_payload={
91
+ "model_id": gen.model_id,
92
+ "gemma_meta": {
93
+ "n_prompt_tokens": gen.n_prompt_tokens,
94
+ "n_generated_tokens": gen.n_generated_tokens,
95
+ "tokens_per_sec": gen.tokens_per_sec,
96
+ "elapsed_sec": gen.elapsed_sec,
97
+ "truncated": gen.truncated,
98
+ },
99
+ },
100
+ )