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.

@@ -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
- from .tool_calls import ToolCall
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
- function_call_id: str
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 types - keeping for compatibility
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
- id: str
86
- index: int
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
- class Message(BaseModel):
93
- role: str
94
- content: str
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 BaseModel instances, TypedDict, and plain dict
131
- FlexibleRunnerMessage = RunnerMessage | MessageDict | dict[str, Any]
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.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.7.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