lite-agent 0.9.0__py3-none-any.whl → 0.11.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.

Potentially problematic release.


This version of lite-agent might be problematic. Click here for more details.

@@ -0,0 +1,232 @@
1
+ """Message format converters for API compatibility."""
2
+
3
+ from typing import Any
4
+
5
+ from lite_agent.loggers import logger
6
+ from lite_agent.types import (
7
+ AssistantTextContent,
8
+ AssistantToolCall,
9
+ AssistantToolCallResult,
10
+ NewAssistantMessage,
11
+ NewSystemMessage,
12
+ NewUserMessage,
13
+ RunnerMessages,
14
+ message_to_llm_dict,
15
+ )
16
+
17
+
18
+ class MessageFormatConverter:
19
+ """Converter for different message API formats."""
20
+
21
+ @staticmethod
22
+ def to_completion_format(messages: RunnerMessages) -> list[dict]:
23
+ """Convert messages to completion API format.
24
+
25
+ This method replaces the complex _convert_responses_to_completions_format
26
+ with a cleaner, more maintainable implementation.
27
+ """
28
+ logger.debug(f"Converting {len(messages)} messages to completion format")
29
+ converted_messages = []
30
+
31
+ for message in messages:
32
+ if isinstance(message, (NewUserMessage, NewSystemMessage)):
33
+ # Handle user and system messages directly
34
+ converted_msg = message_to_llm_dict(message)
35
+ if isinstance(message, NewUserMessage):
36
+ converted_msg = MessageFormatConverter._convert_user_content(converted_msg)
37
+ converted_messages.append(converted_msg)
38
+
39
+ elif isinstance(message, NewAssistantMessage):
40
+ # Handle assistant messages with tool calls
41
+ assistant_msg, tool_results = MessageFormatConverter._process_assistant_message(message)
42
+ converted_messages.append(assistant_msg)
43
+ converted_messages.extend(tool_results)
44
+
45
+ elif isinstance(message, dict):
46
+ converted_msg = MessageFormatConverter._handle_legacy_dict_message(message)
47
+ if converted_msg:
48
+ converted_messages.extend(converted_msg if isinstance(converted_msg, list) else [converted_msg])
49
+
50
+ logger.debug(f"Completed conversion: {len(messages)} -> {len(converted_messages)} messages")
51
+ return converted_messages
52
+
53
+ @staticmethod
54
+ def _process_assistant_message(message: NewAssistantMessage) -> tuple[dict[str, Any], list[dict[str, Any]]]:
55
+ """Process assistant message and extract tool calls/results."""
56
+ text_parts = []
57
+ tool_calls = []
58
+ tool_results = []
59
+
60
+ for content_item in message.content:
61
+ if content_item.type == "text":
62
+ text_parts.append(content_item.text)
63
+ elif content_item.type == "tool_call":
64
+ tool_calls.append({
65
+ "id": content_item.call_id,
66
+ "type": "function",
67
+ "function": {
68
+ "name": content_item.name,
69
+ "arguments": content_item.arguments if isinstance(content_item.arguments, str) else str(content_item.arguments),
70
+ },
71
+ "index": len(tool_calls),
72
+ })
73
+ elif content_item.type == "tool_call_result":
74
+ tool_results.append({
75
+ "role": "tool",
76
+ "tool_call_id": content_item.call_id,
77
+ "content": content_item.output,
78
+ })
79
+
80
+ # Create assistant message
81
+ assistant_msg = {
82
+ "role": "assistant",
83
+ "content": " ".join(text_parts) if text_parts else None,
84
+ }
85
+
86
+ if tool_calls:
87
+ assistant_msg["tool_calls"] = tool_calls
88
+
89
+ return assistant_msg, tool_results
90
+
91
+ @staticmethod
92
+ def _convert_user_content(message_dict: dict[str, Any]) -> dict[str, Any]:
93
+ """Convert user message content for completion API."""
94
+ content = message_dict.get("content")
95
+ if not isinstance(content, list):
96
+ return message_dict
97
+
98
+ converted_content = []
99
+ for item in content:
100
+ # Handle both Pydantic objects and dicts
101
+ if hasattr(item, "model_dump"):
102
+ item_dict = item.model_dump()
103
+ elif isinstance(item, dict):
104
+ item_dict = item
105
+ else:
106
+ converted_content.append(item)
107
+ continue
108
+
109
+ item_type = item_dict.get("type")
110
+ if item_type in ["input_text", "text"]:
111
+ converted_content.append({
112
+ "type": "text",
113
+ "text": item_dict["text"],
114
+ })
115
+ elif item_type in ["input_image", "image"]:
116
+ if item_dict.get("file_id"):
117
+ logger.warning("File ID input not supported for Completion API, skipping")
118
+ continue
119
+
120
+ if not item_dict.get("image_url"):
121
+ logger.warning("Image content missing image_url, skipping")
122
+ continue
123
+
124
+ image_data = {"url": item_dict["image_url"]}
125
+ detail = item_dict.get("detail", "auto")
126
+ if detail:
127
+ image_data["detail"] = detail
128
+
129
+ converted_content.append({
130
+ "type": "image_url",
131
+ "image_url": image_data,
132
+ })
133
+ else:
134
+ # Keep other formats as-is
135
+ converted_content.append(item_dict)
136
+
137
+ result = message_dict.copy()
138
+ result["content"] = converted_content
139
+ return result
140
+
141
+ @staticmethod
142
+ def _handle_legacy_dict_message(message: dict) -> dict | list[dict] | None:
143
+ """Handle legacy dict message formats with simplified logic."""
144
+ message_type = message.get("type")
145
+ role = message.get("role")
146
+
147
+ if message_type == "function_call_output":
148
+ return {
149
+ "role": "tool",
150
+ "tool_call_id": message.get("call_id", ""),
151
+ "content": message.get("output", ""),
152
+ }
153
+ if message_type == "function_call":
154
+ # Function calls should be handled as part of assistant messages
155
+ logger.debug("Standalone function_call message encountered, may be processed with assistant message")
156
+ return None
157
+ if role in ["user", "system", "assistant"]:
158
+ # Standard message, convert content if needed
159
+ converted_msg = message.copy()
160
+ if role == "user" and isinstance(message.get("content"), list):
161
+ converted_msg = MessageFormatConverter._convert_user_content(converted_msg)
162
+ return converted_msg
163
+ logger.warning(f"Unknown message format: {message}")
164
+ return None
165
+
166
+
167
+ class ResponsesFormatConverter:
168
+ """Converter for responses API format."""
169
+
170
+ @staticmethod
171
+ def to_responses_format(messages: RunnerMessages) -> list[dict[str, Any]]:
172
+ """Convert messages to responses API format."""
173
+ result = []
174
+
175
+ for message in messages:
176
+ if isinstance(message, NewAssistantMessage):
177
+ # Convert assistant message content directly
178
+ contents = []
179
+ for item in message.content:
180
+ if isinstance(item, AssistantTextContent):
181
+ contents.append({
182
+ "role": "assistant",
183
+ "content": item.text,
184
+ })
185
+ elif isinstance(item, AssistantToolCall):
186
+ contents.append({
187
+ "type": "function_call",
188
+ "call_id": item.call_id,
189
+ "name": item.name,
190
+ "arguments": item.arguments,
191
+ })
192
+ elif isinstance(item, AssistantToolCallResult):
193
+ contents.append({
194
+ "type": "function_call_output",
195
+ "call_id": item.call_id,
196
+ "output": item.output,
197
+ })
198
+ result.extend(contents)
199
+
200
+ elif isinstance(message, NewUserMessage):
201
+ contents = []
202
+ for item in message.content:
203
+ match item.type:
204
+ case "text":
205
+ contents.append({
206
+ "type": "input_text",
207
+ "text": item.text,
208
+ })
209
+ case "image":
210
+ contents.append({
211
+ "type": "input_image",
212
+ "image_url": item.image_url,
213
+ })
214
+ case "file":
215
+ contents.append({
216
+ "type": "input_file",
217
+ "file_id": item.file_id,
218
+ "file_name": item.file_name,
219
+ })
220
+
221
+ result.append({
222
+ "role": message.role,
223
+ "content": contents,
224
+ })
225
+
226
+ elif isinstance(message, NewSystemMessage):
227
+ result.append({
228
+ "role": "system",
229
+ "content": message.content,
230
+ })
231
+
232
+ return result
@@ -0,0 +1,152 @@
1
+ """Message state management utilities to prevent race conditions."""
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ from lite_agent.loggers import logger
7
+ from lite_agent.types import (
8
+ AssistantMessageMeta,
9
+ AssistantTextContent,
10
+ AssistantToolCall,
11
+ AssistantToolCallResult,
12
+ NewAssistantMessage,
13
+ )
14
+
15
+
16
+ class MessageStateManager:
17
+ """Thread-safe manager for assistant message state during streaming."""
18
+
19
+ def __init__(self):
20
+ self._current_message: NewAssistantMessage | None = None
21
+ self._lock = asyncio.Lock()
22
+ self._finalized = False
23
+
24
+ async def start_message(self, content: str = "", meta: AssistantMessageMeta | None = None) -> None:
25
+ """Start a new assistant message safely."""
26
+ async with self._lock:
27
+ if self._current_message is not None:
28
+ logger.warning("Starting new message while previous message not finalized")
29
+
30
+ if meta is None:
31
+ meta = AssistantMessageMeta()
32
+
33
+ self._current_message = NewAssistantMessage(
34
+ content=[AssistantTextContent(text=content)] if content else [],
35
+ meta=meta,
36
+ )
37
+ self._finalized = False
38
+ logger.debug("Started new assistant message")
39
+
40
+ async def ensure_message_exists(self) -> NewAssistantMessage:
41
+ """Ensure current message exists, create if necessary."""
42
+ async with self._lock:
43
+ if self._current_message is None:
44
+ await self._start_message_internal()
45
+ if self._current_message is None:
46
+ msg = "Failed to create current assistant message"
47
+ raise RuntimeError(msg)
48
+ return self._current_message
49
+
50
+ async def _start_message_internal(self) -> None:
51
+ """Internal method to start message without lock (already locked)."""
52
+ meta = AssistantMessageMeta()
53
+ self._current_message = NewAssistantMessage(
54
+ content=[],
55
+ meta=meta,
56
+ )
57
+ self._finalized = False
58
+
59
+ async def add_text_delta(self, delta: str) -> None:
60
+ """Add text delta to current message safely."""
61
+ if not delta:
62
+ return
63
+
64
+ async with self._lock:
65
+ if self._current_message is None:
66
+ logger.debug("Creating new message for text delta")
67
+ await self._start_message_internal()
68
+
69
+ if self._current_message is None:
70
+ msg = "Failed to ensure current message exists"
71
+ raise RuntimeError(msg)
72
+
73
+ # Find existing text content or create new one
74
+ for item in self._current_message.content:
75
+ if item.type == "text":
76
+ logger.debug(f"Appending text delta (length: {len(delta)})")
77
+ item.text += delta
78
+ return
79
+
80
+ # No text content found, add new one
81
+ logger.debug(f"Adding new text content with delta (length: {len(delta)})")
82
+ self._current_message.content.append(AssistantTextContent(text=delta))
83
+
84
+ async def add_tool_call(self, tool_call: AssistantToolCall) -> None:
85
+ """Add tool call to current message safely."""
86
+ async with self._lock:
87
+ if self._current_message is None:
88
+ await self._start_message_internal()
89
+
90
+ if self._current_message is None:
91
+ msg = "Failed to ensure current message exists"
92
+ raise RuntimeError(msg)
93
+
94
+ self._current_message.content.append(tool_call)
95
+ logger.debug(f"Added tool call: {tool_call.name}")
96
+
97
+ async def add_tool_result(self, result: AssistantToolCallResult) -> None:
98
+ """Add tool call result to current message safely."""
99
+ async with self._lock:
100
+ if self._current_message is None:
101
+ await self._start_message_internal()
102
+
103
+ if self._current_message is None:
104
+ msg = "Failed to ensure current message exists"
105
+ raise RuntimeError(msg)
106
+
107
+ self._current_message.content.append(result)
108
+ logger.debug(f"Added tool result for call: {result.call_id}")
109
+
110
+ async def update_meta(self, **kwargs: Any) -> None: # noqa: ANN401
111
+ """Update message metadata safely."""
112
+ async with self._lock:
113
+ if self._current_message is None:
114
+ return
115
+
116
+ for key, value in kwargs.items():
117
+ if hasattr(self._current_message.meta, key):
118
+ setattr(self._current_message.meta, key, value)
119
+
120
+ async def get_current_message(self) -> NewAssistantMessage | None:
121
+ """Get current message safely."""
122
+ async with self._lock:
123
+ return self._current_message
124
+
125
+ async def finalize_message(self) -> NewAssistantMessage | None:
126
+ """Finalize and return current message, reset state."""
127
+ async with self._lock:
128
+ if self._current_message is None or self._finalized:
129
+ return None
130
+
131
+ finalized_message = self._current_message
132
+ self._current_message = None
133
+ self._finalized = True
134
+ logger.debug("Finalized assistant message")
135
+ return finalized_message
136
+
137
+ async def reset(self) -> None:
138
+ """Reset state manager."""
139
+ async with self._lock:
140
+ self._current_message = None
141
+ self._finalized = False
142
+ logger.debug("Reset message state manager")
143
+
144
+ @property
145
+ def has_current_message(self) -> bool:
146
+ """Check if there's a current message (non-blocking check)."""
147
+ return self._current_message is not None
148
+
149
+ @property
150
+ def is_finalized(self) -> bool:
151
+ """Check if current message is finalized (non-blocking check)."""
152
+ return self._finalized
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lite-agent
3
- Version: 0.9.0
3
+ Version: 0.11.0
4
4
  Summary: A lightweight, extensible framework for building AI agent.
