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

lite_agent/__init__.py CHANGED
@@ -0,0 +1,7 @@
1
+ """Lite Agent - A lightweight AI agent framework."""
2
+
3
+ from .agent import Agent
4
+ from .message_transfers import consolidate_history_transfer
5
+ from .runner import Runner
6
+
7
+ __all__ = ["Agent", "Runner", "consolidate_history_transfer"]
lite_agent/agent.py CHANGED
@@ -1,36 +1,330 @@
1
- from collections.abc import AsyncGenerator, Callable
1
+ from collections.abc import AsyncGenerator, Callable, Sequence
2
+ from pathlib import Path
3
+ from typing import Any, Optional
2
4
 
3
5
  import litellm
4
6
  from funcall import Funcall
7
+ from litellm import CustomStreamWrapper
8
+ from pydantic import BaseModel
5
9
 
6
- from open_agents.chunk_handler import AgentChunk, chunk_handler
7
- from open_agents.types import RunnerMessages
10
+ from lite_agent.loggers import logger
11
+ from lite_agent.stream_handlers import litellm_stream_handler
12
+ from lite_agent.types import AgentChunk, AgentSystemMessage, RunnerMessages, ToolCall, ToolCallChunk, ToolCallResultChunk
13
+
14
+ HANDOFFS_SOURCE_INSTRUCTIONS = """<ExtraGuide>
15
+ You are a parent agent that can assign tasks to sub-agents.
16
+
17
+ You can transfer conversations to other agents for specific tasks.
18
+ If you need to assign tasks to multiple agents, you should break down the tasks and assign them one by one.
19
+ You need to wait for one sub-agent to finish before assigning the task to the next sub-agent.
20
+ </ExtraGuide>"""
21
+
22
+ HANDOFFS_TARGET_INSTRUCTIONS = """<ExtraGuide>
23
+ You are a sub-agent that is assigned to a specific task by your parent agent.
24
+
25
+ Everything you output is intended for your parent agent to read.
26
+ When you finish your task, you should call `transfer_to_parent` to transfer back to parent agent.
27
+ </ExtraGuide>"""
8
28
 
9
29
 
10
30
  class Agent:
11
- def __init__(self, *, model: str, name: str, instructions: str, tools: list[Callable] | None = None) -> None:
31
+ def __init__( # noqa: PLR0913
32
+ self,
33
+ *,
34
+ model: str,
35
+ name: str,
36
+ instructions: str,
37
+ tools: list[Callable] | None = None,
38
+ handoffs: list["Agent"] | None = None,
39
+ message_transfer: Callable[[RunnerMessages], RunnerMessages] | None = None,
40
+ ) -> None:
12
41
  self.name = name
13
42
  self.instructions = instructions
14
- self.fc = Funcall(tools)
15
43
  self.model = model
44
+ self.handoffs = handoffs if handoffs else []
45
+ self._parent: Agent | None = None
46
+ self.message_transfer = message_transfer
47
+ # Initialize Funcall with regular tools
48
+ self.fc = Funcall(tools)
16
49
 
