plm-software 0.1.0__tar.gz

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.
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .DS_Store
5
+ logs/
6
+ .worktrees/
7
+ .benchmarks/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ro-rbl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.5
2
+ Name: plm-software
3
+ Version: 0.1.0
4
+ Summary: plm pack: Code tasks, checked by running the examples in a sandbox
5
+ Project-URL: Homepage, https://github.com/ro-rbl/plm-orchestrator
6
+ Project-URL: Source, https://github.com/ro-rbl/plm-orchestrator
7
+ Project-URL: Issues, https://github.com/ro-rbl/plm-orchestrator/issues
8
+ Author: ro-rbl
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.13
12
+ Requires-Dist: plm-core<0.2,>=0.1
13
+ Description-Content-Type: text/markdown
14
+
15
+ # plm-software
16
+
17
+ Plm pack: Code tasks, checked by running the examples in a sandbox.
18
+
19
+ A domain pack for [plm-core](https://github.com/ro-rbl/plm-orchestrator): install it with `plm packs install software`, which also pulls the
20
+ models it needs into Ollama. Documentation, benchmark results and caveats: https://github.com/ro-rbl/plm-orchestrator
@@ -0,0 +1,6 @@
1
+ # plm-software
2
+
3
+ Plm pack: Code tasks, checked by running the examples in a sandbox.
4
+
5
+ A domain pack for [plm-core](https://github.com/ro-rbl/plm-orchestrator): install it with `plm packs install software`, which also pulls the
6
+ models it needs into Ollama. Documentation, benchmark results and caveats: https://github.com/ro-rbl/plm-orchestrator
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "plm-software"
7
+ version = "0.1.0"
8
+ description = "plm pack: Code tasks, checked by running the examples in a sandbox"
9
+ readme = "README.md"
10
+ authors = [{ name = "ro-rbl" }]
11
+ requires-python = ">=3.13"
12
+ license = "MIT"
13
+ dependencies = ["plm-core>=0.1,<0.2"]
14
+
15
+ [project.entry-points."plm.packs"]
16
+ software = "plm_software.manifest:PACK"
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ packages = ["src/plm_software"]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/ro-rbl/plm-orchestrator"
23
+ Source = "https://github.com/ro-rbl/plm-orchestrator"
24
+ Issues = "https://github.com/ro-rbl/plm-orchestrator/issues"
@@ -0,0 +1 @@
1
+ """Software pack: code tasks, checked by running the examples (and optionally generated tests)."""
@@ -0,0 +1,12 @@
1
+ """Which tasks the software pack claims: a request to write code, or a def signature."""
2
+
3
+ import math
4
+ import re
5
+
6
+ SOFTWARE_TASK = re.compile(r"\b(?:write|implement|create|code)\b.{0,40}\b(?:function|class|method|program)\b|\bdef \w+\s*\(",
7
+ re.IGNORECASE | re.DOTALL)
8
+
9
+
10
+ def claims(text):
11
+ """Certain whenever the text asks for code, so software wins over every numeric pack."""
12
+ return math.inf if SOFTWARE_TASK.search(text) else 0
@@ -0,0 +1,37 @@
1
+ """Prompting for code tasks and pulling the code back out of the reply."""
2
+
3
+ import re
4
+
5
+ from plm_software.sandbox import ALLOWED_IMPORTS
6
+
7
+ # v1 said "including any imports" and listed typing; small models then wrote
8
+ # "from typing import int", which crashes (13 of the 0.6B and 4B errors).
9
+ PROMPT_VERSION = 2
10
+ CODE_FORMAT = (
11
+ "Reply with one ```python code block containing the complete implementation. Import a module "
12
+ "only if your code uses it; built-in types such as int, str, list and dict need no import. "
13
+ "You may only import these standard-library modules: "
14
+ + ", ".join(sorted(ALLOWED_IMPORTS))
15
+ + ". Do not include tests, example usage or input()."
16
+ )
17
+
18
+ CODE_RE = re.compile(r"```(?:python|py)?[ \t]*\n(.*?)```", re.DOTALL | re.IGNORECASE)
19
+
20
+
21
+ def build_prompt(task):
22
+ if not task["examples"]:
23
+ return f"{task['prompt']}\n\n{CODE_FORMAT}"
24
+ examples = "\n".join(f" {e}" for e in task["examples"])
25
+ return f"{task['prompt']}\n\nExamples (each expression must be True):\n{examples}\n\n{CODE_FORMAT}"
26
+
27
+
28
+ def extract_code(text, entry):
29
+ """The last fenced block that defines `entry`; else the last block; else unfenced code."""
30
+ defines = re.compile(rf"^\s*(?:def|class)\s+{re.escape(entry)}\b", re.MULTILINE)
31
+ blocks = CODE_RE.findall(text or "")
32
+ for block in reversed(blocks):
33
+ if defines.search(block):
34
+ return block
35
+ if blocks:
36
+ return blocks[-1]
37
+ return text if defines.search(text or "") else None
@@ -0,0 +1,153 @@
1
+ """Software-pack controller: pick a level, have a specialist write the code, check it, escalate.
2
+
3
+ The check runs the code in the sandbox: it must load, define the requested
4
+ function or class, and pass every example given in the task.
5
+
6
+ Until 2026-10-24 every level used a cheap-first cascade (4B -> 8B -> coder),
7
+ picked on our 45-task eval: 40/45 at 56% of always-coder compute (coder alone
8
+ 39/45). The public benchmarks (evals/bench_code.py, CODE_RULE fixed before
9
+ them, accuracy first as the user chose) disagreed: HumanEval+ and MBPP+ pooled,
10
+ 540 tasks:
11
+
12
+ always coder 14B 410/540 (76%) at 100% of always-coder compute
13
+ cascade 4B -> 8B -> coder (old) 387/540 (72%) at 64%
14
+ cascade 8B -> coder 394/540 (73%) at 77%
15
+ always 8B 353/540 (65%) at 55%
16
+
17
+ The coder beats the cascade by +6 to +40 tasks (paired 95%). The cascade lost
18
+ because the examples check rarely sees a wrong HumanEval answer (its docstrings
19
+ show no parseable examples: 14 of 112 wrong answers caught). So every level goes
20
+ to the coder; the level only sets the token budget.
21
+
22
+ thorough=True adds tests written by the large model from the task alone
23
+ (plm/software/testgen.py). In the eval that reached 42/45 but cost 228% of
24
+ always-coder compute: about 1 in 8 generated tests is itself wrong, which flags
25
+ correct code and forces escalation. So it is opt-in, not the default. When no
26
+ answer passes everything, thorough mode returns the one that passed the examples
27
+ and the most generated tests.
28
+
29
+ Model names can be overridden with PLM_SMALL_MODEL, PLM_LARGE_MODEL and PLM_CODER_MODEL.
30
+ """
31
+
32
+ import os
33
+ import re
34
+ import time
35
+
36
+ from plm.ollama import ask
37
+
38
+ from plm_software.codegen import build_prompt, extract_code
39
+ from plm_software.levels import classify
40
+ from plm_software.sandbox import run_tests
41
+ from plm_software.testgen import failing_tests, generate_tests
42
+
43
+ MODELS = {
44
+ "small": os.environ.get("PLM_SMALL_MODEL", "hf.co/bartowski/Qwen_Qwen3-4B-GGUF:Q4_K_M"),
45
+ "large": os.environ.get("PLM_LARGE_MODEL", "qwen3:8b"),
46
+ "coder": os.environ.get("PLM_CODER_MODEL", "qwen2.5-coder:14b"),
47
+ }
48
+ TEST_WRITER = "large"
49
+
50
+ # Per level: token budget and the escalation chain of model keys. Budgets are
51
+ # >2.5x the longest passing answer at that level in the eval sweep; the coder hit none on the benchmarks.
52
+ CHAIN = ["coder"]
53
+ LEVELS = {
54
+ "calm": {"num_predict": 512, "chain": CHAIN},
55
+ "alert": {"num_predict": 1024, "chain": CHAIN},
56
+ "intensive": {"num_predict": 1536, "chain": CHAIN},
57
+ }
58
+
59
+ ENTRY_RE = re.compile(r"`(\w+)\s*\(|\bdef\s+(\w+)\s*\(|`?\bclass\s+`?(\w+)")
60
+ EXAMPLE_RE = re.compile(r"^\s*(?:assert\s+|>>>\s*)(.+?)\s*$", re.MULTILINE)
61
+
62
+
63
+ def parse_task(text):
64
+ """Split pasted task text into a description, the entry name and example expressions."""
65
+ m = ENTRY_RE.search(text)
66
+ entry = next((g for g in m.groups() if g), None) if m else None
67
+ examples = [e for e in EXAMPLE_RE.findall(text) if re.search(r"==|\bis\b|\bin\b|raises\(", e)]
68
+ description = EXAMPLE_RE.sub("", text).strip()
69
+ return {"prompt": description, "entry": entry, "examples": examples}
70
+
71
+
72
+ def attempt(task, model, num_predict, timeout):
73
+ """One model writes the code; it is checked against the task's own examples."""
74
+ start = time.time()
75
+ r = ask(model, build_prompt(task), False, num_predict, timeout)
76
+ code = extract_code(r["response"], task["entry"] or "")
77
+ step = {"model": model, "tokens": r["eval_tokens"], "prompt_tokens": r["prompt_tokens"], "model_text": r["response"], "code": code,
78
+ "capped": r["done_reason"] == "length", "issues": [], "generated_failures": 0}
79
+ if code is None:
80
+ step["issues"].append("no code block")
81
+ else:
82
+ must_define = [f"callable({task['entry']})"] if task["entry"] else []
83
+ result = run_tests(code, must_define + task["examples"])
84
+ step["examples"] = result
85
+ if result["error"]:
86
+ step["issues"].append(result["error"])
87
+ else:
88
+ step["issues"] += [f"failed: {t}" + (f" ({why})" if why != "evaluated to False" else "")
89
+ for t, why in result["failures"]]
90
+ step["seconds"] = time.time() - start
91
+ return step
92
+
93
+
94
+ def check_generated(step, task, tests):
95
+ """Run the generated tests on code that already passed the examples."""
96
+ result = run_tests(step["code"], tests)
97
+ fails = [(t, "error") for t in tests] if result["error"] else failing_tests(result, task["entry"])
98
+ step["generated_failures"] = len(fails)
99
+ step["issues"] += [f"failed generated test: {t}" for t, _ in fails]
100
+
101
+
102
+ def solve(text, level=None, timeout=600, thorough=False):
103
+ """Write code for one task. Returns the code plus a record of every model call."""
104
+ start = time.time()
105
+ task = parse_task(text)
106
+ if level:
107
+ reason = "set by caller"
108
+ else:
109
+ level, reason = classify(task["prompt"])
110
+ plan = LEVELS[level]
111
+
112
+ steps, writer, tests = [], None, None
113
+ for model_key in plan["chain"]:
114
+ step = attempt(task, MODELS[model_key], plan["num_predict"], timeout)
115
+ steps.append(step)
116
+ if thorough and not step["issues"] and task["entry"]:
117
+ if tests is None: # only pay for test writing once some code passes the examples
118
+ tests, writer = generate_tests(task, MODELS[TEST_WRITER], timeout=timeout)
119
+ writer["tests"] = tests
120
+ check_generated(step, task, tests)
121
+ if not step["issues"]:
122
+ break
123
+
124
+ final = steps[-1]
125
+ if thorough and final["issues"]:
126
+ passed_examples = [s for s in steps if s["code"] and not any(
127
+ not i.startswith("failed generated test") for i in s["issues"])]
128
+ if passed_examples:
129
+ # Fewest generated-test failures; ties go to the later, stronger model,
130
+ # which is the rule evals/simulate_code_routing.py measured.
131
+ final = min(passed_examples, key=lambda s: (s["generated_failures"], -steps.index(s)))
132
+ n_examples = len(task["examples"])
133
+ reason += (f"; {n_examples} example{'s' if n_examples != 1 else ''} to check" if n_examples
134
+ else "; no examples given, so only loading is checked")
135
+ if writer:
136
+ reason += f"; {len(tests)} generated tests"
137
+ # Loading and defining the entry point alone don't show the code works.
138
+ checked = bool(n_examples or tests)
139
+ return {
140
+ "pack": "software",
141
+ "task": text,
142
+ "level": level,
143
+ "reason": reason,
144
+ "answer": {"code": final["code"]} if final["code"] else None,
145
+ "checked": checked,
146
+ "unchecked_reason": None if checked else "no examples given, so only loading was checked",
147
+ "trusted": checked and not final["issues"],
148
+ "final_step": steps.index(final) + 1,
149
+ "steps": steps,
150
+ "test_writer": writer,
151
+ "tokens": sum(s["tokens"] for s in steps) + (writer["tokens"] if writer else 0),
152
+ "seconds": time.time() - start,
153
+ }
@@ -0,0 +1,35 @@
1
+ """Adrenaline level for a coding task, from its description: rules only, no model.
2
+
3
+ calm one transformation: a loop, a comprehension, a builtin
4
+ alert one standard algorithm, or input-validation rules to get exactly right
5
+ intensive designing a data structure, search, optimization/DP, or writing a parser
6
+
7
+ The cue families are written from those definitions, not fitted per task.
8
+ Evaluate them with: .venv/bin/python evals/eval_levels.py --pack software
9
+ """
10
+
11
+ import re
12
+
13
+ INTENSIVE_CUES = {
14
+ "design": r"write a class\b",
15
+ "search": r"shortest|\bpaths?\b|graph|backtrack|queens|ladder|island|sudoku",
16
+ "optimize": r"\b(?:minimum|maximum|fewest|largest number|longest|lexicographically smallest)\b",
17
+ "dp": r"dynamic programming|subsequence|knapsack|levenshtein|edit distance",
18
+ "parser": r"\bparses?\b|\bevaluates?\b|precedence|(?:without|do not use) (?:using )?(?:eval|the \w+ module)",
19
+ }
20
+ ALERT_CUES = {
21
+ "rules": r"\bvalid\b|raise \w*error|leading zeros|at most once|in that order|unbalanced|balanced",
22
+ "algorithm": r"binary search|o\(log n\)|o\(n\)|linear time|merge|frequen|anagram|nested|matrix|rotate"
23
+ r"|roman|encode|cipher|prefix|triangle|bracket|interval|range strings",
24
+ }
25
+
26
+
27
+ def classify(text):
28
+ """Return (level, reason)."""
29
+ hits = lambda cues: sorted(n for n, p in cues.items() if re.search(p, text, re.IGNORECASE)) # noqa: E731
30
+ intensive, alert = hits(INTENSIVE_CUES), hits(ALERT_CUES)
31
+ if intensive:
32
+ return "intensive", "cues: " + ", ".join(intensive)
33
+ if alert:
34
+ return "alert", "cues: " + ", ".join(alert)
35
+ return "calm", "no algorithm cues"
@@ -0,0 +1,9 @@
1
+ """The software pack's manifest: what the router needs to know about it."""
2
+
3
+ from plm.pack import Pack
4
+
5
+ from plm_software import controller
6
+ from plm_software.claims import claims
7
+
8
+ PACK = Pack(name="software", version="0.1.0", claims=claims, priority=100, solve=controller.solve,
9
+ levels=controller.LEVELS, models=controller.MODELS, eval_set="evals/software/tasks.jsonl")
@@ -0,0 +1,127 @@
1
+ """Run model-written code against test expressions in an isolated process.
2
+
3
+ Each test is a Python expression that must evaluate to True. The code runs in a
4
+ separate `python -I -S` process in an empty temp directory, with a time limit
5
+ per test and for the whole run. Imports are limited to a standard-library
6
+ allowlist. Like plm/numeric/calc.py, the screen guards against accidents; it is not a
7
+ security sandbox.
8
+ """
9
+
10
+ import json
11
+ import re
12
+ import subprocess
13
+ import sys
14
+ import tempfile
15
+
16
+ ALLOWED_IMPORTS = {
17
+ "math", "collections", "heapq", "itertools", "functools", "re", "string", "bisect",
18
+ "typing", "dataclasses", "operator", "fractions", "decimal", "copy", "statistics", "enum",
19
+ }
20
+ # Unlike calc.py, dunders are allowed in general (classes need __init__); only the escape hatches are blocked.
21
+ # The builtins are matched only as bare names, so methods like re.compile( stay allowed.
22
+ BLOCKED = re.compile(
23
+ r"(?<![.\w])(?:open|exec|eval|compile|input|breakpoint|globals|locals|vars|__import__)\s*\("
24
+ r"|__(?:builtins|subclasses|globals|code|loader|spec|bases|mro)__"
25
+ )
26
+
27
+ HARNESS = r"""
28
+ import json, signal, sys
29
+
30
+ payload = json.loads(sys.stdin.read())
31
+
32
+ def raises(exc, fn, *args, **kwargs):
33
+ try:
34
+ fn(*args, **kwargs)
35
+ except exc:
36
+ return True
37
+ except Exception:
38
+ return False
39
+ return False
40
+
41
+ def on_alarm(signum, frame):
42
+ raise TimeoutError("test took too long")
43
+
44
+ signal.signal(signal.SIGALRM, on_alarm)
45
+ ns = {"raises": raises, "__name__": "solution"}
46
+ try:
47
+ signal.setitimer(signal.ITIMER_REAL, payload["per_test"])
48
+ exec(compile(payload["code"], "<solution>", "exec"), ns)
49
+ signal.setitimer(signal.ITIMER_REAL, 0)
50
+ except BaseException as e:
51
+ print("\n" + json.dumps({"loaded": False, "error": f"{type(e).__name__}: {e}"[:300], "results": []}))
52
+ sys.exit(0)
53
+
54
+ results = []
55
+ for t in payload["tests"]:
56
+ try:
57
+ signal.setitimer(signal.ITIMER_REAL, payload["per_test"])
58
+ ok = eval(t, ns) is True
59
+ signal.setitimer(signal.ITIMER_REAL, 0)
60
+ results.append([ok, None if ok else "evaluated to False"])
61
+ except BaseException as e:
62
+ signal.setitimer(signal.ITIMER_REAL, 0)
63
+ results.append([False, f"{type(e).__name__}: {e}"[:200]])
64
+ print("\n" + json.dumps({"loaded": True, "error": None, "results": results}))
65
+ """
66
+
67
+
68
+ def screen(code):
69
+ """Return a reason the code is refused, or None if it may run."""
70
+ for line in code.splitlines():
71
+ m = re.match(r"\s*(?:from\s+(\S+)\s+import|import\s+(.+))", line)
72
+ if m:
73
+ names = [m.group(1)] if m.group(1) else [n.split(" as ")[0].strip() for n in m.group(2).split(",")]
74
+ bad = [n for n in names if n.split(".")[0] not in ALLOWED_IMPORTS]
75
+ if bad:
76
+ return f"import not allowed: {', '.join(bad)}"
77
+ hit = BLOCKED.search(code)
78
+ return f"blocked construct: {hit.group().strip()}" if hit else None
79
+
80
+
81
+ def run_tests(code, tests, per_test=5, total=60):
82
+ """Run `code`, then each test. Returns {loaded, error, passed, total, failures}."""
83
+ result = {"loaded": False, "error": None, "passed": 0, "total": len(tests), "failures": []}
84
+ refused = screen(code)
85
+ if refused:
86
+ result["error"] = f"refused: {refused}"
87
+ return result
88
+ payload = json.dumps({"code": code, "tests": tests, "per_test": per_test})
89
+ with tempfile.TemporaryDirectory() as tmp:
90
+ try:
91
+ p = subprocess.run([sys.executable, "-I", "-S", "-c", HARNESS], input=payload, cwd=tmp,
92
+ timeout=total, capture_output=True, text=True)
93
+ except subprocess.TimeoutExpired:
94
+ result["error"] = f"timed out after {total}s"
95
+ return result
96
+ try:
97
+ out = json.loads(p.stdout.strip().splitlines()[-1]) # the code's own prints come first
98
+ except (IndexError, json.JSONDecodeError):
99
+ result["error"] = f"harness crashed: {(p.stderr.strip().splitlines() or ['?'])[-1][:200]}"
100
+ return result
101
+ result["loaded"], result["error"] = out["loaded"], out["error"]
102
+ result["passed"] = sum(ok for ok, _ in out["results"])
103
+ result["failures"] = [(t, why) for t, (ok, why) in zip(tests, out["results"]) if not ok]
104
+ return result
105
+
106
+
107
+ if __name__ == "__main__":
108
+ good = "def add(a, b):\n return a + b\n"
109
+ r = run_tests(good, ["add(1, 2) == 3", "add(-1, 1) == 0"])
110
+ assert (r["passed"], r["total"], r["error"]) == (2, 2, None), r
111
+ r = run_tests(good, ["add(1, 2) == 4", "add(1) == 1"])
112
+ assert r["passed"] == 0 and "TypeError" in r["failures"][1][1], r
113
+ r = run_tests("def f():\n while True: pass\n", ["f() is None", "1 == 1"], per_test=1)
114
+ assert r["passed"] == 1 and "TimeoutError" in r["failures"][0][1], r # one hang fails one test
115
+ assert run_tests("def f(:\n", ["f()"])["error"].startswith("SyntaxError"), run_tests("def f(:\n", ["f()"])
116
+ assert "not allowed" in run_tests("import os\n", [])["error"]
117
+ assert "blocked" in run_tests("x = open('f')\n", [])["error"]
118
+ assert "blocked" in run_tests("x = ().__class__.__bases__\n", [])["error"]
119
+ assert "blocked" in run_tests("x = compile('1', 'f', 'eval')\n", [])["error"]
120
+ r = run_tests("import re\nP = re.compile(r'\\d+')\n", ["P.fullmatch('12') is not None"])
121
+ assert r["passed"] == 1, r # re.compile is a method call, not the compile builtin
122
+ r = run_tests("import collections\nclass C:\n def __init__(self):\n self.d = collections.deque()\n", ["C().d is not None"])
123
+ assert r["passed"] == 1, r # classes with __init__ and allowed imports run
124
+ r = run_tests("print('noise')\ndef g():\n print('more')\n return 1\n", ["g() == 1"])
125
+ assert r["passed"] == 1, r # the solution's own prints don't break result parsing
126
+ assert run_tests("raise SystemExit(3)\n", ["1 == 1"])["error"].startswith("SystemExit")
127
+ print("sandbox self-test: ok")
@@ -0,0 +1,97 @@
1
+ """Stronger code checks: a second model writes extra test cases from the task alone.
2
+
3
+ The test writer never sees the candidate code, so its tests are independent of
4
+ it. Its expected values can still be wrong, and a wrong test flags correct code;
5
+ evals/gen_tests.py measures how often that happens against reference solutions.
6
+ """
7
+
8
+ import ast
9
+ import re
10
+
11
+ from plm.ollama import ask
12
+
13
+ from plm_software.codegen import CODE_RE
14
+
15
+ TESTGEN_PROMPT_VERSION = 1
16
+ N_TESTS = 8
17
+
18
+
19
+ def build_testgen_prompt(task):
20
+ examples = "\n".join(f" {e}" for e in task["examples"])
21
+ shown = f"\n\nExamples (each expression must be True):\n{examples}" if task["examples"] else ""
22
+ return (
23
+ f"{task['prompt']}{shown}\n\n"
24
+ f"Do not write the implementation. Instead, write {N_TESTS} more test cases for it as Python "
25
+ "expressions that must evaluate to True, one per line, in a ```python code block. Each one must "
26
+ f"call `{task['entry']}` and compare the result with the exact expected value, in the same style "
27
+ "as the examples. Cover cases the examples miss, such as empty input, a single element, "
28
+ "duplicates, negative numbers, unsorted input and boundary values, but only where they make sense "
29
+ "for this task and its stated rules. Work out every expected value carefully from the task "
30
+ "description: a wrong expected value is worse than a missing test."
31
+ )
32
+
33
+
34
+ def parse_tests(text, entry, examples=()):
35
+ """Valid, distinct test expressions that call `entry` and aren't copies of the examples."""
36
+ blocks = CODE_RE.findall(text or "") or [text or ""]
37
+ seen = {e.replace(" ", "") for e in examples}
38
+ tests = []
39
+ for line in "\n".join(blocks).splitlines():
40
+ expr = re.sub(r"^\s*(?:assert\s+|>>>\s*)", "", line).split(" #")[0].strip()
41
+ if not expr or expr.startswith("#") or not re.search(rf"\b{re.escape(entry)}\b", expr):
42
+ continue
43
+ try:
44
+ ast.parse(expr, mode="eval")
45
+ except SyntaxError:
46
+ continue
47
+ key = expr.replace(" ", "")
48
+ if key not in seen:
49
+ seen.add(key)
50
+ tests.append(expr)
51
+ return tests[:N_TESTS]
52
+
53
+
54
+ def is_broken_test(why, entry):
55
+ """True if a test failed because the test itself is malformed, whatever the code does.
56
+
57
+ A NameError for anything other than the entry point means the test uses a
58
+ name it never defined (e.g. a misplaced parenthesis in a lambda), so it
59
+ says nothing about the candidate and is ignored.
60
+ """
61
+ m = re.match(r"NameError: name '(\w+)' is not defined", why or "")
62
+ return bool(m) and m.group(1) != entry
63
+
64
+
65
+ def failing_tests(result, entry):
66
+ """Failures from run_tests() that count against the code (broken tests removed)."""
67
+ return [(t, why) for t, why in result["failures"] if not is_broken_test(why, entry)]
68
+
69
+
70
+ def generate_tests(task, model, num_predict=1024, timeout=600):
71
+ """Ask `model` for extra tests. Returns (tests, call record)."""
72
+ r = ask(model, build_testgen_prompt(task), False, num_predict, timeout)
73
+ tests = parse_tests(r["response"], task["entry"], task["examples"])
74
+ return tests, {"model": model, "tokens": r["eval_tokens"], "prompt_tokens": r["prompt_tokens"], "seconds": r["wall_s"], "raw": r["response"],
75
+ "capped": r["done_reason"] == "length"}
76
+
77
+
78
+ if __name__ == "__main__":
79
+ reply = """Here are tests:
80
+ ```python
81
+ assert f([]) == []
82
+ f([1]) == [1] # single
83
+ f([3, 1]) == [1, 3]
84
+ # a comment
85
+ f([2, 2] == [2, 2]
86
+ g(1) == 2
87
+ f([1]) == [1]
88
+ ```"""
89
+ got = parse_tests(reply, "f", examples=["f([3,1]) == [1,3]"])
90
+ assert got == ["f([]) == []", "f([1]) == [1]"], got # strips assert/comments; drops bad syntax, other names, dupes, examples
91
+ assert parse_tests("no code", "f") == []
92
+ assert is_broken_test("NameError: name 'c' is not defined", "LRUCache")
93
+ assert not is_broken_test("NameError: name 'LRUCache' is not defined", "LRUCache") # code didn't define it
94
+ assert not is_broken_test("evaluated to False", "f")
95
+ res = {"failures": [("t1", "NameError: name 'c' is not defined"), ("t2", "evaluated to False")]}
96
+ assert failing_tests(res, "f") == [("t2", "evaluated to False")]
97
+ print("testgen self-test: ok")