livekit-plugins-volcenginee 1.3.0__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,28 @@
1
+ from .llm import LLM
2
+ from .stt import STT
3
+ from .tts import TTS
4
+ from .realtime import RealtimeModel
5
+ from .version import __version__
6
+
7
+ __all__ = ["TTS", "LLM", "STT", "RealtimeModel", "__version__"]
8
+
9
+ from livekit.agents import Plugin
10
+
11
+ from .log import logger
12
+
13
+
14
+ class VolcenginePlugin(Plugin):
15
+ def __init__(self):
16
+ super().__init__(__name__, __version__, __package__, logger)
17
+
18
+
19
+ Plugin.register_plugin(VolcenginePlugin())
20
+
21
+ # Cleanup docs of unexported modules
22
+ _module = dir()
23
+ NOT_IN_ALL = [m for m in _module if m not in __all__]
24
+
25
+ __pdoc__ = {}
26
+
27
+ for n in NOT_IN_ALL:
28
+ __pdoc__[n] = False
@@ -0,0 +1,283 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ import httpx
8
+ import openai
9
+ from openai.types.chat import ChatCompletionChunk, ChatCompletionToolChoiceOptionParam
10
+ from openai.types.chat.chat_completion_chunk import Choice
11
+
12
+ from livekit.agents import APIConnectionError, APIStatusError, APITimeoutError, llm
13
+ from livekit.agents.llm import ToolChoice
14
+ from livekit.agents.llm.chat_context import ChatContext
15
+ from livekit.agents.types import (
16
+ DEFAULT_API_CONNECT_OPTIONS,
17
+ NOT_GIVEN,
18
+ APIConnectOptions,
19
+ NotGivenOr,
20
+ )
21
+ from livekit.agents.utils import is_given
22
+
23
+ from .log import logger
24
+ from .utils import to_chat_ctx, to_fnc_ctx
25
+
26
+
27
+ @dataclass
28
+ class _LLMOptions:
29
+ model: str
30
+ user: NotGivenOr[str]
31
+ temperature: NotGivenOr[float]
32
+ parallel_tool_calls: NotGivenOr[bool]
33
+ tool_choice: NotGivenOr[ToolChoice]
34
+ store: NotGivenOr[bool]
35
+ metadata: NotGivenOr[dict[str, str]]
36
+
37
+
38
+ class LLM(llm.LLM):
39
+ def __init__(
40
+ self,
41
+ *,
42
+ model: str = "doubao-1-5-lite-32k-250115",
43
+ api_key: NotGivenOr[str] = NOT_GIVEN,
44
+ base_url: NotGivenOr[str] = "https://ark.cn-beijing.volces.com/api/v3/",
45
+ client: openai.AsyncClient | None = None,
46
+ user: NotGivenOr[str] = NOT_GIVEN,
47
+ temperature: NotGivenOr[float] = NOT_GIVEN,
48
+ parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN,
49
+ tool_choice: NotGivenOr[ToolChoice] = NOT_GIVEN,
50
+ store: NotGivenOr[bool] = NOT_GIVEN,
51
+ metadata: NotGivenOr[dict[str, str]] = NOT_GIVEN,
52
+ timeout: httpx.Timeout | None = None,
53
+ ) -> None:
54
+ """
55
+ Create a new instance of Volcengine LLM.
56
+
57
+ Args:
58
+ model: The model to use for the LLM, end
59
+ api_key: The API key to use for the LLM.
60
+ base_url: The base URL to use for the LLM.
61
+ """
62
+ super().__init__()
63
+ self._opts = _LLMOptions(
64
+ model=model,
65
+ user=user,
66
+ temperature=temperature,
67
+ parallel_tool_calls=parallel_tool_calls,
68
+ tool_choice=tool_choice,
69
+ store=store,
70
+ metadata=metadata,
71
+ )
72
+ api_key = api_key if is_given(api_key) else os.getenv("VOLCENGINE_LLM_API_KEY")
73
+ if api_key is None:
74
+ raise ValueError("api_key is required")
75
+ self._client = client or openai.AsyncClient(
76
+ api_key=api_key if is_given(api_key) else None,
77
+ base_url=base_url if is_given(base_url) else None,
78
+ max_retries=0,
79
+ http_client=httpx.AsyncClient(
80
+ timeout=timeout
81
+ if timeout
82
+ else httpx.Timeout(connect=15.0, read=5.0, write=5.0, pool=5.0),
83
+ follow_redirects=True,
84
+ limits=httpx.Limits(
85
+ max_connections=50,
86
+ max_keepalive_connections=50,
87
+ keepalive_expiry=120,
88
+ ),
89
+ ),
90
+ )
91
+
92
+ def chat(
93
+ self,
94
+ *,
95
+ chat_ctx: 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
+ extra_kwargs: NotGivenOr[dict[str, Any]] = NOT_GIVEN,
101
+ ) -> LLMStream:
102
+ extra = {}
103
+
104
+ if is_given(extra_kwargs):
105
+ extra.update(extra_kwargs)
106
+
107
+ if is_given(self._opts.metadata):
108
+ extra["metadata"] = self._opts.metadata
109
+
110
+ if is_given(self._opts.user):
111
+ extra["user"] = self._opts.user
112
+
113
+ parallel_tool_calls = (
114
+ parallel_tool_calls
115
+ if is_given(parallel_tool_calls)
116
+ else self._opts.parallel_tool_calls
117
+ )
118
+ if is_given(parallel_tool_calls):
119
+ extra["parallel_tool_calls"] = parallel_tool_calls
120
+
121
+ tool_choice = tool_choice if is_given(tool_choice) else self._opts.tool_choice # type: ignore
122
+ if is_given(tool_choice):
123
+ oai_tool_choice: ChatCompletionToolChoiceOptionParam
124
+ if isinstance(tool_choice, dict):
125
+ oai_tool_choice = {
126
+ "type": "function",
127
+ "function": {"name": tool_choice["function"]["name"]},
128
+ }
129
+ extra["tool_choice"] = oai_tool_choice
130
+ elif tool_choice in ("auto", "required", "none"):
131
+ oai_tool_choice = tool_choice
132
+ extra["tool_choice"] = oai_tool_choice
133
+ logger.info("llm start", extra={"model": self._opts.model})
134
+ return LLMStream(
135
+ self,
136
+ model=self._opts.model,
137
+ client=self._client,
138
+ chat_ctx=chat_ctx,
139
+ tools=tools or [],
140
+ conn_options=conn_options,
141
+ extra_kwargs=extra,
142
+ )
143
+
144
+
145
+ class LLMStream(llm.LLMStream):
146
+ def __init__(
147
+ self,
148
+ llm: LLM,
149
+ *,
150
+ model: str,
151
+ client: openai.AsyncClient,
152
+ chat_ctx: llm.ChatContext,
153
+ tools: list[llm.Tool],
154
+ conn_options: APIConnectOptions,
155
+ extra_kwargs: dict[str, Any],
156
+ ) -> None:
157
+ super().__init__(llm, chat_ctx=chat_ctx, tools=tools, conn_options=conn_options)
158
+ self._model = model
159
+ self._client = client
160
+ self._llm = llm
161
+ self._extra_kwargs = extra_kwargs
162
+
163
+ async def _run(self) -> None:
164
+ # current function call that we're waiting for full completion (args are streamed)
165
+ # (defined inside the _run method to make sure the state is reset for each run/attempt)
166
+ self._oai_stream: openai.AsyncStream[ChatCompletionChunk] | None = None
167
+ self._tool_call_id: str | None = None
168
+ self._fnc_name: str | None = None
169
+ self._fnc_raw_arguments: str | None = None
170
+ self._tool_index: int | None = None
171
+ retryable = True
172
+ first_response = True
173
+ try:
174
+ stream: openai.AsyncStream[
175
+ ChatCompletionChunk
176
+ ] = await self._client.chat.completions.create(
177
+ messages=to_chat_ctx(self._chat_ctx, id(self._llm)),
178
+ tools=to_fnc_ctx(self._tools) if self._tools else openai.NOT_GIVEN,
179
+ model=self._model,
180
+ stream_options={"include_usage": True},
181
+ stream=True,
182
+ **self._extra_kwargs,
183
+ )
184
+
185
+ async with stream:
186
+ async for chunk in stream:
187
+ for choice in chunk.choices:
188
+ chat_chunk = self._parse_choice(chunk.id, choice)
189
+ if chat_chunk is not None:
190
+ retryable = False
191
+ self._event_ch.send_nowait(chat_chunk)
192
+ if first_response:
193
+ logger.info("llm first response")
194
+ first_response = False
195
+
196
+ if chunk.usage is not None:
197
+ retryable = False
198
+ usage_chunk = llm.ChatChunk(
199
+ id=chunk.id,
200
+ usage=llm.CompletionUsage(
201
+ completion_tokens=chunk.usage.completion_tokens,
202
+ prompt_tokens=chunk.usage.prompt_tokens,
203
+ total_tokens=chunk.usage.total_tokens,
204
+ ),
205
+ )
206
+ self._event_ch.send_nowait(usage_chunk)
207
+ logger.info("llm end")
208
+
209
+ except openai.APITimeoutError:
210
+ raise APITimeoutError(retryable=retryable) # noqa: B904
211
+ except openai.APIStatusError as e:
212
+ raise APIStatusError( # noqa: B904
213
+ e.message,
214
+ status_code=e.status_code,
215
+ request_id=e.request_id,
216
+ body=e.body,
217
+ retryable=retryable,
218
+ )
219
+ except Exception as e:
220
+ raise APIConnectionError(retryable=retryable) from e
221
+
222
+ def _parse_choice(self, id: str, choice: Choice) -> llm.ChatChunk | None:
223
+ delta = choice.delta
224
+
225
+ if delta is None:
226
+ return None
227
+
228
+ if delta.tool_calls:
229
+ for tool in delta.tool_calls:
230
+ if not tool.function:
231
+ continue
232
+
233
+ call_chunk = None
234
+ if self._tool_call_id and tool.id and tool.index != self._tool_index:
235
+ call_chunk = llm.ChatChunk(
236
+ id=id,
237
+ delta=llm.ChoiceDelta(
238
+ role="assistant",
239
+ content=delta.content,
240
+ tool_calls=[
241
+ llm.FunctionToolCall(
242
+ arguments=self._fnc_raw_arguments or "",
243
+ name=self._fnc_name or "",
244
+ call_id=self._tool_call_id or "",
245
+ )
246
+ ],
247
+ ),
248
+ )
249
+ self._tool_call_id = self._fnc_name = self._fnc_raw_arguments = None
250
+
251
+ if tool.function.name:
252
+ self._tool_index = tool.index
253
+ self._tool_call_id = tool.id
254
+ self._fnc_name = tool.function.name
255
+ self._fnc_raw_arguments = tool.function.arguments or ""
256
+ elif tool.function.arguments:
257
+ self._fnc_raw_arguments += tool.function.arguments # type: ignore
258
+
259
+ if call_chunk is not None:
260
+ return call_chunk
261
+
262
+ if choice.finish_reason in ("tool_calls", "stop") and self._tool_call_id:
263
+ call_chunk = llm.ChatChunk(
264
+ id=id,
265
+ delta=llm.ChoiceDelta(
266
+ role="assistant",
267
+ content=delta.content,
268
+ tool_calls=[
269
+ llm.FunctionToolCall(
270
+ arguments=self._fnc_raw_arguments or "",
271
+ name=self._fnc_name or "",
272
+ call_id=self._tool_call_id or "",
273
+ )
274
+ ],
275
+ ),
276
+ )
277
+ self._tool_call_id = self._fnc_name = self._fnc_raw_arguments = None
278
+ return call_chunk
279
+
280
+ return llm.ChatChunk(
281
+ id=id,
282
+ delta=llm.ChoiceDelta(content=delta.content, role="assistant"),
283
+ )
@@ -0,0 +1,3 @@
1
+ from logging import getLogger
2
+
3
+ logger = getLogger("livekit.plugins.volcengine")
File without changes