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.
@@ -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",
@@ -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
  ]
@@ -1,24 +1,35 @@
1
1
  # models for full run: AgentRequest, AgentResponse
2
2
 
3
- from pydantic import BaseModel, Field, field_validator
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
+
@@ -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 session responses.
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 response in client.start_session(init_request, audio_source()):
70
- print(response.transcription)
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
- try:
102
- response_data = await self.websocket.recv()
103
- transcription_response = TranscriptionWsResponse(**json.loads(response_data))
104
- yield transcription_response
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 the send task if still running
110
- if not send_task.done():
111
- send_task.cancel()
112
- try:
113
- await send_task
114
- except asyncio.CancelledError:
115
- pass
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