lite-agent 0.3.0__py3-none-any.whl → 0.4.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.
Potentially problematic release.
This version of lite-agent might be problematic. Click here for more details.
- lite_agent/__init__.py +2 -2
- lite_agent/agent.py +181 -69
- lite_agent/chat_display.py +779 -0
- lite_agent/client.py +36 -1
- lite_agent/message_transfers.py +9 -1
- lite_agent/processors/__init__.py +3 -2
- lite_agent/processors/completion_event_processor.py +306 -0
- lite_agent/processors/response_event_processor.py +205 -0
- lite_agent/runner.py +434 -251
- lite_agent/stream_handlers/__init__.py +3 -2
- lite_agent/stream_handlers/litellm.py +48 -70
- lite_agent/types/__init__.py +77 -23
- lite_agent/types/events.py +119 -0
- lite_agent/types/messages.py +256 -48
- {lite_agent-0.3.0.dist-info → lite_agent-0.4.1.dist-info}/METADATA +2 -2
- lite_agent-0.4.1.dist-info/RECORD +23 -0
- lite_agent/processors/stream_chunk_processor.py +0 -106
- lite_agent/rich_helpers.py +0 -503
- lite_agent/types/chunks.py +0 -89
- lite_agent-0.3.0.dist-info/RECORD +0 -22
- {lite_agent-0.3.0.dist-info → lite_agent-0.4.1.dist-info}/WHEEL +0 -0
lite_agent/types/messages.py
CHANGED
|
@@ -1,9 +1,53 @@
|
|
|
1
1
|
from collections.abc import Sequence
|
|
2
|
+
from datetime import datetime, timezone
|
|
2
3
|
from typing import Any, Literal, NotRequired, TypedDict
|
|
3
4
|
|
|
4
|
-
from pydantic import BaseModel
|
|
5
|
+
from pydantic import BaseModel, Field, model_validator
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
|
|
8
|
+
# Base metadata type
|
|
9
|
+
class MessageMeta(BaseModel):
|
|
10
|
+
"""Base metadata for all message types"""
|
|
11
|
+
|
|
12
|
+
sent_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BasicMessageMeta(MessageMeta):
|
|
16
|
+
"""Basic metadata for user messages and function calls"""
|
|
17
|
+
|
|
18
|
+
execution_time_ms: int | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LLMResponseMeta(MessageMeta):
|
|
22
|
+
"""Metadata for LLM responses, includes performance metrics"""
|
|
23
|
+
|
|
24
|
+
latency_ms: int | None = None
|
|
25
|
+
output_time_ms: int | None = None
|
|
26
|
+
input_tokens: int | None = None
|
|
27
|
+
output_tokens: int | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# New unified metadata types
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MessageUsage(BaseModel):
|
|
34
|
+
"""Token usage statistics for messages"""
|
|
35
|
+
|
|
36
|
+
input_tokens: int | None = None
|
|
37
|
+
output_tokens: int | None = None
|
|
38
|
+
total_tokens: int | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AssistantMessageMeta(MessageMeta):
|
|
42
|
+
"""Enhanced metadata for assistant messages"""
|
|
43
|
+
|
|
44
|
+
model: str | None = None
|
|
45
|
+
usage: MessageUsage | None = None
|
|
46
|
+
total_time_ms: int | None = None
|
|
47
|
+
latency_ms: int | None = None
|
|
48
|
+
output_time_ms: int | None = None
|
|
49
|
+
input_tokens: int | None = None
|
|
50
|
+
output_tokens: int | None = None
|
|
7
51
|
|
|
8
52
|
|
|
9
53
|
class ResponseInputImageDict(TypedDict):
|
|
@@ -36,7 +80,7 @@ class SystemMessageDict(TypedDict):
|
|
|
36
80
|
|
|
37
81
|
class FunctionCallDict(TypedDict):
|
|
38
82
|
type: Literal["function_call"]
|
|
39
|
-
|
|
83
|
+
call_id: str
|
|
40
84
|
name: str
|
|
41
85
|
arguments: str
|
|
42
86
|
content: str
|
|
@@ -52,18 +96,107 @@ class FunctionCallOutputDict(TypedDict):
|
|
|
52
96
|
MessageDict = UserMessageDict | AssistantMessageDict | SystemMessageDict | FunctionCallDict | FunctionCallOutputDict
|
|
53
97
|
|
|
54
98
|
|
|
99
|
+
# New structured message content types
|
|
100
|
+
class UserTextContent(BaseModel):
|
|
101
|
+
type: Literal["text"] = "text"
|
|
102
|
+
text: str
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class UserImageContent(BaseModel):
|
|
106
|
+
type: Literal["image"] = "image"
|
|
107
|
+
image_url: str | None = None
|
|
108
|
+
file_id: str | None = None
|
|
109
|
+
detail: Literal["low", "high", "auto"] = "auto"
|
|
110
|
+
|
|
111
|
+
@model_validator(mode="after")
|
|
112
|
+
def validate_image_source(self) -> "UserImageContent":
|
|
113
|
+
if not self.file_id and not self.image_url:
|
|
114
|
+
msg = "UserImageContent must have either file_id or image_url"
|
|
115
|
+
raise ValueError(msg)
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class UserFileContent(BaseModel):
|
|
120
|
+
type: Literal["file"] = "file"
|
|
121
|
+
file_id: str
|
|
122
|
+
file_name: str | None = None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
UserMessageContent = UserTextContent | UserImageContent | UserFileContent
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class AssistantTextContent(BaseModel):
|
|
129
|
+
type: Literal["text"] = "text"
|
|
130
|
+
text: str
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class AssistantToolCall(BaseModel):
|
|
134
|
+
type: Literal["tool_call"] = "tool_call"
|
|
135
|
+
call_id: str
|
|
136
|
+
name: str
|
|
137
|
+
arguments: dict[str, Any] | str
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class AssistantToolCallResult(BaseModel):
|
|
141
|
+
type: Literal["tool_call_result"] = "tool_call_result"
|
|
142
|
+
call_id: str
|
|
143
|
+
output: str
|
|
144
|
+
execution_time_ms: int | None = None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
AssistantMessageContent = AssistantTextContent | AssistantToolCall | AssistantToolCallResult
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# New structured message types
|
|
151
|
+
class NewUserMessage(BaseModel):
|
|
152
|
+
"""User message with structured content support"""
|
|
153
|
+
|
|
154
|
+
role: Literal["user"] = "user"
|
|
155
|
+
content: list[UserMessageContent]
|
|
156
|
+
meta: MessageMeta = Field(default_factory=MessageMeta)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class NewSystemMessage(BaseModel):
|
|
160
|
+
"""System message"""
|
|
161
|
+
|
|
162
|
+
role: Literal["system"] = "system"
|
|
163
|
+
content: str
|
|
164
|
+
meta: MessageMeta = Field(default_factory=MessageMeta)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class NewAssistantMessage(BaseModel):
|
|
168
|
+
"""Assistant message with structured content and metadata"""
|
|
169
|
+
|
|
170
|
+
role: Literal["assistant"] = "assistant"
|
|
171
|
+
content: list[AssistantMessageContent]
|
|
172
|
+
meta: AssistantMessageMeta = Field(default_factory=AssistantMessageMeta)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# Union type for new structured messages
|
|
176
|
+
NewMessage = NewUserMessage | NewSystemMessage | NewAssistantMessage
|
|
177
|
+
NewMessages = Sequence[NewMessage]
|
|
178
|
+
|
|
179
|
+
|
|
55
180
|
# Response API format input types
|
|
56
181
|
class ResponseInputText(BaseModel):
|
|
182
|
+
type: Literal["input_text"] = "input_text"
|
|
57
183
|
text: str
|
|
58
|
-
type: Literal["input_text"]
|
|
59
184
|
|
|
60
185
|
|
|
61
186
|
class ResponseInputImage(BaseModel):
|
|
62
187
|
detail: Literal["low", "high", "auto"] = "auto"
|
|
63
|
-
type: Literal["input_image"]
|
|
188
|
+
type: Literal["input_image"] = "input_image"
|
|
64
189
|
file_id: str | None = None
|
|
65
190
|
image_url: str | None = None
|
|
66
191
|
|
|
192
|
+
@model_validator(mode="after")
|
|
193
|
+
def validate_image_source(self) -> "ResponseInputImage":
|
|
194
|
+
"""Ensure at least one of file_id or image_url is provided."""
|
|
195
|
+
if not self.file_id and not self.image_url:
|
|
196
|
+
msg = "ResponseInputImage must have either file_id or image_url"
|
|
197
|
+
raise ValueError(msg)
|
|
198
|
+
return self
|
|
199
|
+
|
|
67
200
|
|
|
68
201
|
# Compatibility types for old completion API format
|
|
69
202
|
class UserMessageContentItemText(BaseModel):
|
|
@@ -80,56 +213,131 @@ class UserMessageContentItemImageURL(BaseModel):
|
|
|
80
213
|
image_url: UserMessageContentItemImageURLImageURL
|
|
81
214
|
|
|
82
215
|
|
|
83
|
-
# Legacy
|
|
216
|
+
# Legacy compatibility wrapper classes
|
|
217
|
+
class AgentUserMessage(NewUserMessage):
|
|
218
|
+
def __init__(
|
|
219
|
+
self,
|
|
220
|
+
content: str | list[UserMessageContent] | None = None,
|
|
221
|
+
*,
|
|
222
|
+
role: Literal["user"] = "user",
|
|
223
|
+
meta: MessageMeta | None = None,
|
|
224
|
+
):
|
|
225
|
+
if isinstance(content, str):
|
|
226
|
+
content = [UserTextContent(text=content)]
|
|
227
|
+
elif content is None:
|
|
228
|
+
content = []
|
|
229
|
+
super().__init__(
|
|
230
|
+
role=role,
|
|
231
|
+
content=content,
|
|
232
|
+
meta=meta or MessageMeta(),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class AgentAssistantMessage(NewAssistantMessage):
|
|
237
|
+
def __init__(
|
|
238
|
+
self,
|
|
239
|
+
content: str | list[AssistantMessageContent] | None = None,
|
|
240
|
+
*,
|
|
241
|
+
role: Literal["assistant"] = "assistant",
|
|
242
|
+
meta: AssistantMessageMeta | None = None,
|
|
243
|
+
):
|
|
244
|
+
if isinstance(content, str):
|
|
245
|
+
content = [AssistantTextContent(text=content)]
|
|
246
|
+
elif content is None:
|
|
247
|
+
content = []
|
|
248
|
+
super().__init__(
|
|
249
|
+
role=role,
|
|
250
|
+
content=content,
|
|
251
|
+
meta=meta or AssistantMessageMeta(),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
AgentSystemMessage = NewSystemMessage
|
|
256
|
+
RunnerMessage = NewMessage
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# Streaming processor types
|
|
84
260
|
class AssistantMessage(BaseModel):
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
role: Literal["assistant"] = "assistant"
|
|
88
|
-
content: str = ""
|
|
89
|
-
tool_calls: list[ToolCall] | None = None
|
|
90
|
-
|
|
261
|
+
"""
|
|
262
|
+
Temporary assistant message used during streaming processing.
|
|
91
263
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class AgentUserMessage(BaseModel):
|
|
98
|
-
role: Literal["user"]
|
|
99
|
-
content: str | Sequence[ResponseInputText | ResponseInputImage | UserMessageContentItemText | UserMessageContentItemImageURL]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class AgentAssistantMessage(BaseModel):
|
|
103
|
-
role: Literal["assistant"]
|
|
104
|
-
content: str
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class AgentSystemMessage(BaseModel):
|
|
108
|
-
role: Literal["system"]
|
|
109
|
-
content: str
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class AgentFunctionToolCallMessage(BaseModel):
|
|
113
|
-
arguments: str
|
|
114
|
-
type: Literal["function_call"]
|
|
115
|
-
function_call_id: str
|
|
116
|
-
name: str
|
|
117
|
-
content: str
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
class AgentFunctionCallOutput(BaseModel):
|
|
121
|
-
call_id: str
|
|
122
|
-
output: str
|
|
123
|
-
type: Literal["function_call_output"]
|
|
264
|
+
This is a simplified message format used internally by completion event processors
|
|
265
|
+
to accumulate streaming content before converting to the final NewAssistantMessage format.
|
|
266
|
+
"""
|
|
124
267
|
|
|
268
|
+
role: Literal["assistant"] = "assistant"
|
|
269
|
+
id: str = ""
|
|
270
|
+
index: int | None = None
|
|
271
|
+
content: str = ""
|
|
272
|
+
tool_calls: list[Any] | None = None
|
|
125
273
|
|
|
126
|
-
RunnerMessage = AgentUserMessage | AgentAssistantMessage | AgentSystemMessage | AgentFunctionToolCallMessage | AgentFunctionCallOutput
|
|
127
|
-
AgentMessage = RunnerMessage | AgentSystemMessage
|
|
128
274
|
|
|
129
275
|
# Enhanced type definitions for better type hints
|
|
130
|
-
# Supports
|
|
131
|
-
FlexibleRunnerMessage =
|
|
276
|
+
# Supports new message format, legacy messages, and dict (for backward compatibility)
|
|
277
|
+
FlexibleRunnerMessage = NewMessage | AgentUserMessage | AgentAssistantMessage | dict[str, Any]
|
|
132
278
|
RunnerMessages = Sequence[FlexibleRunnerMessage]
|
|
133
279
|
|
|
280
|
+
|
|
134
281
|
# Type alias for user input - supports string, single message, or sequence of messages
|
|
135
282
|
UserInput = str | FlexibleRunnerMessage | RunnerMessages
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def user_message_to_llm_dict(message: NewUserMessage) -> dict[str, Any]:
|
|
286
|
+
"""Convert NewUserMessage to dict for LLM API"""
|
|
287
|
+
# Convert content to simplified format for LLM
|
|
288
|
+
content = message.content[0].text if len(message.content) == 1 and message.content[0].type == "text" else [item.model_dump() for item in message.content]
|
|
289
|
+
return {"role": message.role, "content": content}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def system_message_to_llm_dict(message: NewSystemMessage) -> dict[str, Any]:
|
|
293
|
+
"""Convert NewSystemMessage to dict for LLM API"""
|
|
294
|
+
return {"role": message.role, "content": message.content}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def assistant_message_to_llm_dict(message: NewAssistantMessage) -> dict[str, Any]:
|
|
298
|
+
"""Convert NewAssistantMessage to dict for LLM API"""
|
|
299
|
+
# Separate text content from tool calls
|
|
300
|
+
text_parts = []
|
|
301
|
+
tool_calls = []
|
|
302
|
+
|
|
303
|
+
for item in message.content:
|
|
304
|
+
if item.type == "text":
|
|
305
|
+
text_parts.append(item.text)
|
|
306
|
+
elif item.type == "tool_call":
|
|
307
|
+
tool_calls.append(
|
|
308
|
+
{
|
|
309
|
+
"id": item.call_id,
|
|
310
|
+
"type": "function",
|
|
311
|
+
"function": {
|
|
312
|
+
"name": item.name,
|
|
313
|
+
"arguments": item.arguments if isinstance(item.arguments, str) else str(item.arguments),
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
result = {
|
|
319
|
+
"role": message.role,
|
|
320
|
+
"content": " ".join(text_parts) if text_parts else None,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if tool_calls:
|
|
324
|
+
result["tool_calls"] = tool_calls
|
|
325
|
+
|
|
326
|
+
return result
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def message_to_llm_dict(message: NewMessage) -> dict[str, Any]:
|
|
330
|
+
"""Convert any NewMessage to dict for LLM API"""
|
|
331
|
+
if isinstance(message, NewUserMessage):
|
|
332
|
+
return user_message_to_llm_dict(message)
|
|
333
|
+
if isinstance(message, NewSystemMessage):
|
|
334
|
+
return system_message_to_llm_dict(message)
|
|
335
|
+
if isinstance(message, NewAssistantMessage):
|
|
336
|
+
return assistant_message_to_llm_dict(message)
|
|
337
|
+
# Fallback
|
|
338
|
+
return message.model_dump(exclude={"meta"})
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def messages_to_llm_format(messages: Sequence[NewMessage]) -> list[dict[str, Any]]:
|
|
342
|
+
"""Convert a sequence of NewMessage to LLM format, excluding meta data"""
|
|
343
|
+
return [message_to_llm_dict(message) for message in messages]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lite-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: A lightweight, extensible framework for building AI agent.
|
|
5
5
|
Author-email: Jianqi Pan <jannchie@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -18,7 +18,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Requires-Dist: aiofiles>=24.1.0
|
|
21
|
-
Requires-Dist: funcall>=0.
|
|
21
|
+
Requires-Dist: funcall>=0.10.0
|
|
22
22
|
Requires-Dist: prompt-toolkit>=3.0.51
|
|
23
23
|
Requires-Dist: rich>=14.0.0
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
lite_agent/__init__.py,sha256=Swuefee0etSiaDnn30K2hBNV9UI3hIValW3A-pRE7e0,338
|
|
2
|
+
lite_agent/agent.py,sha256=t4AYlw3aF2DCPXf2W3s7aow0ql1ON5O2Q8VVuyoN6UI,22936
|
|
3
|
+
lite_agent/chat_display.py,sha256=b0sUH3fkutc4e_KAKH7AtPu2msyLloNIAiWqCNavdds,30533
|
|
4
|
+
lite_agent/client.py,sha256=m2jfBPIsleMZ1QCczjyHND-PIF17kQh4RTuf5FaipGM,2571
|
|
5
|
+
lite_agent/loggers.py,sha256=XkNkdqwD_nQGfhQJ-bBWT7koci_mMkNw3aBpyMhOICw,57
|
|
6
|
+
lite_agent/message_transfers.py,sha256=9qucjc-uSIXvVfhcmVRC_0lp0Q8sWp99dV4ReCh6ZlI,4428
|
|
7
|
+
lite_agent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
lite_agent/runner.py,sha256=ACZuFJ2dNpdg4Tzeg-bl4Th1X14uhHJdELcBWe5E_Us,40155
|
|
9
|
+
lite_agent/processors/__init__.py,sha256=ybpAzpMBIE9v5I24wIBZRXeaOaPNTmoKH13aofgNI6Q,234
|
|
10
|
+
lite_agent/processors/completion_event_processor.py,sha256=8fQYRofgBd8t0V3oUakTOmZdv5Q9tCuzADGCGvVgy0k,13442
|
|
11
|
+
lite_agent/processors/response_event_processor.py,sha256=CElJMUzLs8mklVqJtoLiVu-NTq0Dz2NNd9YdAKpjgE0,8088
|
|
12
|
+
lite_agent/stream_handlers/__init__.py,sha256=a5s1GZr42uvndtcQqEhK2cnjGkK8ZFTAZCj3J61Bb5E,209
|
|
13
|
+
lite_agent/stream_handlers/litellm.py,sha256=3D0u7R2ADA8kDwpFImZlw20o-CsmFXVLvq4nvwwD0Rk,2922
|
|
14
|
+
lite_agent/templates/handoffs_source_instructions.xml.j2,sha256=2XsXQlBzk38qbxGrfyt8y2b0KlZmsV_1xavLufcdkHc,428
|
|
15
|
+
lite_agent/templates/handoffs_target_instructions.xml.j2,sha256=gSbWVYYcovPKbGpFc0kqGSJ5Y5UC3fOHyUmZfcrDgSE,356
|
|
16
|
+
lite_agent/templates/wait_for_user_instructions.xml.j2,sha256=wXbcYD5Q1FaCGVBm3Hz_Cp7nnoK7KzloP0ao-jYMwPk,231
|
|
17
|
+
lite_agent/types/__init__.py,sha256=QKuhjFWRcpAlsBK9JYgoCABpoQExwhuyGudJoiiqQfs,3093
|
|
18
|
+
lite_agent/types/events.py,sha256=mFMqV55WWJbPDyb_P61nd3qMLpEnwZgVY6NTKFkINkg,2389
|
|
19
|
+
lite_agent/types/messages.py,sha256=c7nTIWqXNo562het_vaWcZvsoy-adkARwAYn4JNqm0c,9897
|
|
20
|
+
lite_agent/types/tool_calls.py,sha256=Xnut8-2-Ld9vgA2GKJY6BbFlBaAv_n4W7vo7Jx21A-E,260
|
|
21
|
+
lite_agent-0.4.1.dist-info/METADATA,sha256=iQIr1OAdiVK5Ad6Uho65OpqS1u4YC9sOaoxKZ1FssOs,3456
|
|
22
|
+
lite_agent-0.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
23
|
+
lite_agent-0.4.1.dist-info/RECORD,,
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import litellm
|
|
2
|
-
from litellm.types.utils import ChatCompletionDeltaToolCall, ModelResponseStream, StreamingChoices
|
|
3
|
-
|
|
4
|
-
from lite_agent.loggers import logger
|
|
5
|
-
from lite_agent.types import AssistantMessage, ToolCall, ToolCallFunction
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class StreamChunkProcessor:
|
|
9
|
-
"""Processor for handling streaming responses"""
|
|
10
|
-
|
|
11
|
-
def __init__(self) -> None:
|
|
12
|
-
self._current_message: AssistantMessage | None = None
|
|
13
|
-
|
|
14
|
-
def initialize_message(self, chunk: ModelResponseStream, choice: StreamingChoices) -> None:
|
|
15
|
-
"""Initialize the message object"""
|
|
16
|
-
delta = choice.delta
|
|
17
|
-
if delta.role != "assistant":
|
|
18
|
-
logger.warning("Skipping chunk with role: %s", delta.role)
|
|
19
|
-
return
|
|
20
|
-
self._current_message = AssistantMessage(
|
|
21
|
-
id=chunk.id,
|
|
22
|
-
index=choice.index,
|
|
23
|
-
role=delta.role,
|
|
24
|
-
content="",
|
|
25
|
-
)
|
|
26
|
-
logger.debug('Initialized new message: "%s"', self._current_message.id)
|
|
27
|
-
|
|
28
|
-
def update_content(self, content: str) -> None:
|
|
29
|
-
"""Update message content"""
|
|
30
|
-
if self._current_message and content:
|
|
31
|
-
self._current_message.content += content
|
|
32
|
-
|
|
33
|
-
def _initialize_tool_calls(self, tool_calls: list[litellm.ChatCompletionMessageToolCall]) -> None:
|
|
34
|
-
"""Initialize tool calls"""
|
|
35
|
-
if not self._current_message:
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
self._current_message.tool_calls = []
|
|
39
|
-
for call in tool_calls:
|
|
40
|
-
logger.debug("Create new tool call: %s", call.id)
|
|
41
|
-
|
|
42
|
-
def _update_tool_calls(self, tool_calls: list[litellm.ChatCompletionMessageToolCall]) -> None:
|
|
43
|
-
"""Update existing tool calls"""
|
|
44
|
-
if not self._current_message:
|
|
45
|
-
return
|
|
46
|
-
if not hasattr(self._current_message, "tool_calls"):
|
|
47
|
-
self._current_message.tool_calls = []
|
|
48
|
-
if not self._current_message.tool_calls:
|
|
49
|
-
return
|
|
50
|
-
if not tool_calls:
|
|
51
|
-
return
|
|
52
|
-
for current_call, new_call in zip(self._current_message.tool_calls, tool_calls, strict=False):
|
|
53
|
-
if new_call.function.arguments and current_call.function.arguments:
|
|
54
|
-
current_call.function.arguments += new_call.function.arguments
|
|
55
|
-
if new_call.type and new_call.type == "function":
|
|
56
|
-
current_call.type = new_call.type
|
|
57
|
-
elif new_call.type:
|
|
58
|
-
logger.warning("Unexpected tool call type: %s", new_call.type)
|
|
59
|
-
|
|
60
|
-
def update_tool_calls(self, tool_calls: list[ChatCompletionDeltaToolCall]) -> None:
|
|
61
|
-
"""Handle tool call updates"""
|
|
62
|
-
if not tool_calls:
|
|
63
|
-
return
|
|
64
|
-
for call in tool_calls:
|
|
65
|
-
if call.id:
|
|
66
|
-
if call.type == "function":
|
|
67
|
-
new_tool_call = ToolCall(
|
|
68
|
-
id=call.id,
|
|
69
|
-
type=call.type,
|
|
70
|
-
function=ToolCallFunction(
|
|
71
|
-
name=call.function.name or "",
|
|
72
|
-
arguments=call.function.arguments,
|
|
73
|
-
),
|
|
74
|
-
index=call.index,
|
|
75
|
-
)
|
|
76
|
-
if self._current_message is not None:
|
|
77
|
-
if self._current_message.tool_calls is None:
|
|
78
|
-
self._current_message.tool_calls = []
|
|
79
|
-
self._current_message.tool_calls.append(new_tool_call)
|
|
80
|
-
else:
|
|
81
|
-
logger.warning("Unexpected tool call type: %s", call.type)
|
|
82
|
-
elif self._current_message is not None and self._current_message.tool_calls is not None and call.index is not None and 0 <= call.index < len(self._current_message.tool_calls):
|
|
83
|
-
existing_call = self._current_message.tool_calls[call.index]
|
|
84
|
-
if call.function.arguments:
|
|
85
|
-
if existing_call.function.arguments is None:
|
|
86
|
-
existing_call.function.arguments = ""
|
|
87
|
-
existing_call.function.arguments += call.function.arguments
|
|
88
|
-
else:
|
|
89
|
-
logger.warning("Cannot update tool call: current_message or tool_calls is None, or invalid index.")
|
|
90
|
-
|
|
91
|
-
def handle_usage_info(self, chunk: ModelResponseStream) -> litellm.Usage | None:
|
|
92
|
-
"""Handle usage info, return whether this chunk should be skipped"""
|
|
93
|
-
return getattr(chunk, "usage", None)
|
|
94
|
-
|
|
95
|
-
@property
|
|
96
|
-
def is_initialized(self) -> bool:
|
|
97
|
-
"""Check if the current message is initialized"""
|
|
98
|
-
return self._current_message is not None
|
|
99
|
-
|
|
100
|
-
@property
|
|
101
|
-
def current_message(self) -> AssistantMessage:
|
|
102
|
-
"""Get the current message being processed"""
|
|
103
|
-
if not self._current_message:
|
|
104
|
-
msg = "No current message initialized. Call initialize_message first."
|
|
105
|
-
raise ValueError(msg)
|
|
106
|
-
return self._current_message
|