5
5
  Author-email: Jianqi Pan <jannchie@gmail.com>
6
6
  License: MIT
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
16
17
  Classifier: Topic :: Communications :: Chat
17
18
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -1,12 +1,12 @@
1
1
  lite_agent/__init__.py,sha256=Swuefee0etSiaDnn30K2hBNV9UI3hIValW3A-pRE7e0,338
2
- lite_agent/agent.py,sha256=Jls9fwGgMLpFunT8Tr3Cp8P9AyeuvrHklWyFD3mhvtM,32061
3
- lite_agent/chat_display.py,sha256=IG2oM3FtpcfcduG7b6yt2mLFpG-Z6zHxXUN_J1XNVp8,40563
4
- lite_agent/client.py,sha256=PTsic12TVYklUKJzb9gk8-FWl2yhNkSYRS_Cs5dosEU,8677
2
+ lite_agent/agent.py,sha256=UXmQY8vhi_2DIPcWkji0B_pCGlBvZLfx6YoNaUJO6Wc,18028
3
+ lite_agent/chat_display.py,sha256=6gPgutMj7hCUOH6QBcC2f4Bhc93Gdq4vBa_1y6QKt2g,40793
4
+ lite_agent/client.py,sha256=-9BXLhAp3bGJsdKJ02lLpPJeHQKyHKQwhebZ6WCYh_k,9988
5
5
  lite_agent/constants.py,sha256=_xIDdQwaJrWk8N_62o-KYEo3jj1waPJ0ZOd3hHybKNo,718
