livekit-plugins-typesafe 1.8.8__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,45 @@
1
+ # Copyright 2026 LiveKit, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """TypeSafe plugin for LiveKit Agents
16
+
17
+ Jev decision models.
18
+ """
19
+
20
+ from .decision_model import DecisionModel
21
+ from .models import DecisionModels, OpenRouterDecisionModels
22
+ from .version import __version__
23
+
24
+ __all__ = ["DecisionModel", "DecisionModels", "OpenRouterDecisionModels", "__version__"]
25
+
26
+ from livekit.agents import Plugin
27
+
28
+ from .log import logger
29
+
30
+
31
+ class TypeSafePlugin(Plugin):
32
+ def __init__(self) -> None:
33
+ super().__init__(__name__, __version__, __package__, logger)
34
+
35
+
36
+ Plugin.register_plugin(TypeSafePlugin())
37
+
38
+ # Cleanup docs of unexported modules
39
+ _module = dir()
40
+ NOT_IN_ALL = [m for m in _module if m not in __all__]
41
+
42
+ __pdoc__ = {}
43
+
44
+ for n in NOT_IN_ALL:
45
+ __pdoc__[n] = False
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ from collections.abc import Mapping
6
+ from typing import Annotated, Any, Literal
7
+
8
+ import aiohttp
9
+ from pydantic import BaseModel, Field, TypeAdapter, ValidationError
10
+
11
+ from livekit.agents import (
12
+ APIConnectionError,
13
+ APIError,
14
+ APIStatusError,
15
+ APITimeoutError,
16
+ llm,
17
+ utils,
18
+ )
19
+ from livekit.agents.llm.decisions import (
20
+ Choice,
21
+ ChoiceResult,
22
+ Decision,
23
+ DecisionCapabilities,
24
+ DecisionResponse,
25
+ DecisionResult,
26
+ Probability,
27
+ ProbabilityResult,
28
+ ScoreResult,
29
+ )
30
+ from livekit.agents.types import APIConnectOptions
31
+
32
+ from .models import DecisionModels, OpenRouterDecisionModels
33
+
34
+ TYPESAFE_ENDPOINT = "https://api.typesafe.ai/v1/systemone"
35
+ OPENROUTER_ENDPOINT = "https://openrouter.ai/api/alpha/decisions"
36
+
37
+
38
+ class _NoulAnswer(BaseModel):
39
+ type: Literal["noul"]
40
+ noul: float
41
+
42
+
43
+ class _ChoiceAnswer(BaseModel):
44
+ type: Literal["choice"]
45
+ choice: str
46
+ probabilities: dict[str, float]
47
+ confidence: float | None = None
48
+
49
+
50
+ class _ScoreAnswer(BaseModel):
51
+ type: Literal["score"]
52
+ score: float
53
+ probabilities: dict[int, float]
54
+ legend: dict[int, str]
55
+ confidence: float | None = None
56
+
57
+
58
+ class _Usage(BaseModel):
59
+ input_tokens: int | None = None
60
+ output_tokens: int | None = None
61
+
62
+
63
+ _Answer = Annotated[_NoulAnswer | _ChoiceAnswer | _ScoreAnswer, Field(discriminator="type")]
64
+ _answer_adapter = TypeAdapter[_Answer](_Answer)
65
+
66
+
67
+ class _Response(BaseModel):
68
+ answers: dict[str, Any]
69
+ model: str = ""
70
+ usage: _Usage = Field(default_factory=_Usage)
71
+ id: str = ""
72
+
73
+
74
+ class DecisionModel(llm.DecisionModel):
75
+ """TypeSafe Jev, via the TypeSafe API or OpenRouter (:meth:`with_openrouter`).
76
+
77
+ Args:
78
+ model: TypeSafe model identifier.
79
+ api_key: Defaults to TYPESAFE_API_KEY.
80
+ endpoint: Complete HTTP endpoint for batched decisions.
81
+ http_session: Optional caller-owned session. Otherwise uses the job's HTTP
82
+ session. Outside a job, use ``async with utils.http_context.open()``.
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ *,
88
+ model: DecisionModels | str = "jev-1.13.0",
89
+ api_key: str | None = None,
90
+ endpoint: str = TYPESAFE_ENDPOINT,
91
+ http_session: aiohttp.ClientSession | None = None,
92
+ ) -> None:
93
+ super().__init__(
94
+ capabilities=DecisionCapabilities(decision_kinds=["probability", "choice", "score"])
95
+ )
96
+ key = api_key or os.environ.get("TYPESAFE_API_KEY")
97
+ if not key:
98
+ raise ValueError("TypeSafe requires api_key or TYPESAFE_API_KEY")
99
+ self._api_key = key
100
+ self._model = model
101
+ self._endpoint = endpoint
102
+ self._session = http_session
103
+ self._provider = "typesafe"
104
+
105
+ @classmethod
106
+ def with_openrouter(
107
+ cls,
108
+ *,
109
+ model: OpenRouterDecisionModels | str = "typesafe/jev-1.13",
110
+ api_key: str | None = None,
111
+ http_session: aiohttp.ClientSession | None = None,
112
+ ) -> DecisionModel:
113
+ """Use OpenRouter's Decisions API. ``api_key`` defaults to OPENROUTER_API_KEY."""
114
+ key = api_key or os.environ.get("OPENROUTER_API_KEY")
115
+ if not key:
116
+ raise ValueError("TypeSafe via OpenRouter requires api_key or OPENROUTER_API_KEY")
117
+ instance = cls(
118
+ model=model, api_key=key, endpoint=OPENROUTER_ENDPOINT, http_session=http_session
119
+ )
120
+ instance._provider = "openrouter"
121
+ return instance
122
+
123
+ @property
124
+ def model(self) -> str:
125
+ return self._model
126
+
127
+ @property
128
+ def provider(self) -> str:
129
+ return self._provider
130
+
131
+ async def _evaluate_impl(
132
+ self,
133
+ *,
134
+ input: str,
135
+ decisions: Mapping[str, Decision],
136
+ conn_options: APIConnectOptions,
137
+ ) -> DecisionResponse:
138
+ questions = {name: _encode_question(definition) for name, definition in decisions.items()}
139
+ session = self._session or utils.http_context.http_session()
140
+ try:
141
+ async with session.post(
142
+ self._endpoint,
143
+ headers={"Authorization": f"Bearer {self._api_key}"},
144
+ json={"model": self._model, "state": input, "questions": questions},
145
+ timeout=aiohttp.ClientTimeout(total=conn_options.timeout),
146
+ ) as response:
147
+ if response.status >= 400:
148
+ raise APIStatusError(
149
+ "TypeSafe decision request failed",
150
+ status_code=response.status,
151
+ request_id=response.headers.get("x-request-id"),
152
+ )
153
+ parsed = _Response.model_validate(await response.json())
154
+ request_id = parsed.id or response.headers.get("x-request-id", "")
155
+ results: dict[str, DecisionResult] = {}
156
+ errors: dict[str, str] = {}
157
+ for name, raw_answer in parsed.answers.items():
158
+ try:
159
+ results[name] = _decode_answer(_answer_adapter.validate_python(raw_answer))
160
+ except ValidationError:
161
+ errors[name] = "invalid Jev decision answer"
162
+ except APIError as error:
163
+ errors[name] = error.message
164
+ return DecisionResponse(
165
+ results=results,
166
+ errors=errors,
167
+ model=parsed.model,
168
+ provider=self.provider,
169
+ request_id=request_id,
170
+ input_tokens=parsed.usage.input_tokens,
171
+ output_tokens=parsed.usage.output_tokens,
172
+ )
173
+ except asyncio.TimeoutError as exc:
174
+ raise APITimeoutError("TypeSafe request timed out") from exc
175
+ except aiohttp.ContentTypeError as exc:
176
+ raise APIError("invalid TypeSafe decision response", retryable=False) from exc
177
+ except aiohttp.ClientError as exc:
178
+ raise APIConnectionError("TypeSafe connection failed") from exc
179
+ except ValueError as exc:
180
+ raise APIError("invalid TypeSafe decision response", retryable=False) from exc
181
+
182
+
183
+ def _encode_question(definition: Decision) -> dict[str, Any]:
184
+ question: dict[str, Any] = {"instructions": definition.prompt}
185
+ if isinstance(definition, Probability):
186
+ question["type"] = "noul"
187
+ elif isinstance(definition, Choice):
188
+ question.update(type="choice", criteria=definition.options)
189
+ else:
190
+ question.update(type="score", criteria=definition.levels)
191
+ return question
192
+
193
+
194
+ def _decode_answer(answer: _Answer) -> DecisionResult:
195
+ if isinstance(answer, _NoulAnswer):
196
+ return ProbabilityResult(value=answer.noul)
197
+ if isinstance(answer, _ChoiceAnswer):
198
+ return ChoiceResult(
199
+ value=answer.choice,
200
+ probabilities=answer.probabilities,
201
+ provider_data={"confidence": answer.confidence},
202
+ )
203
+ if answer.legend.keys() != answer.probabilities.keys():
204
+ raise APIError("score legend does not match distribution", retryable=False)
205
+ return ScoreResult(
206
+ value=answer.score,
207
+ probabilities=answer.probabilities,
208
+ levels=[answer.legend[index] for index in sorted(answer.legend)],
209
+ provider_data={"confidence": answer.confidence},
210
+ )
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("livekit.plugins.typesafe")
@@ -0,0 +1,5 @@
1
+ from typing import Literal
2
+
3
+ # jev-latest and jev-preview are TypeSafe aliases that move to newer releases.
4
+ DecisionModels = Literal["jev-latest", "jev-preview", "jev-1.14.0", "jev-1.13.0"]
5
+ OpenRouterDecisionModels = Literal["typesafe/jev-1.13"]
File without changes
@@ -0,0 +1,15 @@
1
+ # Copyright 2026 LiveKit, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ __version__ = "1.8.8"
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.5
2
+ Name: livekit-plugins-typesafe
3
+ Version: 1.8.8
4
+ Summary: Agent Framework plugin for TypeSafe Jev decision models.
5
+ Project-URL: Documentation, https://docs.livekit.io
6
+ Project-URL: Website, https://livekit.io/
7
+ Project-URL: Source, https://github.com/livekit/agents
8
+ Author-email: LiveKit <hello@livekit.io>
9
+ License-Expression: Apache-2.0
10
+ Keywords: audio,decisions,jev,livekit,realtime,typesafe,video,webrtc
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.10.0
18
+ Requires-Dist: livekit-agents>=1.8.8
19
+ Description-Content-Type: text/markdown
20
+
21
+ # TypeSafe plugin for LiveKit Agents
22
+
23
+ Support for [TypeSafe](https://typesafe.ai) Jev decision models. Jev evaluates probability
24
+ questions, named choices, and ordered scores over a conversation in one request.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install livekit-plugins-typesafe
30
+ ```
31
+
32
+ ## Pre-requisites
33
+
34
+ Set `TYPESAFE_API_KEY` for the TypeSafe API, or `OPENROUTER_API_KEY` to use Jev through
35
+ OpenRouter's Decisions API.
36
+
37
+ ## Usage
38
+
39
+ ```python
40
+ from livekit.agents import llm, utils
41
+ from livekit.plugins import typesafe
42
+
43
+ async with utils.http_context.open():
44
+ model = typesafe.DecisionModel(model="jev-latest")
45
+ # or: typesafe.DecisionModel.with_openrouter()
46
+ context = llm.ChatContext.empty()
47
+ context.add_message(role="user", content="Please let me speak to a person.")
48
+ response = await model.evaluate(
49
+ input=context,
50
+ decisions={
51
+ "handoff": llm.Probability("The caller requests a human."),
52
+ "intent": llm.Choice(
53
+ "What does the caller want?",
54
+ options={"booking": "Make a reservation", "other": "Anything else"},
55
+ ),
56
+ "frustration": llm.Score(
57
+ "How frustrated is the caller?", levels=["Calm", "Annoyed", "Angry"]
58
+ ),
59
+ },
60
+ )
61
+ for name, result in response.results.items():
62
+ print(name, result.value)
63
+ ```
64
+
65
+ `input` also accepts a string, which is sent as-is. Each requested decision appears in
66
+ either `response.results` or `response.errors`. Request failures and malformed responses
67
+ raise.
68
+
69
+ ## Models
70
+
71
+ `jev-latest` is the most recent stable release and `jev-preview` the most recent release
72
+ overall; TypeSafe moves both aliases as new versions ship. Pin `jev-1.14.0` or
73
+ `jev-1.13.0` for a fixed version.
74
+
75
+ ## Notes
76
+
77
+ - Jev rounds probabilities and scores to two decimal places. Results keep the provider's
78
+ values.
79
+ - Score answers must return a legend that matches the requested levels.
80
+ - `provider_data["confidence"]` describes how concentrated the distribution is, not the
81
+ probability that the answer is correct.
82
+ - A conversation is sent as a `role: text` transcript of user and assistant messages.
83
+ System and developer messages, agent instructions, and non-text content are never sent.
@@ -0,0 +1,9 @@
1
+ livekit/plugins/typesafe/__init__.py,sha256=Lq6jsP2P2t6HgPrA0MK78TLFnmjH1Shlp-BLhZut8vc,1274
2
+ livekit/plugins/typesafe/decision_model.py,sha256=gpYbGYUovTxCrMmSnbTbkMgMIdqPqTOthffQwMBrCY4,7238
3
+ livekit/plugins/typesafe/log.py,sha256=wDP2QbBz-5K1z86lbNu1uuY-dBbuK5MoLtyXk4sPzq0,71
4
+ livekit/plugins/typesafe/models.py,sha256=f-4RjbHHVnTvM13v0SXxgg4p1Aa_Wv_KhNKLTrEm6KU,245
5
+ livekit/plugins/typesafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ livekit/plugins/typesafe/version.py,sha256=DVN3ewC-8eWaCrgOnx9gWXZNQ9s80t7Q2iys4FwK_Mk,600
7
+ livekit_plugins_typesafe-1.8.8.dist-info/METADATA,sha256=4sjJwe-5KiLQO5pfVgorqEfs46NzvTsSP9eFhKEj4uA,3095
8
+ livekit_plugins_typesafe-1.8.8.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
9
+ livekit_plugins_typesafe-1.8.8.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