commonground-engine 0.2.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.
- commonground/__init__.py +0 -0
- commonground/__main__.py +499 -0
- commonground/abduce.py +1332 -0
- commonground/argmap.py +207 -0
- commonground/bench.py +547 -0
- commonground/crosscamp.py +408 -0
- commonground/extract.py +239 -0
- commonground/fol.py +496 -0
- commonground/folio.py +139 -0
- commonground/formalize.py +249 -0
- commonground/ground.py +245 -0
- commonground/lean.py +246 -0
- commonground/load.py +52 -0
- commonground/model.py +94 -0
- commonground/smt.py +137 -0
- commonground/verdict.py +118 -0
- commonground_engine-0.2.0.dist-info/METADATA +59 -0
- commonground_engine-0.2.0.dist-info/RECORD +20 -0
- commonground_engine-0.2.0.dist-info/WHEEL +5 -0
- commonground_engine-0.2.0.dist-info/top_level.txt +1 -0
commonground/__init__.py
ADDED
|
File without changes
|
commonground/__main__.py
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""Command line: ``python -m commonground <command>``.
|
|
2
|
+
|
|
3
|
+
check decide one argument given as formulas
|
|
4
|
+
analyze conflicts, concessions and justifications for a debate file
|
|
5
|
+
extract extract the statements of text files, with verbatim spans
|
|
6
|
+
camp per-camp report: statements -> axioms -> load -> certificates
|
|
7
|
+
crosscamp common ground + fault lines for two or more camp records
|
|
8
|
+
co2 the bundled CO2 golden example, end to end (headless)
|
|
9
|
+
folio run the FOLIO benchmark (reference formalizations, or a language model)
|
|
10
|
+
compare paired comparison of two language-model runs, as JSON
|
|
11
|
+
report the same comparison as a Markdown summary
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from . import argmap, bench, folio, lean
|
|
22
|
+
from .fol import Not, ParseError, parse
|
|
23
|
+
from .verdict import decide
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _check(args) -> int:
|
|
27
|
+
try:
|
|
28
|
+
premises = [parse(p) for p in args.premise]
|
|
29
|
+
conclusion = parse(args.conclusion)
|
|
30
|
+
except ParseError as exc:
|
|
31
|
+
print(f"not a formula: {exc}", file=sys.stderr)
|
|
32
|
+
return 2
|
|
33
|
+
verdict = decide(premises, conclusion, args.timeout_ms)
|
|
34
|
+
out = verdict.to_json()
|
|
35
|
+
if args.lean and verdict.label in ("True", "False"):
|
|
36
|
+
lean_bin = lean.find_lean()
|
|
37
|
+
if lean_bin is None:
|
|
38
|
+
out["lean"] = f"no Lean {lean.TOOLCHAIN} found (set LEAN_BIN)"
|
|
39
|
+
else:
|
|
40
|
+
goal = conclusion if verdict.label == "True" else Not(conclusion)
|
|
41
|
+
certificate = lean.certify([(premises, goal)], lean_bin)[0]
|
|
42
|
+
out["lean"] = {"certified": certificate.accepted, "axioms": certificate.axioms,
|
|
43
|
+
"reason": certificate.reason}
|
|
44
|
+
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
TS_HEADER = """// Generated by `python -m commonground analyze {source} --out {target}`.
|
|
49
|
+
// Do not edit: engine/tests/test_generated.py fails when this file is out of date.
|
|
50
|
+
|
|
51
|
+
export interface StatementGroup {{
|
|
52
|
+
statements: string[];
|
|
53
|
+
positions: string[];
|
|
54
|
+
}}
|
|
55
|
+
|
|
56
|
+
export interface DebateAnalysis {{
|
|
57
|
+
/** SHA-256 of the statements the analysis was computed from. */
|
|
58
|
+
input_hash: string;
|
|
59
|
+
premise_ids: string[];
|
|
60
|
+
positions: string[];
|
|
61
|
+
/** All premises of all positions have a common model. */
|
|
62
|
+
positions_compatible: boolean;
|
|
63
|
+
compatibility_witness: {{ domain_size: number; engine: string; rechecked_by_independent_evaluator: boolean }} | null;
|
|
64
|
+
/** Minimal sets of premises that cannot all hold. Empty when the positions are compatible. */
|
|
65
|
+
conflicts: StatementGroup[];
|
|
66
|
+
/** Smallest sets of premises whose withdrawal restores consistency. */
|
|
67
|
+
minimal_concessions: StatementGroup[];
|
|
68
|
+
/** Premises that belong to every maximal consistent set. */
|
|
69
|
+
uncontested: string[];
|
|
70
|
+
position_consistent: Record<string, boolean>;
|
|
71
|
+
conclusions: Record<string, {{
|
|
72
|
+
owner: string;
|
|
73
|
+
from_all_premises: string;
|
|
74
|
+
from_each_position: Record<string, string>;
|
|
75
|
+
/** Every minimal set of premises that entails the conclusion. */
|
|
76
|
+
justifications: string[][];
|
|
77
|
+
}}>;
|
|
78
|
+
statements_without_formula: string[];
|
|
79
|
+
}}
|
|
80
|
+
|
|
81
|
+
export const analysis: DebateAnalysis = {body};
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def render_analysis(debate: str, out: str | None, timeout_ms: int = 5000) -> str:
|
|
86
|
+
_, statements = argmap.load_debate(debate)
|
|
87
|
+
body = json.dumps(argmap.analyze(statements, timeout_ms), indent=2, ensure_ascii=False)
|
|
88
|
+
if out and out.endswith(".ts"):
|
|
89
|
+
return TS_HEADER.format(source=Path(debate).as_posix(), target=Path(out).as_posix(), body=body)
|
|
90
|
+
return body + "\n"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _analyze(args) -> int:
|
|
94
|
+
text = render_analysis(args.debate, args.out, args.timeout_ms)
|
|
95
|
+
if args.out:
|
|
96
|
+
Path(args.out).write_text(text, encoding="utf-8")
|
|
97
|
+
else:
|
|
98
|
+
print(text, end="")
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _folio(args) -> int:
|
|
103
|
+
out_dir = Path(args.out)
|
|
104
|
+
if args.mode == "reference":
|
|
105
|
+
lean_bin = lean.find_lean() if args.lean else None
|
|
106
|
+
if args.lean and lean_bin is None:
|
|
107
|
+
print(f"no Lean {lean.TOOLCHAIN} found (set LEAN_BIN)", file=sys.stderr)
|
|
108
|
+
return 2
|
|
109
|
+
summary = bench.reference(folio.fetch(Path(args.data)), out_dir, lean_bin, args.timeout_ms)
|
|
110
|
+
elif args.mode == "abduction":
|
|
111
|
+
lean_bin = lean.find_lean() if args.lean else None
|
|
112
|
+
if args.lean and lean_bin is None:
|
|
113
|
+
print(f"no Lean {lean.TOOLCHAIN} found (set LEAN_BIN)", file=sys.stderr)
|
|
114
|
+
return 2
|
|
115
|
+
data_file = folio.fetch(Path(args.data))
|
|
116
|
+
try:
|
|
117
|
+
summary = bench.abduction(data_file, out_dir, args.limit, args.timeout_ms, lean_bin,
|
|
118
|
+
args.lam, args.max_checks)
|
|
119
|
+
except bench.ConfigurationMismatch as exc:
|
|
120
|
+
print(exc, file=sys.stderr)
|
|
121
|
+
return 2
|
|
122
|
+
else:
|
|
123
|
+
from .formalize import OpenAICompatible, ProviderError
|
|
124
|
+
key = os.environ.get(args.api_key_env, "")
|
|
125
|
+
if not (args.base_url and args.model and key):
|
|
126
|
+
print(f"--base-url, --model and the {args.api_key_env} environment variable are required", file=sys.stderr)
|
|
127
|
+
return 2
|
|
128
|
+
try:
|
|
129
|
+
client = OpenAICompatible(args.base_url, args.model, key, json_mode=not args.no_json_mode,
|
|
130
|
+
temperature=None if args.no_temperature else args.temperature,
|
|
131
|
+
max_tokens=args.max_tokens)
|
|
132
|
+
except ValueError as exc:
|
|
133
|
+
print(exc, file=sys.stderr)
|
|
134
|
+
return 2
|
|
135
|
+
data_file = folio.fetch(Path(args.data))
|
|
136
|
+
try:
|
|
137
|
+
summary = bench.with_model(data_file, out_dir, client, args.mode, args.limit, args.timeout_ms)
|
|
138
|
+
except bench.ConfigurationMismatch as exc:
|
|
139
|
+
print(exc, file=sys.stderr)
|
|
140
|
+
return 2
|
|
141
|
+
except ProviderError as exc:
|
|
142
|
+
# The run stops instead of scoring an outage as wrong answers; the same command resumes it.
|
|
143
|
+
print(f"stopped: {exc}\nfinished examples are kept in {out_dir / 'runs.jsonl'}; "
|
|
144
|
+
"run the same command again to resume", file=sys.stderr)
|
|
145
|
+
if exc.status in (400, 422):
|
|
146
|
+
print("if the provider's message names response_format or temperature, add --no-json-mode "
|
|
147
|
+
"or --no-temperature (in a new --out directory: the settings are part of the run)",
|
|
148
|
+
file=sys.stderr)
|
|
149
|
+
return 3
|
|
150
|
+
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _load_plausibilities(path: str | None) -> dict:
|
|
155
|
+
if not path:
|
|
156
|
+
return {}
|
|
157
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
158
|
+
if not isinstance(data, dict):
|
|
159
|
+
raise ValueError("the plausibilities file must be a JSON object mapping FOL text to a number in [0, 1] "
|
|
160
|
+
"or a two-element range [min, max]")
|
|
161
|
+
for key, value in data.items():
|
|
162
|
+
ok = (isinstance(value, (int, float)) and not isinstance(value, bool)) or \
|
|
163
|
+
(isinstance(value, list) and len(value) == 2 and all(
|
|
164
|
+
isinstance(v, (int, float)) and not isinstance(v, bool) for v in value))
|
|
165
|
+
if not ok:
|
|
166
|
+
raise ValueError(f"the plausibility of {key!r} must be a number in [0, 1] or a [min, max] range, "
|
|
167
|
+
f"got {value!r}")
|
|
168
|
+
return data
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _lean_for(args) -> tuple[str | None, bool]:
|
|
172
|
+
"""(lean executable or None, ok). Asking for kernel certificates without a Lean of the
|
|
173
|
+
pinned version is an error, not a silent fall-back to solver-only badges."""
|
|
174
|
+
if not args.lean:
|
|
175
|
+
return None, True
|
|
176
|
+
lean_bin = lean.find_lean()
|
|
177
|
+
if lean_bin is None:
|
|
178
|
+
print(f"no Lean {lean.TOOLCHAIN} found (set LEAN_BIN), or drop --lean for solver-checked verdicts",
|
|
179
|
+
file=sys.stderr)
|
|
180
|
+
return lean_bin, lean_bin is not None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _camp_dir(out: Path, camp: str) -> Path:
|
|
184
|
+
"""<out>/<camp>; a camp name that is not a plain directory name is refused, so a
|
|
185
|
+
record cannot make the report land outside the output directory."""
|
|
186
|
+
if (camp != camp.strip() or camp in ("", ".", "..") or camp.endswith(".")
|
|
187
|
+
or any(ch in '/\\:*?"<>|' or ord(ch) < 32 for ch in camp)):
|
|
188
|
+
raise ValueError(f"camp name {camp!r} cannot be used as a directory name")
|
|
189
|
+
return out / camp
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _write_camp_report(directory: Path, report: dict) -> None:
|
|
193
|
+
from . import abduce
|
|
194
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
(directory / "report.json").write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
196
|
+
(directory / "report.md").write_text(abduce.render_markdown(report), encoding="utf-8")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _camp(args) -> int:
|
|
200
|
+
from . import abduce
|
|
201
|
+
from .argmap import TooMany
|
|
202
|
+
try:
|
|
203
|
+
record = abduce.load_record(args.record)
|
|
204
|
+
candidates = abduce.load_candidates(args.candidates)
|
|
205
|
+
plausibilities = _load_plausibilities(args.plausibilities)
|
|
206
|
+
except (OSError, ValueError) as exc:
|
|
207
|
+
print(f"cannot read the inputs: {exc}", file=sys.stderr)
|
|
208
|
+
return 2
|
|
209
|
+
lean_bin, ok = _lean_for(args)
|
|
210
|
+
if not ok:
|
|
211
|
+
return 2
|
|
212
|
+
try:
|
|
213
|
+
report = abduce.report_camp(record, abduce.ScriptedSource(candidates), plausibilities,
|
|
214
|
+
args.lam, args.timeout_ms, lean_bin, args.max_checks)
|
|
215
|
+
except TooMany as exc:
|
|
216
|
+
print(f"input too large for the documented scope: {exc}", file=sys.stderr)
|
|
217
|
+
return 2
|
|
218
|
+
except ValueError as exc:
|
|
219
|
+
print(f"cannot run the analysis: {exc}", file=sys.stderr)
|
|
220
|
+
return 2
|
|
221
|
+
_write_camp_report(Path(args.out), report)
|
|
222
|
+
print(json.dumps({"out": str(Path(args.out)), "camp": report["camp"],
|
|
223
|
+
"selected_systems": len(report["systems"]),
|
|
224
|
+
"search_complete": report["search_complete"],
|
|
225
|
+
"abstention": report["abstention"]}, ensure_ascii=False))
|
|
226
|
+
return 0
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _crosscamp_summary(out: Path, result: dict, reports: list[dict]) -> str:
|
|
230
|
+
n_mus = sum(len(p.get("mus") or []) for p in result["fault_lines"]["per_system_pair"])
|
|
231
|
+
return json.dumps({"out": str(out), "camps": result["camps"],
|
|
232
|
+
"stated_agreed": len(result["common_ground"]["stated"]["agreed"]),
|
|
233
|
+
"fault_lines_found": n_mus,
|
|
234
|
+
"search_complete": {r["camp"]: r["search_complete"] for r in reports}},
|
|
235
|
+
ensure_ascii=False)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _crosscamp(args) -> int:
|
|
239
|
+
from . import abduce, crosscamp
|
|
240
|
+
from .argmap import TooMany
|
|
241
|
+
if len(args.records) < 2:
|
|
242
|
+
print("crosscamp needs at least two camp records", file=sys.stderr)
|
|
243
|
+
return 2
|
|
244
|
+
if len(args.candidates) != len(args.records):
|
|
245
|
+
print("--candidates once per record, in the same order", file=sys.stderr)
|
|
246
|
+
return 2
|
|
247
|
+
out = Path(args.out)
|
|
248
|
+
try:
|
|
249
|
+
plausibilities = _load_plausibilities(args.plausibilities)
|
|
250
|
+
camps = []
|
|
251
|
+
for rec_path, cand_path in zip(args.records, args.candidates):
|
|
252
|
+
record = abduce.load_record(rec_path)
|
|
253
|
+
camps.append((record, abduce.load_candidates(cand_path), _camp_dir(out, record.camp)))
|
|
254
|
+
names = [record.camp for record, _, _ in camps]
|
|
255
|
+
if len(set(names)) != len(names):
|
|
256
|
+
raise ValueError(f"camp names must be unique, got {names}")
|
|
257
|
+
except (OSError, ValueError) as exc:
|
|
258
|
+
print(f"cannot read the inputs: {exc}", file=sys.stderr)
|
|
259
|
+
return 2
|
|
260
|
+
lean_bin, ok = _lean_for(args)
|
|
261
|
+
if not ok:
|
|
262
|
+
return 2
|
|
263
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
264
|
+
camp_objs = []
|
|
265
|
+
try:
|
|
266
|
+
for record, candidates, camp_dir in camps:
|
|
267
|
+
report = abduce.report_camp(record, abduce.ScriptedSource(candidates), plausibilities,
|
|
268
|
+
args.lam, args.timeout_ms, lean_bin, args.max_checks)
|
|
269
|
+
_write_camp_report(camp_dir, report)
|
|
270
|
+
camp_objs.append(crosscamp.Camp(record, report))
|
|
271
|
+
result = crosscamp.analyze(camp_objs, args.timeout_ms, lean_bin)
|
|
272
|
+
except TooMany as exc:
|
|
273
|
+
print(f"input too large for the documented scope: {exc}; per-camp reports were written",
|
|
274
|
+
file=sys.stderr)
|
|
275
|
+
return 2
|
|
276
|
+
except ValueError as exc:
|
|
277
|
+
print(f"cannot run the analysis: {exc}", file=sys.stderr)
|
|
278
|
+
return 2
|
|
279
|
+
(out / "crosscamp.json").write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
280
|
+
(out / "crosscamp.md").write_text(crosscamp.render_markdown(result), encoding="utf-8")
|
|
281
|
+
print(_crosscamp_summary(out, result, [c.report for c in camp_objs]))
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _camp_from_corpus(camp_id, statements, golden_camp):
|
|
286
|
+
"""Build a Record plus scripted candidates from the CO2 corpus files. The corpus ships
|
|
287
|
+
statement texts, not whole booklet pages: the document is the camp's statements joined
|
|
288
|
+
by newline, spans located in it, so the verbatim-span discipline still holds."""
|
|
289
|
+
from . import abduce, fol
|
|
290
|
+
texts = {s["id"]: s["text"] for s in statements if s["camp"] == camp_id}
|
|
291
|
+
fol_by_id = {s["id"]: s["fol"] for s in golden_camp["statements"]}
|
|
292
|
+
doc_id = f"doc-{camp_id}"
|
|
293
|
+
parts, spans = [], {}
|
|
294
|
+
offset = 0
|
|
295
|
+
for s in golden_camp["statements"]:
|
|
296
|
+
t = texts[s["id"]]
|
|
297
|
+
parts.append(t)
|
|
298
|
+
spans[s["id"]] = (offset, offset + len(t))
|
|
299
|
+
offset += len(t) + 1
|
|
300
|
+
record = abduce.Record(camp_id, [abduce.Document(doc_id, "\n".join(parts))],
|
|
301
|
+
[abduce.RecordStatement(s["id"], "endorsed", fol_by_id[s["id"]], doc_id,
|
|
302
|
+
texts[s["id"]], spans[s["id"]])
|
|
303
|
+
for s in golden_camp["statements"]])
|
|
304
|
+
cands = [abduce.Candidate(c["fol"], [(doc_id, texts[ev], spans[ev]) for ev in c["evidence"]])
|
|
305
|
+
for c in golden_camp["candidate_axioms"]]
|
|
306
|
+
plaus = {fol.to_text(fol.parse(c["fol"])): c["plausibility"] for c in golden_camp["candidate_axioms"]}
|
|
307
|
+
return record, cands, plaus
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _co2(args) -> int:
|
|
311
|
+
from . import abduce, crosscamp
|
|
312
|
+
from .argmap import TooMany
|
|
313
|
+
ex = Path(args.dir)
|
|
314
|
+
try:
|
|
315
|
+
statements = json.loads((ex / "statements.json").read_text(encoding="utf-8"))["statements"]
|
|
316
|
+
golden = json.loads((ex / "golden.json").read_text(encoding="utf-8"))
|
|
317
|
+
except (OSError, ValueError, KeyError) as exc:
|
|
318
|
+
print(f"cannot read the corpus in {ex}: {exc}", file=sys.stderr)
|
|
319
|
+
return 2
|
|
320
|
+
lean_bin, ok = _lean_for(args)
|
|
321
|
+
if not ok:
|
|
322
|
+
return 2
|
|
323
|
+
out = Path(args.out)
|
|
324
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
325
|
+
camps = []
|
|
326
|
+
try:
|
|
327
|
+
for camp_id, g in golden["camps"].items():
|
|
328
|
+
record, cands, plaus = _camp_from_corpus(camp_id, statements, g)
|
|
329
|
+
report = abduce.report_camp(record, abduce.ScriptedSource(cands), plaus, args.lam,
|
|
330
|
+
args.timeout_ms, lean_bin, args.max_checks)
|
|
331
|
+
_write_camp_report(_camp_dir(out, camp_id), report)
|
|
332
|
+
camps.append(crosscamp.Camp(record, report))
|
|
333
|
+
result = crosscamp.analyze(camps, args.timeout_ms, lean_bin)
|
|
334
|
+
except TooMany as exc:
|
|
335
|
+
print(f"input too large for the documented scope: {exc}; per-camp reports were written",
|
|
336
|
+
file=sys.stderr)
|
|
337
|
+
return 2
|
|
338
|
+
except (ValueError, KeyError) as exc:
|
|
339
|
+
print(f"cannot run the analysis on the corpus in {ex}: {exc}", file=sys.stderr)
|
|
340
|
+
return 2
|
|
341
|
+
(out / "crosscamp.json").write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
342
|
+
(out / "crosscamp.md").write_text(crosscamp.render_markdown(result), encoding="utf-8")
|
|
343
|
+
print(_crosscamp_summary(out, result, [c.report for c in camps]))
|
|
344
|
+
return 0
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _extract(args) -> int:
|
|
348
|
+
from . import extract as extract_mod
|
|
349
|
+
from .formalize import OpenAICompatible, ProviderError
|
|
350
|
+
key = os.environ.get(args.api_key_env, "")
|
|
351
|
+
if not (args.base_url and args.model and key):
|
|
352
|
+
print(f"--base-url, --model and the {args.api_key_env} environment variable are required", file=sys.stderr)
|
|
353
|
+
return 2
|
|
354
|
+
try:
|
|
355
|
+
client = OpenAICompatible(args.base_url, args.model, key, json_mode=not args.no_json_mode,
|
|
356
|
+
temperature=None if args.no_temperature else args.temperature,
|
|
357
|
+
max_tokens=args.max_tokens)
|
|
358
|
+
except ValueError as exc:
|
|
359
|
+
print(exc, file=sys.stderr)
|
|
360
|
+
return 2
|
|
361
|
+
try:
|
|
362
|
+
documents = [(path, Path(path).read_text(encoding="utf-8")) for path in args.files]
|
|
363
|
+
except OSError as exc:
|
|
364
|
+
print(f"cannot read a document: {exc}", file=sys.stderr)
|
|
365
|
+
return 2
|
|
366
|
+
out_dir = Path(args.out)
|
|
367
|
+
try:
|
|
368
|
+
summary = extract_mod.run(client, documents, out_dir)
|
|
369
|
+
except ValueError as exc: # a configuration mismatch, or a document that fails validation
|
|
370
|
+
print(exc, file=sys.stderr)
|
|
371
|
+
return 2
|
|
372
|
+
except ProviderError as exc:
|
|
373
|
+
# The run stops instead of scoring an outage; the same command resumes it.
|
|
374
|
+
print(f"stopped: {exc}\nfinished documents are kept in {out_dir / 'runs.jsonl'}; "
|
|
375
|
+
"run the same command again to resume", file=sys.stderr)
|
|
376
|
+
return 3
|
|
377
|
+
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
378
|
+
return 0
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _compare(args) -> int:
|
|
382
|
+
try:
|
|
383
|
+
if args.command == "report":
|
|
384
|
+
print(bench.report(Path(args.first), Path(args.second)), end="")
|
|
385
|
+
else:
|
|
386
|
+
print(json.dumps(bench.compare(Path(args.first), Path(args.second)), indent=2, ensure_ascii=False))
|
|
387
|
+
except (OSError, ValueError, KeyError) as exc:
|
|
388
|
+
print(f"cannot compare: {exc}", file=sys.stderr)
|
|
389
|
+
return 2
|
|
390
|
+
return 0
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def main(argv: list[str] | None = None) -> int:
|
|
394
|
+
parser = argparse.ArgumentParser(prog="commonground", description=__doc__,
|
|
395
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
396
|
+
parser.add_argument("--timeout-ms", type=int, default=5000, help="solver limit per satisfiability question")
|
|
397
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
398
|
+
|
|
399
|
+
check = commands.add_parser("check", help="decide one argument")
|
|
400
|
+
check.add_argument("--premise", action="append", default=[], help="a formula; repeat for each premise")
|
|
401
|
+
check.add_argument("--conclusion", required=True)
|
|
402
|
+
check.add_argument("--lean", action="store_true", help="also ask the Lean kernel to certify the verdict")
|
|
403
|
+
check.set_defaults(run=_check)
|
|
404
|
+
|
|
405
|
+
analyze = commands.add_parser("analyze", help="analyse a debate file")
|
|
406
|
+
analyze.add_argument("debate")
|
|
407
|
+
analyze.add_argument("--out")
|
|
408
|
+
analyze.set_defaults(run=_analyze)
|
|
409
|
+
|
|
410
|
+
camp = commands.add_parser("camp", help="per-camp report: statements -> axioms -> load -> certificates")
|
|
411
|
+
camp.add_argument("record", help="camp record JSON (camp, documents, statements with stances/spans)")
|
|
412
|
+
camp.add_argument("--candidates", required=True,
|
|
413
|
+
help="JSON file of scripted unstated candidates: {\"candidates\": [{\"fol\", \"quotes\": [...]}]}")
|
|
414
|
+
camp.add_argument("--plausibilities", help="optional JSON object mapping FOL text to a plausibility estimate: a number in [0, 1] or a [min, max] range")
|
|
415
|
+
camp.add_argument("--lam", type=float, default=0.3, help="cost weight of one axiom (default 0.3)")
|
|
416
|
+
camp.add_argument("--lean", action="store_true", help="also ask the Lean kernel to certify sufficiency")
|
|
417
|
+
camp.add_argument("--max-checks", type=int, default=12000,
|
|
418
|
+
help="budget of certification checks (default 12000, enough to finish the CO2 "
|
|
419
|
+
"`yes` search, AUDIT-4); a search it stops is reported as incomplete, never "
|
|
420
|
+
"as cost-minimal")
|
|
421
|
+
camp.add_argument("--out", required=True, help="output directory: report.json and report.md")
|
|
422
|
+
camp.set_defaults(run=_camp)
|
|
423
|
+
|
|
424
|
+
xc = commands.add_parser("crosscamp", help="cross-camp report: common ground + fault lines for 2+ camps")
|
|
425
|
+
xc.add_argument("records", nargs="+", help="camp record JSONs (two or more)")
|
|
426
|
+
xc.add_argument("--candidates", nargs="+", required=True,
|
|
427
|
+
help="one scripted-candidates JSON per record, in the same order")
|
|
428
|
+
xc.add_argument("--plausibilities", help="optional JSON object mapping FOL text to a plausibility estimate: a number in [0, 1] or a [min, max] range")
|
|
429
|
+
xc.add_argument("--lam", type=float, default=0.3, help="cost weight of one axiom (default 0.3)")
|
|
430
|
+
xc.add_argument("--lean", action="store_true", help="also ask the Lean kernel to certify sufficiency")
|
|
431
|
+
xc.add_argument("--timeout-ms", type=int, default=5000, help="solver limit per satisfiability question")
|
|
432
|
+
xc.add_argument("--max-checks", type=int, default=12000,
|
|
433
|
+
help="budget of certification checks per camp (default 12000); a search it stops is "
|
|
434
|
+
"reported as incomplete, never as cost-minimal")
|
|
435
|
+
xc.add_argument("--out", required=True,
|
|
436
|
+
help="output directory: <camp>/report.{json,md} per camp, plus crosscamp.{json,md}")
|
|
437
|
+
xc.set_defaults(run=_crosscamp)
|
|
438
|
+
|
|
439
|
+
co2 = commands.add_parser("co2", help="run the bundled CO2 golden example end to end (headless)")
|
|
440
|
+
co2.add_argument("--dir",
|
|
441
|
+
default=str(Path(__file__).resolve().parents[2] / "examples" / "co2-2021"),
|
|
442
|
+
help="corpus directory holding statements.json and golden.json")
|
|
443
|
+
co2.add_argument("--lam", type=float, default=0.3, help="cost weight of one axiom (default 0.3)")
|
|
444
|
+
co2.add_argument("--lean", action="store_true", help="also ask the Lean kernel to certify sufficiency")
|
|
445
|
+
co2.add_argument("--timeout-ms", type=int, default=5000, help="solver limit per satisfiability question")
|
|
446
|
+
co2.add_argument("--max-checks", type=int, default=12000,
|
|
447
|
+
help="cap on certification checks per camp (default 12000, enough for the CO2 `yes` "
|
|
448
|
+
"search; see AUDIT.md AUDIT-4)")
|
|
449
|
+
co2.add_argument("--out", required=True,
|
|
450
|
+
help="output directory: <camp>/report.{json,md} per camp, plus crosscamp.{json,md}")
|
|
451
|
+
co2.set_defaults(run=_co2)
|
|
452
|
+
|
|
453
|
+
ex = commands.add_parser("extract", help="extract the statements of text files, with verbatim spans")
|
|
454
|
+
ex.add_argument("files", nargs="+", help="text files; each file is one document, id = the path as given")
|
|
455
|
+
ex.add_argument("--out", required=True, help="output directory: runs.jsonl, config.json, summary.json")
|
|
456
|
+
ex.add_argument("--base-url")
|
|
457
|
+
ex.add_argument("--model")
|
|
458
|
+
ex.add_argument("--api-key-env", default="LLM_API_KEY", help="name of the variable holding the key")
|
|
459
|
+
ex.add_argument("--no-json-mode", action="store_true",
|
|
460
|
+
help="do not send response_format=json_object (for providers that reject it)")
|
|
461
|
+
ex.add_argument("--temperature", type=float, default=0.0, help="sampling temperature (default 0)")
|
|
462
|
+
ex.add_argument("--no-temperature", action="store_true",
|
|
463
|
+
help="send no temperature (for models that accept only their default)")
|
|
464
|
+
ex.add_argument("--max-tokens", type=int, default=8000, help="reply length limit (default 8000)")
|
|
465
|
+
ex.set_defaults(run=_extract)
|
|
466
|
+
|
|
467
|
+
fol = commands.add_parser("folio", help="run the FOLIO benchmark")
|
|
468
|
+
fol.add_argument("--data", default="data/FOLIO", help="where the dataset repository is (cloned if absent)")
|
|
469
|
+
fol.add_argument("--out", required=True)
|
|
470
|
+
fol.add_argument("--mode", choices=("reference", "llm", "direct", "abduction"), default="reference")
|
|
471
|
+
fol.add_argument("--lam", type=float, default=0.3, help="abduction mode: cost weight of one axiom (default 0.3)")
|
|
472
|
+
fol.add_argument("--max-checks", type=int, default=12000,
|
|
473
|
+
help="abduction mode: cap on certification checks per example (default 12000)")
|
|
474
|
+
fol.add_argument("--lean", action="store_true", help="reference mode: certify verdicts with Lean")
|
|
475
|
+
fol.add_argument("--base-url")
|
|
476
|
+
fol.add_argument("--model")
|
|
477
|
+
fol.add_argument("--api-key-env", default="LLM_API_KEY", help="name of the variable holding the key")
|
|
478
|
+
fol.add_argument("--limit", type=int, help="model and abduction modes: only the first N examples")
|
|
479
|
+
fol.add_argument("--no-json-mode", action="store_true",
|
|
480
|
+
help="do not send response_format=json_object (for providers that reject it)")
|
|
481
|
+
fol.add_argument("--temperature", type=float, default=0.0, help="model modes: sampling temperature (default 0)")
|
|
482
|
+
fol.add_argument("--no-temperature", action="store_true",
|
|
483
|
+
help="send no temperature (for models that accept only their default)")
|
|
484
|
+
fol.add_argument("--max-tokens", type=int, default=4000, help="model modes: reply length limit (default 4000)")
|
|
485
|
+
fol.set_defaults(run=_folio)
|
|
486
|
+
|
|
487
|
+
for name, text in (("compare", "paired comparison of two model runs (McNemar), as JSON"),
|
|
488
|
+
("report", "the same comparison as a Markdown summary")):
|
|
489
|
+
compare = commands.add_parser(name, help=text)
|
|
490
|
+
compare.add_argument("first", help="output directory of one run, e.g. results/direct")
|
|
491
|
+
compare.add_argument("second", help="output directory of the other, e.g. results/llm")
|
|
492
|
+
compare.set_defaults(run=_compare)
|
|
493
|
+
|
|
494
|
+
args = parser.parse_args(argv)
|
|
495
|
+
return args.run(args)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
if __name__ == "__main__":
|
|
499
|
+
sys.exit(main())
|