6
6
  lite_agent/loggers.py,sha256=XkNkdqwD_nQGfhQJ-bBWT7koci_mMkNw3aBpyMhOICw,57
7
7
  lite_agent/message_transfers.py,sha256=N9ViK7Gxqqa1sd3V_hkNuQ9fUipg7M95l-sVBBG2Id4,5357
8
8
  lite_agent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- lite_agent/runner.py,sha256=iv4xqRJKH9PXyg7goQIBd6IfNODUo5GfjBdlZdxv3kw,45150
9
+ lite_agent/runner.py,sha256=qcKS12JRnzwdBDzOlxn8TNJU9nf4Dz2ZzLvD_7yUt2s,43819
10
10
  lite_agent/processors/__init__.py,sha256=ybpAzpMBIE9v5I24wIBZRXeaOaPNTmoKH13aofgNI6Q,234
11
11
  lite_agent/processors/completion_event_processor.py,sha256=zoWvs8dfrIkCSITGtS-4Hpve3WFCA0UUsMvYifL2fw0,13010
12
12
  lite_agent/processors/response_event_processor.py,sha256=Jr3cj1ItJ8aq9UBhEEjDwWDnPNOZ2ZXjWJ3-g4ghkhM,8514
@@ -19,13 +19,16 @@ lite_agent/stream_handlers/litellm.py,sha256=3D0u7R2ADA8kDwpFImZlw20o-CsmFXVLvq4
19
19
  lite_agent/templates/handoffs_source_instructions.xml.j2,sha256=2XsXQlBzk38qbxGrfyt8y2b0KlZmsV_1xavLufcdkHc,428
