livellm 1.5.5__py3-none-any.whl → 1.7.1__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/livellm.py +258 -98
- livellm/models/__init__.py +5 -1
- livellm/models/agent/__init__.py +5 -1
- livellm/models/agent/agent.py +15 -4
- livellm/models/agent/output_schema.py +120 -0
- livellm/models/transcription.py +2 -0
- livellm/transcripton.py +61 -19
- {livellm-1.5.5.dist-info → livellm-1.7.1.dist-info}/METADATA +299 -33
- {livellm-1.5.5.dist-info → livellm-1.7.1.dist-info}/RECORD +11 -10
- {livellm-1.5.5.dist-info → livellm-1.7.1.dist-info}/WHEEL +0 -0
- {livellm-1.5.5.dist-info → livellm-1.7.1.dist-info}/licenses/LICENSE +0 -0
livellm/models/__init__.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from .common import BaseRequest, ProviderKind, Settings, SuccessResponse
|
|
2
2
|
from .fallback import AgentFallbackRequest, AudioFallbackRequest, TranscribeFallbackRequest, FallbackStrategy
|
|
3
|
-
from .agent.agent import AgentRequest, AgentResponse, AgentResponseUsage
|
|
3
|
+
from .agent.agent import AgentRequest, AgentResponse, AgentResponseUsage, ContextOverflowStrategy
|
|
4
4
|
from .agent.chat import Message, MessageRole, TextMessage, BinaryMessage, ToolCallMessage, ToolReturnMessage
|
|
5
5
|
from .agent.tools import Tool, ToolInput, ToolKind, WebSearchInput, MCPStreamableServerInput
|
|
6
|
+
from .agent.output_schema import OutputSchema, PropertyDef
|
|
6
7
|
from .audio.speak import SpeakMimeType, SpeakRequest, SpeakStreamResponse
|
|
7
8
|
from .audio.transcribe import TranscribeRequest, TranscribeResponse, File
|
|
8
9
|
from .transcription import TranscriptionInitWsRequest, TranscriptionAudioChunkWsRequest, TranscriptionWsResponse
|
|
@@ -23,6 +24,7 @@ __all__ = [
|
|
|
23
24
|
"AgentRequest",
|
|
24
25
|
"AgentResponse",
|
|
25
26
|
"AgentResponseUsage",
|
|
27
|
+
"ContextOverflowStrategy",
|
|
26
28
|
"Message",
|
|
27
29
|
"MessageRole",
|
|
28
30
|
"TextMessage",
|
|
@@ -34,6 +36,8 @@ __all__ = [
|
|
|
34
36
|
"ToolKind",
|
|
35
37
|
"WebSearchInput",
|
|
36
38
|
"MCPStreamableServerInput",
|
|
39
|
+
"OutputSchema",
|
|
40
|
+
"PropertyDef",
|
|
37
41
|
# Audio
|
|
38
42
|
"SpeakMimeType",
|
|
39
43
|
"SpeakRequest",
|
livellm/models/agent/__init__.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
from .agent import AgentRequest, AgentResponse, AgentResponseUsage
|
|
1
|
+
from .agent import AgentRequest, AgentResponse, AgentResponseUsage, ContextOverflowStrategy
|
|
2
2
|
from .chat import Message, MessageRole, TextMessage, BinaryMessage, ToolCallMessage, ToolReturnMessage
|
|
3
3
|
from .tools import Tool, ToolInput, ToolKind, WebSearchInput, MCPStreamableServerInput
|
|
4
|
+
from .output_schema import OutputSchema, PropertyDef
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"AgentRequest",
|
|
8
9
|
"AgentResponse",
|
|
9
10
|
"AgentResponseUsage",
|
|
11
|
+
"ContextOverflowStrategy",
|
|
10
12
|
"Message",
|
|
11
13
|
"MessageRole",
|
|
12
14
|
"TextMessage",
|
|
@@ -18,4 +20,6 @@ __all__ = [
|
|
|
18
20
|
"ToolKind",
|
|
19
21
|
"WebSearchInput",
|
|
20
22
|
"MCPStreamableServerInput",
|
|
23
|
+
"OutputSchema",
|
|
24
|
+
"PropertyDef",
|
|
21
25
|
]
|
livellm/models/agent/agent.py
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
1
|
# models for full run: AgentRequest, AgentResponse
|
|
2
2
|
|
|
3
|
-
from pydantic import BaseModel, Field
|
|
4
|
-
from typing import Optional, List, Union
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from typing import Optional, List, Union, Any, Dict
|
|
5
|
+
from enum import Enum
|
|
5
6
|
from .chat import TextMessage, BinaryMessage, ToolCallMessage, ToolReturnMessage
|
|
6
7
|
from .tools import WebSearchInput, MCPStreamableServerInput
|
|
8
|
+
from .output_schema import OutputSchema, PropertyDef
|
|
7
9
|
from ..common import BaseRequest
|
|
8
10
|
|
|
9
11
|
|
|
12
|
+
class ContextOverflowStrategy(str, Enum):
|
|
13
|
+
"""Strategy for handling context overflow when text exceeds context_limit."""
|
|
14
|
+
TRUNCATE = "truncate" # Take beginning, middle, and end portions
|
|
15
|
+
RECYCLE = "recycle" # Iteratively process chunks, merging results
|
|
16
|
+
|
|
17
|
+
|
|
10
18
|
class AgentRequest(BaseRequest):
|
|
11
19
|
model: str = Field(..., description="The model to use")
|
|
12
20
|
messages: List[Union[TextMessage, BinaryMessage, ToolCallMessage, ToolReturnMessage]] = Field(..., description="The messages to use")
|
|
13
21
|
tools: List[Union[WebSearchInput, MCPStreamableServerInput]] = Field(default_factory=list, description="The tools to use")
|
|
14
22
|
gen_config: Optional[dict] = Field(default=None, description="The configuration for the generation")
|
|
15
23
|
include_history: bool = Field(default=False, description="Whether to include full conversation history in the response")
|
|
24
|
+
output_schema: Optional[Union[OutputSchema, Dict[str, Any]]] = Field(default=None, description="JSON schema for structured output. Can be an OutputSchema, a dict representing a JSON schema, or will be converted from a Pydantic BaseModel.")
|
|
25
|
+
context_limit: int = Field(default=0, description="Maximum context size in tokens. If <= 0, context overflow handling is disabled.")
|
|
26
|
+
context_overflow_strategy: ContextOverflowStrategy = Field(default=ContextOverflowStrategy.TRUNCATE, description="Strategy for handling context overflow: 'truncate' or 'recycle'")
|
|
16
27
|
|
|
17
28
|
class AgentResponseUsage(BaseModel):
|
|
18
29
|
input_tokens: int = Field(..., description="The number of input tokens used")
|
|
19
30
|
output_tokens: int = Field(..., description="The number of output tokens used")
|
|
20
31
|
|
|
21
32
|
class AgentResponse(BaseModel):
|
|
22
|
-
output: str = Field(..., description="The output of the response")
|
|
33
|
+
output: str = Field(..., description="The output of the response (JSON string when using output_schema)")
|
|
23
34
|
usage: AgentResponseUsage = Field(..., description="The usage of the response")
|
|
24
|
-
history: Optional[List[Union[TextMessage, BinaryMessage, ToolCallMessage, ToolReturnMessage]]] = Field(default=None, description="Full conversation history including tool calls and returns (only included when include_history=true)")
|
|
35
|
+
history: Optional[List[Union[TextMessage, BinaryMessage, ToolCallMessage, ToolReturnMessage]]] = Field(default=None, description="Full conversation history including tool calls and returns (only included when include_history=true)")
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Output schema models for structured output support."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
from typing import Optional, List, Dict, Any, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PropertyDef(BaseModel):
|
|
8
|
+
"""Definition of a property in the output schema."""
|
|
9
|
+
model_config = ConfigDict(extra="allow")
|
|
10
|
+
|
|
11
|
+
type: Union[str, List[str]] = Field(..., description="Property type: string, integer, number, boolean, array, object, null")
|
|
12
|
+
description: Optional[str] = Field(default=None, description="Description of the property")
|
|
13
|
+
enum: Optional[List[Any]] = Field(default=None, description="Allowed values for the property")
|
|
14
|
+
default: Optional[Any] = Field(default=None, description="Default value")
|
|
15
|
+
# String constraints
|
|
16
|
+
minLength: Optional[int] = Field(default=None, description="Minimum string length")
|
|
17
|
+
maxLength: Optional[int] = Field(default=None, description="Maximum string length")
|
|
18
|
+
pattern: Optional[str] = Field(default=None, description="Regex pattern for string validation")
|
|
19
|
+
# Number constraints
|
|
20
|
+
minimum: Optional[float] = Field(default=None, description="Minimum number value")
|
|
21
|
+
maximum: Optional[float] = Field(default=None, description="Maximum number value")
|
|
22
|
+
exclusiveMinimum: Optional[float] = Field(default=None, description="Exclusive minimum number value")
|
|
23
|
+
exclusiveMaximum: Optional[float] = Field(default=None, description="Exclusive maximum number value")
|
|
24
|
+
# Array constraints
|
|
25
|
+
items: Optional[Union["PropertyDef", Dict[str, Any]]] = Field(default=None, description="Schema for array items")
|
|
26
|
+
minItems: Optional[int] = Field(default=None, description="Minimum array length")
|
|
27
|
+
maxItems: Optional[int] = Field(default=None, description="Maximum array length")
|
|
28
|
+
uniqueItems: Optional[bool] = Field(default=None, description="Whether array items must be unique")
|
|
29
|
+
# Object constraints
|
|
30
|
+
properties: Optional[Dict[str, Union["PropertyDef", Dict[str, Any]]]] = Field(default=None, description="Nested object properties")
|
|
31
|
+
required: Optional[List[str]] = Field(default=None, description="Required properties for nested objects")
|
|
32
|
+
additionalProperties: Optional[Union[bool, "PropertyDef", Dict[str, Any]]] = Field(default=None, description="Schema for additional properties")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OutputSchema(BaseModel):
|
|
36
|
+
"""
|
|
37
|
+
Schema definition for structured output.
|
|
38
|
+
|
|
39
|
+
This model represents a JSON Schema that the AI model must follow when generating responses.
|
|
40
|
+
When provided, the agent will return a JSON string matching the specified schema.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
schema = OutputSchema(
|
|
44
|
+
title="Person",
|
|
45
|
+
description="A person's information",
|
|
46
|
+
properties={
|
|
47
|
+
"name": PropertyDef(type="string", description="The person's name"),
|
|
48
|
+
"age": PropertyDef(type="integer", minimum=0, maximum=150),
|
|
49
|
+
},
|
|
50
|
+
required=["name", "age"]
|
|
51
|
+
)
|
|
52
|
+
"""
|
|
53
|
+
model_config = ConfigDict(extra="allow")
|
|
54
|
+
|
|
55
|
+
title: str = Field(..., description="Name of the schema, used as the output tool name")
|
|
56
|
+
description: Optional[str] = Field(default=None, description="Description to help the model understand what to output")
|
|
57
|
+
properties: Dict[str, Union[PropertyDef, Dict[str, Any]]] = Field(..., description="Dictionary of property definitions")
|
|
58
|
+
required: Optional[List[str]] = Field(default=None, description="List of required property names")
|
|
59
|
+
additionalProperties: Optional[Union[bool, PropertyDef, Dict[str, Any]]] = Field(default=None, description="Whether extra properties are allowed")
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_pydantic(cls, model: type[BaseModel]) -> "OutputSchema":
|
|
63
|
+
"""
|
|
64
|
+
Create an OutputSchema from a Pydantic BaseModel class.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
model: A Pydantic BaseModel class to convert to OutputSchema.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
An OutputSchema instance representing the model's schema.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
class Person(BaseModel):
|
|
74
|
+
name: str
|
|
75
|
+
age: int
|
|
76
|
+
|
|
77
|
+
schema = OutputSchema.from_pydantic(Person)
|
|
78
|
+
"""
|
|
79
|
+
json_schema = model.model_json_schema()
|
|
80
|
+
|
|
81
|
+
# Extract the main properties
|
|
82
|
+
title = json_schema.get("title", model.__name__)
|
|
83
|
+
description = json_schema.get("description")
|
|
84
|
+
properties = json_schema.get("properties", {})
|
|
85
|
+
required = json_schema.get("required")
|
|
86
|
+
|
|
87
|
+
# Handle $defs for nested models (Pydantic generates these for complex models)
|
|
88
|
+
defs = json_schema.get("$defs", {})
|
|
89
|
+
if defs:
|
|
90
|
+
# Inline the definitions into properties
|
|
91
|
+
properties = cls._resolve_refs(properties, defs)
|
|
92
|
+
|
|
93
|
+
return cls(
|
|
94
|
+
title=title,
|
|
95
|
+
description=description,
|
|
96
|
+
properties=properties,
|
|
97
|
+
required=required,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def _resolve_refs(cls, obj: Any, defs: Dict[str, Any]) -> Any:
|
|
102
|
+
"""Recursively resolve $ref references in the schema."""
|
|
103
|
+
if isinstance(obj, dict):
|
|
104
|
+
if "$ref" in obj:
|
|
105
|
+
ref_path = obj["$ref"]
|
|
106
|
+
# Extract the definition name from "#/$defs/ModelName"
|
|
107
|
+
if ref_path.startswith("#/$defs/"):
|
|
108
|
+
def_name = ref_path[len("#/$defs/"):]
|
|
109
|
+
if def_name in defs:
|
|
110
|
+
# Return the resolved definition (also resolve any nested refs)
|
|
111
|
+
return cls._resolve_refs(defs[def_name], defs)
|
|
112
|
+
return obj
|
|
113
|
+
else:
|
|
114
|
+
return {k: cls._resolve_refs(v, defs) for k, v in obj.items()}
|
|
115
|
+
elif isinstance(obj, list):
|
|
116
|
+
return [cls._resolve_refs(item, defs) for item in obj]
|
|
117
|
+
else:
|
|
118
|
+
return obj
|
|
119
|
+
|
|
120
|
+
|
livellm/models/transcription.py
CHANGED
|
@@ -2,6 +2,7 @@ from pydantic import BaseModel, Field, field_validator
|
|
|
2
2
|
from livellm.models.audio.speak import SpeakMimeType
|
|
3
3
|
from typing import Optional
|
|
4
4
|
import base64
|
|
5
|
+
from datetime import datetime
|
|
5
6
|
|
|
6
7
|
class TranscriptionInitWsRequest(BaseModel):
|
|
7
8
|
provider_uid: str = Field(..., description="The provider uid")
|
|
@@ -33,3 +34,4 @@ class TranscriptionAudioChunkWsRequest(BaseModel):
|
|
|
33
34
|
|
|
34
35
|
class TranscriptionWsResponse(BaseModel):
|
|
35
36
|
transcription: str = Field(..., description="The transcription")
|
|
37
|
+
received_at: datetime = Field(default_factory=datetime.now, description="The datetime when the transcription was received")
|
livellm/transcripton.py
CHANGED
|
@@ -47,7 +47,7 @@ class TranscriptionWsClient:
|
|
|
47
47
|
self,
|
|
48
48
|
request: TranscriptionInitWsRequest,
|
|
49
49
|
source: AsyncIterator[TranscriptionAudioChunkWsRequest]
|
|
50
|
-
) -> AsyncIterator[TranscriptionWsResponse]:
|
|
50
|
+
) -> AsyncIterator[list[TranscriptionWsResponse]]:
|
|
51
51
|
"""
|
|
52
52
|
Start a transcription session.
|
|
53
53
|
|
|
@@ -56,7 +56,10 @@ class TranscriptionWsClient:
|
|
|
56
56
|
source: An async iterator that yields audio chunks to transcribe.
|
|
57
57
|
|
|
58
58
|
Returns:
|
|
59
|
-
An async iterator of transcription
|
|
59
|
+
An async iterator that yields lists of transcription responses.
|
|
60
|
+
Each list contains all responses that accumulated since the last yield,
|
|
61
|
+
ordered from oldest to newest (last element is the most recent).
|
|
62
|
+
This prevents slow processing from stalling the entire loop.
|
|
60
63
|
|
|
61
64
|
Example:
|
|
62
65
|
```python
|
|
@@ -66,8 +69,14 @@ class TranscriptionWsClient:
|
|
|
66
69
|
yield TranscriptionAudioChunkWsRequest(audio=chunk)
|
|
67
70
|
|
|
68
71
|
async with TranscriptionWsClient(url) as client:
|
|
69
|
-
async for
|
|
70
|
-
|
|
72
|
+
async for responses in client.start_session(init_request, audio_source()):
|
|
73
|
+
# responses is a list, newest transcription is last
|
|
74
|
+
latest = responses[-1]
|
|
75
|
+
print(f"Latest: {latest.transcription}")
|
|
76
|
+
|
|
77
|
+
# Process all transcriptions if needed
|
|
78
|
+
for resp in responses:
|
|
79
|
+
print(resp.transcription)
|
|
71
80
|
```
|
|
72
81
|
"""
|
|
73
82
|
# Send initialization request as JSON
|
|
@@ -79,6 +88,10 @@ class TranscriptionWsClient:
|
|
|
79
88
|
if not init_response.success:
|
|
80
89
|
raise Exception(f"Failed to start transcription session: {init_response.error}")
|
|
81
90
|
|
|
91
|
+
# Queue to collect incoming transcription responses
|
|
92
|
+
response_queue: asyncio.Queue[TranscriptionWsResponse | None] = asyncio.Queue()
|
|
93
|
+
receiver_done = False
|
|
94
|
+
|
|
82
95
|
# Start sending audio chunks in background
|
|
83
96
|
async def send_chunks():
|
|
84
97
|
try:
|
|
@@ -93,23 +106,52 @@ class TranscriptionWsClient:
|
|
|
93
106
|
await self.websocket.close()
|
|
94
107
|
raise e
|
|
95
108
|
|
|
109
|
+
# Receive transcription responses in background
|
|
110
|
+
async def receive_responses():
|
|
111
|
+
nonlocal receiver_done
|
|
112
|
+
try:
|
|
113
|
+
while True:
|
|
114
|
+
try:
|
|
115
|
+
response_data = await self.websocket.recv()
|
|
116
|
+
transcription_response = TranscriptionWsResponse(**json.loads(response_data))
|
|
117
|
+
await response_queue.put(transcription_response)
|
|
118
|
+
except websockets.ConnectionClosed:
|
|
119
|
+
break
|
|
120
|
+
finally:
|
|
121
|
+
receiver_done = True
|
|
122
|
+
await response_queue.put(None) # Signal end of stream
|
|
123
|
+
|
|
96
124
|
send_task = asyncio.create_task(send_chunks())
|
|
125
|
+
receive_task = asyncio.create_task(receive_responses())
|
|
97
126
|
|
|
98
|
-
# Receive transcription responses
|
|
99
127
|
try:
|
|
100
|
-
while True:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
except websockets.ConnectionClosed:
|
|
106
|
-
# Connection closed, stop receiving
|
|
128
|
+
while True:
|
|
129
|
+
# Wait for at least one response
|
|
130
|
+
first_response = await response_queue.get()
|
|
131
|
+
if first_response is None:
|
|
132
|
+
# End of stream
|
|
107
133
|
break
|
|
134
|
+
|
|
135
|
+
# Collect all additional responses that have accumulated (non-blocking)
|
|
136
|
+
responses = [first_response]
|
|
137
|
+
while True:
|
|
138
|
+
try:
|
|
139
|
+
additional = response_queue.get_nowait()
|
|
140
|
+
if additional is None:
|
|
141
|
+
# End of stream, yield what we have and exit
|
|
142
|
+
yield responses
|
|
143
|
+
return
|
|
144
|
+
responses.append(additional)
|
|
145
|
+
except asyncio.QueueEmpty:
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
yield responses
|
|
108
149
|
finally:
|
|
109
|
-
# Cancel
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
150
|
+
# Cancel tasks if still running
|
|
151
|
+
for task in [send_task, receive_task]:
|
|
152
|
+
if not task.done():
|
|
153
|
+
task.cancel()
|
|
154
|
+
try:
|
|
155
|
+
await task
|
|
156
|
+
except asyncio.CancelledError:
|
|
157
|
+
pass
|