forked from ChelseaKR/fare-policy-assistant
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathextract_template.py
More file actions
169 lines (139 loc) · 6.6 KB
/
Copy pathextract_template.py
File metadata and controls
169 lines (139 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#!/usr/bin/env python3
"""Extract the domain-agnostic parts of this repo into a starter skeleton
for a new policy-assistant domain, per template/MANIFEST.yaml.
This is the concrete half of docs/ROADMAP.md P3-5 ("Generalize the
harness"): docs/adapting.md always described, in prose, which files a new
domain keeps and which it rewrites. This script makes that split runnable --
a second domain assistant can start from an audited skeleton without forking
this repo, and without hand-copying files and hoping the doc was current.
Usage:
uv run python -m scripts.extract_template /path/to/new-domain-assistant
uv run python -m scripts.extract_template --dry-run
What it does:
- Copies every path under `generic:` in template/MANIFEST.yaml verbatim.
- Copies every path under `generic_edit:`, then prints the marker/note so
the follow-up edit is a grep away, not an archaeology project.
- Writes a stub `src/<package>/domain.py` from template/domain.py.stub.
- Writes a starter README pointing back at docs/adapting.md's numbered
checklist (corpus manifest, will-not-do list, forbidden-language
patterns, eval cases, aliases) for what to fill in next.
- Does NOT copy anything under `domain_specific:` -- that is this
project's fare-policy content, listed in the manifest only so the
completeness test (tests/test_extract_template.py) can confirm every
tracked top-level module is classified one way or the other.
It does not touch corpus content, eval case content, or prompts -- those are
exactly what a new domain must author from its own documents.
"""
from __future__ import annotations
import argparse
import shutil
import sys
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover - repo's own env always has PyYAML
print("PyYAML is required (uv run handles this via the repo's env)", file=sys.stderr)
raise
REPO_ROOT = Path(__file__).resolve().parent.parent
MANIFEST_PATH = REPO_ROOT / "template" / "MANIFEST.yaml"
DOMAIN_STUB_PATH = REPO_ROOT / "template" / "domain.py.stub"
def load_manifest(manifest_path: Path = MANIFEST_PATH) -> dict:
with manifest_path.open() as f:
data = yaml.safe_load(f)
for key in ("generic", "generic_edit", "domain_specific"):
data.setdefault(key, [])
return data
def _copy_one(src_rel: str, target_root: Path) -> Path:
src = REPO_ROOT / src_rel
if not src.exists():
raise FileNotFoundError(
f"template/MANIFEST.yaml lists {src_rel!r} but it does not exist "
"in the repo -- the manifest has drifted from the code."
)
dst = target_root / src_rel
dst.parent.mkdir(parents=True, exist_ok=True)
if src.is_dir():
shutil.copytree(src, dst, dirs_exist_ok=True)
else:
shutil.copy2(src, dst)
return dst
def extract(target: Path, manifest: dict | None = None, dry_run: bool = False) -> list[str]:
"""Build the skeleton at `target`. Returns the list of edit reminders to
print (marker/note pairs for generic_edit entries)."""
manifest = manifest or load_manifest()
reminders: list[str] = []
if not dry_run and target.exists() and any(target.iterdir()):
raise FileExistsError(f"{target} exists and is not empty")
for rel in manifest["generic"]:
if dry_run:
print(f"copy {rel}")
else:
_copy_one(rel, target)
for entry in manifest["generic_edit"]:
rel = entry["path"]
if dry_run:
print(f"copy* {rel} (edit needed: {entry.get('marker', '')!r})")
else:
_copy_one(rel, target)
note = entry.get("note", "").strip()
reminders.append(f"{rel}: search for {entry.get('marker', '')!r} -- {note}")
if not dry_run:
stub_dst = target / "src" / "assistant" / "domain.py"
stub_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(DOMAIN_STUB_PATH, stub_dst)
(target / "docs").mkdir(parents=True, exist_ok=True)
_write_starter_readme(target)
else:
print("write src/assistant/domain.py (from template/domain.py.stub)")
print("write GETTING_STARTED.md")
return reminders
def _write_starter_readme(target: Path) -> None:
(target / "GETTING_STARTED.md").write_text(
"# Starting from the fare-policy-assistant skeleton\n\n"
"This tree was generated by fare-policy-assistant's "
"`scripts/extract_template.py` (docs/ROADMAP.md P3-5). It carries "
"over the eval harness, guard architecture, ingest pipeline, CI "
"wiring, and a11y gate unchanged. `docs/adapting.md` (copied "
"alongside this file) is the full checklist; the short version:\n\n"
"0. Fill in `src/assistant/domain.py` (stubbed here with TODOs) --"
" your scopes, aliases, adjacent-topic redirects, fallback contact.\n"
"1. Point `corpus/manifest.yaml` at your documents and run "
"`make fetch && make ingest`.\n"
"2. Write your will-not-do list and encode it three times: system "
"prompt, `guards.py`, eval cases.\n"
"3. Rewrite the determination-language phrase list in `guards.py` "
"for your domain, in every language you serve.\n"
"4. Author eval cases from your actual documents in "
"`evals/suites/` (groundedness, refusal, edge cases, multilingual, "
"freshness).\n"
"5. Set entity aliases on the `DomainProfile` (same step as 0).\n\n"
"Files copied with a required edit are listed in the extraction "
"output; each names a marker string to grep for.\n"
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("target", nargs="?", type=Path, help="Directory to write the skeleton into")
parser.add_argument(
"--dry-run", action="store_true", help="List what would be copied, write nothing"
)
args = parser.parse_args(argv)
if not args.dry_run and args.target is None:
parser.error("target is required unless --dry-run is given")
manifest = load_manifest()
if args.dry_run:
reminders = extract(Path("/dev/null"), manifest=manifest, dry_run=True)
else:
target = args.target
try:
reminders = extract(target, manifest=manifest, dry_run=False)
except FileExistsError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
print(f"Skeleton written to {target}")
if reminders:
print("\nFiles copied as-is but needing a domain-specific edit:")
for r in reminders:
print(f" - {r}")
return 0
if __name__ == "__main__":
raise SystemExit(main())