20
20
  lite_agent/templates/handoffs_target_instructions.xml.j2,sha256=gSbWVYYcovPKbGpFc0kqGSJ5Y5UC3fOHyUmZfcrDgSE,356
21
21
  lite_agent/templates/wait_for_user_instructions.xml.j2,sha256=wXbcYD5Q1FaCGVBm3Hz_Cp7nnoK7KzloP0ao-jYMwPk,231
22
- lite_agent/types/__init__.py,sha256=Zk7Uf1ScOJ5IGfeiJ3PM2FzPD4wmiq-9jzQzLpPpecI,3147
22
+ lite_agent/types/__init__.py,sha256=S28EuUSqvZh18cD0SconkGQpGLPz7l61ZHDwGgTR9vI,3130
23
23
  lite_agent/types/events.py,sha256=mFMqV55WWJbPDyb_P61nd3qMLpEnwZgVY6NTKFkINkg,2389
24
- lite_agent/types/messages.py,sha256=QjWL2McEWckVy7AIogl2HQkUy-XXdNAAcB0oCNnojbg,9922
24
+ lite_agent/types/messages.py,sha256=aq36BOLktd6rwxknqVXOLK7pLlwYktaBFn0Sp71l6nw,9993
25
25
  lite_agent/types/tool_calls.py,sha256=Xnut8-2-Ld9vgA2GKJY6BbFlBaAv_n4W7vo7Jx21A-E,260
