Files
lean-bench/leanbench/srcsimplex.py
claude-bot ca98703538
ci / python (pull_request) Successful in 17s
ci / lean (pull_request) Successful in 1m10s
ci / image (pull_request) Has been skipped
Pilot prep: simplex schema, projection, alignment, lint, stats, pilot scripts, protocol v1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYqwNMyPUHeTbFWg83GQq6
2026-10-22 05:52:28 +00:00

166 lines
6.4 KiB
Python

"""The source-side simplex: what the *prose* gives, assumes and claims.
One model call reads only the book passage -- never the Lean -- and lists its roles.
Keeping the Lean out of view is the point: the schema must come from the source, so
that comparing it with the Lean-side schema is a check and not an echo.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from typing import Any
from .llm import ChatClient, parse_json
PROMPT_VERSION = "srcsimplex-v1"
INTRODUCED = ("given", "forall", "exists")
SYSTEM_PROMPT = """\
You read ONE passage from a mathematics book and describe its logical parameters.
Do not write Lean. Do not solve anything. Describe only what the passage itself says.
Return a JSON object:
{
"roles": [
{"kind": "object", "symbols": ["G"], "description": "finite group",
"keywords": ["finite", "group"], "introduced": "given"},
{"kind": "fact", "symbols": ["n"], "description": "n is positive",
"keywords": ["positive"], "introduced": "given"},
{"kind": "goal", "symbols": [], "description": "every element's order divides |G|",
"keywords": ["order", "divides"], "introduced": "given"}
],
"goal_has_exists": false
}
Rules:
- "object": a mathematical object the passage is about (a group G, an integer n, a
function f). One entry per object, even if several share a description.
- "fact": a stated assumption or condition on those objects ("n > 0", "G is
finite", "a and b are coprime"). Include conditions hidden in prose ("let G be a
finite group" gives an object G AND a fact that G is finite).
- "goal": exactly one entry: the claim being asserted, the property being defined, or
for an exercise the thing to be shown. For a definition, the goal is what is defined.
- "symbols": the letters/names used in the text, as written. Empty if none.
- "keywords": 1-5 lowercase words naming the concepts involved (e.g. "integer",
"prime", "subgroup", "homomorphism", "finite"). Words from the text, not synonyms.
- "introduced" (objects only): "given" if the passage fixes it ("let n be..."),
"forall" if it ranges over all of them ("for every n"), "exists" if the passage asserts
one exists ("there is an n such that"). Use "given" when unsure.
- "goal_has_exists": true only if the claim itself asserts that something exists.
- At least two roles in total, and exactly one goal.
Reply with the JSON object only."""
@dataclass
class SourceRole:
kind: str
symbols: list[str] = field(default_factory=list)
description: str = ""
keywords: list[str] = field(default_factory=list)
introduced: str = "given"
alias: str = ""
def as_dict(self) -> dict[str, Any]:
return {
"alias": self.alias, "kind": self.kind, "symbols": self.symbols,
"description": self.description, "keywords": self.keywords,
"introduced": self.introduced,
}
@dataclass
class SourceSimplex:
roles: list[SourceRole]
goal_has_exists: bool = False
notes: list[str] = field(default_factory=list)
def by_kind(self, kind: str) -> list[SourceRole]:
return [r for r in self.roles if r.kind == kind]
@property
def goal(self) -> SourceRole | None:
goals = self.by_kind("goal")
return goals[0] if goals else None
def as_dict(self) -> dict[str, Any]:
return {
"roles": [r.as_dict() for r in self.roles],
"goal_has_exists": self.goal_has_exists,
"notes": self.notes,
}
def _strings(value: Any) -> list[str]:
if isinstance(value, str):
value = [value]
if not isinstance(value, list):
return []
return [str(v).strip() for v in value if str(v).strip()]
def parse_source_simplex(payload: Any) -> SourceSimplex:
"""Validate a model reply; raises ``ValueError`` when it is not a usable schema."""
if not isinstance(payload, dict) or not isinstance(payload.get("roles"), list):
raise ValueError("reply has no 'roles' list")
roles: list[SourceRole] = []
counters: dict[str, int] = {}
notes: list[str] = []
for item in payload["roles"]:
if not isinstance(item, dict):
continue
kind = str(item.get("kind", "")).strip().lower()
if kind not in ("object", "fact", "goal"):
notes.append(f"dropped role with kind {kind!r}")
continue
introduced = str(item.get("introduced", "given")).strip().lower()
counters[kind] = counters.get(kind, 0) + 1
roles.append(SourceRole(
kind=kind,
symbols=_strings(item.get("symbols")),
description=str(item.get("description", "")).strip(),
keywords=[k.lower() for k in _strings(item.get("keywords"))],
introduced=introduced if introduced in INTRODUCED else "given",
alias=f"{kind}{counters[kind]}",
))
goals = [r for r in roles if r.kind == "goal"]
if len(goals) != 1:
raise ValueError(f"expected exactly one goal role, got {len(goals)}")
if len(roles) < 2:
raise ValueError("a simplex needs at least two roles")
return SourceSimplex(roles, bool(payload.get("goal_has_exists")), notes)
def passage_prompt(text: str, label: str = "") -> list[dict[str, str]]:
header = f"Label: {label}\n\n" if label else ""
body = re.sub(r"[ \t]+", " ", text).strip()
return [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"{header}Passage:\n<<<BEGIN>>>\n{body}\n<<<END>>>"},
]
def extract_source_simplex(
client: ChatClient, text: str, label: str = "", *, attempts: int = 2
) -> SourceSimplex:
"""Ask the model for the passage's roles; retry once if the reply is unusable."""
messages = passage_prompt(text, label)
last: ValueError | None = None
for _ in range(attempts):
payload = client.complete_json(messages) # type: ignore[attr-defined]
try:
return parse_source_simplex(payload)
except ValueError as error:
last = error
messages = messages + [
{"role": "assistant", "content": str(payload)[:3000]},
{"role": "user", "content": f"Not usable: {error}. Reply with the JSON only."},
]
raise ValueError(f"no usable source simplex: {last}")
__all__ = [
"PROMPT_VERSION", "SYSTEM_PROMPT", "SourceRole", "SourceSimplex",
"extract_source_simplex", "parse_json", "parse_source_simplex", "passage_prompt",
]