17
- def prepare_messages(self, messages: RunnerMessages) -> list[dict]:
18
- return [
19
- {
20
- "role": "system",
21
- "content": f"You are {self.name}. {self.instructions}",
50
+ # Set parent for handoff agents
51
+ if handoffs:
52
+ for handoff_agent in handoffs:
53
+ handoff_agent.parent = self
54
+ self._add_transfer_tools(handoffs)
55
+
56
+ # Add transfer_to_parent tool if this agent has a parent (for cases where parent is set externally)
57
+ if self.parent is not None:
58
+ self.add_transfer_to_parent_tool()
59
+
60
+ @property
61
+ def parent(self) -> Optional["Agent"]:
62
+ return self._parent
63
+
64
+ @parent.setter
65
+ def parent(self, value: Optional["Agent"]) -> None:
66
+ self._parent = value
67
+ if value is not None:
68
+ self.add_transfer_to_parent_tool()
69
+
70
+ def _add_transfer_tools(self, handoffs: list["Agent"]) -> None:
71
+ """Add transfer function for handoff agents using dynamic tools.
72
+
73
+ Creates a single 'transfer_to_agent' function that accepts a 'name' parameter
74
+ to specify which agent to transfer the conversation to.
75
+
76
+ Args:
77
+ handoffs: List of Agent objects that can be transferred to
78
+ """
79
+ # Collect all agent names for validation
80
+ agent_names = [agent.name for agent in handoffs]
81
+
82
+ def transfer_handler(name: str) -> str:
83
+ """Handler for transfer_to_agent function."""
84
+ if name in agent_names:
85
+ return f"Transferring to agent: {name}"
86
+
87
+ available_agents = ", ".join(agent_names)
88
+ return f"Agent '{name}' not found. Available agents: {available_agents}"
89
+
90
+ # Add single dynamic tool for all transfers
91
+ self.fc.add_dynamic_tool(
92
+ name="transfer_to_agent",
93
+ description="Transfer conversation to another agent.",
94
+ parameters={
95
+ "name": {
96
+ "type": "string",
97
+ "description": "The name of the agent to transfer to",
98
+ "enum": agent_names,
99
+ },
22
100
  },
23
- *messages,
101
+ required=["name"],
102
+ handler=transfer_handler,
103
+ )
104
+
105
+ def add_transfer_to_parent_tool(self) -> None:
106
+ """Add transfer_to_parent function for agents that have a parent.
107
+
108
+ This tool allows the agent to transfer back to its parent when:
109
+ - The current task is completed
110
+ - The agent cannot solve the current problem
111
+ - Escalation to a higher level is needed
112
+ """
113
+
114
+ def transfer_to_parent_handler() -> str:
115
+ """Handler for transfer_to_parent function."""
116
+ if self.parent:
117
+ return f"Transferring back to parent agent: {self.parent.name}"
118
+ return "No parent agent found"
119
+
120
+ # Add dynamic tool for parent transfer
121
+ self.fc.add_dynamic_tool(
122
+ name="transfer_to_parent",
123
+ description="Transfer conversation back to parent agent when current task is completed or cannot be solved by current agent",
124
+ parameters={},
125
+ required=[],
126
+ handler=transfer_to_parent_handler,
127
+ )
128
+
129
+ def add_handoff(self, agent: "Agent") -> None:
130
+ """Add a handoff agent after initialization.
131
+
132
+ This method allows adding handoff agents dynamically after the agent
133
+ has been constructed. It properly sets up parent-child relationships
134
+ and updates the transfer tools.
135
+
136
+ Args:
137
+ agent: The agent to add as a handoff target
138
+ """
139
+ # Add to handoffs list if not already present
140
+ if agent not in self.handoffs:
141
+ self.handoffs.append(agent)
142
+
143
+ # Set parent relationship
144
+ agent.parent = self
145
+
146
+ # Add transfer_to_parent tool to the handoff agent
147
+ agent.add_transfer_to_parent_tool()
148
+
149
+ # Remove existing transfer tool if it exists and recreate with all agents
150
+ try:
151
+ # Try to remove the existing transfer tool
152
+ if hasattr(self.fc, "remove_dynamic_tool"):
153
+ self.fc.remove_dynamic_tool("transfer_to_agent")
154
+ except Exception as e:
155
+ # If removal fails, log and continue anyway
156
+ logger.debug(f"Failed to remove existing transfer tool: {e}")
157
+
158
+ # Regenerate transfer tools to include the new agent
159
+ self._add_transfer_tools(self.handoffs)
160
+
161
+ def prepare_completion_messages(self, messages: RunnerMessages) -> list[dict[str, str]]:
162
+ # Convert from responses format to completions format
163
+ converted_messages = self._convert_responses_to_completions_format(messages)
164
+
165
+ # Prepare instructions with handoff-specific additions
166
+ instructions = self.instructions
167
+
168
+ # Add source instructions if this agent can handoff to others
169
+ if self.handoffs:
170
+ instructions = HANDOFFS_SOURCE_INSTRUCTIONS + "\n\n" + instructions
171
+
172
+ # Add target instructions if this agent can be handed off to (has a parent)
173
+ if self.parent:
174
+ instructions = HANDOFFS_TARGET_INSTRUCTIONS + "\n\n" + instructions
175
+
176
+ return [
177
+ AgentSystemMessage(
178
+ role="system",
179
+ content=f"You are {self.name}. {instructions}",
180
+ ).model_dump(),
181
+ *converted_messages,
24
182
  ]
25
183
 
26
- async def stream_async(self, messages: RunnerMessages) -> AsyncGenerator[AgentChunk, None]:
27
- self.message_histories = self.prepare_messages(messages)
28
- tools = self.fc.get_tools(target="litellm")
184
+ async def completion(self, messages: RunnerMessages, record_to_file: Path | None = None) -> AsyncGenerator[AgentChunk, None]:
185
+ # Apply message transfer callback if provided
186
+ processed_messages = messages
187
+ if self.message_transfer:
188
+ logger.debug(f"Applying message transfer callback for agent {self.name}")
189
+ processed_messages = self.message_transfer(messages)
190
+
191
+ self.message_histories = self.prepare_completion_messages(processed_messages)
192
+ tools = self.fc.get_tools(target="completion")
29
193
  resp = await litellm.acompletion(
30
194
  model=self.model,
31
195
  messages=self.message_histories,
32
196
  tools=tools,
33
- tool_choice="auto",
197
+ tool_choice="auto", # TODO: make this configurable
34
198
  stream=True,
35
199
  )
36
- return chunk_handler(resp, self.fc)
200
+
201
+ # Ensure resp is a CustomStreamWrapper
202
+ if isinstance(resp, CustomStreamWrapper):
203
+ return litellm_stream_handler(resp, record_to=record_to_file)
204
+ msg = "Response is not a CustomStreamWrapper, cannot stream chunks."
205
+ raise TypeError(msg)
206
+
207
+ async def list_require_confirm_tools(self, tool_calls: Sequence[ToolCall] | None) -> Sequence[ToolCall]:
208
+ if not tool_calls:
209
+ return []
210
+ results = []
211
+ for tool_call in tool_calls:
212
+ tool_func = self.fc.function_registry.get(tool_call.function.name)
213
+ if not tool_func:
214
+ logger.warning("Tool function %s not found in registry", tool_call.function.name)
215
+ continue
216
+ tool_meta = self.fc.get_tool_meta(tool_call.function.name)
217
+ if tool_meta["require_confirm"]:
218
+ logger.debug('Tool call "%s" requires confirmation', tool_call.id)
219
+ results.append(tool_call)
220
+ return results
221
+
222
+ async def handle_tool_calls(self, tool_calls: Sequence[ToolCall] | None, context: Any | None = None) -> AsyncGenerator[ToolCallChunk | ToolCallResultChunk, None]: # noqa: ANN401
223
+ if not tool_calls:
224
+ return
225
+ if tool_calls:
226
+ for tool_call in tool_calls:
227
+ tool_func = self.fc.function_registry.get(tool_call.function.name)
228
+ if not tool_func:
229
+ logger.warning("Tool function %s not found in registry", tool_call.function.name)
230
+ continue
231
+
232
+ for tool_call in tool_calls:
233
+ try:
234
+ yield ToolCallChunk(
235
+ type="tool_call",
236
+ name=tool_call.function.name,
237
+ arguments=tool_call.function.arguments or "",
238
+ )
239
+ content = await self.fc.call_function_async(tool_call.function.name, tool_call.function.arguments or "", context)
240
+ yield ToolCallResultChunk(
241
+ type="tool_call_result",
242
+ tool_call_id=tool_call.id,
243
+ name=tool_call.function.name,
244
+ content=str(content),
245
+ )
246
+ except Exception as e: # noqa: PERF203
247
+ logger.exception("Tool call %s failed", tool_call.id)
248
+ yield ToolCallResultChunk(
249
+ type="tool_call_result",
250
+ tool_call_id=tool_call.id,
251
+ name=tool_call.function.name,
252
+ content=str(e),
253
+ )
254
+
255
+ def _convert_responses_to_completions_format(self, messages: RunnerMessages) -> list[dict]:
256
+ """Convert messages from responses API format to completions API format."""
257
+ converted_messages = []
258
+ i = 0
259
+
260
+ while i < len(messages):
261
+ message = messages[i]
262
+ message_dict = message.model_dump() if isinstance(message, BaseModel) else message
263
+
264
+ message_type = message_dict.get("type")
265
+ role = message_dict.get("role")
266
+
267
+ if role == "assistant":
268
+ # Look ahead for function_call messages
269
+ tool_calls = []
270
+ j = i + 1
271
+
272
+ while j < len(messages):
273
+ next_message = messages[j]
274
+ next_dict = next_message.model_dump() if isinstance(next_message, BaseModel) else next_message
275
+
276
+ if next_dict.get("type") == "function_call":
277
+ tool_call = {
278
+ "id": next_dict["function_call_id"],
279
+ "type": "function",
280
+ "function": {
281
+ "name": next_dict["name"],
282
+ "arguments": next_dict["arguments"],
283
+ },
284
+ "index": len(tool_calls),
285
+ }
286
+ tool_calls.append(tool_call)
287
+ j += 1
288
+ else:
289
+ break
290
+
291
+ # Create assistant message with tool_calls if any
292
+ assistant_msg = message_dict.copy()
293
+ if tool_calls:
294
+ assistant_msg["tool_calls"] = tool_calls
295
+
296
+ converted_messages.append(assistant_msg)
297
+ i = j # Skip the function_call messages we've processed
298
+
299
+ elif message_type == "function_call_output":
300
+ # Convert to tool message
301
+ converted_messages.append(
302
+ {
303
+ "role": "tool",
304
+ "tool_call_id": message_dict["call_id"],
305
+ "content": message_dict["output"],
306
+ },
307
+ )
308
+ i += 1
309
+
310
+ elif message_type == "function_call":
311
+ # This should have been processed with the assistant message
312
+ # Skip it if we encounter it standalone
313
+ i += 1
314
+
315
+ else:
316
+ # Regular message (user, system)
317
+ converted_messages.append(message_dict)
318
+ i += 1
319
+
320
+ return converted_messages
321
+
322
+ def set_message_transfer(self, message_transfer: Callable[[RunnerMessages], RunnerMessages] | None) -> None:
323
+ """Set or update the message transfer callback function.
324
+
325
+ Args:
326
+ message_transfer: A callback function that takes RunnerMessages as input
327
+ and returns RunnerMessages as output. This function will be
328
+ called before making API calls to allow preprocessing of messages.
329
+ """
330
+ self.message_transfer = message_transfer
lite_agent/loggers.py CHANGED
@@ -1,3 +1,3 @@
1
1
  import logging
2
2
 
3
- logger = logging.getLogger("easy_agent")
3
+ logger = logging.getLogger("lite_agent")
@@ -0,0 +1,111 @@
1
+ """
2
+ Predefined message transfer functions for lite-agent.
3
+
4
+ This module provides common message transfer functions that can be used
5
+ with agents to preprocess messages before sending them to the API.
6
+ """
7
+
8
+ from lite_agent.types import RunnerMessages
9
+
10
+
11
+ def consolidate_history_transfer(messages: RunnerMessages) -> RunnerMessages:
12
+ """Consolidate all message history into a single user message with XML format.
13
+
14
+ This message transfer function converts all message history into XML format
15
+ and creates a single user message asking what to do next. This is useful when
16
+ you want to summarize the entire conversation context in a single prompt.
17
+
18
+ Args:
19
+ messages: The original messages to be processed
20
+
21
+ Returns:
22
+ A single user message containing the consolidated history in XML format
23
+
24
+ Example:
25
+ >>> agent = Agent(
26
+ ... model="gpt-4",
27
+ ... name="HistoryAgent",
28
+ ... instructions="You are a helpful assistant.",
29
+ ... message_transfer=consolidate_history_transfer
30
+ ... )
31
+ """
32
+ if not messages:
33
+ return messages
34
+
35
+ # Convert messages to XML format
36
+ xml_content = ["<conversation_history>"]
37
+
38
+ for message in messages:
39
+ xml_content.extend(_process_message_to_xml(message))
40
+
41
+ xml_content.append("</conversation_history>")
42
+
43
+ # Create the consolidated message
44
+ consolidated_content = "以下是目前发生的所有交互:\n\n" + "\n".join(xml_content) + "\n\n接下来该做什么?"
45
+
46
+ # Return a single user message
47
+ return [{"role": "user", "content": consolidated_content}]
48
+
49
+
50
+ def _process_message_to_xml(message: dict | object) -> list[str]:
51
+ """Process a single message and convert it to XML format.
52
+
53
+ Args:
54
+ message: A single message to process
55
+
56
+ Returns:
57
+ List of XML strings representing the message
58
+ """
59
+ xml_lines = []
60
+
61
+ if isinstance(message, dict):
62
+ xml_lines.extend(_process_dict_message(message))
63
+ elif hasattr(message, "role"):
64
+ # Handle Pydantic model format messages
65
+ role = getattr(message, "role", "unknown")
66
+ content = getattr(message, "content", "")
67
+ if isinstance(content, str):
68
+ xml_lines.append(f" <message role='{role}'>{content}</message>")
69
+ elif hasattr(message, "type"):
70
+ # Handle function call messages
71
+ xml_lines.extend(_process_function_message(message))
72
+
73
+ return xml_lines
74
+
75
+
76
+ def _process_dict_message(message: dict) -> list[str]:
77
+ """Process dictionary format message to XML."""
78
+ xml_lines = []
79
+ role = message.get("role", "unknown")
80
+ content = message.get("content", "")
81
+ message_type = message.get("type")
82
+
83
+ if message_type == "function_call":
84
+ name = message.get("name", "unknown")
85
+ arguments = message.get("arguments", "")
86
+ xml_lines.append(f" <function_call name='{name}' arguments='{arguments}' />")
87
+ elif message_type == "function_call_output":
88
+ call_id = message.get("call_id", "unknown")
89
+ output = message.get("output", "")
90
+ xml_lines.append(f" <function_result call_id='{call_id}'>{output}</function_result>")
91
+ elif role in ["user", "assistant", "system"]:
92
+ xml_lines.append(f" <message role='{role}'>{content}</message>")
93
+
94
+ return xml_lines
95
+
96
+
97
+ def _process_function_message(message: dict | object) -> list[str]:
98
+ """Process function call message to XML."""
99
+ xml_lines = []
100
+ message_type = getattr(message, "type", "unknown")
101
+
102
+ if message_type == "function_call":
103
+ name = getattr(message, "name", "unknown")
104
+ arguments = getattr(message, "arguments", "")
105
+ xml_lines.append(f" <function_call name='{name}' arguments='{arguments}' />")
106
+ elif message_type == "function_call_output":
107
+ call_id = getattr(message, "call_id", "unknown")
108
+ output = getattr(message, "output", "")
109
+ xml_lines.append(f" <function_result call_id='{call_id}'>{output}</function_result>")
110
+
111
+ return xml_lines
@@ -1,3 +1,3 @@
1
- from open_agents.processors.stream_chunk_processor import StreamChunkProcessor
1
+ from lite_agent.processors.stream_chunk_processor import StreamChunkProcessor
2
2
 
3
3
  __all__ = ["StreamChunkProcessor"]
@@ -1,53 +1,61 @@
1
1
  import litellm
2
- from funcall import Funcall
3
- from litellm.types.utils import ChatCompletionDeltaToolCall, StreamingChoices
2
+ from litellm.types.utils import ChatCompletionDeltaToolCall, ModelResponseStream, StreamingChoices
4
3
 
5
- from open_agents.loggers import logger
6
- from open_agents.types import AssistantMessage, ToolCall, ToolCallFunction
4
+ from lite_agent.loggers import logger
5
+ from lite_agent.types import AssistantMessage, ToolCall, ToolCallFunction
7
6
 
8
7
 
9
8
  class StreamChunkProcessor:
10
9
  """Processor for handling streaming responses"""
11
10
 
12
- def __init__(self, fc: Funcall) -> None:
13
- self.fc = fc
14
- self.current_message: AssistantMessage = None
11
+ def __init__(self) -> None:
12
+ self._current_message: AssistantMessage | None = None
15
13
 
16
- def initialize_message(self, chunk: litellm.ModelResponseStream, choice: StreamingChoices) -> None:
14
+ def initialize_message(self, chunk: ModelResponseStream, choice: StreamingChoices) -> None:
17
15
  """Initialize the message object"""
18
16
  delta = choice.delta
19
- self.current_message = AssistantMessage(
17
+ if delta.role != "assistant":
18
+ logger.warning("Skipping chunk with role: %s", delta.role)
19
+ return
20
+ self._current_message = AssistantMessage(
20
21
  id=chunk.id,
21
22
  index=choice.index,
22
23
  role=delta.role,
23
24
  content="",
24
25
  )
25
- logger.debug("Initialized new message: %s", self.current_message.id)
26
+ logger.debug('Initialized new message: "%s"', self._current_message.id)
26
27
 
27
28
  def update_content(self, content: str) -> None:
28
29
  """Update message content"""
29
- if self.current_message and content:
30
- self.current_message.content += content
30
+ if self._current_message and content:
31
+ self._current_message.content += content
31
32
 
32
33
  def _initialize_tool_calls(self, tool_calls: list[litellm.ChatCompletionMessageToolCall]) -> None:
33
34
  """Initialize tool calls"""
34
- if not self.current_message:
35
+ if not self._current_message:
35
36
  return
36
37
 
37
- self.current_message.tool_calls = []
38
+ self._current_message.tool_calls = []
38
39
  for call in tool_calls:
39
40
  logger.debug("Create new tool call: %s", call.id)
40
41
 
41
42
  def _update_tool_calls(self, tool_calls: list[litellm.ChatCompletionMessageToolCall]) -> None:
42
43
  """Update existing tool calls"""
43
- if not self.current_message or not self.current_message.tool_calls:
44
+ if not self._current_message:
44
45
  return
45
-
46
- for current_call, new_call in zip(self.current_message.tool_calls, tool_calls, strict=False):
47
- if new_call.function.arguments:
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:
48
54
  current_call.function.arguments += new_call.function.arguments
49
- if new_call.type:
55
+ if new_call.type and new_call.type == "function":
50
56
  current_call.type = new_call.type
57
+ elif new_call.type:
58
+ logger.warning("Unexpected tool call type: %s", new_call.type)
51
59
 
52
60
  def update_tool_calls(self, tool_calls: list[ChatCompletionDeltaToolCall]) -> None:
53
61
  """Handle tool call updates"""
@@ -55,31 +63,44 @@ class StreamChunkProcessor:
55
63
  return
56
64
  for call in tool_calls:
57
65
  if call.id:
58
- new_tool_call = ToolCall(
59
- id=call.id,
60
- type=call.type,
61
- function=ToolCallFunction(
62
- name=call.function.name or "",
63
- arguments=call.function.arguments,
64
- ),
65
- index=call.index,
66
- )
67
- if self.current_message.tool_calls is None:
68
- self.current_message.tool_calls = []
69
- self.current_message.tool_calls.append(new_tool_call)
70
- else:
71
- existing_call = self.current_message.tool_calls[call.index]
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]
72
84
  if call.function.arguments:
85
+ if existing_call.function.arguments is None:
86
+ existing_call.function.arguments = ""
73
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.")
74
90
 
75
- def handle_usage_info(self, chunk: litellm.ModelResponseStream) -> litellm.Usage | None:
91
+ def handle_usage_info(self, chunk: ModelResponseStream) -> litellm.Usage | None:
76
92
  """Handle usage info, return whether this chunk should be skipped"""
77
- usage = getattr(chunk, "usage", None)
78
- if usage:
79
- logger.debug("Model usage: %s", usage)
80
- return usage
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
81
99
 
82
- def finalize_message(self) -> AssistantMessage:
83
- """Finalize message processing"""
84
- logger.debug("Message finalized: %s", self.current_message)
85
- return self.current_message
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