26
26
  lite_agent/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- lite_agent/utils/message_builder.py,sha256=L15e4-qSlCYUNNtJ1zB-EPviuoxH9bTEVNlkvaH0q3U,8142
27
+ lite_agent/utils/advanced_message_builder.py,sha256=3U1PQrbdYQuIkv3j83TggDu3TmFN57TPZU32GTyPe4E,10196
28
+ lite_agent/utils/message_builder.py,sha256=J-yycL9pXSO9MbgC5NEGqvoP1LC2Nxe9r2YRWXoaIAQ,8126
29
+ lite_agent/utils/message_converter.py,sha256=5HmNncTl71TD2M_6Ezz1Tnfavzna8DQYb4-D47Du7mA,9165
30
+ lite_agent/utils/message_state_manager.py,sha256=rFUyqyd_7NdJRtyqsAWGcfwrDIlD6gK2dBDSDx1eGBs,5766
28
31
  lite_agent/utils/metrics.py,sha256=RzOEhCWxbLmmIEkzaxOJ6tAdthI8dv2Foc98Lq8afOQ,1915
29
- lite_agent-0.9.0.dist-info/METADATA,sha256=e1eaVu37mmGYrHI_KofiLTOHCerY6fXY5BPZoIXbTO4,3486
30
- lite_agent-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
- lite_agent-0.9.0.dist-info/RECORD,,
32
+ lite_agent-0.11.0.dist-info/METADATA,sha256=WRwZ5xKujQt0pJdWvIBvhYQsnedrsW9WUqOseXQiVhU,3538
33
+ lite_agent-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
+ lite_agent-0.11.0.dist-info/RECORD,,