livekit-plugins-volcenginee 1.3.3__tar.gz → 1.3.5__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.
Files changed (16) hide show
  1. livekit_plugins_volcenginee-1.3.5/.gitignore +7 -0
  2. {livekit_plugins_volcenginee-1.3.3 → livekit_plugins_volcenginee-1.3.5}/PKG-INFO +6 -1
  3. {livekit_plugins_volcenginee-1.3.3 → livekit_plugins_volcenginee-1.3.5}/livekit/plugins/volcengine/llm.py +33 -16
  4. {livekit_plugins_volcenginee-1.3.3 → livekit_plugins_volcenginee-1.3.5}/livekit/plugins/volcengine/tts.py +5 -1
  5. livekit_plugins_volcenginee-1.3.5/livekit/plugins/volcengine/utils.py +25 -0
  6. livekit_plugins_volcenginee-1.3.5/livekit/plugins/volcengine/version.py +1 -0
  7. {livekit_plugins_volcenginee-1.3.3 → livekit_plugins_volcenginee-1.3.5}/pyproject.toml +20 -0
  8. livekit_plugins_volcenginee-1.3.3/.gitignore +0 -183
  9. livekit_plugins_volcenginee-1.3.3/livekit/plugins/volcengine/utils.py +0 -154
  10. livekit_plugins_volcenginee-1.3.3/livekit/plugins/volcengine/version.py +0 -1
  11. {livekit_plugins_volcenginee-1.3.3 → livekit_plugins_volcenginee-1.3.5}/README.md +0 -0
  12. {livekit_plugins_volcenginee-1.3.3 → livekit_plugins_volcenginee-1.3.5}/livekit/plugins/volcengine/__init__.py +0 -0
  13. {livekit_plugins_volcenginee-1.3.3 → livekit_plugins_volcenginee-1.3.5}/livekit/plugins/volcengine/log.py +0 -0
  14. {livekit_plugins_volcenginee-1.3.3 → livekit_plugins_volcenginee-1.3.5}/livekit/plugins/volcengine/py.typed +0 -0
  15. {livekit_plugins_volcenginee-1.3.3 → livekit_plugins_volcenginee-1.3.5}/livekit/plugins/volcengine/realtime.py +0 -0
  16. {livekit_plugins_volcenginee-1.3.3 → livekit_plugins_volcenginee-1.3.5}/livekit/plugins/volcengine/stt.py +0 -0
@@ -0,0 +1,7 @@
1
+ .vscode/
2
+ .venv/
3
+ .pytest_cache/
4
+ __pycache__/
5
+ .env
6
+ .env.*
7
+ dist/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: livekit-plugins-volcenginee
3
- Version: 1.3.3
3
+ Version: 1.3.5
4
4
  Summary: LiveKit Agent Plugins for Volcengine
5
5
  Author-email: linsanzhu <2287225730@qq.com>
6
6
  Keywords: audio,livekit,realtime,video,webrtc
@@ -18,6 +18,11 @@ Requires-Dist: numpy
18
18
  Requires-Dist: openai>=1.80.0
19
19
  Requires-Dist: osc-data~=0.3.1
20
20
  Requires-Dist: pydantic
21
+ Provides-Extra: test
22
+ Requires-Dist: aiohttp>=3.9; extra == 'test'
23
+ Requires-Dist: pyaudio>=0.2.14; extra == 'test'
24
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
25
+ Requires-Dist: pytest>=7.0; extra == 'test'
21
26
  Description-Content-Type: text/markdown
22
27
 
23
28
  # livekit-plugins-volcengine
