Spaces:
Running
Running
Merge pull request #1 from ray5th/claude/intelligent-varahamihira-81b22c
Browse files- .env.example +0 -1
- .gitignore +0 -9
- README.md +290 -63
- extract_mathlib.py +0 -82
- problems/simple_add.lean +4 -0
- problems/test_problem.lean +4 -0
- requirements.txt +9 -6
- scripts/build_index.py +40 -0
- scripts/run_agent.py +36 -0
- src/__init__.py +0 -1
- src/agent.py +0 -159
- src/cli.py +0 -162
- src/langgraph_agent.py +173 -0
- src/lean_env.py +0 -83
- src/lean_verifier.py +70 -0
- src/lmm_client.py +54 -0
- src/mathlib_corpus.py +117 -0
- src/proof_agent.py +11 -0
- src/rag_chain.py +71 -0
- src/retriever.py +107 -94
- tests/test_lean_verifier.py +57 -0
.env.example
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
ANTHROPIC_API_KEY=your_key_here
|
|
|
|
|
|
.gitignore
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
.env
|
| 2 |
-
.venv/
|
| 3 |
-
venv/
|
| 4 |
-
data/mathlib.index
|
| 5 |
-
data/mathlib_meta.pkl
|
| 6 |
-
__pycache__/
|
| 7 |
-
*.pyc
|
| 8 |
-
*.pyo
|
| 9 |
-
.DS_Store
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,107 +1,334 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
| 6 |
|
| 7 |
-
|
| 8 |
-
- 📚 **Mathlib4 RAG** — FAISS semantic search retrieves relevant verified lemmas to ground each generation
|
| 9 |
-
- 🔁 **Self-correcting** — Parses compiler errors and re-prompts until successful compilation or max retries
|
| 10 |
-
- 🖥️ **Interactive CLI** — Pretty terminal output with `rich`, syntax highlighting, live feedback
|
| 11 |
|
| 12 |
-
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
| 17 |
-
- [Lean 4 + elan](https://leanprover.github.io/lean4/doc/setup.html) installed and `lean` on your `$PATH`
|
| 18 |
-
- An Anthropic API key
|
| 19 |
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
| 23 |
-
git clone https://github.com/YOUR_HANDLE/lean4-helper
|
| 24 |
-
cd lean4-helper
|
| 25 |
|
| 26 |
-
|
| 27 |
-
pip install -r requirements.txt
|
| 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 |
-
python -m src.cli --file my_theorem.lean
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
```
|
| 64 |
|
| 65 |
-
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
```
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
│ ├── agent.py # Agentic Claude feedback loop
|
| 74 |
-
│ └── cli.py # Rich interactive CLI
|
| 75 |
-
├── data/ # FAISS index + metadata (git-ignored)
|
| 76 |
-
├── extract_mathlib.py # Mathlib4 lemma extractor
|
| 77 |
-
├── requirements.txt
|
| 78 |
-
├── .env.example
|
| 79 |
-
└── README.md
|
| 80 |
```
|
| 81 |
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
|
|
|
|
|
|
|
|
|
| 84 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
User Theorem
|
| 86 |
│
|
| 87 |
▼
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
│
|
| 90 |
▼
|
| 91 |
-
|
|
|
|
| 92 |
│
|
| 93 |
▼
|
| 94 |
-
|
|
|
|
| 95 |
│
|
| 96 |
▼
|
| 97 |
-
Lean
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
```
|
| 101 |
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
| 1 |
+
## Brief Non-Technical MVP: AI Lean 4 Proof Assistant
|
| 2 |
|
| 3 |
+
### Project goal
|
| 4 |
|
| 5 |
+
Build an AI assistant that helps users write correct **Lean 4 mathematical proofs**.
|
| 6 |
|
| 7 |
+
The user provides a theorem. The system searches Lean’s Mathlib library, finds useful related theorems, asks an AI model to write a proof, checks the proof using Lean, and automatically retries if the proof fails.
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
The key idea is:
|
| 10 |
|
| 11 |
+
```text
|
| 12 |
+
AI suggests the proof.
|
| 13 |
+
Lean verifies the proof.
|
| 14 |
+
Only Lean decides if the answer is correct.
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## MVP flow
|
| 20 |
+
|
| 21 |
+
```text
|
| 22 |
+
User enters theorem
|
| 23 |
+
↓
|
| 24 |
+
Lean tooling reads the theorem and current proof goal
|
| 25 |
+
↓
|
| 26 |
+
System retrieves useful Mathlib lemmas
|
| 27 |
+
↓
|
| 28 |
+
System reranks the lemmas and keeps the best ones
|
| 29 |
+
↓
|
| 30 |
+
LangChain organizes the prompt and retrieval flow
|
| 31 |
+
↓
|
| 32 |
+
Local LLM generates a Lean proof
|
| 33 |
+
↓
|
| 34 |
+
Lean checks the proof
|
| 35 |
+
↓
|
| 36 |
+
If correct → return final proof
|
| 37 |
+
If wrong → send Lean error back to the AI and retry
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## Simple explanation
|
| 43 |
+
|
| 44 |
+
This project is like an **AI coding assistant for mathematical proofs**.
|
| 45 |
+
|
| 46 |
+
But unlike a normal chatbot, it does not just guess. It uses Lean itself to check whether the proof is truly valid.
|
| 47 |
+
|
| 48 |
+
The assistant works in three stages:
|
| 49 |
+
|
| 50 |
+
```text
|
| 51 |
+
1. Search
|
| 52 |
+
Find useful existing Mathlib theorems.
|
| 53 |
+
|
| 54 |
+
2. Generate
|
| 55 |
+
Ask the AI model to write a proof using those theorems.
|
| 56 |
+
|
| 57 |
+
3. Verify
|
| 58 |
+
Run the proof through Lean and retry if Lean finds an error.
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## Main MVP components
|
| 64 |
+
|
| 65 |
+
### 1. Lean tooling
|
| 66 |
+
|
| 67 |
+
Use **LeanInteract** to connect Python with Lean 4.
|
| 68 |
+
|
| 69 |
+
LeanInteract lets Python interact with Lean through the Lean REPL, which means your program can send Lean code, inspect proof goals, and receive Lean feedback programmatically. ([GitHub][1])
|
| 70 |
+
|
| 71 |
+
In simple terms:
|
| 72 |
+
|
| 73 |
+
```text
|
| 74 |
+
LeanInteract lets the AI system talk to Lean.
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
Instead of only seeing an error like:
|
| 78 |
+
|
| 79 |
+
```text
|
| 80 |
+
type mismatch
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
the system can see more useful information like:
|
| 84 |
+
|
| 85 |
+
```text
|
| 86 |
+
Current goal:
|
| 87 |
+
n : Nat
|
| 88 |
+
⊢ n = n
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
That makes the retry much smarter.
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
### 2. Retrieve Mathlib lemmas
|
| 96 |
+
|
| 97 |
+
The system searches Mathlib for useful theorems.
|
| 98 |
+
|
| 99 |
+
For example, if the theorem is about even numbers, the retriever may find lemmas related to:
|
| 100 |
+
|
| 101 |
+
```text
|
| 102 |
+
Even
|
| 103 |
+
Odd
|
| 104 |
+
Nat.even_or_odd
|
| 105 |
+
parity
|
| 106 |
+
multiplication
|
| 107 |
+
powers
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
This is the “retrieve” part.
|
| 111 |
+
|
| 112 |
+
Use:
|
| 113 |
+
|
| 114 |
+
```text
|
| 115 |
+
FAISS for fast semantic search
|
| 116 |
+
BM25 / keyword search for exact Lean names
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
FAISS is useful for quickly finding similar text from a large collection of embedded documents, while keyword search helps when exact theorem names matter.
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
|
| 123 |
+
### 3. Rerank retrieved lemmas
|
| 124 |
+
|
| 125 |
+
Retrieval alone is not enough.
|
| 126 |
+
|
| 127 |
+
The system may retrieve 50 possible Mathlib lemmas, but many may be weak or irrelevant. So we add a **reranker**.
|
| 128 |
|
| 129 |
+
The improved flow is:
|
|
|
|
|
|
|
| 130 |
|
| 131 |
+
```text
|
| 132 |
+
Retrieve top 50 lemmas
|
| 133 |
+
↓
|
| 134 |
+
Rerank them based on the current theorem and proof goal
|
| 135 |
+
↓
|
| 136 |
+
Keep the best 8–12 lemmas
|
| 137 |
+
↓
|
| 138 |
+
Send only those to the AI model
|
| 139 |
+
```
|
| 140 |
|
| 141 |
+
A Cross-Encoder reranker is useful here because it scores each query-document pair more carefully than the first-pass retriever. Sentence Transformers describes this as a common retrieve-and-rerank setup where fast retrieval finds candidates and the Cross-Encoder reranks them for better relevance. ([SentenceTransformers][2])
|
|
|
|
|
|
|
| 142 |
|
| 143 |
+
In simple terms:
|
|
|
|
| 144 |
|
| 145 |
+
```text
|
| 146 |
+
Retrieve finds possible useful lemmas.
|
| 147 |
+
Rerank chooses the best ones.
|
| 148 |
```
|
| 149 |
|
| 150 |
+
This improves the quality of the context given to the AI.
|
| 151 |
+
|
| 152 |
+
---
|
| 153 |
+
|
| 154 |
+
### 4. LangChain role
|
| 155 |
|
| 156 |
+
Use **LangChain** for orchestration, not for proof correctness.
|
| 157 |
|
| 158 |
+
LangChain can help organize:
|
| 159 |
|
| 160 |
+
```text
|
| 161 |
+
retrieval
|
| 162 |
+
reranking
|
| 163 |
+
prompt templates
|
| 164 |
+
document formatting
|
| 165 |
+
LLM calls
|
| 166 |
+
retry history
|
| 167 |
```
|
| 168 |
|
| 169 |
+
LangChain has retrieval utilities such as contextual compression, where retrieved documents can be filtered or shortened before sending them to the model. ([LangChain][3])
|
| 170 |
|
| 171 |
+
But LangChain does **not** make Lean proofs correct by itself.
|
| 172 |
+
|
| 173 |
+
The correctness still comes from:
|
| 174 |
+
|
| 175 |
+
```text
|
| 176 |
+
Lean compiler
|
| 177 |
+
Lean proof state
|
| 178 |
+
Mathlib retrieval quality
|
| 179 |
+
LLM proof generation
|
| 180 |
+
retry loop
|
| 181 |
```
|
| 182 |
|
| 183 |
+
So the right positioning is:
|
| 184 |
|
| 185 |
+
```text
|
| 186 |
+
LangChain manages the workflow.
|
| 187 |
+
Lean verifies the truth.
|
| 188 |
+
```
|
| 189 |
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
### 5. Local LLM first
|
| 193 |
+
|
| 194 |
+
For the MVP, use a local model first.
|
| 195 |
+
|
| 196 |
+
Example:
|
| 197 |
+
|
| 198 |
+
```text
|
| 199 |
+
Ollama + Qwen Coder
|
| 200 |
+
```
|
| 201 |
|
| 202 |
+
Why local first?
|
|
|
|
| 203 |
|
| 204 |
+
```text
|
| 205 |
+
No API cost
|
| 206 |
+
Easy to test many times
|
| 207 |
+
Good for debugging the pipeline
|
| 208 |
+
Private and simple for development
|
| 209 |
```
|
| 210 |
|
| 211 |
+
Later, once the workflow works, replace the local model with Claude.
|
| 212 |
|
| 213 |
+
The architecture stays the same.
|
| 214 |
+
|
| 215 |
+
---
|
| 216 |
+
|
| 217 |
+
### 6. Lean verification and retry
|
| 218 |
+
|
| 219 |
+
After the AI generates a proof, the system sends it to Lean.
|
| 220 |
+
|
| 221 |
+
If Lean accepts the proof:
|
| 222 |
+
|
| 223 |
+
```text
|
| 224 |
+
Success → return proof
|
| 225 |
```
|
| 226 |
+
|
| 227 |
+
If Lean rejects the proof:
|
| 228 |
+
|
| 229 |
+
```text
|
| 230 |
+
Failure → capture Lean error → add error to prompt → retry
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
```
|
| 232 |
|
| 233 |
+
Example:
|
| 234 |
+
|
| 235 |
+
```text
|
| 236 |
+
Attempt 1:
|
| 237 |
+
AI generates proof.
|
| 238 |
+
Lean says: unknown theorem name.
|
| 239 |
+
|
| 240 |
+
Attempt 2:
|
| 241 |
+
AI sees the error and tries a different lemma.
|
| 242 |
+
Lean says: type mismatch.
|
| 243 |
|
| 244 |
+
Attempt 3:
|
| 245 |
+
AI fixes the proof.
|
| 246 |
+
Lean accepts it.
|
| 247 |
```
|
| 248 |
+
|
| 249 |
+
This is the most important part of the MVP.
|
| 250 |
+
|
| 251 |
+
---
|
| 252 |
+
|
| 253 |
+
## Final MVP architecture
|
| 254 |
+
|
| 255 |
+
```text
|
| 256 |
User Theorem
|
| 257 |
│
|
| 258 |
▼
|
| 259 |
+
LeanInteract
|
| 260 |
+
Get theorem goal and Lean feedback
|
| 261 |
+
│
|
| 262 |
+
▼
|
| 263 |
+
Hybrid Retrieval
|
| 264 |
+
FAISS semantic search + keyword search
|
| 265 |
+
│
|
| 266 |
+
▼
|
| 267 |
+
Reranker
|
| 268 |
+
Keep only the most useful Mathlib lemmas
|
| 269 |
│
|
| 270 |
▼
|
| 271 |
+
LangChain Prompt Flow
|
| 272 |
+
Organize theorem, lemmas, history, and Lean errors
|
| 273 |
│
|
| 274 |
▼
|
| 275 |
+
Local LLM
|
| 276 |
+
Generate Lean proof
|
| 277 |
│
|
| 278 |
▼
|
| 279 |
+
Lean Checker
|
| 280 |
+
Verify proof
|
| 281 |
+
│
|
| 282 |
+
├── Success → Return final proof
|
| 283 |
+
│
|
| 284 |
+
└── Failure → Retry with Lean error
|
| 285 |
+
```
|
| 286 |
+
|
| 287 |
+
---
|
| 288 |
+
|
| 289 |
+
## Libraries to use
|
| 290 |
+
|
| 291 |
+
| Purpose | Library |
|
| 292 |
+
| ---------------------------- | ------------------------------------------------- |
|
| 293 |
+
| Interact with Lean | `LeanInteract` |
|
| 294 |
+
| Semantic retrieval | `sentence-transformers` |
|
| 295 |
+
| Vector search | `faiss-cpu` |
|
| 296 |
+
| Keyword search | `rank-bm25` |
|
| 297 |
+
| Reranking | `sentence-transformers` CrossEncoder or FlashRank |
|
| 298 |
+
| Workflow orchestration | `LangChain` |
|
| 299 |
+
| More advanced workflow graph | `LangGraph` |
|
| 300 |
+
| Local model | `Ollama` |
|
| 301 |
+
| Future stronger model | Claude API |
|
| 302 |
+
|
| 303 |
+
LangGraph is useful later if you want the proof process to behave like a clear state machine: retrieve → generate → verify → retry → success/failure. LangGraph is designed for stateful agent workflows, which fits this kind of multi-step proof loop. ([Emergent Mind][4])
|
| 304 |
+
|
| 305 |
+
---
|
| 306 |
+
|
| 307 |
+
## Non-technical value proposition
|
| 308 |
+
|
| 309 |
+
This MVP creates an AI assistant that helps people write mathematically verified Lean proofs.
|
| 310 |
+
|
| 311 |
+
It does not only generate text. It checks every answer with Lean, learns from errors, and keeps trying until it produces a valid proof.
|
| 312 |
+
|
| 313 |
+
The project demonstrates:
|
| 314 |
+
|
| 315 |
+
```text
|
| 316 |
+
AI-assisted formal reasoning
|
| 317 |
+
verified code generation
|
| 318 |
+
search over mathematical knowledge
|
| 319 |
+
automatic error correction
|
| 320 |
+
LLM + compiler feedback loops
|
| 321 |
```
|
| 322 |
|
| 323 |
+
---
|
| 324 |
+
|
| 325 |
+
## One-line MVP summary
|
| 326 |
+
|
| 327 |
+
```text
|
| 328 |
+
An AI-powered Lean 4 proof assistant that searches Mathlib, reranks useful lemmas, generates a proof with a local LLM, checks it using Lean tooling, and automatically retries using Lean errors until the proof is verified.
|
| 329 |
+
```
|
| 330 |
|
| 331 |
+
[1]: https://github.com/augustepoiroux/LeanInteract?utm_source=chatgpt.com "LeanInteract: A Python Interface for Lean 4"
|
| 332 |
+
[2]: https://sbert.net/examples/sentence_transformer/applications/retrieve_rerank/README.html?utm_source=chatgpt.com "Retrieve & Re-Rank Pipeline"
|
| 333 |
+
[3]: https://www.langchain.com/blog/improving-document-retrieval-with-contextual-compression?utm_source=chatgpt.com "Improving Document Retrieval with Contextual Compression"
|
| 334 |
+
[4]: https://www.emergentmind.com/topics/langgraph?utm_source=chatgpt.com "LangGraph: Modular LLM Agent Orchestration"
|
extract_mathlib.py
DELETED
|
@@ -1,82 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
extract_mathlib.py
|
| 3 |
-
|
| 4 |
-
Extracts lemma names, type signatures, and docstrings from Mathlib4 source files
|
| 5 |
-
and writes them to data/mathlib_lemmas.jsonl for indexing.
|
| 6 |
-
|
| 7 |
-
Usage:
|
| 8 |
-
python extract_mathlib.py --mathlib-path /path/to/mathlib4
|
| 9 |
-
|
| 10 |
-
Each output line is a JSON object:
|
| 11 |
-
{"name": "Nat.add_comm", "type": "∀ (n m : ℕ), n + m = m + n", "doc": "..."}
|
| 12 |
-
"""
|
| 13 |
-
|
| 14 |
-
import argparse
|
| 15 |
-
import json
|
| 16 |
-
import os
|
| 17 |
-
import re
|
| 18 |
-
from pathlib import Path
|
| 19 |
-
|
| 20 |
-
# Regex patterns for Lean 4 declarations
|
| 21 |
-
DECL_PATTERN = re.compile(
|
| 22 |
-
r'(?P<doc>/--.*?-/\s*)?' # optional docstring
|
| 23 |
-
r'(?:theorem|lemma|def|abbrev)\s+'
|
| 24 |
-
r'(?P<name>[\w\.\']+)\s*'
|
| 25 |
-
r'(?P<rest>[^:=]*?):\s*'
|
| 26 |
-
r'(?P<type>.+?)\s*(?::=|where|by)',
|
| 27 |
-
re.DOTALL
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
DOC_PATTERN = re.compile(r'/--\s*(.*?)\s*-/', re.DOTALL)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def extract_from_file(path: Path) -> list[dict]:
|
| 34 |
-
"""Extract lemma entries from a single .lean file."""
|
| 35 |
-
text = path.read_text(encoding="utf-8", errors="ignore")
|
| 36 |
-
entries = []
|
| 37 |
-
for m in DECL_PATTERN.finditer(text):
|
| 38 |
-
name = m.group("name").strip()
|
| 39 |
-
raw_type = m.group("type").strip()
|
| 40 |
-
# Clean up multi-line types
|
| 41 |
-
clean_type = " ".join(raw_type.split())
|
| 42 |
-
doc = ""
|
| 43 |
-
if m.group("doc"):
|
| 44 |
-
doc_match = DOC_PATTERN.search(m.group("doc"))
|
| 45 |
-
if doc_match:
|
| 46 |
-
doc = " ".join(doc_match.group(1).split())
|
| 47 |
-
entries.append({"name": name, "type": clean_type, "doc": doc})
|
| 48 |
-
return entries
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def extract_mathlib(mathlib_path: str, output_path: str = "data/mathlib_lemmas.jsonl"):
|
| 52 |
-
"""Walk all .lean files in mathlib_path and write JSONL output."""
|
| 53 |
-
root = Path(mathlib_path)
|
| 54 |
-
os.makedirs(Path(output_path).parent, exist_ok=True)
|
| 55 |
-
|
| 56 |
-
total = 0
|
| 57 |
-
with open(output_path, "w") as out:
|
| 58 |
-
for lean_file in sorted(root.rglob("*.lean")):
|
| 59 |
-
entries = extract_from_file(lean_file)
|
| 60 |
-
for entry in entries:
|
| 61 |
-
out.write(json.dumps(entry) + "\n")
|
| 62 |
-
total += len(entries)
|
| 63 |
-
if total % 1000 == 0:
|
| 64 |
-
print(f" Extracted {total} lemmas so far...")
|
| 65 |
-
|
| 66 |
-
print(f"Done. {total} lemmas written to {output_path}")
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
if __name__ == "__main__":
|
| 70 |
-
parser = argparse.ArgumentParser(description="Extract Mathlib4 lemmas to JSONL")
|
| 71 |
-
parser.add_argument(
|
| 72 |
-
"--mathlib-path",
|
| 73 |
-
required=True,
|
| 74 |
-
help="Path to the root of the Mathlib4 source tree (e.g. ~/.elan/toolchains/...)",
|
| 75 |
-
)
|
| 76 |
-
parser.add_argument(
|
| 77 |
-
"--output",
|
| 78 |
-
default="data/mathlib_lemmas.jsonl",
|
| 79 |
-
help="Output JSONL file path.",
|
| 80 |
-
)
|
| 81 |
-
args = parser.parse_args()
|
| 82 |
-
extract_mathlib(args.mathlib_path, args.output)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
problems/simple_add.lean
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Mathlib
|
| 2 |
+
|
| 3 |
+
theorem add_zero_simple (n : ℕ) : n + 0 = n := by
|
| 4 |
+
sorry
|
problems/test_problem.lean
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Mathlib
|
| 2 |
+
|
| 3 |
+
theorem square_root_two_irrational (n m : ℕ) (h : n^2 = 2 * m^2) (hm : m ≠ 0) : False := by
|
| 4 |
+
sorry
|
requirements.txt
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
lean-interact
|
| 2 |
+
ollama
|
| 3 |
+
langchain
|
| 4 |
+
langchain-community
|
| 5 |
+
langchain-ollama
|
| 6 |
+
langgraph
|
| 7 |
+
faiss-cpu
|
| 8 |
+
rank-bm25
|
| 9 |
+
sentence-transformers
|
scripts/build_index.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
One-time script to build the FAISS + BM25 index from Mathlib4 source files.
|
| 4 |
+
Run this before using the LangGraph agent for the first time:
|
| 5 |
+
|
| 6 |
+
python scripts/build_index.py
|
| 7 |
+
|
| 8 |
+
Optional flags:
|
| 9 |
+
--mathlib-root PATH Path to Mathlib4 source (auto-detected if omitted)
|
| 10 |
+
--max-files N Limit to first N .lean files (useful for quick testing)
|
| 11 |
+
--index-dir PATH Where to save the index (default: data/mathlib_index)
|
| 12 |
+
"""
|
| 13 |
+
import sys
|
| 14 |
+
import os
|
| 15 |
+
import argparse
|
| 16 |
+
|
| 17 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
|
| 18 |
+
|
| 19 |
+
from retriever import MathLibRetriever
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def main():
|
| 23 |
+
parser = argparse.ArgumentParser(description="Build Mathlib FAISS index.")
|
| 24 |
+
parser.add_argument("--mathlib-root", default=None, help="Path to Mathlib4 source root")
|
| 25 |
+
parser.add_argument("--max-files", type=int, default=None, help="Limit number of .lean files processed")
|
| 26 |
+
parser.add_argument("--index-dir", default=None, help="Directory to save the index")
|
| 27 |
+
args = parser.parse_args()
|
| 28 |
+
|
| 29 |
+
retriever = MathLibRetriever(index_dir=args.index_dir)
|
| 30 |
+
|
| 31 |
+
if retriever.is_index_built() and not args.max_files:
|
| 32 |
+
print(f"Index already exists at {retriever.index_dir}. Delete it to rebuild.")
|
| 33 |
+
return
|
| 34 |
+
|
| 35 |
+
retriever.build(mathlib_root=args.mathlib_root, max_files=args.max_files)
|
| 36 |
+
print("Done. Index is ready for use.")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
if __name__ == "__main__":
|
| 40 |
+
main()
|
scripts/run_agent.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
import argparse
|
| 5 |
+
|
| 6 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
|
| 7 |
+
|
| 8 |
+
from langgraph_agent import LangGraphAgent
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def main():
|
| 12 |
+
parser = argparse.ArgumentParser(description="Run the LangGraph Lean Proof Agent on a file.")
|
| 13 |
+
parser.add_argument("file", help="Path to the .lean file to solve")
|
| 14 |
+
parser.add_argument("--model", default="qwen3-vl:4b", help="Ollama model name")
|
| 15 |
+
parser.add_argument("--retries", type=int, default=5, help="Max retries")
|
| 16 |
+
parser.add_argument("--index-dir", default=None, help="Path to pre-built FAISS index directory")
|
| 17 |
+
|
| 18 |
+
args = parser.parse_args()
|
| 19 |
+
|
| 20 |
+
print(f"Starting LangGraph Proof Agent with model: {args.model}")
|
| 21 |
+
agent = LangGraphAgent(
|
| 22 |
+
model_name=args.model,
|
| 23 |
+
max_retries=args.retries,
|
| 24 |
+
index_dir=args.index_dir,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
success = agent.solve_file(args.file)
|
| 28 |
+
|
| 29 |
+
if success:
|
| 30 |
+
print("\nSuccess! The proof has been verified.")
|
| 31 |
+
else:
|
| 32 |
+
print("\nFailed to verify the proof.")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
if __name__ == "__main__":
|
| 36 |
+
main()
|
src/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# lean4-helper src package
|
|
|
|
|
|
src/agent.py
DELETED
|
@@ -1,159 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
agent.py
|
| 3 |
-
|
| 4 |
-
The core agentic self-correcting loop.
|
| 5 |
-
|
| 6 |
-
Given a Lean 4 theorem statement, this agent will:
|
| 7 |
-
1. Retrieve relevant Mathlib4 lemmas via FAISS.
|
| 8 |
-
2. Prompt Claude to generate a proof.
|
| 9 |
-
3. Validate the proof with the Lean compiler.
|
| 10 |
-
4. On failure, feed the compiler error back to Claude and retry.
|
| 11 |
-
5. Return the final proof on success or raise after max retries.
|
| 12 |
-
"""
|
| 13 |
-
|
| 14 |
-
import os
|
| 15 |
-
from typing import Optional
|
| 16 |
-
|
| 17 |
-
import anthropic
|
| 18 |
-
|
| 19 |
-
from .lean_env import LeanResult, run_lean
|
| 20 |
-
from .retriever import Retriever
|
| 21 |
-
|
| 22 |
-
SYSTEM_PROMPT = """\
|
| 23 |
-
You are an expert Lean 4 theorem prover with deep knowledge of Mathlib4.
|
| 24 |
-
Your task is to write a complete, valid Lean 4 proof for the given theorem.
|
| 25 |
-
|
| 26 |
-
Rules:
|
| 27 |
-
- Output ONLY valid Lean 4 code. No explanation, no markdown, no fences.
|
| 28 |
-
- Begin with the necessary imports (e.g., `import Mathlib`).
|
| 29 |
-
- Use `theorem` or `lemma` keyword as appropriate.
|
| 30 |
-
- Use the provided relevant Mathlib4 lemmas as hints.
|
| 31 |
-
- If a previous attempt failed with a compiler error, fix only the reported issue.
|
| 32 |
-
"""
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def _build_user_message(
|
| 36 |
-
theorem: str,
|
| 37 |
-
context_lemmas: list[dict],
|
| 38 |
-
previous_error: Optional[str] = None,
|
| 39 |
-
previous_code: Optional[str] = None,
|
| 40 |
-
) -> str:
|
| 41 |
-
"""
|
| 42 |
-
Construct the user message for Claude.
|
| 43 |
-
"""
|
| 44 |
-
parts = []
|
| 45 |
-
|
| 46 |
-
# Relevant Mathlib4 lemmas from RAG
|
| 47 |
-
if context_lemmas:
|
| 48 |
-
parts.append("## Potentially Relevant Mathlib4 Lemmas\n")
|
| 49 |
-
for lemma in context_lemmas:
|
| 50 |
-
name = lemma.get("name", "?")
|
| 51 |
-
typ = lemma.get("type", "?")
|
| 52 |
-
doc = lemma.get("doc", "")
|
| 53 |
-
parts.append(f"- `{name} : {typ}`" + (f" — {doc}" if doc else ""))
|
| 54 |
-
parts.append("")
|
| 55 |
-
|
| 56 |
-
# The theorem to prove
|
| 57 |
-
parts.append(f"## Theorem to Prove\n```lean\n{theorem}\n```\n")
|
| 58 |
-
|
| 59 |
-
# Feedback from previous attempt
|
| 60 |
-
if previous_code and previous_error:
|
| 61 |
-
parts.append("## Previous Attempt (FAILED)\n")
|
| 62 |
-
parts.append(f"```lean\n{previous_code}\n```\n")
|
| 63 |
-
parts.append(f"**Compiler Error:**\n```\n{previous_error}\n```\n")
|
| 64 |
-
parts.append("Please fix the error above and output the corrected proof.")
|
| 65 |
-
else:
|
| 66 |
-
parts.append("Please generate a complete Lean 4 proof for the theorem above.")
|
| 67 |
-
|
| 68 |
-
return "\n".join(parts)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
class ProverAgent:
|
| 72 |
-
"""
|
| 73 |
-
Agentic prover that iteratively generates and validates Lean 4 proofs.
|
| 74 |
-
"""
|
| 75 |
-
|
| 76 |
-
def __init__(
|
| 77 |
-
self,
|
| 78 |
-
retriever: Optional[Retriever] = None,
|
| 79 |
-
model: str = "claude-opus-4-5",
|
| 80 |
-
max_retries: int = 5,
|
| 81 |
-
k_lemmas: int = 5,
|
| 82 |
-
lean_timeout: int = 60,
|
| 83 |
-
):
|
| 84 |
-
"""
|
| 85 |
-
Args:
|
| 86 |
-
retriever: Optional FAISS Retriever. If None, no RAG context is used.
|
| 87 |
-
model: Claude model ID to use.
|
| 88 |
-
max_retries: Maximum number of proof attempts before giving up.
|
| 89 |
-
k_lemmas: Number of Mathlib lemmas to retrieve per query.
|
| 90 |
-
lean_timeout: Timeout in seconds for the Lean compiler.
|
| 91 |
-
"""
|
| 92 |
-
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
| 93 |
-
if not api_key:
|
| 94 |
-
raise EnvironmentError(
|
| 95 |
-
"ANTHROPIC_API_KEY is not set. Please add it to your .env file."
|
| 96 |
-
)
|
| 97 |
-
self.client = anthropic.Anthropic(api_key=api_key)
|
| 98 |
-
self.retriever = retriever
|
| 99 |
-
self.model = model
|
| 100 |
-
self.max_retries = max_retries
|
| 101 |
-
self.k_lemmas = k_lemmas
|
| 102 |
-
self.lean_timeout = lean_timeout
|
| 103 |
-
|
| 104 |
-
def prove(self, theorem: str, on_attempt=None) -> tuple[bool, str, int]:
|
| 105 |
-
"""
|
| 106 |
-
Attempt to prove the given Lean 4 theorem statement.
|
| 107 |
-
|
| 108 |
-
Args:
|
| 109 |
-
theorem: A Lean 4 theorem/lemma declaration string.
|
| 110 |
-
on_attempt: Optional callback(attempt, code, result) for streaming UI.
|
| 111 |
-
|
| 112 |
-
Returns:
|
| 113 |
-
(success, final_code, num_attempts)
|
| 114 |
-
"""
|
| 115 |
-
# RAG: retrieve relevant lemmas
|
| 116 |
-
context_lemmas = []
|
| 117 |
-
if self.retriever:
|
| 118 |
-
context_lemmas = self.retriever.retrieve(theorem, k=self.k_lemmas)
|
| 119 |
-
|
| 120 |
-
messages = []
|
| 121 |
-
previous_code = None
|
| 122 |
-
previous_error = None
|
| 123 |
-
|
| 124 |
-
for attempt in range(1, self.max_retries + 1):
|
| 125 |
-
user_content = _build_user_message(
|
| 126 |
-
theorem=theorem,
|
| 127 |
-
context_lemmas=context_lemmas,
|
| 128 |
-
previous_error=previous_error,
|
| 129 |
-
previous_code=previous_code,
|
| 130 |
-
)
|
| 131 |
-
|
| 132 |
-
messages.append({"role": "user", "content": user_content})
|
| 133 |
-
|
| 134 |
-
# Call Claude
|
| 135 |
-
response = self.client.messages.create(
|
| 136 |
-
model=self.model,
|
| 137 |
-
max_tokens=2048,
|
| 138 |
-
system=SYSTEM_PROMPT,
|
| 139 |
-
messages=messages,
|
| 140 |
-
)
|
| 141 |
-
generated_code = response.content[0].text.strip()
|
| 142 |
-
|
| 143 |
-
# Append assistant response to conversation history
|
| 144 |
-
messages.append({"role": "assistant", "content": generated_code})
|
| 145 |
-
|
| 146 |
-
# Validate with Lean compiler
|
| 147 |
-
lean_result: LeanResult = run_lean(generated_code, timeout=self.lean_timeout)
|
| 148 |
-
|
| 149 |
-
if on_attempt:
|
| 150 |
-
on_attempt(attempt, generated_code, lean_result)
|
| 151 |
-
|
| 152 |
-
if lean_result.success:
|
| 153 |
-
return True, generated_code, attempt
|
| 154 |
-
|
| 155 |
-
# Feed error back for next iteration
|
| 156 |
-
previous_code = generated_code
|
| 157 |
-
previous_error = "\n".join(lean_result.errors) or lean_result.output[:500]
|
| 158 |
-
|
| 159 |
-
return False, previous_code or "", self.max_retries
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/cli.py
DELETED
|
@@ -1,162 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
cli.py
|
| 3 |
-
|
| 4 |
-
Interactive CLI for the Agentic Lean 4 Theorem Prover.
|
| 5 |
-
|
| 6 |
-
Usage:
|
| 7 |
-
python -m src.cli
|
| 8 |
-
python -m src.cli --theorem "theorem add_comm (a b : Nat) : a + b = b + a := by sorry"
|
| 9 |
-
python -m src.cli --file theorems.lean
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
import argparse
|
| 13 |
-
import os
|
| 14 |
-
import sys
|
| 15 |
-
from pathlib import Path
|
| 16 |
-
|
| 17 |
-
from dotenv import load_dotenv
|
| 18 |
-
from rich.console import Console
|
| 19 |
-
from rich.markdown import Markdown
|
| 20 |
-
from rich.panel import Panel
|
| 21 |
-
from rich.syntax import Syntax
|
| 22 |
-
from rich.text import Text
|
| 23 |
-
from rich import print as rprint
|
| 24 |
-
from rich.rule import Rule
|
| 25 |
-
from rich.spinner import Spinner
|
| 26 |
-
from rich.live import Live
|
| 27 |
-
from rich import box
|
| 28 |
-
|
| 29 |
-
from .agent import ProverAgent
|
| 30 |
-
from .lean_env import LeanResult
|
| 31 |
-
|
| 32 |
-
# Load .env for ANTHROPIC_API_KEY
|
| 33 |
-
load_dotenv()
|
| 34 |
-
|
| 35 |
-
console = Console()
|
| 36 |
-
|
| 37 |
-
BANNER = """
|
| 38 |
-
[bold cyan]╔══════════════════════════════════════════════════╗[/bold cyan]
|
| 39 |
-
[bold cyan]║ 🧮 Agentic Lean 4 Theorem Prover ║[/bold cyan]
|
| 40 |
-
[bold cyan]║ Powered by Claude + FAISS + Mathlib4 RAG ║[/bold cyan]
|
| 41 |
-
[bold cyan]╚══════════════════════════════════════════════════╝[/bold cyan]
|
| 42 |
-
"""
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def on_attempt_callback(attempt: int, code: str, result: LeanResult):
|
| 46 |
-
"""Rich-formatted callback for each proof attempt."""
|
| 47 |
-
console.print(Rule(f"[bold]Attempt {attempt}[/bold]", style="dim"))
|
| 48 |
-
console.print(Syntax(code, "lean", theme="monokai", line_numbers=True))
|
| 49 |
-
|
| 50 |
-
if result.success:
|
| 51 |
-
console.print(Panel("[bold green]✅ Lean accepted this proof![/bold green]", box=box.ROUNDED))
|
| 52 |
-
else:
|
| 53 |
-
errors_text = "\n".join(result.errors) if result.errors else result.output[:300]
|
| 54 |
-
console.print(
|
| 55 |
-
Panel(
|
| 56 |
-
f"[bold red]❌ Compiler rejected proof:[/bold red]\n\n[red]{errors_text}[/red]",
|
| 57 |
-
box=box.ROUNDED,
|
| 58 |
-
)
|
| 59 |
-
)
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def run_prover(theorem: str, use_rag: bool = True, max_retries: int = 5):
|
| 63 |
-
"""
|
| 64 |
-
Main entry point: set up the agent and run it on the given theorem.
|
| 65 |
-
"""
|
| 66 |
-
console.print(BANNER)
|
| 67 |
-
console.print(Panel(f"[bold yellow]Theorem to prove:[/bold yellow]\n\n{theorem}", box=box.ROUNDED))
|
| 68 |
-
|
| 69 |
-
# Optionally initialize retriever
|
| 70 |
-
retriever = None
|
| 71 |
-
if use_rag:
|
| 72 |
-
index_path = "data/mathlib.index"
|
| 73 |
-
meta_path = "data/mathlib_meta.pkl"
|
| 74 |
-
if os.path.exists(index_path) and os.path.exists(meta_path):
|
| 75 |
-
from .retriever import Retriever
|
| 76 |
-
console.print("[dim]Loading FAISS index for Mathlib4 RAG...[/dim]")
|
| 77 |
-
retriever = Retriever(index_path=index_path, meta_path=meta_path)
|
| 78 |
-
console.print("[green]✓ FAISS index loaded.[/green]\n")
|
| 79 |
-
else:
|
| 80 |
-
console.print(
|
| 81 |
-
"[yellow]⚠ FAISS index not found. Running without RAG context.[/yellow]\n"
|
| 82 |
-
"[dim]Run `python -m src.retriever` to build the index first.[/dim]\n"
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
agent = ProverAgent(retriever=retriever, max_retries=max_retries)
|
| 86 |
-
|
| 87 |
-
console.print(Rule("[bold]Starting Proof Search[/bold]", style="cyan"))
|
| 88 |
-
success, final_code, num_attempts = agent.prove(theorem, on_attempt=on_attempt_callback)
|
| 89 |
-
|
| 90 |
-
console.print(Rule(style="cyan"))
|
| 91 |
-
if success:
|
| 92 |
-
console.print(
|
| 93 |
-
Panel(
|
| 94 |
-
f"[bold green]🎉 Proof found in {num_attempts} attempt(s)![/bold green]",
|
| 95 |
-
box=box.DOUBLE,
|
| 96 |
-
)
|
| 97 |
-
)
|
| 98 |
-
console.print("\n[bold]Final Proof:[/bold]")
|
| 99 |
-
console.print(Syntax(final_code, "lean", theme="monokai", line_numbers=True))
|
| 100 |
-
else:
|
| 101 |
-
console.print(
|
| 102 |
-
Panel(
|
| 103 |
-
f"[bold red]💀 Failed to find a proof after {num_attempts} attempts.[/bold red]\n"
|
| 104 |
-
"[dim]Try increasing --max-retries or refining the theorem statement.[/dim]",
|
| 105 |
-
box=box.DOUBLE,
|
| 106 |
-
)
|
| 107 |
-
)
|
| 108 |
-
if final_code:
|
| 109 |
-
console.print("\n[bold]Last Generated Code:[/bold]")
|
| 110 |
-
console.print(Syntax(final_code, "lean", theme="monokai", line_numbers=True))
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def main():
|
| 114 |
-
parser = argparse.ArgumentParser(
|
| 115 |
-
description="Agentic Lean 4 Theorem Prover powered by Claude + FAISS + Mathlib4"
|
| 116 |
-
)
|
| 117 |
-
group = parser.add_mutually_exclusive_group()
|
| 118 |
-
group.add_argument(
|
| 119 |
-
"--theorem",
|
| 120 |
-
type=str,
|
| 121 |
-
help="A Lean 4 theorem declaration string to prove.",
|
| 122 |
-
)
|
| 123 |
-
group.add_argument(
|
| 124 |
-
"--file",
|
| 125 |
-
type=str,
|
| 126 |
-
help="Path to a .lean file containing the theorem to prove.",
|
| 127 |
-
)
|
| 128 |
-
parser.add_argument(
|
| 129 |
-
"--no-rag",
|
| 130 |
-
action="store_true",
|
| 131 |
-
help="Disable Mathlib4 RAG retrieval.",
|
| 132 |
-
)
|
| 133 |
-
parser.add_argument(
|
| 134 |
-
"--max-retries",
|
| 135 |
-
type=int,
|
| 136 |
-
default=5,
|
| 137 |
-
help="Maximum number of proof attempts (default: 5).",
|
| 138 |
-
)
|
| 139 |
-
args = parser.parse_args()
|
| 140 |
-
|
| 141 |
-
if args.file:
|
| 142 |
-
theorem = Path(args.file).read_text().strip()
|
| 143 |
-
elif args.theorem:
|
| 144 |
-
theorem = args.theorem.strip()
|
| 145 |
-
else:
|
| 146 |
-
console.print("[bold cyan]Enter your Lean 4 theorem (end with Ctrl+D on a new line):[/bold cyan]")
|
| 147 |
-
try:
|
| 148 |
-
lines = sys.stdin.read()
|
| 149 |
-
theorem = lines.strip()
|
| 150 |
-
except KeyboardInterrupt:
|
| 151 |
-
console.print("\n[yellow]Cancelled.[/yellow]")
|
| 152 |
-
sys.exit(0)
|
| 153 |
-
|
| 154 |
-
if not theorem:
|
| 155 |
-
console.print("[red]No theorem provided. Exiting.[/red]")
|
| 156 |
-
sys.exit(1)
|
| 157 |
-
|
| 158 |
-
run_prover(theorem=theorem, use_rag=not args.no_rag, max_retries=args.max_retries)
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
if __name__ == "__main__":
|
| 162 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/langgraph_agent.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import List, TypedDict
|
| 3 |
+
|
| 4 |
+
from langgraph.graph import END, StateGraph
|
| 5 |
+
|
| 6 |
+
from lean_verifier import LeanEnvironment
|
| 7 |
+
from rag_chain import RAGProofChain
|
| 8 |
+
from retriever import MathLibRetriever
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# ---------------------------------------------------------------------------
|
| 12 |
+
# State
|
| 13 |
+
# ---------------------------------------------------------------------------
|
| 14 |
+
|
| 15 |
+
class ProofState(TypedDict):
|
| 16 |
+
file_path: str
|
| 17 |
+
lean_code: str
|
| 18 |
+
goals: List[str]
|
| 19 |
+
errors: List[str]
|
| 20 |
+
attempt: int
|
| 21 |
+
max_retries: int
|
| 22 |
+
status: str # "pending" | "success" | "failed"
|
| 23 |
+
retrieved_lemmas: list
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
# Nodes
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
|
| 30 |
+
def _read_file(path: str) -> str:
|
| 31 |
+
with open(path, "r") as f:
|
| 32 |
+
return f.read()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _write_file(path: str, code: str) -> None:
|
| 36 |
+
with open(path, "w") as f:
|
| 37 |
+
f.write(code)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _extract_lean_code(text: str) -> str:
|
| 41 |
+
if "```lean" in text:
|
| 42 |
+
return text.split("```lean")[1].split("```")[0].strip()
|
| 43 |
+
if "```" in text:
|
| 44 |
+
return text.split("```")[1].split("```")[0].strip()
|
| 45 |
+
return text.strip()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def make_verify_node(lean_env: LeanEnvironment):
|
| 49 |
+
def verify_node(state: ProofState) -> ProofState:
|
| 50 |
+
print(f"\n--- Attempt {state['attempt'] + 1} / {state['max_retries']} ---")
|
| 51 |
+
code = _read_file(state["file_path"])
|
| 52 |
+
result = lean_env.verify_proof(code)
|
| 53 |
+
|
| 54 |
+
new_status = "success" if result["status"] == "success" else "pending"
|
| 55 |
+
if new_status == "success":
|
| 56 |
+
print("Proof verified successfully!")
|
| 57 |
+
else:
|
| 58 |
+
print(
|
| 59 |
+
f"Verification failed. "
|
| 60 |
+
f"Errors: {len(result['errors'])}, Goals: {len(result['goals'])}"
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
return {
|
| 64 |
+
**state,
|
| 65 |
+
"lean_code": code,
|
| 66 |
+
"errors": result["errors"],
|
| 67 |
+
"goals": result["goals"],
|
| 68 |
+
"status": new_status,
|
| 69 |
+
}
|
| 70 |
+
return verify_node
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def make_retrieve_node(retriever: MathLibRetriever):
|
| 74 |
+
def retrieve_node(state: ProofState) -> ProofState:
|
| 75 |
+
query = " ".join(state["goals"] + state["errors"])
|
| 76 |
+
print("Retrieving relevant Mathlib lemmas…")
|
| 77 |
+
lemmas = retriever.retrieve(query)
|
| 78 |
+
print(f" Retrieved {len(lemmas)} lemma(s).")
|
| 79 |
+
return {**state, "retrieved_lemmas": lemmas}
|
| 80 |
+
return retrieve_node
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def make_generate_node(chain: RAGProofChain):
|
| 84 |
+
def generate_node(state: ProofState) -> ProofState:
|
| 85 |
+
print("Generating proof with LLM…")
|
| 86 |
+
raw = chain.generate(
|
| 87 |
+
lean_code=state["lean_code"],
|
| 88 |
+
goals=state["goals"],
|
| 89 |
+
errors=state["errors"],
|
| 90 |
+
retrieved_lemmas=state["retrieved_lemmas"],
|
| 91 |
+
)
|
| 92 |
+
new_code = _extract_lean_code(raw)
|
| 93 |
+
|
| 94 |
+
if not new_code or new_code.strip() == state["lean_code"].strip():
|
| 95 |
+
print("LLM produced no changes.")
|
| 96 |
+
return {**state, "attempt": state["attempt"] + 1, "status": "failed"}
|
| 97 |
+
|
| 98 |
+
_write_file(state["file_path"], new_code)
|
| 99 |
+
print("File updated.")
|
| 100 |
+
return {
|
| 101 |
+
**state,
|
| 102 |
+
"lean_code": new_code,
|
| 103 |
+
"attempt": state["attempt"] + 1,
|
| 104 |
+
}
|
| 105 |
+
return generate_node
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# ---------------------------------------------------------------------------
|
| 109 |
+
# Router
|
| 110 |
+
# ---------------------------------------------------------------------------
|
| 111 |
+
|
| 112 |
+
def should_continue(state: ProofState) -> str:
|
| 113 |
+
if state["status"] == "success":
|
| 114 |
+
return END
|
| 115 |
+
if state["attempt"] >= state["max_retries"]:
|
| 116 |
+
return END
|
| 117 |
+
return "retrieve"
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
# ---------------------------------------------------------------------------
|
| 121 |
+
# Graph assembly
|
| 122 |
+
# ---------------------------------------------------------------------------
|
| 123 |
+
|
| 124 |
+
def build_graph(lean_env: LeanEnvironment, retriever: MathLibRetriever, chain: RAGProofChain):
|
| 125 |
+
g = StateGraph(ProofState)
|
| 126 |
+
|
| 127 |
+
g.add_node("verify", make_verify_node(lean_env))
|
| 128 |
+
g.add_node("retrieve", make_retrieve_node(retriever))
|
| 129 |
+
g.add_node("generate", make_generate_node(chain))
|
| 130 |
+
|
| 131 |
+
g.set_entry_point("verify")
|
| 132 |
+
g.add_conditional_edges("verify", should_continue, {"retrieve": "retrieve", END: END})
|
| 133 |
+
g.add_edge("retrieve", "generate")
|
| 134 |
+
g.add_edge("generate", "verify")
|
| 135 |
+
|
| 136 |
+
return g.compile()
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# ---------------------------------------------------------------------------
|
| 140 |
+
# Public entry point
|
| 141 |
+
# ---------------------------------------------------------------------------
|
| 142 |
+
|
| 143 |
+
class LangGraphAgent:
|
| 144 |
+
def __init__(
|
| 145 |
+
self,
|
| 146 |
+
model_name: str = "qwen3-vl:4b",
|
| 147 |
+
max_retries: int = 5,
|
| 148 |
+
index_dir: str | None = None,
|
| 149 |
+
):
|
| 150 |
+
self._lean_env = LeanEnvironment(use_mathlib=True)
|
| 151 |
+
self._retriever = MathLibRetriever(index_dir=index_dir)
|
| 152 |
+
self._chain = RAGProofChain(model_name=model_name)
|
| 153 |
+
self._graph = build_graph(self._lean_env, self._retriever, self._chain)
|
| 154 |
+
self._max_retries = max_retries
|
| 155 |
+
|
| 156 |
+
def solve_file(self, file_path: str) -> bool:
|
| 157 |
+
if not os.path.exists(file_path):
|
| 158 |
+
print(f"Error: {file_path} not found.")
|
| 159 |
+
return False
|
| 160 |
+
|
| 161 |
+
initial: ProofState = {
|
| 162 |
+
"file_path": file_path,
|
| 163 |
+
"lean_code": "",
|
| 164 |
+
"goals": [],
|
| 165 |
+
"errors": [],
|
| 166 |
+
"attempt": 0,
|
| 167 |
+
"max_retries": self._max_retries,
|
| 168 |
+
"status": "pending",
|
| 169 |
+
"retrieved_lemmas": [],
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
final = self._graph.invoke(initial)
|
| 173 |
+
return final["status"] == "success"
|
src/lean_env.py
DELETED
|
@@ -1,83 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
lean_env.py
|
| 3 |
-
|
| 4 |
-
Wrapper around the Lean 4 compiler. Sends Lean 4 code to the compiler,
|
| 5 |
-
captures stdout/stderr, and parses the output to determine success or failure.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import subprocess
|
| 9 |
-
import tempfile
|
| 10 |
-
import os
|
| 11 |
-
import re
|
| 12 |
-
from dataclasses import dataclass
|
| 13 |
-
from typing import Optional
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
@dataclass
|
| 17 |
-
class LeanResult:
|
| 18 |
-
success: bool
|
| 19 |
-
output: str
|
| 20 |
-
errors: list[str]
|
| 21 |
-
raw: str
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def run_lean(code: str, timeout: int = 60) -> LeanResult:
|
| 25 |
-
"""
|
| 26 |
-
Write `code` to a temporary .lean file, run `lean` on it, and parse
|
| 27 |
-
the compiler output.
|
| 28 |
-
|
| 29 |
-
Args:
|
| 30 |
-
code: Full Lean 4 source code string to validate.
|
| 31 |
-
timeout: Maximum seconds to wait for the compiler.
|
| 32 |
-
|
| 33 |
-
Returns:
|
| 34 |
-
A LeanResult with success flag, cleaned output, and parsed errors.
|
| 35 |
-
"""
|
| 36 |
-
with tempfile.NamedTemporaryFile(
|
| 37 |
-
mode="w", suffix=".lean", delete=False
|
| 38 |
-
) as f:
|
| 39 |
-
f.write(code)
|
| 40 |
-
tmp_path = f.name
|
| 41 |
-
|
| 42 |
-
try:
|
| 43 |
-
result = subprocess.run(
|
| 44 |
-
["lean", tmp_path],
|
| 45 |
-
capture_output=True,
|
| 46 |
-
text=True,
|
| 47 |
-
timeout=timeout,
|
| 48 |
-
)
|
| 49 |
-
raw = result.stdout + result.stderr
|
| 50 |
-
errors = _parse_errors(raw)
|
| 51 |
-
success = result.returncode == 0 and len(errors) == 0
|
| 52 |
-
return LeanResult(success=success, output=raw.strip(), errors=errors, raw=raw)
|
| 53 |
-
except FileNotFoundError:
|
| 54 |
-
return LeanResult(
|
| 55 |
-
success=False,
|
| 56 |
-
output="",
|
| 57 |
-
errors=["Lean 4 not found. Please install it via elan."],
|
| 58 |
-
raw="",
|
| 59 |
-
)
|
| 60 |
-
except subprocess.TimeoutExpired:
|
| 61 |
-
return LeanResult(
|
| 62 |
-
success=False,
|
| 63 |
-
output="",
|
| 64 |
-
errors=[f"Lean compiler timed out after {timeout}s."],
|
| 65 |
-
raw="",
|
| 66 |
-
)
|
| 67 |
-
finally:
|
| 68 |
-
os.unlink(tmp_path)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def _parse_errors(output: str) -> list[str]:
|
| 72 |
-
"""
|
| 73 |
-
Extract human-readable error messages from Lean 4 compiler output.
|
| 74 |
-
Lean 4 errors look like:
|
| 75 |
-
/path/to/file.lean:10:5: error: unknown identifier 'foo'
|
| 76 |
-
"""
|
| 77 |
-
pattern = re.compile(r".*?:\d+:\d+:\s*(error|warning):\s*(.+)")
|
| 78 |
-
errors = []
|
| 79 |
-
for line in output.splitlines():
|
| 80 |
-
m = pattern.match(line)
|
| 81 |
-
if m and m.group(1) == "error":
|
| 82 |
-
errors.append(m.group(2).strip())
|
| 83 |
-
return errors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lean_verifier.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any, List
|
| 2 |
+
|
| 3 |
+
from lean_interact import LeanREPLConfig, LeanServer, Command, TempRequireProject, LeanRequire
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class LeanEnvironment:
|
| 7 |
+
"""
|
| 8 |
+
Manages the Lean REPL environment for verifying Lean 4 proofs.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
def __init__(self, use_mathlib: bool = True, lean_version: str = "v4.8.0"):
|
| 12 |
+
"""
|
| 13 |
+
Initializes the Lean environment.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
use_mathlib (bool): If True, configures a TempRequireProject with Mathlib.
|
| 17 |
+
This may take a while to build on the first run.
|
| 18 |
+
lean_version (str): The Lean 4 version to use. Default is v4.8.0.
|
| 19 |
+
"""
|
| 20 |
+
self.lean_version = lean_version
|
| 21 |
+
self.use_mathlib = use_mathlib
|
| 22 |
+
|
| 23 |
+
if self.use_mathlib:
|
| 24 |
+
# We use TempRequireProject with mathlib as specified in lean_interact documentation
|
| 25 |
+
project = TempRequireProject(
|
| 26 |
+
lean_version=self.lean_version,
|
| 27 |
+
require="mathlib"
|
| 28 |
+
)
|
| 29 |
+
self.config = LeanREPLConfig(project=project)
|
| 30 |
+
else:
|
| 31 |
+
self.config = LeanREPLConfig(lean_version=self.lean_version)
|
| 32 |
+
|
| 33 |
+
self.server = LeanServer(self.config)
|
| 34 |
+
|
| 35 |
+
def verify_proof(self, lean_code: str) -> Dict[str, Any]:
|
| 36 |
+
"""
|
| 37 |
+
Executes a block of Lean code and verifies if it is a correct proof.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
lean_code (str): The full Lean 4 code string containing imports, theorem statement, and proof.
|
| 41 |
+
|
| 42 |
+
Returns:
|
| 43 |
+
dict: A dictionary containing the status, errors (if any), and goals (if open sorries remain).
|
| 44 |
+
"""
|
| 45 |
+
response = self.server.run(Command(cmd=lean_code))
|
| 46 |
+
|
| 47 |
+
errors = []
|
| 48 |
+
goals = []
|
| 49 |
+
|
| 50 |
+
# Check for error or warning messages
|
| 51 |
+
if hasattr(response, 'messages') and response.messages:
|
| 52 |
+
for msg in response.messages:
|
| 53 |
+
if msg.severity in ['error', 'warning']:
|
| 54 |
+
# E.g., 'declaration uses 'sorry'' is a warning, but we might want to capture it
|
| 55 |
+
errors.append(msg.data)
|
| 56 |
+
|
| 57 |
+
# Check for open goals (sorries)
|
| 58 |
+
if hasattr(response, 'sorries') and response.sorries:
|
| 59 |
+
for sorry in response.sorries:
|
| 60 |
+
if sorry.goal:
|
| 61 |
+
goals.append(sorry.goal)
|
| 62 |
+
|
| 63 |
+
is_success = len(errors) == 0 and len(goals) == 0
|
| 64 |
+
|
| 65 |
+
return {
|
| 66 |
+
"status": "success" if is_success else "failure",
|
| 67 |
+
"errors": errors,
|
| 68 |
+
"goals": goals,
|
| 69 |
+
"env": getattr(response, "env", None)
|
| 70 |
+
}
|
src/lmm_client.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ollama
|
| 2 |
+
from typing import List, Dict, Any, Optional
|
| 3 |
+
|
| 4 |
+
class LMMClient:
|
| 5 |
+
"""
|
| 6 |
+
Client for interacting with local LMMs via Ollama.
|
| 7 |
+
Focuses on Qwen3-VL:4B for high-reasoning tasks.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
def __init__(self, model_name: str = "qwen3-vl:4b"):
|
| 11 |
+
self.model_name = model_name
|
| 12 |
+
|
| 13 |
+
def chat(self, prompt: str, system_prompt: Optional[str] = None) -> str:
|
| 14 |
+
"""
|
| 15 |
+
Sends a chat request to the model.
|
| 16 |
+
"""
|
| 17 |
+
messages = []
|
| 18 |
+
if system_prompt:
|
| 19 |
+
messages.append({'role': 'system', 'content': system_prompt})
|
| 20 |
+
|
| 21 |
+
messages.append({'role': 'user', 'content': prompt})
|
| 22 |
+
|
| 23 |
+
response = ollama.chat(
|
| 24 |
+
model=self.model_name,
|
| 25 |
+
messages=messages
|
| 26 |
+
)
|
| 27 |
+
return response['message']['content']
|
| 28 |
+
|
| 29 |
+
def generate_proof_steps(self, lean_code: str, goals: List[str], errors: List[str]) -> str:
|
| 30 |
+
"""
|
| 31 |
+
Specific helper to generate proof steps based on current Lean state.
|
| 32 |
+
"""
|
| 33 |
+
system_prompt = (
|
| 34 |
+
"You are an expert Lean 4 proof assistant. "
|
| 35 |
+
"Your goal is to complete the proof by replacing 'sorry' with valid Lean 4 code. "
|
| 36 |
+
"Use Mathlib theorems where appropriate. "
|
| 37 |
+
"Respond ONLY with the corrected Lean code block."
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
prompt = f"""
|
| 41 |
+
Current Lean Code:
|
| 42 |
+
```lean
|
| 43 |
+
{lean_code}
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
Current Proof Goals:
|
| 47 |
+
{chr(10).join(goals)}
|
| 48 |
+
|
| 49 |
+
Lean Errors:
|
| 50 |
+
{chr(10).join(errors)}
|
| 51 |
+
|
| 52 |
+
Please provide the corrected Lean code. Focus on solving the current goals and fixing the errors.
|
| 53 |
+
"""
|
| 54 |
+
return self.chat(prompt, system_prompt)
|
src/mathlib_corpus.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import glob
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import List, Optional
|
| 6 |
+
|
| 7 |
+
from langchain_core.documents import Document
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# Regex to capture optional docstring + declaration line
|
| 11 |
+
_DECL_PATTERN = re.compile(
|
| 12 |
+
r'(?:/--\s*(.*?)\s*-/)?\s*' # optional /-- docstring -/
|
| 13 |
+
r'(?:@\[.*?\]\s*)*' # optional attributes
|
| 14 |
+
r'(theorem|lemma|def|noncomputable def)\s+'
|
| 15 |
+
r'(\S+)\s*' # declaration name
|
| 16 |
+
r'(.*?)\s*:=', # everything up to :=
|
| 17 |
+
re.DOTALL,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _find_mathlib_root() -> Optional[str]:
|
| 22 |
+
"""
|
| 23 |
+
Returns the path to the Mathlib4 source directory, searching common locations.
|
| 24 |
+
"""
|
| 25 |
+
candidates = [
|
| 26 |
+
# Lake package cache (used by lean-interact's TempRequireProject)
|
| 27 |
+
os.path.expanduser("~/.elan/toolchains"),
|
| 28 |
+
os.path.expanduser("~/.cache/mathlib"),
|
| 29 |
+
# Nix / Homebrew Lean setups
|
| 30 |
+
"/usr/local/lib/lean",
|
| 31 |
+
"/opt/homebrew/lib/lean",
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
# Also check for a .lake/packages directory next to the current file
|
| 35 |
+
here = Path(__file__).resolve().parent.parent
|
| 36 |
+
lake_pkg = here / ".lake" / "packages" / "mathlib" / "Mathlib"
|
| 37 |
+
if lake_pkg.exists():
|
| 38 |
+
return str(lake_pkg.parent)
|
| 39 |
+
|
| 40 |
+
for root in candidates:
|
| 41 |
+
if not os.path.isdir(root):
|
| 42 |
+
continue
|
| 43 |
+
# Walk up to 4 levels looking for a Mathlib directory
|
| 44 |
+
for dirpath, dirnames, _ in os.walk(root):
|
| 45 |
+
depth = dirpath.replace(root, "").count(os.sep)
|
| 46 |
+
if depth > 4:
|
| 47 |
+
dirnames.clear()
|
| 48 |
+
continue
|
| 49 |
+
if "Mathlib" in dirnames:
|
| 50 |
+
return dirpath
|
| 51 |
+
return None
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _parse_lean_file(path: str) -> List[Document]:
|
| 55 |
+
"""
|
| 56 |
+
Extracts theorem/lemma/def declarations from a single .lean file.
|
| 57 |
+
Returns a list of Documents with page_content = "<name> : <signature>".
|
| 58 |
+
"""
|
| 59 |
+
try:
|
| 60 |
+
text = Path(path).read_text(encoding="utf-8", errors="ignore")
|
| 61 |
+
except OSError:
|
| 62 |
+
return []
|
| 63 |
+
|
| 64 |
+
docs = []
|
| 65 |
+
for match in _DECL_PATTERN.finditer(text):
|
| 66 |
+
docstring = (match.group(1) or "").strip()
|
| 67 |
+
kind = match.group(2)
|
| 68 |
+
name = match.group(3)
|
| 69 |
+
signature = re.sub(r'\s+', ' ', match.group(4)).strip()
|
| 70 |
+
|
| 71 |
+
content = f"{name} : {signature}"
|
| 72 |
+
if docstring:
|
| 73 |
+
content = f"{docstring}\n{content}"
|
| 74 |
+
|
| 75 |
+
line = text[: match.start()].count("\n") + 1
|
| 76 |
+
docs.append(Document(
|
| 77 |
+
page_content=content,
|
| 78 |
+
metadata={"kind": kind, "name": name, "file": path, "line": line},
|
| 79 |
+
))
|
| 80 |
+
return docs
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class MathLibCorpus:
|
| 84 |
+
"""
|
| 85 |
+
Extracts LangChain Documents from Mathlib4 source files on disk.
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
def __init__(self, mathlib_root: Optional[str] = None):
|
| 89 |
+
self.mathlib_root = mathlib_root or _find_mathlib_root()
|
| 90 |
+
|
| 91 |
+
def extract(self, max_files: Optional[int] = None) -> List[Document]:
|
| 92 |
+
"""
|
| 93 |
+
Walks Mathlib source files and extracts declaration Documents.
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
max_files: If set, stop after processing this many .lean files
|
| 97 |
+
(useful for quick tests).
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
List of LangChain Documents, one per declaration found.
|
| 101 |
+
"""
|
| 102 |
+
if not self.mathlib_root:
|
| 103 |
+
raise RuntimeError(
|
| 104 |
+
"Could not locate Mathlib4 source. "
|
| 105 |
+
"Pass mathlib_root explicitly or run `lake exe cache get` first."
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
pattern = os.path.join(self.mathlib_root, "**", "*.lean")
|
| 109 |
+
files = glob.glob(pattern, recursive=True)
|
| 110 |
+
if max_files:
|
| 111 |
+
files = files[:max_files]
|
| 112 |
+
|
| 113 |
+
docs: List[Document] = []
|
| 114 |
+
for path in files:
|
| 115 |
+
docs.extend(_parse_lean_file(path))
|
| 116 |
+
|
| 117 |
+
return docs
|
src/proof_agent.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langgraph_agent import LangGraphAgent
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class ProofAgent:
|
| 5 |
+
"""Thin compatibility wrapper around LangGraphAgent."""
|
| 6 |
+
|
| 7 |
+
def __init__(self, model_name: str = "qwen3-vl:4b", max_retries: int = 5):
|
| 8 |
+
self._agent = LangGraphAgent(model_name=model_name, max_retries=max_retries)
|
| 9 |
+
|
| 10 |
+
def solve_file(self, file_path: str) -> bool:
|
| 11 |
+
return self._agent.solve_file(file_path)
|
src/rag_chain.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
|
| 3 |
+
from langchain_core.documents import Document
|
| 4 |
+
from langchain_core.output_parsers import StrOutputParser
|
| 5 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 6 |
+
from langchain_ollama import OllamaLLM
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
_SYSTEM = (
|
| 10 |
+
"You are an expert Lean 4 proof assistant with deep knowledge of Mathlib. "
|
| 11 |
+
"Your task is to complete the proof by replacing every `sorry` with valid Lean 4 tactic code. "
|
| 12 |
+
"Use only Mathlib theorems and tactics. "
|
| 13 |
+
"Respond ONLY with the corrected Lean code inside a single ```lean ... ``` block."
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
_HUMAN = """\
|
| 17 |
+
## Current Lean Code
|
| 18 |
+
```lean
|
| 19 |
+
{lean_code}
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## Open Proof Goals
|
| 23 |
+
{goals}
|
| 24 |
+
|
| 25 |
+
## Lean Errors
|
| 26 |
+
{errors}
|
| 27 |
+
|
| 28 |
+
## Relevant Mathlib Lemmas
|
| 29 |
+
{retrieved_lemmas}
|
| 30 |
+
|
| 31 |
+
Provide the corrected Lean code that solves all goals and fixes all errors.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _format_docs(docs: List[Document]) -> str:
|
| 36 |
+
if not docs:
|
| 37 |
+
return "(none retrieved)"
|
| 38 |
+
return "\n".join(
|
| 39 |
+
f"- `{d.metadata.get('name', '?')}`: {d.page_content}" for d in docs
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class RAGProofChain:
|
| 44 |
+
"""
|
| 45 |
+
LangChain LCEL chain: retrieved context + proof state → corrected Lean code.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def __init__(self, model_name: str = "qwen3-vl:4b"):
|
| 49 |
+
prompt = ChatPromptTemplate.from_messages([
|
| 50 |
+
("system", _SYSTEM),
|
| 51 |
+
("human", _HUMAN),
|
| 52 |
+
])
|
| 53 |
+
llm = OllamaLLM(model=model_name)
|
| 54 |
+
self._chain = prompt | llm | StrOutputParser()
|
| 55 |
+
|
| 56 |
+
def generate(
|
| 57 |
+
self,
|
| 58 |
+
lean_code: str,
|
| 59 |
+
goals: List[str],
|
| 60 |
+
errors: List[str],
|
| 61 |
+
retrieved_lemmas: List[Document],
|
| 62 |
+
) -> str:
|
| 63 |
+
"""
|
| 64 |
+
Generate corrected Lean code given the current proof state and retrieved lemmas.
|
| 65 |
+
"""
|
| 66 |
+
return self._chain.invoke({
|
| 67 |
+
"lean_code": lean_code,
|
| 68 |
+
"goals": "\n".join(goals) or "(none)",
|
| 69 |
+
"errors": "\n".join(errors) or "(none)",
|
| 70 |
+
"retrieved_lemmas": _format_docs(retrieved_lemmas),
|
| 71 |
+
})
|
src/retriever.py
CHANGED
|
@@ -1,113 +1,126 @@
|
|
| 1 |
-
"""
|
| 2 |
-
retriever.py
|
| 3 |
-
|
| 4 |
-
Builds and queries a FAISS index over Mathlib4 lemma docstrings.
|
| 5 |
-
|
| 6 |
-
Usage:
|
| 7 |
-
# Build index from a jsonl file of lemmas:
|
| 8 |
-
build_index("data/mathlib_lemmas.jsonl", "data/mathlib.index")
|
| 9 |
-
|
| 10 |
-
# Query at runtime:
|
| 11 |
-
retriever = Retriever("data/mathlib.index", "data/mathlib_lemmas.jsonl")
|
| 12 |
-
lemmas = retriever.retrieve("commutativity of addition", k=5)
|
| 13 |
-
"""
|
| 14 |
-
|
| 15 |
-
import json
|
| 16 |
import os
|
| 17 |
-
import pickle
|
| 18 |
from pathlib import Path
|
| 19 |
-
from typing import Optional
|
| 20 |
|
| 21 |
-
import
|
| 22 |
-
|
| 23 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
-
INDEX_FILE = "data/mathlib.index"
|
| 27 |
-
META_FILE = "data/mathlib_meta.pkl"
|
| 28 |
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
|
|
|
|
| 31 |
"""
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
| 33 |
"""
|
| 34 |
|
| 35 |
def __init__(
|
| 36 |
self,
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
):
|
| 41 |
-
self.
|
| 42 |
-
self.
|
| 43 |
-
|
| 44 |
-
|
| 45 |
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
"""
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
Args:
|
| 51 |
-
query: Natural
|
| 52 |
-
k:
|
| 53 |
|
| 54 |
Returns:
|
| 55 |
-
List of
|
| 56 |
"""
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
return [
|
| 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 |
-
print(f"Index saved to {index_path} ({len(texts)} vectors, dim={dim})")
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
if __name__ == "__main__":
|
| 113 |
-
build_index()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
|
|
|
| 2 |
from pathlib import Path
|
| 3 |
+
from typing import List, Optional
|
| 4 |
|
| 5 |
+
from langchain_community.retrievers import BM25Retriever
|
| 6 |
+
from langchain_community.vectorstores import FAISS
|
| 7 |
+
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
|
| 8 |
+
from langchain.retrievers import EnsembleRetriever
|
| 9 |
+
from langchain.retrievers.document_compressors import CrossEncoderReranker
|
| 10 |
+
from langchain.retrievers import ContextualCompressionRetriever
|
| 11 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 12 |
+
from langchain_core.documents import Document
|
| 13 |
|
| 14 |
+
from mathlib_corpus import MathLibCorpus
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
_DEFAULT_INDEX_DIR = Path(__file__).resolve().parent.parent / "data" / "mathlib_index"
|
| 17 |
+
_EMBED_MODEL = "all-MiniLM-L6-v2"
|
| 18 |
+
_RERANK_MODEL = "cross-encoder/ms-marco-MiniLM-L-6-v2"
|
| 19 |
|
| 20 |
+
|
| 21 |
+
class MathLibRetriever:
|
| 22 |
"""
|
| 23 |
+
Hybrid FAISS + BM25 retriever with CrossEncoder reranking over Mathlib lemmas.
|
| 24 |
+
|
| 25 |
+
On first use, call build() to create and persist the index.
|
| 26 |
+
Subsequent runs load from disk automatically.
|
| 27 |
"""
|
| 28 |
|
| 29 |
def __init__(
|
| 30 |
self,
|
| 31 |
+
index_dir: Optional[str] = None,
|
| 32 |
+
top_k: int = 20,
|
| 33 |
+
rerank_top_k: int = 5,
|
| 34 |
):
|
| 35 |
+
self.index_dir = Path(index_dir) if index_dir else _DEFAULT_INDEX_DIR
|
| 36 |
+
self.top_k = top_k
|
| 37 |
+
self.rerank_top_k = rerank_top_k
|
| 38 |
+
self._retriever = None
|
| 39 |
|
| 40 |
+
# ------------------------------------------------------------------
|
| 41 |
+
# Public API
|
| 42 |
+
# ------------------------------------------------------------------
|
| 43 |
+
|
| 44 |
+
def build(self, mathlib_root: Optional[str] = None, max_files: Optional[int] = None) -> None:
|
| 45 |
+
"""
|
| 46 |
+
Extract Mathlib documents, build FAISS + BM25 indices, and persist to disk.
|
| 47 |
+
Call this once (via scripts/build_index.py) before first use.
|
| 48 |
"""
|
| 49 |
+
print("Extracting Mathlib corpus…")
|
| 50 |
+
corpus = MathLibCorpus(mathlib_root=mathlib_root)
|
| 51 |
+
docs = corpus.extract(max_files=max_files)
|
| 52 |
+
print(f" {len(docs)} declarations extracted.")
|
| 53 |
+
|
| 54 |
+
embeddings = self._embeddings()
|
| 55 |
+
|
| 56 |
+
print("Building FAISS index…")
|
| 57 |
+
faiss_store = FAISS.from_documents(docs, embeddings)
|
| 58 |
+
self.index_dir.mkdir(parents=True, exist_ok=True)
|
| 59 |
+
faiss_store.save_local(str(self.index_dir))
|
| 60 |
+
print(f" Index saved to {self.index_dir}")
|
| 61 |
+
|
| 62 |
+
self._retriever = self._build_retriever(faiss_store, docs)
|
| 63 |
+
|
| 64 |
+
def retrieve(self, query: str, k: Optional[int] = None) -> List[Document]:
|
| 65 |
+
"""
|
| 66 |
+
Retrieve and rerank the most relevant Mathlib lemmas for a query.
|
| 67 |
|
| 68 |
Args:
|
| 69 |
+
query: Natural-language or Lean-syntax query (e.g., proof goals + errors).
|
| 70 |
+
k: Number of results to return after reranking (defaults to self.rerank_top_k).
|
| 71 |
|
| 72 |
Returns:
|
| 73 |
+
List of Documents ranked by relevance.
|
| 74 |
"""
|
| 75 |
+
if self._retriever is None:
|
| 76 |
+
self._load()
|
| 77 |
+
results = self._retriever.invoke(query)
|
| 78 |
+
return results[: k or self.rerank_top_k]
|
| 79 |
+
|
| 80 |
+
def is_index_built(self) -> bool:
|
| 81 |
+
return (self.index_dir / "index.faiss").exists()
|
| 82 |
+
|
| 83 |
+
# ------------------------------------------------------------------
|
| 84 |
+
# Internal helpers
|
| 85 |
+
# ------------------------------------------------------------------
|
| 86 |
+
|
| 87 |
+
def _embeddings(self) -> HuggingFaceEmbeddings:
|
| 88 |
+
return HuggingFaceEmbeddings(model_name=_EMBED_MODEL)
|
| 89 |
+
|
| 90 |
+
def _load(self) -> None:
|
| 91 |
+
if not self.is_index_built():
|
| 92 |
+
raise RuntimeError(
|
| 93 |
+
f"No FAISS index found at {self.index_dir}. "
|
| 94 |
+
"Run `python scripts/build_index.py` first."
|
| 95 |
+
)
|
| 96 |
+
print("Loading FAISS index from disk…")
|
| 97 |
+
embeddings = self._embeddings()
|
| 98 |
+
faiss_store = FAISS.load_local(
|
| 99 |
+
str(self.index_dir),
|
| 100 |
+
embeddings,
|
| 101 |
+
allow_dangerous_deserialization=True,
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
# Re-build BM25 from FAISS docstore
|
| 105 |
+
docs = list(faiss_store.docstore._dict.values())
|
| 106 |
+
self._retriever = self._build_retriever(faiss_store, docs)
|
| 107 |
+
|
| 108 |
+
def _build_retriever(self, faiss_store: FAISS, docs: List[Document]):
|
| 109 |
+
faiss_retriever = faiss_store.as_retriever(
|
| 110 |
+
search_kwargs={"k": self.top_k}
|
| 111 |
+
)
|
| 112 |
+
bm25_retriever = BM25Retriever.from_documents(docs)
|
| 113 |
+
bm25_retriever.k = self.top_k
|
| 114 |
+
|
| 115 |
+
ensemble = EnsembleRetriever(
|
| 116 |
+
retrievers=[faiss_retriever, bm25_retriever],
|
| 117 |
+
weights=[0.6, 0.4],
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
cross_encoder = HuggingFaceCrossEncoder(model_name=_RERANK_MODEL)
|
| 121 |
+
reranker = CrossEncoderReranker(model=cross_encoder, top_n=self.rerank_top_k)
|
| 122 |
+
|
| 123 |
+
return ContextualCompressionRetriever(
|
| 124 |
+
base_compressor=reranker,
|
| 125 |
+
base_retriever=ensemble,
|
| 126 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_lean_verifier.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import unittest
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
# Add src to Python path
|
| 6 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
|
| 7 |
+
|
| 8 |
+
from lean_verifier import LeanEnvironment
|
| 9 |
+
|
| 10 |
+
class TestLeanVerifier(unittest.TestCase):
|
| 11 |
+
@classmethod
|
| 12 |
+
def setUpClass(cls):
|
| 13 |
+
# We'll use Mathlib here since our goal is to verify it works with the MVP setup
|
| 14 |
+
cls.lean_env = LeanEnvironment(use_mathlib=True)
|
| 15 |
+
|
| 16 |
+
def test_correct_proof(self):
|
| 17 |
+
lean_code = """
|
| 18 |
+
import Mathlib
|
| 19 |
+
|
| 20 |
+
theorem add_comm_test (n m : Nat) : n + m = m + n := by
|
| 21 |
+
exact Nat.add_comm n m
|
| 22 |
+
"""
|
| 23 |
+
result = self.lean_env.verify_proof(lean_code)
|
| 24 |
+
self.assertEqual(result["status"], "success")
|
| 25 |
+
self.assertEqual(len(result["errors"]), 0)
|
| 26 |
+
self.assertEqual(len(result["goals"]), 0)
|
| 27 |
+
|
| 28 |
+
def test_incorrect_proof_type_mismatch(self):
|
| 29 |
+
lean_code = """
|
| 30 |
+
import Mathlib
|
| 31 |
+
|
| 32 |
+
theorem add_comm_test (n m : Nat) : n + m = m + n := by
|
| 33 |
+
exact n
|
| 34 |
+
"""
|
| 35 |
+
result = self.lean_env.verify_proof(lean_code)
|
| 36 |
+
self.assertEqual(result["status"], "failure")
|
| 37 |
+
self.assertTrue(any("type mismatch" in err or "application type mismatch" in err for err in result["errors"]),
|
| 38 |
+
f"Expected type mismatch error, got: {result['errors']}")
|
| 39 |
+
|
| 40 |
+
def test_incomplete_proof_sorry(self):
|
| 41 |
+
lean_code = """
|
| 42 |
+
import Mathlib
|
| 43 |
+
|
| 44 |
+
theorem my_incomplete_thm (n : Nat) : n = 5 → n = 5 := by
|
| 45 |
+
sorry
|
| 46 |
+
"""
|
| 47 |
+
result = self.lean_env.verify_proof(lean_code)
|
| 48 |
+
self.assertEqual(result["status"], "failure")
|
| 49 |
+
# Ensure it has an error indicating sorry
|
| 50 |
+
self.assertTrue(any("uses 'sorry'" in err for err in result["errors"]),
|
| 51 |
+
f"Expected sorry warning/error, got: {result['errors']}")
|
| 52 |
+
# Ensure it outputs the goal
|
| 53 |
+
self.assertEqual(len(result["goals"]), 1)
|
| 54 |
+
self.assertIn("⊢ n = 5 → n = 5", result["goals"][0])
|
| 55 |
+
|
| 56 |
+
if __name__ == '__main__':
|
| 57 |
+
unittest.main()
|