engineering-argument-language 3.2.3__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.
- eal/__init__.py +3 -0
- eal/abstractions.py +144 -0
- eal/acquisition_coordination.py +80 -0
- eal/api_load_methods.py +77 -0
- eal/aspic.py +478 -0
- eal/aspic_compiler.py +445 -0
- eal/aspic_export.py +309 -0
- eal/builtin_methods.py +115 -0
- eal/catalogue.py +384 -0
- eal/cli.py +204 -0
- eal/client_transports.py +69 -0
- eal/collection_identity.py +28 -0
- eal/collection_scheduler.py +72 -0
- eal/command_process.py +118 -0
- eal/command_supervisor.py +112 -0
- eal/composition.py +531 -0
- eal/credentials.py +33 -0
- eal/dialectic.py +321 -0
- eal/discovery.py +127 -0
- eal/evaluator.py +619 -0
- eal/expressions.py +404 -0
- eal/extensions.py +38 -0
- eal/formatter.py +156 -0
- eal/generated/EALLexer.py +434 -0
- eal/generated/EALParser.py +6559 -0
- eal/generated/EALVisitor.py +393 -0
- eal/generated/__init__.py +1 -0
- eal/host.py +179 -0
- eal/host_redaction.py +51 -0
- eal/knowledge.py +96 -0
- eal/limits.py +88 -0
- eal/mcp_guard.py +56 -0
- eal/methods.py +463 -0
- eal/model.py +266 -0
- eal/model_context.py +55 -0
- eal/modes.py +121 -0
- eal/observation_reuse.py +128 -0
- eal/operation_contracts.py +25 -0
- eal/operations.py +155 -0
- eal/packets.py +604 -0
- eal/parser.py +407 -0
- eal/planning.py +136 -0
- eal/propositions.py +231 -0
- eal/reachability.py +85 -0
- eal/reasoning/__init__.py +25 -0
- eal/reasoning/abductive.py +45 -0
- eal/reasoning/analogical.py +41 -0
- eal/reasoning/causal.py +46 -0
- eal/reasoning/counterfactual.py +64 -0
- eal/reasoning/deductive.py +72 -0
- eal/reasoning/inductive.py +29 -0
- eal/reasoning/strategy.py +18 -0
- eal/reasoning/structured.py +11 -0
- eal/reasoning/temporal.py +47 -0
- eal/reasoning/validation.py +90 -0
- eal/registered_assessment.py +181 -0
- eal/runtime.py +454 -0
- eal/sampled_negative.py +131 -0
- eal/scope_transfer.py +59 -0
- eal/semantics.py +656 -0
- eal/server.py +79 -0
- eal/server_auth.py +37 -0
- eal/server_settings.py +258 -0
- eal/source_printer.py +129 -0
- eal/store.py +272 -0
- eal/tool_acquisition.py +383 -0
- engineering_argument_language-3.2.3.dist-info/METADATA +188 -0
- engineering_argument_language-3.2.3.dist-info/RECORD +72 -0
- engineering_argument_language-3.2.3.dist-info/WHEEL +5 -0
- engineering_argument_language-3.2.3.dist-info/entry_points.txt +4 -0
- engineering_argument_language-3.2.3.dist-info/licenses/LICENSE +24 -0
- engineering_argument_language-3.2.3.dist-info/top_level.txt +1 -0
eal/knowledge.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Developer-facing EAL knowledge base and prompt adapter.
|
|
2
|
+
|
|
3
|
+
The application chooses a registered entry and claim. The host selects the
|
|
4
|
+
source, reuses eligible observations, runs missing collectors, evaluates the
|
|
5
|
+
declared method and produces a bounded packet. A text model receives that
|
|
6
|
+
packet as context, without needing EAL parsing, tool use or model reasoning.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
from .catalogue import WorkspaceKnowledgeCatalogue
|
|
16
|
+
from .registered_assessment import RegisteredAssessmentHost
|
|
17
|
+
from .runtime import ReasoningService
|
|
18
|
+
from .model_context import CONTEXT_INSTRUCTION, ModelContextBuilder
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EALKnowledgeBase:
|
|
22
|
+
"""Compose the persistent catalogue and assessment path for an application."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, workspace: str | Path, registry_path: str | Path | None = None,
|
|
25
|
+
database_path: str | Path | None = None, *, method_registry: Any = None, limits=None):
|
|
26
|
+
self.service = ReasoningService(
|
|
27
|
+
workspace, registry_path, database_path, method_registry=method_registry, limits=limits,
|
|
28
|
+
)
|
|
29
|
+
self.catalogue = WorkspaceKnowledgeCatalogue(self.service)
|
|
30
|
+
self.host = RegisteredAssessmentHost(self.service, self.catalogue)
|
|
31
|
+
|
|
32
|
+
def register(self, path: str, *, entry_id: str | None = None,
|
|
33
|
+
context: dict[str, Any] | None = None,
|
|
34
|
+
claims: list[str] | tuple[str, ...] | None = None,
|
|
35
|
+
label: str | None = None) -> dict[str, Any]:
|
|
36
|
+
return self.catalogue.register(
|
|
37
|
+
path, entry_id=entry_id, context=context, claims=claims, label=label,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def register_tree(self, directory: str = ".", *, context: dict[str, Any] | None = None,
|
|
41
|
+
limit: int = 512) -> dict[str, Any]:
|
|
42
|
+
return self.catalogue.register_tree(directory, context=context, limit=limit)
|
|
43
|
+
|
|
44
|
+
def sources(self, *, limit: int = 50, offset: int = 0) -> list[dict[str, Any]]:
|
|
45
|
+
return self.catalogue.list(limit=limit, offset=offset)
|
|
46
|
+
|
|
47
|
+
def find(self, query: str | None = None, *, claim: str | None = None,
|
|
48
|
+
context: dict[str, Any] | None = None, path: str | None = None,
|
|
49
|
+
limit: int = 50) -> list[dict[str, Any]]:
|
|
50
|
+
return self.catalogue.find(query, claim=claim, context=context, path=path, limit=limit)
|
|
51
|
+
|
|
52
|
+
def assess(self, entry_id: str, claim: str, *, context: dict | None = None,
|
|
53
|
+
now: str | None = None,
|
|
54
|
+
reuse: Literal["compatible", "fresh"] = "compatible") -> dict[str, Any]:
|
|
55
|
+
return self.host.assess(entry_id, claim, context=context, now=now, reuse=reuse)
|
|
56
|
+
|
|
57
|
+
def history(self, entry_id: str, *, source_digest: str | None = None,
|
|
58
|
+
limit: int = 50) -> dict[str, Any]:
|
|
59
|
+
return self.host.history(entry_id, source_digest=source_digest, limit=limit)
|
|
60
|
+
|
|
61
|
+
def explain(self, assessment_id: str, claim: str | None = None) -> dict[str, Any]:
|
|
62
|
+
return self.service.explain(assessment_id, claim)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ModelContextAdapter:
|
|
66
|
+
"""Build a checked prompt context for a model with no tool or reasoning API.
|
|
67
|
+
|
|
68
|
+
The caller selects a claim by exact ID; free-form questions do not choose
|
|
69
|
+
sources or change an assessment's status. The host's returned assessment
|
|
70
|
+
remains the authoritative result if a model paraphrases it incorrectly.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, knowledge: EALKnowledgeBase):
|
|
74
|
+
self.knowledge = knowledge
|
|
75
|
+
|
|
76
|
+
def prepare(self, question: str, entry_id: str, claim: str, *,
|
|
77
|
+
context: dict | None = None, now: str | None = None,
|
|
78
|
+
reuse: Literal["compatible", "fresh"] = "compatible") -> dict[str, Any]:
|
|
79
|
+
if not isinstance(question, str) or not question.strip() or len(question.encode("utf-8")) > 8192:
|
|
80
|
+
raise ValueError("question must contain one to 8192 UTF-8 bytes")
|
|
81
|
+
assessment = self.knowledge.assess(
|
|
82
|
+
entry_id, claim, context=context, now=now, reuse=reuse,
|
|
83
|
+
)
|
|
84
|
+
host_context = ModelContextBuilder().build(assessment)
|
|
85
|
+
return {
|
|
86
|
+
"schema": "EAL/model-context/2", "assessment": assessment,
|
|
87
|
+
"context": host_context,
|
|
88
|
+
"messages": [
|
|
89
|
+
{"role": "system", "content": (
|
|
90
|
+
CONTEXT_INSTRUCTION
|
|
91
|
+
+ json.dumps(host_context, ensure_ascii=False, sort_keys=True,
|
|
92
|
+
separators=(",", ":"), allow_nan=False)
|
|
93
|
+
)},
|
|
94
|
+
{"role": "user", "content": question},
|
|
95
|
+
],
|
|
96
|
+
}
|
eal/limits.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Host-owned resource budgets, independent of source-language expressiveness."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
from dataclasses import asdict, dataclass, fields
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import tomllib
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ExecutionLimits:
|
|
14
|
+
source_bytes: int = 1024 * 1024
|
|
15
|
+
source_tokens: int = 100_000
|
|
16
|
+
structural_values: int = 100_000
|
|
17
|
+
declarations: int = 4096
|
|
18
|
+
premise_depth: int = 128
|
|
19
|
+
expansion_depth: int = 128
|
|
20
|
+
applications: int = 1000
|
|
21
|
+
expanded_references: int = 100_000
|
|
22
|
+
expression_nodes: int = 10_000
|
|
23
|
+
expression_depth: int = 128
|
|
24
|
+
json_depth: int = 64
|
|
25
|
+
json_nodes: int = 100_000
|
|
26
|
+
record_bytes: int = 4 * 1024 * 1024
|
|
27
|
+
collection_evidence: int = 128
|
|
28
|
+
composed_edges: int = 131_072
|
|
29
|
+
abstract_attacks: int = 65_536
|
|
30
|
+
formal_premises: int = 64
|
|
31
|
+
formal_rules: int = 64
|
|
32
|
+
formal_antecedents: int = 8
|
|
33
|
+
formal_atoms: int = 128
|
|
34
|
+
formal_contraries: int = 128
|
|
35
|
+
formal_arguments: int = 128
|
|
36
|
+
formal_defeats: int = 4096
|
|
37
|
+
formal_construction: int = 100_000
|
|
38
|
+
extension_search: int = 100_000
|
|
39
|
+
|
|
40
|
+
def __post_init__(self):
|
|
41
|
+
for field in fields(self):
|
|
42
|
+
value = getattr(self, field.name)
|
|
43
|
+
if type(value) is not int or value < 1:
|
|
44
|
+
raise ValueError(f"Budget {field.name} must be a positive integer")
|
|
45
|
+
|
|
46
|
+
def describe(self):
|
|
47
|
+
return asdict(self)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def load(cls, path: str | Path):
|
|
51
|
+
with Path(path).open("rb") as source:
|
|
52
|
+
supplied = tomllib.load(source)
|
|
53
|
+
if set(supplied) != {"limits"} or type(supplied["limits"]) is not dict:
|
|
54
|
+
raise ValueError("Budget file must contain exactly a [limits] table")
|
|
55
|
+
return cls(**supplied["limits"])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_ACTIVE = ContextVar("eal_execution_limits", default=ExecutionLimits())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def current_limits() -> ExecutionLimits:
|
|
62
|
+
return _ACTIVE.get()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@contextmanager
|
|
66
|
+
def using_limits(limits: ExecutionLimits):
|
|
67
|
+
if type(limits) is not ExecutionLimits:
|
|
68
|
+
raise TypeError("Host budgets require ExecutionLimits")
|
|
69
|
+
token = _ACTIVE.set(limits)
|
|
70
|
+
try:
|
|
71
|
+
yield
|
|
72
|
+
finally:
|
|
73
|
+
_ACTIVE.reset(token)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def bounded(function):
|
|
77
|
+
"""Carry a parsed programme's captured host budget across pure passes."""
|
|
78
|
+
@wraps(function)
|
|
79
|
+
def run(program, *args, **kwargs):
|
|
80
|
+
limits = kwargs.get('limits') or next((getattr(value, "limits", None) for value in (program, *args)
|
|
81
|
+
if type(getattr(value, "limits", None)) is ExecutionLimits), None)
|
|
82
|
+
with using_limits(limits if type(limits) is ExecutionLimits else current_limits()):
|
|
83
|
+
return function(program, *args, **kwargs)
|
|
84
|
+
return run
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BudgetExceeded(ValueError):
|
|
88
|
+
"""An operation is incomplete; exhaustion supplies no logical conclusion."""
|
eal/mcp_guard.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Enforce MCP schemas, admission limits and workspace acquisition exclusion."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import anyio
|
|
11
|
+
from fastmcp.exceptions import ToolError
|
|
12
|
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
13
|
+
from jsonschema import Draft202012Validator
|
|
14
|
+
|
|
15
|
+
from .operation_contracts import ACQUISITION_OPERATIONS
|
|
16
|
+
from .acquisition_coordination import WorkspaceAcquisition
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OperationGuard(Middleware):
|
|
20
|
+
"""Bound active calls and validate schemas before framework coercion."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, schemas: dict[str, dict[str, Any]], *, max_in_flight: int = 1):
|
|
23
|
+
if type(max_in_flight) is not int or not 1 <= max_in_flight <= 1024:
|
|
24
|
+
raise ValueError("max_in_flight must be an integer from 1 through 1024")
|
|
25
|
+
self._validators = {name: Draft202012Validator(schema) for name, schema in schemas.items()}
|
|
26
|
+
self._slots = anyio.Semaphore(max_in_flight)
|
|
27
|
+
|
|
28
|
+
async def on_call_tool(self, context: MiddlewareContext, call_next):
|
|
29
|
+
validator = self._validators.get(context.message.name)
|
|
30
|
+
if validator is not None:
|
|
31
|
+
errors = validator.iter_errors(context.message.arguments or {})
|
|
32
|
+
error = next(errors, None)
|
|
33
|
+
if error is not None:
|
|
34
|
+
raise ToolError("Tool input schema rejected request: " + error.message)
|
|
35
|
+
async with self._slots:
|
|
36
|
+
return await call_next(context)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def guard_acquisition(name: str, handler: Callable[..., dict[str, Any]],
|
|
40
|
+
workspace: Path) -> Callable[..., dict[str, Any]]:
|
|
41
|
+
"""Serialise complete acquisition operations across local server processes.
|
|
42
|
+
|
|
43
|
+
Locking precedes registered-observation reuse, so a waiting assessment can
|
|
44
|
+
reuse the preceding assessment's newly acquired observation. Independent
|
|
45
|
+
collectors inside one operation still use the existing bounded scheduler.
|
|
46
|
+
"""
|
|
47
|
+
if name not in ACQUISITION_OPERATIONS:
|
|
48
|
+
return handler
|
|
49
|
+
coordinator = WorkspaceAcquisition(workspace)
|
|
50
|
+
|
|
51
|
+
@wraps(handler)
|
|
52
|
+
def guarded(*args, **kwargs):
|
|
53
|
+
with coordinator.hold():
|
|
54
|
+
return handler(*args, **kwargs)
|
|
55
|
+
|
|
56
|
+
return guarded
|