@@ -21,7 +21,7 @@ from livekit.agents.types import (
21
21
  from livekit.agents.utils import is_given
22
22
 
23
23
  from .log import logger
24
- from .utils import to_chat_ctx, to_fnc_ctx
24
+ from .utils import to_fnc_ctx
25
25
 
26
26
 
27
27
  @dataclass
@@ -32,7 +32,18 @@ class _LLMOptions:
32
32
  parallel_tool_calls: NotGivenOr[bool]
33
33
  tool_choice: NotGivenOr[ToolChoice]
34
34
  metadata: NotGivenOr[dict[str, str]]
35
- thinking: NotGivenOr[Literal["enabled", "disabled", "auto"] | dict[str, Any]]
35
+ reasoning_effort: NotGivenOr[Literal["minimal", "low", "medium", "high", "max"]]
36
+
37
+ def __post_init__(self) -> None:
38
+ if is_given(self.reasoning_effort):
39
+ if self.reasoning_effort not in (
40
+ "minimal", "low", "medium", "high", "max",
41
+ ):
42
+ raise ValueError(
43
+ f"reasoning_effort must be one of "
44
+ f"'minimal', 'low', 'medium', 'high', 'max'; "
45
+ f"got {self.reasoning_effort!r}"
46
+ )
36
47
 
37
48
 
38
49
  class LLM(llm.LLM):
@@ -48,7 +59,9 @@ class LLM(llm.LLM):
48
59
  parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN,
49
60
  tool_choice: NotGivenOr[ToolChoice] = NOT_GIVEN,
50
61
  metadata: NotGivenOr[dict[str, str]] = NOT_GIVEN,
51
- thinking: NotGivenOr[Literal["enabled", "disabled", "auto"] | dict[str, Any]] = NOT_GIVEN,
62
+ reasoning_effort: NotGivenOr[
63
+ Literal["minimal", "low", "medium", "high", "max"]
64
+ ] = NOT_GIVEN,
52
65
  timeout: httpx.Timeout | None = None,
53
66
  ) -> None:
54
67
  """
@@ -58,11 +71,14 @@ class LLM(llm.LLM):
58
71
  model: The model to use for the LLM, end
59
72
  api_key: The API key to use for the LLM.
60
73
  base_url: The base URL to use for the LLM.
61
- thinking: Controls deep thinking mode. Pass ``"disabled"`` to turn
62
- it off, ``"enabled"`` to force it on, ``"auto"`` to let the
63
- model decide, or a raw ``{"type": "..."}`` dict for extended
64
- parameters. See the Volcengine Chat Completions API reference
65
- for details. Omit to use the API default (``enabled``).
74
+ reasoning_effort: Constrains the effort the model spends on
75
+ reasoning before producing a final answer. One of
76
+ ``"minimal"``, ``"low"``, ``"medium"``, ``"high"``, ``"max"``.
77
+ Mapped to Volcengine Ark's ``reasoning_effort`` field; also
78
+ accepted by the OpenAI Python SDK as a standard Chat
79
+ Completions kwarg (same name, same semantics) so it is
80
+ forwarded at the top level of the request body. Omit to use
81
+ the API default (``medium``).
66
82
  """
67
83
  super().__init__()
68
84
  self._opts = _LLMOptions(
@@ -72,7 +88,7 @@ class LLM(llm.LLM):
72
88
  parallel_tool_calls=parallel_tool_calls,
73
89
  tool_choice=tool_choice,
74
90
  metadata=metadata,
75
- thinking=thinking,
91
+ reasoning_effort=reasoning_effort,
76
92
  )
77
93
  api_key = api_key if is_given(api_key) else os.getenv("VOLCENGINE_LLM_API_KEY")
78
94
  if api_key is None:
@@ -118,12 +134,13 @@ class LLM(llm.LLM):
118
134
  if is_given(self._opts.temperature):
119
135
  extra["temperature"] = self._opts.temperature
120
136
 
121
- if is_given(self._opts.thinking):
122
- extra["thinking"] = (
123
- self._opts.thinking
124
- if isinstance(self._opts.thinking, dict)
125
- else {"type": self._opts.thinking}
126
- )
137
+ if is_given(self._opts.reasoning_effort):
138
+ # ``reasoning_effort`` is part of the OpenAI Chat Completions
139
+ # signature (added for the o1 series; the OpenAI Python SDK
140
+ # accepts it at the top level). Volcengine Ark also accepts the
141
+ # same field name on its OpenAI-compatible endpoint, so we can
142
+ # forward it as a normal kwarg without ``extra_body``.
143
+ extra["reasoning_effort"] = self._opts.reasoning_effort
127
144
 
