neurocore-skill-math 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- neurocore_skill_math/__init__.py +72 -0
- neurocore_skill_math/_availability.py +51 -0
- neurocore_skill_math/_base.py +112 -0
- neurocore_skill_math/_formats.py +54 -0
- neurocore_skill_math/_llm.py +54 -0
- neurocore_skill_math/_run.py +81 -0
- neurocore_skill_math/atp.py +188 -0
- neurocore_skill_math/cas.py +144 -0
- neurocore_skill_math/check.py +26 -0
- neurocore_skill_math/formal.py +238 -0
- neurocore_skill_math/numeric.py +84 -0
- neurocore_skill_math/planning.py +84 -0
- neurocore_skill_math/prep.py +120 -0
- neurocore_skill_math/report.py +91 -0
- neurocore_skill_math/smt.py +145 -0
- neurocore_skill_math/symbolic.py +161 -0
- neurocore_skill_math-0.1.0.dist-info/METADATA +120 -0
- neurocore_skill_math-0.1.0.dist-info/RECORD +20 -0
- neurocore_skill_math-0.1.0.dist-info/WHEEL +4 -0
- neurocore_skill_math-0.1.0.dist-info/entry_points.txt +25 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""neurocore-skill-math — a math-proof toolchain for NeuroCore.
|
|
2
|
+
|
|
3
|
+
One package exposing many small composable skills (CAS, SMT, ATP, formal proof
|
|
4
|
+
assistants). Each skill detects whether its backend tool is installed and degrades
|
|
5
|
+
gracefully when it is not (status ``tool_unavailable``).
|
|
6
|
+
"""
|
|
7
|
+
from neurocore_skill_math.atp import (
|
|
8
|
+
EproverProveTptpSkill,
|
|
9
|
+
Mace4CountermodelSkill,
|
|
10
|
+
Prover9ProveSkill,
|
|
11
|
+
VampireProveTptpSkill,
|
|
12
|
+
)
|
|
13
|
+
from neurocore_skill_math.cas import (
|
|
14
|
+
GapGroupTheorySkill,
|
|
15
|
+
PariGpNumberTheorySkill,
|
|
16
|
+
SagemathComputeSkill,
|
|
17
|
+
)
|
|
18
|
+
from neurocore_skill_math.formal import (
|
|
19
|
+
CoqCheckSkill,
|
|
20
|
+
IsabelleCheckTheorySkill,
|
|
21
|
+
Lean4CheckSkill,
|
|
22
|
+
Lean4FormalizeStatementSkill,
|
|
23
|
+
Lean4RepairSkill,
|
|
24
|
+
)
|
|
25
|
+
from neurocore_skill_math.numeric import MpmathHighPrecisionCheckSkill
|
|
26
|
+
from neurocore_skill_math.planning import LlmProofPlannerSkill, TheoremRetrieverSkill
|
|
27
|
+
from neurocore_skill_math.prep import (
|
|
28
|
+
MathDomainClassifierSkill,
|
|
29
|
+
MathProblemParserSkill,
|
|
30
|
+
MathStatementNormalizerSkill,
|
|
31
|
+
)
|
|
32
|
+
from neurocore_skill_math.report import ProofReportBuilderSkill
|
|
33
|
+
from neurocore_skill_math.smt import Cvc5SmtCheckSkill, Z3SmtCheckSkill
|
|
34
|
+
from neurocore_skill_math.symbolic import (
|
|
35
|
+
SympyCalculusSkill,
|
|
36
|
+
SympySimplifySkill,
|
|
37
|
+
SympySolveSkill,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Group 1 — prep
|
|
42
|
+
"MathProblemParserSkill",
|
|
43
|
+
"MathDomainClassifierSkill",
|
|
44
|
+
"MathStatementNormalizerSkill",
|
|
45
|
+
# Group 2 — symbolic / numeric / CAS
|
|
46
|
+
"SympySimplifySkill",
|
|
47
|
+
"SympySolveSkill",
|
|
48
|
+
"SympyCalculusSkill",
|
|
49
|
+
"MpmathHighPrecisionCheckSkill",
|
|
50
|
+
"SagemathComputeSkill",
|
|
51
|
+
"PariGpNumberTheorySkill",
|
|
52
|
+
"GapGroupTheorySkill",
|
|
53
|
+
# Group 3 — counterexample / SMT
|
|
54
|
+
"Z3SmtCheckSkill",
|
|
55
|
+
"Cvc5SmtCheckSkill",
|
|
56
|
+
"Mace4CountermodelSkill",
|
|
57
|
+
# Group 4 — ATP
|
|
58
|
+
"VampireProveTptpSkill",
|
|
59
|
+
"EproverProveTptpSkill",
|
|
60
|
+
"Prover9ProveSkill",
|
|
61
|
+
# Group 4/5 — planning, retrieval
|
|
62
|
+
"LlmProofPlannerSkill",
|
|
63
|
+
"TheoremRetrieverSkill",
|
|
64
|
+
# Group 5 — formalization
|
|
65
|
+
"Lean4FormalizeStatementSkill",
|
|
66
|
+
"Lean4CheckSkill",
|
|
67
|
+
"Lean4RepairSkill",
|
|
68
|
+
"IsabelleCheckTheorySkill",
|
|
69
|
+
"CoqCheckSkill",
|
|
70
|
+
# Group 6 — reporting
|
|
71
|
+
"ProofReportBuilderSkill",
|
|
72
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Detect which math tools (CLIs and Python libs) are available at runtime.
|
|
2
|
+
|
|
3
|
+
Skills use this to degrade gracefully when a backend is not installed, and the
|
|
4
|
+
``python -m neurocore_skill_math.check`` command prints a full report.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import importlib.util
|
|
9
|
+
import shutil
|
|
10
|
+
|
|
11
|
+
# CLI executables expected on PATH (Ubuntu installer provides these).
|
|
12
|
+
CLI_TOOLS: list[str] = [
|
|
13
|
+
"gp", # PARI/GP
|
|
14
|
+
"gap", # GAP
|
|
15
|
+
"sage", # SageMath (or via docker)
|
|
16
|
+
"docker", # for the SageMath container fallback
|
|
17
|
+
"z3", # z3 CLI (the skill prefers the python binding)
|
|
18
|
+
"cvc5", # cvc5 CLI
|
|
19
|
+
"vampire",
|
|
20
|
+
"eprover",
|
|
21
|
+
"prover9",
|
|
22
|
+
"mace4",
|
|
23
|
+
"lean",
|
|
24
|
+
"lake",
|
|
25
|
+
"isabelle",
|
|
26
|
+
"coqc",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Python libraries used directly by skills.
|
|
30
|
+
PY_LIBS: list[str] = ["sympy", "mpmath", "z3", "cvc5", "numpy", "scipy", "networkx"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def tool_available(name: str) -> bool:
|
|
34
|
+
"""True if a CLI executable is on PATH."""
|
|
35
|
+
return shutil.which(name) is not None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def lib_available(module: str) -> bool:
|
|
39
|
+
"""True if a Python module can be imported."""
|
|
40
|
+
try:
|
|
41
|
+
return importlib.util.find_spec(module) is not None
|
|
42
|
+
except (ImportError, ValueError):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def availability_report() -> dict[str, dict[str, bool]]:
|
|
47
|
+
"""Return {'cli': {tool: bool}, 'lib': {module: bool}}."""
|
|
48
|
+
return {
|
|
49
|
+
"cli": {t: tool_available(t) for t in CLI_TOOLS},
|
|
50
|
+
"lib": {m: lib_available(m) for m in PY_LIBS},
|
|
51
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Shared base for all math skills.
|
|
2
|
+
|
|
3
|
+
``MathSkill`` standardizes:
|
|
4
|
+
- configurable IO keys (``input_key`` / ``output_key`` + extras), per the design doc;
|
|
5
|
+
- a uniform **result envelope** every skill writes to context;
|
|
6
|
+
- **availability detection** + graceful degradation (never crash the flow when a
|
|
7
|
+
backend tool/lib is missing);
|
|
8
|
+
- **port** helpers so skills can drive graph routing.
|
|
9
|
+
|
|
10
|
+
Concrete skills subclass this, set ``default_input_key`` / ``default_output_key`` /
|
|
11
|
+
``required_tool`` / ``required_lib`` / ``tool_name``, and implement ``_compute``.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from flowengine import FlowContext
|
|
19
|
+
from neurocore import AsyncSkill
|
|
20
|
+
|
|
21
|
+
from neurocore_skill_math._availability import lib_available, tool_available
|
|
22
|
+
|
|
23
|
+
# Envelope status values.
|
|
24
|
+
STATUS_OK = "ok"
|
|
25
|
+
STATUS_REFUTED = "refuted"
|
|
26
|
+
STATUS_PROVED = "proved"
|
|
27
|
+
STATUS_UNKNOWN = "unknown"
|
|
28
|
+
STATUS_UNAVAILABLE = "tool_unavailable"
|
|
29
|
+
STATUS_ERROR = "error"
|
|
30
|
+
STATUS_TIMEOUT = "timeout"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MathSkill(AsyncSkill):
|
|
34
|
+
"""Base class for math-toolchain skills (not registered itself)."""
|
|
35
|
+
|
|
36
|
+
# Subclasses override these.
|
|
37
|
+
default_input_key: str = "math.normalized"
|
|
38
|
+
default_output_key: str = "math.result"
|
|
39
|
+
required_tool: str | None = None # CLI executable to check on PATH
|
|
40
|
+
required_lib: str | None = None # importable module to check
|
|
41
|
+
tool_name: str = "math"
|
|
42
|
+
|
|
43
|
+
def init(self, config: dict[str, Any]) -> None:
|
|
44
|
+
super().init(config)
|
|
45
|
+
self.in_key: str = self.config.get("input_key", self.default_input_key)
|
|
46
|
+
self.out_key: str = self.config.get("output_key", self.default_output_key)
|
|
47
|
+
self.timeout: float = float(self.config.get("timeout_seconds", 30))
|
|
48
|
+
|
|
49
|
+
# -- availability ------------------------------------------------------
|
|
50
|
+
def is_available(self) -> bool:
|
|
51
|
+
tool_ok = not self.required_tool or tool_available(self.required_tool)
|
|
52
|
+
lib_ok = not self.required_lib or lib_available(self.required_lib)
|
|
53
|
+
return tool_ok and lib_ok
|
|
54
|
+
|
|
55
|
+
def health_check(self) -> bool:
|
|
56
|
+
return self.is_initialized and self.is_available()
|
|
57
|
+
|
|
58
|
+
# -- execution ---------------------------------------------------------
|
|
59
|
+
async def process(self, context: FlowContext) -> FlowContext: # type: ignore[override]
|
|
60
|
+
if not self.is_available():
|
|
61
|
+
missing = self.required_tool or self.required_lib or self.tool_name
|
|
62
|
+
self._write(
|
|
63
|
+
context,
|
|
64
|
+
self.envelope(
|
|
65
|
+
STATUS_UNAVAILABLE,
|
|
66
|
+
available=False,
|
|
67
|
+
error=f"{missing!r} is not installed; skill skipped.",
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
self.port(context, "tool_unavailable")
|
|
71
|
+
return context
|
|
72
|
+
|
|
73
|
+
payload = context.get(self.in_key)
|
|
74
|
+
start = time.perf_counter()
|
|
75
|
+
try:
|
|
76
|
+
env = await self._compute(payload, context)
|
|
77
|
+
except Exception as exc: # noqa: BLE001 — degrade, don't crash the flow
|
|
78
|
+
env = self.envelope(STATUS_ERROR, error=str(exc))
|
|
79
|
+
env.setdefault("duration_ms", round((time.perf_counter() - start) * 1000, 2))
|
|
80
|
+
self._write(context, env)
|
|
81
|
+
return context
|
|
82
|
+
|
|
83
|
+
async def _compute(self, payload: Any, context: FlowContext) -> dict[str, Any]:
|
|
84
|
+
"""Run the skill's work and return a result envelope. May also set ports."""
|
|
85
|
+
raise NotImplementedError
|
|
86
|
+
|
|
87
|
+
# -- helpers -----------------------------------------------------------
|
|
88
|
+
def envelope(
|
|
89
|
+
self,
|
|
90
|
+
status: str,
|
|
91
|
+
*,
|
|
92
|
+
result: Any = None,
|
|
93
|
+
log: str = "",
|
|
94
|
+
error: str | None = None,
|
|
95
|
+
available: bool = True,
|
|
96
|
+
**extra: Any,
|
|
97
|
+
) -> dict[str, Any]:
|
|
98
|
+
return {
|
|
99
|
+
"status": status,
|
|
100
|
+
"tool": self.tool_name,
|
|
101
|
+
"available": available,
|
|
102
|
+
"result": result,
|
|
103
|
+
"log": log,
|
|
104
|
+
"error": error,
|
|
105
|
+
**extra,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
def _write(self, context: FlowContext, env: dict[str, Any]) -> None:
|
|
109
|
+
context.set(self.out_key, env)
|
|
110
|
+
|
|
111
|
+
def port(self, context: FlowContext, name: str) -> None:
|
|
112
|
+
self.set_output_port(context, name)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Assemble solver inputs (SMT-LIB, TPTP) from skill payloads.
|
|
2
|
+
|
|
3
|
+
A payload may be a ready-made source string, or a structured dict carrying the
|
|
4
|
+
source under a known key (``smtlib`` / ``tptp`` / ``prover9``). Skills accept both
|
|
5
|
+
so an upstream LLM/normalizer can emit either form.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _coerce(payload: Any, *keys: str) -> str | None:
|
|
13
|
+
"""Pull a source string out of a payload (raw string or dict with one of keys)."""
|
|
14
|
+
if isinstance(payload, str):
|
|
15
|
+
return payload
|
|
16
|
+
if hasattr(payload, "get"): # dict / DotDict
|
|
17
|
+
for k in keys:
|
|
18
|
+
v = payload.get(k)
|
|
19
|
+
if isinstance(v, str) and v.strip():
|
|
20
|
+
return v
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def as_smtlib(payload: Any) -> str | None:
|
|
25
|
+
"""Return an SMT-LIB v2 script from the payload, or None if not provided.
|
|
26
|
+
|
|
27
|
+
Ensures a ``(check-sat)`` is present so the solver actually runs.
|
|
28
|
+
"""
|
|
29
|
+
src = _coerce(payload, "smtlib", "smt", "source")
|
|
30
|
+
if src is None:
|
|
31
|
+
return None
|
|
32
|
+
if "(check-sat)" not in src:
|
|
33
|
+
src = src.rstrip() + "\n(check-sat)\n"
|
|
34
|
+
return src
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def as_tptp(payload: Any) -> str | None:
|
|
38
|
+
"""Return a TPTP problem string from the payload, or None."""
|
|
39
|
+
return _coerce(payload, "tptp", "fof", "source")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def as_prover9(payload: Any) -> str | None:
|
|
43
|
+
"""Return a Prover9/Mace4 input deck from the payload, or None."""
|
|
44
|
+
return _coerce(payload, "prover9", "mace4", "source")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_smt_result(stdout: str) -> str:
|
|
48
|
+
"""Map raw solver stdout to 'sat' | 'unsat' | 'unknown'."""
|
|
49
|
+
text = stdout.strip().lower()
|
|
50
|
+
if "unsat" in text:
|
|
51
|
+
return "unsat"
|
|
52
|
+
if "sat" in text:
|
|
53
|
+
return "sat"
|
|
54
|
+
return "unknown"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Base for LLM-backed math skills (problem prep, planning, formalization)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from neurocore_skill_math._base import MathSkill
|
|
9
|
+
|
|
10
|
+
_JSON_RE = re.compile(r"\{.*\}", re.DOTALL)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def extract_json(text: str) -> dict[str, Any]:
|
|
14
|
+
"""Best-effort: parse the first JSON object in ``text``; else wrap as {'raw': ...}."""
|
|
15
|
+
match = _JSON_RE.search(text or "")
|
|
16
|
+
if match:
|
|
17
|
+
try:
|
|
18
|
+
obj = json.loads(match.group(0))
|
|
19
|
+
if isinstance(obj, dict):
|
|
20
|
+
return obj
|
|
21
|
+
except json.JSONDecodeError:
|
|
22
|
+
pass
|
|
23
|
+
return {"raw": (text or "").strip()}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LlmMathSkill(MathSkill):
|
|
27
|
+
"""MathSkill whose backend is the injected LLM provider (`requires_llm=True`)."""
|
|
28
|
+
|
|
29
|
+
tool_name = "llm"
|
|
30
|
+
|
|
31
|
+
def is_available(self) -> bool:
|
|
32
|
+
return getattr(self, "llm", None) is not None
|
|
33
|
+
|
|
34
|
+
async def _ask(self, user: str, system: str | None = None) -> str:
|
|
35
|
+
from neurocore.llm.provider import LLMMessage
|
|
36
|
+
|
|
37
|
+
response = await self.llm.complete(
|
|
38
|
+
[LLMMessage(role="user", content=user)], system=system
|
|
39
|
+
)
|
|
40
|
+
return response.content
|
|
41
|
+
|
|
42
|
+
async def _ask_json(self, user: str, system: str | None = None) -> dict[str, Any]:
|
|
43
|
+
return extract_json(await self._ask(user, system))
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def _text(payload: Any, *keys: str) -> str:
|
|
47
|
+
if isinstance(payload, str):
|
|
48
|
+
return payload
|
|
49
|
+
if hasattr(payload, "get"):
|
|
50
|
+
for k in (*keys, "problem", "statement", "normalized", "raw"):
|
|
51
|
+
v = payload.get(k)
|
|
52
|
+
if isinstance(v, str) and v.strip():
|
|
53
|
+
return v
|
|
54
|
+
return str(payload or "")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Subprocess helpers with timeouts for external math tools.
|
|
2
|
+
|
|
3
|
+
Kept small and dependency-free. ``run_cli`` is the seam tests monkeypatch so the
|
|
4
|
+
suite needs neither the real provers nor a network.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CliResult:
|
|
14
|
+
"""Outcome of a CLI invocation."""
|
|
15
|
+
|
|
16
|
+
returncode: int
|
|
17
|
+
stdout: str
|
|
18
|
+
stderr: str
|
|
19
|
+
timed_out: bool = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_cli(
|
|
23
|
+
cmd: list[str],
|
|
24
|
+
*,
|
|
25
|
+
stdin: str | None = None,
|
|
26
|
+
timeout: float = 30.0,
|
|
27
|
+
env: dict[str, str] | None = None,
|
|
28
|
+
cwd: str | None = None,
|
|
29
|
+
) -> CliResult:
|
|
30
|
+
"""Run a command, capturing stdout/stderr, returning a CliResult.
|
|
31
|
+
|
|
32
|
+
A timeout yields ``timed_out=True`` (never raises); other OS errors surface
|
|
33
|
+
as a non-zero return code with the message on stderr.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
proc = subprocess.run(
|
|
37
|
+
cmd,
|
|
38
|
+
input=stdin,
|
|
39
|
+
capture_output=True,
|
|
40
|
+
text=True,
|
|
41
|
+
timeout=timeout,
|
|
42
|
+
env=env,
|
|
43
|
+
cwd=cwd,
|
|
44
|
+
check=False,
|
|
45
|
+
)
|
|
46
|
+
return CliResult(proc.returncode, proc.stdout, proc.stderr)
|
|
47
|
+
except subprocess.TimeoutExpired as exc:
|
|
48
|
+
out = exc.stdout or ""
|
|
49
|
+
err = exc.stderr or ""
|
|
50
|
+
out = out.decode() if isinstance(out, bytes) else out
|
|
51
|
+
err = err.decode() if isinstance(err, bytes) else err
|
|
52
|
+
return CliResult(-1, out, err, timed_out=True)
|
|
53
|
+
except OSError as exc:
|
|
54
|
+
return CliResult(-1, "", str(exc))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def docker_cmd(
|
|
58
|
+
image: str,
|
|
59
|
+
args: list[str],
|
|
60
|
+
*,
|
|
61
|
+
network: bool = False,
|
|
62
|
+
memory: str | None = "4g",
|
|
63
|
+
cpus: str | None = "2",
|
|
64
|
+
mounts: list[tuple[str, str]] | None = None,
|
|
65
|
+
read_only: bool = True,
|
|
66
|
+
) -> list[str]:
|
|
67
|
+
"""Build a sandboxed ``docker run`` command (per design doc §C).
|
|
68
|
+
|
|
69
|
+
Defaults: no network, capped memory/cpus, read-only mounts.
|
|
70
|
+
"""
|
|
71
|
+
cmd = ["docker", "run", "--rm"]
|
|
72
|
+
if not network:
|
|
73
|
+
cmd += ["--network", "none"]
|
|
74
|
+
if memory:
|
|
75
|
+
cmd += ["--memory", memory]
|
|
76
|
+
if cpus:
|
|
77
|
+
cmd += ["--cpus", cpus]
|
|
78
|
+
for src, dst in mounts or []:
|
|
79
|
+
cmd += ["-v", f"{src}:{dst}{':ro' if read_only else ''}"]
|
|
80
|
+
cmd += [image, *args]
|
|
81
|
+
return cmd
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Group 3/4 — automated theorem provers and finite-model search.
|
|
2
|
+
|
|
3
|
+
- ``vampire_prove_tptp`` / ``eprover_prove_tptp`` — first-order ATP on TPTP input.
|
|
4
|
+
- ``prover9_prove`` — Prover9 (its own syntax).
|
|
5
|
+
- ``mace4_countermodel`` — Mace4 finite-model search (counterexamples).
|
|
6
|
+
|
|
7
|
+
Provers set port ``proof_found`` / ``no_proof``; Mace4 sets ``counterexample_found`` /
|
|
8
|
+
``no_counterexample``.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from flowengine import FlowContext
|
|
17
|
+
from neurocore import SkillMeta
|
|
18
|
+
|
|
19
|
+
from neurocore_skill_math._base import (
|
|
20
|
+
STATUS_ERROR,
|
|
21
|
+
STATUS_OK,
|
|
22
|
+
STATUS_PROVED,
|
|
23
|
+
STATUS_REFUTED,
|
|
24
|
+
STATUS_TIMEOUT,
|
|
25
|
+
STATUS_UNKNOWN,
|
|
26
|
+
MathSkill,
|
|
27
|
+
)
|
|
28
|
+
from neurocore_skill_math._formats import as_prover9, as_tptp
|
|
29
|
+
from neurocore_skill_math._run import run_cli
|
|
30
|
+
|
|
31
|
+
_TPTP_SCHEMA = {"properties": {
|
|
32
|
+
"input_key": {"type": "string"}, "output_key": {"type": "string"},
|
|
33
|
+
"premises_key": {"type": "string"}, "timeout_seconds": {"type": "integer"},
|
|
34
|
+
}}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _szs_verdict(text: str) -> str:
|
|
38
|
+
"""Map prover stdout (SZS ontology / common phrases) to proved/disproved/unknown."""
|
|
39
|
+
t = text.lower()
|
|
40
|
+
if ("refutation found" in t or "szs status theorem" in t
|
|
41
|
+
or "szs status unsatisfiable" in t or "proof found" in t
|
|
42
|
+
or "theorem proved" in t):
|
|
43
|
+
return "proved"
|
|
44
|
+
if ("countersatisfiable" in t or "szs status satisfiable" in t
|
|
45
|
+
or "search failed" in t):
|
|
46
|
+
return "disproved"
|
|
47
|
+
return "unknown"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _TptpProver(MathSkill):
|
|
51
|
+
default_input_key = "math.normalized"
|
|
52
|
+
cli: str = "" # subclass sets the executable
|
|
53
|
+
|
|
54
|
+
def _write_tmp(self, content: str, suffix: str) -> str:
|
|
55
|
+
fd, path = tempfile.mkstemp(prefix="nc-atp-", suffix=suffix)
|
|
56
|
+
with os.fdopen(fd, "w") as fh:
|
|
57
|
+
fh.write(content)
|
|
58
|
+
return path
|
|
59
|
+
|
|
60
|
+
def _route_proof(self, context: FlowContext, verdict: str, log: str) -> dict[str, Any]:
|
|
61
|
+
if verdict == "proved":
|
|
62
|
+
self.port(context, "proof_found")
|
|
63
|
+
return self.envelope(STATUS_PROVED, result={"proof_found": True}, log=log[:4000])
|
|
64
|
+
self.port(context, "no_proof")
|
|
65
|
+
status = STATUS_OK if verdict == "disproved" else STATUS_UNKNOWN
|
|
66
|
+
return self.envelope(status, result={"proof_found": False, "verdict": verdict},
|
|
67
|
+
log=log[:4000])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class VampireProveTptpSkill(_TptpProver):
|
|
71
|
+
default_output_key = "proof.vampire"
|
|
72
|
+
required_tool = "vampire"
|
|
73
|
+
cli = "vampire"
|
|
74
|
+
tool_name = "vampire"
|
|
75
|
+
|
|
76
|
+
skill_meta = SkillMeta(
|
|
77
|
+
name="vampire_prove_tptp",
|
|
78
|
+
version="0.1.0",
|
|
79
|
+
description="Prove a TPTP first-order conjecture with Vampire.",
|
|
80
|
+
author="NeuroCore Contributors",
|
|
81
|
+
provides=["proof.vampire"], consumes=["math.normalized", "proof.premises"],
|
|
82
|
+
tags=["math", "atp", "tptp", "vampire"], config_schema=_TPTP_SCHEMA,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
async def _compute(self, payload: Any, context: FlowContext) -> dict[str, Any]:
|
|
86
|
+
tptp = as_tptp(payload)
|
|
87
|
+
if not tptp:
|
|
88
|
+
return self.envelope(STATUS_ERROR, error="no TPTP input provided")
|
|
89
|
+
path = self._write_tmp(tptp, ".p")
|
|
90
|
+
res = run_cli(["vampire", "-t", str(int(self.timeout)), path], timeout=self.timeout + 5)
|
|
91
|
+
if res.timed_out:
|
|
92
|
+
self.port(context, "no_proof")
|
|
93
|
+
return self.envelope(STATUS_TIMEOUT, error="vampire timed out")
|
|
94
|
+
return self._route_proof(context, _szs_verdict(res.stdout), res.stdout)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class EproverProveTptpSkill(_TptpProver):
|
|
98
|
+
default_output_key = "proof.eprover"
|
|
99
|
+
required_tool = "eprover"
|
|
100
|
+
cli = "eprover"
|
|
101
|
+
tool_name = "eprover"
|
|
102
|
+
|
|
103
|
+
skill_meta = SkillMeta(
|
|
104
|
+
name="eprover_prove_tptp",
|
|
105
|
+
version="0.1.0",
|
|
106
|
+
description="Prove a TPTP first-order conjecture with the E prover.",
|
|
107
|
+
author="NeuroCore Contributors",
|
|
108
|
+
provides=["proof.eprover"], consumes=["math.normalized", "proof.premises"],
|
|
109
|
+
tags=["math", "atp", "tptp", "eprover"], config_schema=_TPTP_SCHEMA,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def _compute(self, payload: Any, context: FlowContext) -> dict[str, Any]:
|
|
113
|
+
tptp = as_tptp(payload)
|
|
114
|
+
if not tptp:
|
|
115
|
+
return self.envelope(STATUS_ERROR, error="no TPTP input provided")
|
|
116
|
+
path = self._write_tmp(tptp, ".p")
|
|
117
|
+
res = run_cli(
|
|
118
|
+
["eprover", "--auto", "--tptp3-format", f"--cpu-limit={int(self.timeout)}", path],
|
|
119
|
+
timeout=self.timeout + 5,
|
|
120
|
+
)
|
|
121
|
+
if res.timed_out:
|
|
122
|
+
self.port(context, "no_proof")
|
|
123
|
+
return self.envelope(STATUS_TIMEOUT, error="eprover timed out")
|
|
124
|
+
return self._route_proof(context, _szs_verdict(res.stdout), res.stdout)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Prover9ProveSkill(_TptpProver):
|
|
128
|
+
default_output_key = "proof.prover9"
|
|
129
|
+
required_tool = "prover9"
|
|
130
|
+
cli = "prover9"
|
|
131
|
+
tool_name = "prover9"
|
|
132
|
+
|
|
133
|
+
skill_meta = SkillMeta(
|
|
134
|
+
name="prover9_prove",
|
|
135
|
+
version="0.1.0",
|
|
136
|
+
description="Prove a conjecture with Prover9 (Prover9 input syntax).",
|
|
137
|
+
author="NeuroCore Contributors",
|
|
138
|
+
provides=["proof.prover9"], consumes=["math.normalized", "proof.premises"],
|
|
139
|
+
tags=["math", "atp", "prover9"], config_schema=_TPTP_SCHEMA,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
async def _compute(self, payload: Any, context: FlowContext) -> dict[str, Any]:
|
|
143
|
+
deck = as_prover9(payload)
|
|
144
|
+
if not deck:
|
|
145
|
+
return self.envelope(STATUS_ERROR, error="no Prover9 input provided")
|
|
146
|
+
res = run_cli(["prover9"], stdin=deck, timeout=self.timeout)
|
|
147
|
+
if res.timed_out:
|
|
148
|
+
self.port(context, "no_proof")
|
|
149
|
+
return self.envelope(STATUS_TIMEOUT, error="prover9 timed out")
|
|
150
|
+
return self._route_proof(context, _szs_verdict(res.stdout), res.stdout)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Mace4CountermodelSkill(MathSkill):
|
|
154
|
+
default_input_key = "math.normalized"
|
|
155
|
+
default_output_key = "counterexamples.mace4"
|
|
156
|
+
required_tool = "mace4"
|
|
157
|
+
tool_name = "mace4"
|
|
158
|
+
|
|
159
|
+
skill_meta = SkillMeta(
|
|
160
|
+
name="mace4_countermodel",
|
|
161
|
+
version="0.1.0",
|
|
162
|
+
description="Search for a finite countermodel with Mace4.",
|
|
163
|
+
author="NeuroCore Contributors",
|
|
164
|
+
provides=["counterexamples.mace4"], consumes=["math.normalized"],
|
|
165
|
+
tags=["math", "counterexample", "finite-model", "mace4"],
|
|
166
|
+
config_schema={"properties": {
|
|
167
|
+
"input_key": {"type": "string"}, "output_key": {"type": "string"},
|
|
168
|
+
"timeout_seconds": {"type": "integer"}, "max_domain_size": {"type": "integer"},
|
|
169
|
+
}},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
async def _compute(self, payload: Any, context: FlowContext) -> dict[str, Any]:
|
|
173
|
+
deck = as_prover9(payload)
|
|
174
|
+
if not deck:
|
|
175
|
+
return self.envelope(STATUS_ERROR, error="no Mace4 input provided")
|
|
176
|
+
n = int(self.config.get("max_domain_size", 8))
|
|
177
|
+
res = run_cli(["mace4", "-n", str(n)], stdin=deck, timeout=self.timeout)
|
|
178
|
+
if res.timed_out:
|
|
179
|
+
self.port(context, "no_counterexample")
|
|
180
|
+
return self.envelope(STATUS_TIMEOUT, error="mace4 timed out")
|
|
181
|
+
found = "model" in res.stdout.lower() and "exiting with" in res.stdout.lower()
|
|
182
|
+
if found:
|
|
183
|
+
self.port(context, "counterexample_found")
|
|
184
|
+
return self.envelope(STATUS_REFUTED, result={"countermodel": res.stdout.strip()},
|
|
185
|
+
counterexample_found=True)
|
|
186
|
+
self.port(context, "no_counterexample")
|
|
187
|
+
return self.envelope(STATUS_OK, result={"countermodel": None},
|
|
188
|
+
counterexample_found=False)
|