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.
- data_morph_gemma-0.1.0.dist-info/METADATA +177 -0
- data_morph_gemma-0.1.0.dist-info/RECORD +39 -0
- data_morph_gemma-0.1.0.dist-info/WHEEL +4 -0
- data_morph_gemma-0.1.0.dist-info/entry_points.txt +2 -0
- data_morph_gemma-0.1.0.dist-info/licenses/LICENSE +25 -0
- datamorph/__init__.py +19 -0
- datamorph/cli.py +84 -0
- datamorph/convert.py +146 -0
- datamorph/data/__init__.py +1 -0
- datamorph/data/collect.py +221 -0
- datamorph/data/envelope.py +20 -0
- datamorph/data/generators/__init__.py +1 -0
- datamorph/data/generators/base.py +48 -0
- datamorph/data/generators/uc1_csv_to_json.py +64 -0
- datamorph/data/generators/uc2_json_to_csv.py +59 -0
- datamorph/data/generators/uc3_txt_log_to_csv.py +64 -0
- datamorph/data/generators/uc4_csv_to_txt_report.py +62 -0
- datamorph/data/generators/uc5_schema_migration.py +49 -0
- datamorph/data/sandbox.py +95 -0
- datamorph/data/teacher_script.py +114 -0
- datamorph/evaluation/__init__.py +0 -0
- datamorph/evaluation/metrics.py +264 -0
- datamorph/evaluation/output_cleanup.py +116 -0
- datamorph/evaluation/runner.py +218 -0
- datamorph/evaluation/teacher.py +193 -0
- datamorph/extractor/__init__.py +15 -0
- datamorph/extractor/base.py +26 -0
- datamorph/extractor/csv_extractor.py +515 -0
- datamorph/extractor/json_extractor.py +447 -0
- datamorph/extractor/json_walker.py +217 -0
- datamorph/extractor/sampler.py +68 -0
- datamorph/extractor/txt_extractor.py +199 -0
- datamorph/extractor/warning_rules.py +473 -0
- datamorph/features/__init__.py +1 -0
- datamorph/features/format_pairs.py +57 -0
- datamorph/model.py +63 -0
- datamorph/models/__init__.py +0 -0
- datamorph/models/gemma_mlx.py +163 -0
- 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
|
+
)
|