Spaces:
Sleeping
Fix LLM hallucinated imports and slow thinking mode (#2)
Browse files* Fix import paths and Mathlib auto-detection for LangChain 1.x
EnsembleRetriever and CrossEncoderReranker moved to langchain_classic
in LangChain 1.x — update retriever.py imports accordingly.
MathLibCorpus now searches lean-interact's TempRequireProject cache
(.lake/packages/mathlib) for Mathlib source and prefers the Mathlib/
subdirectory to avoid import-only root files returning zero declarations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix LLM hallucinated imports and slow thinking mode
- langgraph_agent.py: add _sanitize_imports() which strips all LLM-generated
import lines and replaces them with `import Mathlib`. LLMs consistently
hallucinate non-existent Lean paths (e.g. Mathlib.TacticLibrary.Peel) that
break compilation; the correct import for any Mathlib proof is always
`import Mathlib`.
- rag_chain.py: tighten system prompt with explicit rules (keep imports,
no spurious `open`, prefer simple tactics). Add num_predict=1024 cap and
think=False to disable chain-of-thought mode on qwen3/gemma3 models —
without this, models spend 20+ minutes generating reasoning tokens before
the actual answer.
Tested: simple_add.lean (n + 0 = n) solved in 2 attempts with gemma3:12b,
verified by Lean REPL. Proof: `simp`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: p4r5kpftnp-cmd <p4r5kpftnp@privaterelay.appleid.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- src/langgraph_agent.py +12 -1
- src/mathlib_corpus.py +24 -13
- src/rag_chain.py +13 -3
- src/retriever.py +2 -3
|
@@ -45,6 +45,17 @@ def _extract_lean_code(text: str) -> str:
|
|
| 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']} ---")
|
|
@@ -89,7 +100,7 @@ def make_generate_node(chain: RAGProofChain):
|
|
| 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.")
|
|
|
|
| 45 |
return text.strip()
|
| 46 |
|
| 47 |
|
| 48 |
+
def _sanitize_imports(code: str) -> str:
|
| 49 |
+
"""
|
| 50 |
+
LLMs often hallucinate Lean import paths. This function strips all `import`
|
| 51 |
+
lines from the generated code and replaces them with `import Mathlib`, which
|
| 52 |
+
is the correct single import for any Mathlib-based proof.
|
| 53 |
+
"""
|
| 54 |
+
lines = code.splitlines()
|
| 55 |
+
non_import_lines = [l for l in lines if not l.strip().startswith("import ")]
|
| 56 |
+
return "import Mathlib\n\n" + "\n".join(non_import_lines).lstrip()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
def make_verify_node(lean_env: LeanEnvironment):
|
| 60 |
def verify_node(state: ProofState) -> ProofState:
|
| 61 |
print(f"\n--- Attempt {state['attempt'] + 1} / {state['max_retries']} ---")
|
|
|
|
| 100 |
errors=state["errors"],
|
| 101 |
retrieved_lemmas=state["retrieved_lemmas"],
|
| 102 |
)
|
| 103 |
+
new_code = _sanitize_imports(_extract_lean_code(raw))
|
| 104 |
|
| 105 |
if not new_code or new_code.strip() == state["lean_code"].strip():
|
| 106 |
print("LLM produced no changes.")
|
|
@@ -22,28 +22,36 @@ 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 >
|
| 47 |
dirnames.clear()
|
| 48 |
continue
|
| 49 |
if "Mathlib" in dirnames:
|
|
@@ -105,7 +113,10 @@ class MathLibCorpus:
|
|
| 105 |
"Pass mathlib_root explicitly or run `lake exe cache get` first."
|
| 106 |
)
|
| 107 |
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
| 109 |
files = glob.glob(pattern, recursive=True)
|
| 110 |
if max_files:
|
| 111 |
files = files[:max_files]
|
|
|
|
| 22 |
"""
|
| 23 |
Returns the path to the Mathlib4 source directory, searching common locations.
|
| 24 |
"""
|
| 25 |
+
# 1. .lake/packages next to the current project file
|
| 26 |
+
here = Path(__file__).resolve().parent.parent
|
| 27 |
+
lake_pkg = here / ".lake" / "packages" / "mathlib"
|
| 28 |
+
if (lake_pkg / "Mathlib").exists():
|
| 29 |
+
return str(lake_pkg)
|
| 30 |
+
|
| 31 |
+
# 2. lean-interact's TempRequireProject cache
|
| 32 |
+
try:
|
| 33 |
+
from lean_interact.config import DEFAULT_CACHE_DIR
|
| 34 |
+
tmp_root = DEFAULT_CACHE_DIR / "tmp_projects"
|
| 35 |
+
for lean_ver_dir in sorted(tmp_root.iterdir(), reverse=True):
|
| 36 |
+
for project_dir in lean_ver_dir.iterdir():
|
| 37 |
+
candidate = project_dir / ".lake" / "packages" / "mathlib"
|
| 38 |
+
if (candidate / "Mathlib").exists():
|
| 39 |
+
return str(candidate)
|
| 40 |
+
except Exception:
|
| 41 |
+
pass
|
| 42 |
+
|
| 43 |
+
# 3. Broad filesystem search in common Lean locations
|
| 44 |
candidates = [
|
|
|
|
| 45 |
os.path.expanduser("~/.elan/toolchains"),
|
|
|
|
|
|
|
| 46 |
"/usr/local/lib/lean",
|
| 47 |
"/opt/homebrew/lib/lean",
|
| 48 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
for root in candidates:
|
| 50 |
if not os.path.isdir(root):
|
| 51 |
continue
|
|
|
|
| 52 |
for dirpath, dirnames, _ in os.walk(root):
|
| 53 |
depth = dirpath.replace(root, "").count(os.sep)
|
| 54 |
+
if depth > 6:
|
| 55 |
dirnames.clear()
|
| 56 |
continue
|
| 57 |
if "Mathlib" in dirnames:
|
|
|
|
| 113 |
"Pass mathlib_root explicitly or run `lake exe cache get` first."
|
| 114 |
)
|
| 115 |
|
| 116 |
+
# Prefer Mathlib/ subdirectory if it exists (avoids index-only files at root)
|
| 117 |
+
mathlib_src = os.path.join(self.mathlib_root, "Mathlib")
|
| 118 |
+
search_root = mathlib_src if os.path.isdir(mathlib_src) else self.mathlib_root
|
| 119 |
+
pattern = os.path.join(search_root, "**", "*.lean")
|
| 120 |
files = glob.glob(pattern, recursive=True)
|
| 121 |
if max_files:
|
| 122 |
files = files[:max_files]
|
|
@@ -9,8 +9,12 @@ from langchain_ollama import OllamaLLM
|
|
| 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 |
-
"
|
| 13 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
)
|
| 15 |
|
| 16 |
_HUMAN = """\
|
|
@@ -50,7 +54,13 @@ class RAGProofChain:
|
|
| 50 |
("system", _SYSTEM),
|
| 51 |
("human", _HUMAN),
|
| 52 |
])
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
self._chain = prompt | llm | StrOutputParser()
|
| 55 |
|
| 56 |
def generate(
|
|
|
|
| 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 |
+
"RULES:\n"
|
| 13 |
+
"1. Keep `import Mathlib` exactly as-is at the top. Do NOT add, remove, or change any import lines.\n"
|
| 14 |
+
"2. Do NOT add `open` statements unless they were already in the original code.\n"
|
| 15 |
+
"3. Keep the theorem signature exactly as given — do not change argument names or types.\n"
|
| 16 |
+
"4. Replace `sorry` with valid Lean 4 tactic(s). Prefer simple tactics: `simp`, `omega`, `ring`, `exact`, `apply`.\n"
|
| 17 |
+
"5. Respond ONLY with the complete corrected Lean code inside a single ```lean ... ``` block."
|
| 18 |
)
|
| 19 |
|
| 20 |
_HUMAN = """\
|
|
|
|
| 54 |
("system", _SYSTEM),
|
| 55 |
("human", _HUMAN),
|
| 56 |
])
|
| 57 |
+
# Disable thinking/chain-of-thought mode (qwen3, gemma3) and cap output
|
| 58 |
+
# so the agent doesn't spend minutes generating reasoning tokens.
|
| 59 |
+
llm = OllamaLLM(
|
| 60 |
+
model=model_name,
|
| 61 |
+
num_predict=1024, # cap response length
|
| 62 |
+
options={"think": False}, # disable thinking mode (qwen3/gemma3)
|
| 63 |
+
)
|
| 64 |
self._chain = prompt | llm | StrOutputParser()
|
| 65 |
|
| 66 |
def generate(
|
|
@@ -5,9 +5,8 @@ from typing import List, Optional
|
|
| 5 |
from langchain_community.retrievers import BM25Retriever
|
| 6 |
from langchain_community.vectorstores import FAISS
|
| 7 |
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
|
| 8 |
-
from
|
| 9 |
-
from
|
| 10 |
-
from langchain.retrievers import ContextualCompressionRetriever
|
| 11 |
from langchain_huggingface import HuggingFaceEmbeddings
|
| 12 |
from langchain_core.documents import Document
|
| 13 |
|
|
|
|
| 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_classic.retrievers import EnsembleRetriever, ContextualCompressionRetriever
|
| 9 |
+
from langchain_classic.retrievers.document_compressors import CrossEncoderReranker
|
|
|
|
| 10 |
from langchain_huggingface import HuggingFaceEmbeddings
|
| 11 |
from langchain_core.documents import Document
|
| 12 |
|