py-harness-cli 0.3.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 (92) hide show
  1. finetune/__init__.py +1 -0
  2. finetune/agent_system.py +41 -0
  3. finetune/agent_traces.py +157 -0
  4. finetune/everyday.py +30 -0
  5. finetune/hf_ollama.py +158 -0
  6. finetune/huggingface_store.py +144 -0
  7. finetune/models.py +74 -0
  8. finetune/paths.py +11 -0
  9. finetune/python_vibe.py +788 -0
  10. finetune/splits.py +54 -0
  11. finetune/systems.py +9 -0
  12. harness/__init__.py +42 -0
  13. harness/__main__.py +8 -0
  14. harness/act/__init__.py +6 -0
  15. harness/act/autofix/__init__.py +110 -0
  16. harness/act/autofix/additions.py +217 -0
  17. harness/act/autofix/conflicts.py +124 -0
  18. harness/act/autofix/cover.py +419 -0
  19. harness/act/autofix/mechanical.py +151 -0
  20. harness/act/autofix/missing_imports.py +50 -0
  21. harness/act/autofix/moves.py +439 -0
  22. harness/act/autofix/names.py +339 -0
  23. harness/act/autofix/scaffold.py +224 -0
  24. harness/act/code.py +157 -0
  25. harness/act/gate.py +229 -0
  26. harness/act/parse.py +247 -0
  27. harness/act/patch_fix.py +138 -0
  28. harness/act/tools.py +244 -0
  29. harness/agent/__init__.py +11 -0
  30. harness/agent/dispatch.py +235 -0
  31. harness/agent/loop.py +699 -0
  32. harness/agent/options.py +144 -0
  33. harness/agent/policy.py +856 -0
  34. harness/agent/prompt.py +170 -0
  35. harness/cli.py +393 -0
  36. harness/editor_kit.py +265 -0
  37. harness/guard/__init__.py +6 -0
  38. harness/guard/fallbacks.py +6 -0
  39. harness/guard/loop_guard.py +57 -0
  40. harness/guard/python_vibe.py +68 -0
  41. harness/guard/run.py +41 -0
  42. harness/guard/types.py +19 -0
  43. harness/locate.py +767 -0
  44. harness/mcp_stdio.py +306 -0
  45. harness/memory/__init__.py +5 -0
  46. harness/memory/conversation.py +104 -0
  47. harness/model/__init__.py +6 -0
  48. harness/model/chat_backend.py +100 -0
  49. harness/model/engine.py +165 -0
  50. harness/model/ollama_generate.py +60 -0
  51. harness/model/openai_generate.py +156 -0
  52. harness/model/outbound.py +83 -0
  53. harness/model/route.py +90 -0
  54. harness/observe/__init__.py +6 -0
  55. harness/observe/eval_gate.py +80 -0
  56. harness/observe/eval_loop.py +185 -0
  57. harness/observe/eval_tasks.py +399 -0
  58. harness/observe/report_md.py +102 -0
  59. harness/observe/trace_record.py +79 -0
  60. harness/openai_api.py +81 -0
  61. harness/paths.py +88 -0
  62. harness/py.typed +0 -0
  63. harness/scan/__init__.py +6 -0
  64. harness/scan/app_spec.py +338 -0
  65. harness/scan/design.py +112 -0
  66. harness/scan/existing.py +131 -0
  67. harness/scan/layout.py +254 -0
  68. harness/scan/names.py +308 -0
  69. harness/scan/project_brief.py +287 -0
  70. harness/scan/project_docs.py +42 -0
  71. harness/scan/project_scan.py +49 -0
  72. harness/scan/repo_map.py +101 -0
  73. harness/secrets.py +39 -0
  74. harness/server.py +199 -0
  75. harness/ship/__init__.py +1 -0
  76. harness/ship/bot_pr.py +221 -0
  77. harness/ship/git_ship.py +262 -0
  78. harness/ship/identity.py +62 -0
  79. harness/ship/ticket.py +251 -0
  80. harness/skillkit/__init__.py +6 -0
  81. harness/skillkit/catalog.py +241 -0
  82. harness/skillkit/refuse_change.py +640 -0
  83. harness/skillkit/refuse_finish.py +295 -0
  84. harness/skillkit/target.py +238 -0
  85. harness/task.py +717 -0
  86. py_harness_cli-0.3.0.dist-info/METADATA +177 -0
  87. py_harness_cli-0.3.0.dist-info/RECORD +92 -0
  88. py_harness_cli-0.3.0.dist-info/WHEEL +5 -0
  89. py_harness_cli-0.3.0.dist-info/entry_points.txt +3 -0
  90. py_harness_cli-0.3.0.dist-info/licenses/LICENSE +202 -0
  91. py_harness_cli-0.3.0.dist-info/licenses/NOTICE +6 -0
  92. py_harness_cli-0.3.0.dist-info/top_level.txt +2 -0
