Ray5th p4r5kpftnp-cmd Claude Sonnet 4.6 commited on
Commit
975f30b
·
unverified ·
1 Parent(s): 4be8b34

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 CHANGED
@@ -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.")
src/mathlib_corpus.py CHANGED
@@ -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 > 4:
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
- pattern = os.path.join(self.mathlib_root, "**", "*.lean")
 
 
 
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]
src/rag_chain.py CHANGED
@@ -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
- "Use only Mathlib theorems and tactics. "
13
- "Respond ONLY with the corrected Lean code inside a single ```lean ... ``` block."
 
 
 
 
14
  )
15
 
16
  _HUMAN = """\
@@ -50,7 +54,13 @@ class RAGProofChain:
50
  ("system", _SYSTEM),
51
  ("human", _HUMAN),
52
  ])
53
- llm = OllamaLLM(model=model_name)
 
 
 
 
 
 
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(
src/retriever.py CHANGED
@@ -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 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
 
 
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