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.
- livekit_plugins_typesafe-1.8.8/.gitignore +180 -0
- livekit_plugins_typesafe-1.8.8/PKG-INFO +83 -0
- livekit_plugins_typesafe-1.8.8/README.md +63 -0
- livekit_plugins_typesafe-1.8.8/livekit/plugins/typesafe/__init__.py +45 -0
- livekit_plugins_typesafe-1.8.8/livekit/plugins/typesafe/decision_model.py +210 -0
- livekit_plugins_typesafe-1.8.8/livekit/plugins/typesafe/log.py +3 -0
- livekit_plugins_typesafe-1.8.8/livekit/plugins/typesafe/models.py +5 -0
- livekit_plugins_typesafe-1.8.8/livekit/plugins/typesafe/py.typed +0 -0
- livekit_plugins_typesafe-1.8.8/livekit/plugins/typesafe/version.py +15 -0
- livekit_plugins_typesafe-1.8.8/pyproject.toml +40 -0
|
@@ -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
|
+
)
|
|
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,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" }
|