Add textbook formalization pipeline (extract -> LLM formalize -> elaboration check -> library assembly)
- extract_section.py: slice a book section out of a pdftotext -layout dump and list its labeled Theorem/Proposition/Lemma/Corollary/Definition items. - formalize.py: LLM-formalize a section into Lean 4 candidate statements (preferring Mathlib vocabulary) plus a definitions manifest. - check_statements.py: elaboration-only sanity check against a real Mathlib-backed Lean project (must run outside the Cowork sandbox, which has no Lean toolchain and can't run macOS binaries anyway). - build_library.py: assemble solved lean-bench results into a reusable .lean file, reusing each winning proof verbatim. - llm.py: standalone OpenAI-compatible chat client + .env loader, kept independent of lean_agent so this pipeline works without it installed. - Source PDFs and raw/layout text dumps are gitignored (copyrighted material); only code and the final short Lean statement datasets are tracked.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from textbook.build_library import build
|
||||
|
||||
|
||||
def write_jsonl(path, records):
|
||||
path.write_text('\n'.join(json.dumps(r) for r in records) + '\n', encoding='utf-8')
|
||||
|
||||
|
||||
class BuildLibraryTests(unittest.TestCase):
|
||||
def test_build_includes_only_solved_theorems_and_hoists_shared_imports(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
directory = Path(directory)
|
||||
dataset = directory / 'dataset.jsonl'
|
||||
results = directory / 'results.jsonl'
|
||||
write_jsonl(dataset, [
|
||||
{"id": "solved-one", "statement": "True", "prelude": "import Mathlib",
|
||||
"source_label": "Theorem 1", "natural_language": "trivial"},
|
||||
{"id": "unsolved-one", "statement": "False", "prelude": "import Mathlib",
|
||||
"source_label": "Theorem 2", "natural_language": "not actually true"},
|
||||
])
|
||||
write_jsonl(results, [
|
||||
{"id": "solved-one", "success": True,
|
||||
"attempts": [{"proof": "trivial", "success": True, "diagnostics": ""}]},
|
||||
{"id": "unsolved-one", "success": False, "attempts": []},
|
||||
])
|
||||
|
||||
text, solved, skipped = build(dataset, results)
|
||||
|
||||
self.assertEqual(solved, ['solved-one'])
|
||||
self.assertEqual(skipped, ['unsolved-one'])
|
||||
self.assertIn('import Mathlib', text)
|
||||
self.assertEqual(text.count('import Mathlib'), 1) # hoisted once, not per-theorem
|
||||
self.assertIn('theorem solved_one : True := by', text)
|
||||
self.assertNotIn('unsolved_one', text)
|
||||
|
||||
def test_build_keeps_non_import_prelude_attached_per_theorem(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
directory = Path(directory)
|
||||
dataset = directory / 'dataset.jsonl'
|
||||
results = directory / 'results.jsonl'
|
||||
write_jsonl(dataset, [
|
||||
{"id": "custom-def", "statement": "myPred 1", "prelude": "def myPred (n : Nat) := True",
|
||||
"source_label": "Theorem 3", "natural_language": ""},
|
||||
])
|
||||
write_jsonl(results, [
|
||||
{"id": "custom-def", "success": True,
|
||||
"attempts": [{"proof": "trivial", "success": True, "diagnostics": ""}]},
|
||||
])
|
||||
|
||||
text, solved, skipped = build(dataset, results)
|
||||
|
||||
self.assertIn('def myPred (n : Nat) := True', text)
|
||||
self.assertEqual(solved, ['custom-def'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,49 @@
|
||||
import unittest
|
||||
from textbook.extract_section import extract_section, labeled_statements
|
||||
|
||||
FIXTURE = """\
|
||||
1.6. Divisibility in the Integers
|
||||
intro text
|
||||
|
||||
Proposition 1.6.1.
|
||||
body
|
||||
|
||||
Theorem 1.6.6. There are infinitely many prime numbers.
|
||||
Proof. blah
|
||||
36 1. ALGEBRAIC THEMES
|
||||
|
||||
|
||||
Theorem 1.6.6. There are infinitely many prime numbers.
|
||||
Proof continues after a page break, repeats the label.
|
||||
|
||||
1.7. Modular Arithmetic
|
||||
next section text
|
||||
"""
|
||||
|
||||
|
||||
class ExtractSectionTests(unittest.TestCase):
|
||||
def test_extract_section_slices_between_headings(self):
|
||||
section = extract_section(FIXTURE, '1.6. Divisibility in the Integers', '1.7. Modular Arithmetic')
|
||||
self.assertIn('Theorem 1.6.6', section)
|
||||
self.assertNotIn('next section text', section)
|
||||
|
||||
def test_extract_section_missing_heading_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
extract_section(FIXTURE, '1.6. Divisibility in the Integers', '9.9. Does Not Exist')
|
||||
|
||||
def test_labeled_statements_dedupes_and_orders(self):
|
||||
section = extract_section(FIXTURE, '1.6. Divisibility in the Integers', '1.7. Modular Arithmetic')
|
||||
items = labeled_statements(section, '1.6.')
|
||||
labels = [item['label'] for item in items]
|
||||
# Theorem 1.6.6 appears twice (page-break repeat) but must be kept once, and
|
||||
# book order (1.6.1 before 1.6.6) must be preserved.
|
||||
self.assertEqual(labels, ['1.6.1', '1.6.6'])
|
||||
|
||||
def test_labeled_statements_filters_by_prefix(self):
|
||||
section = extract_section(FIXTURE, '1.6. Divisibility in the Integers', '1.7. Modular Arithmetic')
|
||||
items = labeled_statements(section, '1.7.')
|
||||
self.assertEqual(items, [])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,58 @@
|
||||
import json
|
||||
import unittest
|
||||
|
||||
from textbook.formalize import extract_json_block, validate
|
||||
|
||||
VALID_REPLY = """Here is the result:
|
||||
```json
|
||||
{
|
||||
"definitions": [
|
||||
{"label": "Definition 1.6.3", "concept": "prime number", "mathlib_ref": "Nat.Prime",
|
||||
"lean_code": null, "notes": ""}
|
||||
],
|
||||
"theorems": [
|
||||
{"id": "goodman-1-6-6", "source_label": "Theorem 1.6.6", "kind": "theorem",
|
||||
"statement": "\\u2200 n : \\u2115, \\u2203 p, p > n \\u2227 p.Prime",
|
||||
"prelude": "import Mathlib", "natural_language": "There are infinitely many primes.",
|
||||
"requires": ["Nat.Prime"], "include_in_bench": true, "exclude_reason": null}
|
||||
]
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
class FormalizeParsingTests(unittest.TestCase):
|
||||
def test_extract_json_block_from_fenced_reply(self):
|
||||
data = extract_json_block(VALID_REPLY)
|
||||
self.assertEqual(set(data), {"definitions", "theorems"})
|
||||
|
||||
def test_extract_json_block_without_fence_falls_back_to_whole_text(self):
|
||||
payload = '{"theorems": [], "definitions": []}'
|
||||
self.assertEqual(extract_json_block(payload), json.loads(payload))
|
||||
|
||||
def test_validate_accepts_well_formed_response(self):
|
||||
data = extract_json_block(VALID_REPLY)
|
||||
theorems, definitions = validate(data)
|
||||
self.assertEqual(len(theorems), 1)
|
||||
self.assertEqual(len(definitions), 1)
|
||||
|
||||
def test_validate_rejects_missing_field(self):
|
||||
data = {"theorems": [{"id": "x", "source_label": "Theorem 1", "statement": "True"}],
|
||||
"definitions": []}
|
||||
with self.assertRaises(ValueError):
|
||||
validate(data)
|
||||
|
||||
def test_validate_rejects_duplicate_ids(self):
|
||||
entry = {"id": "dup", "source_label": "Theorem 1", "kind": "theorem", "statement": "True",
|
||||
"prelude": "import Mathlib", "include_in_bench": True}
|
||||
data = {"theorems": [entry, dict(entry)], "definitions": []}
|
||||
with self.assertRaises(ValueError):
|
||||
validate(data)
|
||||
|
||||
def test_validate_rejects_non_object_response(self):
|
||||
with self.assertRaises(ValueError):
|
||||
validate([])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user