livekit-plugins-typesafe 1.8.8__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.
@@ -0,0 +1,180 @@
1
+ **/.vscode
2
+ **/.DS_Store
3
+ .env
4
+
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # pdm
109
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110
+ #pdm.lock
111
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112
+ # in version control.
113
+ # https://pdm.fming.dev/#use-with-ide
114
+ .pdm.toml
115
+
116
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117
+ __pypackages__/
118
+
119
+ # Celery stuff
120
+ celerybeat-schedule
121
+ celerybeat.pid
122
+
123
+ # SageMath parsed files
124
+ *.sage.py
125
+
126
+ # Environments
127
+ .env
128
+ .venv
129
+ env/
130
+ venv/
131
+ ENV/
132
+ env.bak/
133
+ venv.bak/
134
+
135
+ # Spyder project settings
136
+ .spyderproject
137
+ .spyproject
138
+
139
+ # Rope project settings
140
+ .ropeproject
141
+
142
+ # mkdocs documentation
143
+ /site
144
+
145
+ # mypy
146
+ .mypy_cache/
147
+ .dmypy.json
148
+ dmypy.json
149
+
150
+ # trunk
151
+ .trunk/
152
+
153
+ # Pyre type checker
154
+ .pyre/
155
+
156
+ # pytype static type analyzer
157
+ .pytype/
158
+
159
+ # Cython debug symbols
160
+ cython_debug/
161
+
162
+ # PyCharm
163
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
164
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
165
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
166
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
167
+ .idea/
168
+
169
+ node_modules
170
+
171
+ credentials.json
172
+ pyrightconfig.json
173
+ docs/
174
+
175
+ # Database files
176
+ *.db
177
+
178
+
179
+ # Examples for development
180
+ examples/dev/*
@@ -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,63 @@
1
+ # TypeSafe plugin for LiveKit Agents
2
+
3
+ Support for [TypeSafe](https://typesafe.ai) Jev decision models. Jev evaluates probability
4
+ questions, named choices, and ordered scores over a conversation in one request.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install livekit-plugins-typesafe
10
+ ```
11
+
12
+ ## Pre-requisites
13
+
14
+ Set `TYPESAFE_API_KEY` for the TypeSafe API, or `OPENROUTER_API_KEY` to use Jev through
15
+ OpenRouter's Decisions API.
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ from livekit.agents import llm, utils
21
+ from livekit.plugins import typesafe
22
+
23
+ async with utils.http_context.open():
24
+ model = typesafe.DecisionModel(model="jev-latest")
25
+ # or: typesafe.DecisionModel.with_openrouter()
26
+ context = llm.ChatContext.empty()
27
+ context.add_message(role="user", content="Please let me speak to a person.")
28
+ response = await model.evaluate(
29
+ input=context,
30
+ decisions={
31
+ "handoff": llm.Probability("The caller requests a human."),
32
+ "intent": llm.Choice(
33
+ "What does the caller want?",
34
+ options={"booking": "Make a reservation", "other": "Anything else"},
35
+ ),
36
+ "frustration": llm.Score(
37
+ "How frustrated is the caller?", levels=["Calm", "Annoyed", "Angry"]
38
+ ),
39
+ },
40
+ )
41
+ for name, result in response.results.items():
42
+ print(name, result.value)
43
+ ```
44
+
45
+ `input` also accepts a string, which is sent as-is. Each requested decision appears in
46
+ either `response.results` or `response.errors`. Request failures and malformed responses
47
+ raise.
48
+
49
+ ## Models
50
+
51
+ `jev-latest` is the most recent stable release and `jev-preview` the most recent release
52
+ overall; TypeSafe moves both aliases as new versions ship. Pin `jev-1.14.0` or
53
+ `jev-1.13.0` for a fixed version.
54
+
55
+ ## Notes
56
+
57
+ - Jev rounds probabilities and scores to two decimal places. Results keep the provider's
58
+ values.
59
+ - Score answers must return a legend that matches the requested levels.
60
+ - `provider_data["confidence"]` describes how concentrated the distribution is, not the
61
+ probability that the answer is correct.
62
+ - A conversation is sent as a `role: text` transcript of user and assistant messages.
63
+ System and developer messages, agent instructions, and non-text content are never sent.
@@ -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"]
@@ -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,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "livekit-plugins-typesafe"
7
+ dynamic = ["version"]
8
+ description = "Agent Framework plugin for TypeSafe Jev decision models."
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.10.0"
12
+ authors = [{ name = "LiveKit", email = "hello@livekit.io" }]
13
+ keywords = ["webrtc", "realtime", "audio", "video", "livekit", "typesafe", "jev", "decisions"]
14
+ classifiers = [
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: Apache Software License",
17
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ ]
22
+ dependencies = ["livekit-agents>=1.8.8"]
23
+
24
+ [project.urls]
25
+ Documentation = "https://docs.livekit.io"
26
+ Website = "https://livekit.io/"
27
+ Source = "https://github.com/livekit/agents"
28
+
29
+ [tool.hatch.version]
30
+ path = "livekit/plugins/typesafe/version.py"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["livekit"]
34
+
35
+ [tool.hatch.build.targets.sdist]
36
+ include = ["/livekit"]
37
+
38
+ [tool.uv]
39
+ exclude-newer = "7 days"
40
+ exclude-newer-package = { livekit-agents = "0 days" }