128
145
  parallel_tool_calls = (
129
146
  parallel_tool_calls
@@ -189,7 +206,7 @@ class LLMStream(llm.LLMStream):
189
206
  stream: openai.AsyncStream[
190
207
  ChatCompletionChunk
191
208
  ] = await self._client.chat.completions.create(
192
- messages=to_chat_ctx(self._chat_ctx, id(self._llm)),
209
+ messages=self._chat_ctx.to_provider_format(format="openai")[0],
193
210
  tools=to_fnc_ctx(self._tools) if self._tools else openai.NOT_GIVEN,
194
211
  model=self._model,
195
212
  stream_options={"include_usage": True},
@@ -10,7 +10,7 @@ from collections.abc import ByteString
10
10
  from typing import Literal, Tuple
11
11
 
12
12
  import aiohttp
13
- from pydantic import BaseModel, Field
13
+ from pydantic import BaseModel, ConfigDict, Field
14
14
  from osc_data.text_stream import TextStreamSentencizer
15
15
 
16
16
  from livekit.agents import (
@@ -46,6 +46,10 @@ def _volume_to_loudness_rate(volume: float) -> int:
46
46
 
47
47
 
48
48
  class _TTSOptions(BaseModel):
49
+ # Reject unknown fields so the legacy ``speech_rate`` / ``loudness_rate``
50
+ # kwargs that were removed from the public API surface immediately.
51
+ model_config = ConfigDict(extra="forbid")
52
+
49
53
  api_key: str
50
54
  resource_id: str | None = None
51
55
  voice: str = "zh_female_xiaohe_uranus_bigtts"
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections.abc import Sequence
5
+ from typing import Awaitable, Callable, Union, cast
6
+
7
+ from livekit.agents import llm
8
+ from openai.types.chat import ChatCompletionToolParam
9
+
10
+ AsyncAzureADTokenProvider = Callable[[], Union[str, Awaitable[str]]]
11
+
12
+
13
+ def get_base_url(base_url: str | None) -> str:
14
+ if not base_url:
15
+ base_url = os.getenv(
16
+ "VOLCENGINE_LLM_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3"
17
+ )
18
+ return base_url
19
+
20
+
21
+ def to_fnc_ctx(fnc_ctx: Sequence[llm.Tool]) -> list[ChatCompletionToolParam]:
22
+ return cast(
23
+ list[ChatCompletionToolParam],
24
+ llm.ToolContext(fnc_ctx).parse_function_tools("openai"),
25
+ )
@@ -0,0 +1 @@
1
+ __version__ = "1.3.5"
@@ -27,6 +27,13 @@ classifiers = [
27
27
  "Programming Language :: Python :: 3 :: Only",
28
28
  ]
29
29
 
30
+ [project.optional-dependencies]
31
+ test = [
32
+ "pytest>=7.0",
33
+ "pytest-asyncio>=0.21",
34
+ "aiohttp>=3.9",
35
+ "pyaudio>=0.2.14",
36
+ ]
30
37
 
31
38
 
32
39
  [build-system]
@@ -41,3 +48,16 @@ packages = ["livekit"]
41
48
 
42
49
  [tool.hatch.build.targets.sdist]
43
50
  include = ["/livekit"]
51
+
52
+ [tool.pytest.ini_options]
53
+ asyncio_mode = "auto"
54
+ testpaths = ["tests"]
55
+ log_cli = true
56
+ log_cli_level = "INFO"
57
+ log_cli_format = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
58
+ log_cli_date_format = "%H:%M:%S"
59
+ markers = [
60
+ "integration: marks tests that require real Volcengine API credentials (skipped automatically when keys are not set in the environment)",
61
+ "llm: marks LLM integration tests (require VOLCENGINE_LLM_API_KEY)",
62
+ "tts: marks TTS integration tests (require VOLCENGINE_TTS_API_KEY)",
63
+ ]
@@ -1,183 +0,0 @@
1
- # Byte-compiled / optimized / DLL files
2
- __pycache__/
3
- *.py[cod]
4
- *$py.class
5
-
6
- # C extensions
7
- *.so
8
-
9
- # Distribution / packaging
10
- .Python
11
- build/
12
- develop-eggs/
13
- dist/
14
- downloads/
15
- eggs/
16
- .eggs/
17
- lib/
18
- lib64/
19
- parts/
20
- sdist/
21
- var/
22
- wheels/
23
- share/python-wheels/
24
- *.egg-info/
25
- .installed.cfg
26
- *.egg
27
- MANIFEST
28
-
29
- # PyInstaller
30
- # Usually these files are written by a python script from a template
31
- # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
- *.manifest
33
- *.spec
34
-
35
- # Installer logs
36
- pip-log.txt
37
- pip-delete-this-directory.txt
38
-
39
- # Unit test / coverage reports
40
- htmlcov/
41
- .tox/
42
- .nox/
43
- .coverage
44
- .coverage.*
45
- .cache
46
- nosetests.xml
47
- coverage.xml
48
- *.cover
49
- *.py,cover
50
- .hypothesis/
51
- .pytest_cache/
52
- cover/
53
-
54
- # Translations
55
- *.mo
56
- *.pot
57
-
58
- # Django stuff:
59
- *.log
60
- local_settings.py
61
- db.sqlite3
62
- db.sqlite3-journal
63
-
64
- # Flask stuff:
65
- instance/
66
- .webassets-cache
67
-
68
- # Scrapy stuff:
69
- .scrapy
70
-
71
- # Sphinx documentation
72
- docs/_build/
73
-
74
- # PyBuilder
75
- .pybuilder/
76
- target/
77
-
78
- # Jupyter Notebook
79
- .ipynb_checkpoints
80
-
81
- # IPython
82
- profile_default/
83
- ipython_config.py
84
-
85
- # pyenv
86
- # For a library or package, you might want to ignore these files since the code is
87
- # intended to run in multiple environments; otherwise, check them in:
88
- # .python-version
89
-
90
- # pipenv
91
- # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
- # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
- # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
- # install all needed dependencies.
95
- #Pipfile.lock
96
-
97
- # UV
98
- # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
- # This is especially recommended for binary packages to ensure reproducibility, and is more
100
- # commonly ignored for libraries.
101
- #uv.lock
102
-
103
- # poetry
104
- # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
- # This is especially recommended for binary packages to ensure reproducibility, and is more
106
- # commonly ignored for libraries.
107
- # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
- #poetry.lock
109
-
110
- # pdm
111
- # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
- #pdm.lock
113
- # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
- # in version control.
115
- # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
- .pdm.toml
117
- .pdm-python
118
- .pdm-build/
119
-
120
- # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
- __pypackages__/
122
-
123
- # Celery stuff
124
- celerybeat-schedule
125
- celerybeat.pid
126
-
127
- # SageMath parsed files
128
- *.sage.py
129
-
130
- # Environments
131
- .env
132
- .venv
133
- env/
134
- venv/
135
- ENV/
136
- env.bak/
137
- venv.bak/
138
-
139
- # Spyder project settings
140
- .spyderproject
141
- .spyproject
142
-
143
- # Rope project settings
144
- .ropeproject
145
-
146
- # mkdocs documentation
147
- /site
148
-
149
- # mypy
150
- .mypy_cache/
151
- .dmypy.json
152
- dmypy.json
153
-
154
- # Pyre type checker
155
- .pyre/
156
-
157
- # pytype static type analyzer
158
- .pytype/
159
-
160
- # Cython debug symbols
161
- cython_debug/
162
-
163
- # PyCharm
164
- # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
- # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
- # and can be added to the global gitignore or merged into this file. For a more nuclear
167
- # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
- #.idea/
169
-
170
- # Ruff stuff:
171
- .ruff_cache/
172
-
173
- # PyPI configuration file
174
- .pypirc
175
-
176
- # tests
177
- test.py
178
-
179
- .python-version
180
-
181
- *.ipynb
182
- config.yaml
183
- uv.lock
@@ -1,154 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import base64
4
- import os
5
- from collections import OrderedDict
6
- from collections.abc import Awaitable, Sequence
7
- from typing import Any, Callable, Union, cast
8
-
9
- from openai.types.chat import (
10
- ChatCompletionContentPartParam,
11
- ChatCompletionMessageParam,
12
- ChatCompletionToolParam,
13
- )
14
-
15
- from livekit.agents import llm
16
-
17
- AsyncAzureADTokenProvider = Callable[[], Union[str, Awaitable[str]]]
18
-
19
-
20
- def get_base_url(base_url: str | None) -> str:
21
- if not base_url:
22
- base_url = os.getenv(
23
- "VOLCENGINE_LLM_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3"
24
- )
25
- return base_url
26
-
27
-
28
- def to_fnc_ctx(fnc_ctx: Sequence[llm.Tool]) -> list[ChatCompletionToolParam]:
29
- return cast(
30
- list[ChatCompletionToolParam],
31
- llm.ToolContext(fnc_ctx).parse_function_tools("openai"),
32
- )
33
-
34
-
35
- def to_chat_ctx(
36
- chat_ctx: llm.ChatContext, cache_key: Any
37
- ) -> list[ChatCompletionMessageParam]:
38
- # group the message and function_calls
39
- item_groups: dict[str, list[llm.ChatItem]] = OrderedDict()
40
- for item in chat_ctx.items:
41
- if (
42
- item.type == "message" and item.role == "assistant"
43
- ) or item.type == "function_call":
44
- group_id = item.id.split("/")[0]
45
- if group_id not in item_groups:
46
- item_groups[group_id] = []
47
- item_groups[group_id].append(item)
48
- else:
49
- item_groups[item.id] = [item]
50
-
51
- return [_group_to_chat_item(items, cache_key) for items in item_groups.values()]
52
-
53
-
54
- def _group_to_chat_item(
55
- items: list[llm.ChatItem], cache_key: Any
56
- ) -> ChatCompletionMessageParam:
57
- if len(items) == 1:
58
- return _to_chat_item(items[0], cache_key)
59
- else:
60
- msg = {"role": "assistant", "tool_calls": []}
61
- for item in items:
62
- if item.type == "message":
63
- assert item.role == "assistant", (
64
- "only assistant messages can be grouped"
65
- )
66
- assert "content" not in msg, (
67
- "only one assistant message is allowed in a group"
68
- )
69
-
70
- msg.update(_to_chat_item(item, cache_key))
71
- elif item.type == "function_call":
72
- msg["tool_calls"].append(
73
- {
74
- "id": item.call_id,
75
- "type": "function",
76
- "function": {"name": item.name, "arguments": item.arguments},
77
- }
78
- )
79
- return msg
80
-
81
-
82
- def _to_chat_item(msg: llm.ChatItem, cache_key: Any) -> ChatCompletionMessageParam:
83
- if msg.type == "message":
84
- list_content: list[ChatCompletionContentPartParam] = []
85
- text_content = ""
86
- for content in msg.content:
87
- if isinstance(content, str):
88
- if text_content:
89
- text_content += "\n"
90
- text_content += content
91
- elif isinstance(content, llm.ImageContent):
92
- list_content.append(_to_image_content(content, cache_key))
93
-
94
- if not list_content:
95
- # certain providers require text-only content in a string vs a list.
96
- # for max-compatibility, we will combine all text content into a single string.
97
- return {
98
- "role": msg.role, # type: ignore
99
- "content": text_content,
100
- }
101
-
102
- if text_content:
103
- list_content.append({"type": "text", "text": text_content})
104
-
105
- return {
106
- "role": msg.role, # type: ignore
107
- "content": list_content,
108
- }
109
-
110
- elif msg.type == "function_call":
111
- return {
112
- "role": "assistant",
113
- "tool_calls": [
114
- {
115
- "id": msg.call_id,
116
- "type": "function",
117
- "function": {
118
- "name": msg.name,
119
- "arguments": msg.arguments,
120
- },
121
- }
122
- ],
123
- }
124
-
125
- elif msg.type == "function_call_output":
126
- return {
127
- "role": "tool",
128
- "tool_call_id": msg.call_id,
129
- "content": msg.output,
130
- }
131
-
132
-
133
- def _to_image_content(
134
- image: llm.ImageContent, cache_key: Any
135
- ) -> ChatCompletionContentPartParam:
136
- img = llm.utils.serialize_image(image)
137
- if img.external_url:
138
- return {
139
- "type": "image_url",
140
- "image_url": {
141
- "url": img.external_url,
142
- "detail": img.inference_detail,
143
- },
144
- }
145
- if cache_key not in image._cache:
146
- image._cache[cache_key] = img.data_bytes
147
- b64_data = base64.b64encode(image._cache[cache_key]).decode("utf-8")
148
- return {
149
- "type": "image_url",
150
- "image_url": {
151
- "url": f"data:{img.mime_type};base64,{b64_data}",
152
- "detail": img.inference_detail,
153
- },
154
- }
@@ -1 +0,0 @@
1
- __version__ = "1.3.3"