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.
@@ -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
+ )
@@ -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)
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.