unicode-logic-kit 0.31.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.
- unicode_logic_kit/__init__.py +385 -0
- unicode_logic_kit/__main__.py +520 -0
- unicode_logic_kit/_deadline.py +219 -0
- unicode_logic_kit/ace/__init__.py +126 -0
- unicode_logic_kit/ace/_align.py +135 -0
- unicode_logic_kit/ace/chem_lexicon.py +128 -0
- unicode_logic_kit/ace/drs_reader.py +570 -0
- unicode_logic_kit/ace/mapping.py +666 -0
- unicode_logic_kit/ace/reverse_modal.py +138 -0
- unicode_logic_kit/ace/runner.py +551 -0
- unicode_logic_kit/ace/translate.py +452 -0
- unicode_logic_kit/ace/verbalize.py +1070 -0
- unicode_logic_kit/api.py +1284 -0
- unicode_logic_kit/atp/__init__.py +177 -0
- unicode_logic_kit/atp/_ascii_names.py +113 -0
- unicode_logic_kit/atp/_html.py +72 -0
- unicode_logic_kit/atp/_substructural_input.py +228 -0
- unicode_logic_kit/atp/_tff_problem.py +715 -0
- unicode_logic_kit/atp/_tptp_problem.py +1111 -0
- unicode_logic_kit/atp/_writer_support.py +289 -0
- unicode_logic_kit/atp/clingo_backend.py +1180 -0
- unicode_logic_kit/atp/cvc5_backend.py +1385 -0
- unicode_logic_kit/atp/eprover_backend.py +732 -0
- unicode_logic_kit/atp/finite_domain.py +1055 -0
- unicode_logic_kit/atp/fitch.py +1547 -0
- unicode_logic_kit/atp/fitch_search.py +551 -0
- unicode_logic_kit/atp/hets_backend.py +339 -0
- unicode_logic_kit/atp/hybrid_down.py +120 -0
- unicode_logic_kit/atp/incremental.py +250 -0
- unicode_logic_kit/atp/kripke_enum.py +741 -0
- unicode_logic_kit/atp/lambek.py +436 -0
- unicode_logic_kit/atp/leo3_backend.py +332 -0
- unicode_logic_kit/atp/linear.py +738 -0
- unicode_logic_kit/atp/lj.py +705 -0
- unicode_logic_kit/atp/logic_backends.py +566 -0
- unicode_logic_kit/atp/ltl_tableau.py +1084 -0
- unicode_logic_kit/atp/minizinc_backend.py +1402 -0
- unicode_logic_kit/atp/modal_tableau.py +1382 -0
- unicode_logic_kit/atp/nanocop_backend.py +410 -0
- unicode_logic_kit/atp/portfolio.py +489 -0
- unicode_logic_kit/atp/protocol.py +1803 -0
- unicode_logic_kit/atp/prover9_entailment.py +1153 -0
- unicode_logic_kit/atp/resolution.py +1376 -0
- unicode_logic_kit/atp/resolution_check.py +1114 -0
- unicode_logic_kit/atp/sequent.py +1050 -0
- unicode_logic_kit/atp/tableau.py +921 -0
- unicode_logic_kit/atp/tableau_check.py +543 -0
- unicode_logic_kit/atp/tptp_ncl.py +811 -0
- unicode_logic_kit/atp/tptp_tff.py +1546 -0
- unicode_logic_kit/atp/tstp.py +1333 -0
- unicode_logic_kit/atp/tstp_check.py +1096 -0
- unicode_logic_kit/atp/twee_backend.py +236 -0
- unicode_logic_kit/atp/twee_check.py +711 -0
- unicode_logic_kit/atp/twee_entailment.py +953 -0
- unicode_logic_kit/atp/vampire_entailment.py +540 -0
- unicode_logic_kit/atp/z3_arith.py +470 -0
- unicode_logic_kit/atp/z3_equivalence.py +36 -0
- unicode_logic_kit/atp/z3_fuzzy.py +362 -0
- unicode_logic_kit/atp/z3_input.py +500 -0
- unicode_logic_kit/atp/z3_models.py +208 -0
- unicode_logic_kit/chem/__init__.py +88 -0
- unicode_logic_kit/chem/_naming.py +284 -0
- unicode_logic_kit/chem/cache.py +185 -0
- unicode_logic_kit/chem/interop.py +244 -0
- unicode_logic_kit/chem/mol.py +525 -0
- unicode_logic_kit/chem/signature.py +112 -0
- unicode_logic_kit/comorphism.py +497 -0
- unicode_logic_kit/dl/__init__.py +384 -0
- unicode_logic_kit/dl/classification.py +227 -0
- unicode_logic_kit/dl/concepts.py +632 -0
- unicode_logic_kit/dl/datatypes.py +818 -0
- unicode_logic_kit/dl/owl_functional.py +2433 -0
- unicode_logic_kit/dl/owl_manchester.py +1637 -0
- unicode_logic_kit/dl/owl_reasoner.py +790 -0
- unicode_logic_kit/dl/parser.py +391 -0
- unicode_logic_kit/dl/tableau.py +4048 -0
- unicode_logic_kit/dl/translate.py +2704 -0
- unicode_logic_kit/drt/__init__.py +94 -0
- unicode_logic_kit/drt/export.py +179 -0
- unicode_logic_kit/drt/nodes.py +506 -0
- unicode_logic_kit/drt/parser.py +965 -0
- unicode_logic_kit/drt/resolve.py +195 -0
- unicode_logic_kit/drt/reverse.py +175 -0
- unicode_logic_kit/eval/__init__.py +106 -0
- unicode_logic_kit/eval/batch.py +382 -0
- unicode_logic_kit/eval/canonical.py +663 -0
- unicode_logic_kit/eval/chem_batch.py +606 -0
- unicode_logic_kit/eval/converses.py +200 -0
- unicode_logic_kit/eval/datasets/__init__.py +136 -0
- unicode_logic_kit/eval/datasets/_base.py +263 -0
- unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
- unicode_logic_kit/eval/datasets/c3po.py +678 -0
- unicode_logic_kit/eval/datasets/folio.py +158 -0
- unicode_logic_kit/eval/datasets/fracas.py +418 -0
- unicode_logic_kit/eval/datasets/groves.py +191 -0
- unicode_logic_kit/eval/datasets/logicbench.py +467 -0
- unicode_logic_kit/eval/datasets/logicnli.py +303 -0
- unicode_logic_kit/eval/datasets/malls.py +133 -0
- unicode_logic_kit/eval/datasets/pfolio.py +594 -0
- unicode_logic_kit/eval/datasets/pmb.py +242 -0
- unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
- unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
- unicode_logic_kit/eval/datasets/proverqa.py +674 -0
- unicode_logic_kit/eval/datasets/willow.py +478 -0
- unicode_logic_kit/eval/equivalence.py +466 -0
- unicode_logic_kit/eval/exercise_gen.py +533 -0
- unicode_logic_kit/eval/explain.py +791 -0
- unicode_logic_kit/eval/generality.py +750 -0
- unicode_logic_kit/eval/metric_hf.py +458 -0
- unicode_logic_kit/eval/predicate_match.py +343 -0
- unicode_logic_kit/eval/theory_check.py +1170 -0
- unicode_logic_kit/eval/validate.py +306 -0
- unicode_logic_kit/fol/__init__.py +177 -0
- unicode_logic_kit/fol/_atom_keys.py +510 -0
- unicode_logic_kit/fol/_fol_nodes.py +3586 -0
- unicode_logic_kit/fol/_free_parameters.py +105 -0
- unicode_logic_kit/fol/_ho_nodes.py +448 -0
- unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
- unicode_logic_kit/fol/_identifiers.py +1091 -0
- unicode_logic_kit/fol/_lambek_nodes.py +112 -0
- unicode_logic_kit/fol/_linear_nodes.py +352 -0
- unicode_logic_kit/fol/_modal_nodes.py +1467 -0
- unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
- unicode_logic_kit/fol/_numeral_symbols.py +231 -0
- unicode_logic_kit/fol/_so_nodes.py +200 -0
- unicode_logic_kit/fol/_symbol_names.py +81 -0
- unicode_logic_kit/fol/_team_nodes.py +181 -0
- unicode_logic_kit/fol/_tptp_symbols.py +551 -0
- unicode_logic_kit/fol/_truth_constants.py +117 -0
- unicode_logic_kit/fol/casl_export.py +1135 -0
- unicode_logic_kit/fol/casl_import.py +929 -0
- unicode_logic_kit/fol/derivation.py +367 -0
- unicode_logic_kit/fol/dialect_detect.py +70 -0
- unicode_logic_kit/fol/dialect_repair.py +537 -0
- unicode_logic_kit/fol/frames.py +637 -0
- unicode_logic_kit/fol/grammars/terminals.lark +31 -0
- unicode_logic_kit/fol/lambda_tools.py +297 -0
- unicode_logic_kit/fol/latex_input.py +429 -0
- unicode_logic_kit/fol/modal_translation.py +944 -0
- unicode_logic_kit/fol/msflparser.py +1033 -0
- unicode_logic_kit/fol/naming.py +422 -0
- unicode_logic_kit/fol/nodes.py +241 -0
- unicode_logic_kit/fol/normalforms.py +492 -0
- unicode_logic_kit/fol/pal.py +287 -0
- unicode_logic_kit/fol/prolog_export.py +566 -0
- unicode_logic_kit/fol/prolog_input.py +505 -0
- unicode_logic_kit/fol/prover9_input.py +1325 -0
- unicode_logic_kit/fol/qml.py +1760 -0
- unicode_logic_kit/fol/qmltp_input.py +525 -0
- unicode_logic_kit/fol/sanitize.py +221 -0
- unicode_logic_kit/fol/serialize.py +79 -0
- unicode_logic_kit/fol/signature.py +1290 -0
- unicode_logic_kit/fol/simplify_check.py +544 -0
- unicode_logic_kit/fol/spans.py +594 -0
- unicode_logic_kit/fol/tptp_input.py +1503 -0
- unicode_logic_kit/fol/tptp_repair.py +941 -0
- unicode_logic_kit/fol/unification.py +157 -0
- unicode_logic_kit/fol/verbalize.py +263 -0
- unicode_logic_kit/hets/__init__.py +163 -0
- unicode_logic_kit/hets/bridge.py +142 -0
- unicode_logic_kit/hets/client.py +748 -0
- unicode_logic_kit/hets/docker.py +420 -0
- unicode_logic_kit/hets/dol.py +712 -0
- unicode_logic_kit/hets/haskell_json.py +355 -0
- unicode_logic_kit/hets/owl_backend.py +794 -0
- unicode_logic_kit/hets/owl_cli.py +598 -0
- unicode_logic_kit/hets/symbols.py +512 -0
- unicode_logic_kit/hol/__init__.py +140 -0
- unicode_logic_kit/hol/_ho_common.py +323 -0
- unicode_logic_kit/hol/_isabelle_binders.py +125 -0
- unicode_logic_kit/hol/classical.py +812 -0
- unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
- unicode_logic_kit/hol/deepshallow/_common.py +177 -0
- unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
- unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
- unicode_logic_kit/hol/deepshallow/modal.py +217 -0
- unicode_logic_kit/hol/deepshallow/qml.py +406 -0
- unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
- unicode_logic_kit/hol/free.py +753 -0
- unicode_logic_kit/hol/goedel.py +336 -0
- unicode_logic_kit/hol/ho_modal.py +1743 -0
- unicode_logic_kit/hol/intuitionistic.py +403 -0
- unicode_logic_kit/hol/isabelle_conditional.py +593 -0
- unicode_logic_kit/hol/isabelle_modal.py +1908 -0
- unicode_logic_kit/hol/isabelle_relevant.py +412 -0
- unicode_logic_kit/hol/isabelle_runner.py +1147 -0
- unicode_logic_kit/hol/isabelle_substructural.py +884 -0
- unicode_logic_kit/hol/lean.py +1018 -0
- unicode_logic_kit/hol/manyvalued.py +921 -0
- unicode_logic_kit/hol/secondorder.py +687 -0
- unicode_logic_kit/hol/thf_modal.py +941 -0
- unicode_logic_kit/hol/thirdorder.py +397 -0
- unicode_logic_kit/ilp/__init__.py +89 -0
- unicode_logic_kit/ilp/readback.py +389 -0
- unicode_logic_kit/ilp/separation.py +153 -0
- unicode_logic_kit/ilp/task.py +730 -0
- unicode_logic_kit/logic.py +163 -0
- unicode_logic_kit/mcp/__init__.py +28 -0
- unicode_logic_kit/mcp/__main__.py +5 -0
- unicode_logic_kit/mcp/chem_tools.py +1031 -0
- unicode_logic_kit/mcp/server.py +2453 -0
- unicode_logic_kit/mcp/syntax_spec.py +681 -0
- unicode_logic_kit/prob/__init__.py +53 -0
- unicode_logic_kit/prob/_bdd.py +225 -0
- unicode_logic_kit/prob/_column_gen.py +668 -0
- unicode_logic_kit/prob/distribution.py +686 -0
- unicode_logic_kit/prob/nilsson.py +470 -0
- unicode_logic_kit/py.typed +0 -0
- unicode_logic_kit/semantics/__init__.py +137 -0
- unicode_logic_kit/semantics/_modal_reject.py +156 -0
- unicode_logic_kit/semantics/action_models.py +466 -0
- unicode_logic_kit/semantics/asp_models.py +1200 -0
- unicode_logic_kit/semantics/conditional.py +580 -0
- unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
- unicode_logic_kit/semantics/free_logic.py +913 -0
- unicode_logic_kit/semantics/fuzzy.py +384 -0
- unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
- unicode_logic_kit/semantics/intuitionistic.py +581 -0
- unicode_logic_kit/semantics/kripke.py +1139 -0
- unicode_logic_kit/semantics/manyvalued.py +580 -0
- unicode_logic_kit/semantics/matrix.py +342 -0
- unicode_logic_kit/semantics/model_eval.py +1135 -0
- unicode_logic_kit/semantics/modelfinder.py +1036 -0
- unicode_logic_kit/semantics/nonmonotonic.py +372 -0
- unicode_logic_kit/semantics/relevant.py +331 -0
- unicode_logic_kit/semantics/secondorder.py +657 -0
- unicode_logic_kit/semantics/structures.py +352 -0
- unicode_logic_kit/semantics/tarski.py +975 -0
- unicode_logic_kit/semantics/team.py +315 -0
- unicode_logic_kit/semantics/team_translation.py +416 -0
- unicode_logic_kit/semantics/thirdorder.py +358 -0
- unicode_logic_kit/semantics/tnorm.py +85 -0
- unicode_logic_kit/semantics/truthtable.py +201 -0
- unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
- unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
- unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
- unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,2453 @@
|
|
|
1
|
+
"""The MCP tool layer over :mod:`unicode_logic_kit.api`.
|
|
2
|
+
|
|
3
|
+
Design contract (see the package docstring for the why):
|
|
4
|
+
|
|
5
|
+
* every tool accepts formula TEXT and parses it with ``api.parse_any``
|
|
6
|
+
(dialect auto-detection; an optional ``dialect`` hint) — a parse failure
|
|
7
|
+
ALWAYS comes back as ``{"ok": False, "argument": <which input failed —
|
|
8
|
+
"text", "conclusion", "formula1", "premise[2]", …>, "errors": [...]}``
|
|
9
|
+
with every attempted dialect's diagnostics, exactly what a repair loop
|
|
10
|
+
needs, never a bare exception string. One uniform shape across every
|
|
11
|
+
tool and every argument position (review-hardened: a generic client
|
|
12
|
+
checks ``result.get("ok") is False``, full stop);
|
|
13
|
+
* results are the ``to_dict()`` payloads of the underlying API objects,
|
|
14
|
+
untouched — the MCP layer adds no vocabulary of its own beyond TEXT
|
|
15
|
+
renderings of what is already there (``unicode``, ``axioms_unicode``,
|
|
16
|
+
``box``), because every tool takes text and a result is only usable as the
|
|
17
|
+
next call's input if it comes back as text;
|
|
18
|
+
* exceptions that ARE the API's documented contract surface as structured
|
|
19
|
+
``{"error": {"type": ..., "message": ...}}`` dicts (``BackendUnavailable``
|
|
20
|
+
carries its actionable install/start instructions verbatim), so an agent
|
|
21
|
+
can react without parsing tracebacks.
|
|
22
|
+
|
|
23
|
+
The functions below are plain synchronous callables registered on an
|
|
24
|
+
:class:`mcp.server.MCPServer`; they are importable and testable without any
|
|
25
|
+
transport running.
|
|
26
|
+
|
|
27
|
+
STABILITY POLICY (the registered tool SURFACE — names and input schemas):
|
|
28
|
+
within a minor release line (0.N.x) a registered tool is never renamed or
|
|
29
|
+
removed, and its ``input_schema`` (auto-derived by the ``mcp`` SDK from the
|
|
30
|
+
function signature: property names, types, required-ness) only ever gains
|
|
31
|
+
new OPTIONAL parameters — an existing parameter's name, type and
|
|
32
|
+
required/optional flag are stable. Tool bodies are thin wrappers over
|
|
33
|
+
:mod:`unicode_logic_kit.api`, so the payload *contents* already inherit that
|
|
34
|
+
module's own STABILITY POLICY (see its docstring); this paragraph covers
|
|
35
|
+
the tool *surface* specifically, which a schema/name-based MCP client
|
|
36
|
+
depends on in a way a ``pip`` version pin cannot express for a JSON-RPC
|
|
37
|
+
session. ``tests/test_mcp_stability.py`` pins the current tool-schema
|
|
38
|
+
baseline (derived from a real ``list_tools()`` call) and fails with a
|
|
39
|
+
readable diff on any surface drift.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import functools
|
|
43
|
+
from typing import List, Optional
|
|
44
|
+
|
|
45
|
+
from .. import api
|
|
46
|
+
from ..atp.protocol import (
|
|
47
|
+
PROVED,
|
|
48
|
+
BackendUnavailable,
|
|
49
|
+
available_backends,
|
|
50
|
+
default_chain,
|
|
51
|
+
_REGISTRY,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
__all__ = ["create_server", "main"]
|
|
55
|
+
|
|
56
|
+
_SERVER_NAME = "unicode-logic-kit"
|
|
57
|
+
|
|
58
|
+
_INSTRUCTIONS = """Logic toolbox for NL->FOL work: parse (any dialect:
|
|
59
|
+
unicode, TPTP, LaTeX, Prover9, SMT-LIB), well-formedness checks, graded
|
|
60
|
+
equivalence, proving/refuting over a multi-backend portfolio (list_backends
|
|
61
|
+
shows what is registered and available right now), self-explaining
|
|
62
|
+
countermodels, formula diagnosis for repair loops (you are the fixer:
|
|
63
|
+
diagnose -> apply the suggestion -> diagnose again), mechanical repair of
|
|
64
|
+
the failures that have one right answer (repair_formula: an illegal symbol
|
|
65
|
+
name renamed invertibly, a free variable closed on request -- but never a
|
|
66
|
+
bracket guessed into a mixed conjunction/disjunction), logic-to-logic
|
|
67
|
+
translation, and English verbalization. Error-analysis layer:
|
|
68
|
+
compare_formulas gives the full prediction-vs-gold breakdown (structural /
|
|
69
|
+
canonical / vocabulary-aligned match, solver equivalence, symbol diff),
|
|
70
|
+
score_batch aggregates it over a corpus, check_consistency decides whether
|
|
71
|
+
a premise SET is satisfiable (with a model witness), get_signature extracts
|
|
72
|
+
the vocabulary of a formula set, detect_dialect shows what the input looks
|
|
73
|
+
like, normalize/render convert between normal forms and concrete syntaxes,
|
|
74
|
+
truth_table decides propositional formulas by enumeration (classical/K3/LP),
|
|
75
|
+
drs_to_fol turns discourse boxes (donkey sentences, cross-sentence
|
|
76
|
+
anaphora) into provable FOL, and list_translations enumerates the
|
|
77
|
+
logic-to-logic edges translate can follow. A translation comes with side
|
|
78
|
+
axioms (frame conditions; for a many-sorted formula the non-emptiness of
|
|
79
|
+
every sort AND the membership of every sorted constant in its sort):
|
|
80
|
+
translate returns them next to the translated formula, and they go into
|
|
81
|
+
prove / find_countermodel as SEPARATE premises -- without them a valid
|
|
82
|
+
formula comes back refuted.
|
|
83
|
+
Probabilistic layer (exact,
|
|
84
|
+
no sampling): probability_bounds computes Nilsson-style entailed bounds
|
|
85
|
+
from probability-interval premises, probability_query answers
|
|
86
|
+
ProbLog-style queries under distribution semantics. Formulas are passed
|
|
87
|
+
as plain text; results are structured JSON. In the unicode syntax a
|
|
88
|
+
constant whose name is one letter, starts upper-case or holds a space or
|
|
89
|
+
punctuation is written in single quotes ('k2', 'Alice', 'John Doe'), and
|
|
90
|
+
every tool takes that text back as it gives it.
|
|
91
|
+
|
|
92
|
+
Self-correction loop: every parse failure comes back as {"ok": false,
|
|
93
|
+
"argument": ..., "errors": [...], "spec_topic": ...}. Call get_syntax_spec
|
|
94
|
+
with that topic to retrieve the exact rule (naming conventions, operator
|
|
95
|
+
precedence, quantifier scope, the counting quantifier, the chemical
|
|
96
|
+
signature, or the catalogue of known failure modes), then regenerate. The
|
|
97
|
+
grammar therefore does not need to live in your prompt."""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
#: Lowercased substring of a parse-error message -> the syntax_spec topic
|
|
101
|
+
#: that explains it. Deliberately a small, honest heuristic: it tells the
|
|
102
|
+
#: caller where to LOOK, it does not claim to have diagnosed the formula —
|
|
103
|
+
#: which is why the fallback is "overview" (whose first entry is the
|
|
104
|
+
#: naming/dialect confusion behind most failures) rather than a guess.
|
|
105
|
+
#:
|
|
106
|
+
#: ORDER IS PRIORITY, most specific first: within ONE message a generic
|
|
107
|
+
#: needle such as "unexpected character" also matches the precise
|
|
108
|
+
#: mixed-connective diagnosis, so the earlier entry decides what that message
|
|
109
|
+
#: is about (see :func:`_spec_topic_for`).
|
|
110
|
+
_SPEC_HINTS = (
|
|
111
|
+
# Mixed same-level connectives: the kit's unicode grammar refuses
|
|
112
|
+
# 'A ∧ B ∨ C' outright instead of resolving it by precedence, so the fix
|
|
113
|
+
# is brackets and the topic is operators — never naming, however much the
|
|
114
|
+
# message mentions a predicate.
|
|
115
|
+
("cannot mix", "operators"),
|
|
116
|
+
("without parentheses", "operators"),
|
|
117
|
+
("parenthesise", "operators"),
|
|
118
|
+
# "… after universal quantifier '∀'" — a quantifier that never got its
|
|
119
|
+
# bound variable, which is a scope question and not a name question.
|
|
120
|
+
("after universal quantifier", "quantifiers"),
|
|
121
|
+
("after existential quantifier", "quantifiers"),
|
|
122
|
+
("invalid name", "naming"),
|
|
123
|
+
("invalid variable", "naming"),
|
|
124
|
+
("invalid name/constant", "naming"),
|
|
125
|
+
("not bound", "quantifiers"),
|
|
126
|
+
("free variable", "quantifiers"),
|
|
127
|
+
("incomplete formula", "operators"),
|
|
128
|
+
("unexpected token", "operators"),
|
|
129
|
+
("unexpected character", "naming"),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
#: How far into the input a dialect got before giving up — imported from the
|
|
134
|
+
#: core facade, which needs the same measure to pick the one error message it
|
|
135
|
+
#: turns into a repair suggestion. One implementation, two callers.
|
|
136
|
+
_message_progress = api._message_progress
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _spec_topic_for(errors) -> Optional[str]:
|
|
140
|
+
"""The syntax_spec topic most likely to explain these parse errors.
|
|
141
|
+
|
|
142
|
+
``errors`` holds one entry per candidate dialect that was tried, and the
|
|
143
|
+
two available signals disagree often enough that neither decides alone:
|
|
144
|
+
|
|
145
|
+
* how FAR a dialect got before giving up — the dialects without
|
|
146
|
+
quantifiers abandon '∀x (P(x) ∧ Q(x) ⊕ R(x))' at position 1 and call it
|
|
147
|
+
a naming problem, and they are the majority, but the ones that read as
|
|
148
|
+
far as the ⊕ named the real cause;
|
|
149
|
+
* how MANY dialects agree — in '∀ P(x)' six of them report a quantifier
|
|
150
|
+
left without its variable and a single second-order reading happens to
|
|
151
|
+
consume the whole string, so distance alone would hand the answer to
|
|
152
|
+
the outlier.
|
|
153
|
+
|
|
154
|
+
So each message votes with a weight given by the RANK of its distance
|
|
155
|
+
among the distinct distances seen (farthest wins, but a near-unanimous
|
|
156
|
+
verdict one step back still outweighs a lone outlier), and the topic with
|
|
157
|
+
the highest total wins. Ties — including the case where every dialect
|
|
158
|
+
stopped at the same place — go to the more specific needle
|
|
159
|
+
(``_SPEC_HINTS`` order). This is a routing hint, not a diagnosis: the
|
|
160
|
+
caller gets the rule most likely to explain the rejection, and the
|
|
161
|
+
messages themselves stay in the response.
|
|
162
|
+
|
|
163
|
+
Each message votes exactly once, for its first matching needle — a mixed
|
|
164
|
+
connective is reported as an unexpected character too, and counting that
|
|
165
|
+
message for both topics would let the vague reading dilute the precise
|
|
166
|
+
one. Matching is case-insensitive: the parsers capitalise their messages
|
|
167
|
+
inconsistently, and a hint that silently stops matching because of a
|
|
168
|
+
capital letter is worse than no hint at all.
|
|
169
|
+
"""
|
|
170
|
+
votes = []
|
|
171
|
+
for entry in errors:
|
|
172
|
+
message = entry.get("message", "").lower()
|
|
173
|
+
for rank, (needle, topic) in enumerate(_SPEC_HINTS):
|
|
174
|
+
if needle in message:
|
|
175
|
+
votes.append((_message_progress(message), topic, rank))
|
|
176
|
+
break
|
|
177
|
+
if not votes:
|
|
178
|
+
return "overview"
|
|
179
|
+
|
|
180
|
+
weight_of = {distance: index for index, distance
|
|
181
|
+
in enumerate(sorted({v[0] for v in votes}))}
|
|
182
|
+
scores: dict = {}
|
|
183
|
+
for progress, topic, rank in votes:
|
|
184
|
+
score, best_rank = scores.get(topic, (0, rank))
|
|
185
|
+
scores[topic] = (score + weight_of[progress], min(best_rank, rank))
|
|
186
|
+
return min(scores, key=lambda topic: (-scores[topic][0], scores[topic][1]))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _parse(text: str, dialect: Optional[str], argument: str = "text"):
|
|
190
|
+
"""``(node, None)`` on success, ``(None, error_dict)`` on failure.
|
|
191
|
+
|
|
192
|
+
``argument`` names WHICH tool input failed in the uniform error shape
|
|
193
|
+
(see the module docstring) so multi-argument tools stay distinguishable
|
|
194
|
+
without inventing per-tool nesting. The failure also carries
|
|
195
|
+
``spec_topic``: the :func:`syntax_spec` topic to fetch before retrying —
|
|
196
|
+
what turns a bare rejection into a correction loop the caller can close
|
|
197
|
+
on its own.
|
|
198
|
+
"""
|
|
199
|
+
parsed = api.parse_any(text, hint=dialect)
|
|
200
|
+
if not parsed.ok:
|
|
201
|
+
errors = parsed.to_dict()["errors"]
|
|
202
|
+
return None, {"ok": False, "argument": argument, "errors": errors,
|
|
203
|
+
"spec_topic": _spec_topic_for(errors)}
|
|
204
|
+
return parsed.formula, None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _error(exc: Exception) -> dict:
|
|
208
|
+
return {"error": {"type": type(exc).__name__, "message": str(exc)}}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _text_size(value) -> int:
|
|
212
|
+
"""The length of the longest text inside ``value`` (a string, or lists and dicts of them)."""
|
|
213
|
+
longest, pending = 0, [value]
|
|
214
|
+
while pending:
|
|
215
|
+
item = pending.pop()
|
|
216
|
+
if isinstance(item, str):
|
|
217
|
+
longest = max(longest, len(item))
|
|
218
|
+
elif isinstance(item, dict):
|
|
219
|
+
pending.extend(item.values())
|
|
220
|
+
elif isinstance(item, (list, tuple)):
|
|
221
|
+
pending.extend(item)
|
|
222
|
+
return longest
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _nesting(value) -> int:
|
|
226
|
+
"""How many containers (dicts, lists) lie on the longest path from ``value`` down."""
|
|
227
|
+
deepest, pending = 0, [(value, 1)]
|
|
228
|
+
while pending:
|
|
229
|
+
item, level = pending.pop()
|
|
230
|
+
if isinstance(item, dict):
|
|
231
|
+
deepest = max(deepest, level)
|
|
232
|
+
pending.extend((child, level + 1) for child in item.values())
|
|
233
|
+
elif isinstance(item, (list, tuple)):
|
|
234
|
+
deepest = max(deepest, level)
|
|
235
|
+
pending.extend((child, level + 1) for child in item)
|
|
236
|
+
return deepest
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _answers_deep_input(tool):
|
|
240
|
+
"""``tool``, which never lets a ``RecursionError`` leave it.
|
|
241
|
+
|
|
242
|
+
A formula nested a few hundred levels deep is read by the parser and then walked by
|
|
243
|
+
recursive code (a normal form, a renderer, a node comparison), which runs out of the
|
|
244
|
+
interpreter's recursion limit. The call is then made again where ``api`` reads a deep
|
|
245
|
+
formula (:func:`unicode_logic_kit.api._call_deep`: a worker thread whose stack and recursion
|
|
246
|
+
limit are sized for it), with the nesting bounded by the length of the longest text of the
|
|
247
|
+
arguments (a text cannot be nested deeper than it is long). A call that still runs out is
|
|
248
|
+
answered as the structured ``{"error": ...}`` that every other refusal of this module is,
|
|
249
|
+
never as an exception that leaves the tool.
|
|
250
|
+
"""
|
|
251
|
+
@functools.wraps(tool)
|
|
252
|
+
def guarded(*args, **kwargs):
|
|
253
|
+
try:
|
|
254
|
+
return tool(*args, **kwargs)
|
|
255
|
+
except RecursionError:
|
|
256
|
+
pass
|
|
257
|
+
size = max((_text_size(value) for value in (*args, *kwargs.values())), default=0)
|
|
258
|
+
try:
|
|
259
|
+
return api._call_deep(min(size, api._DEEP_MAX_LEVELS), lambda: tool(*args, **kwargs))
|
|
260
|
+
except RecursionError:
|
|
261
|
+
return _error(RecursionError(
|
|
262
|
+
f"{tool.__name__}: the input is nested more deeply than this tool can process "
|
|
263
|
+
f"(the interpreter's recursion limit ran out, also on the worker that reads deep "
|
|
264
|
+
f"formulas); no result was produced"))
|
|
265
|
+
|
|
266
|
+
guarded.answers_deep_input = True
|
|
267
|
+
return guarded
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _answers_serializable(tool):
|
|
271
|
+
"""``tool``, whose answer is one the transport can write out, or else a refusal by name.
|
|
272
|
+
|
|
273
|
+
A tool answers with a dict, and the MCP SDK writes that dict as JSON with a limit of its own
|
|
274
|
+
on how deeply it may be nested. An answer nested deeper (the abstract syntax tree of a formula
|
|
275
|
+
a few hundred quantifiers deep) ends in the SDK's own ``ToolError`` whose text speaks of a
|
|
276
|
+
circular reference, which this answer is not. The answer is tried with the SDK's own JSON
|
|
277
|
+
writer; one it cannot write is replaced by the structured ``{"error": ...}`` that names the
|
|
278
|
+
nesting and says what to ask for instead. A call from Python, which needs no JSON, reaches
|
|
279
|
+
the tool itself and is not held to this limit (see :func:`_registered`).
|
|
280
|
+
"""
|
|
281
|
+
@functools.wraps(tool)
|
|
282
|
+
def guarded(*args, **kwargs):
|
|
283
|
+
result = tool(*args, **kwargs)
|
|
284
|
+
try:
|
|
285
|
+
import pydantic_core
|
|
286
|
+
except ImportError: # no SDK, nothing is written out
|
|
287
|
+
return result
|
|
288
|
+
try:
|
|
289
|
+
pydantic_core.to_json(result, fallback=str)
|
|
290
|
+
except ValueError:
|
|
291
|
+
return _error(ValueError(
|
|
292
|
+
f"{tool.__name__}: the input was read, but its answer is nested {_nesting(result)} "
|
|
293
|
+
f"levels deep, more than the MCP transport can write as JSON. Ask for a text form "
|
|
294
|
+
f"of it instead (render, which answers a formula as text), or give a shallower "
|
|
295
|
+
f"input"))
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
return guarded
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _registered(tool):
|
|
302
|
+
"""``tool`` as the server registers it: guarded against a deep input and a deep answer."""
|
|
303
|
+
return _answers_serializable(
|
|
304
|
+
tool if getattr(tool, "answers_deep_input", False) else _answers_deep_input(tool))
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _signature_argument(signature):
|
|
308
|
+
"""The ``signature`` a tool was handed, as ``api`` reads it.
|
|
309
|
+
|
|
310
|
+
What ``get_signature`` returns is ``{"ok": True, "signature": {...}}``; the tools that take a
|
|
311
|
+
signature accept that whole result, or just its ``signature`` value, or the loose form
|
|
312
|
+
``{"predicates": ..., "functions": ..., "constants": ...}``. A dict that is exactly such a
|
|
313
|
+
result is replaced by its ``signature`` value; anything else is returned as it is, for
|
|
314
|
+
``api`` to read or to refuse.
|
|
315
|
+
"""
|
|
316
|
+
if (isinstance(signature, dict) and set(signature) == {"ok", "signature"}
|
|
317
|
+
and signature["ok"] is True and isinstance(signature["signature"], dict)):
|
|
318
|
+
return signature["signature"]
|
|
319
|
+
return signature
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _unicode_texts(*concepts):
|
|
323
|
+
"""``([text, ...], None)`` or ``(None, {"error": ...})`` for description-logic concepts.
|
|
324
|
+
|
|
325
|
+
``Concept.to_unicode`` refuses (``ValueError``) a concept one of whose names makes the glyph
|
|
326
|
+
text read back as ANOTHER concept (a class named ``<A⊓B>`` prints as the intersection of
|
|
327
|
+
``<A`` and ``B>``). A tool answers that refusal like every other refusal of the ``dl``
|
|
328
|
+
package: as the structured error, never as an exception that leaves the tool.
|
|
329
|
+
"""
|
|
330
|
+
try:
|
|
331
|
+
return [concept.to_unicode() for concept in concepts], None
|
|
332
|
+
except ValueError as exc:
|
|
333
|
+
return None, _error(exc)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _normalize_converses(converses: List[dict]):
|
|
337
|
+
"""JSON-dict converse declarations -> the internal tuple form.
|
|
338
|
+
|
|
339
|
+
``converses`` (``compare_formulas``/``score_batch``'s own parameter) is
|
|
340
|
+
a list of ``{"a": [name, arity], "b": [name, arity], "permutation":
|
|
341
|
+
[...]}`` dicts — the JSON-friendly spelling of
|
|
342
|
+
:data:`unicode_logic_kit.eval.converses.ConverseDeclaration`. Raises
|
|
343
|
+
``ValueError`` for a malformed entry (missing key, wrong shape) BEFORE
|
|
344
|
+
``api.equivalent``/``eval.compute_fol_metrics`` ever see it; the
|
|
345
|
+
declaration's own semantic validity (arity match, permutation
|
|
346
|
+
bijectivity, no self-pair, …) is ``validate_converses``'s job, run
|
|
347
|
+
inside those calls — this function only bridges the wire shape.
|
|
348
|
+
"""
|
|
349
|
+
normalized = []
|
|
350
|
+
for i, entry in enumerate(converses):
|
|
351
|
+
try:
|
|
352
|
+
a_name, a_arity = entry["a"]
|
|
353
|
+
b_name, b_arity = entry["b"]
|
|
354
|
+
permutation = entry["permutation"]
|
|
355
|
+
normalized.append((
|
|
356
|
+
(a_name, int(a_arity)), (b_name, int(b_arity)),
|
|
357
|
+
tuple(int(p) for p in permutation)))
|
|
358
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
359
|
+
raise ValueError(
|
|
360
|
+
f"converses[{i}]: expected "
|
|
361
|
+
'{"a": [name, arity], "b": [name, arity], "permutation": '
|
|
362
|
+
f'[...]}}, got {entry!r}') from exc
|
|
363
|
+
return normalized
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# --------------------------------------------------------------------------
|
|
367
|
+
# Tool implementations (plain functions; registered in create_server)
|
|
368
|
+
# --------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
@_answers_deep_input
|
|
371
|
+
def parse_formula(text: str, dialect: Optional[str] = None) -> dict:
|
|
372
|
+
"""Parse formula text (dialect auto-detected) to the kit's JSON AST."""
|
|
373
|
+
parsed = api.parse_any(text, hint=dialect)
|
|
374
|
+
result = parsed.to_dict()
|
|
375
|
+
if parsed.ok:
|
|
376
|
+
# The unicode rendering is what an LLM wants to read back.
|
|
377
|
+
result["unicode"] = parsed.formula.to_unicode_str()
|
|
378
|
+
else:
|
|
379
|
+
result["argument"] = "text"
|
|
380
|
+
return result
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@_answers_deep_input
|
|
384
|
+
def check_formula(text: str, dialect: Optional[str] = None,
|
|
385
|
+
signature: Optional[dict] = None) -> dict:
|
|
386
|
+
"""Well-formedness + optional signature conformance for formula text.
|
|
387
|
+
|
|
388
|
+
``signature`` is what ``get_signature`` returns (its whole result, or just its
|
|
389
|
+
``signature`` value), or
|
|
390
|
+
the loose form ``{"predicates": {"Human": 1}, "functions": {"father": 1},
|
|
391
|
+
"constants": ["socrates"]}``. The truth constants ``⊤`` / ``⊥`` are logical
|
|
392
|
+
constants, never an unknown predicate. A malformed ``signature`` comes back
|
|
393
|
+
as the structured ``{"error": ...}``.
|
|
394
|
+
"""
|
|
395
|
+
node, err = _parse(text, dialect)
|
|
396
|
+
if err is not None:
|
|
397
|
+
return err
|
|
398
|
+
try:
|
|
399
|
+
return api.check(node, signature=_signature_argument(signature)).to_dict()
|
|
400
|
+
except (TypeError, ValueError) as exc:
|
|
401
|
+
return _error(exc)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@_answers_deep_input
|
|
405
|
+
def prove(conclusion: str, premises: Optional[List[str]] = None,
|
|
406
|
+
logic: str = "auto", backends: Optional[List[str]] = None,
|
|
407
|
+
timeout_ms: int = 10000, dialect: Optional[str] = None) -> dict:
|
|
408
|
+
"""Decide premises |= conclusion; the Verdict dict carries provenance."""
|
|
409
|
+
node, err = _parse(conclusion, dialect, argument="conclusion")
|
|
410
|
+
if err is not None:
|
|
411
|
+
return err
|
|
412
|
+
parsed_premises = []
|
|
413
|
+
for i, p in enumerate(premises or []):
|
|
414
|
+
pnode, perr = _parse(p, dialect, argument=f"premise[{i}]")
|
|
415
|
+
if perr is not None:
|
|
416
|
+
return perr
|
|
417
|
+
parsed_premises.append(pnode)
|
|
418
|
+
try:
|
|
419
|
+
verdict = api.prove(node, parsed_premises, logic=logic,
|
|
420
|
+
backends=backends, timeout=timeout_ms)
|
|
421
|
+
except (BackendUnavailable, ValueError) as exc:
|
|
422
|
+
return _error(exc)
|
|
423
|
+
return verdict.to_dict()
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@_answers_deep_input
|
|
427
|
+
def find_countermodel(formula: str, premises: Optional[List[str]] = None,
|
|
428
|
+
logic: str = "auto",
|
|
429
|
+
dialect: Optional[str] = None) -> dict:
|
|
430
|
+
"""A countermodel to premises |= formula, with an English explanation."""
|
|
431
|
+
node, err = _parse(formula, dialect, argument="formula")
|
|
432
|
+
if err is not None:
|
|
433
|
+
return err
|
|
434
|
+
parsed_premises = []
|
|
435
|
+
for i, p in enumerate(premises or []):
|
|
436
|
+
pnode, perr = _parse(p, dialect, argument=f"premise[{i}]")
|
|
437
|
+
if perr is not None:
|
|
438
|
+
return perr
|
|
439
|
+
parsed_premises.append(pnode)
|
|
440
|
+
try:
|
|
441
|
+
return api.countermodel(node, parsed_premises, logic=logic).to_dict()
|
|
442
|
+
except (BackendUnavailable, ValueError) as exc:
|
|
443
|
+
return _error(exc)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@_answers_deep_input
|
|
447
|
+
def check_equivalence(formula1: str, formula2: str, method: str = "auto",
|
|
448
|
+
timeout_ms: int = 10000,
|
|
449
|
+
dialect: Optional[str] = None) -> dict:
|
|
450
|
+
"""Graded equivalence (exact -> canonical -> aligned -> solver)."""
|
|
451
|
+
node1, err1 = _parse(formula1, dialect, argument="formula1")
|
|
452
|
+
if err1 is not None:
|
|
453
|
+
return err1
|
|
454
|
+
node2, err2 = _parse(formula2, dialect, argument="formula2")
|
|
455
|
+
if err2 is not None:
|
|
456
|
+
return err2
|
|
457
|
+
try:
|
|
458
|
+
return api.equivalent(node1, node2, method=method,
|
|
459
|
+
timeout=timeout_ms).to_dict()
|
|
460
|
+
except ValueError as exc:
|
|
461
|
+
return _error(exc)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@_answers_deep_input
|
|
465
|
+
def diagnose(text: str, dialect: Optional[str] = None,
|
|
466
|
+
signature: Optional[dict] = None) -> dict:
|
|
467
|
+
"""One diagnose round of the repair loop; YOU are the fixer.
|
|
468
|
+
|
|
469
|
+
Returns ``{ok, diagnostics, suggestion, converged}`` for the given
|
|
470
|
+
text, plus ``spec_topic`` when it did not parse. Apply the suggestion to
|
|
471
|
+
the text yourself and call again; ``converged=True`` means the text
|
|
472
|
+
parses and checks clean. ``signature`` is read as ``check_formula`` reads
|
|
473
|
+
it (``get_signature``'s whole result, or its ``signature`` value); a malformed one comes
|
|
474
|
+
back as the structured ``{"error": ...}``.
|
|
475
|
+
"""
|
|
476
|
+
try:
|
|
477
|
+
step = next(api.repair(text, dialect=dialect, signature=_signature_argument(signature)))
|
|
478
|
+
except (TypeError, ValueError) as exc:
|
|
479
|
+
return _error(exc)
|
|
480
|
+
result = step.to_dict()
|
|
481
|
+
# Same routing every other tool's failure carries: the diagnosis names
|
|
482
|
+
# WHAT broke, spec_topic names the rule to look up before retrying. A
|
|
483
|
+
# loop that has only the message has to guess which rule it violated.
|
|
484
|
+
parse_errors = result.get("diagnostics", {}).get("parse")
|
|
485
|
+
if parse_errors:
|
|
486
|
+
result["spec_topic"] = _spec_topic_for(parse_errors)
|
|
487
|
+
return result
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@_answers_deep_input
|
|
491
|
+
def repair_formula(text: str, dialect: Optional[str] = None,
|
|
492
|
+
close_free_variables: bool = False,
|
|
493
|
+
sanitize_invalid_names: bool = True) -> dict:
|
|
494
|
+
"""Mechanically repair what CAN be repaired; report the rest.
|
|
495
|
+
|
|
496
|
+
The counterpart to ``diagnose`` (where you are the fixer): this fixes the
|
|
497
|
+
two failure shapes that have one right answer, so they cost you no
|
|
498
|
+
attempt. A name no symbol class of this dialect accepts (a chemical name
|
|
499
|
+
with digits, commas or hyphens) is renamed to a legal predicate, with the
|
|
500
|
+
original kept in ``names`` — nothing is lost. A free variable is reported
|
|
501
|
+
and, with ``close_free_variables=True``, closed.
|
|
502
|
+
|
|
503
|
+
What it deliberately does NOT do is bracket a formula that mixes ∧ and ∨
|
|
504
|
+
at the same level: the readings differ and choosing one would be a guess.
|
|
505
|
+
That comes back ``ok=False`` with kind ``"mixed_connectives"`` — write the
|
|
506
|
+
brackets you mean and call again.
|
|
507
|
+
|
|
508
|
+
Returns ``{ok, formula, repaired_text, issues, changed, dialect, names}``,
|
|
509
|
+
plus ``spec_topic`` when nothing parsed. ``repaired_text`` is in the kit's
|
|
510
|
+
unicode syntax and re-parses to ``formula``.
|
|
511
|
+
"""
|
|
512
|
+
from ..fol.dialect_repair import repair_formula as _repair
|
|
513
|
+
|
|
514
|
+
result = _repair(text, dialect=dialect,
|
|
515
|
+
close_free_variables=close_free_variables,
|
|
516
|
+
sanitize_invalid_names=sanitize_invalid_names).to_dict()
|
|
517
|
+
if not result["ok"]:
|
|
518
|
+
# Same routing every other tool's failure carries: the issue names
|
|
519
|
+
# WHAT broke, spec_topic names the rule to look up before retrying.
|
|
520
|
+
result["spec_topic"] = _spec_topic_for(result["issues"])
|
|
521
|
+
return result
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
#: The per-edge options ``translate`` forwards, named EXACTLY as the
|
|
525
|
+
#: comorphism edges declare them (``Comorphism.options``) — this layer invents
|
|
526
|
+
#: no option of its own. ``tests/test_mcp_server.py`` pins that this tuple is
|
|
527
|
+
#: the union of what the registry's edges declare, so an edge that grows an
|
|
528
|
+
#: option cannot become silently unreachable over MCP.
|
|
529
|
+
_TRANSLATE_OPTIONS = ("frame", "systems", "temporal_closure", "signature",
|
|
530
|
+
"mode", "bridges")
|
|
531
|
+
|
|
532
|
+
#: Source logics whose text must NOT go through ``parse_any``'s classical-first
|
|
533
|
+
#: mode ladder, because an earlier mode reads the same string as a DIFFERENT
|
|
534
|
+
#: formula: 'P ⊕ Q' is classical Xor in the ``fol`` mode and a strong
|
|
535
|
+
#: Łukasiewicz disjunction in the fuzzy ones, so a fuzzy term parsed by the
|
|
536
|
+
#: ladder would be translated as the classical formula it is not. The dialects
|
|
537
|
+
#: are tried in order and used only when the caller gave no ``dialect`` of
|
|
538
|
+
#: their own; the two fuzzy modes are disjoint (``msfl`` accepts only SORTED
|
|
539
|
+
#: quantifiers, ``fl`` only unsorted ones), hence both.
|
|
540
|
+
_SOURCE_DIALECTS = {"fuzzy": ("fl", "msfl")}
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _translate_options(frame, systems, temporal_closure, signature, mode,
|
|
544
|
+
bridges) -> dict:
|
|
545
|
+
"""The per-edge options that were actually given, shaped for the registry.
|
|
546
|
+
|
|
547
|
+
``None`` means "not given" and is dropped, so an edge's own default
|
|
548
|
+
(``frame="K"``, ``mode="constant"``, ``temporal_closure=True``) applies.
|
|
549
|
+
This checks only the SHAPE of each value — a ``temporal_closure="false"``
|
|
550
|
+
string would be truthy and silently mean ``True`` — and leaves membership
|
|
551
|
+
(is that a known frame / mode / bridge / modal family?) and "does any edge
|
|
552
|
+
on this path take that option?" to the edges and the registry, whose
|
|
553
|
+
refusals already name what is accepted and which the tool returns as
|
|
554
|
+
``{"error": ...}``. ``ValueError`` on a malformed value.
|
|
555
|
+
"""
|
|
556
|
+
options: dict = {}
|
|
557
|
+
|
|
558
|
+
def given(name, value, kind, what):
|
|
559
|
+
if value is None:
|
|
560
|
+
return False
|
|
561
|
+
# bool is an int subclass, but 'frame': true is not a frame name.
|
|
562
|
+
if not isinstance(value, kind) or (kind is not bool
|
|
563
|
+
and isinstance(value, bool)):
|
|
564
|
+
raise ValueError(
|
|
565
|
+
f"translate: {name} must be {what}, got {value!r}")
|
|
566
|
+
return True
|
|
567
|
+
|
|
568
|
+
if given("frame", frame, str,
|
|
569
|
+
"a string — a modal system name such as 'S4', or a "
|
|
570
|
+
"Scott–Lemmon spec like 'G(1,1,1,1)'"):
|
|
571
|
+
options["frame"] = frame
|
|
572
|
+
if given("mode", mode, str,
|
|
573
|
+
"a string — the quantified-modal domain regime, such as "
|
|
574
|
+
"'constant' or 'varying'"):
|
|
575
|
+
options["mode"] = mode
|
|
576
|
+
if given("temporal_closure", temporal_closure, bool, "true or false"):
|
|
577
|
+
options["temporal_closure"] = temporal_closure
|
|
578
|
+
if given("systems", systems, dict,
|
|
579
|
+
"an object mapping a modal family to a system name, e.g. "
|
|
580
|
+
'{"epistemic": "S5"}'):
|
|
581
|
+
if not all(isinstance(k, str) and isinstance(v, str)
|
|
582
|
+
for k, v in systems.items()):
|
|
583
|
+
raise ValueError(
|
|
584
|
+
f"translate: systems must map a family name to a system "
|
|
585
|
+
f"name (both strings), got {systems!r}")
|
|
586
|
+
options["systems"] = dict(systems)
|
|
587
|
+
if given("bridges", bridges, (list, tuple), "a list of bridge names"):
|
|
588
|
+
if not all(isinstance(b, str) for b in bridges):
|
|
589
|
+
raise ValueError(
|
|
590
|
+
f"translate: bridges must be a list of strings, got "
|
|
591
|
+
f"{bridges!r}")
|
|
592
|
+
options["bridges"] = list(bridges)
|
|
593
|
+
if given("signature", signature, dict,
|
|
594
|
+
'a signature object, e.g. {"subsorts": {"Human": ["Animal"]}}'):
|
|
595
|
+
from ..fol.signature import Signature
|
|
596
|
+
|
|
597
|
+
# from_dict's own refusals (unknown key, wrong-typed section, a subsort
|
|
598
|
+
# cycle) name the offending entry; they surface through _error.
|
|
599
|
+
options["signature"] = Signature.from_dict(signature)
|
|
600
|
+
return options
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _text_of(node) -> str:
|
|
604
|
+
"""The unicode rendering of ``node``, in a form the OTHER tools can read.
|
|
605
|
+
|
|
606
|
+
Every tool here takes formula TEXT, so a translated formula and its side
|
|
607
|
+
axioms are only usable as premises if their text parses again, and as the
|
|
608
|
+
same formula. A constant always does: the printer writes it in single
|
|
609
|
+
quotes whenever its bare name would read as something else (``'a'``,
|
|
610
|
+
``'k2'``, ``'Alice'``, ``'John Doe'``), so a constant of any name that has
|
|
611
|
+
a text reads back as itself. A name that is not a constant's is printed as
|
|
612
|
+
it is, and the text grammar can still refuse or misread it: the bound
|
|
613
|
+
variables the translations mint are legal names since 0.30.0, but a
|
|
614
|
+
variable the CALLER names ``hasChild`` is a binder the grammar refuses (a
|
|
615
|
+
binder is one letter and digits), and a predicate that starts lower-case
|
|
616
|
+
(an OWL-style role ``hasChild(x, y)``) reads as a function application,
|
|
617
|
+
which is no formula. Rendered as it is, the result can look right and
|
|
618
|
+
``prove`` rejects it. So the node is rendered as the kit prints it and,
|
|
619
|
+
failing that, with its bound variables alpha-renamed to ``q0``, ``q1``, …
|
|
620
|
+
— a renaming that changes no meaning, and the one thing that mends the
|
|
621
|
+
first case — and the first spelling that reads back as EXACTLY the same
|
|
622
|
+
formula wins. Failing both, the first that parses at all (a FREE variable
|
|
623
|
+
named like a constant, ``hasChild``, reads back as that constant, and
|
|
624
|
+
alpha-renaming leaves a free name alone), and failing that the plain
|
|
625
|
+
printing; the ``result`` / ``axioms`` ASTs next to it stay the authority.
|
|
626
|
+
A rendering that already reads back is left exactly as the kit prints it.
|
|
627
|
+
|
|
628
|
+
Raises:
|
|
629
|
+
ValueError: ``node`` holds a constant that has no text (an empty name,
|
|
630
|
+
or a name with a control character): the printer refuses it by
|
|
631
|
+
name, and ``translate`` answers that as a structured error.
|
|
632
|
+
"""
|
|
633
|
+
from ..eval.canonical import _alpha_normalize
|
|
634
|
+
|
|
635
|
+
spellings = (node, _alpha_normalize(node))
|
|
636
|
+
texts = [spelling.to_unicode_str() for spelling in spellings]
|
|
637
|
+
readings: dict = {}
|
|
638
|
+
|
|
639
|
+
def reads(i: int, exact: bool) -> bool:
|
|
640
|
+
if i not in readings:
|
|
641
|
+
readings[i] = api.parse_any(texts[i])
|
|
642
|
+
reading = readings[i]
|
|
643
|
+
return reading.ok and (not exact or reading.formula == spellings[i])
|
|
644
|
+
|
|
645
|
+
for exact in (True, False):
|
|
646
|
+
for i in range(len(spellings)):
|
|
647
|
+
if reads(i, exact):
|
|
648
|
+
return texts[i]
|
|
649
|
+
return texts[0]
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _text_of_term(value) -> Optional[str]:
|
|
653
|
+
"""``unicode`` text for a Node or a DL concept, else ``None``."""
|
|
654
|
+
if hasattr(value, "to_unicode_str"):
|
|
655
|
+
return _text_of(value)
|
|
656
|
+
if hasattr(value, "to_unicode"): # dl.Concept spelling
|
|
657
|
+
return value.to_unicode()
|
|
658
|
+
return None
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _parse_term(term: str, from_logic: str, dialect: Optional[str]):
|
|
662
|
+
"""``(payload, None)`` or ``(None, error_dict)`` for the source logic's
|
|
663
|
+
own term type (see :func:`translate` for the list)."""
|
|
664
|
+
if from_logic == "casl":
|
|
665
|
+
return term, None
|
|
666
|
+
if from_logic == "alc":
|
|
667
|
+
from ..dl import ConceptSyntaxError, parse_concept
|
|
668
|
+
|
|
669
|
+
try:
|
|
670
|
+
return parse_concept(term), None
|
|
671
|
+
except ConceptSyntaxError as exc:
|
|
672
|
+
return None, {"ok": False, "argument": "term",
|
|
673
|
+
"errors": [{"dialect": "alc", "message": str(exc)}]}
|
|
674
|
+
if from_logic == "drs":
|
|
675
|
+
from .. import drt
|
|
676
|
+
|
|
677
|
+
try:
|
|
678
|
+
return drt.parse_drs(term), None
|
|
679
|
+
except (drt.DRSSyntaxError, ValueError) as exc:
|
|
680
|
+
return None, {"ok": False, "argument": "term",
|
|
681
|
+
"errors": [{"dialect": "drs_box",
|
|
682
|
+
"message": str(exc)}]}
|
|
683
|
+
hints = (dialect,) if dialect else _SOURCE_DIALECTS.get(from_logic, (None,))
|
|
684
|
+
failures: list = []
|
|
685
|
+
for hint in hints:
|
|
686
|
+
node, err = _parse(term, hint, argument="term")
|
|
687
|
+
if err is None:
|
|
688
|
+
return node, None
|
|
689
|
+
failures.append(err)
|
|
690
|
+
err = failures[0]
|
|
691
|
+
if len(failures) > 1: # keep every attempted dialect's diagnosis
|
|
692
|
+
errors = [e for failure in failures for e in failure["errors"]]
|
|
693
|
+
err = {"ok": False, "argument": "term", "errors": errors,
|
|
694
|
+
"spec_topic": _spec_topic_for(errors)}
|
|
695
|
+
if from_logic == "qml" and dialect is None:
|
|
696
|
+
# Quantified modal logic over SORTED quantifiers ('□∀x:Human …') is
|
|
697
|
+
# something the qml edge translates, but no single parse_any mode
|
|
698
|
+
# reads modal operators and sorts together — so only after the whole
|
|
699
|
+
# ladder has failed, and only for this source logic, try that one
|
|
700
|
+
# combination. A failure keeps the ladder's diagnostics.
|
|
701
|
+
from ..fol.msflparser import MSFLParser
|
|
702
|
+
|
|
703
|
+
try:
|
|
704
|
+
return MSFLParser(many_sorted=True, modal=True).parse(term), None
|
|
705
|
+
except Exception: # noqa: BLE001 - parser errors
|
|
706
|
+
pass
|
|
707
|
+
return None, err
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
@_answers_deep_input
|
|
711
|
+
def translate(term: str, from_logic: str, to_logic: str,
|
|
712
|
+
dialect: Optional[str] = None,
|
|
713
|
+
frame: Optional[str] = None,
|
|
714
|
+
systems: Optional[dict] = None,
|
|
715
|
+
temporal_closure: Optional[bool] = None,
|
|
716
|
+
signature: Optional[dict] = None,
|
|
717
|
+
mode: Optional[str] = None,
|
|
718
|
+
bridges: Optional[List[str]] = None) -> dict:
|
|
719
|
+
"""Translate between logics over the comorphism registry — and pass every entry of the returned ``axioms`` as a SEPARATE premise next to ``result`` (never conjoined onto it, never dropped), or the translated formula answers a different question and a valid formula comes back refuted.
|
|
720
|
+
|
|
721
|
+
The result is ``{result, unicode, axioms, axioms_unicode, guarantee,
|
|
722
|
+
source, target, path, lossy, note}``. ``result`` is the translated term
|
|
723
|
+
(JSON AST) and ``unicode`` its text; ``axioms`` are the side conditions of
|
|
724
|
+
the translation, already in the TARGET logic (``axioms_unicode`` is the
|
|
725
|
+
same list as text, entry for entry), e.g. the frame conditions of a modal
|
|
726
|
+
system, or for a many-sorted formula BOTH the non-emptiness of every sort
|
|
727
|
+
(an ``∃`` sentence about the sort ``Human``) AND the membership atom of
|
|
728
|
+
every sorted constant (``Human(socrates)``: a constant written
|
|
729
|
+
``socrates:Human`` is an element of ``Human``) -- a caller who passes only
|
|
730
|
+
the first answers a weaker question. To
|
|
731
|
+
decide a question about the translated formula, call ``prove`` with the
|
|
732
|
+
``unicode`` as the conclusion and ``axioms_unicode`` among the
|
|
733
|
+
``premises``; ``find_countermodel`` and ``check_consistency`` take them
|
|
734
|
+
the same way. ``guarantee`` is what the translation preserves ONCE those
|
|
735
|
+
axioms are added — ``faithful`` (every question transfers),
|
|
736
|
+
``validity`` (validity and entailment transfer), ``satisfiability`` (only
|
|
737
|
+
satisfiability: a validity answer through it means nothing), ``lossy``
|
|
738
|
+
(neither; ``note`` says what is dropped) — or ``null`` when an edge on the
|
|
739
|
+
path declares none, which is NOT the same as faithful. ``note`` carries
|
|
740
|
+
the conventions (e.g. the free world variable ``w`` a modal image is
|
|
741
|
+
anchored at). ``list_translations`` shows the logic labels, the edges and
|
|
742
|
+
the options each edge takes.
|
|
743
|
+
|
|
744
|
+
The term's PARSER follows the source logic's own term type: ``"casl"``
|
|
745
|
+
terms are CASL spec TEXT passed through verbatim (the dynamic
|
|
746
|
+
``hets:<Name>`` edges); ``"alc"`` terms are description-logic concept
|
|
747
|
+
text (``Human ⊓ ∃hasChild.Doctor``) parsed by the DL grammar —
|
|
748
|
+
review-confirmed: the registered alc→modal/alc→fol edges take
|
|
749
|
+
``Concept`` objects that no FOL-family grammar can produce; ``"drs"``
|
|
750
|
+
terms are discourse-representation boxes in the compact box notation
|
|
751
|
+
(``[x | Farmer(x), Runs(x)]``, as ``drs_to_fol`` takes); ``"fuzzy"`` terms
|
|
752
|
+
are read in the Łukasiewicz dialect (so ``⊕`` is the strong disjunction,
|
|
753
|
+
not Xor); every other source logic parses the term as a formula via
|
|
754
|
+
``parse_any`` (``"qml"`` additionally reads sorted quantifiers under
|
|
755
|
+
modal operators). Node/Concept results gain a ``"unicode"`` rendering; a
|
|
756
|
+
DRS result gains ``"box"``, its box notation. Bound variables the
|
|
757
|
+
translations name in a way the text grammar rejects are renamed ``q0``,
|
|
758
|
+
``q1``, … in the text renderings only, so every text here can be passed
|
|
759
|
+
back to the other tools.
|
|
760
|
+
|
|
761
|
+
Options are forwarded to the edges on the path that declare them and
|
|
762
|
+
omitted ones keep the edge's default: ``frame`` (modal system, default
|
|
763
|
+
``K``), ``systems`` (``{"epistemic": "S5"}``-style, per agent family) and
|
|
764
|
+
``temporal_closure`` for ``modal``/``qml`` sources; ``mode`` (domain
|
|
765
|
+
regime) and ``bridges`` (cross-family frame conditions) for ``qml``;
|
|
766
|
+
``signature`` (``{"subsorts": {"Human": ["Animal"]}}``) for ``msfol``,
|
|
767
|
+
where it adds one axiom per declared subsort edge. An option no edge on the
|
|
768
|
+
path takes, or an unknown frame / mode / bridge / family, is a structured
|
|
769
|
+
error naming what is accepted.
|
|
770
|
+
"""
|
|
771
|
+
try:
|
|
772
|
+
options = _translate_options(frame, systems, temporal_closure,
|
|
773
|
+
signature, mode, bridges)
|
|
774
|
+
except (ValueError, TypeError) as exc:
|
|
775
|
+
return _error(exc)
|
|
776
|
+
payload, err = _parse_term(term, from_logic, dialect)
|
|
777
|
+
if err is not None:
|
|
778
|
+
return err
|
|
779
|
+
from ..comorphism import DEFAULT_REGISTRY
|
|
780
|
+
|
|
781
|
+
try:
|
|
782
|
+
# The registry, not api.translate: that facade takes no options, and
|
|
783
|
+
# a translation that silently ignores frame= is the wrong question.
|
|
784
|
+
result = DEFAULT_REGISTRY.translate(payload, from_logic, to_logic,
|
|
785
|
+
**options)
|
|
786
|
+
except (ValueError, TypeError, NotImplementedError) as exc:
|
|
787
|
+
return _error(exc)
|
|
788
|
+
try:
|
|
789
|
+
rendered = result.to_dict()
|
|
790
|
+
text = _text_of_term(result.result)
|
|
791
|
+
if text is not None:
|
|
792
|
+
rendered["unicode"] = text
|
|
793
|
+
elif hasattr(result.result, "to_box_notation"): # drt.DRS
|
|
794
|
+
rendered["box"] = result.result.to_box_notation()
|
|
795
|
+
# Parallel to ``axioms`` (same length, same order); an axiom with no text
|
|
796
|
+
# form falls back to the repr that ``to_dict`` already gave it.
|
|
797
|
+
rendered["axioms_unicode"] = [_text_of_term(a) or repr(a)
|
|
798
|
+
for a in result.axioms]
|
|
799
|
+
except (ValueError, NotImplementedError) as exc:
|
|
800
|
+
# A result with no faithful text (a concept whose name reads back as another
|
|
801
|
+
# concept, see ``Concept.to_unicode``) is a refusal, not a crash.
|
|
802
|
+
return _error(exc)
|
|
803
|
+
return rendered
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
@_answers_deep_input
|
|
807
|
+
def verbalize(text: str, dialect: Optional[str] = None) -> dict:
|
|
808
|
+
"""Render a formula as deterministic English (fol.to_english)."""
|
|
809
|
+
from ..fol import to_english
|
|
810
|
+
|
|
811
|
+
node, err = _parse(text, dialect)
|
|
812
|
+
if err is not None:
|
|
813
|
+
return err
|
|
814
|
+
try:
|
|
815
|
+
return {"ok": True, "english": to_english(node)}
|
|
816
|
+
except (ValueError, NotImplementedError) as exc:
|
|
817
|
+
return _error(exc)
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
@_answers_deep_input
|
|
821
|
+
def list_backends() -> dict:
|
|
822
|
+
"""Registry introspection: what can decide, and what runs by default."""
|
|
823
|
+
return {
|
|
824
|
+
"registered": sorted(_REGISTRY),
|
|
825
|
+
"available": list(available_backends()),
|
|
826
|
+
"default_chains": {"fol": list(default_chain("fol")),
|
|
827
|
+
"modal": list(default_chain("modal"))},
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
# --------------------------------------------------------------------------
|
|
832
|
+
# Error-analysis / conversion layer (second tool wave)
|
|
833
|
+
# --------------------------------------------------------------------------
|
|
834
|
+
|
|
835
|
+
# form name -> (callable path, what the result means relative to the input).
|
|
836
|
+
# tseitin_cnf and skolemize deliberately do NOT claim equivalence — an agent
|
|
837
|
+
# that feeds the result back into prove() must know the difference.
|
|
838
|
+
_NORMALIZE_SEMANTICS = {
|
|
839
|
+
"nnf": "equivalent", "pnf": "equivalent", "cnf": "equivalent",
|
|
840
|
+
"dnf": "equivalent", "canonical": "equivalent",
|
|
841
|
+
"tseitin_cnf": "equisatisfiable",
|
|
842
|
+
"skolemize": "satisfiability-preserving",
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
@_answers_deep_input
|
|
847
|
+
def normalize(text: str, form: str = "nnf",
|
|
848
|
+
dialect: Optional[str] = None) -> dict:
|
|
849
|
+
"""Rewrite a formula into a normal form.
|
|
850
|
+
|
|
851
|
+
``form``: ``nnf`` / ``pnf`` / ``cnf`` / ``dnf`` (equivalence-preserving),
|
|
852
|
+
``canonical`` (the eval layer's comparison normal form: alpha-renaming,
|
|
853
|
+
commutativity/associativity, duplication, double negation quotiented
|
|
854
|
+
out), ``tseitin_cnf`` (EQUISATISFIABLE only — fresh definitional atoms),
|
|
855
|
+
``skolemize`` (satisfiability-preserving — existentials become Skolem
|
|
856
|
+
terms). The ``semantics`` key states which of those relations the result
|
|
857
|
+
bears to the input, and ``is_horn`` reports whether the input's clausal
|
|
858
|
+
form is Horn (``None`` where that computation is not applicable).
|
|
859
|
+
"""
|
|
860
|
+
from ..fol import normalforms
|
|
861
|
+
from ..eval import canonicalize as _canonicalize
|
|
862
|
+
|
|
863
|
+
node, err = _parse(text, dialect)
|
|
864
|
+
if err is not None:
|
|
865
|
+
return err
|
|
866
|
+
transforms = {
|
|
867
|
+
"nnf": normalforms.to_nnf, "pnf": normalforms.to_pnf,
|
|
868
|
+
"cnf": normalforms.to_cnf, "dnf": normalforms.to_dnf,
|
|
869
|
+
"tseitin_cnf": normalforms.to_tseitin_cnf,
|
|
870
|
+
"skolemize": normalforms.skolemize,
|
|
871
|
+
"canonical": _canonicalize,
|
|
872
|
+
}
|
|
873
|
+
if form not in transforms:
|
|
874
|
+
return _error(ValueError(
|
|
875
|
+
f"normalize: unknown form {form!r} (one of {sorted(transforms)})"))
|
|
876
|
+
try:
|
|
877
|
+
result = transforms[form](node)
|
|
878
|
+
except (ValueError, TypeError, NotImplementedError) as exc:
|
|
879
|
+
return _error(exc)
|
|
880
|
+
try:
|
|
881
|
+
horn = normalforms.is_horn(node)
|
|
882
|
+
except Exception:
|
|
883
|
+
horn = None # presentational extra, never fatal
|
|
884
|
+
return {"ok": True, "form": form,
|
|
885
|
+
"semantics": _NORMALIZE_SEMANTICS[form],
|
|
886
|
+
"unicode": result.to_unicode_str(),
|
|
887
|
+
"formula": result.to_dict(),
|
|
888
|
+
"is_horn": horn}
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
@_answers_deep_input
|
|
892
|
+
def render(text: str, to: str = "tptp",
|
|
893
|
+
dialect: Optional[str] = None) -> dict:
|
|
894
|
+
"""Render a formula in another concrete syntax.
|
|
895
|
+
|
|
896
|
+
``to``: ``unicode`` / ``tptp`` / ``prover9`` / ``latex`` / ``smtlib``
|
|
897
|
+
(a standalone SMT-LIB2 problem: ``(set-logic ...)``, the declaration
|
|
898
|
+
preamble, and one ``(assert ...)`` — no premises through this tool; use
|
|
899
|
+
``unicode_logic_kit.atp.z3_input.to_smtlib`` directly for an entailment
|
|
900
|
+
with premises) / ``casl`` (a bare CASL formula via ``formula_to_casl``)
|
|
901
|
+
/ ``json`` (the versioned ``serialize`` envelope — the only target whose
|
|
902
|
+
``rendered`` is a dict, not a string) / ``english`` (deterministic
|
|
903
|
+
verbalization). A family without the requested rendering surfaces its
|
|
904
|
+
own ``NotImplementedError``/``ValueError`` as a structured error — for
|
|
905
|
+
``smtlib`` this is ``to_z3``'s own refusal (second/third-order,
|
|
906
|
+
modal/hybrid/linear/Lambek/team constructs have no first-order SMT-LIB2
|
|
907
|
+
encoding), named by construct, reused rather than reimplemented.
|
|
908
|
+
|
|
909
|
+
A constant whose name is not a bare word is written in single quotes in
|
|
910
|
+
``unicode`` (``P('k2')``, ``Q('John Doe')``), and that text goes back into
|
|
911
|
+
every tool. ``latex`` writes a constant by its name, never in quotes, so
|
|
912
|
+
the LaTeX text of such a formula does not read back as that constant
|
|
913
|
+
(``P(k2)`` is the variable ``k2``), and the LaTeX reader refuses a quote:
|
|
914
|
+
pass the ``unicode`` text on. A target that cannot spell a name (TPTP and
|
|
915
|
+
Prover9 for ``John Doe``) refuses it as a structured error.
|
|
916
|
+
|
|
917
|
+
``tptp`` also refuses a formula in which two DISTINCT names of one kind
|
|
918
|
+
would be written as the same TPTP word (the constants ``θ`` and ``theta``,
|
|
919
|
+
or ``gaseous`` and ``Gaseous`` read from TPTP text: both fold to one
|
|
920
|
+
identifier, which would turn ``P(a) <-> P(b)`` into a tautology). The error
|
|
921
|
+
names both symbols and the shared word; rename one of them and render
|
|
922
|
+
again. It checks the one formula it renders — to build a problem from
|
|
923
|
+
several formulas use ``unicode_logic_kit.atp.generate_tptp_problem_with_mapping``,
|
|
924
|
+
which checks them together.
|
|
925
|
+
"""
|
|
926
|
+
node, err = _parse(text, dialect)
|
|
927
|
+
if err is not None:
|
|
928
|
+
return err
|
|
929
|
+
try:
|
|
930
|
+
if to == "unicode":
|
|
931
|
+
rendered = node.to_unicode_str()
|
|
932
|
+
elif to == "tptp":
|
|
933
|
+
rendered = node.to_tptp()
|
|
934
|
+
elif to == "prover9":
|
|
935
|
+
rendered = node.to_prover9()
|
|
936
|
+
elif to == "latex":
|
|
937
|
+
rendered = node.to_latex()
|
|
938
|
+
elif to == "smtlib":
|
|
939
|
+
rendered = node.to_smtlib()
|
|
940
|
+
elif to == "casl":
|
|
941
|
+
from ..fol.casl_export import formula_to_casl
|
|
942
|
+
rendered = formula_to_casl(node)
|
|
943
|
+
elif to == "json":
|
|
944
|
+
from ..fol.serialize import serialize
|
|
945
|
+
rendered = serialize(node)
|
|
946
|
+
elif to == "english":
|
|
947
|
+
from ..fol import to_english
|
|
948
|
+
rendered = to_english(node)
|
|
949
|
+
else:
|
|
950
|
+
return _error(ValueError(
|
|
951
|
+
f"render: unknown target {to!r} (one of ['casl', 'english', "
|
|
952
|
+
f"'json', 'latex', 'prover9', 'smtlib', 'tptp', 'unicode'])"))
|
|
953
|
+
except (ValueError, TypeError, NotImplementedError) as exc:
|
|
954
|
+
return _error(exc)
|
|
955
|
+
return {"ok": True, "to": to, "rendered": rendered}
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
@_answers_deep_input
|
|
959
|
+
def detect_dialect(text: str) -> dict:
|
|
960
|
+
"""What syntax does this text look like, and what does it parse as?
|
|
961
|
+
|
|
962
|
+
``candidates`` is the detector's ordered nomination list (always ending
|
|
963
|
+
in ``"unicode"``, the mode-ladder catch-all); ``parsed_as`` is the
|
|
964
|
+
dialect that actually accepted the text (``None`` if nothing did, with
|
|
965
|
+
every attempt's diagnostic in ``errors``).
|
|
966
|
+
"""
|
|
967
|
+
from ..fol.dialect_detect import detect_dialects
|
|
968
|
+
|
|
969
|
+
parsed = api.parse_any(text)
|
|
970
|
+
return {"ok": parsed.ok,
|
|
971
|
+
"candidates": list(detect_dialects(text)),
|
|
972
|
+
"parsed_as": parsed.dialect if parsed.ok else None,
|
|
973
|
+
"errors": [] if parsed.ok else parsed.to_dict()["errors"]}
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _vocabulary_diff(report_a, report_b) -> dict:
|
|
977
|
+
"""Per-namespace symbol diff between two ValidationReports."""
|
|
978
|
+
diff = {}
|
|
979
|
+
for section in ("predicates", "functions", "constants"):
|
|
980
|
+
a = set(getattr(report_a, section))
|
|
981
|
+
b = set(getattr(report_b, section))
|
|
982
|
+
diff[section] = {"only_in_predicted": sorted(a - b),
|
|
983
|
+
"only_in_gold": sorted(b - a),
|
|
984
|
+
"shared": sorted(a & b)}
|
|
985
|
+
return diff
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
@_answers_deep_input
|
|
989
|
+
def compare_formulas(predicted: str, gold: str, timeout_ms: int = 10000,
|
|
990
|
+
dialect: Optional[str] = None,
|
|
991
|
+
converses: Optional[List[dict]] = None) -> dict:
|
|
992
|
+
"""The full prediction-vs-gold error-analysis breakdown for one pair.
|
|
993
|
+
|
|
994
|
+
Layers, strictest first: ``structural_equal`` (raw AST equality),
|
|
995
|
+
``canonical_exact_match`` (alpha-renaming, commutativity/associativity,
|
|
996
|
+
duplication, double negation quotiented out),
|
|
997
|
+
``aligned_exact_match`` (canonical match after Levenshtein-guided,
|
|
998
|
+
namespace- and arity-aware symbol renaming; ``aligned_predicted`` shows
|
|
999
|
+
the renamed prediction), ``equivalence`` (the graded ladder's full
|
|
1000
|
+
verdict dict, solver level included). ``vocabulary`` lists the symbols
|
|
1001
|
+
(``Name/arity`` for predicates/functions) each side uses and the other
|
|
1002
|
+
does not — the usual first stop when a match fails.
|
|
1003
|
+
|
|
1004
|
+
``converses`` OPTIONALLY declares argument-permutation bridging axioms
|
|
1005
|
+
— e.g. ``LovedBy(x, y) ↔ Loves(y, x)`` — honoured ONLY by the solver
|
|
1006
|
+
level inside ``equivalence`` (see
|
|
1007
|
+
:func:`unicode_logic_kit.eval.equivalence.equivalent`'s ``converses``
|
|
1008
|
+
parameter and :mod:`unicode_logic_kit.eval.converses`); each entry is
|
|
1009
|
+
``{"a": [name, arity], "b": [name, arity], "permutation": [...]}``, e.g.
|
|
1010
|
+
``{"a": ["LovedBy", 2], "b": ["Loves", 2], "permutation": [1, 0]}``. A
|
|
1011
|
+
malformed entry (missing key, wrong shape) comes back as the top-level
|
|
1012
|
+
``{"error": {...}}`` shape; a structurally invalid declaration (bad
|
|
1013
|
+
arity/permutation, self-pair, …) instead lands inside
|
|
1014
|
+
``equivalence.error`` — the same place any other ``ValueError`` from the
|
|
1015
|
+
equivalence call surfaces — since it is only caught once the axioms are
|
|
1016
|
+
actually built. A modal ``predicted``/``gold`` pair with non-empty
|
|
1017
|
+
``converses`` also lands in ``equivalence.error`` (``equivalent()``
|
|
1018
|
+
raises ``NotImplementedError`` there — no modal bridging route exists —
|
|
1019
|
+
which this tool catches alongside ``ValueError``, never lets escape as a
|
|
1020
|
+
raw exception). ``converse_axioms_applied`` lists the axioms that were
|
|
1021
|
+
actually built (unicode-rendered, e.g.
|
|
1022
|
+
``"∀v0 ∀v1 (LovedBy(v0, v1) ↔ Loves(v1, v0))"``), or is ``None`` when no
|
|
1023
|
+
``converses`` were given.
|
|
1024
|
+
"""
|
|
1025
|
+
from ..eval import (exact_match, align_symbols, aligned_exact_match)
|
|
1026
|
+
from ..eval.validate import validate
|
|
1027
|
+
|
|
1028
|
+
pred, err = _parse(predicted, dialect, argument="predicted")
|
|
1029
|
+
if err is not None:
|
|
1030
|
+
return err
|
|
1031
|
+
ref, err = _parse(gold, dialect, argument="gold")
|
|
1032
|
+
if err is not None:
|
|
1033
|
+
return err
|
|
1034
|
+
|
|
1035
|
+
converses_tuples = None
|
|
1036
|
+
if converses:
|
|
1037
|
+
try:
|
|
1038
|
+
converses_tuples = _normalize_converses(converses)
|
|
1039
|
+
except ValueError as exc:
|
|
1040
|
+
return _error(exc)
|
|
1041
|
+
|
|
1042
|
+
try:
|
|
1043
|
+
aligned = align_symbols(pred, ref)
|
|
1044
|
+
aligned_unicode = aligned.to_unicode_str()
|
|
1045
|
+
aligned_ok = aligned_exact_match(pred, ref)
|
|
1046
|
+
except (ValueError, NotImplementedError):
|
|
1047
|
+
aligned_unicode = None # family without alignment support
|
|
1048
|
+
aligned_ok = None
|
|
1049
|
+
|
|
1050
|
+
axioms_applied = None
|
|
1051
|
+
try:
|
|
1052
|
+
if converses_tuples:
|
|
1053
|
+
from ..eval.converses import converse_axioms
|
|
1054
|
+
axioms_applied = [ax.to_unicode_str()
|
|
1055
|
+
for ax in converse_axioms(converses_tuples)]
|
|
1056
|
+
equivalence = api.equivalent(pred, ref, timeout=timeout_ms,
|
|
1057
|
+
converses=converses_tuples).to_dict()
|
|
1058
|
+
except (ValueError, NotImplementedError) as exc:
|
|
1059
|
+
# NotImplementedError: a modal pair with a non-empty `converses` --
|
|
1060
|
+
# equivalent() raises that deliberately (no modal bridging route
|
|
1061
|
+
# exists), and it must land in the same structured `equivalence.error`
|
|
1062
|
+
# shape as a ValueError, not escape as a raw exception over the wire.
|
|
1063
|
+
equivalence = {"error": str(exc)}
|
|
1064
|
+
return {
|
|
1065
|
+
"ok": True,
|
|
1066
|
+
"structural_equal": pred == ref,
|
|
1067
|
+
"canonical_exact_match": exact_match(pred, ref),
|
|
1068
|
+
"aligned_exact_match": aligned_ok,
|
|
1069
|
+
"aligned_predicted": aligned_unicode,
|
|
1070
|
+
"equivalence": equivalence,
|
|
1071
|
+
"vocabulary": _vocabulary_diff(validate(pred), validate(ref)),
|
|
1072
|
+
"converse_axioms_applied": axioms_applied,
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
@_answers_deep_input
|
|
1077
|
+
def score_batch(predictions: List[str], references: List[str],
|
|
1078
|
+
method: str = "auto", timeout_ms: int = 10000,
|
|
1079
|
+
converses: Optional[List[dict]] = None) -> dict:
|
|
1080
|
+
"""Corpus-level NL->FOL metrics over aligned prediction/reference lists.
|
|
1081
|
+
|
|
1082
|
+
The six-key dict of ``eval.compute_fol_metrics``: ``exact_match``,
|
|
1083
|
+
``equivalence_accuracy`` (honest — undecided pairs are NOT counted as
|
|
1084
|
+
refuted), ``mean_partial_credit``, ``parse_failure_rate``,
|
|
1085
|
+
``solver_unknown_rate``, ``n``. ``converses`` (same JSON shape as
|
|
1086
|
+
``compare_formulas``'s own parameter — see its docstring) is forwarded
|
|
1087
|
+
to every pair; when non-empty the dict gains a seventh key,
|
|
1088
|
+
``converse_matched_rate`` — the fraction of pairs the solver proved
|
|
1089
|
+
equivalent USING the declared axioms, separately visible from (and
|
|
1090
|
+
subtractable out of) ``equivalence_accuracy``. A malformed or
|
|
1091
|
+
structurally invalid ``converses`` declaration, or a modal pair in the
|
|
1092
|
+
batch hit with non-empty ``converses`` (``NotImplementedError`` — no
|
|
1093
|
+
modal bridging route exists), surfaces as the top-level ``{"error":
|
|
1094
|
+
{...}}`` shape rather than escaping as a raw exception.
|
|
1095
|
+
"""
|
|
1096
|
+
from ..eval import compute_fol_metrics
|
|
1097
|
+
|
|
1098
|
+
try:
|
|
1099
|
+
converses_tuples = _normalize_converses(converses) if converses else None
|
|
1100
|
+
return {"ok": True,
|
|
1101
|
+
**compute_fol_metrics(predictions, references,
|
|
1102
|
+
method=method, timeout_ms=timeout_ms,
|
|
1103
|
+
converses=converses_tuples)}
|
|
1104
|
+
except (ValueError, NotImplementedError) as exc:
|
|
1105
|
+
# NotImplementedError: same modal+converses case compare_formulas
|
|
1106
|
+
# guards against above -- must return the tool's structured error
|
|
1107
|
+
# shape (_error), never escape as a raw exception over the wire.
|
|
1108
|
+
return _error(exc)
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
@_answers_deep_input
|
|
1112
|
+
def check_consistency(formulas: List[str], logic: str = "auto",
|
|
1113
|
+
timeout_ms: int = 10000,
|
|
1114
|
+
dialect: Optional[str] = None) -> dict:
|
|
1115
|
+
"""Is this SET of formulas jointly satisfiable?
|
|
1116
|
+
|
|
1117
|
+
Encoding: a fresh nullary atom ``q`` (guaranteed absent from the input
|
|
1118
|
+
vocabulary) gives the contradiction ``q ∧ ¬q``; classically (and under
|
|
1119
|
+
the kit's local-consequence modal reading) the set is unsatisfiable iff
|
|
1120
|
+
it entails that contradiction, and a countermodel to that entailment IS
|
|
1121
|
+
a model of the set. Verdicts: ``consistent=True`` carries the model
|
|
1122
|
+
witness + English gloss (``method="model"``), ``consistent=False``
|
|
1123
|
+
carries the refutation verdict (``method="refutation"``),
|
|
1124
|
+
``consistent=None`` means both searches came back empty within the
|
|
1125
|
+
budgets (``method="inconclusive"`` — never a claim either way).
|
|
1126
|
+
"""
|
|
1127
|
+
from ..fol.nodes import Atom, Not, And as _And
|
|
1128
|
+
from ..fol._identifiers import symbol_names
|
|
1129
|
+
|
|
1130
|
+
parsed = []
|
|
1131
|
+
for i, f in enumerate(formulas or []):
|
|
1132
|
+
node, err = _parse(f, dialect, argument=f"formula[{i}]")
|
|
1133
|
+
if err is not None:
|
|
1134
|
+
return err
|
|
1135
|
+
parsed.append(node)
|
|
1136
|
+
|
|
1137
|
+
# Fresh against EVERY name of the problem, of every kind (a predicate, a
|
|
1138
|
+
# constant, a sort, ...): a backend may keep them in one namespace.
|
|
1139
|
+
used = symbol_names(*parsed)
|
|
1140
|
+
fresh = "ufk_absurd"
|
|
1141
|
+
while fresh in used:
|
|
1142
|
+
fresh += "_"
|
|
1143
|
+
contradiction = _And(Atom(fresh, ()), Not(Atom(fresh, ())))
|
|
1144
|
+
|
|
1145
|
+
try:
|
|
1146
|
+
witness = api.countermodel(contradiction, parsed, logic=logic,
|
|
1147
|
+
timeout=timeout_ms)
|
|
1148
|
+
if witness.found:
|
|
1149
|
+
return {"ok": True, "consistent": True, "method": "model",
|
|
1150
|
+
"model": witness.model, "backend": witness.backend,
|
|
1151
|
+
"explanation_nl": witness.explanation_nl}
|
|
1152
|
+
verdict = api.prove(contradiction, parsed, logic=logic,
|
|
1153
|
+
timeout=timeout_ms)
|
|
1154
|
+
except (BackendUnavailable, ValueError) as exc:
|
|
1155
|
+
return _error(exc)
|
|
1156
|
+
if verdict.status == PROVED:
|
|
1157
|
+
return {"ok": True, "consistent": False, "method": "refutation",
|
|
1158
|
+
"verdict": verdict.to_dict()}
|
|
1159
|
+
return {"ok": True, "consistent": None, "method": "inconclusive",
|
|
1160
|
+
"verdict": verdict.to_dict()}
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
@_answers_deep_input
|
|
1164
|
+
def get_signature(formulas: List[str],
|
|
1165
|
+
dialect: Optional[str] = None) -> dict:
|
|
1166
|
+
"""Extract the inferred vocabulary (Signature) of a formula set.
|
|
1167
|
+
|
|
1168
|
+
The result dict is ``fol.Signature.from_formulas(...)``'s rich form —
|
|
1169
|
+
predicates/functions with arities and inferred sorts, constants, sort
|
|
1170
|
+
names — ready to pass back as ``check_formula``'s / ``diagnose``'s
|
|
1171
|
+
``signature`` argument to hold FURTHER generations to this vocabulary:
|
|
1172
|
+
the whole result, ``{"ok": True, "signature": {...}}``, or just its
|
|
1173
|
+
``signature`` value are both accepted.
|
|
1174
|
+
"""
|
|
1175
|
+
from ..fol.signature import Signature
|
|
1176
|
+
|
|
1177
|
+
parsed = []
|
|
1178
|
+
for i, f in enumerate(formulas or []):
|
|
1179
|
+
node, err = _parse(f, dialect, argument=f"formula[{i}]")
|
|
1180
|
+
if err is not None:
|
|
1181
|
+
return err
|
|
1182
|
+
parsed.append(node)
|
|
1183
|
+
try:
|
|
1184
|
+
return {"ok": True,
|
|
1185
|
+
"signature": Signature.from_formulas(parsed).to_dict()}
|
|
1186
|
+
except (ValueError, NotImplementedError) as exc:
|
|
1187
|
+
return _error(exc)
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
# Enumerating value_count**atom_count rows must not melt the transport: the
|
|
1191
|
+
# cap bounds the ROW COUNT (4096 = 12 classical or ~7 three-valued atoms).
|
|
1192
|
+
_TRUTH_TABLE_MAX_ROWS = 4096
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
@_answers_deep_input
|
|
1196
|
+
def truth_table(text: str, logic: str = "classical",
|
|
1197
|
+
dialect: Optional[str] = None) -> dict:
|
|
1198
|
+
"""Decide a propositional formula by full enumeration.
|
|
1199
|
+
|
|
1200
|
+
``logic``: ``classical`` (values {0,1}), ``K3`` (strong Kleene) or
|
|
1201
|
+
``LP`` (Priest, paraconsistent designation). Quantified formulas and
|
|
1202
|
+
tables beyond 4096 rows are refused as structured errors. ``rows``
|
|
1203
|
+
aligns each assignment with ``atoms``; ``markdown`` is the rendered
|
|
1204
|
+
table for direct display.
|
|
1205
|
+
"""
|
|
1206
|
+
from ..semantics.truthtable import (
|
|
1207
|
+
truth_table as _truth_table, _collect_atoms, _VALUES)
|
|
1208
|
+
|
|
1209
|
+
node, err = _parse(text, dialect)
|
|
1210
|
+
if err is not None:
|
|
1211
|
+
return err
|
|
1212
|
+
if logic not in _VALUES:
|
|
1213
|
+
return _error(ValueError(
|
|
1214
|
+
f"truth_table: unknown logic {logic!r} "
|
|
1215
|
+
f"(one of {sorted(_VALUES)})"))
|
|
1216
|
+
try:
|
|
1217
|
+
atoms = _collect_atoms(node) # rejects quantified formulas
|
|
1218
|
+
except ValueError as exc:
|
|
1219
|
+
return _error(exc)
|
|
1220
|
+
n_rows = len(_VALUES[logic]) ** len(atoms)
|
|
1221
|
+
if n_rows > _TRUTH_TABLE_MAX_ROWS:
|
|
1222
|
+
return _error(ValueError(
|
|
1223
|
+
f"truth_table: {len(atoms)} atoms give {n_rows} rows under "
|
|
1224
|
+
f"{logic} (cap {_TRUTH_TABLE_MAX_ROWS}); use prove or "
|
|
1225
|
+
f"find_countermodel instead"))
|
|
1226
|
+
try:
|
|
1227
|
+
# _collect_atoms only rejects QUANTIFIERS; a modal/temporal/fuzzy/
|
|
1228
|
+
# lambda node walks through it and only the evaluator refuses it
|
|
1229
|
+
# (NotImplementedError/TypeError from the Kleene tables) — that
|
|
1230
|
+
# refusal is part of the documented contract and must surface as a
|
|
1231
|
+
# structured error, not a traceback (review-hardened).
|
|
1232
|
+
tt = _truth_table(node, logic=logic)
|
|
1233
|
+
except (ValueError, TypeError, NotImplementedError) as exc:
|
|
1234
|
+
return _error(exc)
|
|
1235
|
+
return {"ok": True, "logic": tt.logic, "atoms": list(tt.atoms),
|
|
1236
|
+
"rows": [{"assignment": list(assignment), "value": value,
|
|
1237
|
+
"designated": designated}
|
|
1238
|
+
for assignment, value, designated in tt.rows],
|
|
1239
|
+
"is_tautology": tt.is_tautology,
|
|
1240
|
+
"is_contradiction": tt.is_contradiction,
|
|
1241
|
+
"is_satisfiable": tt.is_satisfiable,
|
|
1242
|
+
"markdown": tt.render()}
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
@_answers_deep_input
|
|
1246
|
+
def drs_to_fol(text: str, format: str = "box",
|
|
1247
|
+
resolve_pronouns: bool = False) -> dict:
|
|
1248
|
+
"""Translate a discourse representation structure into provable FOL.
|
|
1249
|
+
|
|
1250
|
+
``format="box"`` parses the compact box notation
|
|
1251
|
+
(``[x, y | Farmer(x), Donkey(y), Owns(x, y)] -> [ | Beats(x, y)]``),
|
|
1252
|
+
``format="sbn"`` the documented Parallel-Meaning-Bank SBN subset.
|
|
1253
|
+
``resolve_pronouns=True`` runs accessibility-respecting anaphora
|
|
1254
|
+
resolution first (PRONOUN-marked referents; ambiguity/no-candidate
|
|
1255
|
+
failures surface as structured errors) and reports each resolution.
|
|
1256
|
+
The result is the standard translation — donkey-sentence universals
|
|
1257
|
+
come out right — as a formula ``prove``/``check_formula`` accept.
|
|
1258
|
+
"""
|
|
1259
|
+
from .. import drt
|
|
1260
|
+
|
|
1261
|
+
if format == "box":
|
|
1262
|
+
parse, error_type = drt.parse_drs, drt.DRSSyntaxError
|
|
1263
|
+
elif format == "sbn":
|
|
1264
|
+
parse, error_type = drt.parse_sbn, drt.SBNSyntaxError
|
|
1265
|
+
else:
|
|
1266
|
+
return _error(ValueError(
|
|
1267
|
+
f"drs_to_fol: unknown format {format!r} (one of ['box', 'sbn'])"))
|
|
1268
|
+
try:
|
|
1269
|
+
box = parse(text)
|
|
1270
|
+
except (error_type, ValueError) as exc:
|
|
1271
|
+
return {"ok": False, "argument": "text",
|
|
1272
|
+
"errors": [{"dialect": f"drs_{format}", "message": str(exc)}]}
|
|
1273
|
+
|
|
1274
|
+
resolutions = None
|
|
1275
|
+
if resolve_pronouns:
|
|
1276
|
+
try:
|
|
1277
|
+
report = drt.resolve_anaphora(box)
|
|
1278
|
+
except ValueError as exc:
|
|
1279
|
+
return _error(exc)
|
|
1280
|
+
box = report.drs
|
|
1281
|
+
resolutions = [r.to_dict() for r in report.resolutions]
|
|
1282
|
+
try:
|
|
1283
|
+
node = drt.drs_to_fol(box)
|
|
1284
|
+
except (ValueError, NotImplementedError) as exc:
|
|
1285
|
+
return _error(exc)
|
|
1286
|
+
result = {"ok": True, "unicode": node.to_unicode_str(),
|
|
1287
|
+
"formula": node.to_dict()}
|
|
1288
|
+
if resolutions is not None:
|
|
1289
|
+
result["resolutions"] = resolutions
|
|
1290
|
+
return result
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
def _exact_fraction(value, where: str):
|
|
1294
|
+
"""Coerce a JSON-transported probability to an exact Fraction.
|
|
1295
|
+
|
|
1296
|
+
ints and strings go straight to ``Fraction`` (``"7/10"`` and ``"0.7"``
|
|
1297
|
+
are both exact); a float is read through its shortest-repr DECIMAL
|
|
1298
|
+
(``0.7`` → ``Fraction("0.7")`` = 7/10 — what the JSON author wrote, not
|
|
1299
|
+
the binary artefact ``Fraction(0.7)`` would preserve). The prob layer
|
|
1300
|
+
itself refuses floats outright; this adapter exists because JSON has no
|
|
1301
|
+
rational type. Booleans are refused (bool is an int subclass in Python,
|
|
1302
|
+
but a JSON ``true`` is not a probability — silently reading it as 1
|
|
1303
|
+
would be the quiet coercion this kit never does). Raises ValueError
|
|
1304
|
+
with ``where`` on everything else.
|
|
1305
|
+
"""
|
|
1306
|
+
from fractions import Fraction
|
|
1307
|
+
|
|
1308
|
+
try:
|
|
1309
|
+
if isinstance(value, float):
|
|
1310
|
+
return Fraction(repr(value))
|
|
1311
|
+
if isinstance(value, (int, str)) and not isinstance(value, bool):
|
|
1312
|
+
return Fraction(value)
|
|
1313
|
+
except (ValueError, ZeroDivisionError) as exc:
|
|
1314
|
+
raise ValueError(f"{where}: not a probability: {value!r} ({exc})")
|
|
1315
|
+
raise ValueError(f"{where}: expected int, string or number, got "
|
|
1316
|
+
f"{type(value).__name__}")
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
@_answers_deep_input
|
|
1320
|
+
def probability_bounds(conclusion: str, constraints: List[dict],
|
|
1321
|
+
max_atoms: int = 12,
|
|
1322
|
+
dialect: Optional[str] = None,
|
|
1323
|
+
strategy: str = "direct",
|
|
1324
|
+
max_columns: int = 500) -> dict:
|
|
1325
|
+
"""Nilsson-style probabilistic entailment: tightest bounds on P(conclusion).
|
|
1326
|
+
|
|
1327
|
+
Each constraint dict: ``{"formula": <text>}`` plus either
|
|
1328
|
+
``"probability"`` (pins the value exactly) or ``"lower"``/``"upper"``
|
|
1329
|
+
(interval; missing ends default to 0/1), plus optional ``"given"``
|
|
1330
|
+
(formula text — makes it the conditional ``P(formula | given)``).
|
|
1331
|
+
Probabilities travel as ``"7/10"`` / ``"0.7"`` strings, ints, or JSON
|
|
1332
|
+
numbers (read decimally). Propositional only; the answer is the EXACT
|
|
1333
|
+
``[lower, upper]`` interval (fraction strings, with float shadows for
|
|
1334
|
+
convenience) entailed by the constraint polytope — ``lower == upper ==
|
|
1335
|
+
1`` is classical entailment as a corner case.
|
|
1336
|
+
|
|
1337
|
+
``strategy`` picks the solving ALGORITHM, never the semantics (see
|
|
1338
|
+
:func:`unicode_logic_kit.prob.nilsson.entailment_bounds`): ``"direct"``
|
|
1339
|
+
(the default, unchanged) enumerates all ``2^n`` worlds and is capped
|
|
1340
|
+
by ``max_atoms`` (12 by default); ``"column_generation"`` never
|
|
1341
|
+
materialises that many worlds, so it can go past ``max_atoms`` —
|
|
1342
|
+
its own brake is ``max_columns`` (500 by default, raising a
|
|
1343
|
+
structured error rather than ever returning an unproven bound).
|
|
1344
|
+
Both strategies solve the identical linear program and agree
|
|
1345
|
+
exactly (never a tolerance) wherever both can answer.
|
|
1346
|
+
"""
|
|
1347
|
+
from ..prob import ProbConstraint, entailment_bounds
|
|
1348
|
+
|
|
1349
|
+
if strategy not in ("direct", "column_generation"):
|
|
1350
|
+
return _error(ValueError(
|
|
1351
|
+
f"probability_bounds: unknown strategy {strategy!r} "
|
|
1352
|
+
"(one of ['direct', 'column_generation'])"))
|
|
1353
|
+
|
|
1354
|
+
node, err = _parse(conclusion, dialect, argument="conclusion")
|
|
1355
|
+
if err is not None:
|
|
1356
|
+
return err
|
|
1357
|
+
parsed = []
|
|
1358
|
+
try:
|
|
1359
|
+
for i, c in enumerate(constraints or []):
|
|
1360
|
+
if "formula" not in c:
|
|
1361
|
+
return _error(ValueError(
|
|
1362
|
+
f"constraints[{i}]: missing the 'formula' key"))
|
|
1363
|
+
fnode, ferr = _parse(c["formula"], dialect,
|
|
1364
|
+
argument=f"constraints[{i}].formula")
|
|
1365
|
+
if ferr is not None:
|
|
1366
|
+
return ferr
|
|
1367
|
+
given = None
|
|
1368
|
+
if c.get("given") is not None:
|
|
1369
|
+
given, gerr = _parse(c["given"], dialect,
|
|
1370
|
+
argument=f"constraints[{i}].given")
|
|
1371
|
+
if gerr is not None:
|
|
1372
|
+
return gerr
|
|
1373
|
+
if "probability" in c:
|
|
1374
|
+
lo = hi = _exact_fraction(c["probability"],
|
|
1375
|
+
f"constraints[{i}].probability")
|
|
1376
|
+
else:
|
|
1377
|
+
lo = _exact_fraction(c.get("lower", 0),
|
|
1378
|
+
f"constraints[{i}].lower")
|
|
1379
|
+
hi = _exact_fraction(c.get("upper", 1),
|
|
1380
|
+
f"constraints[{i}].upper")
|
|
1381
|
+
parsed.append(ProbConstraint(fnode, lo, hi, given))
|
|
1382
|
+
bounds = entailment_bounds(parsed, node, max_atoms=max_atoms,
|
|
1383
|
+
strategy=strategy,
|
|
1384
|
+
max_columns=max_columns)
|
|
1385
|
+
except (ValueError, TypeError) as exc:
|
|
1386
|
+
return _error(exc)
|
|
1387
|
+
return {"ok": True, **bounds.to_dict(),
|
|
1388
|
+
"lower_float": float(bounds.lower),
|
|
1389
|
+
"upper_float": float(bounds.upper)}
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
@_answers_deep_input
|
|
1393
|
+
def probability_query(goal: str, facts: List[dict],
|
|
1394
|
+
rules: Optional[List[str]] = None,
|
|
1395
|
+
hard_facts: Optional[List[str]] = None,
|
|
1396
|
+
max_choice_facts: int = 16,
|
|
1397
|
+
dialect: Optional[str] = None) -> dict:
|
|
1398
|
+
"""Exact ProbLog-style query under Sato's distribution semantics.
|
|
1399
|
+
|
|
1400
|
+
``facts``: ``{"atom": <ground atom text>, "prob": <"3/10" | 0.3 | …>}``
|
|
1401
|
+
— independent Bernoulli facts. ``rules``/``hard_facts``: definite
|
|
1402
|
+
clauses (a ground atom, or ``∀``-quantified ``body → head`` with a
|
|
1403
|
+
positive conjunctive body and a single positive head atom — anything
|
|
1404
|
+
else is refused loudly). The goal may use ∧/∨/¬ and ∀/∃ over the
|
|
1405
|
+
program's finite constants; negation reads closed-world against each
|
|
1406
|
+
total choice's least model (the ProbLog convention). The result is the
|
|
1407
|
+
exact probability as a fraction string (float shadow included).
|
|
1408
|
+
"""
|
|
1409
|
+
from ..prob import ProbFact, ProbProgram, query as _prob_query
|
|
1410
|
+
|
|
1411
|
+
gnode, err = _parse(goal, dialect, argument="goal")
|
|
1412
|
+
if err is not None:
|
|
1413
|
+
return err
|
|
1414
|
+
try:
|
|
1415
|
+
prob_facts = []
|
|
1416
|
+
for i, f in enumerate(facts or []):
|
|
1417
|
+
if "atom" not in f or "prob" not in f:
|
|
1418
|
+
return _error(ValueError(
|
|
1419
|
+
f"facts[{i}]: needs both 'atom' and 'prob' keys"))
|
|
1420
|
+
anode, aerr = _parse(f["atom"], dialect,
|
|
1421
|
+
argument=f"facts[{i}].atom")
|
|
1422
|
+
if aerr is not None:
|
|
1423
|
+
return aerr
|
|
1424
|
+
prob_facts.append(ProbFact(
|
|
1425
|
+
anode, _exact_fraction(f["prob"], f"facts[{i}].prob")))
|
|
1426
|
+
rule_nodes = []
|
|
1427
|
+
for i, r in enumerate(rules or []):
|
|
1428
|
+
rnode, rerr = _parse(r, dialect, argument=f"rules[{i}]")
|
|
1429
|
+
if rerr is not None:
|
|
1430
|
+
return rerr
|
|
1431
|
+
rule_nodes.append(rnode)
|
|
1432
|
+
hard_nodes = []
|
|
1433
|
+
for i, h in enumerate(hard_facts or []):
|
|
1434
|
+
hnode, herr = _parse(h, dialect, argument=f"hard_facts[{i}]")
|
|
1435
|
+
if herr is not None:
|
|
1436
|
+
return herr
|
|
1437
|
+
hard_nodes.append(hnode)
|
|
1438
|
+
program = ProbProgram(prob_facts, rule_nodes, hard_nodes)
|
|
1439
|
+
p = _prob_query(program, gnode, max_choice_facts=max_choice_facts)
|
|
1440
|
+
except (ValueError, TypeError) as exc:
|
|
1441
|
+
return _error(exc)
|
|
1442
|
+
return {"ok": True, "probability": str(p), "probability_float": float(p)}
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
@_answers_deep_input
|
|
1446
|
+
def get_syntax_spec(topic: str = "overview",
|
|
1447
|
+
dialect: Optional[str] = None) -> dict:
|
|
1448
|
+
"""Retrieve the kit's syntax specification — look up the rule you broke.
|
|
1449
|
+
|
|
1450
|
+
Topics: ``overview`` (dialects and the mistake everyone makes), ``naming``
|
|
1451
|
+
(what makes a symbol a variable / constant / predicate / function, and how
|
|
1452
|
+
TPTP inverts the convention), ``dialects``, ``operators`` (precedence
|
|
1453
|
+
table), ``quantifiers`` (scope rules), ``counting`` (the cardinality
|
|
1454
|
+
quantifier and why it replaces long existential chains), ``chemistry``
|
|
1455
|
+
(molecule-as-structure signature), ``description-logic`` (the ALC glyph
|
|
1456
|
+
and OWL Manchester concept syntaxes the ``dl_*`` tools accept, plus their
|
|
1457
|
+
TBox/ABox JSON row shapes), ``errors`` (measured LLM failure modes,
|
|
1458
|
+
each with a fix and the topic that explains it).
|
|
1459
|
+
|
|
1460
|
+
Every parse failure this server returns carries a ``spec_topic`` naming
|
|
1461
|
+
the topic to fetch before retrying, so a generate → fail → look up → fix
|
|
1462
|
+
loop needs no grammar in the prompt. Every example served here is parsed
|
|
1463
|
+
and rendering-checked by the kit's own test suite, so the spec cannot
|
|
1464
|
+
drift from the parser.
|
|
1465
|
+
"""
|
|
1466
|
+
from .syntax_spec import syntax_spec
|
|
1467
|
+
|
|
1468
|
+
try:
|
|
1469
|
+
return {"ok": True, **syntax_spec(topic, dialect)}
|
|
1470
|
+
except ValueError as exc:
|
|
1471
|
+
return _error(exc)
|
|
1472
|
+
|
|
1473
|
+
|
|
1474
|
+
@_answers_deep_input
|
|
1475
|
+
def list_translations() -> dict:
|
|
1476
|
+
"""Enumerate the logic-to-logic edges the translate tool can follow.
|
|
1477
|
+
|
|
1478
|
+
The kit's own comorphism registry (BFS-composable); after a Hets bridge
|
|
1479
|
+
refresh the dynamic ``hets:<Name>`` edges appear here too. ``logics`` is
|
|
1480
|
+
every label ``translate`` accepts as ``from_logic`` / ``to_logic``.
|
|
1481
|
+
``lossy`` edges do not preserve the full source semantics; ``note``
|
|
1482
|
+
carries the conventions a consumer must know. ``guarantee`` is what the
|
|
1483
|
+
edge preserves (``faithful`` / ``validity`` / ``satisfiability`` /
|
|
1484
|
+
``lossy``, or ``null`` when the edge declares none — not the same as
|
|
1485
|
+
faithful). ``options`` are the ``translate`` parameters the edge reads
|
|
1486
|
+
(``frame``, ``systems``, ``temporal_closure``, ``signature``, ``mode``,
|
|
1487
|
+
``bridges``), and ``side_axioms`` says whether the edge can return
|
|
1488
|
+
``axioms`` that must be passed as separate premises.
|
|
1489
|
+
"""
|
|
1490
|
+
from ..comorphism import DEFAULT_REGISTRY
|
|
1491
|
+
|
|
1492
|
+
edges = DEFAULT_REGISTRY.edges()
|
|
1493
|
+
return {"logics": sorted({label for e in edges
|
|
1494
|
+
for label in (e.source, e.target)}),
|
|
1495
|
+
"edges": [{"name": e.name, "source": e.source,
|
|
1496
|
+
"target": e.target, "lossy": e.lossy, "note": e.note,
|
|
1497
|
+
"guarantee": e.guarantee,
|
|
1498
|
+
"options": sorted(e.options),
|
|
1499
|
+
"side_axioms": e.axioms is not None}
|
|
1500
|
+
for e in edges]}
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
# --------------------------------------------------------------------------
|
|
1504
|
+
# Description-logic (ALCHQ) reasoning tools — pure wiring over dl.tableau /
|
|
1505
|
+
# dl.classification, with input parsed by dl.parser (the ALC glyph syntax,
|
|
1506
|
+
# ``syntax="alc"``, default) or dl.owl_manchester (OWL 2 Manchester Syntax,
|
|
1507
|
+
# ``syntax="manchester"``). Conventions match every tool above: a bad
|
|
1508
|
+
# CONCEPT/AXIOM text is the uniform ``{"ok": False, "argument": ...,
|
|
1509
|
+
# "errors": [...], "spec_topic": "description-logic"}`` shape (whether the
|
|
1510
|
+
# grammar rejected it as :class:`~unicode_logic_kit.dl.ConceptSyntaxError` or
|
|
1511
|
+
# :class:`~unicode_logic_kit.dl.ManchesterSyntaxError` — including a
|
|
1512
|
+
# Manchester construct outside ALCHQ, e.g. ``Self``/``inverse``/a
|
|
1513
|
+
# nominal, which that parser rejects by NAME rather than a bare syntax
|
|
1514
|
+
# error; a ``value`` restriction (``hasChild value Doctor``) is READ, and is
|
|
1515
|
+
# then the tableau's refusal below, ``UnsupportedConceptError``, because it is
|
|
1516
|
+
# a nominal in disguise); a documented, non-text exception — every refusal class the ``dl``
|
|
1517
|
+
# package raises on purpose (:func:`_dl_errors`: a qualified number
|
|
1518
|
+
# restriction on a non-simple role, an axiom KIND / concept / datatype the
|
|
1519
|
+
# tableau does not decide, a malformed role) or the tableau's step-budget
|
|
1520
|
+
# ``RuntimeError`` — is ``{"error": {"type": ..., "message": ...}}``. A
|
|
1521
|
+
# :class:`Concept` has no ``to_dict()`` (see ``dl.concepts``), so results
|
|
1522
|
+
# carry it as ``*_unicode`` text (``Concept.to_unicode()``) rather than a
|
|
1523
|
+
# JSON AST, exactly like ``translate()``'s own ``alc`` branch above. A concept
|
|
1524
|
+
# one of whose names would make that text read back as ANOTHER concept (a class
|
|
1525
|
+
# named ``<A⊓B>``) has no faithful text: ``to_unicode()`` refuses it with a
|
|
1526
|
+
# ``ValueError``, and the tool answers with the same structured error
|
|
1527
|
+
# (:func:`_unicode_texts`) before it reasons, never with the exception.
|
|
1528
|
+
# --------------------------------------------------------------------------
|
|
1529
|
+
|
|
1530
|
+
def _check_dl_syntax(syntax: str):
|
|
1531
|
+
"""Validate ``syntax`` against the two dialects DL tools understand.
|
|
1532
|
+
|
|
1533
|
+
``None`` on success. Otherwise the structured ``{"error": {...}}`` shape
|
|
1534
|
+
(a caller/config mistake, not a bad concept TEXT). Callers whose
|
|
1535
|
+
row-building loops may run zero iterations (an empty/absent
|
|
1536
|
+
``tbox``/``abox``, so :func:`_parse_dl` is never reached) MUST call this
|
|
1537
|
+
unconditionally up front, or an invalid ``syntax`` is silently accepted
|
|
1538
|
+
instead of refused.
|
|
1539
|
+
"""
|
|
1540
|
+
if syntax in ("alc", "manchester"):
|
|
1541
|
+
return None
|
|
1542
|
+
return _error(ValueError(
|
|
1543
|
+
f"dl: unknown syntax {syntax!r} (one of ['alc', 'manchester'])"))
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
def _dl_errors():
|
|
1547
|
+
"""The exceptions a description-logic tool turns into an ``{"error": ...}``
|
|
1548
|
+
payload instead of letting them escape: the tableau's decidability refusal
|
|
1549
|
+
(a number restriction on a non-simple role), the NAMED refusals of a
|
|
1550
|
+
fragment it does not decide (an axiom kind, a concept, a datatype), the
|
|
1551
|
+
refusal of a malformed role (:class:`~unicode_logic_kit.dl.RoleExpressionError`,
|
|
1552
|
+
which the role builders raise and which a query-time check of a role name
|
|
1553
|
+
raises too) and the ``RuntimeError`` of an exhausted resource budget.
|
|
1554
|
+
|
|
1555
|
+
ONE place, shared by every ``dl_*`` reasoning tool: a refusal class the
|
|
1556
|
+
``dl`` package gains is added HERE and is then reported by all of them.
|
|
1557
|
+
``tests/test_mcp_server.py`` classifies every exception class the package
|
|
1558
|
+
defines against this tuple, so a class added there and forgotten here fails
|
|
1559
|
+
that test instead of reaching a caller as a bare exception.
|
|
1560
|
+
|
|
1561
|
+
The syntax errors of the two readers (``ConceptSyntaxError``,
|
|
1562
|
+
``ManchesterSyntaxError``) are deliberately NOT here: they are text
|
|
1563
|
+
mistakes, reported by :func:`_parse_dl` in the uniform ``ok=False`` shape.
|
|
1564
|
+
``dl`` is imported here, not at module level, like everywhere else in this
|
|
1565
|
+
file."""
|
|
1566
|
+
from .. import dl
|
|
1567
|
+
|
|
1568
|
+
return (dl.NonSimpleRoleError, dl.UnsupportedAxiomError,
|
|
1569
|
+
dl.UnsupportedConceptError, dl.UnsupportedDatatypeError,
|
|
1570
|
+
dl.RoleExpressionError, RuntimeError)
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
def _dl_datatype_names(rows) -> List[str]:
|
|
1574
|
+
"""The names a ``tbox`` row list DEFINES as datatypes (its ``{"datatype":
|
|
1575
|
+
name, "definition": ...}`` rows), in order.
|
|
1576
|
+
|
|
1577
|
+
A user-defined datatype is an ordinary name, so the Manchester reader can
|
|
1578
|
+
tell ``d some Digit`` (data) from ``r some Dog`` (object) only if it is
|
|
1579
|
+
told ``Digit`` is a datatype; every tool that reads concept text next to a
|
|
1580
|
+
``tbox`` passes these names on. A built-in datatype (``xsd:integer``) needs
|
|
1581
|
+
no listing."""
|
|
1582
|
+
return [row["datatype"] for row in rows or []
|
|
1583
|
+
if isinstance(row, dict) and isinstance(row.get("datatype"), str)]
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
def _parse_dl(text: str, syntax: str, argument: str, datatypes=()):
|
|
1587
|
+
"""Parse concept TEXT into a :class:`~unicode_logic_kit.dl.Concept`.
|
|
1588
|
+
|
|
1589
|
+
``syntax="alc"`` (default) reads the glyph syntax via ``dl.parse_concept``;
|
|
1590
|
+
``syntax="manchester"`` reads OWL 2 Manchester Syntax via
|
|
1591
|
+
``dl.parse_manchester``. ``(concept, None)`` on success, ``(None,
|
|
1592
|
+
error_dict)`` on failure — see this section's own header comment for the
|
|
1593
|
+
two error shapes. ``datatypes``: the user-defined datatype names (see
|
|
1594
|
+
:func:`_dl_datatype_names`), used by the Manchester reader only — the glyph
|
|
1595
|
+
syntax has no data layer, so it cannot say a data restriction at all.
|
|
1596
|
+
"""
|
|
1597
|
+
from .. import dl
|
|
1598
|
+
|
|
1599
|
+
if not isinstance(text, str):
|
|
1600
|
+
# A malformed row shape, like every other non-str below: reported, not
|
|
1601
|
+
# raised (a JSON number in a concept slot used to escape as a TypeError
|
|
1602
|
+
# from inside the reader).
|
|
1603
|
+
return None, _error(ValueError(
|
|
1604
|
+
f"{argument}: expected concept text (str), got {type(text).__name__}"))
|
|
1605
|
+
if syntax == "alc":
|
|
1606
|
+
try:
|
|
1607
|
+
return dl.parse_concept(text), None
|
|
1608
|
+
except dl.ConceptSyntaxError as exc:
|
|
1609
|
+
return None, {"ok": False, "argument": argument,
|
|
1610
|
+
"errors": [{"dialect": "alc", "message": str(exc)}],
|
|
1611
|
+
"spec_topic": "description-logic"}
|
|
1612
|
+
if syntax == "manchester":
|
|
1613
|
+
try:
|
|
1614
|
+
return dl.parse_manchester(text, datatypes=datatypes), None
|
|
1615
|
+
except dl.ManchesterSyntaxError as exc:
|
|
1616
|
+
return None, {"ok": False, "argument": argument,
|
|
1617
|
+
"errors": [{"dialect": "manchester", "message": str(exc)}],
|
|
1618
|
+
"spec_topic": "description-logic"}
|
|
1619
|
+
return None, _check_dl_syntax(syntax)
|
|
1620
|
+
|
|
1621
|
+
|
|
1622
|
+
def _parse_dl_data_range(text, argument: str, datatypes=()):
|
|
1623
|
+
"""Parse DATA RANGE text (always Manchester: the glyph syntax has no data
|
|
1624
|
+
layer) into a :class:`~unicode_logic_kit.dl.datatypes.DataRange`.
|
|
1625
|
+
``(range, None)`` / ``(None, error_dict)`` like :func:`_parse_dl`."""
|
|
1626
|
+
from .. import dl
|
|
1627
|
+
|
|
1628
|
+
if not isinstance(text, str):
|
|
1629
|
+
return None, _error(ValueError(
|
|
1630
|
+
f"{argument}: expected data range text (str), got {type(text).__name__}"))
|
|
1631
|
+
try:
|
|
1632
|
+
return dl.parse_manchester_data_range(text, datatypes=datatypes), None
|
|
1633
|
+
except dl.ManchesterSyntaxError as exc:
|
|
1634
|
+
return None, {"ok": False, "argument": argument,
|
|
1635
|
+
"errors": [{"dialect": "manchester", "message": str(exc)}],
|
|
1636
|
+
"spec_topic": "description-logic"}
|
|
1637
|
+
|
|
1638
|
+
|
|
1639
|
+
def _parse_dl_literal(text, argument: str):
|
|
1640
|
+
"""Parse one LITERAL text (``"400"^^xsd:integer``, ``"abc"@en``, a bare
|
|
1641
|
+
numeral) into a :class:`~unicode_logic_kit.dl.datatypes.Literal`.
|
|
1642
|
+
``(literal, None)`` / ``(None, error_dict)`` like :func:`_parse_dl`."""
|
|
1643
|
+
from .. import dl
|
|
1644
|
+
|
|
1645
|
+
if not isinstance(text, str):
|
|
1646
|
+
return None, _error(ValueError(
|
|
1647
|
+
f"{argument}: expected literal text (str), got {type(text).__name__}"))
|
|
1648
|
+
try:
|
|
1649
|
+
return dl.parse_manchester_literal(text), None
|
|
1650
|
+
except dl.ManchesterSyntaxError as exc:
|
|
1651
|
+
return None, {"ok": False, "argument": argument,
|
|
1652
|
+
"errors": [{"dialect": "manchester", "message": str(exc)}],
|
|
1653
|
+
"spec_topic": "description-logic"}
|
|
1654
|
+
|
|
1655
|
+
|
|
1656
|
+
#: ``row key -> (TBox builder, how many roles the value holds)`` for the
|
|
1657
|
+
#: role-box row shapes that take ONE key. ``"one"`` is a single role name
|
|
1658
|
+
#: (every characteristic axiom), ``"many"`` a list of role names.
|
|
1659
|
+
_DL_ROLE_ROW_SHAPES = {
|
|
1660
|
+
"transitive": ("add_transitive_role", "one"),
|
|
1661
|
+
"symmetric": ("add_symmetric_role", "one"),
|
|
1662
|
+
"asymmetric": ("add_asymmetric_role", "one"),
|
|
1663
|
+
"reflexive": ("add_reflexive_role", "one"),
|
|
1664
|
+
"irreflexive": ("add_irreflexive_role", "one"),
|
|
1665
|
+
"functional": ("add_functional_role", "one"),
|
|
1666
|
+
"inversefunctional": ("add_inverse_functional_role", "one"),
|
|
1667
|
+
"inverseroles": ("add_inverse_roles", "many"),
|
|
1668
|
+
"disjointroles": ("add_disjoint_roles", "many"),
|
|
1669
|
+
"equivroles": ("add_equivalent_roles", "many"),
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
#: ``row key -> (TBox builder, OWL keyword)`` for the two role-box axioms
|
|
1673
|
+
#: whose value is a ROLE plus a CLASS EXPRESSION: ``{"domainrole": r,
|
|
1674
|
+
#: "domain": <concept text>}`` and its ``range`` twin. A table of their own
|
|
1675
|
+
#: because the concept text has to go through :func:`_parse_dl` under the
|
|
1676
|
+
#: caller's ``syntax``, which no role-name row needs.
|
|
1677
|
+
_DL_FILLER_ROLE_ROW_SHAPES = {
|
|
1678
|
+
"domain": ("add_role_domain", "domainrole", "ObjectPropertyDomain"),
|
|
1679
|
+
"range": ("add_role_range", "rangerole", "ObjectPropertyRange"),
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
|
|
1683
|
+
#: ``row key -> (TBox builder, how many property names the value holds)`` for
|
|
1684
|
+
#: the data-box row shapes that take ONE key of property names, the data twin of
|
|
1685
|
+
#: :data:`_DL_ROLE_ROW_SHAPES`.
|
|
1686
|
+
_DL_DATA_NAME_ROW_SHAPES = {
|
|
1687
|
+
"equivdata": ("add_equivalent_data_properties", "many"),
|
|
1688
|
+
"disjointdata": ("add_disjoint_data_properties", "many"),
|
|
1689
|
+
"functionaldata": ("add_functional_data_property", "one"),
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
#: Every key that marks a row as a DATA-box row. Checked BEFORE the role rows:
|
|
1693
|
+
#: ``{"domaindata": d, "domain": text}`` carries the key ``"domain"``, which the
|
|
1694
|
+
#: role-domain branch would otherwise claim and reject for lacking ``domainrole``.
|
|
1695
|
+
_DL_DATA_ROW_KEYS = ("subdata", "domaindata", "rangedata", "datatype",
|
|
1696
|
+
*_DL_DATA_NAME_ROW_SHAPES)
|
|
1697
|
+
|
|
1698
|
+
|
|
1699
|
+
def _add_dl_data_row(tbox, row, i: int, syntax: str, argument: str, datatypes):
|
|
1700
|
+
"""Add the data-box row ``row`` (the ``i``-th) to ``tbox``; ``None`` on
|
|
1701
|
+
success, an error dict on the first failure (same two shapes as
|
|
1702
|
+
:func:`_build_dl_tbox`)."""
|
|
1703
|
+
from .. import dl
|
|
1704
|
+
|
|
1705
|
+
where = f"{argument}[{i}]"
|
|
1706
|
+
names = [key for key in _DL_DATA_ROW_KEYS if key in row]
|
|
1707
|
+
if len(names) != 1:
|
|
1708
|
+
return _error(ValueError(
|
|
1709
|
+
f"{where}: a data-box row carries exactly one of {sorted(_DL_DATA_ROW_KEYS)}, "
|
|
1710
|
+
f"got {names}"))
|
|
1711
|
+
key = names[0]
|
|
1712
|
+
try:
|
|
1713
|
+
if key == "subdata":
|
|
1714
|
+
if "supdata" not in row:
|
|
1715
|
+
return _error(ValueError(
|
|
1716
|
+
f"{where}: a 'subdata' row needs 'supdata', got {sorted(row)}"))
|
|
1717
|
+
tbox.add_data_property_inclusion(row["subdata"], row["supdata"])
|
|
1718
|
+
elif key == "domaindata":
|
|
1719
|
+
if "domain" not in row:
|
|
1720
|
+
return _error(ValueError(
|
|
1721
|
+
f"{where}: a 'domaindata' row needs 'domain' (the class text), "
|
|
1722
|
+
f"got {sorted(row)}"))
|
|
1723
|
+
concept, err = _parse_dl(row["domain"], syntax, f"{where}.domain", datatypes)
|
|
1724
|
+
if err is not None:
|
|
1725
|
+
return err
|
|
1726
|
+
tbox.add_data_property_domain(row["domaindata"], concept)
|
|
1727
|
+
elif key == "rangedata":
|
|
1728
|
+
if "range" not in row:
|
|
1729
|
+
return _error(ValueError(
|
|
1730
|
+
f"{where}: a 'rangedata' row needs 'range' (the data range text), "
|
|
1731
|
+
f"got {sorted(row)}"))
|
|
1732
|
+
datarange, err = _parse_dl_data_range(row["range"], f"{where}.range", datatypes)
|
|
1733
|
+
if err is not None:
|
|
1734
|
+
return err
|
|
1735
|
+
tbox.add_data_property_range(row["rangedata"], datarange)
|
|
1736
|
+
elif key == "datatype":
|
|
1737
|
+
if "definition" not in row:
|
|
1738
|
+
return _error(ValueError(
|
|
1739
|
+
f"{where}: a 'datatype' row needs 'definition' (the data range "
|
|
1740
|
+
f"text), got {sorted(row)}"))
|
|
1741
|
+
if not isinstance(row["datatype"], str):
|
|
1742
|
+
return _error(ValueError(
|
|
1743
|
+
f"{where}.datatype: expected a datatype name (str), got "
|
|
1744
|
+
f"{row['datatype']!r}"))
|
|
1745
|
+
datarange, err = _parse_dl_data_range(
|
|
1746
|
+
row["definition"], f"{where}.definition", datatypes)
|
|
1747
|
+
if err is not None:
|
|
1748
|
+
return err
|
|
1749
|
+
tbox.add_datatype_definition(row["datatype"], datarange)
|
|
1750
|
+
else:
|
|
1751
|
+
builder, arity = _DL_DATA_NAME_ROW_SHAPES[key]
|
|
1752
|
+
value = row[key]
|
|
1753
|
+
if arity == "one":
|
|
1754
|
+
if not isinstance(value, str):
|
|
1755
|
+
return _error(ValueError(
|
|
1756
|
+
f"{where}.{key}: expected a data property name (str), "
|
|
1757
|
+
f"got {value!r}"))
|
|
1758
|
+
names_ = [value]
|
|
1759
|
+
else:
|
|
1760
|
+
if not (isinstance(value, list) and len(value) >= 2
|
|
1761
|
+
and all(isinstance(name, str) for name in value)):
|
|
1762
|
+
return _error(ValueError(
|
|
1763
|
+
f"{where}.{key}: expected a list of at least 2 data "
|
|
1764
|
+
f"property names, got {value!r}"))
|
|
1765
|
+
names_ = value
|
|
1766
|
+
getattr(tbox, builder)(*names_)
|
|
1767
|
+
except (dl.RoleExpressionError, dl.UnsupportedDatatypeError) as exc:
|
|
1768
|
+
return _error(exc)
|
|
1769
|
+
return None
|
|
1770
|
+
|
|
1771
|
+
|
|
1772
|
+
def _build_dl_tbox(rows, syntax: str, argument: str = "tbox"):
|
|
1773
|
+
"""Build a :class:`~unicode_logic_kit.dl.TBox` from JSON row dicts.
|
|
1774
|
+
|
|
1775
|
+
Each row is one of ``TBox``'s axiom shapes. The concept-level two:
|
|
1776
|
+
|
|
1777
|
+
* ``{"sub": <text>, "sup": <text>}`` — a general concept inclusion
|
|
1778
|
+
(``TBox.add``);
|
|
1779
|
+
* ``{"equiv": [<text>, <text>]}`` — an equivalence (``TBox.add_equivalence``).
|
|
1780
|
+
|
|
1781
|
+
The role box (see "Role hierarchies and transitive roles (RBox)" and "The
|
|
1782
|
+
rest of the OWL 2 role box" in :mod:`unicode_logic_kit.dl.tableau`'s module
|
|
1783
|
+
docstring; the role values are NAMES, never concept text, so ``syntax``
|
|
1784
|
+
does not apply to them):
|
|
1785
|
+
|
|
1786
|
+
* ``{"subrole": <role>, "suprole": <role>}`` — a role inclusion;
|
|
1787
|
+
* ``{"chain": [<role>, …], "suprole": <role>}`` — a property chain
|
|
1788
|
+
(``TBox.add_role_chain``), checked BEFORE ``subrole`` so the two
|
|
1789
|
+
``suprole`` shapes cannot be confused;
|
|
1790
|
+
* ``{"inverseroles": [p, q]}``, ``{"disjointroles": [p, q, …]}``,
|
|
1791
|
+
``{"equivroles": [p, q, …]}`` — the n-ary role axioms;
|
|
1792
|
+
* ``{"transitive": <role>}`` and its six siblings ``symmetric``,
|
|
1793
|
+
``asymmetric``, ``reflexive``, ``irreflexive``, ``functional``,
|
|
1794
|
+
``inversefunctional`` — the characteristic axioms;
|
|
1795
|
+
* ``{"domainrole": <role>, "domain": <text>}`` and
|
|
1796
|
+
``{"rangerole": <role>, "range": <text>}`` — the two role-box axioms
|
|
1797
|
+
whose right-hand side is a CLASS EXPRESSION (``TBox.add_role_domain`` /
|
|
1798
|
+
``add_role_range``), so that text IS parsed under ``syntax``.
|
|
1799
|
+
|
|
1800
|
+
The data box (see "The data layer" in :mod:`unicode_logic_kit.dl.translate`'s
|
|
1801
|
+
module docstring; property and datatype values are NAMES, and the data range
|
|
1802
|
+
text is ALWAYS OWL 2 Manchester syntax, since the glyph syntax has no data
|
|
1803
|
+
layer):
|
|
1804
|
+
|
|
1805
|
+
* ``{"subdata": <prop>, "supdata": <prop>}`` — ``SubDataPropertyOf``;
|
|
1806
|
+
* ``{"equivdata": [p, q, …]}`` and ``{"disjointdata": [p, q, …]}`` — the
|
|
1807
|
+
n-ary data property axioms;
|
|
1808
|
+
* ``{"functionaldata": <prop>}`` — ``FunctionalDataProperty``;
|
|
1809
|
+
* ``{"domaindata": <prop>, "domain": <class text>}`` — ``DataPropertyDomain``
|
|
1810
|
+
(the class text is parsed under ``syntax``);
|
|
1811
|
+
* ``{"rangedata": <prop>, "range": <data range text>}`` —
|
|
1812
|
+
``DataPropertyRange``;
|
|
1813
|
+
* ``{"datatype": <name>, "definition": <data range text>}`` —
|
|
1814
|
+
``DatatypeDefinition``. The name is then a datatype in every other row's
|
|
1815
|
+
text, which is how ``d some Digit`` is told from ``r some Dog``.
|
|
1816
|
+
|
|
1817
|
+
A row is accepted even when the in-house tableau refuses to REASON over
|
|
1818
|
+
that kind: which kinds it decides is recorded in ``dl.tableau._AXIOM_KINDS``
|
|
1819
|
+
and enforced at query time, and the tool then reports that refusal rather
|
|
1820
|
+
than silently answering about a weaker knowledge base.
|
|
1821
|
+
|
|
1822
|
+
``rows`` ``None``/``[]`` is the empty TBox. ``(tbox, None)`` on success,
|
|
1823
|
+
``(None, error_dict)`` on the first failure: concept TEXT inside a row is
|
|
1824
|
+
parsed with :func:`_parse_dl` under the same ``syntax`` (the uniform
|
|
1825
|
+
``ok=False`` shape); a malformed row SHAPE (missing/unrecognised keys, a
|
|
1826
|
+
non-2-element ``equiv``, a role name that is not a string, an OWL 2
|
|
1827
|
+
built-in role name) is a caller/config mistake, reported as
|
|
1828
|
+
``{"error": {...}}``.
|
|
1829
|
+
"""
|
|
1830
|
+
from .. import dl
|
|
1831
|
+
|
|
1832
|
+
tbox = dl.TBox()
|
|
1833
|
+
datatypes = _dl_datatype_names(rows)
|
|
1834
|
+
for i, row in enumerate(rows or []):
|
|
1835
|
+
if not isinstance(row, dict):
|
|
1836
|
+
return None, _error(ValueError(
|
|
1837
|
+
f"{argument}[{i}]: expected an object, got {type(row).__name__}"))
|
|
1838
|
+
if "sub" in row and "sup" in row:
|
|
1839
|
+
sub, err = _parse_dl(row["sub"], syntax, f"{argument}[{i}].sub", datatypes)
|
|
1840
|
+
if err is not None:
|
|
1841
|
+
return None, err
|
|
1842
|
+
sup, err = _parse_dl(row["sup"], syntax, f"{argument}[{i}].sup", datatypes)
|
|
1843
|
+
if err is not None:
|
|
1844
|
+
return None, err
|
|
1845
|
+
tbox.add(sub, sup)
|
|
1846
|
+
elif "equiv" in row:
|
|
1847
|
+
pair = row["equiv"]
|
|
1848
|
+
if not (isinstance(pair, list) and len(pair) == 2):
|
|
1849
|
+
return None, _error(ValueError(
|
|
1850
|
+
f"{argument}[{i}].equiv: expected a 2-element list, got {pair!r}"))
|
|
1851
|
+
c, err = _parse_dl(pair[0], syntax, f"{argument}[{i}].equiv[0]", datatypes)
|
|
1852
|
+
if err is not None:
|
|
1853
|
+
return None, err
|
|
1854
|
+
d, err = _parse_dl(pair[1], syntax, f"{argument}[{i}].equiv[1]", datatypes)
|
|
1855
|
+
if err is not None:
|
|
1856
|
+
return None, err
|
|
1857
|
+
tbox.add_equivalence(c, d)
|
|
1858
|
+
elif any(key in row for key in _DL_DATA_ROW_KEYS):
|
|
1859
|
+
err = _add_dl_data_row(tbox, row, i, syntax, argument, datatypes)
|
|
1860
|
+
if err is not None:
|
|
1861
|
+
return None, err
|
|
1862
|
+
elif "chain" in row and "suprole" in row:
|
|
1863
|
+
chain = row["chain"]
|
|
1864
|
+
if not (isinstance(chain, list) and len(chain) >= 2
|
|
1865
|
+
and all(isinstance(role, str) for role in chain)):
|
|
1866
|
+
return None, _error(ValueError(
|
|
1867
|
+
f"{argument}[{i}].chain: expected a list of at least 2 role "
|
|
1868
|
+
f"names, got {chain!r}"))
|
|
1869
|
+
try:
|
|
1870
|
+
tbox.add_role_chain(chain, row["suprole"])
|
|
1871
|
+
except dl.RoleExpressionError as exc:
|
|
1872
|
+
return None, _error(exc)
|
|
1873
|
+
elif "subrole" in row and "suprole" in row:
|
|
1874
|
+
try:
|
|
1875
|
+
tbox.add_role_inclusion(row["subrole"], row["suprole"])
|
|
1876
|
+
except dl.RoleExpressionError as exc:
|
|
1877
|
+
return None, _error(exc)
|
|
1878
|
+
elif any(key in row for key in _DL_FILLER_ROLE_ROW_SHAPES):
|
|
1879
|
+
key = next(k for k in _DL_FILLER_ROLE_ROW_SHAPES if k in row)
|
|
1880
|
+
builder, role_key, _keyword = _DL_FILLER_ROLE_ROW_SHAPES[key]
|
|
1881
|
+
if role_key not in row:
|
|
1882
|
+
return None, _error(ValueError(
|
|
1883
|
+
f"{argument}[{i}]: a {key!r} row needs {role_key!r} "
|
|
1884
|
+
f"(the role the axiom is about), got {sorted(row)}"))
|
|
1885
|
+
filler, err = _parse_dl(row[key], syntax, f"{argument}[{i}].{key}",
|
|
1886
|
+
datatypes)
|
|
1887
|
+
if err is not None:
|
|
1888
|
+
return None, err
|
|
1889
|
+
try:
|
|
1890
|
+
getattr(tbox, builder)(row[role_key], filler)
|
|
1891
|
+
except dl.RoleExpressionError as exc:
|
|
1892
|
+
return None, _error(exc)
|
|
1893
|
+
else:
|
|
1894
|
+
key = next((k for k in _DL_ROLE_ROW_SHAPES if k in row), None)
|
|
1895
|
+
if key is None:
|
|
1896
|
+
return None, _error(ValueError(
|
|
1897
|
+
f"{argument}[{i}]: expected keys 'sub'+'sup', 'equiv', "
|
|
1898
|
+
f"'subrole'+'suprole', 'chain'+'suprole', "
|
|
1899
|
+
f"'domainrole'+'domain', 'rangerole'+'range', "
|
|
1900
|
+
f"'subdata'+'supdata', 'domaindata'+'domain', "
|
|
1901
|
+
f"'rangedata'+'range', 'datatype'+'definition', or one of "
|
|
1902
|
+
f"{sorted(_DL_ROLE_ROW_SHAPES)} / "
|
|
1903
|
+
f"{sorted(_DL_DATA_NAME_ROW_SHAPES)}, got {sorted(row)}"))
|
|
1904
|
+
builder, arity = _DL_ROLE_ROW_SHAPES[key]
|
|
1905
|
+
value = row[key]
|
|
1906
|
+
if arity == "one":
|
|
1907
|
+
if not isinstance(value, str):
|
|
1908
|
+
return None, _error(ValueError(
|
|
1909
|
+
f"{argument}[{i}].{key}: expected a role name (str), "
|
|
1910
|
+
f"got {value!r}"))
|
|
1911
|
+
roles = [value]
|
|
1912
|
+
else:
|
|
1913
|
+
if not (isinstance(value, list) and len(value) >= 2
|
|
1914
|
+
and all(isinstance(role, str) for role in value)):
|
|
1915
|
+
return None, _error(ValueError(
|
|
1916
|
+
f"{argument}[{i}].{key}: expected a list of at least 2 "
|
|
1917
|
+
f"role names, got {value!r}"))
|
|
1918
|
+
roles = value
|
|
1919
|
+
try:
|
|
1920
|
+
getattr(tbox, builder)(*roles)
|
|
1921
|
+
except dl.RoleExpressionError as exc:
|
|
1922
|
+
return None, _error(exc)
|
|
1923
|
+
return tbox, None
|
|
1924
|
+
|
|
1925
|
+
|
|
1926
|
+
def _build_dl_abox(concepts, roles, distinct, syntax: str,
|
|
1927
|
+
same=None, negative_roles=None, data=None,
|
|
1928
|
+
negative_data=None, datatypes=()):
|
|
1929
|
+
"""Build a :class:`~unicode_logic_kit.dl.ABox` from JSON rows.
|
|
1930
|
+
|
|
1931
|
+
``concepts``: ``[individual, concept_text]`` pairs (``ABox.assert_concept``).
|
|
1932
|
+
``roles``: ``[a, b, role]`` triples (``ABox.assert_role``). ``distinct``:
|
|
1933
|
+
``[a, b]`` pairs (``ABox.assert_distinct`` — the only thing that forces two
|
|
1934
|
+
individuals apart, since this reasoner has no unique name assumption; see
|
|
1935
|
+
:mod:`unicode_logic_kit.dl.tableau`'s "Qualified number restrictions"
|
|
1936
|
+
section). ``same``: ``[a, b]`` pairs (``ABox.assert_same`` — the mirror of
|
|
1937
|
+
``distinct``, decided by node merging). ``negative_roles``: ``[a, b, role]``
|
|
1938
|
+
triples (``ABox.assert_negative_role`` — ``¬role(a, b)``). The last two are
|
|
1939
|
+
optional; without them the MCP description-logic tools could express a
|
|
1940
|
+
strictly smaller class of knowledge bases than the Python API. The same
|
|
1941
|
+
holds for ``data`` and ``negative_data``: ``[individual, property, literal]``
|
|
1942
|
+
triples (``ABox.assert_data`` / ``assert_negative_data``), the literal being
|
|
1943
|
+
Manchester literal text (``"400"^^xsd:integer``, ``"abc"@en``, a bare
|
|
1944
|
+
numeral). ``datatypes``: the user-defined datatype names the concept texts
|
|
1945
|
+
may mention (see :func:`_dl_datatype_names`).
|
|
1946
|
+
``(abox, None)`` on success, ``(None, error_dict)`` on the first
|
|
1947
|
+
failure: a concept TEXT failure is the uniform ``ok=False`` shape
|
|
1948
|
+
(argument ``"concepts[i][1]"``); a malformed row shape (a row of the wrong
|
|
1949
|
+
length, or an individual, role or property name that is not a string) is
|
|
1950
|
+
``{"error": {...}}``, and so is a role or data property name an ABox
|
|
1951
|
+
builder refuses by name (:class:`~unicode_logic_kit.dl.RoleExpressionError`,
|
|
1952
|
+
:class:`~unicode_logic_kit.dl.UnsupportedDatatypeError`).
|
|
1953
|
+
"""
|
|
1954
|
+
from .. import dl
|
|
1955
|
+
|
|
1956
|
+
try:
|
|
1957
|
+
return _dl_abox_from_rows(concepts, roles, distinct, syntax, same,
|
|
1958
|
+
negative_roles, data, negative_data, datatypes)
|
|
1959
|
+
except (dl.RoleExpressionError, dl.UnsupportedDatatypeError) as exc:
|
|
1960
|
+
return None, _error(exc)
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
def _dl_abox_from_rows(concepts, roles, distinct, syntax, same, negative_roles,
|
|
1964
|
+
data, negative_data, datatypes):
|
|
1965
|
+
"""The body of :func:`_build_dl_abox`, which adds the one ``try`` around it."""
|
|
1966
|
+
from .. import dl
|
|
1967
|
+
|
|
1968
|
+
abox = dl.ABox()
|
|
1969
|
+
for i, pair in enumerate(concepts or []):
|
|
1970
|
+
if not (isinstance(pair, list) and len(pair) == 2):
|
|
1971
|
+
return None, _error(ValueError(
|
|
1972
|
+
f"concepts[{i}]: expected [individual, concept_text], got {pair!r}"))
|
|
1973
|
+
individual, text = pair
|
|
1974
|
+
if not isinstance(individual, str):
|
|
1975
|
+
return None, _error(ValueError(
|
|
1976
|
+
f"concepts[{i}][0]: individual name must be a str, got "
|
|
1977
|
+
f"{type(individual).__name__}"))
|
|
1978
|
+
concept, err = _parse_dl(text, syntax, f"concepts[{i}][1]", datatypes)
|
|
1979
|
+
if err is not None:
|
|
1980
|
+
return None, err
|
|
1981
|
+
abox.assert_concept(individual, concept)
|
|
1982
|
+
for i, triple in enumerate(roles or []):
|
|
1983
|
+
if not (isinstance(triple, list) and len(triple) == 3
|
|
1984
|
+
and all(isinstance(name, str) for name in triple)):
|
|
1985
|
+
return None, _error(ValueError(
|
|
1986
|
+
f"roles[{i}]: expected [a, b, role] (three strings), got {triple!r}"))
|
|
1987
|
+
a, b, role = triple
|
|
1988
|
+
abox.assert_role(a, b, role)
|
|
1989
|
+
for i, pair in enumerate(distinct or []):
|
|
1990
|
+
if not (isinstance(pair, list) and len(pair) == 2
|
|
1991
|
+
and all(isinstance(name, str) for name in pair)):
|
|
1992
|
+
return None, _error(ValueError(
|
|
1993
|
+
f"distinct[{i}]: expected [a, b] (two strings), got {pair!r}"))
|
|
1994
|
+
a, b = pair
|
|
1995
|
+
abox.assert_distinct(a, b)
|
|
1996
|
+
for i, pair in enumerate(same or []):
|
|
1997
|
+
if not (isinstance(pair, list) and len(pair) == 2
|
|
1998
|
+
and all(isinstance(name, str) for name in pair)):
|
|
1999
|
+
return None, _error(ValueError(
|
|
2000
|
+
f"same[{i}]: expected [a, b] (two strings), got {pair!r}"))
|
|
2001
|
+
a, b = pair
|
|
2002
|
+
abox.assert_same(a, b)
|
|
2003
|
+
for i, triple in enumerate(negative_roles or []):
|
|
2004
|
+
if not (isinstance(triple, list) and len(triple) == 3
|
|
2005
|
+
and all(isinstance(name, str) for name in triple)):
|
|
2006
|
+
return None, _error(ValueError(
|
|
2007
|
+
f"negative_roles[{i}]: expected [a, b, role] (three strings), "
|
|
2008
|
+
f"got {triple!r}"))
|
|
2009
|
+
a, b, role = triple
|
|
2010
|
+
abox.assert_negative_role(a, b, role)
|
|
2011
|
+
for field, rows, assert_ in (("data", data, abox.assert_data),
|
|
2012
|
+
("negative_data", negative_data,
|
|
2013
|
+
abox.assert_negative_data)):
|
|
2014
|
+
for i, triple in enumerate(rows or []):
|
|
2015
|
+
if not (isinstance(triple, list) and len(triple) == 3
|
|
2016
|
+
and isinstance(triple[0], str) and isinstance(triple[1], str)):
|
|
2017
|
+
return None, _error(ValueError(
|
|
2018
|
+
f"{field}[{i}]: expected [individual, property, literal], "
|
|
2019
|
+
f"got {triple!r}"))
|
|
2020
|
+
individual, prop, text = triple
|
|
2021
|
+
value, err = _parse_dl_literal(text, f"{field}[{i}][2]")
|
|
2022
|
+
if err is not None:
|
|
2023
|
+
return None, err
|
|
2024
|
+
assert_(individual, prop, value)
|
|
2025
|
+
return abox, None
|
|
2026
|
+
|
|
2027
|
+
|
|
2028
|
+
@_answers_deep_input
|
|
2029
|
+
def dl_concept_satisfiable(concept: str, tbox: Optional[List[dict]] = None,
|
|
2030
|
+
syntax: str = "alc") -> dict:
|
|
2031
|
+
"""Is ``concept`` satisfiable with respect to ``tbox`` (the ALCHQ tableau)?
|
|
2032
|
+
|
|
2033
|
+
``concept``/``tbox`` text and ``syntax`` follow this section's own header
|
|
2034
|
+
comment; ``tbox`` rows follow :func:`_build_dl_tbox`.
|
|
2035
|
+
|
|
2036
|
+
Returns ``{"ok": True, "satisfiable": bool, "concept_unicode": str}``.
|
|
2037
|
+
"""
|
|
2038
|
+
from .. import dl
|
|
2039
|
+
|
|
2040
|
+
c, err = _parse_dl(concept, syntax, "concept", _dl_datatype_names(tbox))
|
|
2041
|
+
if err is not None:
|
|
2042
|
+
return err
|
|
2043
|
+
tb, err = _build_dl_tbox(tbox, syntax)
|
|
2044
|
+
if err is not None:
|
|
2045
|
+
return err
|
|
2046
|
+
texts, err = _unicode_texts(c)
|
|
2047
|
+
if err is not None:
|
|
2048
|
+
return err
|
|
2049
|
+
try:
|
|
2050
|
+
satisfiable = dl.concept_satisfiable(c, tb)
|
|
2051
|
+
except _dl_errors() as exc:
|
|
2052
|
+
return _error(exc)
|
|
2053
|
+
return {"ok": True, "satisfiable": satisfiable, "concept_unicode": texts[0]}
|
|
2054
|
+
|
|
2055
|
+
|
|
2056
|
+
@_answers_deep_input
|
|
2057
|
+
def dl_subsumes(sub: str, sup: str, tbox: Optional[List[dict]] = None,
|
|
2058
|
+
syntax: str = "alc") -> dict:
|
|
2059
|
+
"""Does ``tbox`` entail ``sub ⊑ sup`` (every model puts ``sub`` in ``sup``)?
|
|
2060
|
+
|
|
2061
|
+
Returns ``{"ok": True, "subsumes": bool, "sub_unicode": str, "sup_unicode": str}``.
|
|
2062
|
+
"""
|
|
2063
|
+
from .. import dl
|
|
2064
|
+
|
|
2065
|
+
datatypes = _dl_datatype_names(tbox)
|
|
2066
|
+
sub_c, err = _parse_dl(sub, syntax, "sub", datatypes)
|
|
2067
|
+
if err is not None:
|
|
2068
|
+
return err
|
|
2069
|
+
sup_c, err = _parse_dl(sup, syntax, "sup", datatypes)
|
|
2070
|
+
if err is not None:
|
|
2071
|
+
return err
|
|
2072
|
+
tb, err = _build_dl_tbox(tbox, syntax)
|
|
2073
|
+
if err is not None:
|
|
2074
|
+
return err
|
|
2075
|
+
texts, err = _unicode_texts(sub_c, sup_c)
|
|
2076
|
+
if err is not None:
|
|
2077
|
+
return err
|
|
2078
|
+
try:
|
|
2079
|
+
holds = dl.subsumes(sub_c, sup_c, tb)
|
|
2080
|
+
except _dl_errors() as exc:
|
|
2081
|
+
return _error(exc)
|
|
2082
|
+
return {"ok": True, "subsumes": holds,
|
|
2083
|
+
"sub_unicode": texts[0], "sup_unicode": texts[1]}
|
|
2084
|
+
|
|
2085
|
+
|
|
2086
|
+
@_answers_deep_input
|
|
2087
|
+
def dl_equivalent(c: str, d: str, tbox: Optional[List[dict]] = None,
|
|
2088
|
+
syntax: str = "alc") -> dict:
|
|
2089
|
+
"""Does ``tbox`` entail ``c ≡ d`` (mutual subsumption)?
|
|
2090
|
+
|
|
2091
|
+
Returns ``{"ok": True, "equivalent": bool, "c_unicode": str, "d_unicode": str}``.
|
|
2092
|
+
"""
|
|
2093
|
+
from .. import dl
|
|
2094
|
+
|
|
2095
|
+
datatypes = _dl_datatype_names(tbox)
|
|
2096
|
+
c_concept, err = _parse_dl(c, syntax, "c", datatypes)
|
|
2097
|
+
if err is not None:
|
|
2098
|
+
return err
|
|
2099
|
+
d_concept, err = _parse_dl(d, syntax, "d", datatypes)
|
|
2100
|
+
if err is not None:
|
|
2101
|
+
return err
|
|
2102
|
+
tb, err = _build_dl_tbox(tbox, syntax)
|
|
2103
|
+
if err is not None:
|
|
2104
|
+
return err
|
|
2105
|
+
texts, err = _unicode_texts(c_concept, d_concept)
|
|
2106
|
+
if err is not None:
|
|
2107
|
+
return err
|
|
2108
|
+
try:
|
|
2109
|
+
holds = dl.equivalent(c_concept, d_concept, tb)
|
|
2110
|
+
except _dl_errors() as exc:
|
|
2111
|
+
return _error(exc)
|
|
2112
|
+
return {"ok": True, "equivalent": holds,
|
|
2113
|
+
"c_unicode": texts[0], "d_unicode": texts[1]}
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
@_answers_deep_input
|
|
2117
|
+
def dl_abox_consistent(concepts: List[List[str]],
|
|
2118
|
+
roles: Optional[List[List[str]]] = None,
|
|
2119
|
+
distinct: Optional[List[List[str]]] = None,
|
|
2120
|
+
tbox: Optional[List[dict]] = None,
|
|
2121
|
+
syntax: str = "alc",
|
|
2122
|
+
same: Optional[List[List[str]]] = None,
|
|
2123
|
+
negative_roles: Optional[List[List[str]]] = None,
|
|
2124
|
+
data: Optional[List[List[str]]] = None,
|
|
2125
|
+
negative_data: Optional[List[List[str]]] = None) -> dict:
|
|
2126
|
+
"""Is the knowledge base ``(tbox, abox)`` consistent (does it have a model)?
|
|
2127
|
+
|
|
2128
|
+
``concepts``/``roles``/``distinct``/``same``/``negative_roles``/``data``/
|
|
2129
|
+
``negative_data`` build the ABox — see :func:`_build_dl_abox`; ``tbox``
|
|
2130
|
+
follows :func:`_build_dl_tbox`. A knowledge base with DATA assertions or a
|
|
2131
|
+
data box is expressible here, but the in-house tableau REFUSES it by name
|
|
2132
|
+
(it has no data domain): the reply is then an ``{"error": ...}`` naming the
|
|
2133
|
+
refused kinds, never an answer about a weaker knowledge base.
|
|
2134
|
+
|
|
2135
|
+
Returns ``{"ok": True, "consistent": bool}``.
|
|
2136
|
+
"""
|
|
2137
|
+
from .. import dl
|
|
2138
|
+
|
|
2139
|
+
err = _check_dl_syntax(syntax)
|
|
2140
|
+
if err is not None:
|
|
2141
|
+
return err
|
|
2142
|
+
abox, err = _build_dl_abox(concepts, roles, distinct, syntax,
|
|
2143
|
+
same, negative_roles, data, negative_data,
|
|
2144
|
+
_dl_datatype_names(tbox))
|
|
2145
|
+
if err is not None:
|
|
2146
|
+
return err
|
|
2147
|
+
tb, err = _build_dl_tbox(tbox, syntax)
|
|
2148
|
+
if err is not None:
|
|
2149
|
+
return err
|
|
2150
|
+
try:
|
|
2151
|
+
consistent = dl.abox_consistent(abox, tb)
|
|
2152
|
+
except _dl_errors() as exc:
|
|
2153
|
+
return _error(exc)
|
|
2154
|
+
return {"ok": True, "consistent": consistent}
|
|
2155
|
+
|
|
2156
|
+
|
|
2157
|
+
@_answers_deep_input
|
|
2158
|
+
def dl_instance_check(individual: str, concept: str,
|
|
2159
|
+
concepts: List[List[str]],
|
|
2160
|
+
roles: Optional[List[List[str]]] = None,
|
|
2161
|
+
distinct: Optional[List[List[str]]] = None,
|
|
2162
|
+
tbox: Optional[List[dict]] = None,
|
|
2163
|
+
syntax: str = "alc",
|
|
2164
|
+
same: Optional[List[List[str]]] = None,
|
|
2165
|
+
negative_roles: Optional[List[List[str]]] = None,
|
|
2166
|
+
data: Optional[List[List[str]]] = None,
|
|
2167
|
+
negative_data: Optional[List[List[str]]] = None) -> dict:
|
|
2168
|
+
"""Does the knowledge base entail ``individual : concept``?
|
|
2169
|
+
|
|
2170
|
+
Open-world (:func:`~unicode_logic_kit.dl.instance_check`'s own contract):
|
|
2171
|
+
``entailed=False`` means "not entailed", never "entailed to be false".
|
|
2172
|
+
``concepts``/``roles``/``distinct``/``tbox`` build the KB exactly like
|
|
2173
|
+
:func:`dl_abox_consistent`; ``concept`` is the query, parsed the same way.
|
|
2174
|
+
|
|
2175
|
+
Returns ``{"ok": True, "entailed": bool, "individual": str,
|
|
2176
|
+
"concept_unicode": str}``.
|
|
2177
|
+
"""
|
|
2178
|
+
from .. import dl
|
|
2179
|
+
|
|
2180
|
+
datatypes = _dl_datatype_names(tbox)
|
|
2181
|
+
query, err = _parse_dl(concept, syntax, "concept", datatypes)
|
|
2182
|
+
if err is not None:
|
|
2183
|
+
return err
|
|
2184
|
+
abox, err = _build_dl_abox(concepts, roles, distinct, syntax,
|
|
2185
|
+
same, negative_roles, data, negative_data,
|
|
2186
|
+
datatypes)
|
|
2187
|
+
if err is not None:
|
|
2188
|
+
return err
|
|
2189
|
+
tb, err = _build_dl_tbox(tbox, syntax)
|
|
2190
|
+
if err is not None:
|
|
2191
|
+
return err
|
|
2192
|
+
texts, err = _unicode_texts(query)
|
|
2193
|
+
if err is not None:
|
|
2194
|
+
return err
|
|
2195
|
+
try:
|
|
2196
|
+
entailed = dl.instance_check(abox, individual, query, tb)
|
|
2197
|
+
except _dl_errors() as exc:
|
|
2198
|
+
return _error(exc)
|
|
2199
|
+
return {"ok": True, "entailed": entailed, "individual": individual,
|
|
2200
|
+
"concept_unicode": texts[0]}
|
|
2201
|
+
|
|
2202
|
+
|
|
2203
|
+
@_answers_deep_input
|
|
2204
|
+
def dl_instance_retrieval(concept: str, concepts: List[List[str]],
|
|
2205
|
+
roles: Optional[List[List[str]]] = None,
|
|
2206
|
+
distinct: Optional[List[List[str]]] = None,
|
|
2207
|
+
tbox: Optional[List[dict]] = None,
|
|
2208
|
+
syntax: str = "alc",
|
|
2209
|
+
same: Optional[List[List[str]]] = None,
|
|
2210
|
+
negative_roles: Optional[List[List[str]]] = None,
|
|
2211
|
+
data: Optional[List[List[str]]] = None,
|
|
2212
|
+
negative_data: Optional[List[List[str]]] = None) -> dict:
|
|
2213
|
+
"""Every ABox individual the knowledge base entails is a ``concept``.
|
|
2214
|
+
|
|
2215
|
+
Sweeps :func:`~unicode_logic_kit.dl.instance_check` over every individual
|
|
2216
|
+
named in the ABox — including one that appears only in a role assertion.
|
|
2217
|
+
Arguments as :func:`dl_instance_check`.
|
|
2218
|
+
|
|
2219
|
+
Returns ``{"ok": True, "individuals": [str, ...], "concept_unicode": str}``
|
|
2220
|
+
(``individuals`` sorted, for a deterministic payload).
|
|
2221
|
+
"""
|
|
2222
|
+
from .. import dl
|
|
2223
|
+
|
|
2224
|
+
datatypes = _dl_datatype_names(tbox)
|
|
2225
|
+
query, err = _parse_dl(concept, syntax, "concept", datatypes)
|
|
2226
|
+
if err is not None:
|
|
2227
|
+
return err
|
|
2228
|
+
abox, err = _build_dl_abox(concepts, roles, distinct, syntax,
|
|
2229
|
+
same, negative_roles, data, negative_data,
|
|
2230
|
+
datatypes)
|
|
2231
|
+
if err is not None:
|
|
2232
|
+
return err
|
|
2233
|
+
tb, err = _build_dl_tbox(tbox, syntax)
|
|
2234
|
+
if err is not None:
|
|
2235
|
+
return err
|
|
2236
|
+
texts, err = _unicode_texts(query)
|
|
2237
|
+
if err is not None:
|
|
2238
|
+
return err
|
|
2239
|
+
try:
|
|
2240
|
+
individuals = dl.instance_retrieval(abox, query, tb)
|
|
2241
|
+
except _dl_errors() as exc:
|
|
2242
|
+
return _error(exc)
|
|
2243
|
+
return {"ok": True, "individuals": sorted(individuals),
|
|
2244
|
+
"concept_unicode": texts[0]}
|
|
2245
|
+
|
|
2246
|
+
|
|
2247
|
+
@_answers_deep_input
|
|
2248
|
+
def dl_classify(tbox: Optional[List[dict]] = None,
|
|
2249
|
+
concepts: Optional[List[str]] = None,
|
|
2250
|
+
syntax: str = "alc") -> dict:
|
|
2251
|
+
"""Classify every named concept of ``tbox`` into a subsumption hierarchy.
|
|
2252
|
+
|
|
2253
|
+
``tbox`` follows :func:`_build_dl_tbox` (``None``/``[]`` classifies the
|
|
2254
|
+
empty TBox — every concept is then its own isolated node). ``concepts``
|
|
2255
|
+
is extra concept TEXT to bring names of interest into the vocabulary even
|
|
2256
|
+
when they never occur in a ``tbox`` axiom (see
|
|
2257
|
+
:func:`~unicode_logic_kit.dl.classify`'s own ``concepts`` parameter) —
|
|
2258
|
+
parsed under the same ``syntax``.
|
|
2259
|
+
|
|
2260
|
+
Returns ``{"ok": True, "equivalents": {name: [str, ...]}, "parents":
|
|
2261
|
+
{name: [str, ...]}, "children": {name: [str, ...]}, "ancestors": {name:
|
|
2262
|
+
[str, ...]}}`` — :class:`~unicode_logic_kit.dl.Classification`'s frozensets
|
|
2263
|
+
rendered as sorted lists for a deterministic JSON payload. ``equivalents``
|
|
2264
|
+
is keyed by, and includes, the lexicographically smallest name in each
|
|
2265
|
+
mutual-subsumption synonym class; ``parents``/``children`` are the
|
|
2266
|
+
transitively-reduced Hasse diagram (direct super-/sub-concepts only);
|
|
2267
|
+
``ancestors`` is the full transitive closure.
|
|
2268
|
+
"""
|
|
2269
|
+
from .. import dl
|
|
2270
|
+
|
|
2271
|
+
err = _check_dl_syntax(syntax)
|
|
2272
|
+
if err is not None:
|
|
2273
|
+
return err
|
|
2274
|
+
tb, err = _build_dl_tbox(tbox, syntax)
|
|
2275
|
+
if err is not None:
|
|
2276
|
+
return err
|
|
2277
|
+
extra = []
|
|
2278
|
+
datatypes = _dl_datatype_names(tbox)
|
|
2279
|
+
for i, text in enumerate(concepts or []):
|
|
2280
|
+
concept, err = _parse_dl(text, syntax, f"concepts[{i}]", datatypes)
|
|
2281
|
+
if err is not None:
|
|
2282
|
+
return err
|
|
2283
|
+
extra.append(concept)
|
|
2284
|
+
try:
|
|
2285
|
+
result = dl.classify(tb, extra or None)
|
|
2286
|
+
except _dl_errors() as exc:
|
|
2287
|
+
return _error(exc)
|
|
2288
|
+
return {"ok": True,
|
|
2289
|
+
"equivalents": {k: sorted(v) for k, v in result.equivalents.items()},
|
|
2290
|
+
"parents": {k: sorted(v) for k, v in result.parents.items()},
|
|
2291
|
+
"children": {k: sorted(v) for k, v in result.children.items()},
|
|
2292
|
+
"ancestors": {k: sorted(v) for k, v in result.ancestors.items()}}
|
|
2293
|
+
|
|
2294
|
+
|
|
2295
|
+
def _role_axiom_payload(axiom) -> dict:
|
|
2296
|
+
"""``dl.parse_manchester_role_axiom``'s tuple as a JSON payload.
|
|
2297
|
+
|
|
2298
|
+
One function over its THREE tuple shapes, keyed by the TAG and read off the
|
|
2299
|
+
reader's own tables (``_BINARY_ROLE_FRAMES`` / ``_FILLER_ROLE_FRAMES`` /
|
|
2300
|
+
``_CHARACTERISTIC_TAGS``), so a shape the reader gains cannot be named
|
|
2301
|
+
differently here.
|
|
2302
|
+
|
|
2303
|
+
Until 0.30.0 this branch ended in ``_, role = axiom``, so every shape other
|
|
2304
|
+
than ``("subproperty", sub, sup)`` and a characteristic crashed the tool
|
|
2305
|
+
with ``ValueError: too many values to unpack`` — an ``InverseOf``,
|
|
2306
|
+
``DisjointWith`` or ``EquivalentTo`` axiom the reader already read
|
|
2307
|
+
perfectly well. Deriving the tags means a reader shape with no payload here
|
|
2308
|
+
is reported as an error naming itself, not as a crash.
|
|
2309
|
+
"""
|
|
2310
|
+
from ..dl import owl_manchester as _manchester
|
|
2311
|
+
|
|
2312
|
+
pairs = {tag for tag, _spelling in _manchester._BINARY_ROLE_FRAMES.values()}
|
|
2313
|
+
fillers = {tag for tag, _spelling in _manchester._FILLER_ROLE_FRAMES.values()}
|
|
2314
|
+
characteristics = set(_manchester._CHARACTERISTIC_TAGS.values())
|
|
2315
|
+
tag = axiom[0]
|
|
2316
|
+
if tag in pairs and len(axiom) == 3:
|
|
2317
|
+
return {"ok": True, "kind": tag,
|
|
2318
|
+
"sub_role": axiom[1], "super_role": axiom[2]}
|
|
2319
|
+
if tag in fillers and len(axiom) == 3:
|
|
2320
|
+
texts, err = _unicode_texts(axiom[2])
|
|
2321
|
+
if err is not None:
|
|
2322
|
+
return err
|
|
2323
|
+
return {"ok": True, "kind": tag, "role": axiom[1],
|
|
2324
|
+
"concept_unicode": texts[0]}
|
|
2325
|
+
if tag in characteristics and len(axiom) == 2:
|
|
2326
|
+
return {"ok": True, "kind": tag, "role": axiom[1]}
|
|
2327
|
+
return _error(ValueError(
|
|
2328
|
+
f"dl_parse_manchester: dl.parse_manchester_role_axiom returned the "
|
|
2329
|
+
f"shape {axiom!r}, which this tool has no payload for — add one"))
|
|
2330
|
+
|
|
2331
|
+
|
|
2332
|
+
@_answers_deep_input
|
|
2333
|
+
def dl_parse_manchester(text: str, kind: str = "concept") -> dict:
|
|
2334
|
+
"""Parse OWL 2 Manchester Syntax text, three ways.
|
|
2335
|
+
|
|
2336
|
+
``kind="concept"`` (default): a class expression via ``dl.parse_manchester``
|
|
2337
|
+
— returns the parsed concept's unicode rendering plus a ``to_manchester``
|
|
2338
|
+
round-trip (``dl.to_manchester(dl.parse_manchester(text))``), so a caller
|
|
2339
|
+
can confirm ``text`` normalises the way it expects.
|
|
2340
|
+
``kind="axiom"``: a ``SubClassOf``/``EquivalentTo`` axiom via
|
|
2341
|
+
``dl.parse_manchester_axiom``.
|
|
2342
|
+
``kind="role_axiom"``: a ``SubPropertyOf``/``Characteristics: Transitive``
|
|
2343
|
+
RBox axiom via ``dl.parse_manchester_role_axiom`` (see
|
|
2344
|
+
:mod:`unicode_logic_kit.dl.tableau`'s "Role hierarchies and transitive
|
|
2345
|
+
roles (RBox)").
|
|
2346
|
+
|
|
2347
|
+
Returns, on success: ``{"ok": True, "concept_unicode": str, "manchester":
|
|
2348
|
+
str}`` (``kind="concept"``); ``{"ok": True, "kind": "subclass"|
|
|
2349
|
+
"equivalent", "sub_unicode": str, "sup_unicode": str}`` (``kind="axiom"``);
|
|
2350
|
+
and for ``kind="role_axiom"`` one of three shapes, by the axiom read (see
|
|
2351
|
+
:func:`_role_axiom_payload`): ``{"ok": True, "kind": "subproperty"|
|
|
2352
|
+
"equivalentproperty"|"inverse"|"disjoint", "sub_role": str, "super_role":
|
|
2353
|
+
str}``, ``{"ok": True, "kind": "domain"|"range", "role": str,
|
|
2354
|
+
"concept_unicode": str}``, or ``{"ok": True, "kind": "transitive"|…,
|
|
2355
|
+
"role": str}`` for a ``Characteristics:`` declaration.
|
|
2356
|
+
Malformed/unsupported ``text`` is the uniform ``ok=False`` shape (see this
|
|
2357
|
+
section's own header comment); an unknown ``kind`` is
|
|
2358
|
+
``{"error": {"type": "ValueError", ...}}``.
|
|
2359
|
+
"""
|
|
2360
|
+
from .. import dl
|
|
2361
|
+
|
|
2362
|
+
if kind == "concept":
|
|
2363
|
+
try:
|
|
2364
|
+
concept = dl.parse_manchester(text)
|
|
2365
|
+
except dl.ManchesterSyntaxError as exc:
|
|
2366
|
+
return {"ok": False, "argument": "text",
|
|
2367
|
+
"errors": [{"dialect": "manchester", "message": str(exc)}],
|
|
2368
|
+
"spec_topic": "description-logic"}
|
|
2369
|
+
texts, err = _unicode_texts(concept)
|
|
2370
|
+
if err is not None:
|
|
2371
|
+
return err
|
|
2372
|
+
try:
|
|
2373
|
+
manchester = dl.to_manchester(concept)
|
|
2374
|
+
except ValueError as exc: # a name parse_manchester could not read back
|
|
2375
|
+
return _error(exc)
|
|
2376
|
+
return {"ok": True, "concept_unicode": texts[0], "manchester": manchester}
|
|
2377
|
+
if kind == "axiom":
|
|
2378
|
+
try:
|
|
2379
|
+
label, sub, sup = dl.parse_manchester_axiom(text)
|
|
2380
|
+
except dl.ManchesterSyntaxError as exc:
|
|
2381
|
+
return {"ok": False, "argument": "text",
|
|
2382
|
+
"errors": [{"dialect": "manchester", "message": str(exc)}],
|
|
2383
|
+
"spec_topic": "description-logic"}
|
|
2384
|
+
texts, err = _unicode_texts(sub, sup)
|
|
2385
|
+
if err is not None:
|
|
2386
|
+
return err
|
|
2387
|
+
return {"ok": True, "kind": label,
|
|
2388
|
+
"sub_unicode": texts[0], "sup_unicode": texts[1]}
|
|
2389
|
+
if kind == "role_axiom":
|
|
2390
|
+
try:
|
|
2391
|
+
axiom = dl.parse_manchester_role_axiom(text)
|
|
2392
|
+
except dl.ManchesterSyntaxError as exc:
|
|
2393
|
+
return {"ok": False, "argument": "text",
|
|
2394
|
+
"errors": [{"dialect": "manchester", "message": str(exc)}],
|
|
2395
|
+
"spec_topic": "description-logic"}
|
|
2396
|
+
return _role_axiom_payload(axiom)
|
|
2397
|
+
return _error(ValueError(
|
|
2398
|
+
f"dl_parse_manchester: unknown kind {kind!r} "
|
|
2399
|
+
"(one of ['concept', 'axiom', 'role_axiom'])"))
|
|
2400
|
+
|
|
2401
|
+
|
|
2402
|
+
# --------------------------------------------------------------------------
|
|
2403
|
+
# Server assembly
|
|
2404
|
+
# --------------------------------------------------------------------------
|
|
2405
|
+
|
|
2406
|
+
def create_server():
|
|
2407
|
+
"""Build the :class:`mcp.server.MCPServer` with every tool registered.
|
|
2408
|
+
|
|
2409
|
+
Imported lazily so the whole subpackage stays importable-in-theory even
|
|
2410
|
+
without the optional SDK — but this function needs it: a missing SDK
|
|
2411
|
+
raises ImportError with the install hint.
|
|
2412
|
+
"""
|
|
2413
|
+
try:
|
|
2414
|
+
from mcp.server import MCPServer
|
|
2415
|
+
except ImportError as exc:
|
|
2416
|
+
raise ImportError(
|
|
2417
|
+
"unicode_logic_kit.mcp needs the MCP SDK: "
|
|
2418
|
+
"pip install 'unicode-logic-kit[mcp]' (or: pip install 'mcp>=2.0')"
|
|
2419
|
+
) from exc
|
|
2420
|
+
|
|
2421
|
+
from .chem_tools import (
|
|
2422
|
+
molecule_to_structure, check_molecule, check_molecules,
|
|
2423
|
+
explain_molecule_failure, simplify_definition, chemical_signature,
|
|
2424
|
+
)
|
|
2425
|
+
|
|
2426
|
+
server = MCPServer(_SERVER_NAME, instructions=_INSTRUCTIONS)
|
|
2427
|
+
for fn in (parse_formula, check_formula, prove, find_countermodel,
|
|
2428
|
+
check_equivalence, diagnose, repair_formula, translate,
|
|
2429
|
+
verbalize, list_backends,
|
|
2430
|
+
normalize, render, detect_dialect, compare_formulas,
|
|
2431
|
+
score_batch, check_consistency, get_signature, truth_table,
|
|
2432
|
+
drs_to_fol, list_translations,
|
|
2433
|
+
probability_bounds, probability_query, get_syntax_spec,
|
|
2434
|
+
# Chemistry: molecules as structures, definitions checked
|
|
2435
|
+
# against them, failures explained (mcp.chem_tools).
|
|
2436
|
+
molecule_to_structure, check_molecule, check_molecules,
|
|
2437
|
+
explain_molecule_failure, simplify_definition,
|
|
2438
|
+
chemical_signature,
|
|
2439
|
+
# Description logic: ALCHQ tableau reasoning (concept
|
|
2440
|
+
# satisfiability, subsumption, ABox consistency, instance/
|
|
2441
|
+
# realization queries, TBox classification) plus OWL
|
|
2442
|
+
# Manchester Syntax parsing — ALC glyph or Manchester input,
|
|
2443
|
+
# selected by each tool's own `syntax` parameter.
|
|
2444
|
+
dl_concept_satisfiable, dl_subsumes, dl_equivalent,
|
|
2445
|
+
dl_abox_consistent, dl_instance_check, dl_instance_retrieval,
|
|
2446
|
+
dl_classify, dl_parse_manchester):
|
|
2447
|
+
server.tool()(_registered(fn))
|
|
2448
|
+
return server
|
|
2449
|
+
|
|
2450
|
+
|
|
2451
|
+
def main() -> None:
|
|
2452
|
+
"""Run the server on stdio (the transport MCP clients spawn)."""
|
|
2453
|
+
create_server().run("stdio")
|