finetune/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Build LoRA datasets and train the python-vibe model."""
@@ -0,0 +1,41 @@
1
+ """System prompt for the everyday tool loop. Kept short for the 8B.
2
+
3
+ The paths here are placeholders, not examples. An 8B copies the first block
4
+ it sees, so a literal path in this template is a path it will write to in
5
+ whatever repo it is pointed at. `harness.agent.prompt` fills them from the
6
+ project in front of the model before the prompt is sent.
7
+
8
+ Placeholders: {{module}} {{test}}
9
+ """
10
+
11
+ AGENT_SYSTEM = """\
12
+ One Action per turn. Never paste a list of Actions. Copy one block only.
13
+
14
+ Action: locate
15
+ Query: apply_source
16
+
17
+ Action: patch
18
+ Path: {{module}}
19
+ Append:
20
+ def multiply(left: int, right: int) -> int:
21
+ return left * right
22
+
23
+ Action: run
24
+ Argv: -m unittest discover -s tests -q
25
+
26
+ Action: ask
27
+ Query: one short question, when the task could mean two different things
28
+ Append:
29
+ - the first reading
30
+ - the second reading
31
+
32
+ Action: done
33
+ Summary: one sentence, in your own words, about this project
34
+
35
+ If the harness already shows # auto-read, Action: done.
36
+ If the harness already shows (no hits) for a new function, Action: patch + Append.
37
+ Find: must be a full unique line. Path stays in the project. No curl|sh.
38
+ Names are snake_case (total_price), not calc/tmp/x.
39
+ Never answer by repeating an instruction you were given. Quote the code.
40
+ Ship an issue: issue → branch → patch → commit → push → pr. No force. Not main.
41
+ """
@@ -0,0 +1,157 @@
1
+ """Seed explore→edit→run traces. Templates, not 2k live sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from finetune.agent_system import AGENT_SYSTEM
6
+
7
+
8
+ def seed_pairs() -> list[tuple[str, str]]:
9
+ pairs: list[tuple[str, str]] = []
10
+ files = (
11
+ "src/app.py",
12
+ "tools/system/work_items/validation.py",
13
+ "lib/stats.py",
14
+ "pkg/util_stats.py",
15
+ )
16
+ for path in files:
17
+ pairs.append(
18
+ (
19
+ f"Project root: /tmp/app\nTask: find where {path.split('/')[-1]} defines main helpers\n"
20
+ "Start with Action: grep or Action: read. Do not Action: done yet.",
21
+ f"Action: grep\nQuery: def ",
22
+ )
23
+ )
24
+ pairs.append(
25
+ (
26
+ f"Tool result:\n{path}:12:def compute_total(rows):\n\nNext Action:",
27
+ f"Action: read\nPath: {path}",
28
+ )
29
+ )
30
+ pairs.append(
31
+ (
32
+ f"Tool result:\n{path} (1200 chars)\ndef compute_total(rows):\n return tota\n\nNext Action:",
33
+ f"Action: edit\nPath: {path}\n```python\ndef compute_total(rows):\n return sum(rows)\n```",
34
+ )
35
+ )
36
+ pairs.append(
37
+ (
38
+ "Tool result:\nwrote "
39
+ + path
40
+ + " (backup "
41
+ + path.split("/")[-1]
42
+ + ".bak)\n\nNext Action:",
43
+ "Action: run\nArgv: -m unittest discover -s tests -q",
44
+ )
45
+ )
46
+ pairs.append(
47
+ (
48
+ "Tool result:\nexit 0\n.\n----------------------------------------------------------------------\nRan 1 test in 0.01s\n\nOK\n\nNext Action:",
49
+ "Action: done\nSummary: Fixed NameError in compute_total (tota → sum(rows)) and tests passed.",
50
+ )
51
+ )
52
+
53
+ pairs.append(
54
+ (
55
+ "Could not parse. Reply with one Action: glob|grep|read|edit|run|done",
56
+ "Action: grep\nQuery: NameError",
57
+ )
58
+ )
59
+ pairs.append(("Tool result:\n(no hits)\n\nNext Action:", "Action: glob\nPattern: **/*.py"))
60
+ pairs.append(
61
+ (
62
+ "Tool result:\npkg/util_stats.py\ntests/test_util_stats.py\n\nNext Action:",
63
+ "Action: read\nPath: tests/test_util_stats.py",
64
+ )
65
+ )
66
+ pairs.append(
67
+ (
68
+ "Project root: /tmp/app\nTask: do not rewrite the whole repo after seeing __init__.py\n"
69
+ "Start with Action: grep or Action: read. Do not Action: done yet.",
70
+ "Action: grep\nQuery: TODO|FIXME|NameError",
71
+ )
72
+ )
73
+ for query in ("class ", "import json", "def test_", "except Exception", "Path("):
74
+ pairs.append((f"Find uses of {query.strip()}", f"Action: grep\nQuery: {query.strip()}"))
75
+ pairs.append(
76
+ (
77
+ "Tool result:\nsrc/app.py:1:from pathlib import Path\n\nNext Action:",
78
+ "Action: read\nPath: src/app.py",
79
+ )
80
+ )
81
+ pairs.append(
82
+ (
83
+ "Task: run the unit tests after the edit. Do not Action: done yet.",
84
+ "Action: run\nArgv: -m unittest discover -s tests -q",
85
+ )
86
+ )
87
+ pairs.append(
88
+ (
89
+ "Tool result:\nexit 1\nNameError: name 'tota' is not defined\n\nNext Action:",
90
+ "Action: grep\nQuery: tota",
91
+ )
92
+ )
93
+ pairs.append(
94
+ (
95
+ "Tool result:\npkg/util_stats.py:71:return tota\n\nNext Action:",
96
+ "Action: patch\nPath: pkg/util_stats.py\nFind: return tota\nReplace: return sum(cleaned)",
97
+ )
98
+ )
99
+ pairs.append(
100
+ (
101
+ "Mode: large files=120 size=400.0 KB\n"
102
+ "Large project — use the harness.\n"
103
+ "Start with Action: map, then grep. Do not Action: done yet.",
104
+ "Action: map\nScope: src",
105
+ )
106
+ )
107
+ pairs.append(
108
+ (
109
+ "Tool result:\nmap src 12 files 40.0 KB\n src/harness/code.py 3.1 KB\n\nNext Action:",
110
+ "Action: grep\nQuery: def apply_source",
111
+ )
112
+ )
113
+ pairs.append(
114
+ (
115
+ "This is a question. Read what you need, then Action: done with the answer. "
116
+ "Do not edit unless asked.",
117
+ "Action: read\nPath: src/harness/code.py",
118
+ )
119
+ )
120
+ pairs.append(
121
+ (
122
+ "This is an add-feature task. Grep first. If it is missing, add the "
123
+ "smallest change plus a test, then run. Do not invent extras.",
124
+ "Action: skill\nName: add-feature",
125
+ )
126
+ )
127
+ pairs.append(
128
+ (
129
+ "This is a new-package task. First Action: edit Path: pkg/__init__.py "
130
+ "(exports only).",
131
+ "Action: edit\nPath: pkg/__init__.py\n```python\n"
132
+ '"""Public exports only. Implementation lives in sibling modules."""\n```',
133
+ )
134
+ )
135
+ pairs.append(
136
+ (
137
+ "This is a smell/rename task. Patch one opaque name to readable "
138
+ "snake_case. Do not add features.",
139
+ "Action: patch\nPath: pkg/mathy.py\nFind: def calc(x, y):\n"
140
+ "Replace: def total_price(quantity: int, unit_price: int) -> int:",
141
+ )
142
+ )
143
+ pairs.append(
144
+ (
145
+ "Task: add a function multiply(a, b) and a unit test\n",
146
+ "Action: grep\nQuery: def multiply",
147
+ )
148
+ )
149
+ return pairs
150
+
151
+
152
+ def all_pairs() -> list[tuple[str, str]]:
153
+ return seed_pairs()
154
+
155
+
156
+ def system_prompt() -> str:
157
+ return AGENT_SYSTEM
finetune/everyday.py ADDED
@@ -0,0 +1,30 @@
1
+ """Everyday laptop brain vs the public 0.5B sidecar."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ # 0.5B stays on the Hub and in smoke. This is what agent.py should use daily.
8
+ DEFAULT_EVERYDAY_OLLAMA = os.environ.get("OLLAMA_MODEL", "llama3.1:8b")
9
+ TINY_OLLAMA = "qwen2.5-coder:0.5b"
10
+ EVERYDAY_OLLAMA_CHOICES = (
11
+ "llama3.1:8b",
12
+ "qwen2.5-coder:7b",
13
+ "qwen2.5-coder:14b",
14
+ "qwen2.5-coder:32b",
15
+ )
16
+ EVERYDAY_MLX_BASE = "mlx-community/Qwen2.5-Coder-7B-Instruct-4bit"
17
+ EVERYDAY_SLUG = "python-vibe-8b"
18
+ TINY_MODELS = frozenset(
19
+ {
20
+ TINY_OLLAMA,
21
+ "qwen2.5-coder:0.5b",
22
+ "python-vibe",
23
+ "python-vibe-0.5b",
24
+ }
25
+ )
26
+
27
+
28
+ def is_tiny_model(name: str) -> bool:
29
+ lowered = name.strip().lower()
30
+ return lowered in TINY_MODELS or lowered.endswith(":0.5b") or lowered.endswith("-0.5b")
finetune/hf_ollama.py ADDED
@@ -0,0 +1,158 @@
1
+ """Hugging Face GGUFs that are not in the Ollama library.
2
+
3
+ OpenCoder 8B and SWE-agent-LM 7B fit this laptop as Q4_K_M (~4.7 GB
4
+ each). They are not `ollama pull` tags. Download the GGUF, write a
5
+ Modelfile that only names the file, then `ollama create`. The harness
6
+ already sends the agent system prompt; do not bake it into the tag.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import shutil
12
+ import subprocess
13
+ import tempfile
14
+ from collections.abc import Callable, Sequence
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ Downloader = Callable[[str, str], Path]
19
+ Creator = Callable[[str, Path], None]
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class ImportSpec:
24
+ key: str
25
+ ollama_tag: str
26
+ source: str
27
+ gguf_repo: str
28
+ filename: str
29
+ default_quant: str
30
+ about_gb: float
31
+ license: str
32
+ note: str
33
+
34
+
35
+ IMPORTS: dict[str, ImportSpec] = {
36
+ "opencoder": ImportSpec(
37
+ key="opencoder",
38
+ ollama_tag="opencoder:8b",
39
+ source="infly/OpenCoder-8B-Instruct",
40
+ gguf_repo="bartowski/OpenCoder-8B-Instruct-GGUF",
41
+ filename="OpenCoder-8B-Instruct-{quant}.gguf",
42
+ default_quant="Q4_K_M",
43
+ about_gb=4.7,
44
+ license="INF",
45
+ note="Code instruct. Not trained on python-vibe Action:.",
46
+ ),
47
+ "swe-agent-lm": ImportSpec(
48
+ key="swe-agent-lm",
49
+ ollama_tag="swe-agent-lm:7b",
50
+ source="SWE-bench/SWE-agent-LM-7B",
51
+ gguf_repo="mradermacher/SWE-agent-LM-7B-GGUF",
52
+ filename="SWE-agent-LM-7B.{quant}.gguf",
53
+ default_quant="Q4_K_M",
54
+ about_gb=4.7,
55
+ license="Apache-2.0",
56
+ note="Qwen2.5-Coder-7B-Instruct plus 5k SWE-agent traces. Their tools, not Action:.",
57
+ ),
58
+ }
59
+
60
+ LAPTOP_QUANT = "Q4_K_M"
61
+
62
+
63
+ def resolve(name: str) -> ImportSpec:
64
+ try:
65
+ return IMPORTS[name]
66
+ except KeyError:
67
+ known = ", ".join(sorted(IMPORTS))
68
+ raise SystemExit(f"unknown import {name!r}: use {known}") from None
69
+
70
+
71
+ def names() -> list[str]:
72
+ return sorted(IMPORTS)
73
+
74
+
75
+ def gguf_filename(spec: ImportSpec, quant: str | None = None) -> str:
76
+ chosen = (quant or spec.default_quant).strip()
77
+ if not chosen:
78
+ raise SystemExit("quant is empty")
79
+ return spec.filename.format(quant=chosen)
80
+
81
+
82
+ def write_modelfile(gguf: Path, dest: Path) -> Path:
83
+ if not gguf.is_file():
84
+ raise SystemExit(f"no GGUF: {gguf}")
85
+ dest.parent.mkdir(parents=True, exist_ok=True)
86
+ dest.write_text(f"FROM {gguf.resolve()}\n", encoding="utf-8")
87
+ return dest
88
+
89
+
90
+ def _hf_download(repo_id: str, filename: str) -> Path:
91
+ from huggingface_hub import hf_hub_download
92
+
93
+ from finetune.huggingface_store import optional_token
94
+
95
+ return Path(
96
+ hf_hub_download(
97
+ repo_id=repo_id,
98
+ filename=filename,
99
+ token=optional_token(),
100
+ )
101
+ )
102
+
103
+
104
+ def download_gguf(
105
+ spec: ImportSpec,
106
+ *,
107
+ quant: str | None = None,
108
+ downloader: Downloader | None = None,
109
+ ) -> Path:
110
+ filename = gguf_filename(spec, quant)
111
+ print(f"downloading https://huggingface.co/{spec.gguf_repo}/{filename}")
112
+ fetch = downloader or _hf_download
113
+ return fetch(spec.gguf_repo, filename)
114
+
115
+
116
+ def _ollama_create(tag: str, gguf: Path) -> None:
117
+ ollama = shutil.which("ollama")
118
+ if not ollama:
119
+ raise SystemExit("ollama not on PATH")
120
+ with tempfile.TemporaryDirectory() as tmp:
121
+ dest = write_modelfile(gguf, Path(tmp) / "Modelfile")
122
+ subprocess.check_call([ollama, "create", tag, "-f", str(dest)])
123
+
124
+
125
+ def import_one(
126
+ name: str,
127
+ *,
128
+ quant: str | None = None,
129
+ create: bool = True,
130
+ downloader: Downloader | None = None,
131
+ creator: Creator | None = None,
132
+ ) -> tuple[ImportSpec, Path]:
133
+ spec = resolve(name)
134
+ gguf = download_gguf(spec, quant=quant, downloader=downloader)
135
+ if create:
136
+ make = creator or _ollama_create
137
+ make(spec.ollama_tag, gguf)
138
+ return spec, gguf
139
+
140
+
141
+ def import_many(
142
+ keys: Sequence[str],
143
+ *,
144
+ quant: str | None = None,
145
+ create: bool = True,
146
+ downloader: Downloader | None = None,
147
+ creator: Creator | None = None,
148
+ ) -> list[tuple[ImportSpec, Path]]:
149
+ return [
150
+ import_one(
151
+ key,
152
+ quant=quant,
153
+ create=create,
154
+ downloader=downloader,
155
+ creator=creator,
156
+ )
157
+ for key in keys
158
+ ]
@@ -0,0 +1,144 @@
1
+ """Download official Hub weights; upload only to HF_USER / HF_REPO / whoami."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import shutil
8
+ from pathlib import Path
9
+
10
+ from finetune.models import ModelSpec, publish_hf_repo
11
+ from finetune.paths import PROJECT_ROOT
12
+
13
+ CARDS = PROJECT_ROOT / "cards"
14
+ BEST_ADAPTER = "0000100_adapters.safetensors"
15
+ _HUB_CONFIG_KEYS = ("fine_tune_type", "num_layers", "lora_parameters")
16
+
17
+
18
+ def require_token() -> str:
19
+ token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
20
+ if token:
21
+ return token
22
+ try:
23
+ from huggingface_hub import get_token
24
+
25
+ token = get_token()
26
+ except Exception:
27
+ token = None
28
+ if not token:
29
+ raise SystemExit(
30
+ "No Hugging Face token. Run `hf auth login` or export HF_TOKEN. "
31
+ "Uploads go to HF_REPO, or HF_USER/<slug>, or your logged-in account — "
32
+ "never to the official repo unless that is you."
33
+ )
34
+ return token
35
+
36
+
37
+ def write_card(spec: ModelSpec, dest: Path) -> Path:
38
+ src = CARDS / f"{spec.name}.md"
39
+ if not src.is_file():
40
+ raise FileNotFoundError(src)
41
+ dest.mkdir(parents=True, exist_ok=True)
42
+ readme = dest / "README.md"
43
+ readme.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
44
+ return readme
45
+
46
+
47
+ def push_card(spec: ModelSpec, *, token: str) -> str:
48
+ """Upload only the model card, leaving the weights untouched.
49
+
50
+ The description changes far more often than the weights do, and
51
+ re-uploading a folder to change one text file is both slow and a way to
52
+ publish something by accident.
53
+ """
54
+ import tempfile
55
+
56
+ from huggingface_hub import HfApi
57
+
58
+ repo_id = publish_hf_repo(spec)
59
+ with tempfile.TemporaryDirectory() as tmp:
60
+ readme = write_card(spec, Path(tmp))
61
+ HfApi(token=token).upload_file(
62
+ path_or_fileobj=str(readme),
63
+ path_in_repo="README.md",
64
+ repo_id=repo_id,
65
+ repo_type="model",
66
+ commit_message=f"update the {spec.name} card",
67
+ )
68
+ return f"https://huggingface.co/{repo_id}"
69
+
70
+
71
+ def push_folder(spec: ModelSpec, folder: Path, *, private: bool, token: str) -> str:
72
+ if not folder.is_dir() or not any(folder.iterdir()):
73
+ raise FileNotFoundError(f"nothing to upload in {folder}")
74
+ from huggingface_hub import HfApi
75
+
76
+ repo_id = publish_hf_repo(spec)
77
+ write_card(spec, folder)
78
+ api = HfApi(token=token)
79
+ api.create_repo(repo_id, repo_type="model", private=private, exist_ok=True)
80
+ api.upload_folder(
81
+ folder_path=str(folder),
82
+ repo_id=repo_id,
83
+ repo_type="model",
84
+ commit_message=f"save {spec.name} ({folder.name})",
85
+ )
86
+ return f"https://huggingface.co/{repo_id}"
87
+
88
+
89
+ def optional_token() -> str | None:
90
+ try:
91
+ return require_token()
92
+ except SystemExit:
93
+ return None
94
+
95
+
96
+ def _weights_file(adapter_dir: Path) -> Path | None:
97
+ best = adapter_dir / BEST_ADAPTER
98
+ latest = adapter_dir / "adapters.safetensors"
99
+ if best.is_file():
100
+ return best
101
+ if latest.is_file():
102
+ return latest
103
+ return None
104
+
105
+
106
+ def stage_adapter_bundle(spec: ModelSpec) -> Path:
107
+ """Copy the best checkpoint + a path-free config into a folder safe to upload."""
108
+ src = spec.adapter_path
109
+ weights = _weights_file(src)
110
+ if weights is None:
111
+ raise FileNotFoundError(f"no adapters in {src}")
112
+ dest = spec.adapter_path.parent / f"{spec.name}-hub"
113
+ if dest.exists():
114
+ shutil.rmtree(dest)
115
+ dest.mkdir(parents=True)
116
+ shutil.copy2(weights, dest / "adapters.safetensors")
117
+ cfg_path = src / "adapter_config.json"
118
+ raw = json.loads(cfg_path.read_text(encoding="utf-8")) if cfg_path.is_file() else {}
119
+ slim = {key: raw[key] for key in _HUB_CONFIG_KEYS if key in raw}
120
+ (dest / "adapter_config.json").write_text(
121
+ json.dumps(slim, indent=2) + "\n", encoding="utf-8"
122
+ )
123
+ write_card(spec, dest)
124
+ return dest
125
+
126
+
127
+ def pull_folder(spec: ModelSpec, dest: Path, *, token: str | None) -> Path:
128
+ from huggingface_hub import snapshot_download
129
+
130
+ dest.mkdir(parents=True, exist_ok=True)
131
+ snapshot_download(
132
+ repo_id=spec.hf_repo,
133
+ local_dir=str(dest),
134
+ token=token,
135
+ )
136
+ return dest
137
+
138
+
139
+ def ensure_adapters(spec: ModelSpec) -> Path:
140
+ """Local adapters if present; otherwise download the public Hub repo."""
141
+ if _weights_file(spec.adapter_path) is not None:
142
+ return spec.adapter_path
143
+ print(f"downloading https://huggingface.co/{spec.hf_repo} → {spec.adapter_path}")
144
+ return pull_folder(spec, spec.adapter_path, token=optional_token())
finetune/models.py ADDED
@@ -0,0 +1,74 @@
1
+ """Qwen2.5-Coder-0.5B 4-bit — fits a cheap cloud box (~400 MB)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from finetune.paths import ADAPTERS_ROOT, FUSED_ROOT
10
+ from finetune.systems import PYTHON_VIBE_SYSTEM
11
+
12
+ # Published weights anyone may download. Not a contributor identity.
13
+ # Uploads never use this unless HF_REPO / HF_USER / `hf auth login` say so.
14
+ OFFICIAL_HF_REPO = "YauhenBichel/python-vibe-0.5b"
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ModelSpec:
19
+ name: str
20
+ mlx_base: str
21
+ ollama_base: str
22
+ hf_repo: str
23
+ system: str
24
+ adapter_path: Path
25
+ fused_path: Path
26
+ ram_mb: int
27
+
28
+ @property
29
+ def hf_slug(self) -> str:
30
+ return self.hf_repo.rsplit("/", 1)[-1]
31
+
32
+
33
+ def logged_in_hf_user() -> str | None:
34
+ try:
35
+ from huggingface_hub import whoami
36
+
37
+ info = whoami()
38
+ except Exception:
39
+ return None
40
+ if isinstance(info, dict):
41
+ name = info.get("name")
42
+ if isinstance(name, str) and name:
43
+ return name
44
+ return None
45
+
46
+
47
+ def publish_hf_repo(spec: ModelSpec) -> str:
48
+ """Where *this machine* may upload. Never defaults to the official account."""
49
+ override = (os.environ.get("HF_REPO") or os.environ.get("PYTHON_VIBE_HF_REPO") or "").strip()
50
+ if override:
51
+ return override
52
+ user = (os.environ.get("HF_USER") or "").strip() or logged_in_hf_user()
53
+ if not user:
54
+ raise SystemExit(
55
+ "Refusing to upload to the official Hub repo. Set HF_USER or HF_REPO "
56
+ f"to your namespace (example: HF_USER=alice → alice/{spec.hf_slug}), "
57
+ "or run `hf auth login` as yourself. "
58
+ f"People download official weights from {spec.hf_repo}."
59
+ )
60
+ return f"{user}/{spec.hf_slug}"
61
+
62
+
63
+ SPECS: dict[str, ModelSpec] = {
64
+ "python-vibe": ModelSpec(
65
+ name="python-vibe",
66
+ mlx_base="mlx-community/Qwen2.5-Coder-0.5B-Instruct-4bit",
67
+ ollama_base="qwen2.5-coder:0.5b",
68
+ hf_repo=OFFICIAL_HF_REPO,
69
+ system=PYTHON_VIBE_SYSTEM,
70
+ adapter_path=ADAPTERS_ROOT / "python-vibe",
71
+ fused_path=FUSED_ROOT / "python-vibe",
72
+ ram_mb=400,
73
+ ),
74
+ }
finetune/paths.py ADDED
@@ -0,0 +1,11 @@
1
+ """Default locations for this project's outputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
8
+ DATA_ROOT = PROJECT_ROOT / "data"
9
+ ADAPTERS_ROOT = PROJECT_ROOT / "adapters"
10
+ FUSED_ROOT = PROJECT_ROOT / "fused"
11
+ CONFIGS_ROOT = PROJECT_ROOT / "configs"