livellm 1.1.1__py3-none-any.whl → 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.
- livellm/__init__.py +6 -2
- livellm/livellm.py +671 -69
- livellm/models/__init__.py +5 -0
- livellm/models/agent/agent.py +3 -4
- livellm/models/audio/speak.py +13 -0
- livellm/models/audio/transcribe.py +7 -8
- livellm/models/fallback.py +3 -3
- livellm/models/transcription.py +32 -0
- livellm/models/ws.py +28 -0
- livellm/transcripton.py +114 -0
- livellm-1.3.0.dist-info/METADATA +634 -0
- livellm-1.3.0.dist-info/RECORD +20 -0
- livellm-1.1.1.dist-info/METADATA +0 -625
- livellm-1.1.1.dist-info/RECORD +0 -17
- {livellm-1.1.1.dist-info → livellm-1.3.0.dist-info}/WHEEL +0 -0
- {livellm-1.1.1.dist-info → livellm-1.3.0.dist-info}/licenses/LICENSE +0 -0
livellm/livellm.py
CHANGED
|
@@ -3,14 +3,632 @@ import asyncio
|
|
|
3
3
|
import httpx
|
|
4
4
|
import json
|
|
5
5
|
import warnings
|
|
6
|
-
from typing import List, Optional, AsyncIterator, Union
|
|
6
|
+
from typing import List, Optional, AsyncIterator, Union, overload
|
|
7
7
|
from .models.common import Settings, SuccessResponse
|
|
8
8
|
from .models.agent.agent import AgentRequest, AgentResponse
|
|
9
|
-
from .models.audio.speak import SpeakRequest
|
|
9
|
+
from .models.audio.speak import SpeakRequest, EncodedSpeakResponse
|
|
10
10
|
from .models.audio.transcribe import TranscribeRequest, TranscribeResponse, File
|
|
11
11
|
from .models.fallback import AgentFallbackRequest, AudioFallbackRequest, TranscribeFallbackRequest
|
|
12
|
+
import websockets
|
|
13
|
+
from .models.ws import WsRequest, WsResponse, WsStatus, WsAction
|
|
14
|
+
from .transcripton import TranscriptionWsClient
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseLivellmClient(ABC):
|
|
20
|
+
|
|
21
|
+
@overload
|
|
22
|
+
async def agent_run(
|
|
23
|
+
self,
|
|
24
|
+
request: Union[AgentRequest, AgentFallbackRequest],
|
|
25
|
+
) -> AgentResponse:
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
@overload
|
|
29
|
+
async def agent_run(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
provider_uid: str,
|
|
33
|
+
model: str,
|
|
34
|
+
messages: list,
|
|
35
|
+
tools: Optional[list] = None,
|
|
36
|
+
**kwargs
|
|
37
|
+
) -> AgentResponse:
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
async def handle_agent_run(self, request: Union[AgentRequest, AgentFallbackRequest]) -> AgentResponse:
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
async def agent_run(
|
|
46
|
+
self,
|
|
47
|
+
request: Optional[Union[AgentRequest, AgentFallbackRequest]] = None,
|
|
48
|
+
*,
|
|
49
|
+
provider_uid: Optional[str] = None,
|
|
50
|
+
model: Optional[str] = None,
|
|
51
|
+
messages: Optional[list] = None,
|
|
52
|
+
tools: Optional[list] = None,
|
|
53
|
+
**kwargs
|
|
54
|
+
) -> AgentResponse:
|
|
55
|
+
"""
|
|
56
|
+
Run an agent request.
|
|
57
|
+
|
|
58
|
+
Can be called in two ways:
|
|
59
|
+
|
|
60
|
+
1. With a request object:
|
|
61
|
+
await client.agent_run(AgentRequest(...))
|
|
62
|
+
await client.agent_run(AgentFallbackRequest(...))
|
|
63
|
+
|
|
64
|
+
2. With individual parameters (keyword arguments):
|
|
65
|
+
await client.agent_run(
|
|
66
|
+
provider_uid="...",
|
|
67
|
+
model="gpt-4",
|
|
68
|
+
messages=[TextMessage(...)],
|
|
69
|
+
tools=[]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
request: An AgentRequest or AgentFallbackRequest object
|
|
74
|
+
provider_uid: The provider UID string
|
|
75
|
+
model: The model to use
|
|
76
|
+
messages: List of messages
|
|
77
|
+
tools: Optional list of tools
|
|
78
|
+
gen_config: Optional generation configuration
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
AgentResponse with the agent's output
|
|
82
|
+
"""
|
|
83
|
+
# Check if first argument is a request object
|
|
84
|
+
if request is not None:
|
|
85
|
+
if not isinstance(request, (AgentRequest, AgentFallbackRequest)):
|
|
86
|
+
raise TypeError(
|
|
87
|
+
f"First positional argument must be AgentRequest or AgentFallbackRequest, got {type(request)}"
|
|
88
|
+
)
|
|
89
|
+
return await self.handle_agent_run(request)
|
|
90
|
+
|
|
91
|
+
# Otherwise, use keyword arguments
|
|
92
|
+
if provider_uid is None or model is None or messages is None:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
"provider_uid, model, and messages are required. "
|
|
95
|
+
"Alternatively, pass an AgentRequest object as the first positional argument."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
agent_request = AgentRequest(
|
|
99
|
+
provider_uid=provider_uid,
|
|
100
|
+
model=model,
|
|
101
|
+
messages=messages,
|
|
102
|
+
tools=tools or [],
|
|
103
|
+
gen_config=kwargs or None
|
|
104
|
+
)
|
|
105
|
+
return await self.handle_agent_run(agent_request)
|
|
106
|
+
|
|
107
|
+
@overload
|
|
108
|
+
def agent_run_stream(
|
|
109
|
+
self,
|
|
110
|
+
request: Union[AgentRequest, AgentFallbackRequest],
|
|
111
|
+
) -> AsyncIterator[AgentResponse]:
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
@overload
|
|
115
|
+
def agent_run_stream(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
provider_uid: str,
|
|
119
|
+
model: str,
|
|
120
|
+
messages: list,
|
|
121
|
+
tools: Optional[list] = None,
|
|
122
|
+
**kwargs
|
|
123
|
+
) -> AsyncIterator[AgentResponse]:
|
|
124
|
+
...
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
async def handle_agent_run_stream(self, request: Union[AgentRequest, AgentFallbackRequest]) -> AsyncIterator[AgentResponse]:
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
async def agent_run_stream(
|
|
132
|
+
self,
|
|
133
|
+
request: Optional[Union[AgentRequest, AgentFallbackRequest]] = None,
|
|
134
|
+
*,
|
|
135
|
+
provider_uid: Optional[str] = None,
|
|
136
|
+
model: Optional[str] = None,
|
|
137
|
+
messages: Optional[list] = None,
|
|
138
|
+
tools: Optional[list] = None,
|
|
139
|
+
**kwargs
|
|
140
|
+
) -> AsyncIterator[AgentResponse]:
|
|
141
|
+
"""
|
|
142
|
+
Run an agent request with streaming response.
|
|
143
|
+
|
|
144
|
+
Can be called in two ways:
|
|
145
|
+
|
|
146
|
+
1. With a request object:
|
|
147
|
+
async for chunk in client.agent_run_stream(AgentRequest(...)):
|
|
148
|
+
...
|
|
149
|
+
async for chunk in client.agent_run_stream(AgentFallbackRequest(...)):
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
2. With individual parameters (keyword arguments):
|
|
153
|
+
async for chunk in client.agent_run_stream(
|
|
154
|
+
provider_uid="...",
|
|
155
|
+
model="gpt-4",
|
|
156
|
+
messages=[TextMessage(...)],
|
|
157
|
+
tools=[]
|
|
158
|
+
):
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
request: An AgentRequest or AgentFallbackRequest object
|
|
163
|
+
provider_uid: The provider UID string
|
|
164
|
+
model: The model to use
|
|
165
|
+
messages: List of messages
|
|
166
|
+
tools: Optional list of tools
|
|
167
|
+
gen_config: Optional generation configuration
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
AsyncIterator of AgentResponse chunks
|
|
171
|
+
"""
|
|
172
|
+
# Check if first argument is a request object
|
|
173
|
+
if request is not None:
|
|
174
|
+
if not isinstance(request, (AgentRequest, AgentFallbackRequest)):
|
|
175
|
+
raise TypeError(
|
|
176
|
+
f"First positional argument must be AgentRequest or AgentFallbackRequest, got {type(request)}"
|
|
177
|
+
)
|
|
178
|
+
stream = self.handle_agent_run_stream(request)
|
|
179
|
+
else:
|
|
180
|
+
# Otherwise, use keyword arguments
|
|
181
|
+
if provider_uid is None or model is None or messages is None:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
"provider_uid, model, and messages are required. "
|
|
184
|
+
"Alternatively, pass an AgentRequest object as the first positional argument."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
agent_request = AgentRequest(
|
|
188
|
+
provider_uid=provider_uid,
|
|
189
|
+
model=model,
|
|
190
|
+
messages=messages,
|
|
191
|
+
tools=tools or [],
|
|
192
|
+
gen_config=kwargs or None
|
|
193
|
+
)
|
|
194
|
+
stream = self.handle_agent_run_stream(agent_request)
|
|
195
|
+
|
|
196
|
+
async for chunk in stream:
|
|
197
|
+
yield chunk
|
|
198
|
+
|
|
199
|
+
@overload
|
|
200
|
+
async def speak(
|
|
201
|
+
self,
|
|
202
|
+
request: Union[SpeakRequest, AudioFallbackRequest],
|
|
203
|
+
) -> bytes:
|
|
204
|
+
...
|
|
205
|
+
|
|
206
|
+
@overload
|
|
207
|
+
async def speak(
|
|
208
|
+
self,
|
|
209
|
+
*,
|
|
210
|
+
provider_uid: str,
|
|
211
|
+
model: str,
|
|
212
|
+
text: str,
|
|
213
|
+
voice: str,
|
|
214
|
+
mime_type: str,
|
|
215
|
+
sample_rate: int,
|
|
216
|
+
chunk_size: int = 20,
|
|
217
|
+
**kwargs
|
|
218
|
+
) -> bytes:
|
|
219
|
+
...
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@abstractmethod
|
|
223
|
+
async def handle_speak(self, request: Union[SpeakRequest, AudioFallbackRequest]) -> bytes:
|
|
224
|
+
...
|
|
225
|
+
|
|
226
|
+
async def speak(
|
|
227
|
+
self,
|
|
228
|
+
request: Optional[Union[SpeakRequest, AudioFallbackRequest]] = None,
|
|
229
|
+
*,
|
|
230
|
+
provider_uid: Optional[str] = None,
|
|
231
|
+
model: Optional[str] = None,
|
|
232
|
+
text: Optional[str] = None,
|
|
233
|
+
voice: Optional[str] = None,
|
|
234
|
+
mime_type: Optional[str] = None,
|
|
235
|
+
sample_rate: Optional[int] = None,
|
|
236
|
+
chunk_size: int = 20,
|
|
237
|
+
**kwargs
|
|
238
|
+
) -> bytes:
|
|
239
|
+
"""
|
|
240
|
+
Generate speech from text.
|
|
241
|
+
|
|
242
|
+
Can be called in two ways:
|
|
243
|
+
|
|
244
|
+
1. With a request object:
|
|
245
|
+
await client.speak(SpeakRequest(...))
|
|
246
|
+
await client.speak(AudioFallbackRequest(...))
|
|
247
|
+
|
|
248
|
+
2. With individual parameters (keyword arguments):
|
|
249
|
+
await client.speak(
|
|
250
|
+
provider_uid="...",
|
|
251
|
+
model="tts-1",
|
|
252
|
+
text="Hello, world!",
|
|
253
|
+
voice="alloy",
|
|
254
|
+
mime_type="audio/pcm",
|
|
255
|
+
sample_rate=24000
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
request: A SpeakRequest or AudioFallbackRequest object
|
|
260
|
+
provider_uid: The provider UID string
|
|
261
|
+
model: The model to use for TTS
|
|
262
|
+
text: The text to convert to speech
|
|
263
|
+
voice: The voice to use
|
|
264
|
+
mime_type: The MIME type of the output audio
|
|
265
|
+
sample_rate: The sample rate of the output audio
|
|
266
|
+
chunk_size: Chunk size in milliseconds (default: 20ms)
|
|
267
|
+
gen_config: Optional generation configuration
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Audio data as bytes
|
|
271
|
+
"""
|
|
272
|
+
# Check if first argument is a request object
|
|
273
|
+
if request is not None:
|
|
274
|
+
if not isinstance(request, (SpeakRequest, AudioFallbackRequest)):
|
|
275
|
+
raise TypeError(
|
|
276
|
+
f"First positional argument must be SpeakRequest or AudioFallbackRequest, got {type(request)}"
|
|
277
|
+
)
|
|
278
|
+
return await self.handle_speak(request)
|
|
279
|
+
|
|
280
|
+
# Otherwise, use keyword arguments
|
|
281
|
+
if provider_uid is None or model is None or text is None or voice is None or mime_type is None or sample_rate is None:
|
|
282
|
+
raise ValueError(
|
|
283
|
+
"provider_uid, model, text, voice, mime_type, and sample_rate are required. "
|
|
284
|
+
"Alternatively, pass a SpeakRequest object as the first positional argument."
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
speak_request = SpeakRequest(
|
|
288
|
+
provider_uid=provider_uid,
|
|
289
|
+
model=model,
|
|
290
|
+
text=text,
|
|
291
|
+
voice=voice,
|
|
292
|
+
mime_type=mime_type,
|
|
293
|
+
sample_rate=sample_rate,
|
|
294
|
+
chunk_size=chunk_size,
|
|
295
|
+
gen_config=kwargs or None
|
|
296
|
+
)
|
|
297
|
+
return await self.handle_speak(speak_request)
|
|
298
|
+
|
|
299
|
+
@overload
|
|
300
|
+
def speak_stream(
|
|
301
|
+
self,
|
|
302
|
+
request: Union[SpeakRequest, AudioFallbackRequest],
|
|
303
|
+
) -> AsyncIterator[bytes]:
|
|
304
|
+
...
|
|
305
|
+
|
|
306
|
+
@overload
|
|
307
|
+
def speak_stream(
|
|
308
|
+
self,
|
|
309
|
+
*,
|
|
310
|
+
provider_uid: str,
|
|
311
|
+
model: str,
|
|
312
|
+
text: str,
|
|
313
|
+
voice: str,
|
|
314
|
+
mime_type: str,
|
|
315
|
+
sample_rate: int,
|
|
316
|
+
chunk_size: int = 20,
|
|
317
|
+
**kwargs
|
|
318
|
+
) -> AsyncIterator[bytes]:
|
|
319
|
+
...
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@abstractmethod
|
|
323
|
+
async def handle_speak_stream(self, request: Union[SpeakRequest, AudioFallbackRequest]) -> AsyncIterator[bytes]:
|
|
324
|
+
...
|
|
325
|
+
|
|
326
|
+
async def speak_stream(
|
|
327
|
+
self,
|
|
328
|
+
request: Optional[Union[SpeakRequest, AudioFallbackRequest]] = None,
|
|
329
|
+
*,
|
|
330
|
+
provider_uid: Optional[str] = None,
|
|
331
|
+
model: Optional[str] = None,
|
|
332
|
+
text: Optional[str] = None,
|
|
333
|
+
voice: Optional[str] = None,
|
|
334
|
+
mime_type: Optional[str] = None,
|
|
335
|
+
sample_rate: Optional[int] = None,
|
|
336
|
+
chunk_size: int = 20,
|
|
337
|
+
**kwargs
|
|
338
|
+
) -> AsyncIterator[bytes]:
|
|
339
|
+
"""
|
|
340
|
+
Generate speech from text with streaming response.
|
|
341
|
+
|
|
342
|
+
Can be called in two ways:
|
|
343
|
+
|
|
344
|
+
1. With a request object:
|
|
345
|
+
async for chunk in client.speak_stream(SpeakRequest(...)):
|
|
346
|
+
...
|
|
347
|
+
async for chunk in client.speak_stream(AudioFallbackRequest(...)):
|
|
348
|
+
...
|
|
349
|
+
|
|
350
|
+
2. With individual parameters (keyword arguments):
|
|
351
|
+
async for chunk in client.speak_stream(
|
|
352
|
+
provider_uid="...",
|
|
353
|
+
model="tts-1",
|
|
354
|
+
text="Hello, world!",
|
|
355
|
+
voice="alloy",
|
|
356
|
+
mime_type="audio/pcm",
|
|
357
|
+
sample_rate=24000
|
|
358
|
+
):
|
|
359
|
+
...
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
request: A SpeakRequest or AudioFallbackRequest object
|
|
363
|
+
provider_uid: The provider UID string
|
|
364
|
+
model: The model to use for TTS
|
|
365
|
+
text: The text to convert to speech
|
|
366
|
+
voice: The voice to use
|
|
367
|
+
mime_type: The MIME type of the output audio
|
|
368
|
+
sample_rate: The sample rate of the output audio
|
|
369
|
+
chunk_size: Chunk size in milliseconds (default: 20ms)
|
|
370
|
+
gen_config: Optional generation configuration
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
AsyncIterator of audio data chunks as bytes
|
|
374
|
+
"""
|
|
375
|
+
# Check if first argument is a request object
|
|
376
|
+
if request is not None:
|
|
377
|
+
if not isinstance(request, (SpeakRequest, AudioFallbackRequest)):
|
|
378
|
+
raise TypeError(
|
|
379
|
+
f"First positional argument must be SpeakRequest or AudioFallbackRequest, got {type(request)}"
|
|
380
|
+
)
|
|
381
|
+
speak_stream = self.handle_speak_stream(request)
|
|
382
|
+
else:
|
|
383
|
+
# Otherwise, use keyword arguments
|
|
384
|
+
if provider_uid is None or model is None or text is None or voice is None or mime_type is None or sample_rate is None:
|
|
385
|
+
raise ValueError(
|
|
386
|
+
"provider_uid, model, text, voice, mime_type, and sample_rate are required. "
|
|
387
|
+
"Alternatively, pass a SpeakRequest object as the first positional argument."
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
speak_request = SpeakRequest(
|
|
391
|
+
provider_uid=provider_uid,
|
|
392
|
+
model=model,
|
|
393
|
+
text=text,
|
|
394
|
+
voice=voice,
|
|
395
|
+
mime_type=mime_type,
|
|
396
|
+
sample_rate=sample_rate,
|
|
397
|
+
chunk_size=chunk_size,
|
|
398
|
+
gen_config=kwargs or None
|
|
399
|
+
)
|
|
400
|
+
speak_stream = self.handle_speak_stream(speak_request)
|
|
401
|
+
async for chunk in speak_stream:
|
|
402
|
+
yield chunk
|
|
403
|
+
|
|
404
|
+
@overload
|
|
405
|
+
async def transcribe(
|
|
406
|
+
self,
|
|
407
|
+
request: Union[TranscribeRequest, TranscribeFallbackRequest],
|
|
408
|
+
) -> TranscribeResponse:
|
|
409
|
+
...
|
|
410
|
+
|
|
411
|
+
@overload
|
|
412
|
+
async def transcribe(
|
|
413
|
+
self,
|
|
414
|
+
*,
|
|
415
|
+
provider_uid: str,
|
|
416
|
+
file: File,
|
|
417
|
+
model: str,
|
|
418
|
+
language: Optional[str] = None,
|
|
419
|
+
**kwargs
|
|
420
|
+
) -> TranscribeResponse:
|
|
421
|
+
...
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@abstractmethod
|
|
425
|
+
async def handle_transcribe(self, request: Union[TranscribeRequest, TranscribeFallbackRequest]) -> TranscribeResponse:
|
|
426
|
+
...
|
|
427
|
+
|
|
428
|
+
async def transcribe(
|
|
429
|
+
self,
|
|
430
|
+
request: Optional[Union[TranscribeRequest, TranscribeFallbackRequest]] = None,
|
|
431
|
+
*,
|
|
432
|
+
provider_uid: Optional[str] = None,
|
|
433
|
+
file: Optional[File] = None,
|
|
434
|
+
model: Optional[str] = None,
|
|
435
|
+
language: Optional[str] = None,
|
|
436
|
+
**kwargs
|
|
437
|
+
) -> TranscribeResponse:
|
|
438
|
+
"""
|
|
439
|
+
Transcribe audio to text.
|
|
440
|
+
|
|
441
|
+
Can be called in two ways:
|
|
442
|
+
|
|
443
|
+
1. With a request object:
|
|
444
|
+
await client.transcribe(TranscribeRequest(...))
|
|
445
|
+
|
|
446
|
+
2. With individual parameters (keyword arguments):
|
|
447
|
+
await client.transcribe(
|
|
448
|
+
provider_uid="...",
|
|
449
|
+
file=("filename", audio_bytes, "audio/wav"),
|
|
450
|
+
model="whisper-1"
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
request: A TranscribeRequest or TranscribeFallbackRequest object
|
|
455
|
+
provider_uid: The provider UID string
|
|
456
|
+
file: The audio file as a tuple (filename, content, content_type)
|
|
457
|
+
model: The model to use for transcription
|
|
458
|
+
language: Optional language code
|
|
459
|
+
gen_config: Optional generation configuration
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
TranscribeResponse with transcription text and detected language
|
|
463
|
+
"""
|
|
464
|
+
# Check if first argument is a request object
|
|
465
|
+
if request is not None:
|
|
466
|
+
if not isinstance(request, (TranscribeRequest, TranscribeFallbackRequest)):
|
|
467
|
+
raise TypeError(
|
|
468
|
+
f"First positional argument must be TranscribeRequest or TranscribeFallbackRequest, got {type(request)}"
|
|
469
|
+
)
|
|
470
|
+
# JSON-based request
|
|
471
|
+
return await self.handle_transcribe(request)
|
|
472
|
+
|
|
473
|
+
# Otherwise, use keyword arguments with multipart form-data request
|
|
474
|
+
if provider_uid is None or file is None or model is None:
|
|
475
|
+
raise ValueError(
|
|
476
|
+
"provider_uid, file, and model are required. "
|
|
477
|
+
"Alternatively, pass a TranscribeRequest object as the first positional argument."
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
transcribe_request = TranscribeRequest(
|
|
481
|
+
provider_uid=provider_uid,
|
|
482
|
+
file=file,
|
|
483
|
+
model=model,
|
|
484
|
+
language=language,
|
|
485
|
+
gen_config=kwargs or None
|
|
486
|
+
)
|
|
487
|
+
return await self.handle_transcribe(transcribe_request)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class LivellmWsClient(BaseLivellmClient):
|
|
491
|
+
"""WebSocket-based LiveLLM client for real-time bidirectional communication."""
|
|
492
|
+
|
|
493
|
+
def __init__(
|
|
494
|
+
self,
|
|
495
|
+
base_url: str,
|
|
496
|
+
timeout: Optional[float] = None
|
|
497
|
+
):
|
|
498
|
+
# Convert HTTP(S) URL to WS(S) URL
|
|
499
|
+
base_url = base_url.rstrip("/")
|
|
500
|
+
if base_url.startswith("https://"):
|
|
501
|
+
ws_url = base_url.replace("https://", "wss://")
|
|
502
|
+
elif base_url.startswith("http://"):
|
|
503
|
+
ws_url = base_url.replace("http://", "ws://")
|
|
504
|
+
else:
|
|
505
|
+
ws_url = base_url
|
|
506
|
+
|
|
507
|
+
# Root WebSocket base URL (without path) and main /ws endpoint
|
|
508
|
+
self._ws_root_base_url = ws_url
|
|
509
|
+
self.base_url = f"{ws_url}/livellm/ws"
|
|
510
|
+
self.timeout = timeout
|
|
511
|
+
self.websocket = None
|
|
512
|
+
# Lazily-created clients
|
|
513
|
+
self._transcription = None
|
|
514
|
+
|
|
515
|
+
async def connect(self):
|
|
516
|
+
"""Establish WebSocket connection."""
|
|
517
|
+
if self.websocket is not None:
|
|
518
|
+
return self.websocket
|
|
519
|
+
|
|
520
|
+
self.websocket = await websockets.connect(
|
|
521
|
+
self.base_url,
|
|
522
|
+
open_timeout=self.timeout,
|
|
523
|
+
close_timeout=self.timeout
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return self.websocket
|
|
527
|
+
|
|
528
|
+
async def disconnect(self):
|
|
529
|
+
"""Close WebSocket connection."""
|
|
530
|
+
if self.websocket is not None:
|
|
531
|
+
await self.websocket.close()
|
|
532
|
+
self.websocket = None
|
|
533
|
+
|
|
534
|
+
async def get_response(self, action: WsAction, payload: dict) -> WsResponse:
|
|
535
|
+
"""Send a request and wait for response."""
|
|
536
|
+
if self.websocket is None:
|
|
537
|
+
await self.connect()
|
|
538
|
+
|
|
539
|
+
request = WsRequest(action=action, payload=payload)
|
|
540
|
+
await self.websocket.send(json.dumps(request.model_dump()))
|
|
541
|
+
|
|
542
|
+
response_data = await self.websocket.recv()
|
|
543
|
+
response = WsResponse(**json.loads(response_data))
|
|
544
|
+
|
|
545
|
+
if response.status == WsStatus.ERROR:
|
|
546
|
+
raise Exception(f"WebSocket request failed: {response.error}")
|
|
547
|
+
|
|
548
|
+
return response
|
|
549
|
+
|
|
550
|
+
async def get_response_stream(self, action: WsAction, payload: dict) -> AsyncIterator[WsResponse]:
|
|
551
|
+
"""Send a request and stream responses."""
|
|
552
|
+
if self.websocket is None:
|
|
553
|
+
await self.connect()
|
|
554
|
+
|
|
555
|
+
request = WsRequest(action=action, payload=payload)
|
|
556
|
+
await self.websocket.send(json.dumps(request.model_dump()))
|
|
557
|
+
|
|
558
|
+
while True:
|
|
559
|
+
response_data = await self.websocket.recv()
|
|
560
|
+
response = WsResponse(**json.loads(response_data))
|
|
561
|
+
|
|
562
|
+
if response.status == WsStatus.ERROR:
|
|
563
|
+
raise Exception(f"WebSocket stream failed: {response.error}")
|
|
564
|
+
|
|
565
|
+
yield response
|
|
566
|
+
|
|
567
|
+
if response.status == WsStatus.SUCCESS:
|
|
568
|
+
break
|
|
569
|
+
|
|
570
|
+
# Implement abstract methods from BaseLivellmClient
|
|
571
|
+
|
|
572
|
+
async def handle_agent_run(self, request: Union[AgentRequest, AgentFallbackRequest]) -> AgentResponse:
|
|
573
|
+
"""Handle agent run via WebSocket."""
|
|
574
|
+
response = await self.get_response(
|
|
575
|
+
WsAction.AGENT_RUN,
|
|
576
|
+
request.model_dump()
|
|
577
|
+
)
|
|
578
|
+
return AgentResponse(**response.data)
|
|
579
|
+
|
|
580
|
+
async def handle_agent_run_stream(self, request: Union[AgentRequest, AgentFallbackRequest]) -> AsyncIterator[AgentResponse]:
|
|
581
|
+
"""Handle streaming agent run via WebSocket."""
|
|
582
|
+
async for response in self.get_response_stream(WsAction.AGENT_RUN_STREAM, request.model_dump()):
|
|
583
|
+
yield AgentResponse(**response.data)
|
|
584
|
+
|
|
585
|
+
async def handle_speak(self, request: Union[SpeakRequest, AudioFallbackRequest]) -> EncodedSpeakResponse:
|
|
586
|
+
"""Handle speak request via WebSocket."""
|
|
587
|
+
response = await self.get_response(
|
|
588
|
+
WsAction.AUDIO_SPEAK,
|
|
589
|
+
request.model_dump()
|
|
590
|
+
)
|
|
591
|
+
return EncodedSpeakResponse(**response.data)
|
|
592
|
+
|
|
593
|
+
async def handle_speak_stream(self, request: Union[SpeakRequest, AudioFallbackRequest]) -> AsyncIterator[EncodedSpeakResponse]:
|
|
594
|
+
"""Handle streaming speak request via WebSocket."""
|
|
595
|
+
async for response in self.get_response_stream(WsAction.AUDIO_SPEAK_STREAM, request.model_dump()):
|
|
596
|
+
yield EncodedSpeakResponse(**response.data)
|
|
597
|
+
|
|
598
|
+
async def handle_transcribe(self, request: Union[TranscribeRequest, TranscribeFallbackRequest]) -> TranscribeResponse:
|
|
599
|
+
"""Handle transcribe request via WebSocket."""
|
|
600
|
+
response = await self.get_response(
|
|
601
|
+
WsAction.AUDIO_TRANSCRIBE,
|
|
602
|
+
request.model_dump()
|
|
603
|
+
)
|
|
604
|
+
return TranscribeResponse(**response.data)
|
|
605
|
+
|
|
606
|
+
# Context manager support
|
|
607
|
+
|
|
608
|
+
async def __aenter__(self):
|
|
609
|
+
await self.connect()
|
|
610
|
+
return self
|
|
611
|
+
|
|
612
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
613
|
+
await self.disconnect()
|
|
614
|
+
|
|
615
|
+
@property
|
|
616
|
+
def transcription(self) -> TranscriptionWsClient:
|
|
617
|
+
"""
|
|
618
|
+
Lazily-initialized WebSocket transcription client that shares the same
|
|
619
|
+
server base URL and timeout as this realtime client.
|
|
620
|
+
"""
|
|
621
|
+
if self._transcription is None:
|
|
622
|
+
# Use the ws root (e.g. ws://host:port) and let TranscriptionWsClient
|
|
623
|
+
# append its own /livellm/ws/transcription path.
|
|
624
|
+
self._transcription = TranscriptionWsClient(
|
|
625
|
+
base_url=self._ws_root_base_url,
|
|
626
|
+
timeout=self.timeout,
|
|
627
|
+
)
|
|
628
|
+
return self._transcription
|
|
629
|
+
|
|
630
|
+
class LivellmClient(BaseLivellmClient):
|
|
631
|
+
"""HTTP-based LiveLLM client for request-response communication."""
|
|
14
632
|
|
|
15
633
|
def __init__(
|
|
16
634
|
self,
|
|
@@ -18,8 +636,10 @@ class LivellmClient:
|
|
|
18
636
|
timeout: Optional[float] = None,
|
|
19
637
|
configs: Optional[List[Settings]] = None
|
|
20
638
|
):
|
|
21
|
-
|
|
22
|
-
self.
|
|
639
|
+
# Root server URL (http/https, without trailing slash)
|
|
640
|
+
self._root_base_url = base_url.rstrip("/")
|
|
641
|
+
# HTTP API base URL for this client
|
|
642
|
+
self.base_url = f"{self._root_base_url}/livellm"
|
|
23
643
|
self.timeout = timeout
|
|
24
644
|
self.client = httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout) \
|
|
25
645
|
if self.timeout else httpx.AsyncClient(base_url=self.base_url)
|
|
@@ -27,10 +647,27 @@ class LivellmClient:
|
|
|
27
647
|
self.headers = {
|
|
28
648
|
"Content-Type": "application/json",
|
|
29
649
|
}
|
|
650
|
+
# Lazily-created realtime (WebSocket) client
|
|
651
|
+
self._realtime = None
|
|
30
652
|
if configs:
|
|
31
653
|
self.update_configs_post_init(configs)
|
|
32
|
-
|
|
33
654
|
|
|
655
|
+
@property
|
|
656
|
+
def realtime(self) -> LivellmWsClient:
|
|
657
|
+
"""
|
|
658
|
+
Lazily-initialized WebSocket client for realtime operations (agent, audio, etc.)
|
|
659
|
+
that shares the same server base URL and timeout as this HTTP client.
|
|
660
|
+
|
|
661
|
+
Example:
|
|
662
|
+
client = LivellmClient(base_url=\"http://localhost:8000\")
|
|
663
|
+
async with client.realtime as session:
|
|
664
|
+
response = await session.agent_run(...)
|
|
665
|
+
"""
|
|
666
|
+
if self._realtime is None:
|
|
667
|
+
# Pass the same root base URL; LivellmWsClient will handle ws/wss conversion.
|
|
668
|
+
self._realtime = LivellmWsClient(self._root_base_url, timeout=self.timeout)
|
|
669
|
+
return self._realtime
|
|
670
|
+
|
|
34
671
|
def update_configs_post_init(self, configs: List[Settings]) -> SuccessResponse:
|
|
35
672
|
"""
|
|
36
673
|
Update the configs after the client is initialized.
|
|
@@ -121,22 +758,22 @@ class LivellmClient:
|
|
|
121
758
|
error_response = error_response.decode("utf-8")
|
|
122
759
|
raise Exception(f"Failed to post to {endpoint}: {error_response}")
|
|
123
760
|
if expect_stream:
|
|
124
|
-
async def
|
|
761
|
+
async def json_stream_response() -> AsyncIterator[dict]:
|
|
125
762
|
async for chunk in response.aiter_lines():
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
763
|
+
chunk = chunk.strip()
|
|
764
|
+
if not chunk:
|
|
765
|
+
continue
|
|
766
|
+
yield json.loads(chunk)
|
|
767
|
+
async def bytes_stream_response() -> AsyncIterator[bytes]:
|
|
768
|
+
async for chunk in response.aiter_bytes():
|
|
769
|
+
yield chunk
|
|
770
|
+
stream_response = json_stream_response if expect_json else bytes_stream_response
|
|
133
771
|
return stream_response()
|
|
134
772
|
else:
|
|
135
773
|
if expect_json:
|
|
136
774
|
return response.json()
|
|
137
775
|
else:
|
|
138
776
|
return response.content
|
|
139
|
-
|
|
140
777
|
|
|
141
778
|
async def ping(self) -> SuccessResponse:
|
|
142
779
|
result = await self.get("ping")
|
|
@@ -169,15 +806,10 @@ class LivellmClient:
|
|
|
169
806
|
config: Settings = config
|
|
170
807
|
await self.delete_config(config.uid)
|
|
171
808
|
await self.client.aclose()
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
178
|
-
"""Async context manager exit."""
|
|
179
|
-
await self.cleanup()
|
|
180
|
-
|
|
809
|
+
# Also close any realtime WebSocket client if it was created
|
|
810
|
+
if self._realtime is not None:
|
|
811
|
+
await self._realtime.disconnect()
|
|
812
|
+
|
|
181
813
|
def __del__(self):
|
|
182
814
|
"""
|
|
183
815
|
Destructor to clean up resources when the client is garbage collected.
|
|
@@ -205,61 +837,31 @@ class LivellmClient:
|
|
|
205
837
|
# Silently fail - we're in a destructor
|
|
206
838
|
pass
|
|
207
839
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
840
|
+
# Implement abstract methods from BaseLivellmClient
|
|
841
|
+
|
|
842
|
+
async def handle_agent_run(self, request: Union[AgentRequest, AgentFallbackRequest]) -> AgentResponse:
|
|
843
|
+
"""Handle agent run via HTTP."""
|
|
212
844
|
result = await self.post(request.model_dump(), "agent/run", expect_json=True)
|
|
213
845
|
return AgentResponse(**result)
|
|
214
846
|
|
|
215
|
-
async def
|
|
216
|
-
|
|
217
|
-
request: Union[AgentRequest, AgentFallbackRequest]
|
|
218
|
-
) -> AsyncIterator[AgentResponse]:
|
|
847
|
+
async def handle_agent_run_stream(self, request: Union[AgentRequest, AgentFallbackRequest]) -> AsyncIterator[AgentResponse]:
|
|
848
|
+
"""Handle streaming agent run via HTTP."""
|
|
219
849
|
stream = await self.post(request.model_dump(), "agent/run_stream", expect_stream=True, expect_json=True)
|
|
220
850
|
async for chunk in stream:
|
|
221
851
|
yield AgentResponse(**chunk)
|
|
222
852
|
|
|
223
|
-
async def
|
|
224
|
-
|
|
225
|
-
request: Union[SpeakRequest, AudioFallbackRequest]
|
|
226
|
-
) -> bytes:
|
|
853
|
+
async def handle_speak(self, request: Union[SpeakRequest, AudioFallbackRequest]) -> bytes:
|
|
854
|
+
"""Handle speak request via HTTP."""
|
|
227
855
|
return await self.post(request.model_dump(), "audio/speak", expect_json=False)
|
|
228
856
|
|
|
229
|
-
async def
|
|
230
|
-
|
|
231
|
-
request
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
async def transcribe(
|
|
237
|
-
self,
|
|
238
|
-
provider_uid: str,
|
|
239
|
-
file: File,
|
|
240
|
-
model: str,
|
|
241
|
-
language: Optional[str] = None,
|
|
242
|
-
gen_config: Optional[dict] = None
|
|
243
|
-
) -> TranscribeResponse:
|
|
244
|
-
files = {
|
|
245
|
-
"file": file
|
|
246
|
-
}
|
|
247
|
-
data = {
|
|
248
|
-
"provider_uid": provider_uid,
|
|
249
|
-
"model": model,
|
|
250
|
-
"language": language,
|
|
251
|
-
"gen_config": json.dumps(gen_config) if gen_config else None
|
|
252
|
-
}
|
|
253
|
-
result = await self.post_multipart(files, data, "audio/transcribe")
|
|
254
|
-
return TranscribeResponse(**result)
|
|
857
|
+
async def handle_speak_stream(self, request: Union[SpeakRequest, AudioFallbackRequest]) -> AsyncIterator[bytes]:
|
|
858
|
+
"""Handle streaming speak request via HTTP."""
|
|
859
|
+
speak_stream = await self.post(request.model_dump(), "audio/speak_stream", expect_stream=True, expect_json=False)
|
|
860
|
+
async for chunk in speak_stream:
|
|
861
|
+
yield chunk
|
|
255
862
|
|
|
256
|
-
async def
|
|
257
|
-
|
|
258
|
-
request: Union[TranscribeRequest, TranscribeFallbackRequest]
|
|
259
|
-
) -> TranscribeResponse:
|
|
863
|
+
async def handle_transcribe(self, request: Union[TranscribeRequest, TranscribeFallbackRequest]) -> TranscribeResponse:
|
|
864
|
+
"""Handle transcribe request via HTTP."""
|
|
260
865
|
result = await self.post(request.model_dump(), "audio/transcribe_json", expect_json=True)
|
|
261
866
|
return TranscribeResponse(**result)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
867
|
|