semantic-python 0.1.0a1__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.
- semantic_python/__init__.py +50 -0
- semantic_python/backends/__init__.py +12 -0
- semantic_python/backends/base.py +31 -0
- semantic_python/backends/fake.py +66 -0
- semantic_python/backends/laya.py +75 -0
- semantic_python/backends/openai.py +118 -0
- semantic_python/cache.py +46 -0
- semantic_python/config.py +101 -0
- semantic_python/decision.py +125 -0
- semantic_python/py.typed +1 -0
- semantic_python/replay.py +80 -0
- semantic_python/semantic.py +80 -0
- semantic_python-0.1.0a1.dist-info/METADATA +228 -0
- semantic_python-0.1.0a1.dist-info/RECORD +16 -0
- semantic_python-0.1.0a1.dist-info/WHEEL +4 -0
- semantic_python-0.1.0a1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Opt-in semantic values for ordinary Python syntax."""
|
|
2
|
+
|
|
3
|
+
from .backends import (
|
|
4
|
+
BackendBase,
|
|
5
|
+
DecisionBackend,
|
|
6
|
+
FakeBackend,
|
|
7
|
+
LayaBackend,
|
|
8
|
+
OpenAIBackend,
|
|
9
|
+
)
|
|
10
|
+
from .cache import MemoryDecisionCache, decision_cache_key, state_digest
|
|
11
|
+
from .config import Config, configuration, configure, get_config, reset_config
|
|
12
|
+
from .decision import (
|
|
13
|
+
BackendError,
|
|
14
|
+
ConfigurationError,
|
|
15
|
+
Decision,
|
|
16
|
+
InvalidComparisonError,
|
|
17
|
+
SemanticError,
|
|
18
|
+
TruthPolicy,
|
|
19
|
+
UncertainDecisionError,
|
|
20
|
+
UncertaintyPolicy,
|
|
21
|
+
)
|
|
22
|
+
from .replay import DecisionRecorder, ReplayBackend
|
|
23
|
+
from .semantic import Semantic
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"BackendBase",
|
|
27
|
+
"BackendError",
|
|
28
|
+
"Config",
|
|
29
|
+
"ConfigurationError",
|
|
30
|
+
"Decision",
|
|
31
|
+
"DecisionBackend",
|
|
32
|
+
"DecisionRecorder",
|
|
33
|
+
"FakeBackend",
|
|
34
|
+
"InvalidComparisonError",
|
|
35
|
+
"LayaBackend",
|
|
36
|
+
"MemoryDecisionCache",
|
|
37
|
+
"OpenAIBackend",
|
|
38
|
+
"ReplayBackend",
|
|
39
|
+
"Semantic",
|
|
40
|
+
"SemanticError",
|
|
41
|
+
"TruthPolicy",
|
|
42
|
+
"UncertainDecisionError",
|
|
43
|
+
"UncertaintyPolicy",
|
|
44
|
+
"configuration",
|
|
45
|
+
"configure",
|
|
46
|
+
"decision_cache_key",
|
|
47
|
+
"get_config",
|
|
48
|
+
"reset_config",
|
|
49
|
+
"state_digest",
|
|
50
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .base import BackendBase, DecisionBackend
|
|
2
|
+
from .fake import FakeBackend
|
|
3
|
+
from .laya import LayaBackend
|
|
4
|
+
from .openai import OpenAIBackend
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"BackendBase",
|
|
8
|
+
"DecisionBackend",
|
|
9
|
+
"FakeBackend",
|
|
10
|
+
"LayaBackend",
|
|
11
|
+
"OpenAIBackend",
|
|
12
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Backend-neutral semantic decision protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Protocol, runtime_checkable
|
|
7
|
+
|
|
8
|
+
from ..decision import Decision
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class DecisionBackend(Protocol):
|
|
13
|
+
@property
|
|
14
|
+
def identity(self) -> str:
|
|
15
|
+
"""Stable backend/model/version identity used by cache keys."""
|
|
16
|
+
|
|
17
|
+
def boolean(self, state: str, proposition: str) -> Decision[bool]: ...
|
|
18
|
+
|
|
19
|
+
def choice(self, state: str, options: Sequence[str]) -> Decision[str]: ...
|
|
20
|
+
|
|
21
|
+
def score(self, state: str, rubric: str) -> Decision[float]: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BackendBase:
|
|
25
|
+
"""Convenience base for boolean-only v0.1 backends."""
|
|
26
|
+
|
|
27
|
+
def choice(self, state: str, options: Sequence[str]) -> Decision[str]:
|
|
28
|
+
raise NotImplementedError("choice decisions are not implemented in v0.1")
|
|
29
|
+
|
|
30
|
+
def score(self, state: str, rubric: str) -> Decision[float]:
|
|
31
|
+
raise NotImplementedError("score decisions are not implemented in v0.1")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Deterministic, offline backend for examples and tests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from typing import TypeAlias
|
|
7
|
+
|
|
8
|
+
from ..cache import state_digest
|
|
9
|
+
from ..decision import Decision
|
|
10
|
+
from .base import BackendBase
|
|
11
|
+
|
|
12
|
+
FakeResult: TypeAlias = bool | float | tuple[bool, float]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FakeBackend(BackendBase):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
responses: Mapping[tuple[str, str], FakeResult] | None = None,
|
|
19
|
+
*,
|
|
20
|
+
default: FakeResult | None = None,
|
|
21
|
+
resolver: Callable[[str, str], FakeResult] | None = None,
|
|
22
|
+
name: str = "fake",
|
|
23
|
+
model: str = "deterministic-v1",
|
|
24
|
+
) -> None:
|
|
25
|
+
self.responses = dict(responses or {})
|
|
26
|
+
self.default = default
|
|
27
|
+
self.resolver = resolver
|
|
28
|
+
self.name = name
|
|
29
|
+
self.model = model
|
|
30
|
+
self.calls = 0
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def identity(self) -> str:
|
|
34
|
+
return f"{self.name}:{self.model}"
|
|
35
|
+
|
|
36
|
+
def boolean(self, state: str, proposition: str) -> Decision[bool]:
|
|
37
|
+
self.calls += 1
|
|
38
|
+
key = (state, proposition)
|
|
39
|
+
if key in self.responses:
|
|
40
|
+
result = self.responses[key]
|
|
41
|
+
elif self.resolver is not None:
|
|
42
|
+
result = self.resolver(state, proposition)
|
|
43
|
+
elif self.default is not None:
|
|
44
|
+
result = self.default
|
|
45
|
+
else:
|
|
46
|
+
raise KeyError(f"no fake response for state/proposition: {key!r}")
|
|
47
|
+
value, probability = self._coerce(result)
|
|
48
|
+
return Decision(
|
|
49
|
+
value=value,
|
|
50
|
+
probability=probability,
|
|
51
|
+
backend=self.name,
|
|
52
|
+
model=self.model,
|
|
53
|
+
proposition=proposition,
|
|
54
|
+
state_hash=state_digest(state),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _coerce(result: FakeResult) -> tuple[bool, float]:
|
|
59
|
+
if isinstance(result, tuple):
|
|
60
|
+
value, probability = result
|
|
61
|
+
elif isinstance(result, bool):
|
|
62
|
+
value, probability = result, 1.0 if result else 0.0
|
|
63
|
+
else:
|
|
64
|
+
probability = float(result)
|
|
65
|
+
value = probability >= 0.5
|
|
66
|
+
return bool(value), float(probability)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Optional adapter for the local Laya decision engine.
|
|
2
|
+
|
|
3
|
+
Laya is imported and its model is loaded only when the first decision is made.
|
|
4
|
+
Its zero-shot outputs, especially negation, require application-level evaluation;
|
|
5
|
+
they are probabilities, not proof or authorization for consequential actions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..cache import state_digest
|
|
14
|
+
from ..decision import BackendError, Decision
|
|
15
|
+
from .base import BackendBase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LayaBackend(BackendBase):
|
|
19
|
+
"""Use Laya's ``noul`` head for local proposition decisions.
|
|
20
|
+
|
|
21
|
+
Pass an injected router in tests or advanced deployments. Otherwise ``laya``
|
|
22
|
+
is imported lazily; installing the core package does not import torch.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, router: Any | None = None, *, model: str | None = None) -> None:
|
|
26
|
+
self._router = router
|
|
27
|
+
self.model = model
|
|
28
|
+
try:
|
|
29
|
+
self.version = version("laya")
|
|
30
|
+
except PackageNotFoundError:
|
|
31
|
+
self.version = "not-installed" if router is None else "injected"
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def identity(self) -> str:
|
|
35
|
+
return f"laya:{self.version}:{self.model or 'auto'}"
|
|
36
|
+
|
|
37
|
+
def _get_router(self) -> Any:
|
|
38
|
+
if self._router is None:
|
|
39
|
+
try:
|
|
40
|
+
from laya import Router # type: ignore[import-not-found]
|
|
41
|
+
except ImportError as error:
|
|
42
|
+
raise BackendError("LayaBackend requires the optional 'laya' package") from error
|
|
43
|
+
self._router = Router()
|
|
44
|
+
return self._router
|
|
45
|
+
|
|
46
|
+
def boolean(self, state: str, proposition: str) -> Decision[bool]:
|
|
47
|
+
questions = {
|
|
48
|
+
"decision": {
|
|
49
|
+
"type": "noul",
|
|
50
|
+
"instructions": proposition,
|
|
51
|
+
"criteria": {
|
|
52
|
+
"true": proposition,
|
|
53
|
+
"false": f"It is not true that: {proposition}",
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
kwargs = {"model": self.model} if self.model is not None else {}
|
|
58
|
+
try:
|
|
59
|
+
result = self._get_router().predict(state, questions, **kwargs)
|
|
60
|
+
probability = float(result["answers"]["decision"]["noul"])
|
|
61
|
+
routing = result["routing"]
|
|
62
|
+
routed_model = str(routing["model"])
|
|
63
|
+
except BackendError:
|
|
64
|
+
raise
|
|
65
|
+
except (KeyError, TypeError, ValueError) as error:
|
|
66
|
+
raise BackendError(f"Laya returned an invalid decision payload: {error}") from error
|
|
67
|
+
return Decision(
|
|
68
|
+
value=probability >= 0.5,
|
|
69
|
+
probability=probability,
|
|
70
|
+
backend="laya",
|
|
71
|
+
model=routed_model,
|
|
72
|
+
proposition=proposition,
|
|
73
|
+
state_hash=state_digest(state),
|
|
74
|
+
metadata={"routing": dict(routing), "provider_version": self.version},
|
|
75
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Optional OpenAI Responses API backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..cache import state_digest
|
|
12
|
+
from ..decision import BackendError, Decision
|
|
13
|
+
from .base import BackendBase
|
|
14
|
+
|
|
15
|
+
_SCHEMA = {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"properties": {
|
|
18
|
+
"value": {
|
|
19
|
+
"type": "boolean",
|
|
20
|
+
"description": "Whether the proposition is true; true iff probability is at least 0.5.",
|
|
21
|
+
},
|
|
22
|
+
"probability": {
|
|
23
|
+
"type": "number",
|
|
24
|
+
"minimum": 0,
|
|
25
|
+
"maximum": 1,
|
|
26
|
+
"description": (
|
|
27
|
+
"P(proposition=true), not confidence in the selected boolean value. "
|
|
28
|
+
"A false value therefore has probability below 0.5."
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
"required": ["value", "probability"],
|
|
33
|
+
"additionalProperties": False,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OpenAIBackend(BackendBase):
|
|
38
|
+
"""Remote semantic decisions using OpenAI Structured Outputs.
|
|
39
|
+
|
|
40
|
+
``probability`` is model-reported and must be calibrated on application data
|
|
41
|
+
before it is used for consequential automation.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, client: Any | None = None, *, model: str = "gpt-6-luna") -> None:
|
|
45
|
+
self._client = client
|
|
46
|
+
self.model = model
|
|
47
|
+
try:
|
|
48
|
+
self.version = version("openai")
|
|
49
|
+
except PackageNotFoundError:
|
|
50
|
+
self.version = "not-installed" if client is None else "injected"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def identity(self) -> str:
|
|
54
|
+
return f"openai:{self.version}:{self.model}"
|
|
55
|
+
|
|
56
|
+
def _get_client(self) -> Any:
|
|
57
|
+
if self._client is None:
|
|
58
|
+
try:
|
|
59
|
+
OpenAI = import_module("openai").OpenAI
|
|
60
|
+
except ImportError as error:
|
|
61
|
+
raise BackendError(
|
|
62
|
+
"OpenAIBackend requires the optional 'openai' package"
|
|
63
|
+
) from error
|
|
64
|
+
if not os.environ.get("OPENAI_API_KEY"):
|
|
65
|
+
raise BackendError("OpenAIBackend requires OPENAI_API_KEY")
|
|
66
|
+
self._client = OpenAI()
|
|
67
|
+
return self._client
|
|
68
|
+
|
|
69
|
+
def boolean(self, state: str, proposition: str) -> Decision[bool]:
|
|
70
|
+
try:
|
|
71
|
+
response = self._get_client().responses.create(
|
|
72
|
+
model=self.model,
|
|
73
|
+
instructions=(
|
|
74
|
+
"Decide whether the proposition is true of the supplied state. "
|
|
75
|
+
"Return the boolean judgment and your estimated P(proposition=true). "
|
|
76
|
+
"Probability always means probability that the proposition is true, "
|
|
77
|
+
"not confidence in whichever boolean you select. Set value=true if and "
|
|
78
|
+
"only if probability is at least 0.5."
|
|
79
|
+
),
|
|
80
|
+
input=f"State:\n{state}\n\nProposition:\n{proposition}",
|
|
81
|
+
text={
|
|
82
|
+
"format": {
|
|
83
|
+
"type": "json_schema",
|
|
84
|
+
"name": "semantic_decision",
|
|
85
|
+
"strict": True,
|
|
86
|
+
"schema": _SCHEMA,
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
payload = json.loads(response.output_text)
|
|
91
|
+
value = payload["value"]
|
|
92
|
+
probability = float(payload["probability"])
|
|
93
|
+
response_id = getattr(response, "id", None)
|
|
94
|
+
except BackendError:
|
|
95
|
+
raise
|
|
96
|
+
except Exception as error:
|
|
97
|
+
raise BackendError(f"OpenAI returned an invalid decision: {error}") from error
|
|
98
|
+
if not isinstance(value, bool):
|
|
99
|
+
raise BackendError("OpenAI decision value must be a boolean")
|
|
100
|
+
if value is not (probability >= 0.5):
|
|
101
|
+
raise BackendError(
|
|
102
|
+
"OpenAI returned inconsistent value and P(proposition=true): "
|
|
103
|
+
f"value={value!r}, probability={probability!r}"
|
|
104
|
+
)
|
|
105
|
+
return Decision(
|
|
106
|
+
value=value,
|
|
107
|
+
probability=probability,
|
|
108
|
+
backend="openai",
|
|
109
|
+
model=self.model,
|
|
110
|
+
proposition=proposition,
|
|
111
|
+
state_hash=state_digest(state),
|
|
112
|
+
metadata={
|
|
113
|
+
"response_id": response_id,
|
|
114
|
+
"confidence_kind": "model_reported",
|
|
115
|
+
"probability_semantics": "p_proposition_true",
|
|
116
|
+
"provider_version": self.version,
|
|
117
|
+
},
|
|
118
|
+
)
|
semantic_python/cache.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Stable cache keys and an in-memory decision cache."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import replace
|
|
8
|
+
from threading import RLock
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .decision import Decision
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def state_digest(state: str) -> str:
|
|
15
|
+
return hashlib.sha256(state.encode("utf-8")).hexdigest()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def decision_cache_key(
|
|
19
|
+
*, backend_identity: str, state: str, proposition: str, configuration: Any
|
|
20
|
+
) -> str:
|
|
21
|
+
config = json.dumps(configuration, sort_keys=True, separators=(",", ":"), default=str)
|
|
22
|
+
material = "\0".join((backend_identity, state_digest(state), proposition, config))
|
|
23
|
+
return hashlib.sha256(material.encode("utf-8")).hexdigest()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MemoryDecisionCache:
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._items: dict[str, Decision[Any]] = {}
|
|
29
|
+
self._lock = RLock()
|
|
30
|
+
|
|
31
|
+
def get(self, key: str) -> Decision[Any] | None:
|
|
32
|
+
with self._lock:
|
|
33
|
+
decision = self._items.get(key)
|
|
34
|
+
return replace(decision, cached=True) if decision is not None else None
|
|
35
|
+
|
|
36
|
+
def put(self, key: str, decision: Decision[Any]) -> None:
|
|
37
|
+
with self._lock:
|
|
38
|
+
self._items[key] = replace(decision, cached=False)
|
|
39
|
+
|
|
40
|
+
def clear(self) -> None:
|
|
41
|
+
with self._lock:
|
|
42
|
+
self._items.clear()
|
|
43
|
+
|
|
44
|
+
def __len__(self) -> int:
|
|
45
|
+
with self._lock:
|
|
46
|
+
return len(self._items)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Runtime configuration scoped with context variables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from contextvars import ContextVar
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
from .backends.base import DecisionBackend
|
|
12
|
+
from .cache import MemoryDecisionCache
|
|
13
|
+
from .decision import ConfigurationError, TruthPolicy, UncertaintyPolicy
|
|
14
|
+
from .replay import DecisionRecorder
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class Config:
|
|
19
|
+
backend: DecisionBackend | None = None
|
|
20
|
+
policy: TruthPolicy = field(default_factory=TruthPolicy)
|
|
21
|
+
cache: MemoryDecisionCache | None = None
|
|
22
|
+
recorder: DecisionRecorder | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _default_config() -> Config:
|
|
26
|
+
# Equality followed by inequality should reuse the same underlying judgment.
|
|
27
|
+
return Config(cache=MemoryDecisionCache())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_DEFAULT = _default_config()
|
|
31
|
+
_config: ContextVar[Config] = ContextVar("sem_config", default=_DEFAULT)
|
|
32
|
+
_UNSET = object()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_config() -> Config:
|
|
36
|
+
return _config.get()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def configure(
|
|
40
|
+
*,
|
|
41
|
+
backend: DecisionBackend | object | None = _UNSET,
|
|
42
|
+
threshold: float | None = None,
|
|
43
|
+
true_threshold: float | None = None,
|
|
44
|
+
false_threshold: float | None = None,
|
|
45
|
+
uncertainty: UncertaintyPolicy | str | None = None,
|
|
46
|
+
fallback: bool | None = None,
|
|
47
|
+
cache: MemoryDecisionCache | bool | object | None = _UNSET,
|
|
48
|
+
recorder: DecisionRecorder | object | None = _UNSET,
|
|
49
|
+
) -> Config:
|
|
50
|
+
current = get_config()
|
|
51
|
+
high = threshold if threshold is not None else true_threshold
|
|
52
|
+
low = (
|
|
53
|
+
1.0 - threshold
|
|
54
|
+
if threshold is not None
|
|
55
|
+
else false_threshold
|
|
56
|
+
if false_threshold is not None
|
|
57
|
+
else current.policy.false_threshold
|
|
58
|
+
)
|
|
59
|
+
uncertainty_policy = (
|
|
60
|
+
UncertaintyPolicy(uncertainty) if uncertainty is not None else current.policy.uncertainty
|
|
61
|
+
)
|
|
62
|
+
policy = TruthPolicy(
|
|
63
|
+
false_threshold=low,
|
|
64
|
+
true_threshold=high if high is not None else current.policy.true_threshold,
|
|
65
|
+
uncertainty=uncertainty_policy,
|
|
66
|
+
fallback=fallback if fallback is not None else current.policy.fallback,
|
|
67
|
+
)
|
|
68
|
+
selected_cache = MemoryDecisionCache() if cache is True else (None if cache is False else cache)
|
|
69
|
+
result = Config(
|
|
70
|
+
backend=current.backend if backend is _UNSET else cast(DecisionBackend | None, backend),
|
|
71
|
+
policy=policy,
|
|
72
|
+
cache=(
|
|
73
|
+
current.cache if cache is _UNSET else cast(MemoryDecisionCache | None, selected_cache)
|
|
74
|
+
),
|
|
75
|
+
recorder=(
|
|
76
|
+
current.recorder if recorder is _UNSET else cast(DecisionRecorder | None, recorder)
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
_config.set(result)
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def reset_config() -> None:
|
|
84
|
+
_config.set(_default_config())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@contextmanager
|
|
88
|
+
def configuration(**changes: Any) -> Iterator[Config]:
|
|
89
|
+
previous = get_config()
|
|
90
|
+
try:
|
|
91
|
+
configured = configure(**changes)
|
|
92
|
+
yield configured
|
|
93
|
+
finally:
|
|
94
|
+
_config.set(previous)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def require_backend() -> DecisionBackend:
|
|
98
|
+
backend = get_config().backend
|
|
99
|
+
if backend is None:
|
|
100
|
+
raise ConfigurationError("no semantic backend configured; call configure(backend=...)")
|
|
101
|
+
return backend
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Structured semantic decisions and their truthiness policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass, field, replace
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Any, Generic, TypeVar
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SemanticError(Exception):
|
|
15
|
+
"""Base class for semantic-runtime errors."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BackendError(SemanticError):
|
|
19
|
+
"""A semantic backend could not produce a decision."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConfigurationError(SemanticError):
|
|
23
|
+
"""The semantic runtime is not configured correctly."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InvalidComparisonError(TypeError, SemanticError):
|
|
27
|
+
"""An operand has no defined implicit semantic comparison."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UncertaintyPolicy(str, Enum):
|
|
31
|
+
RAISE = "raise"
|
|
32
|
+
FALLBACK = "fallback"
|
|
33
|
+
USE_VALUE = "use_value"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class UncertainDecisionError(SemanticError):
|
|
37
|
+
"""Raised when a decision falls inside the configured uncertainty band."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, decision: Decision[Any]) -> None:
|
|
40
|
+
self.decision = decision
|
|
41
|
+
super().__init__(
|
|
42
|
+
f"semantic decision is uncertain (probability={decision.probability:.3f}, "
|
|
43
|
+
f"proposition={decision.proposition!r})"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class TruthPolicy:
|
|
49
|
+
"""Controls conversion of a boolean decision to a Python bool."""
|
|
50
|
+
|
|
51
|
+
false_threshold: float = 0.15
|
|
52
|
+
true_threshold: float = 0.85
|
|
53
|
+
uncertainty: UncertaintyPolicy = UncertaintyPolicy.RAISE
|
|
54
|
+
fallback: bool = False
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
if not 0.0 <= self.false_threshold < self.true_threshold <= 1.0:
|
|
58
|
+
raise ValueError("thresholds must satisfy 0 <= false < true <= 1")
|
|
59
|
+
object.__setattr__(self, "uncertainty", UncertaintyPolicy(self.uncertainty))
|
|
60
|
+
|
|
61
|
+
def resolve(self, decision: Decision[bool]) -> bool:
|
|
62
|
+
if decision.probability >= self.true_threshold:
|
|
63
|
+
return True
|
|
64
|
+
if decision.probability <= self.false_threshold:
|
|
65
|
+
return False
|
|
66
|
+
if self.uncertainty is UncertaintyPolicy.FALLBACK:
|
|
67
|
+
return self.fallback
|
|
68
|
+
if self.uncertainty is UncertaintyPolicy.USE_VALUE:
|
|
69
|
+
return decision.value
|
|
70
|
+
raise UncertainDecisionError(decision)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class Decision(Generic[T]):
|
|
75
|
+
"""A typed result retaining confidence and provenance.
|
|
76
|
+
|
|
77
|
+
``probability`` is the probability that ``proposition`` is true, rather than
|
|
78
|
+
confidence in the selected value. This makes negation mechanically exact.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
value: T
|
|
82
|
+
probability: float
|
|
83
|
+
backend: str
|
|
84
|
+
proposition: str
|
|
85
|
+
state_hash: str
|
|
86
|
+
model: str | None = None
|
|
87
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
88
|
+
policy: TruthPolicy = field(default_factory=TruthPolicy, repr=False, compare=False)
|
|
89
|
+
cached: bool = field(default=False, compare=False)
|
|
90
|
+
replayed: bool = field(default=False, compare=False)
|
|
91
|
+
|
|
92
|
+
def __post_init__(self) -> None:
|
|
93
|
+
if not 0.0 <= self.probability <= 1.0:
|
|
94
|
+
raise ValueError("probability must be between 0 and 1")
|
|
95
|
+
if not self.backend:
|
|
96
|
+
raise ValueError("backend must not be empty")
|
|
97
|
+
if not self.proposition:
|
|
98
|
+
raise ValueError("proposition must not be empty")
|
|
99
|
+
if not self.state_hash:
|
|
100
|
+
raise ValueError("state_hash must not be empty")
|
|
101
|
+
object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
|
|
102
|
+
|
|
103
|
+
def __bool__(self) -> bool:
|
|
104
|
+
if not isinstance(self.value, bool):
|
|
105
|
+
raise TypeError("only boolean decisions have truthiness")
|
|
106
|
+
return self.policy.resolve(self) # type: ignore[arg-type]
|
|
107
|
+
|
|
108
|
+
def negated(self) -> Decision[bool]:
|
|
109
|
+
if not isinstance(self.value, bool):
|
|
110
|
+
raise TypeError("only boolean decisions can be negated")
|
|
111
|
+
return Decision(
|
|
112
|
+
value=not self.value,
|
|
113
|
+
probability=1.0 - self.probability,
|
|
114
|
+
backend=self.backend,
|
|
115
|
+
model=self.model,
|
|
116
|
+
proposition=f"not ({self.proposition})",
|
|
117
|
+
state_hash=self.state_hash,
|
|
118
|
+
metadata={**self.metadata, "negated_from": self.proposition},
|
|
119
|
+
policy=self.policy,
|
|
120
|
+
cached=self.cached,
|
|
121
|
+
replayed=self.replayed,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def with_policy(self, policy: TruthPolicy) -> Decision[T]:
|
|
125
|
+
return replace(self, policy=policy)
|
semantic_python/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker for PEP 561 inline type information.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Privacy-conscious JSONL recording and deterministic replay."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from threading import RLock
|
|
9
|
+
from typing import IO, Any, cast
|
|
10
|
+
|
|
11
|
+
from .backends.base import BackendBase
|
|
12
|
+
from .cache import state_digest
|
|
13
|
+
from .decision import Decision
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def decision_to_record(decision: Decision[Any]) -> dict[str, Any]:
|
|
17
|
+
return {
|
|
18
|
+
"value": decision.value,
|
|
19
|
+
"probability": decision.probability,
|
|
20
|
+
"backend": decision.backend,
|
|
21
|
+
"model": decision.model,
|
|
22
|
+
"proposition": decision.proposition,
|
|
23
|
+
"state_hash": decision.state_hash,
|
|
24
|
+
"metadata": dict(decision.metadata),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DecisionRecorder:
|
|
29
|
+
"""Append decisions as JSON lines; raw semantic state is never recorded."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, target: str | Path | IO[str]) -> None:
|
|
32
|
+
self.target = target
|
|
33
|
+
self._lock = RLock()
|
|
34
|
+
|
|
35
|
+
def record(self, decision: Decision[Any]) -> None:
|
|
36
|
+
line = json.dumps(decision_to_record(decision), sort_keys=True) + "\n"
|
|
37
|
+
with self._lock:
|
|
38
|
+
if hasattr(self.target, "write"):
|
|
39
|
+
stream = cast(IO[str], self.target)
|
|
40
|
+
stream.write(line)
|
|
41
|
+
stream.flush()
|
|
42
|
+
else:
|
|
43
|
+
with Path(self.target).open("a", encoding="utf-8") as stream:
|
|
44
|
+
stream.write(line)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ReplayBackend(BackendBase):
|
|
48
|
+
"""Backend that matches recorded decisions by state hash and proposition."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, source: str | Path | IO[str] | Iterable[dict[str, Any]]) -> None:
|
|
51
|
+
if isinstance(source, (str, Path)):
|
|
52
|
+
with Path(source).open(encoding="utf-8") as stream:
|
|
53
|
+
records = [json.loads(line) for line in stream if line.strip()]
|
|
54
|
+
elif hasattr(source, "read"):
|
|
55
|
+
replay_stream = cast(IO[str], source)
|
|
56
|
+
records = [json.loads(line) for line in replay_stream if line.strip()]
|
|
57
|
+
else:
|
|
58
|
+
records = list(source)
|
|
59
|
+
self._records = {(r["state_hash"], r["proposition"]): r for r in records}
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def identity(self) -> str:
|
|
63
|
+
return "replay:jsonl-v1"
|
|
64
|
+
|
|
65
|
+
def boolean(self, state: str, proposition: str) -> Decision[bool]:
|
|
66
|
+
digest = state_digest(state)
|
|
67
|
+
try:
|
|
68
|
+
record = self._records[(digest, proposition)]
|
|
69
|
+
except KeyError as error:
|
|
70
|
+
raise KeyError(f"no replay decision for {digest}:{proposition}") from error
|
|
71
|
+
return Decision(
|
|
72
|
+
value=bool(record["value"]),
|
|
73
|
+
probability=float(record["probability"]),
|
|
74
|
+
backend=str(record["backend"]),
|
|
75
|
+
model=record.get("model"),
|
|
76
|
+
proposition=proposition,
|
|
77
|
+
state_hash=digest,
|
|
78
|
+
metadata=record.get("metadata", {}),
|
|
79
|
+
replayed=True,
|
|
80
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""The explicit semantic value boundary."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar, Generic, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
from .cache import decision_cache_key, state_digest
|
|
8
|
+
from .config import get_config, require_backend
|
|
9
|
+
from .decision import BackendError, Decision, InvalidComparisonError
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Semantic(Generic[T]):
|
|
15
|
+
__slots__ = ("_value",)
|
|
16
|
+
__hash__: ClassVar[None] = None # type: ignore[assignment]
|
|
17
|
+
|
|
18
|
+
def __init__(self, value: T) -> None:
|
|
19
|
+
if not isinstance(value, str):
|
|
20
|
+
raise TypeError("v0.1 supports only Semantic[str]")
|
|
21
|
+
self._value = cast(str, value)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def value(self) -> T:
|
|
25
|
+
return cast(T, self._value)
|
|
26
|
+
|
|
27
|
+
def __repr__(self) -> str:
|
|
28
|
+
return f"Semantic({self._value!r})"
|
|
29
|
+
|
|
30
|
+
def __eq__(self, proposition: object) -> Decision[bool]: # type: ignore[override]
|
|
31
|
+
if isinstance(proposition, Semantic):
|
|
32
|
+
raise InvalidComparisonError(
|
|
33
|
+
"Semantic-to-Semantic equality is ambiguous; compare explicitly"
|
|
34
|
+
)
|
|
35
|
+
if not isinstance(proposition, str):
|
|
36
|
+
raise InvalidComparisonError(
|
|
37
|
+
"Semantic[str] can only be compared with a proposition string"
|
|
38
|
+
)
|
|
39
|
+
return self._decide(proposition)
|
|
40
|
+
|
|
41
|
+
def __ne__(self, proposition: object) -> Decision[bool]: # type: ignore[override]
|
|
42
|
+
return self.__eq__(proposition).negated()
|
|
43
|
+
|
|
44
|
+
def _decide(self, proposition: str) -> Decision[bool]:
|
|
45
|
+
config = get_config()
|
|
46
|
+
backend = require_backend()
|
|
47
|
+
identity = getattr(backend, "identity", type(backend).__qualname__)
|
|
48
|
+
key = decision_cache_key(
|
|
49
|
+
backend_identity=identity,
|
|
50
|
+
state=self._value,
|
|
51
|
+
proposition=proposition,
|
|
52
|
+
configuration={
|
|
53
|
+
"false_threshold": config.policy.false_threshold,
|
|
54
|
+
"true_threshold": config.policy.true_threshold,
|
|
55
|
+
"uncertainty": config.policy.uncertainty.value,
|
|
56
|
+
"fallback": config.policy.fallback,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
if config.cache is not None:
|
|
60
|
+
cached = config.cache.get(key)
|
|
61
|
+
if cached is not None:
|
|
62
|
+
return cached.with_policy(config.policy)
|
|
63
|
+
try:
|
|
64
|
+
raw = backend.boolean(self._value, proposition)
|
|
65
|
+
except (KeyboardInterrupt, SystemExit):
|
|
66
|
+
raise
|
|
67
|
+
except Exception as error:
|
|
68
|
+
if isinstance(error, BackendError):
|
|
69
|
+
raise
|
|
70
|
+
raise BackendError(f"backend {identity!r} failed: {error}") from error
|
|
71
|
+
if not isinstance(raw, Decision) or not isinstance(raw.value, bool):
|
|
72
|
+
raise BackendError("boolean backend must return Decision[bool]")
|
|
73
|
+
if raw.proposition != proposition or raw.state_hash != state_digest(self._value):
|
|
74
|
+
raise BackendError("backend returned mismatched proposition or state hash")
|
|
75
|
+
decision = raw.with_policy(config.policy)
|
|
76
|
+
if config.cache is not None:
|
|
77
|
+
config.cache.put(key, decision)
|
|
78
|
+
if config.recorder is not None:
|
|
79
|
+
config.recorder.record(decision)
|
|
80
|
+
return decision
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: semantic-python
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Experimental semantic values for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/SaiRiteshThela/semantic-python
|
|
6
|
+
Project-URL: Repository, https://github.com/SaiRiteshThela/semantic-python
|
|
7
|
+
Project-URL: Issues, https://github.com/SaiRiteshThela/semantic-python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/SaiRiteshThela/semantic-python/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Sai Ritesh Thela
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai,decision-models,python,semantic
|
|
13
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
24
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
28
|
+
Requires-Dist: twine>=6.0; extra == 'dev'
|
|
29
|
+
Provides-Extra: laya
|
|
30
|
+
Requires-Dist: laya<0.5,>=0.4.0; extra == 'laya'
|
|
31
|
+
Provides-Extra: notebook
|
|
32
|
+
Requires-Dist: ipykernel>=6.29; extra == 'notebook'
|
|
33
|
+
Provides-Extra: openai
|
|
34
|
+
Requires-Dist: openai<3,>=2; extra == 'openai'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# Semantic Python
|
|
38
|
+
|
|
39
|
+
> Python, but `==` can understand meaning.
|
|
40
|
+
|
|
41
|
+
Semantic Python is not a model like Laya or Jev. It is an opt-in Python value
|
|
42
|
+
layer that turns backend inference into inspectable decisions for normal Python
|
|
43
|
+
control flow. Only `Semantic(...)` values can invoke a model.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m pip install 'semantic-python[openai]'
|
|
49
|
+
export OPENAI_API_KEY="..."
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Demo
|
|
53
|
+
|
|
54
|
+
Use natural-language intent in ordinary loops and async code:
|
|
55
|
+
|
|
56
|
+
`functions` · `if` · `try/except` · `for` · `while` · `comprehensions` ·
|
|
57
|
+
`generators` · `classes` · `with` · `async/await`
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import asyncio
|
|
61
|
+
|
|
62
|
+
from semantic_python import (
|
|
63
|
+
OpenAIBackend,
|
|
64
|
+
Semantic,
|
|
65
|
+
UncertainDecisionError,
|
|
66
|
+
configure,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
STOP = "the user wants to stop"
|
|
70
|
+
|
|
71
|
+
configure(
|
|
72
|
+
backend=OpenAIBackend(model="gpt-6-luna"),
|
|
73
|
+
true_threshold=0.85,
|
|
74
|
+
false_threshold=0.15,
|
|
75
|
+
uncertainty="raise",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def decide(text: str):
|
|
80
|
+
return Semantic(text) == STOP
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
messages = [
|
|
84
|
+
"Continue with the next ticket.",
|
|
85
|
+
"Save the work and close this session.",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
for text in messages:
|
|
89
|
+
decision = decide(text)
|
|
90
|
+
print(f"stop={decision.value} p={decision.probability:.2f} model={decision.model}")
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
if decision:
|
|
94
|
+
print("Stop requested")
|
|
95
|
+
break
|
|
96
|
+
except UncertainDecisionError:
|
|
97
|
+
print("Send to human review")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def decide_async(text: str):
|
|
101
|
+
return await asyncio.to_thread(decide, text)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async_result = asyncio.run(decide_async(text))
|
|
105
|
+
opposite = Semantic(text) != STOP
|
|
106
|
+
|
|
107
|
+
print("async cache hit:", async_result.cached)
|
|
108
|
+
print("negated:", opposite.value, opposite.probability)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The comparison returns a bool-coercible `Decision`, so `if` and loops work
|
|
112
|
+
normally while probability, model provenance, caching, and replay remain
|
|
113
|
+
available. `!=` negates the same judgment instead of issuing a second inference.
|
|
114
|
+
|
|
115
|
+
## More Python patterns
|
|
116
|
+
|
|
117
|
+
These examples continue with the configured backend and `STOP` proposition from
|
|
118
|
+
the main demo.
|
|
119
|
+
|
|
120
|
+
### `while`
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
queue = iter(["Keep going", "Not yet", "Finished for today"])
|
|
124
|
+
message = Semantic(next(queue))
|
|
125
|
+
|
|
126
|
+
while message != STOP:
|
|
127
|
+
print("Processing:", message.value)
|
|
128
|
+
message = Semantic(next(queue))
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Comprehensions and generators
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
texts = ["Continue", "Pause here", "That is all for today"]
|
|
135
|
+
decisions = {text: decide(text) for text in texts}
|
|
136
|
+
|
|
137
|
+
stop_requests = [text for text, decision in decisions.items() if decision.value]
|
|
138
|
+
|
|
139
|
+
probabilities = (decision.probability for decision in decisions.values())
|
|
140
|
+
print(stop_requests, list(probabilities))
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Classes
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from dataclasses import dataclass
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True, slots=True)
|
|
150
|
+
class IntentRule:
|
|
151
|
+
proposition: str
|
|
152
|
+
|
|
153
|
+
def evaluate(self, text: str):
|
|
154
|
+
return Semantic(text) == self.proposition
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
stop_rule = IntentRule("the user wants to stop")
|
|
158
|
+
decision = stop_rule.evaluate("Please close the session")
|
|
159
|
+
print(decision.value, decision.probability)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Context managers and offline tests
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from semantic_python import FakeBackend, configuration
|
|
166
|
+
|
|
167
|
+
state = "I'm done"
|
|
168
|
+
fixtures = {(state, STOP): (True, 0.99)}
|
|
169
|
+
|
|
170
|
+
with configuration(backend=FakeBackend(fixtures), cache=False):
|
|
171
|
+
assert Semantic(state) == STOP
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Backends
|
|
175
|
+
|
|
176
|
+
Application code stays the same when the configured backend changes.
|
|
177
|
+
|
|
178
|
+
| Backend | Install | Purpose |
|
|
179
|
+
| --- | --- | --- |
|
|
180
|
+
| `OpenAIBackend` | `semantic-python[openai]` | Hosted inference |
|
|
181
|
+
| `LayaBackend` | `semantic-python[laya]` | Local inference |
|
|
182
|
+
| `FakeBackend` | Included | Deterministic offline tests |
|
|
183
|
+
|
|
184
|
+
OpenAI receives the wrapped state and proposition. Laya may download model
|
|
185
|
+
checkpoints on first use. FakeBackend performs no network requests.
|
|
186
|
+
|
|
187
|
+
## Semantics
|
|
188
|
+
|
|
189
|
+
| Expression | Result |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| `plain_string == other` | Ordinary Python equality |
|
|
192
|
+
| `Semantic(text) == proposition` | Inspectable semantic `Decision` |
|
|
193
|
+
| `Semantic(text) != proposition` | Negated cached judgment |
|
|
194
|
+
| `semantic is other` | Ordinary Python identity |
|
|
195
|
+
| `Semantic(...) == Semantic(...)` | Rejected as ambiguous |
|
|
196
|
+
| `hash(Semantic(...))` | Rejected; inference is never used for hashing |
|
|
197
|
+
|
|
198
|
+
Probabilities at or above `0.85` resolve true, probabilities at or below `0.15`
|
|
199
|
+
resolve false, and the default policy raises `UncertainDecisionError` between
|
|
200
|
+
those thresholds. Backend failures are raised rather than converted to `False`.
|
|
201
|
+
|
|
202
|
+
## Examples
|
|
203
|
+
|
|
204
|
+
- [OpenAI demo](examples/semantic_python_demo.ipynb)
|
|
205
|
+
- [Python control-flow demo](examples/python_primitives_demo.ipynb)
|
|
206
|
+
- [Stop loop](examples/stop_loop.py)
|
|
207
|
+
- [Ticket routing](examples/ticket_routing.py)
|
|
208
|
+
- [Uncertainty and escalation](examples/escalation.py)
|
|
209
|
+
|
|
210
|
+
The offline examples run without credentials:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
python -m pip install -e .
|
|
214
|
+
python examples/stop_loop.py
|
|
215
|
+
python examples/ticket_routing.py
|
|
216
|
+
python examples/escalation.py
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Safety
|
|
220
|
+
|
|
221
|
+
- Model probability is an estimate, not truth or authorization.
|
|
222
|
+
- Hosted inference can transmit sensitive text and incur cost.
|
|
223
|
+
- Consequential actions should use conservative thresholds and human review.
|
|
224
|
+
|
|
225
|
+
See [semantics](docs/semantics.md), [backends](docs/backends.md), and
|
|
226
|
+
[uncertainty](docs/uncertainty.md) for the complete behavior.
|
|
227
|
+
|
|
228
|
+
MIT licensed. Experimental and pre-alpha.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
semantic_python/__init__.py,sha256=sQP8Wv9-muAi8nuzFK7KJ7b5-SnnWlpVZoUlBDcIU70,1134
|
|
2
|
+
semantic_python/cache.py,sha256=sOfcKJs6JAe-gMbSrNLQ0c8ZlZoAXISRnDRA8KoI9xc,1367
|
|
3
|
+
semantic_python/config.py,sha256=RvGAfO2hVbnnSjeyXGwftEiqsoVlOqDxGU_SRnG0IjY,3198
|
|
4
|
+
semantic_python/decision.py,sha256=lLmARLTXmDwm37b_kyU8sRQw_aw0JpuZNO2nkkq4-lU,4346
|
|
5
|
+
semantic_python/py.typed,sha256=GOJzfT--I5WXaP7ohUQllUQsQIgGVdJh2w-iL5dW-vM,46
|
|
6
|
+
semantic_python/replay.py,sha256=g2OJeZofIa_BN6hjI088x8Ur5qdlgIZIsq01sPV0zv0,2831
|
|
7
|
+
semantic_python/semantic.py,sha256=fkLFcf7rqg9Vka0K5QqCKdmHRXEKJ_Wq5BWLzyaDCX8,3124
|
|
8
|
+
semantic_python/backends/__init__.py,sha256=LERLDCYvr9VxnASNK-jOTSTbQ0bX_bXhodqkk0kMwJw,257
|
|
9
|
+
semantic_python/backends/base.py,sha256=ONQEaGQ1jdQMpc27xR1ux1axfsz6HRh0ri1rO4ixDEE,986
|
|
10
|
+
semantic_python/backends/fake.py,sha256=y2B6peYWxgaiO8YF35FXScQl8b3KQG-JrlHsmDbKxuM,2099
|
|
11
|
+
semantic_python/backends/laya.py,sha256=EpZbFS5WMaCjNUbrM196KC-jfXSc_JeSuECDJ69RS9I,2818
|
|
12
|
+
semantic_python/backends/openai.py,sha256=OCHa00tTGX8yrP5Ueh-rStxGFKPa0Qr2Vcc5uHmY82I,4356
|
|
13
|
+
semantic_python-0.1.0a1.dist-info/METADATA,sha256=U8PW302ALYHUxo7AZYjcFKtaQCiETdUd9vXIGOhFJc4,6675
|
|
14
|
+
semantic_python-0.1.0a1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
15
|
+
semantic_python-0.1.0a1.dist-info/licenses/LICENSE,sha256=Px0VHU5RfTBJkwd499xoGherVtp2ENPcmNnQtZ0XC2g,1085
|
|
16
|
+
semantic_python-0.1.0a1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Semantic Python contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|