rawintent 4.0.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.
@@ -0,0 +1,336 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import inspect
5
+ import json
6
+ import random
7
+ import re
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any, Iterable, Iterator
11
+
12
+ from ..english_python import EnglishPython
13
+ from .rawlang import Instruction, Program
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class TrainingExample:
18
+ source: str
19
+ target: str
20
+ action: str | None = None
21
+ family: str | None = None
22
+ kind: str = "single"
23
+ split: str | None = None
24
+
25
+ def to_dict(self) -> dict[str, Any]:
26
+ data = {"source": self.source, "target": self.target, "action": self.action}
27
+ if self.family is not None:
28
+ data["family"] = self.family
29
+ if self.kind:
30
+ data["kind"] = self.kind
31
+ if self.split is not None:
32
+ data["split"] = self.split
33
+ return data
34
+
35
+
36
+ _SAMPLE_VALUES: dict[str, list[Any]] = {
37
+ "text": ["hello world", "RawIntent is useful", "The quick brown fox"],
38
+ "value": ["hello world", {"name": "Will", "active": True}, [1, 2, 3]],
39
+ "key": ["secret-key", "demo-key"],
40
+ "url": ["https://example.com", "https://httpbin.org/get"],
41
+ "path": ["notes.txt", "data/example.txt", "./README.md"],
42
+ "source": ["source.txt", "input.txt"], "destination": ["copy.txt", "output.txt"],
43
+ "name": ["HOME", "PATH", "DEMO_VALUE"], "pattern": [r"\\d+", r"[A-Za-z]+"],
44
+ "replacement": ["X", "word"], "algorithm": ["sha256", "sha512"],
45
+ "expression": ["2 + 3 * 4", "sqrt(81) + 1"], "count": [3, 5], "length": [16, 24, 32],
46
+ "low": [1, 10], "high": [10, 100], "minimum": [1, 10], "maximum": [10, 100],
47
+ "start": [1, 10], "stop": [10, 100], "number": [9, 25, 144],
48
+ "base": [2, 3, 10], "exponent": [2, 3, 4], "a": [12, 18, 48], "b": [8, 24, 36],
49
+ "timeout": [20.0], "items": [["red", "blue", "green"], [1, 2, 3, 4]],
50
+ "values": [[1, 2, 3, 4, 5], [10, 20, 30]], "choices": [["red", "blue", "green"], ["alpha", "beta", "gamma"]],
51
+ "width": [40, 72], "places": [2, 4], "numerator": [1, 3], "denominator": [2, 4],
52
+ "year": [2028, 2032], "month": [1, 9, 12], "size": [3, 5],
53
+ }
54
+
55
+
56
+ def _sample_for(name: str, rng: random.Random, action: str = "") -> Any:
57
+ if name == "value":
58
+ if action.startswith(("math.", "bisect.")): return rng.choice([4, 9, 16, 25, 81])
59
+ if action.startswith("json.") or action.startswith(("copy.", "format.pretty", "query.build")):
60
+ return rng.choice([{"name": "RawIntent", "active": True}, [1, 2, 3]])
61
+ return rng.choice(_SAMPLE_VALUES["value"])
62
+ if name == "text" and action == "config.parse": return "[app]\nname=rawintent\nenabled=true"
63
+ if name == "text" and action == "csv.parse": return "name,score\nAda,10\nGrace,9"
64
+ if name == "text" and action in {"gzip.decompress", "zlib.decompress"}:
65
+ return "H4sIAAAAAAAC/8tIzcnJBwCGphA2BQAAAA==" if action == "gzip.decompress" else "eJzLSM3JyQcABiwCFQ=="
66
+ if name == "groups": return [[1, 2], ["a", "b"]]
67
+ if name == "parts": return ["home", "user", "file.txt"]
68
+ if name == "word": return "appl"
69
+ if name == "steps": return 2
70
+ if name in _SAMPLE_VALUES: return rng.choice(_SAMPLE_VALUES[name])
71
+ if name.endswith("_text"): return rng.choice(_SAMPLE_VALUES["text"])
72
+ if "url" in name: return rng.choice(_SAMPLE_VALUES["url"])
73
+ if "path" in name or "file" in name: return rng.choice(_SAMPLE_VALUES["path"])
74
+ if name in {"x", "y", "n", "k"}: return rng.randint(2, 12)
75
+ return "example"
76
+
77
+
78
+ def _arguments_for_intent(action: str, intent: Any, rng: random.Random) -> dict[str, Any]:
79
+ sig = inspect.signature(intent.handler)
80
+ args: dict[str, Any] = {}
81
+ declared = set(intent.entities)
82
+ for p in sig.parameters.values():
83
+ if p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD): continue
84
+ if p.default is inspect.Parameter.empty or p.name in declared:
85
+ args[p.name] = _sample_for(p.name, rng, action)
86
+ return args
87
+
88
+
89
+ def _english_value(name: str, value: Any) -> str:
90
+ if name == "url": return str(value)
91
+ if name in {"low", "minimum", "start"}: return f"minimum {value}"
92
+ if name in {"high", "maximum", "stop"}: return f"maximum {value}"
93
+ if name == "count": return f"count {value}"
94
+ if name == "length": return f"length {value}"
95
+ if name in {"values", "items", "choices"}: return "values " + json.dumps(value, ensure_ascii=False, separators=(",", ":"))
96
+ if isinstance(value, str): return json.dumps(value, ensure_ascii=False)
97
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
98
+
99
+
100
+ def _realize(base: str, args: dict[str, Any]) -> str:
101
+ chunks = [base]
102
+ for name, value in args.items():
103
+ if name == "original_text": continue
104
+ chunks.append(_english_value(name, value))
105
+ return " ".join(chunks)
106
+
107
+
108
+ def _drop_char(text: str, rng: random.Random) -> str:
109
+ c = [i for i, ch in enumerate(text) if ch.isalpha()]
110
+ if len(c) < 6: return text
111
+ i = rng.choice(c); return text[:i] + text[i+1:]
112
+
113
+
114
+ def _swap_chars(text: str, rng: random.Random) -> str:
115
+ c = [i for i in range(len(text)-1) if text[i].isalpha() and text[i+1].isalpha()]
116
+ if not c: return text
117
+ i = rng.choice(c); return text[:i] + text[i+1] + text[i] + text[i+2:]
118
+
119
+
120
+ _REPL = {
121
+ "pick": ["choose", "grab", "select"], "generate": ["make", "create", "produce"],
122
+ "calculate": ["compute", "work out", "figure out"], "random": ["random", "arbitrary"],
123
+ "number": ["number", "integer", "num"], "text": ["text", "string"],
124
+ "file": ["file", "document"], "get": ["get", "grab", "fetch"], "make": ["make", "create", "build"],
125
+ }
126
+
127
+
128
+ def _synonymize(text: str, rng: random.Random) -> str:
129
+ words = text.split()
130
+ for i, w in enumerate(words):
131
+ key = re.sub(r"[^a-z]", "", w.lower())
132
+ if key in _REPL and rng.random() < .7:
133
+ rep = rng.choice(_REPL[key]); words[i] = rep + (w[-1] if w and not w[-1].isalnum() else "")
134
+ return " ".join(words)
135
+
136
+
137
+ def augment(text: str, rng: random.Random, variant: int) -> str:
138
+ v = variant % 18
139
+ if v == 0: return text
140
+ if v == 1: return "please " + text
141
+ if v == 2: return text + " please"
142
+ if v == 3: return _drop_char(text, rng)
143
+ if v == 4: return _swap_chars(text, rng)
144
+ if v == 5: return _synonymize(text, rng)
145
+ if v == 6: return text.lower()
146
+ if v == 7: return text.upper()
147
+ if v == 8: return "hey can you " + _synonymize(text, rng)
148
+ if v == 9: return "yo can u " + _synonymize(text, rng).replace("number", "num")
149
+ if v == 10: return "could you maybe " + _synonymize(text, rng)
150
+ if v == 11: return re.sub(r"\s+", " ", text)
151
+ if v == 12: return text.rstrip(".!?") + rng.choice(["?", "!", "..."])
152
+ if v == 13: return "i need you to " + _synonymize(text, rng)
153
+ if v == 14: return _drop_char(_swap_chars(text, rng), rng)
154
+ if v == 15: return _synonymize(text, rng).replace("characters", "chars").replace("through", "thru")
155
+ if v == 16: return "basically just " + _synonymize(text, rng)
156
+ return "can you just " + text + " for me"
157
+
158
+
159
+ def _family(action: str, index: int, base: str) -> str:
160
+ digest = hashlib.sha1(base.encode("utf-8")).hexdigest()[:8]
161
+ return f"{action}:phrase:{index}:{digest}"
162
+
163
+
164
+ def _target(action: str, intent: Any, rng: random.Random) -> tuple[str, dict[str, Any]] | None:
165
+ args = _arguments_for_intent(action, intent, rng)
166
+ if "original_text" in args: return None
167
+ return Program([Instruction(action, args)]).to_source(), args
168
+
169
+
170
+ def generate_examples(engine: EnglishPython | None = None, *, variants_per_phrase: int = 6, samples_per_phrase: int = 2, seed: int = 7) -> Iterator[TrainingExample]:
171
+ engine = engine or EnglishPython(); rng = random.Random(seed)
172
+ for action, intent in sorted(engine.intents.items()):
173
+ if intent.handler is None or action == "python.call": continue
174
+ for idx, base in enumerate(intent.examples):
175
+ for _ in range(max(1, samples_per_phrase)):
176
+ made = _target(action, intent, rng)
177
+ if made is None: continue
178
+ target, args = made
179
+ for variant in range(max(1, variants_per_phrase)):
180
+ # Noise is applied only to the instruction phrase, never to literal argument values.
181
+ yield TrainingExample(_realize(augment(base, rng, variant), args), target, action, _family(action, idx, base), "single")
182
+ for x in _composition_examples():
183
+ yield x
184
+
185
+
186
+ _COMPOSITIONS = [
187
+ ("random_sqrt", ["pick a random number from 1 to 10 then take its square root", "choose a number one through ten and give me the square root"], 'LET n = random.integer low=1 high=10\nLET root = math.sqrt value=$n\nRETURN $root'),
188
+ ("password_lower", ["make a secure 24 character password then lowercase it", "generate a 24 char secure password and turn it lowercase"], 'LET password = secrets.password length=24\nLET lower = text.lower text=$password\nRETURN $lower'),
189
+ ("uuid_upper", ["make a uuid then uppercase it", "generate a uuid and return it in uppercase"], 'LET id = uuid.v4\nLET upper = text.upper text=$id\nRETURN $upper'),
190
+ ("b64_roundtrip", ["base64 encode hello world then decode it", "encode hello world as base64 and decode the result"], 'LET encoded = base64.encode text="hello world"\nLET decoded = base64.decode text=$encoded\nRETURN $decoded'),
191
+ ("power_sqrt", ["raise 3 to the second power then take the square root", "square three and then get the square root"], 'LET p = math.power base=3 exponent=2\nLET root = math.sqrt value=$p\nRETURN $root'),
192
+ ("choice_upper", ["pick one of red blue green then uppercase it", "randomly choose red blue or green and make it uppercase"], 'LET picked = random.choice items=["red","blue","green"]\nLET upper = text.upper text=$picked\nRETURN $upper'),
193
+ ("token_upper", ["make a secure hex token then uppercase it", "generate a 16 byte secure hex token and capitalize it"], 'LET token = secrets.token_hex length=16\nLET upper = text.upper text=$token\nRETURN $upper'),
194
+ ("slug_upper", ["slugify hello world then uppercase the slug", "make hello world a url slug and uppercase it"], 'LET slug = text.slug text="hello world"\nLET upper = text.upper text=$slug\nRETURN $upper'),
195
+ ("hex_lower", ["hex encode hello world then lowercase it", "convert hello world to hex and make the result lowercase"], 'LET encoded = hex.encode text="hello world"\nLET lower = text.lower text=$encoded\nRETURN $lower'),
196
+ ("url_roundtrip", ["url encode hello world then decode it", "percent encode hello world and decode the result"], 'LET encoded = url.quote text="hello world"\nLET decoded = url.unquote text=$encoded\nRETURN $decoded'),
197
+ ("hash_upper", ["sha256 hash hello world then uppercase the hash", "hash hello world with sha256 and capitalize the digest"], 'LET digest = hash.digest text="hello world" algorithm="sha256"\nLET upper = text.upper text=$digest\nRETURN $upper'),
198
+ ("uuid_lower", ["make a uuid and lowercase it", "generate uuid4 then make the result lowercase"], 'LET id = uuid.v4\nLET lower = text.lower text=$id\nRETURN $lower'),
199
+ ]
200
+
201
+
202
+ def _composition_examples() -> list[TrainingExample]:
203
+ out=[]
204
+ for fam, phrases, target in _COMPOSITIONS:
205
+ for p in phrases: out.append(TrainingExample(p, target, "composition", f"composition:{fam}", "composition"))
206
+ return out
207
+
208
+
209
+ _UNKNOWN_FAMILIES = [
210
+ ("orbit", ["calculate the orbital decay rate of this satellite", "predict how fast this spacecraft orbit will decay"]),
211
+ ("image", ["generate a photorealistic picture of a dragon", "draw me a watercolor mountain image"]),
212
+ ("video", ["render a 3d animation in blender", "make a cinematic video from this prompt"]),
213
+ ("database", ["query my postgres users table", "run this sql against my production database"]),
214
+ ("slack", ["send this message to my slack channel", "post an update in slack"]),
215
+ ("email", ["email my manager that i will be late", "send an email to alex"]),
216
+ ("calendar", ["book a meeting with the whole team next friday", "invite sam to a calendar meeting"]),
217
+ ("browser", ["open chrome and click the login button", "use my browser to fill this form"]),
218
+ ("tensorflow", ["train a tensorflow image classifier", "build me a keras neural network"]),
219
+ ("audio", ["transcribe this audio recording", "remove background noise from my microphone file"]),
220
+ ("pdf", ["digitally sign this pdf", "merge these pdf pages into a booklet"]),
221
+ ("docker", ["start my docker compose stack", "restart the nginx docker container"]),
222
+ ("aws", ["deploy this lambda function to aws", "create an s3 bucket in my account"]),
223
+ ("git", ["rebase my branch onto main", "push these commits to github"]),
224
+ ("vision", ["tell me what is in this photograph", "read the handwritten note in this image"]),
225
+ ("music", ["compose a piano melody", "generate a midi chord progression"]),
226
+ ("maps", ["route me around traffic to boston", "find the fastest driving directions"]),
227
+ ("weather_live", ["tell me the live weather outside right now", "is it currently raining at my house"]),
228
+ ]
229
+
230
+
231
+ def _emit_family(engine: EnglishPython, action: str, intent: Any, base: str, idx: int, split: str, variants: int, samples: int, rng: random.Random) -> list[TrainingExample]:
232
+ out=[]; fam=_family(action, idx, base)
233
+ for _ in range(max(1, samples)):
234
+ made=_target(action, intent, rng)
235
+ if made is None: continue
236
+ target,args=made
237
+ # Evaluation uses a different augmentation region to make wording/noise less train-like.
238
+ offset = 0 if split == "train" else 8
239
+ for v in range(max(1, variants)):
240
+ out.append(TrainingExample(_realize(augment(base, rng, offset+v), args), target, action, fam, "single", split))
241
+ return out
242
+
243
+
244
+ def generate_generalization_splits(engine: EnglishPython | None = None, *, train_variants: int = 8, eval_variants: int = 6, samples_per_phrase: int = 3, seed: int = 7, include_unknown: bool = True, include_compositions: bool = True) -> dict[str, list[TrainingExample]]:
245
+ """Create leakage-resistant train/validation/test sets.
246
+
247
+ Phrase families are assigned to a split *before* augmentation. Thus a held-out base phrase and all
248
+ of its noisy variants never occur in training. Composition families and unknown-capability concepts
249
+ are also split as whole families.
250
+ """
251
+ engine = engine or EnglishPython(); rng=random.Random(seed)
252
+ splits={"train":[], "validation":[], "test":[]}
253
+ for action,intent in sorted(engine.intents.items()):
254
+ if intent.handler is None or action == "python.call": continue
255
+ indices=list(range(len(intent.examples)))
256
+ random.Random(f"{seed}:{action}").shuffle(indices)
257
+ assign={i:"train" for i in indices}
258
+ if len(indices)>=2: assign[indices[-1]]="test"
259
+ if len(indices)>=3: assign[indices[-2]]="validation"
260
+ for i,base in enumerate(intent.examples):
261
+ split=assign[i]; variants=train_variants if split=="train" else eval_variants
262
+ splits[split].extend(_emit_family(engine, action, intent, base, i, split, variants, samples_per_phrase, rng))
263
+
264
+ if include_compositions:
265
+ families=list(_COMPOSITIONS); random.Random(seed+101).shuffle(families)
266
+ n=len(families); n_test=max(2, n//5); n_val=max(1, n//6)
267
+ test_names={x[0] for x in families[:n_test]}; val_names={x[0] for x in families[n_test:n_test+n_val]}
268
+ for fam,phrases,target in _COMPOSITIONS:
269
+ split="test" if fam in test_names else "validation" if fam in val_names else "train"
270
+ variants=train_variants if split=="train" else eval_variants
271
+ for phrase in phrases:
272
+ for v in range(max(1,variants)):
273
+ splits[split].append(TrainingExample(augment(phrase,rng,(0 if split=="train" else 8)+v),target,"composition",f"composition:{fam}","composition",split))
274
+
275
+ if include_unknown:
276
+ unknown=list(_UNKNOWN_FAMILIES); random.Random(seed+202).shuffle(unknown)
277
+ n=len(unknown); n_test=max(3,n//5); n_val=max(2,n//6)
278
+ test_names={x[0] for x in unknown[:n_test]}; val_names={x[0] for x in unknown[n_test:n_test+n_val]}
279
+ for fam,phrases in _UNKNOWN_FAMILIES:
280
+ split="test" if fam in test_names else "validation" if fam in val_names else "train"
281
+ variants=train_variants if split=="train" else eval_variants
282
+ for phrase in phrases:
283
+ for v in range(max(1,variants)):
284
+ splits[split].append(TrainingExample(augment(phrase,rng,(0 if split=="train" else 8)+v),"UNKNOWN_CAPABILITY","unknown",f"unknown:{fam}","unknown",split))
285
+
286
+ # Hard guarantee against family leakage.
287
+ fams={k:{x.family for x in v if x.family} for k,v in splits.items()}
288
+ if fams["train"] & fams["validation"] or fams["train"] & fams["test"] or fams["validation"] & fams["test"]:
289
+ raise RuntimeError("Generalization split leaked a phrase family across datasets.")
290
+ for k in splits: random.Random(f"{seed}:{k}:shuffle").shuffle(splits[k])
291
+ return splits
292
+
293
+
294
+ def write_generalization_dataset(directory: str | Path, engine: EnglishPython | None = None, **kwargs: Any) -> dict[str, Any]:
295
+ directory=Path(directory); directory.mkdir(parents=True,exist_ok=True)
296
+ engine = engine or EnglishPython()
297
+ splits=generate_generalization_splits(engine, **kwargs)
298
+ source_sets = {name: {x.source for x in items} for name, items in splits.items()}
299
+ if source_sets["train"] & source_sets["validation"] or source_sets["train"] & source_sets["test"] or source_sets["validation"] & source_sets["test"]:
300
+ raise RuntimeError("Generalization split leaked an exact source sentence across datasets.")
301
+ manifest={
302
+ "format":"rawintent-generalization-v1",
303
+ "generation": dict(kwargs),
304
+ "registered_actions": len([i for i in engine.intents.values() if i.handler is not None and i.name != "python.call"]),
305
+ "counts":{},"families":{},"kinds":{},
306
+ "exact_source_overlap": {"train_validation": 0, "train_test": 0, "validation_test": 0},
307
+ }
308
+ for split,items in splits.items():
309
+ write_jsonl(directory/f"{split}.jsonl", items)
310
+ manifest["counts"][split]=len(items)
311
+ manifest["families"][split]=len({x.family for x in items if x.family})
312
+ kinds: dict[str,int]={}
313
+ for x in items: kinds[x.kind]=kinds.get(x.kind,0)+1
314
+ manifest["kinds"][split]=kinds
315
+ (directory/"manifest.json").write_text(json.dumps(manifest,indent=2),encoding="utf-8")
316
+ return manifest
317
+
318
+
319
+ def write_jsonl(path: str | Path, examples: Iterable[TrainingExample]) -> int:
320
+ path=Path(path); path.parent.mkdir(parents=True,exist_ok=True); count=0
321
+ with path.open("w",encoding="utf-8") as fh:
322
+ for item in examples:
323
+ fh.write(json.dumps(item.to_dict(),ensure_ascii=False)+"\n"); count+=1
324
+ return count
325
+
326
+
327
+ def read_jsonl(path: str | Path) -> list[TrainingExample]:
328
+ out=[]
329
+ with Path(path).open("r",encoding="utf-8") as fh:
330
+ for line_no,line in enumerate(fh,1):
331
+ if not line.strip(): continue
332
+ data=json.loads(line)
333
+ if not isinstance(data.get("source"),str) or not isinstance(data.get("target"),str):
334
+ raise ValueError(f"Line {line_no} needs string source and target fields.")
335
+ out.append(TrainingExample(data["source"],data["target"],data.get("action"),data.get("family"),data.get("kind","single"),data.get("split")))
336
+ return out
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..english_python import EnglishPython
6
+ from .rawlang import Instruction, Program
7
+
8
+
9
+ class DeterministicCompiler:
10
+ """Immediate compiler for commands already understood by RawIntent's rule engine."""
11
+
12
+ def __init__(self, engine: EnglishPython | None = None, **engine_options: Any) -> None:
13
+ self.engine = engine or EnglishPython(**engine_options)
14
+
15
+ def compile_program(self, text: str) -> Program:
16
+ matches = self.engine.parse_many(text)
17
+ statements: list[Instruction] = []
18
+ for match in matches:
19
+ if match.intent is None:
20
+ raise ValueError(match.message or "I couldn't understand that request.")
21
+ if match.negated:
22
+ raise ValueError(match.message or "That request says not to perform the action.")
23
+ if match.missing:
24
+ raise ValueError(match.message or f"The request is missing: {', '.join(match.missing)}")
25
+ statements.append(Instruction(match.intent, dict(match.entities)))
26
+ return Program(statements)
27
+
28
+ def compile(self, text: str) -> str:
29
+ return self.compile_program(text).to_source()
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict
4
+ from pathlib import Path
5
+ from typing import Any, Callable
6
+
7
+ from .dataset import TrainingExample, read_jsonl
8
+ from .deterministic import DeterministicCompiler
9
+ from .rawlang import Instruction, Program, Reference, ReturnStatement, UnknownCapabilityStatement, parse_program
10
+
11
+
12
+ def _value_key(value: Any) -> Any:
13
+ if isinstance(value, Reference):
14
+ return ("ref", value.root, tuple(value.path))
15
+ if isinstance(value, list):
16
+ return tuple(_value_key(v) for v in value)
17
+ if isinstance(value, tuple):
18
+ return tuple(_value_key(v) for v in value)
19
+ if isinstance(value, dict):
20
+ return tuple(sorted((str(k), _value_key(v)) for k, v in value.items()))
21
+ return value
22
+
23
+
24
+ def program_signature(program: Program) -> tuple[Any, ...]:
25
+ out: list[Any] = []
26
+ for stmt in program.statements:
27
+ if isinstance(stmt, UnknownCapabilityStatement):
28
+ out.append(("unknown",))
29
+ elif isinstance(stmt, ReturnStatement):
30
+ out.append(("return", _value_key(stmt.value)))
31
+ else:
32
+ out.append(("instruction", stmt.assign, stmt.action, tuple(sorted((k, _value_key(v)) for k, v in stmt.arguments.items()))))
33
+ return tuple(out)
34
+
35
+
36
+ def action_signature(program: Program) -> tuple[str, ...]:
37
+ actions=[]
38
+ for stmt in program.statements:
39
+ if isinstance(stmt, UnknownCapabilityStatement): actions.append("UNKNOWN_CAPABILITY")
40
+ elif isinstance(stmt, Instruction): actions.append(stmt.action)
41
+ return tuple(actions)
42
+
43
+
44
+ def _safe_program(source: str) -> tuple[Program | None, str | None]:
45
+ try:
46
+ return parse_program(source), None
47
+ except Exception as exc:
48
+ return None, str(exc)
49
+
50
+
51
+ def _is_unknown(program: Program | None) -> bool:
52
+ return bool(program and len(program.statements) == 1 and isinstance(program.statements[0], UnknownCapabilityStatement))
53
+
54
+
55
+ def _rate(n: int, d: int) -> float:
56
+ return n / d if d else 0.0
57
+
58
+
59
+ def evaluate_examples(
60
+ examples: list[TrainingExample],
61
+ compile_fn: Callable[[str], str],
62
+ *,
63
+ max_examples: int | None = None,
64
+ failure_limit: int = 25,
65
+ ) -> dict[str, Any]:
66
+ if max_examples is not None:
67
+ examples = examples[:max_examples]
68
+ totals = {"count": 0, "syntax_valid": 0, "exact": 0, "semantic": 0, "action": 0}
69
+ confusion = {"unknown_tp": 0, "unknown_fp": 0, "unknown_fn": 0, "unknown_tn": 0}
70
+ by_kind: dict[str, dict[str, int]] = {}
71
+ failures: list[dict[str, Any]] = []
72
+
73
+ for item in examples:
74
+ totals["count"] += 1
75
+ kind = item.kind or "single"
76
+ bucket = by_kind.setdefault(kind, {"count": 0, "syntax_valid": 0, "exact": 0, "semantic": 0, "action": 0})
77
+ bucket["count"] += 1
78
+ try:
79
+ predicted = compile_fn(item.source).strip()
80
+ compile_error = None
81
+ except Exception as exc:
82
+ predicted = ""
83
+ compile_error = str(exc)
84
+
85
+ target = item.target.strip()
86
+ target_program, target_error = _safe_program(target)
87
+ if target_error:
88
+ raise ValueError(f"Invalid benchmark target for {item.source!r}: {target_error}")
89
+ predicted_program, syntax_error = _safe_program(predicted) if predicted else (None, compile_error or "empty output")
90
+
91
+ expected_unknown = _is_unknown(target_program)
92
+ predicted_unknown = _is_unknown(predicted_program)
93
+ if expected_unknown and predicted_unknown: confusion["unknown_tp"] += 1
94
+ elif not expected_unknown and predicted_unknown: confusion["unknown_fp"] += 1
95
+ elif expected_unknown and not predicted_unknown: confusion["unknown_fn"] += 1
96
+ else: confusion["unknown_tn"] += 1
97
+
98
+ exact = predicted == target
99
+ syntax_valid = predicted_program is not None
100
+ semantic = bool(syntax_valid and program_signature(predicted_program) == program_signature(target_program))
101
+ action_match = bool(syntax_valid and action_signature(predicted_program) == action_signature(target_program))
102
+ for name, yes in (("syntax_valid", syntax_valid), ("exact", exact), ("semantic", semantic), ("action", action_match)):
103
+ if yes:
104
+ totals[name] += 1
105
+ bucket[name] += 1
106
+
107
+ if not semantic and len(failures) < failure_limit:
108
+ failures.append({
109
+ "source": item.source,
110
+ "target": target,
111
+ "prediction": predicted,
112
+ "kind": kind,
113
+ "family": item.family,
114
+ "syntax_error": syntax_error,
115
+ })
116
+
117
+ n = totals["count"]
118
+ tp, fp, fn = confusion["unknown_tp"], confusion["unknown_fp"], confusion["unknown_fn"]
119
+ precision = _rate(tp, tp + fp); recall = _rate(tp, tp + fn)
120
+ f1 = (2 * precision * recall / (precision + recall)) if precision + recall else 0.0
121
+ result: dict[str, Any] = {
122
+ "count": n,
123
+ "syntax_validity": _rate(totals["syntax_valid"], n),
124
+ "exact_match": _rate(totals["exact"], n),
125
+ "semantic_match": _rate(totals["semantic"], n),
126
+ "action_accuracy": _rate(totals["action"], n),
127
+ "unknown": {**confusion, "precision": precision, "recall": recall, "f1": f1},
128
+ "by_kind": {},
129
+ "failures": failures,
130
+ }
131
+ for kind, b in sorted(by_kind.items()):
132
+ d=b["count"]
133
+ result["by_kind"][kind]={
134
+ "count": d,
135
+ "syntax_validity": _rate(b["syntax_valid"], d),
136
+ "exact_match": _rate(b["exact"], d),
137
+ "semantic_match": _rate(b["semantic"], d),
138
+ "action_accuracy": _rate(b["action"], d),
139
+ }
140
+ return result
141
+
142
+
143
+ def evaluate_dataset(
144
+ data_path: str | Path,
145
+ *,
146
+ checkpoint: str | Path | None = None,
147
+ device: str = "cpu",
148
+ deterministic: bool = False,
149
+ max_examples: int | None = None,
150
+ failure_limit: int = 25,
151
+ ) -> dict[str, Any]:
152
+ examples = read_jsonl(data_path)
153
+ if deterministic:
154
+ compiler = DeterministicCompiler()
155
+ compile_fn = compiler.compile
156
+ mode = "deterministic"
157
+ else:
158
+ if checkpoint is None:
159
+ raise ValueError("A neural checkpoint is required unless deterministic=True.")
160
+ from .compiler import NeuralCompiler
161
+ compiler = NeuralCompiler(checkpoint, device=device)
162
+ # Do not syntax-validate here; syntax validity is one of the benchmark metrics.
163
+ compile_fn = lambda text: compiler.compile(text, validate_syntax=False)
164
+ mode = "neural"
165
+ metrics = evaluate_examples(examples, compile_fn, max_examples=max_examples, failure_limit=failure_limit)
166
+ metrics["mode"] = mode
167
+ metrics["data"] = str(data_path)
168
+ if checkpoint is not None: metrics["checkpoint"] = str(checkpoint)
169
+ return metrics
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from dataclasses import dataclass, field
5
+ from typing import Any
6
+
7
+ from ..english_python import EnglishPython
8
+ from ..errors import ConfigurationError, english_exception
9
+ from ..models import EnglishIssue
10
+ from .rawlang import Instruction, Program, RawLangSyntaxError, Reference, ReturnStatement, UnknownCapabilityStatement, ensure_program
11
+
12
+
13
+ @dataclass(slots=True)
14
+ class ExecutionResult:
15
+ ok: bool
16
+ value: Any = None
17
+ variables: dict[str, Any] = field(default_factory=dict)
18
+ outputs: list[Any] = field(default_factory=list)
19
+ error: EnglishIssue | None = None
20
+ failed_line: int | None = None
21
+
22
+ @property
23
+ def message(self) -> str | None:
24
+ return self.error.message if self.error else None
25
+
26
+
27
+ class RawLangExecutor:
28
+ """Validated RawLang executor backed by an :class:`EnglishPython` capability registry."""
29
+
30
+ def __init__(self, engine: EnglishPython | None = None, **engine_options: Any) -> None:
31
+ self.engine = engine or EnglishPython(**engine_options)
32
+
33
+ def schema(self) -> dict[str, dict[str, Any]]:
34
+ out: dict[str, dict[str, Any]] = {}
35
+ for name, intent in sorted(self.engine.intents.items()):
36
+ if intent.handler is None:
37
+ continue
38
+ sig = inspect.signature(intent.handler)
39
+ params: dict[str, Any] = {}
40
+ for p in sig.parameters.values():
41
+ if p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD):
42
+ continue
43
+ params[p.name] = {
44
+ "required": p.default is inspect.Parameter.empty,
45
+ "default": None if p.default is inspect.Parameter.empty else p.default,
46
+ "annotation": None if p.annotation is inspect.Parameter.empty else str(p.annotation),
47
+ }
48
+ out[name] = {
49
+ "description": intent.description,
50
+ "parameters": params,
51
+ }
52
+ return out
53
+
54
+ def _resolve(self, value: Any, variables: dict[str, Any]) -> Any:
55
+ if isinstance(value, Reference):
56
+ if value.root not in variables:
57
+ raise ValueError(f"RawLang variable {value.root!r} has not been created yet.")
58
+ current = variables[value.root]
59
+ for part in value.path:
60
+ if isinstance(part, int):
61
+ current = current[part]
62
+ elif isinstance(current, dict):
63
+ if part not in current:
64
+ raise KeyError(part)
65
+ current = current[part]
66
+ else:
67
+ current = getattr(current, part)
68
+ return current
69
+ if isinstance(value, list):
70
+ return [self._resolve(v, variables) for v in value]
71
+ if isinstance(value, tuple):
72
+ return tuple(self._resolve(v, variables) for v in value)
73
+ if isinstance(value, dict):
74
+ return {k: self._resolve(v, variables) for k, v in value.items()}
75
+ return value
76
+
77
+ def _call(self, stmt: Instruction, variables: dict[str, Any]) -> Any:
78
+ if stmt.action not in self.engine.intents:
79
+ raise ConfigurationError(
80
+ f"RawLang action {stmt.action!r} is not registered. Use the schema command to see available actions."
81
+ )
82
+ intent = self.engine.intents[stmt.action]
83
+ if intent.handler is None:
84
+ raise ConfigurationError(f"RawLang action {stmt.action!r} does not have an executable handler.")
85
+ kwargs = {k: self._resolve(v, variables) for k, v in stmt.arguments.items()}
86
+ sig = inspect.signature(intent.handler)
87
+ try:
88
+ bound = sig.bind(**kwargs)
89
+ except TypeError as exc:
90
+ raise TypeError(f"{stmt.action}: {exc}") from exc
91
+ bound.apply_defaults()
92
+ return intent.handler(*bound.args, **bound.kwargs)
93
+
94
+ def execute(self, program: str | Program) -> ExecutionResult:
95
+ try:
96
+ parsed = ensure_program(program)
97
+ except RawLangSyntaxError as exc:
98
+ return ExecutionResult(False, error=EnglishIssue("INVALID_RAWLANG", str(exc)))
99
+ except Exception as exc:
100
+ return ExecutionResult(False, error=english_exception(exc))
101
+
102
+ variables: dict[str, Any] = {}
103
+ outputs: list[Any] = []
104
+ last: Any = None
105
+ for stmt in parsed.statements:
106
+ try:
107
+ if isinstance(stmt, UnknownCapabilityStatement):
108
+ return ExecutionResult(
109
+ False,
110
+ value=last,
111
+ variables=variables,
112
+ outputs=outputs,
113
+ error=EnglishIssue(
114
+ "UNKNOWN_CAPABILITY",
115
+ "I understand the request, but this RawIntent runtime does not have a registered capability for it yet.",
116
+ ),
117
+ failed_line=stmt.line,
118
+ )
119
+ if isinstance(stmt, ReturnStatement):
120
+ value = self._resolve(stmt.value, variables)
121
+ return ExecutionResult(True, value=value, variables=variables, outputs=outputs)
122
+ value = self._call(stmt, variables)
123
+ last = value
124
+ outputs.append(value)
125
+ if stmt.assign:
126
+ variables[stmt.assign] = value
127
+ except Exception as exc:
128
+ issue = english_exception(exc, intent=stmt.action if isinstance(stmt, Instruction) else None)
129
+ return ExecutionResult(False, value=last, variables=variables, outputs=outputs, error=issue, failed_line=stmt.line)
130
+ return ExecutionResult(True, value=last, variables=variables, outputs=outputs)