jev2semopt 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jev2semopt/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """Public bounded semantic operators; importing does not register pandas accessors."""
2
+ from llm2jev import Choice, Noul, Score
3
+
4
+ from .adapters.llm2jev import LLM2JevBackend
5
+ from .adapters.jev import JevAPIError, JevBackend
6
+ from .contracts import BoundDecision, DecisionBackend
7
+ from .decorators import decision
8
+ from .engine import SemEngine
9
+ from .execution import EvaluationError
10
+
11
+ __all__ = [
12
+ "BoundDecision", "Choice", "DecisionBackend", "EvaluationError", "LLM2JevBackend",
13
+ "JevAPIError", "JevBackend", "Noul", "Score", "SemEngine", "decision",
14
+ ]
@@ -0,0 +1 @@
1
+ """Provider adapters translate public decision APIs into the execution ports."""
@@ -0,0 +1,123 @@
1
+ """Translate the documented TypeSafe HTTP contract into borrowed typed decisions."""
2
+ from copy import deepcopy
3
+ from typing import Any, Dict, Optional, Protocol
4
+
5
+ from llm2jev import Answer, Choice, ChoiceAnswer, JSONValue, Noul, NoulAnswer, Question, Score, ScoreAnswer, State
6
+
7
+ from ..contracts import BoundDecision
8
+ from ..execution import EvaluationError, snapshot, validate_answer
9
+
10
+
11
+ class _Response(Protocol):
12
+ """The narrow HTTP response surface consumed by the official API adapter."""
13
+
14
+ status_code: int
15
+
16
+ # Decode the response without exposing its contents in adapter diagnostics.
17
+ def json(self) -> Any:
18
+ ...
19
+
20
+
21
+ class _Client(Protocol):
22
+ """A caller-owned HTTP client, structurally satisfied by httpx.Client."""
23
+
24
+ # Authentication, base URL, timeouts, and connection lifetimes belong to the client.
25
+ def post(self, url: str, *, json: Any) -> _Response:
26
+ ...
27
+
28
+
29
+ class JevAPIError(EvaluationError):
30
+ """Sanitized official API failure with optional HTTP status, never response bodies."""
31
+
32
+ # Retain only a numeric status useful for caller-controlled retry or authentication handling.
33
+ def __init__(self, message: str, *, status_code: Optional[int] = None) -> None:
34
+ super().__init__(message, status_code=status_code)
35
+
36
+
37
+ # Canonical decimal rubric keys prevent collisions such as '0' and '00' during conversion.
38
+ def _levels(value: object) -> Dict[int, Any]:
39
+ if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
40
+ raise ValueError("invalid rubric map")
41
+ expected = {str(i) for i in range(len(value))}
42
+ if set(value) != expected:
43
+ raise ValueError("invalid rubric positions")
44
+ return {int(key): description for key, description in value.items()}
45
+
46
+
47
+ # Delegate numeric invariants to the existing public domain constructors, preserving confidence.
48
+ def _answer(payload: object, question: Question) -> Answer:
49
+ if not isinstance(payload, dict) or payload.get("type") != question.type:
50
+ raise ValueError("unexpected answer family")
51
+ if isinstance(question, Noul):
52
+ return NoulAnswer(noul=payload["noul"])
53
+ if isinstance(question, Choice):
54
+ answer = ChoiceAnswer(choice=payload["choice"], confidence=payload["confidence"],
55
+ probabilities=payload["probabilities"])
56
+ return answer
57
+ if isinstance(question, Score):
58
+ scored = ScoreAnswer(score=payload["score"], confidence=payload["confidence"],
59
+ legend=_levels(payload["legend"]), probabilities=_levels(payload["probabilities"]))
60
+ return scored
61
+ raise TypeError("unsupported question")
62
+
63
+
64
+ class _BoundJev:
65
+ """An immutable rule snapshot sharing a borrowed authenticated HTTP connection pool."""
66
+
67
+ # Store a detached rule; no record state or authentication data is retained by the binding.
68
+ def __init__(self, client: _Client, model: str, question: Question) -> None:
69
+ self._client = client
70
+ self._model = model
71
+ self._question = deepcopy(question)
72
+
73
+ # Make one request per record; fail closed without retries or generated-text fallbacks.
74
+ def evaluate(self, *, state: State) -> Answer:
75
+ if not isinstance(state, (str, dict, list)):
76
+ raise ValueError("Jev state must be a string, object, or array")
77
+ detached = snapshot(state)[1]
78
+ try:
79
+ response = self._client.post("systemone", json={
80
+ "model": self._model, "state": detached,
81
+ "questions": {"decision": self._question.to_dict()},
82
+ })
83
+ except Exception:
84
+ raise JevAPIError("Jev request failed") from None
85
+ if response.status_code != 200:
86
+ raise JevAPIError("Jev returned an unsuccessful HTTP status", status_code=response.status_code)
87
+ try:
88
+ body = response.json()
89
+ if not isinstance(body, dict) or not isinstance(body.get("model"), str) or not body["model"]:
90
+ raise ValueError("invalid response envelope")
91
+ answers = body["answers"]
92
+ if not isinstance(answers, dict) or set(answers) != {"decision"}:
93
+ raise ValueError("invalid answer envelope")
94
+ answer = _answer(answers["decision"], self._question)
95
+ validate_answer(self._question, answer)
96
+ return answer
97
+ except Exception:
98
+ raise JevAPIError("Jev returned an invalid decision response") from None
99
+
100
+
101
+ class JevBackend:
102
+ """Use the official TypeSafe API through a caller-owned HTTP client.
103
+
104
+ Configure the client's base_url as https://api.typesafe.ai/v1/ and its
105
+ Authorization header as Bearer <key>. This adapter never closes the client.
106
+ """
107
+
108
+ # Model aliases are explicit; the default follows the official documented public alias.
109
+ def __init__(self, client: _Client, *, model: str = "jev-latest") -> None:
110
+ if not isinstance(model, str) or not model.strip():
111
+ raise ValueError("model must be a nonempty string")
112
+ self._client = client
113
+ self._model = model
114
+
115
+ # Validate API-specific limits before any request, including on empty tables.
116
+ def bind(self, *, question: Question) -> BoundDecision:
117
+ if not isinstance(question, (Noul, Choice, Score)):
118
+ raise TypeError("question must be Noul, Choice, or Score")
119
+ if question.instructions is None or question.instructions == "":
120
+ raise ValueError("official Jev questions require instructions")
121
+ if isinstance(question, Choice) and not 2 <= len(question.criteria) <= 255:
122
+ raise ValueError("JevBackend supports 2 through 255 choice options")
123
+ return _BoundJev(self._client, self._model, question)
@@ -0,0 +1,31 @@
1
+ """Borrow an LLM2Jev service without owning, importing, or closing model providers."""
2
+ from llm2jev import JSONValue, Answer, BoundEvaluator, LLM2Jev, Question, State
3
+
4
+ from ..contracts import BoundDecision
5
+
6
+
7
+ class _BoundLLM2Jev:
8
+ """Extract a single named answer from a public LLM2Jev response."""
9
+
10
+ # The adapter owns only the binding; the caller owns runtime shutdown.
11
+ def __init__(self, evaluator: BoundEvaluator) -> None:
12
+ self._evaluator = evaluator
13
+
14
+ # Missing answers remain errors, never synthetic negative decisions.
15
+ def evaluate(self, *, state: State) -> Answer:
16
+ return self._evaluator.evaluate(state=state).answers["decision"]
17
+
18
+
19
+ class LLM2JevBackend:
20
+ """Adapt an explicitly configured LLM2Jev service and model name."""
21
+
22
+ # Keep encoder, distribution policy, and device configuration on the service.
23
+ def __init__(self, client: LLM2Jev, *, model: str) -> None:
24
+ if not isinstance(model, str) or not model.strip():
25
+ raise ValueError("model must be a nonempty string")
26
+ self._client = client
27
+ self._model = model
28
+
29
+ # Compile reusable questions once for each relational operation or decorated function.
30
+ def bind(self, *, question: Question) -> BoundDecision:
31
+ return _BoundLLM2Jev(self._client.bind(model=self._model, questions={"decision": question}))
@@ -0,0 +1,20 @@
1
+ """Typed decision ports isolate relational execution from model providers."""
2
+ from typing import Protocol
3
+
4
+ from llm2jev import JSONValue, Answer, Question, State
5
+
6
+
7
+ class BoundDecision(Protocol):
8
+ """A reusable question with no ownership of the underlying model runtime."""
9
+
10
+ # Evaluate one detached state and return the answer family requested at binding.
11
+ def evaluate(self, *, state: State) -> Answer:
12
+ ...
13
+
14
+
15
+ class DecisionBackend(Protocol):
16
+ """Bind questions independently of records and provider resource lifetimes."""
17
+
18
+ # Invalid rules must fail at binding, including for empty tables.
19
+ def bind(self, *, question: Question) -> BoundDecision:
20
+ ...
@@ -0,0 +1,27 @@
1
+ """Decorators turn state-building functions into reusable typed decisions."""
2
+ from functools import wraps
3
+ from inspect import iscoroutinefunction, isgeneratorfunction, isasyncgenfunction
4
+ from typing import Any, Callable
5
+
6
+ from llm2jev import JSONValue, Answer, Question, State
7
+
8
+ from .engine import SemEngine
9
+
10
+
11
+ # Bind once at decoration time; each call evaluates fresh state without cross-call caching.
12
+ def decision(engine: SemEngine, *, question: Question) -> Callable[[Callable[..., State]], Callable[..., Answer]]:
13
+ """Decorate a synchronous JSON state builder; return its typed Jev answer."""
14
+ session = engine._session(question)
15
+
16
+ # Preserve the state builder's metadata so debugging still identifies application intent.
17
+ def decorate(builder: Callable[..., State]) -> Callable[..., Answer]:
18
+ if not callable(builder) or iscoroutinefunction(builder) or isgeneratorfunction(builder) or isasyncgenfunction(builder):
19
+ raise TypeError("decision requires a synchronous JSON state builder")
20
+ # Builder failures belong to application code and propagate without reinterpretation.
21
+ @wraps(builder)
22
+ def evaluate(*args: Any, **kwargs: Any) -> Answer:
23
+ return session.run([builder(*args, **kwargs)])[0]
24
+
25
+ return evaluate
26
+
27
+ return decorate
jev2semopt/engine.py ADDED
@@ -0,0 +1,152 @@
1
+ """Relational operators consume typed decisions and preserve positional row identity."""
2
+ import math
3
+ from itertools import product
4
+ from typing import Iterable, List, Optional, Sequence, Tuple, cast
5
+
6
+ import pandas as pd
7
+ from llm2jev import JSONValue, Choice, ChoiceAnswer, Noul, NoulAnswer, Question, Score, ScoreAnswer, State
8
+
9
+ from .contracts import DecisionBackend
10
+ from .execution import EvaluationSession
11
+ from .table import output_column, records, validate_frame
12
+
13
+
14
+ # A threshold is model support, not an assertion about calibrated correctness.
15
+ def _threshold(value: float) -> None:
16
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or not 0 <= value <= 1:
17
+ raise ValueError("threshold must be finite and between zero and one")
18
+
19
+
20
+ # Instructions remain reusable rules; row values are never interpolated into them.
21
+ def _predicate(instructions: str) -> Noul:
22
+ if not isinstance(instructions, str) or not instructions.strip():
23
+ raise ValueError("instructions must be a nonempty string")
24
+ return Noul(instructions=instructions)
25
+
26
+
27
+ class SemEngine:
28
+ """Explicit backend configuration with no process-global settings or owned runtime."""
29
+
30
+ # Duplicate reuse is opt-in because stochastic/stateful backends can change their answers.
31
+ def __init__(self, backend: DecisionBackend, *, deduplicate: bool = False) -> None:
32
+ if not isinstance(deduplicate, bool):
33
+ raise ValueError("deduplicate must be boolean")
34
+ self._backend = backend
35
+ self._deduplicate = deduplicate
36
+
37
+ # Share the evaluation policy across dataframe operators and function decorators.
38
+ def _session(self, question: Question) -> EvaluationSession:
39
+ return EvaluationSession(self._backend, question, deduplicate=self._deduplicate)
40
+
41
+ # Keep rows at or above the threshold, retaining duplicate index labels in input order.
42
+ def sem_filter(
43
+ self, frame: pd.DataFrame, instructions: str, *, threshold: float = 0.5,
44
+ columns: Optional[Sequence[str]] = None, probability_column: Optional[str] = None,
45
+ ) -> pd.DataFrame:
46
+ _threshold(threshold)
47
+ if probability_column is not None:
48
+ output_column(frame, probability_column)
49
+ states = records(frame, columns)
50
+ session = self._session(_predicate(instructions))
51
+ probabilities = [cast(NoulAnswer, answer).noul for answer in session.run(states)]
52
+ positions = [i for i, probability in enumerate(probabilities) if probability >= threshold]
53
+ result = frame.iloc[positions].copy()
54
+ if probability_column is not None:
55
+ result[probability_column] = pd.array([probabilities[i] for i in positions], dtype="float64")
56
+ return result
57
+
58
+ # A bounded map adds chosen labels; it never parses generated text or invents categories.
59
+ def sem_map(
60
+ self, frame: pd.DataFrame, question: Choice, *, output: str,
61
+ columns: Optional[Sequence[str]] = None,
62
+ ) -> pd.DataFrame:
63
+ if not isinstance(question, Choice):
64
+ raise TypeError("sem_map requires a Choice question")
65
+ output_column(frame, output)
66
+ states = records(frame, columns)
67
+ answers = self._session(question).run(states)
68
+ result = frame.copy()
69
+ result[output] = pd.array([cast(ChoiceAnswer, answer).choice for answer in answers], dtype="object")
70
+ return result
71
+
72
+ # Scores are expectations over an explicit ordinal rubric, not pairwise comparisons.
73
+ def sem_score(
74
+ self, frame: pd.DataFrame, question: Score, *, output: str = "_score",
75
+ columns: Optional[Sequence[str]] = None,
76
+ ) -> pd.DataFrame:
77
+ if not isinstance(question, Score):
78
+ raise TypeError("sem_score requires a Score question")
79
+ output_column(frame, output)
80
+ states = records(frame, columns)
81
+ answers = self._session(question).run(states)
82
+ result = frame.copy()
83
+ result[output] = pd.array([cast(ScoreAnswer, answer).score for answer in answers], dtype="float64")
84
+ return result
85
+
86
+ # Stable descending ranking breaks ties by original row position, including duplicate indices.
87
+ def sem_topk(
88
+ self, frame: pd.DataFrame, question: Score, *, k: int,
89
+ output: str = "_score", columns: Optional[Sequence[str]] = None,
90
+ ) -> pd.DataFrame:
91
+ if isinstance(k, bool) or not isinstance(k, int) or k < 0:
92
+ raise ValueError("k must be a nonnegative integer")
93
+ validate_frame(frame)
94
+ source = frame.iloc[:0] if k == 0 else frame
95
+ scored = self.sem_score(source, question, output=output, columns=columns)
96
+ return scored.sort_values(output, ascending=False, kind="mergesort").iloc[:k].copy()
97
+
98
+ # Inner join evaluates explicit positional pairs; candidate pruning changes recall scope.
99
+ def sem_join(
100
+ self, left: pd.DataFrame, right: pd.DataFrame, instructions: str, *,
101
+ threshold: float = 0.5, left_columns: Optional[Sequence[str]] = None,
102
+ right_columns: Optional[Sequence[str]] = None,
103
+ candidates: Optional[Iterable[Tuple[int, int]]] = None, max_pairs: int = 100000,
104
+ ) -> pd.DataFrame:
105
+ _threshold(threshold)
106
+ if isinstance(max_pairs, bool) or not isinstance(max_pairs, int) or max_pairs < 0:
107
+ raise ValueError("max_pairs must be a nonnegative integer")
108
+ validate_frame(left)
109
+ validate_frame(right)
110
+ if candidates is None and len(left) * len(right) > max_pairs:
111
+ raise ValueError("join exceeds max_pairs; supply a shortlist or raise the limit")
112
+ pairs = self._pairs(left, right, candidates, max_pairs)
113
+ left_states = records(left, left_columns)
114
+ right_states = records(right, right_columns)
115
+ states: List[State] = [{"left": left_states[i], "right": right_states[j]} for i, j in pairs]
116
+ answers = self._session(_predicate(instructions)).run(states)
117
+ selected = [(i, j, cast(NoulAnswer, answer).noul)
118
+ for (i, j), answer in zip(pairs, answers)
119
+ if cast(NoulAnswer, answer).noul >= threshold]
120
+ # iloc avoids label-based multiplication when either index contains duplicates.
121
+ lpart = left.iloc[[i for i, _, _ in selected]].reset_index(drop=True).add_prefix("left.")
122
+ rpart = right.iloc[[j for _, j, _ in selected]].reset_index(drop=True).add_prefix("right.")
123
+ result = pd.concat([lpart, rpart], axis=1)
124
+ result["_left_position"] = pd.Series([i for i, _, _ in selected], dtype="int64")
125
+ result["_right_position"] = pd.Series([j for _, j, _ in selected], dtype="int64")
126
+ result["_probability"] = pd.Series([p for _, _, p in selected], dtype="float64")
127
+ return result
128
+
129
+ # Validate all candidate pairs before model work; each pair occurs at most once.
130
+ @staticmethod
131
+ def _pairs(
132
+ left: pd.DataFrame, right: pd.DataFrame,
133
+ candidates: Optional[Iterable[Tuple[int, int]]], max_pairs: int,
134
+ ) -> List[Tuple[int, int]]:
135
+ source = product(range(len(left)), range(len(right))) if candidates is None else candidates
136
+ pairs: List[Tuple[int, int]] = []
137
+ seen = set()
138
+ for count, pair in enumerate(source):
139
+ if count >= max_pairs:
140
+ raise ValueError("candidate stream exceeds max_pairs")
141
+ if not isinstance(pair, (tuple, list)) or len(pair) != 2:
142
+ raise ValueError("each candidate must contain two integer positions")
143
+ i, j = pair
144
+ if any(isinstance(x, bool) or not isinstance(x, int) for x in (i, j)):
145
+ raise ValueError("candidate positions must be integers")
146
+ if not 0 <= i < len(left) or not 0 <= j < len(right):
147
+ raise ValueError("candidate position out of range")
148
+ if (i, j) in seen:
149
+ raise ValueError("candidate pairs must be unique")
150
+ seen.add((i, j))
151
+ pairs.append((i, j))
152
+ return pairs
@@ -0,0 +1,102 @@
1
+ """Operation-local evaluation owns validation, duplicate reuse, and safe failures."""
2
+ import json
3
+ import math
4
+ from copy import deepcopy
5
+ from typing import Dict, Iterable, List, Optional, Tuple, Type
6
+
7
+ from llm2jev import JSONValue, Answer, Choice, ChoiceAnswer, Noul, NoulAnswer, Question, Score, ScoreAnswer, State
8
+
9
+ from .contracts import BoundDecision, DecisionBackend
10
+
11
+
12
+ _ANSWER_TYPES: Dict[Type[object], Type[object]] = {Noul: NoulAnswer, Choice: ChoiceAnswer, Score: ScoreAnswer}
13
+
14
+
15
+ class EvaluationError(RuntimeError):
16
+ """A backend failed or violated its answer contract; messages omit payloads."""
17
+
18
+ # Only a checked HTTP status crosses the safe failure boundary alongside generic messages.
19
+ def __init__(self, message: str, *, status_code: Optional[int] = None) -> None:
20
+ super().__init__(message)
21
+ self.status_code = status_code
22
+
23
+
24
+ # Validate JSON recursively without coercing keys, tuples, dates, or missing values.
25
+ def _check_json(value: object) -> None:
26
+ if value is None or isinstance(value, (str, bool, int)):
27
+ return
28
+ if isinstance(value, float) and math.isfinite(value):
29
+ return
30
+ if isinstance(value, list):
31
+ for item in value:
32
+ _check_json(item)
33
+ return
34
+ if isinstance(value, dict) and all(isinstance(key, str) for key in value):
35
+ for item in value.values():
36
+ _check_json(item)
37
+ return
38
+ raise ValueError("state must contain finite JSON values and string object keys")
39
+
40
+
41
+ # JSON round-tripping detaches nested state before a backend can mutate caller data.
42
+ def snapshot(state: State) -> Tuple[str, State]:
43
+ try:
44
+ _check_json(state)
45
+ encoded = json.dumps(state, sort_keys=True, ensure_ascii=True, allow_nan=False)
46
+ return encoded, json.loads(encoded)
47
+ except (ValueError, TypeError, RecursionError, OverflowError):
48
+ raise ValueError("state must contain finite, acyclic JSON values") from None
49
+
50
+
51
+ # One contract check serves both direct adapter bindings and dataframe execution.
52
+ def validate_answer(question: Question, answer: Answer) -> None:
53
+ if type(question) not in _ANSWER_TYPES or not isinstance(answer, _ANSWER_TYPES[type(question)]):
54
+ raise TypeError("unexpected answer family")
55
+ if isinstance(question, Choice) and isinstance(answer, ChoiceAnswer):
56
+ if set(answer.probabilities) != set(question.criteria):
57
+ raise ValueError("unexpected choice labels")
58
+ if isinstance(question, Score) and isinstance(answer, ScoreAnswer):
59
+ if dict(answer.legend) != dict(enumerate(question.criteria)):
60
+ raise ValueError("unexpected score rubric")
61
+
62
+
63
+ class EvaluationSession:
64
+ """One question binding; cache lifetime is exactly one operator execution."""
65
+
66
+ # Binding before reading rows makes invalid rules fail even on empty inputs.
67
+ def __init__(self, backend: DecisionBackend, question: Question, *, deduplicate: bool) -> None:
68
+ if type(question) not in _ANSWER_TYPES:
69
+ raise TypeError("question must be Noul, Choice, or Score")
70
+ self._question = deepcopy(question)
71
+ self._deduplicate = deduplicate
72
+ try:
73
+ self._bound: BoundDecision = backend.bind(question=deepcopy(self._question))
74
+ except Exception:
75
+ raise EvaluationError("decision binding failed") from None
76
+
77
+ # Fail closed on family/schema mismatch; a typed answer alone does not prove rule alignment.
78
+ def _evaluate(self, state: State, position: int) -> Answer:
79
+ try:
80
+ answer = self._bound.evaluate(state=state)
81
+ validate_answer(self._question, answer)
82
+ return deepcopy(answer)
83
+ except Exception as error:
84
+ status = error.status_code if isinstance(error, EvaluationError) else None
85
+ if isinstance(status, bool) or not isinstance(status, int) or not 100 <= status <= 599:
86
+ status = None
87
+ raise EvaluationError("decision evaluation failed at position {}".format(position), status_code=status) from None
88
+
89
+ # Cache keys include the full projected state, never dataframe indices or mutable globals.
90
+ def run(self, states: Iterable[State]) -> List[Answer]:
91
+ cache: Dict[str, Answer] = {}
92
+ answers: List[Answer] = []
93
+ for position, state in enumerate(states):
94
+ key, detached = snapshot(state)
95
+ if self._deduplicate and key in cache:
96
+ answer = cache[key]
97
+ else:
98
+ answer = self._evaluate(detached, position)
99
+ if self._deduplicate:
100
+ cache[key] = answer
101
+ answers.append(answer)
102
+ return answers
jev2semopt/pandas.py ADDED
@@ -0,0 +1,36 @@
1
+ """Opt-in pandas accessor registration delegates all semantics to an explicit engine."""
2
+ from typing import Any
3
+
4
+ import pandas as pd
5
+ from llm2jev import Choice, Score
6
+
7
+ from .engine import SemEngine
8
+
9
+
10
+ @pd.api.extensions.register_dataframe_accessor("jev")
11
+ class JevAccessor:
12
+ """Use df.jev.sem_filter(engine, ...) without process-global model settings."""
13
+
14
+ # Pandas owns the accessor lifetime; it never retains a runtime or a decision cache.
15
+ def __init__(self, frame: pd.DataFrame) -> None:
16
+ self._frame = frame
17
+
18
+ # All validation and threshold semantics stay with the relational implementation.
19
+ def sem_filter(self, engine: SemEngine, instructions: str, **kwargs: Any) -> pd.DataFrame:
20
+ return engine.sem_filter(self._frame, instructions, **kwargs)
21
+
22
+ # Forward bounded mapping without maintaining a second operator implementation.
23
+ def sem_map(self, engine: SemEngine, question: Choice, **kwargs: Any) -> pd.DataFrame:
24
+ return engine.sem_map(self._frame, question, **kwargs)
25
+
26
+ # Keep rubric interpretation exclusively inside the shared score operator.
27
+ def sem_score(self, engine: SemEngine, question: Score, **kwargs: Any) -> pd.DataFrame:
28
+ return engine.sem_score(self._frame, question, **kwargs)
29
+
30
+ # Delegate tie handling and k validation to the engine.
31
+ def sem_topk(self, engine: SemEngine, question: Score, **kwargs: Any) -> pd.DataFrame:
32
+ return engine.sem_topk(self._frame, question, **kwargs)
33
+
34
+ # Explicit engine injection prevents model configuration from leaking across dataframes.
35
+ def sem_join(self, engine: SemEngine, right: pd.DataFrame, instructions: str, **kwargs: Any) -> pd.DataFrame:
36
+ return engine.sem_join(self._frame, right, instructions, **kwargs)
jev2semopt/py.typed ADDED
File without changes
jev2semopt/table.py ADDED
@@ -0,0 +1,40 @@
1
+ """Dataframe boundaries preserve row positions while isolating decision state."""
2
+ from typing import List, Optional, Sequence
3
+
4
+ import pandas as pd
5
+ from llm2jev import JSONValue, State
6
+
7
+ from .execution import snapshot
8
+
9
+
10
+ # Flat unique string columns make state field ownership unambiguous.
11
+ def validate_frame(frame: pd.DataFrame) -> None:
12
+ if not isinstance(frame, pd.DataFrame):
13
+ raise TypeError("expected a pandas DataFrame")
14
+ if not frame.columns.is_unique or any(not isinstance(key, str) for key in frame.columns):
15
+ raise ValueError("dataframe columns must be unique strings")
16
+
17
+
18
+ # Snapshot only explicitly selected fields; dataframe index labels never enter model state.
19
+ def records(frame: pd.DataFrame, columns: Optional[Sequence[str]]) -> List[State]:
20
+ validate_frame(frame)
21
+ if isinstance(columns, (str, bytes)):
22
+ raise ValueError("columns must be a sequence of column names")
23
+ selected = list(frame.columns) if columns is None else list(columns)
24
+ if any(not isinstance(key, str) for key in selected) or len(set(selected)) != len(selected):
25
+ raise ValueError("selected columns must be unique strings")
26
+ if any(key not in frame.columns for key in selected):
27
+ raise ValueError("selected column is absent from the dataframe")
28
+ if not selected:
29
+ return [{} for _ in range(len(frame))]
30
+ return [snapshot(dict(zip(selected, values)))[1]
31
+ for values in frame.loc[:, selected].itertuples(index=False, name=None)]
32
+
33
+
34
+ # Reject overwrites before inference to avoid wasted calls and silent data loss.
35
+ def output_column(frame: pd.DataFrame, name: str) -> None:
36
+ validate_frame(frame)
37
+ if not isinstance(name, str) or not name:
38
+ raise ValueError("output column must be a nonempty string")
39
+ if name in frame.columns:
40
+ raise ValueError("output column already exists")
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.4
2
+ Name: jev2semopt
3
+ Version: 0.1.0
4
+ Summary: Bounded semantic dataframe operators powered by Jev-style decisions
5
+ Project-URL: Repository, https://github.com/Qingbolan/Jev2SemOpt
6
+ Project-URL: Documentation, https://github.com/Qingbolan/Jev2SemOpt/tree/main/docs
7
+ Author-email: "Silan.Hu" <silan.hu@u.nus.edu>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ License-File: NOTICE
11
+ Keywords: dataframe,jev,llm,semantic-operators
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.8
16
+ Requires-Dist: llm2jev<0.3,>=0.2.1
17
+ Requires-Dist: pandas<3,>=1.5
18
+ Provides-Extra: jev
19
+ Requires-Dist: httpx<1,>=0.27; extra == 'jev'
20
+ Provides-Extra: ollama
21
+ Requires-Dist: llm2jev[ollama]<0.3,>=0.2.1; extra == 'ollama'
22
+ Provides-Extra: transformers
23
+ Requires-Dist: llm2jev[transformers]<0.3,>=0.2.1; extra == 'transformers'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Jev2SemOpt — Typed decisions over tables
27
+
28
+ **Filter tickets. Assign queues. Rank records. Match pairs.**
29
+
30
+ A support pipeline needs to keep refund requests, assign a fixed queue, and count
31
+ the resulting tickets. Jev2SemOpt makes those dataset operations explicit: select
32
+ the fields the model may see, bind a typed question once, evaluate each record,
33
+ and apply the result with ordinary Pandas operations.
34
+
35
+ Jev2SemOpt is an independent, early-stage Python library inspired by
36
+ [LOTUS semantic operators](https://github.com/lotus-data/lotus), using
37
+ [LLM2Jev](https://github.com/Qingbolan/llm2jev-releases) for Jev-style decisions.
38
+ The Jev interface concepts and `Choice`, `Score`, `Noul` vocabulary originate with
39
+ [TypeSafe](https://typesafe.ai/blog/introducing-system-one-models-and-jev).
40
+ Use a local LLM2Jev runtime or the official TypeSafe HTTP API through `JevBackend`.
41
+ The HTTP adapter is contract-tested; hosted Jev accuracy has not been measured.
42
+
43
+ ## Install and run
44
+
45
+ Python **3.8+**. The core requires Pandas and LLM2Jev; model providers are opt-in.
46
+
47
+ ```bash
48
+ pip install jev2semopt
49
+ # Official hosted Jev (requires a TypeSafe API key):
50
+ pip install 'jev2semopt[jev]'
51
+ # Local providers:
52
+ pip install 'jev2semopt[transformers]'
53
+ # or: pip install 'jev2semopt[ollama]'
54
+ ```
55
+
56
+ For a model-free example or development:
57
+
58
+ ```bash
59
+ git clone https://github.com/Qingbolan/Jev2SemOpt.git
60
+ cd Jev2SemOpt
61
+ uv sync --group dev
62
+ uv run python examples/refund_pipeline.py
63
+ uv run python examples/decorated_decision.py
64
+ ```
65
+
66
+ These examples use synthetic scores and require no model or API key. Production
67
+ and development dependencies resolve from PyPI. See [release verification](docs/releasing.md).
68
+
69
+ ## A refund pipeline
70
+
71
+ Configure execution explicitly. The application owns the runtime and closes it;
72
+ Jev2SemOpt only borrows it. The following requires compatible local model weights
73
+ and a model-appropriate LLM2Jev encoder/label configuration; it is an API example,
74
+ not a validated inference configuration.
75
+
76
+ ```python
77
+ import pandas as pd
78
+ from llm2jev import LLM2Jev, TransformersRuntime
79
+ from jev2semopt import LLM2JevBackend, SemEngine, Choice
80
+
81
+ tickets = pd.DataFrame({
82
+ "id": [101, 102],
83
+ "text": ["Please refund my order.", "Where is my parcel?"],
84
+ })
85
+
86
+ with TransformersRuntime("/path/to/local/model", device="cpu") as runtime:
87
+ engine = SemEngine(LLM2JevBackend(
88
+ LLM2Jev(runtime=runtime, model_identity=runtime.identity),
89
+ model=runtime.identity.name,
90
+ ))
91
+ refunds = engine.sem_filter(
92
+ tickets, "Does text explicitly request a refund?",
93
+ columns=["text"], threshold=0.7, probability_column="refund_support",
94
+ )
95
+ routed = engine.sem_map(
96
+ refunds,
97
+ Choice(instructions="Which team should handle text?", criteria={
98
+ "billing": "Payments, invoices, or refunds",
99
+ "delivery": "Shipping, tracking, or missing parcels",
100
+ "other": "Requests outside billing and delivery",
101
+ }),
102
+ columns=["text"], output="queue",
103
+ )
104
+ counts = routed.groupby("queue").size()
105
+ ```
106
+
107
+ Only `text` enters the decision state. IDs remain available in the output. Column
108
+ names in instructions refer to keys in that state; there is **no `{column}` string
109
+ interpolation**. Thresholds require validation on your own labeled workload.
110
+
111
+ ## Operators and boundaries
112
+
113
+ Official hosted Jev is also supported through `JevBackend`, using a caller-owned
114
+ HTTP client and TypeSafe API key. See the [official API integration](docs/official-jev.md)
115
+ for setup and verification limits. An OpenRouter key does not authenticate TypeSafe.
116
+
117
+ | Operator | Jev decision | Output and scope |
118
+ | --- | --- | --- |
119
+ | `sem_filter(frame, instructions, ...)` | `Noul` | Rows with support **≥ threshold**, original order and index |
120
+ | `sem_map(frame, Choice(...), output=...)` | `Choice` | All rows plus a chosen label from supplied alternatives |
121
+ | `sem_score(frame, Score(...), ...)` | `Score` | All rows plus expected ordinal rubric level |
122
+ | `sem_topk(frame, Score(...), k=...)` | `Score` | Largest expected levels; stable source-order ties |
123
+ | `sem_join(left, right, instructions, ...)` | `Noul` | Inner join over candidate pairs; namespaced source fields and positions |
124
+
125
+ LOTUS provides a broader semantic operator model, including generated projections,
126
+ extraction, aggregation, and comparator-based ranking. Jev2SemOpt deliberately
127
+ restricts mappings to finite alternatives and ranks by an explicit ordinal rubric.
128
+ It is not a drop-in LOTUS replacement. Free-text extraction, summaries, vector
129
+ search, learned cascades, SQL planning, asynchronous execution, and distributed
130
+ execution are not implemented. Count/group/sum the typed outputs with Pandas;
131
+ there is no misleading `sem_agg` alias for a generative summary.
132
+
133
+ ```python
134
+ from jev2semopt import Score
135
+
136
+ ranked = engine.sem_topk(
137
+ tickets,
138
+ Score(instructions="How urgent is text?", criteria=[
139
+ "Routine request", "Time-sensitive issue", "Immediate safety or service emergency",
140
+ ]),
141
+ columns=["text"], k=10, output="urgency",
142
+ )
143
+
144
+ matches = engine.sem_join(
145
+ tickets, policies,
146
+ "Does left.text describe a case covered by right.policy?",
147
+ left_columns=["text"], right_columns=["policy"],
148
+ candidates=[(0, 1), (1, 0)], # row POSITIONS, not index labels
149
+ threshold=0.8,
150
+ )
151
+ ```
152
+
153
+ The join output includes `left.<column>`, `right.<column>`, `_left_position`,
154
+ `_right_position`, and `_probability`. Without candidates it evaluates every pair,
155
+ subject to a default 100,000-pair limit. A shortlist can reduce work but can also
156
+ exclude true matches; the caller owns its recall. See [API contracts](docs/api.md).
157
+
158
+ ## Python-native integration
159
+
160
+ The Pandas accessor is opt-in and uses Pandas' registration decorator. It forwards
161
+ to the same engine implementation and does not install global model settings:
162
+
163
+ ```python
164
+ import jev2semopt.pandas
165
+
166
+ refunds = tickets.jev.sem_filter(
167
+ engine, "Does text request a refund?", columns=["text"], threshold=0.7,
168
+ )
169
+ ```
170
+
171
+ Use `@decision` when application code already builds the state for one decision:
172
+
173
+ ```python
174
+ from jev2semopt import Noul, decision
175
+
176
+ @decision(engine, question=Noul(instructions="Does text explicitly request a refund?"))
177
+ def refund_requested(text):
178
+ return {"text": text}
179
+
180
+ answer = refund_requested("Please refund my order.")
181
+ print(answer.noul)
182
+ ```
183
+
184
+ The decorator binds once, preserves function metadata with `functools.wraps`, and
185
+ evaluates new state on each call. It does not cache results across calls. Decorated
186
+ functions must be synchronous JSON-state builders and now return typed answers.
187
+
188
+ ## Measured results
189
+
190
+ On **50 balanced SciFact records**, the same GPT-4o-mini scored **31/50 with LOTUS**
191
+ and **32/50 with Jev-style / LLM2Jev**. The one-record difference does not establish
192
+ an accuracy improvement: the paired 95% interval spans −8 to +12 percentage points.
193
+ Jev-style improves precision but lowers recall and F1 in this run.
194
+
195
+ ![Small-sample accuracy, precision, recall and F1 for LOTUS and Jev-style](https://raw.githubusercontent.com/Qingbolan/Jev2SemOpt/v0.1.0/docs/assets/scifact-small-sample.png)
196
+
197
+ On **400 local Qwen2.5-0.5B candidate pairs**, Jev-style filtering was 1.97× faster,
198
+ but both filters had roughly 4% precision. Three-level Jev-style scoring was 2.70×
199
+ slower than LOTUS scoring and reduced ranking quality. BM25 had the highest nDCG.
200
+
201
+ ![Local ranking quality and operator time, including the BM25 baseline](https://raw.githubusercontent.com/Qingbolan/Jev2SemOpt/v0.1.0/docs/assets/scifact-local.png)
202
+
203
+ These are **official SciFact dataset subsets with adapted protocols**, not full
204
+ LOTUS paper reproduction or measurements of TypeSafe's hosted Jev model. Unjudged
205
+ documents count as negatives under the qrel convention. Prompts differ between
206
+ operators; these experiments do not isolate probability assembly as the cause.
207
+ [Sampling, confusion matrices, costs, raw observations, and reproduction](https://github.com/Qingbolan/Jev2SemOpt/blob/v0.1.0/benchmarks/README.md).
208
+
209
+ ## Execution cost and score meaning
210
+
211
+ `SemEngine(backend, deduplicate=True)` reuses identical selected JSON state **within
212
+ one operation**. This is opt-in and assumes the backend is deterministic and has no
213
+ per-call side effects. There is no global cache and no stale reuse across operations.
214
+ Question binding avoids recompilation; it is not a KV cache or a batched model call.
215
+
216
+ For `N` rows, filtering requires `N` one-candidate evaluations; mapping with `C`
217
+ choices requires `N × C` binary candidates; scoring with `R` rubric levels requires
218
+ `N × R`. An exhaustive join needs `L × R` pair evaluations. Duplicate reuse reduces
219
+ these counts to unique selected states. Calls are sequential. Join candidates and
220
+ results are materialized in memory; this version targets bounded in-memory tables.
221
+
222
+ No end-to-end speedup, calibrated correctness, or equivalence to LOTUS quality is
223
+ claimed. `Noul` is label-conditioned support; `Score` is an expected equally spaced
224
+ rubric index. These are decision signals, not probabilities that the answer is right.
225
+ LLM2Jev's Transformers adapter reads next-token logits; its Ollama adapter requests
226
+ one token to obtain exact binary logprobs. Jev2SemOpt never parses generated prose.
227
+ See [evaluation protocol](docs/evaluation.md).
228
+
229
+ ## Architecture and development
230
+
231
+ ```text
232
+ DataFrame API / @decision
233
+
234
+ SemEngine — positional relational semantics
235
+
236
+ EvaluationSession — one rule, detached state, operation-local reuse
237
+
238
+ DecisionBackend.bind → BoundDecision.evaluate → typed Answer
239
+
240
+ ├─ LLM2JevBackend → public LLM2Jev API → caller-owned runtime
241
+ └─ JevBackend → TypeSafe HTTP API → caller-owned HTTP client
242
+ ```
243
+
244
+ The engine depends on a backend protocol, not Ollama or Transformers. Models,
245
+ prompts, device configuration, binary labels, and runtime lifecycle stay in
246
+ LLM2Jev for local execution; hosted execution uses the supplied HTTP client. There is no new resource lifecycle to duplicate in the table layer.
247
+
248
+ ```text
249
+ src/jev2semopt/
250
+ ├── contracts.py Backend and bound-decision protocols
251
+ ├── execution.py State isolation, answer checks, safe failure boundary
252
+ ├── engine.py Filter/map/score/top-k/join semantics
253
+ ├── table.py Dataframe schema and state projection
254
+ ├── decorators.py Function-to-decision binding
255
+ ├── pandas.py Opt-in accessor registration
256
+ └── adapters/ LLM2Jev service and official Jev HTTP integration
257
+ ```
258
+
259
+ Run the checks in [CONTRIBUTING.md](CONTRIBUTING.md). Architectural decisions,
260
+ privacy constraints, and delivery status are documented in
261
+ [architecture](docs/architecture.md), [privacy](docs/privacy.md), and
262
+ [implementation plan](docs/implementation-plan.md). Attribution is recorded in
263
+ [NOTICE](NOTICE). Licensed under [MIT](LICENSE); [dependency and benchmark attribution](docs/licenses.md).
264
+
265
+ [Local verification record](docs/verification.md): 50 deterministic tests passed
266
+ on Python 3.8, 3.12, and 3.14. Real-model observations are recorded separately in
267
+ the [benchmark report](benchmarks/README.md).
@@ -0,0 +1,16 @@
1
+ jev2semopt/__init__.py,sha256=DfrWETT5xRzGcahIXLz2tdkjj1guIBmiDwUuA09BHUk,556
2
+ jev2semopt/contracts.py,sha256=PWHLZH7HvXH61joLzMXZV1OxKGDm7JhBzkXes34FLwk,686
3
+ jev2semopt/decorators.py,sha256=8Rm68mkq8Bq_60B78wKq60UIXlHl0tkw1hPWlQdKEXs,1303
4
+ jev2semopt/engine.py,sha256=XnFoiD3VRDQAZtTY2ZR17FZJpErtsQDhwp8m2v0-r-Q,7973
5
+ jev2semopt/execution.py,sha256=e_t0ORM-QxjzIlFxw7SdSfjp9eUafVVo9KS79NF4yqE,4648
6
+ jev2semopt/pandas.py,sha256=DjkKl1D0vU0imJxVtW1KC6vFvi-k1oNgcj7-J3DRDWk,1755
7
+ jev2semopt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ jev2semopt/table.py,sha256=NRmg761DdImnFKkAuabSm3V6SfQZUkkHt0WhRx2B2y4,1859
9
+ jev2semopt/adapters/__init__.py,sha256=DYTwzYMUX1Tn4xZruJfMLlE5CZSHGL9L8li1Sb4KYxg,81
10
+ jev2semopt/adapters/jev.py,sha256=BCkDst93KK50Op5KnXqf6MEVU3_c8MmHk6AjBh65NhQ,5705
11
+ jev2semopt/adapters/llm2jev.py,sha256=6PYgT72RKuw7djoDXjFZGEvynCYTmIFvrM9ZPUrg_9I,1349
12
+ jev2semopt-0.1.0.dist-info/METADATA,sha256=hy-Fsv1kfRPyN53cCYQDR_sJQu6TsPZi4ANWbOuOi48,12060
13
+ jev2semopt-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
14
+ jev2semopt-0.1.0.dist-info/licenses/LICENSE,sha256=dOMFrR-h0QzbHz5boUZGzEEl3ZnRDObocLTx1FJa3EI,1093
15
+ jev2semopt-0.1.0.dist-info/licenses/NOTICE,sha256=xrBwCjFzAALOziyyK1gaZZHAgKetmTHLO6FChzR9mVY,1057
16
+ jev2semopt-0.1.0.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 Silan.Hu and Jev2SemOpt 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.
@@ -0,0 +1,20 @@
1
+ Jev2SemOpt
2
+ Copyright (c) 2026 Silan.Hu and Jev2SemOpt contributors
3
+
4
+ The semantic-operator model and operator vocabulary are inspired by LOTUS:
5
+ https://github.com/lotus-data/lotus
6
+ https://arxiv.org/abs/2407.11418
7
+ LOTUS is Apache-2.0 licensed. Its operator implementation is an external
8
+ benchmark dependency, not part of the Jev2SemOpt runtime. The reranking benchmark
9
+ objective/prompt is adapted from its llm-eval experiment; see
10
+ docs/licenses/LOTUS-Apache-2.0.txt for the upstream license.
11
+
12
+ Jev state/questions and Choice, Score, Noul interface concepts originate with TypeSafe:
13
+ https://typesafe.ai/blog/introducing-system-one-models-and-jev
14
+ This project is independent. Its HTTP adapter targets the documented TypeSafe
15
+ API; hosted model behavior and accuracy have not been verified.
16
+
17
+ LLM2Jev provides public decision types and execution as an external dependency:
18
+ https://github.com/Qingbolan/llm2jev-releases
19
+ Its architectural separation and documentation structure informed this project.
20
+ LLM2Jev is MIT licensed, copyright (c) 2026 LLM2Jev contributors.