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.
- finetune/__init__.py +1 -0
- finetune/agent_system.py +41 -0
- finetune/agent_traces.py +157 -0
- finetune/everyday.py +30 -0
- finetune/hf_ollama.py +158 -0
- finetune/huggingface_store.py +144 -0
- finetune/models.py +74 -0
- finetune/paths.py +11 -0
- finetune/python_vibe.py +788 -0
- finetune/splits.py +54 -0
- finetune/systems.py +9 -0
- harness/__init__.py +42 -0
- harness/__main__.py +8 -0
- harness/act/__init__.py +6 -0
- harness/act/autofix/__init__.py +110 -0
- harness/act/autofix/additions.py +217 -0
- harness/act/autofix/conflicts.py +124 -0
- harness/act/autofix/cover.py +419 -0
- harness/act/autofix/mechanical.py +151 -0
- harness/act/autofix/missing_imports.py +50 -0
- harness/act/autofix/moves.py +439 -0
- harness/act/autofix/names.py +339 -0
- harness/act/autofix/scaffold.py +224 -0
- harness/act/code.py +157 -0
- harness/act/gate.py +229 -0
- harness/act/parse.py +247 -0
- harness/act/patch_fix.py +138 -0
- harness/act/tools.py +244 -0
- harness/agent/__init__.py +11 -0
- harness/agent/dispatch.py +235 -0
- harness/agent/loop.py +699 -0
- harness/agent/options.py +144 -0
- harness/agent/policy.py +856 -0
- harness/agent/prompt.py +170 -0
- harness/cli.py +393 -0
- harness/editor_kit.py +265 -0
- harness/guard/__init__.py +6 -0
- harness/guard/fallbacks.py +6 -0
- harness/guard/loop_guard.py +57 -0
- harness/guard/python_vibe.py +68 -0
- harness/guard/run.py +41 -0
- harness/guard/types.py +19 -0
- harness/locate.py +767 -0
- harness/mcp_stdio.py +306 -0
- harness/memory/__init__.py +5 -0
- harness/memory/conversation.py +104 -0
- harness/model/__init__.py +6 -0
- harness/model/chat_backend.py +100 -0
- harness/model/engine.py +165 -0
- harness/model/ollama_generate.py +60 -0
- harness/model/openai_generate.py +156 -0
- harness/model/outbound.py +83 -0
- harness/model/route.py +90 -0
- harness/observe/__init__.py +6 -0
- harness/observe/eval_gate.py +80 -0
- harness/observe/eval_loop.py +185 -0
- harness/observe/eval_tasks.py +399 -0
- harness/observe/report_md.py +102 -0
- harness/observe/trace_record.py +79 -0
- harness/openai_api.py +81 -0
- harness/paths.py +88 -0
- harness/py.typed +0 -0
- harness/scan/__init__.py +6 -0
- harness/scan/app_spec.py +338 -0
- harness/scan/design.py +112 -0
- harness/scan/existing.py +131 -0
- harness/scan/layout.py +254 -0
- harness/scan/names.py +308 -0
- harness/scan/project_brief.py +287 -0
- harness/scan/project_docs.py +42 -0
- harness/scan/project_scan.py +49 -0
- harness/scan/repo_map.py +101 -0
- harness/secrets.py +39 -0
- harness/server.py +199 -0
- harness/ship/__init__.py +1 -0
- harness/ship/bot_pr.py +221 -0
- harness/ship/git_ship.py +262 -0
- harness/ship/identity.py +62 -0
- harness/ship/ticket.py +251 -0
- harness/skillkit/__init__.py +6 -0
- harness/skillkit/catalog.py +241 -0
- harness/skillkit/refuse_change.py +640 -0
- harness/skillkit/refuse_finish.py +295 -0
- harness/skillkit/target.py +238 -0
- harness/task.py +717 -0
- py_harness_cli-0.3.0.dist-info/METADATA +177 -0
- py_harness_cli-0.3.0.dist-info/RECORD +92 -0
- py_harness_cli-0.3.0.dist-info/WHEEL +5 -0
- py_harness_cli-0.3.0.dist-info/entry_points.txt +3 -0
- py_harness_cli-0.3.0.dist-info/licenses/LICENSE +202 -0
- py_harness_cli-0.3.0.dist-info/licenses/NOTICE +6 -0
- 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."""
|
finetune/agent_system.py
ADDED
|
@@ -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
|
+
"""
|
finetune/agent_traces.py
ADDED
|
@@ -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"
|