livekit-plugins-lehui 0.0.1__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,222 @@
1
+ .DS_Store
2
+
3
+ # Byte-compiled / optimized / DLL files
4
+ __pycache__/
5
+ *.py[codz]
6
+ *$py.class
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # PyInstaller
32
+ # Usually these files are written by a python script from a template
33
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
34
+ *.manifest
35
+ *.spec
36
+
37
+ # Installer logs
38
+ pip-log.txt
39
+ pip-delete-this-directory.txt
40
+
41
+ # Unit test / coverage reports
42
+ htmlcov/
43
+ .tox/
44
+ .nox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ *.py.cover
52
+ *.lcov
53
+ .hypothesis/
54
+ .pytest_cache/
55
+ cover/
56
+
57
+ # Translations
58
+ *.mo
59
+ *.pot
60
+
61
+ # Django stuff:
62
+ *.log
63
+ local_settings.py
64
+ db.sqlite3
65
+ db.sqlite3-journal
66
+
67
+ # Flask stuff:
68
+ instance/
69
+ .webassets-cache
70
+
71
+ # Scrapy stuff:
72
+ .scrapy
73
+
74
+ # Sphinx documentation
75
+ docs/_build/
76
+
77
+ # PyBuilder
78
+ .pybuilder/
79
+ target/
80
+
81
+ # Jupyter Notebook
82
+ .ipynb_checkpoints
83
+
84
+ # IPython
85
+ profile_default/
86
+ ipython_config.py
87
+
88
+ # pyenv
89
+ # For a library or package, you might want to ignore these files since the code is
90
+ # intended to run in multiple environments; otherwise, check them in:
91
+ # .python-version
92
+
93
+ # pipenv
94
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
96
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
97
+ # install all needed dependencies.
98
+ # Pipfile.lock
99
+
100
+ # UV
101
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
102
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
103
+ # commonly ignored for libraries.
104
+ uv.lock
105
+
106
+ # poetry
107
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
108
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
109
+ # commonly ignored for libraries.
110
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
111
+ # poetry.lock
112
+ # poetry.toml
113
+
114
+ # pdm
115
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
116
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
117
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
118
+ # pdm.lock
119
+ # pdm.toml
120
+ .pdm-python
121
+ .pdm-build/
122
+
123
+ # pixi
124
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
125
+ # pixi.lock
126
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
127
+ # in the .venv directory. It is recommended not to include this directory in version control.
128
+ .pixi/*
129
+ !.pixi/config.toml
130
+
131
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
132
+ __pypackages__/
133
+
134
+ # Celery stuff
135
+ celerybeat-schedule*
136
+ celerybeat.pid
137
+
138
+ # Redis
139
+ *.rdb
140
+ *.aof
141
+ *.pid
142
+
143
+ # RabbitMQ
144
+ mnesia/
145
+ rabbitmq/
146
+ rabbitmq-data/
147
+
148
+ # ActiveMQ
149
+ activemq-data/
150
+
151
+ # SageMath parsed files
152
+ *.sage.py
153
+
154
+ # Environments
155
+ .env
156
+ .envrc
157
+ .venv
158
+ env/
159
+ venv/
160
+ ENV/
161
+ env.bak/
162
+ venv.bak/
163
+
164
+ # Spyder project settings
165
+ .spyderproject
166
+ .spyproject
167
+
168
+ # Rope project settings
169
+ .ropeproject
170
+
171
+ # mkdocs documentation
172
+ /site
173
+
174
+ # mypy
175
+ .mypy_cache/
176
+ .dmypy.json
177
+ dmypy.json
178
+
179
+ # Pyre type checker
180
+ .pyre/
181
+
182
+ # pytype static type analyzer
183
+ .pytype/
184
+
185
+ # Cython debug symbols
186
+ cython_debug/
187
+
188
+ # PyCharm
189
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
190
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
191
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
192
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
193
+ .idea/
194
+
195
+ # Abstra
196
+ # Abstra is an AI-powered process automation framework.
197
+ # Ignore directories containing user credentials, local state, and settings.
198
+ # Learn more at https://abstra.io/docs
199
+ .abstra/
200
+
201
+ # Visual Studio Code
202
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore that
203
+ # can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
204
+ # and can be added to the global gitignore or merged into this file. However, if you prefer, you
205
+ # could uncomment the following to ignore the entire vscode folder
206
+ .vscode/
207
+ # Temporary file for partial code execution
208
+ tempCodeRunnerFile.py
209
+
210
+ # Ruff stuff:
211
+ .ruff_cache/
212
+
213
+ # PyPI configuration file
214
+ .pypirc
215
+
216
+ # Marimo
217
+ marimo/_static/
218
+ marimo/_lsp/
219
+ __marimo__/
220
+
221
+ # Streamlit
222
+ .streamlit/secrets.toml
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.5
2
+ Name: livekit-plugins-lehui
3
+ Version: 0.0.1
4
+ Summary: Provider integrations for LiveKit Agents maintained by Lehui
5
+ Requires-Python: >=3.10.0
6
+ Requires-Dist: aiohttp>=3.10.0
7
+ Requires-Dist: livekit-agents[openai]>=1.6.2
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Lehui plugins for LiveKit Agents
11
+
12
+ Provider integrations for LiveKit Agents maintained by Lehui.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install livekit-plugins-lehui
18
+ ```
19
+
20
+ ## Plugins
21
+
22
+ - [Aliyun](livekit-plugins-aliyun): realtime speech-to-text, text-to-speech, and
23
+ LLM support using DashScope.
24
+
25
+ ## Publishing
26
+
27
+ Update `project.version` in `pyproject.toml`, then push `main` and a matching tag:
28
+
29
+ ```bash
30
+ git push origin main
31
+ git tag v<X.Y.Z>
32
+ git push origin v<X.Y.Z>
33
+ ```
@@ -0,0 +1,24 @@
1
+ # Lehui plugins for LiveKit Agents
2
+
3
+ Provider integrations for LiveKit Agents maintained by Lehui.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install livekit-plugins-lehui
9
+ ```
10
+
11
+ ## Plugins
12
+
13
+ - [Aliyun](livekit-plugins-aliyun): realtime speech-to-text, text-to-speech, and
14
+ LLM support using DashScope.
15
+
16
+ ## Publishing
17
+
18
+ Update `project.version` in `pyproject.toml`, then push `main` and a matching tag:
19
+
20
+ ```bash
21
+ git push origin main
22
+ git tag v<X.Y.Z>
23
+ git push origin v<X.Y.Z>
24
+ ```
@@ -0,0 +1,114 @@
1
+ # Aliyun plugin for LiveKit Agents
2
+
3
+ Realtime speech-to-text, text-to-speech, and LLM support for LiveKit Agents using
4
+ Aliyun DashScope.
5
+
6
+ The plugin exposes:
7
+
8
+ - `livekit.plugins.aliyun.STT`
9
+ - `livekit.plugins.aliyun.QwenTTSRealtime`
10
+ - `livekit.plugins.aliyun.LLM`
11
+
12
+ ## Pre-requisites
13
+
14
+ You'll need an API key from Aliyun. It can be set as an environment variable: `DASHSCOPE_API_KEY`
15
+
16
+ ## Quick Start
17
+
18
+ ```python
19
+ from dotenv import load_dotenv
20
+
21
+ from livekit.agents import Agent, AgentSession
22
+ from livekit.plugins import aliyun
23
+
24
+ load_dotenv()
25
+
26
+ session = AgentSession(
27
+ llm=aliyun.LLM(),
28
+ stt=aliyun.STT(),
29
+ tts=aliyun.QwenTTSRealtime(),
30
+ )
31
+
32
+ agent = Agent(instructions="Reply briefly and clearly.")
33
+ ```
34
+
35
+ ## LLM
36
+
37
+ ### Common Options
38
+
39
+ ```python
40
+ from livekit.plugins import aliyun
41
+
42
+ llm = aliyun.LLM(
43
+ model="qwen-plus",
44
+ enable_thinking=False,
45
+ )
46
+ ```
47
+
48
+ - `api_key`: DashScope API key. Overrides the environment variable.
49
+ - `model`: DashScope OpenAI-compatible chat model name. Defaults to `qwen-plus`.
50
+ - `base_url`: Override the OpenAI-compatible endpoint. Defaults to
51
+ `https://dashscope.aliyuncs.com/compatible-mode/v1`.
52
+ - `enable_thinking`: Whether to enable Qwen deep-thinking mode. Defaults to `False`.
53
+ - `thinking_budget`: Optional token budget for thinking mode. Requires
54
+ `enable_thinking=True`.
55
+ - `max_tokens`: Optional maximum output token count.
56
+ - `seed`: Optional deterministic sampling seed.
57
+ - `tool_choice`: Supports `auto`, `none`, and forcing a specific function tool. The
58
+ `required` option is not supported.
59
+ - `preserve_thinking`: Not supported in this version. Reasoning content is not persisted
60
+ or returned as ordinary text.
61
+
62
+ ## STT
63
+
64
+ ### Common Options
65
+
66
+ ```python
67
+ from livekit.plugins import aliyun
68
+
69
+ stt = aliyun.STT(
70
+ model="qwen3-asr-flash-realtime",
71
+ language="zh",
72
+ sample_rate=16000,
73
+ interim_results=True,
74
+ )
75
+ ```
76
+
77
+ - `api_key`: DashScope API key. Overrides the environment variable.
78
+ - `model`: Realtime ASR model name.
79
+ - `language`: Optional language code such as `zh`, `yue`, or `en`.
80
+ - `sample_rate`: Input sample rate. Typical value is `16000`.
81
+ - `interim_results`: Whether to emit interim transcripts.
82
+ - `turn_detection`: Server-side VAD settings. Set it to `None` to use manual mode.
83
+
84
+ ### Manual Mode
85
+
86
+ To disable server-side turn detection and use manual mode:
87
+
88
+ ```python
89
+ from livekit.plugins import aliyun
90
+
91
+ stt = aliyun.STT(turn_detection=None)
92
+ ```
93
+
94
+ ## Qwen TTS Realtime
95
+
96
+ ### Common Options
97
+
98
+ ```python
99
+ from livekit.plugins import aliyun
100
+
101
+ tts = aliyun.QwenTTSRealtime(
102
+ model="qwen3-tts-flash-realtime",
103
+ voice="Cherry",
104
+ language_type="Chinese",
105
+ )
106
+ ```
107
+
108
+ - `api_key`: DashScope API key. Overrides the environment variable.
109
+ - `model`: Realtime TTS model name.
110
+ - `voice`: Realtime TTS voice name.
111
+ - `language_type`: One of `Auto`, `Chinese`, `English`, `German`, `Italian`,
112
+ `Portuguese`, `Spanish`, `Japanese`, `Korean`, `French`, or `Russian`.
113
+ - `base_url`: Override the websocket endpoint. For the international region, use
114
+ `wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime`.
@@ -0,0 +1,20 @@
1
+ from importlib.metadata import version as distribution_version
2
+
3
+ from livekit.agents import Plugin
4
+
5
+ from .llm import LLM
6
+ from .log import logger
7
+ from .qwen_tts_realtime import QwenTTSRealtime
8
+ from .stt import STT
9
+
10
+ __version__ = distribution_version("livekit-plugins-lehui")
11
+
12
+ __all__ = ["STT", "QwenTTSRealtime", "LLM", "__version__"]
13
+
14
+
15
+ class AliyunPlugin(Plugin):
16
+ def __init__(self) -> None:
17
+ super().__init__(__name__, __version__, __package__, logger)
18
+
19
+
20
+ Plugin.register_plugin(AliyunPlugin())
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ import httpx
7
+ import openai
8
+ from openai.types.chat import completion_create_params
9
+
10
+ from livekit.agents import llm
11
+ from livekit.agents.llm import ToolChoice, utils as llm_utils
12
+ from livekit.agents.types import (
13
+ DEFAULT_API_CONNECT_OPTIONS,
14
+ NOT_GIVEN,
15
+ APIConnectOptions,
16
+ NotGivenOr,
17
+ )
18
+ from livekit.agents.utils import is_given
19
+ from livekit.plugins.openai.llm import LLM as OpenAILLM, LLMStream as OpenAILLMStream
20
+
21
+ DEFAULT_MODEL = "qwen-plus"
22
+ DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
23
+
24
+
25
+ class LLM(OpenAILLM):
26
+ def __init__(
27
+ self,
28
+ *,
29
+ model: str = DEFAULT_MODEL,
30
+ api_key: NotGivenOr[str] = NOT_GIVEN,
31
+ base_url: NotGivenOr[str] = DEFAULT_BASE_URL,
32
+ client: openai.AsyncClient | None = None,
33
+ user: NotGivenOr[str] = NOT_GIVEN,
34
+ temperature: NotGivenOr[float] = NOT_GIVEN,
35
+ top_p: NotGivenOr[float] = NOT_GIVEN,
36
+ parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN,
37
+ tool_choice: NotGivenOr[ToolChoice] = NOT_GIVEN,
38
+ max_tokens: NotGivenOr[int] = NOT_GIVEN,
39
+ seed: NotGivenOr[int] = NOT_GIVEN,
40
+ enable_thinking: bool = False,
41
+ thinking_budget: NotGivenOr[int] = NOT_GIVEN,
42
+ extra_body: NotGivenOr[dict[str, Any]] = NOT_GIVEN,
43
+ extra_headers: NotGivenOr[dict[str, str]] = NOT_GIVEN,
44
+ extra_query: NotGivenOr[dict[str, str]] = NOT_GIVEN,
45
+ timeout: httpx.Timeout | None = None,
46
+ max_retries: NotGivenOr[int] = NOT_GIVEN,
47
+ ) -> None:
48
+ """
49
+ Create a new instance of Aliyun DashScope LLM.
50
+
51
+ ``api_key`` must be set to your DashScope API key, either using the argument or by
52
+ setting the ``DASHSCOPE_API_KEY`` environment variable.
53
+ """
54
+ _validate_tool_choice(tool_choice, enable_thinking=enable_thinking)
55
+ merged_body = _merge_extra_body(
56
+ extra_body,
57
+ enable_thinking=enable_thinking,
58
+ thinking_budget=thinking_budget,
59
+ max_tokens=max_tokens,
60
+ seed=seed,
61
+ )
62
+
63
+ self._enable_thinking = enable_thinking
64
+ self._aliyun_extra_body = merged_body
65
+
66
+ super().__init__(
67
+ model=model,
68
+ api_key=_get_api_key(api_key),
69
+ base_url=base_url,
70
+ client=client,
71
+ user=user,
72
+ temperature=temperature,
73
+ top_p=top_p,
74
+ parallel_tool_calls=parallel_tool_calls,
75
+ tool_choice=tool_choice,
76
+ extra_body=NOT_GIVEN,
77
+ extra_headers=extra_headers,
78
+ extra_query=extra_query,
79
+ timeout=timeout,
80
+ max_retries=max_retries,
81
+ _strict_tool_schema=False,
82
+ )
83
+
84
+ @property
85
+ def model(self) -> str:
86
+ return self._opts.model
87
+
88
+ @property
89
+ def provider(self) -> str:
90
+ return "Aliyun"
91
+
92
+ def chat(
93
+ self,
94
+ *,
95
+ chat_ctx: llm.ChatContext,
96
+ tools: list[llm.Tool] | None = None,
97
+ conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
98
+ parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN,
99
+ tool_choice: NotGivenOr[ToolChoice] = NOT_GIVEN,
100
+ response_format: NotGivenOr[
101
+ completion_create_params.ResponseFormat | type[llm_utils.ResponseFormatT]
102
+ ] = NOT_GIVEN,
103
+ extra_kwargs: NotGivenOr[dict[str, Any]] = NOT_GIVEN,
104
+ ) -> OpenAILLMStream:
105
+ resolved_tool_choice = tool_choice if is_given(tool_choice) else self._opts.tool_choice
106
+ _validate_tool_choice(resolved_tool_choice, enable_thinking=self._enable_thinking)
107
+ merged_extra_kwargs = _merge_call_extra_kwargs(
108
+ extra_kwargs,
109
+ aliyun_extra_body=self._aliyun_extra_body,
110
+ enable_thinking=self._enable_thinking,
111
+ )
112
+
113
+ return super().chat(
114
+ chat_ctx=chat_ctx,
115
+ tools=tools,
116
+ conn_options=conn_options,
117
+ parallel_tool_calls=parallel_tool_calls,
118
+ tool_choice=tool_choice,
119
+ response_format=response_format,
120
+ extra_kwargs=merged_extra_kwargs,
121
+ )
122
+
123
+
124
+ def _get_api_key(key: NotGivenOr[str]) -> str:
125
+ dashscope_api_key = key if is_given(key) else os.environ.get("DASHSCOPE_API_KEY")
126
+ if not dashscope_api_key:
127
+ raise ValueError(
128
+ "DASHSCOPE_API_KEY is required, either as argument or set "
129
+ "DASHSCOPE_API_KEY environment variable"
130
+ )
131
+ return dashscope_api_key
132
+
133
+
134
+ def _merge_extra_body(
135
+ extra_body: NotGivenOr[dict[str, Any]],
136
+ *,
137
+ enable_thinking: bool,
138
+ thinking_budget: NotGivenOr[int],
139
+ max_tokens: NotGivenOr[int],
140
+ seed: NotGivenOr[int],
141
+ ) -> dict[str, Any]:
142
+ merged_body = dict(extra_body) if is_given(extra_body) else {}
143
+ _validate_no_preserve_thinking(merged_body)
144
+
145
+ if is_given(thinking_budget):
146
+ _validate_thinking_budget(thinking_budget, enable_thinking=enable_thinking)
147
+ merged_body["thinking_budget"] = thinking_budget
148
+ if is_given(max_tokens):
149
+ merged_body["max_tokens"] = max_tokens
150
+ if is_given(seed):
151
+ merged_body["seed"] = seed
152
+
153
+ merged_body["enable_thinking"] = enable_thinking
154
+ return merged_body
155
+
156
+
157
+ def _validate_tool_choice(
158
+ tool_choice: NotGivenOr[ToolChoice],
159
+ *,
160
+ enable_thinking: bool,
161
+ ) -> None:
162
+ if not is_given(tool_choice):
163
+ return
164
+ if tool_choice == "required":
165
+ raise ValueError("Aliyun LLM does not support tool_choice='required'")
166
+ if enable_thinking and isinstance(tool_choice, dict):
167
+ raise ValueError(
168
+ "Aliyun LLM does not support forcing a specific tool when enable_thinking=True"
169
+ )
170
+
171
+
172
+ def _merge_call_extra_kwargs(
173
+ extra_kwargs: NotGivenOr[dict[str, Any]],
174
+ *,
175
+ aliyun_extra_body: dict[str, Any],
176
+ enable_thinking: bool,
177
+ ) -> dict[str, Any]:
178
+ merged_kwargs = dict(extra_kwargs) if is_given(extra_kwargs) else {}
179
+ per_call_body = _get_per_call_extra_body(merged_kwargs)
180
+ _validate_no_preserve_thinking(per_call_body)
181
+
182
+ if "thinking_budget" in per_call_body:
183
+ _validate_thinking_budget(per_call_body["thinking_budget"], enable_thinking=enable_thinking)
184
+
185
+ final_body = {**aliyun_extra_body, **per_call_body}
186
+ final_body["enable_thinking"] = enable_thinking
187
+ merged_kwargs["extra_body"] = final_body
188
+ return merged_kwargs
189
+
190
+
191
+ def _get_per_call_extra_body(extra_kwargs: dict[str, Any]) -> dict[str, Any]:
192
+ extra_body = extra_kwargs.get("extra_body", NOT_GIVEN)
193
+ if not is_given(extra_body):
194
+ return {}
195
+ if not isinstance(extra_body, dict):
196
+ raise TypeError("extra_kwargs['extra_body'] must be a dict")
197
+ return dict(extra_body)
198
+
199
+
200
+ def _validate_no_preserve_thinking(extra_body: dict[str, Any]) -> None:
201
+ if "preserve_thinking" in extra_body:
202
+ raise ValueError("Aliyun LLM does not support preserve_thinking")
203
+
204
+
205
+ def _validate_thinking_budget(thinking_budget: int, *, enable_thinking: bool) -> None:
206
+ if not enable_thinking:
207
+ raise ValueError("thinking_budget requires enable_thinking=True")
208
+ if thinking_budget < 0:
209
+ raise ValueError("thinking_budget must be greater than or equal to 0")
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("livekit.plugins.aliyun")