origin-lang 0.1.0__tar.gz
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.
- origin_lang-0.1.0/PKG-INFO +15 -0
- origin_lang-0.1.0/origin_lang/__init__.py +13 -0
- origin_lang-0.1.0/origin_lang/boundary.py +41 -0
- origin_lang-0.1.0/origin_lang/value.py +147 -0
- origin_lang-0.1.0/origin_lang.egg-info/PKG-INFO +15 -0
- origin_lang-0.1.0/origin_lang.egg-info/SOURCES.txt +9 -0
- origin_lang-0.1.0/origin_lang.egg-info/dependency_links.txt +1 -0
- origin_lang-0.1.0/origin_lang.egg-info/top_level.txt +1 -0
- origin_lang-0.1.0/pyproject.toml +23 -0
- origin_lang-0.1.0/setup.cfg +4 -0
- origin_lang-0.1.0/tests/test_value.py +242 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: origin-lang
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Every value is either in the interior or at a boundary. The runtime knows which.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/knoxvilledatabase/origin
|
|
7
|
+
Keywords: ai,safety,boundary,inference,type-system
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Python: >=3.10
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Origin: every value is either in the interior or at a boundary.
|
|
3
|
+
|
|
4
|
+
from origin_lang import Value, boundary
|
|
5
|
+
|
|
6
|
+
This is the pure Python version. Runtime enforcement, same concept.
|
|
7
|
+
For compile-time enforcement, see the Rust crate.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from origin_lang.value import Value
|
|
11
|
+
from origin_lang.boundary import boundary, BoundaryKind
|
|
12
|
+
|
|
13
|
+
__all__ = ["Value", "boundary", "BoundaryKind"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The @boundary decorator: mark a class as a boundary kind.
|
|
3
|
+
|
|
4
|
+
@boundary
|
|
5
|
+
class LowConfidence:
|
|
6
|
+
confidence: float
|
|
7
|
+
threshold: float
|
|
8
|
+
|
|
9
|
+
This turns the class into a frozen dataclass that can be used
|
|
10
|
+
as the reason field in Value.boundary().
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BoundaryKind:
|
|
17
|
+
"""Base class for boundary kinds. Optional — @boundary is simpler."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def boundary(cls):
|
|
22
|
+
"""Decorator that turns a class into a boundary kind.
|
|
23
|
+
|
|
24
|
+
Wraps it as a dataclass and marks it as a BoundaryKind.
|
|
25
|
+
|
|
26
|
+
@boundary
|
|
27
|
+
class LowConfidence:
|
|
28
|
+
confidence: float
|
|
29
|
+
threshold: float
|
|
30
|
+
|
|
31
|
+
Value.boundary(LowConfidence(confidence=0.61, threshold=0.85), last=diagnosis)
|
|
32
|
+
"""
|
|
33
|
+
# Make it a dataclass if it isn't already
|
|
34
|
+
cls = dataclass(frozen=True)(cls)
|
|
35
|
+
|
|
36
|
+
# Register as a BoundaryKind
|
|
37
|
+
if BoundaryKind not in cls.__mro__:
|
|
38
|
+
# Can't modify __bases__ on a dataclass, so register it differently
|
|
39
|
+
cls._is_boundary_kind = True
|
|
40
|
+
|
|
41
|
+
return cls
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The core type. Interior or Boundary. One distinction. Every domain.
|
|
3
|
+
|
|
4
|
+
Value<T, B> in Rust. Value[T, B] in Python. Same idea:
|
|
5
|
+
- Interior(value): the value is in safe territory.
|
|
6
|
+
- Boundary(reason, last): the value crossed the edge.
|
|
7
|
+
reason says which boundary. last says what the computation knew.
|
|
8
|
+
|
|
9
|
+
The `last` field is what neither exceptions nor Result carry.
|
|
10
|
+
It's always here. You can't drop it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
from typing import TypeVar, Generic, Callable
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
U = TypeVar("U")
|
|
18
|
+
B = TypeVar("B")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Value(Generic[T, B]):
|
|
22
|
+
"""A value that is either interior or at a boundary."""
|
|
23
|
+
|
|
24
|
+
__slots__ = ("_interior", "_reason", "_last", "_is_interior")
|
|
25
|
+
|
|
26
|
+
def __init__(self, *, _interior=None, _reason=None, _last=None, _is_interior=True):
|
|
27
|
+
self._interior = _interior
|
|
28
|
+
self._reason = _reason
|
|
29
|
+
self._last = _last
|
|
30
|
+
self._is_interior = _is_interior
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def interior(value: T) -> Value[T, B]:
|
|
34
|
+
"""Construct an interior value."""
|
|
35
|
+
return Value(_interior=value, _is_interior=True)
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def boundary(reason: B, *, last: T) -> Value[T, B]:
|
|
39
|
+
"""Construct a boundary value with a reason and last known value."""
|
|
40
|
+
if last is None:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"boundary() requires last= — the whole point is preserving "
|
|
43
|
+
"what the computation knew before it hit the boundary"
|
|
44
|
+
)
|
|
45
|
+
return Value(_reason=reason, _last=last, _is_interior=False)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def is_interior(self) -> bool:
|
|
49
|
+
return self._is_interior
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def is_boundary(self) -> bool:
|
|
53
|
+
return not self._is_interior
|
|
54
|
+
|
|
55
|
+
def unwrap(self) -> T:
|
|
56
|
+
"""Get the interior value, or raise if boundary.
|
|
57
|
+
|
|
58
|
+
In boundary-checked code, prefer match instead.
|
|
59
|
+
"""
|
|
60
|
+
if self._is_interior:
|
|
61
|
+
return self._interior
|
|
62
|
+
raise BoundaryError(self._reason, self._last)
|
|
63
|
+
|
|
64
|
+
def or_default(self, fallback: T) -> T:
|
|
65
|
+
"""Get the interior value, or return a fallback."""
|
|
66
|
+
if self._is_interior:
|
|
67
|
+
return self._interior
|
|
68
|
+
return fallback
|
|
69
|
+
|
|
70
|
+
def map(self, f: Callable[[T], U]) -> Value[U, B]:
|
|
71
|
+
"""Map over the value. Boundaries propagate with mapped last."""
|
|
72
|
+
if self._is_interior:
|
|
73
|
+
return Value.interior(f(self._interior))
|
|
74
|
+
return Value.boundary(self._reason, last=f(self._last))
|
|
75
|
+
|
|
76
|
+
def __repr__(self):
|
|
77
|
+
if self._is_interior:
|
|
78
|
+
return f"Value.Interior({self._interior!r})"
|
|
79
|
+
return f"Value.Boundary({self._reason!r}, last={self._last!r})"
|
|
80
|
+
|
|
81
|
+
# -- Pattern matching support (Python 3.10+) --
|
|
82
|
+
|
|
83
|
+
class Interior:
|
|
84
|
+
"""Match pattern for interior values.
|
|
85
|
+
|
|
86
|
+
case Value.Interior(diagnosis):
|
|
87
|
+
treat(diagnosis)
|
|
88
|
+
"""
|
|
89
|
+
__match_args__ = ("value",)
|
|
90
|
+
|
|
91
|
+
def __init__(self, value):
|
|
92
|
+
self.value = value
|
|
93
|
+
|
|
94
|
+
class Boundary:
|
|
95
|
+
"""Match pattern for boundary values.
|
|
96
|
+
|
|
97
|
+
case Value.Boundary(LowConfidence(confidence=c), last=diagnosis):
|
|
98
|
+
refer_to_specialist(diagnosis, c)
|
|
99
|
+
"""
|
|
100
|
+
__match_args__ = ("reason", "last")
|
|
101
|
+
|
|
102
|
+
def __init__(self, reason, last):
|
|
103
|
+
self.reason = reason
|
|
104
|
+
self.last = last
|
|
105
|
+
|
|
106
|
+
# Enable generic subscript: Value[T, B]
|
|
107
|
+
def __class_getitem__(cls, params):
|
|
108
|
+
return cls
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def match(value: Value[T, B]):
|
|
112
|
+
"""Unpack a Value for pattern matching.
|
|
113
|
+
|
|
114
|
+
With Python 3.10+ match/case:
|
|
115
|
+
|
|
116
|
+
match match(result):
|
|
117
|
+
case Value.Interior(diagnosis):
|
|
118
|
+
treat(diagnosis)
|
|
119
|
+
case Value.Boundary(LowConfidence(confidence=c), last=diagnosis):
|
|
120
|
+
refer_to_specialist(diagnosis, c)
|
|
121
|
+
|
|
122
|
+
Or with isinstance:
|
|
123
|
+
|
|
124
|
+
m = match(result)
|
|
125
|
+
if isinstance(m, Value.Interior):
|
|
126
|
+
treat(m.value)
|
|
127
|
+
elif isinstance(m, Value.Boundary):
|
|
128
|
+
handle(m.reason, m.last)
|
|
129
|
+
"""
|
|
130
|
+
if value.is_interior:
|
|
131
|
+
return Value.Interior(value._interior)
|
|
132
|
+
return Value.Boundary(value._reason, value._last)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class BoundaryError(Exception):
|
|
136
|
+
"""Raised when unwrap() is called on a boundary value.
|
|
137
|
+
|
|
138
|
+
Carries the reason and the last known value, because even
|
|
139
|
+
in an exception, the information shouldn't be lost.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, reason, last):
|
|
143
|
+
self.reason = reason
|
|
144
|
+
self.last = last
|
|
145
|
+
super().__init__(
|
|
146
|
+
f"called unwrap() on a Boundary: {reason!r} (last={last!r})"
|
|
147
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: origin-lang
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Every value is either in the interior or at a boundary. The runtime knows which.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/knoxvilledatabase/origin
|
|
7
|
+
Keywords: ai,safety,boundary,inference,type-system
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Python: >=3.10
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
origin_lang
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "origin-lang"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Every value is either in the interior or at a boundary. The runtime knows which."
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
keywords = ["ai", "safety", "boundary", "inference", "type-system"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Topic :: Software Development :: Libraries",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/knoxvilledatabase/origin"
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Tests for Origin's Python implementation."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from origin_lang import Value, boundary
|
|
6
|
+
from origin_lang.value import match, BoundaryError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# -- Domain types --
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Diagnosis:
|
|
13
|
+
condition: str
|
|
14
|
+
icd_code: str
|
|
15
|
+
confidence: float
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@boundary
|
|
19
|
+
class LowConfidence:
|
|
20
|
+
confidence: float
|
|
21
|
+
threshold: float
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@boundary
|
|
25
|
+
class Hallucinated:
|
|
26
|
+
claim: str
|
|
27
|
+
evidence_score: float
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@boundary
|
|
31
|
+
class OutOfDomain:
|
|
32
|
+
distance: float
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# -- Construction --
|
|
36
|
+
|
|
37
|
+
class TestConstruction:
|
|
38
|
+
def test_interior(self):
|
|
39
|
+
v = Value.interior(42)
|
|
40
|
+
assert v.is_interior
|
|
41
|
+
assert not v.is_boundary
|
|
42
|
+
|
|
43
|
+
def test_boundary(self):
|
|
44
|
+
v = Value.boundary(LowConfidence(0.3, 0.85), last="partial result")
|
|
45
|
+
assert v.is_boundary
|
|
46
|
+
assert not v.is_interior
|
|
47
|
+
|
|
48
|
+
def test_boundary_requires_last(self):
|
|
49
|
+
with pytest.raises(ValueError, match="requires last"):
|
|
50
|
+
Value.boundary(LowConfidence(0.3, 0.85), last=None)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# -- The point: last is always preserved --
|
|
54
|
+
|
|
55
|
+
class TestLastPreserved:
|
|
56
|
+
def test_last_carries_diagnosis(self):
|
|
57
|
+
"""The moment: 'I've never had this before. I always lost that value.'"""
|
|
58
|
+
diagnosis = Diagnosis("Pneumonia", "J18.9", 0.61)
|
|
59
|
+
v = Value.boundary(
|
|
60
|
+
LowConfidence(confidence=0.61, threshold=0.85),
|
|
61
|
+
last=diagnosis,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
m = match(v)
|
|
65
|
+
assert isinstance(m, Value.Boundary)
|
|
66
|
+
assert m.last.condition == "Pneumonia"
|
|
67
|
+
assert m.last.confidence == 0.61
|
|
68
|
+
assert m.reason.confidence == 0.61
|
|
69
|
+
|
|
70
|
+
def test_last_carries_through_hallucination(self):
|
|
71
|
+
diagnosis = Diagnosis("Time travel syndrome", "Z99.99", 0.03)
|
|
72
|
+
v = Value.boundary(
|
|
73
|
+
Hallucinated(claim="Time travel syndrome", evidence_score=0.03),
|
|
74
|
+
last=diagnosis,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
m = match(v)
|
|
78
|
+
assert isinstance(m, Value.Boundary)
|
|
79
|
+
assert m.last.condition == "Time travel syndrome"
|
|
80
|
+
assert m.reason.evidence_score == 0.03
|
|
81
|
+
|
|
82
|
+
def test_interior_has_value(self):
|
|
83
|
+
diagnosis = Diagnosis("ACS", "I21.9", 0.92)
|
|
84
|
+
v = Value.interior(diagnosis)
|
|
85
|
+
|
|
86
|
+
m = match(v)
|
|
87
|
+
assert isinstance(m, Value.Interior)
|
|
88
|
+
assert m.value.condition == "ACS"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# -- Pattern matching (Python 3.10+) --
|
|
92
|
+
|
|
93
|
+
class TestPatternMatching:
|
|
94
|
+
def test_match_interior(self):
|
|
95
|
+
v = Value.interior(Diagnosis("ACS", "I21.9", 0.92))
|
|
96
|
+
|
|
97
|
+
result = match(v)
|
|
98
|
+
match result:
|
|
99
|
+
case Value.Interior(d):
|
|
100
|
+
assert d.condition == "ACS"
|
|
101
|
+
case _:
|
|
102
|
+
pytest.fail("expected Interior")
|
|
103
|
+
|
|
104
|
+
def test_match_boundary_with_last(self):
|
|
105
|
+
diagnosis = Diagnosis("Pneumonia", "J18.9", 0.61)
|
|
106
|
+
v = Value.boundary(
|
|
107
|
+
LowConfidence(confidence=0.61, threshold=0.85),
|
|
108
|
+
last=diagnosis,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
result = match(v)
|
|
112
|
+
match result:
|
|
113
|
+
case Value.Boundary(LowConfidence(confidence=c, threshold=t), last=d):
|
|
114
|
+
assert c == 0.61
|
|
115
|
+
assert t == 0.85
|
|
116
|
+
assert d.condition == "Pneumonia"
|
|
117
|
+
case _:
|
|
118
|
+
pytest.fail("expected Boundary with LowConfidence")
|
|
119
|
+
|
|
120
|
+
def test_match_different_boundary_kinds(self):
|
|
121
|
+
v1 = Value.boundary(
|
|
122
|
+
LowConfidence(0.3, 0.85),
|
|
123
|
+
last=Diagnosis("Unknown", "R69", 0.3),
|
|
124
|
+
)
|
|
125
|
+
v2 = Value.boundary(
|
|
126
|
+
Hallucinated("Temporal displacement", 0.03),
|
|
127
|
+
last=Diagnosis("Temporal displacement", "Z99.99", 0.03),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
m1 = match(v1)
|
|
131
|
+
m2 = match(v2)
|
|
132
|
+
|
|
133
|
+
match m1:
|
|
134
|
+
case Value.Boundary(LowConfidence(), last=d):
|
|
135
|
+
assert d.condition == "Unknown"
|
|
136
|
+
case _:
|
|
137
|
+
pytest.fail("expected LowConfidence")
|
|
138
|
+
|
|
139
|
+
match m2:
|
|
140
|
+
case Value.Boundary(Hallucinated(claim=c), last=d):
|
|
141
|
+
assert c == "Temporal displacement"
|
|
142
|
+
assert d.condition == "Temporal displacement"
|
|
143
|
+
case _:
|
|
144
|
+
pytest.fail("expected Hallucinated")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# -- Unwrap --
|
|
148
|
+
|
|
149
|
+
class TestUnwrap:
|
|
150
|
+
def test_unwrap_interior(self):
|
|
151
|
+
v = Value.interior(42)
|
|
152
|
+
assert v.unwrap() == 42
|
|
153
|
+
|
|
154
|
+
def test_unwrap_boundary_raises(self):
|
|
155
|
+
v = Value.boundary(LowConfidence(0.3, 0.85), last="partial")
|
|
156
|
+
with pytest.raises(BoundaryError) as exc_info:
|
|
157
|
+
v.unwrap()
|
|
158
|
+
# Even the exception preserves the information
|
|
159
|
+
assert exc_info.value.last == "partial"
|
|
160
|
+
assert exc_info.value.reason == LowConfidence(0.3, 0.85)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# -- or_default --
|
|
164
|
+
|
|
165
|
+
class TestOrDefault:
|
|
166
|
+
def test_interior_ignores_fallback(self):
|
|
167
|
+
v = Value.interior(42)
|
|
168
|
+
assert v.or_default(0) == 42
|
|
169
|
+
|
|
170
|
+
def test_boundary_uses_fallback(self):
|
|
171
|
+
v = Value.boundary(LowConfidence(0.3, 0.85), last="partial")
|
|
172
|
+
assert v.or_default("fallback") == "fallback"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# -- Map --
|
|
176
|
+
|
|
177
|
+
class TestMap:
|
|
178
|
+
def test_map_interior(self):
|
|
179
|
+
v = Value.interior(5)
|
|
180
|
+
result = v.map(lambda x: x * 2)
|
|
181
|
+
assert result.is_interior
|
|
182
|
+
assert result.unwrap() == 10
|
|
183
|
+
|
|
184
|
+
def test_map_boundary_preserves_reason_and_maps_last(self):
|
|
185
|
+
v = Value.boundary(LowConfidence(0.3, 0.85), last=5)
|
|
186
|
+
result = v.map(lambda x: x * 2)
|
|
187
|
+
assert result.is_boundary
|
|
188
|
+
m = match(result)
|
|
189
|
+
assert m.reason == LowConfidence(0.3, 0.85)
|
|
190
|
+
assert m.last == 10
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# -- Repr --
|
|
194
|
+
|
|
195
|
+
class TestRepr:
|
|
196
|
+
def test_interior_repr(self):
|
|
197
|
+
v = Value.interior(42)
|
|
198
|
+
assert repr(v) == "Value.Interior(42)"
|
|
199
|
+
|
|
200
|
+
def test_boundary_repr(self):
|
|
201
|
+
v = Value.boundary(LowConfidence(0.3, 0.85), last="partial")
|
|
202
|
+
assert "Boundary" in repr(v)
|
|
203
|
+
assert "last=" in repr(v)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# -- Pipeline demo --
|
|
207
|
+
|
|
208
|
+
class TestPipeline:
|
|
209
|
+
"""Same scenario as the Rust comparison: 5-layer healthcare pipeline."""
|
|
210
|
+
|
|
211
|
+
def infer(self, symptoms: list[str]) -> Value[Diagnosis, LowConfidence]:
|
|
212
|
+
if not symptoms:
|
|
213
|
+
return Value.boundary(
|
|
214
|
+
OutOfDomain(distance=1.0),
|
|
215
|
+
last=Diagnosis("Unknown", "R69", 0.0),
|
|
216
|
+
)
|
|
217
|
+
if "chest pain" in symptoms:
|
|
218
|
+
return Value.interior(Diagnosis("ACS", "I21.9", 0.92))
|
|
219
|
+
return Value.boundary(
|
|
220
|
+
LowConfidence(confidence=0.30, threshold=0.50),
|
|
221
|
+
last=Diagnosis("Nonspecific", "R69", 0.30),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def test_interior_pipeline(self):
|
|
225
|
+
result = self.infer(["chest pain"])
|
|
226
|
+
assert result.is_interior
|
|
227
|
+
assert result.unwrap().condition == "ACS"
|
|
228
|
+
|
|
229
|
+
def test_boundary_preserves_last(self):
|
|
230
|
+
result = self.infer(["fatigue"])
|
|
231
|
+
assert result.is_boundary
|
|
232
|
+
m = match(result)
|
|
233
|
+
# This is the moment. The diagnosis is still here.
|
|
234
|
+
assert m.last.condition == "Nonspecific"
|
|
235
|
+
assert m.last.confidence == 0.30
|
|
236
|
+
|
|
237
|
+
def test_empty_symptoms_out_of_domain(self):
|
|
238
|
+
result = self.infer([])
|
|
239
|
+
assert result.is_boundary
|
|
240
|
+
m = match(result)
|
|
241
|
+
assert isinstance(m.reason, OutOfDomain)
|
|
242
|
+
assert m.last.condition == "Unknown"
|