lite-agent 0.1.0__py3-none-any.whl → 0.3.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 +8 -0
- lite_agent/agent.py +395 -20
- lite_agent/client.py +34 -0
- lite_agent/loggers.py +1 -1
- lite_agent/message_transfers.py +111 -0
- lite_agent/processors/__init__.py +1 -1
- lite_agent/processors/stream_chunk_processor.py +63 -42
- lite_agent/rich_helpers.py +503 -0
- lite_agent/runner.py +612 -31
- lite_agent/stream_handlers/__init__.py +5 -0
- lite_agent/stream_handlers/litellm.py +106 -0
- lite_agent/templates/handoffs_source_instructions.xml.j2 +10 -0
- lite_agent/templates/handoffs_target_instructions.xml.j2 +9 -0
- lite_agent/templates/wait_for_user_instructions.xml.j2 +6 -0
- lite_agent/types/__init__.py +75 -0
- lite_agent/types/chunks.py +89 -0
- lite_agent/types/messages.py +135 -0
- lite_agent/types/tool_calls.py +15 -0
- lite_agent-0.3.0.dist-info/METADATA +111 -0
- lite_agent-0.3.0.dist-info/RECORD +22 -0
- lite_agent/__main__.py +0 -110
- lite_agent/chunk_handler.py +0 -166
- lite_agent/types.py +0 -152
- lite_agent-0.1.0.dist-info/METADATA +0 -22
- lite_agent-0.1.0.dist-info/RECORD +0 -13
- {lite_agent-0.1.0.dist-info → lite_agent-0.3.0.dist-info}/WHEEL +0 -0
lite_agent/__init__.py
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Lite Agent - A lightweight AI agent framework."""
|
|
2
|
+
|
|
3
|
+
from .agent import Agent
|
|
4
|
+
from .message_transfers import consolidate_history_transfer
|
|
5
|
+
from .rich_helpers import print_chat_history, print_chat_summary
|
|
6
|
+
from .runner import Runner
|
|
7
|
+
|
|
8
|
+
__all__ = ["Agent", "Runner", "consolidate_history_transfer", "print_chat_history", "print_chat_summary"]
|
lite_agent/agent.py
CHANGED
|
@@ -1,36 +1,411 @@
|
|
|
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
|
-
import litellm
|
|
4
5
|
from funcall import Funcall
|
|
6
|
+
from jinja2 import Environment, FileSystemLoader
|
|
7
|
+
from litellm import CustomStreamWrapper
|
|
8
|
+
from pydantic import BaseModel
|
|
5
9
|
|
|
6
|
-
from
|
|
7
|
-
from
|
|
10
|
+
from lite_agent.client import BaseLLMClient, LiteLLMClient
|
|
11
|
+
from lite_agent.loggers import logger
|
|
12
|
+
from lite_agent.stream_handlers import litellm_stream_handler
|
|
13
|
+
from lite_agent.types import AgentChunk, AgentSystemMessage, RunnerMessages, ToolCall, ToolCallChunk, ToolCallResultChunk
|
|
14
|
+
|
|
15
|
+
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
16
|
+
jinja_env = Environment(loader=FileSystemLoader(str(TEMPLATES_DIR)), autoescape=True)
|
|
17
|
+
|
|
18
|
+
HANDOFFS_SOURCE_INSTRUCTIONS_TEMPLATE = jinja_env.get_template("handoffs_source_instructions.xml.j2")
|
|
19
|
+
HANDOFFS_TARGET_INSTRUCTIONS_TEMPLATE = jinja_env.get_template("handoffs_target_instructions.xml.j2")
|
|
20
|
+
WAIT_FOR_USER_INSTRUCTIONS_TEMPLATE = jinja_env.get_template("wait_for_user_instructions.xml.j2")
|
|
8
21
|
|
|
9
22
|
|
|
10
23
|
class Agent:
|
|
11
|
-
def __init__(
|
|
24
|
+
def __init__( # noqa: PLR0913
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
model: str | BaseLLMClient,
|
|
28
|
+
name: str,
|
|
29
|
+
instructions: str,
|
|
30
|
+
tools: list[Callable] | None = None,
|
|
31
|
+
handoffs: list["Agent"] | None = None,
|
|
32
|
+
message_transfer: Callable[[RunnerMessages], RunnerMessages] | None = None,
|
|
33
|
+
completion_condition: str = "stop",
|
|
34
|
+
) -> None:
|
|
12
35
|
self.name = name
|
|
13
36
|
self.instructions = instructions
|
|
37
|
+
if isinstance(model, BaseLLMClient):
|
|
38
|
+
# If model is a BaseLLMClient instance, use it directly
|
|
39
|
+
self.client = model
|
|
40
|
+
else:
|
|
41
|
+
# Otherwise, create a LitellmClient instance
|
|
42
|
+
self.client = LiteLLMClient(model=model)
|
|
43
|
+
self.completion_condition = completion_condition
|
|
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
|
|
14
48
|
self.fc = Funcall(tools)
|
|
15
|
-
self.model = model
|
|
16
49
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
50
|
+
# Add wait_for_user tool if completion condition is "call"
|
|
51
|
+
if completion_condition == "call":
|
|
52
|
+
self._add_wait_for_user_tool()
|
|
53
|
+
|
|
54
|
+
# Set parent for handoff agents
|
|
55
|
+
if handoffs:
|
|
56
|
+
for handoff_agent in handoffs:
|
|
57
|
+
handoff_agent.parent = self
|
|
58
|
+
self._add_transfer_tools(handoffs)
|
|
59
|
+
|
|
60
|
+
# Add transfer_to_parent tool if this agent has a parent (for cases where parent is set externally)
|
|
61
|
+
if self.parent is not None:
|
|
62
|
+
self.add_transfer_to_parent_tool()
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def parent(self) -> Optional["Agent"]:
|
|
66
|
+
return self._parent
|
|
67
|
+
|
|
68
|
+
@parent.setter
|
|
69
|
+
def parent(self, value: Optional["Agent"]) -> None:
|
|
70
|
+
self._parent = value
|
|
71
|
+
if value is not None:
|
|
72
|
+
self.add_transfer_to_parent_tool()
|
|
73
|
+
|
|
74
|
+
def _add_transfer_tools(self, handoffs: list["Agent"]) -> None:
|
|
75
|
+
"""Add transfer function for handoff agents using dynamic tools.
|
|
76
|
+
|
|
77
|
+
Creates a single 'transfer_to_agent' function that accepts a 'name' parameter
|
|
78
|
+
to specify which agent to transfer the conversation to.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
handoffs: List of Agent objects that can be transferred to
|
|
82
|
+
"""
|
|
83
|
+
# Collect all agent names for validation
|
|
84
|
+
agent_names = [agent.name for agent in handoffs]
|
|
85
|
+
|
|
86
|
+
def transfer_handler(name: str) -> str:
|
|
87
|
+
"""Handler for transfer_to_agent function."""
|
|
88
|
+
if name in agent_names:
|
|
89
|
+
return f"Transferring to agent: {name}"
|
|
90
|
+
|
|
91
|
+
available_agents = ", ".join(agent_names)
|
|
92
|
+
return f"Agent '{name}' not found. Available agents: {available_agents}"
|
|
93
|
+
|
|
94
|
+
# Add single dynamic tool for all transfers
|
|
95
|
+
self.fc.add_dynamic_tool(
|
|
96
|
+
name="transfer_to_agent",
|
|
97
|
+
description="Transfer conversation to another agent.",
|
|
98
|
+
parameters={
|
|
99
|
+
"name": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "The name of the agent to transfer to",
|
|
102
|
+
"enum": agent_names,
|
|
103
|
+
},
|
|
22
104
|
},
|
|
23
|
-
|
|
105
|
+
required=["name"],
|
|
106
|
+
handler=transfer_handler,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def add_transfer_to_parent_tool(self) -> None:
|
|
110
|
+
"""Add transfer_to_parent function for agents that have a parent.
|
|
111
|
+
|
|
112
|
+
This tool allows the agent to transfer back to its parent when:
|
|
113
|
+
- The current task is completed
|
|
114
|
+
- The agent cannot solve the current problem
|
|
115
|
+
- Escalation to a higher level is needed
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def transfer_to_parent_handler() -> str:
|
|
119
|
+
"""Handler for transfer_to_parent function."""
|
|
120
|
+
if self.parent:
|
|
121
|
+
return f"Transferring back to parent agent: {self.parent.name}"
|
|
122
|
+
return "No parent agent found"
|
|
123
|
+
|
|
124
|
+
# Add dynamic tool for parent transfer
|
|
125
|
+
self.fc.add_dynamic_tool(
|
|
126
|
+
name="transfer_to_parent",
|
|
127
|
+
description="Transfer conversation back to parent agent when current task is completed or cannot be solved by current agent",
|
|
128
|
+
parameters={},
|
|
129
|
+
required=[],
|
|
130
|
+
handler=transfer_to_parent_handler,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def add_handoff(self, agent: "Agent") -> None:
|
|
134
|
+
"""Add a handoff agent after initialization.
|
|
135
|
+
|
|
136
|
+
This method allows adding handoff agents dynamically after the agent
|
|
137
|
+
has been constructed. It properly sets up parent-child relationships
|
|
138
|
+
and updates the transfer tools.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
agent: The agent to add as a handoff target
|
|
142
|
+
"""
|
|
143
|
+
# Add to handoffs list if not already present
|
|
144
|
+
if agent not in self.handoffs:
|
|
145
|
+
self.handoffs.append(agent)
|
|
146
|
+
|
|
147
|
+
# Set parent relationship
|
|
148
|
+
agent.parent = self
|
|
149
|
+
|
|
150
|
+
# Add transfer_to_parent tool to the handoff agent
|
|
151
|
+
agent.add_transfer_to_parent_tool()
|
|
152
|
+
|
|
153
|
+
# Remove existing transfer tool if it exists and recreate with all agents
|
|
154
|
+
try:
|
|
155
|
+
# Try to remove the existing transfer tool
|
|
156
|
+
if hasattr(self.fc, "remove_dynamic_tool"):
|
|
157
|
+
self.fc.remove_dynamic_tool("transfer_to_agent")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
# If removal fails, log and continue anyway
|
|
160
|
+
logger.debug(f"Failed to remove existing transfer tool: {e}")
|
|
161
|
+
|
|
162
|
+
# Regenerate transfer tools to include the new agent
|
|
163
|
+
self._add_transfer_tools(self.handoffs)
|
|
164
|
+
|
|
165
|
+
def prepare_completion_messages(self, messages: RunnerMessages) -> list[dict[str, str]]:
|
|
166
|
+
# Convert from responses format to completions format
|
|
167
|
+
converted_messages = self._convert_responses_to_completions_format(messages)
|
|
168
|
+
|
|
169
|
+
# Prepare instructions with handoff-specific additions
|
|
170
|
+
instructions = self.instructions
|
|
171
|
+
|
|
172
|
+
# Add source instructions if this agent can handoff to others
|
|
173
|
+
if self.handoffs:
|
|
174
|
+
instructions = HANDOFFS_SOURCE_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
175
|
+
|
|
176
|
+
# Add target instructions if this agent can be handed off to (has a parent)
|
|
177
|
+
if self.parent:
|
|
178
|
+
instructions = HANDOFFS_TARGET_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
179
|
+
|
|
180
|
+
# Add wait_for_user instructions if completion condition is "call"
|
|
181
|
+
if self.completion_condition == "call":
|
|
182
|
+
instructions = WAIT_FOR_USER_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
183
|
+
|
|
184
|
+
return [
|
|
185
|
+
AgentSystemMessage(
|
|
186
|
+
role="system",
|
|
187
|
+
content=f"You are {self.name}. {instructions}",
|
|
188
|
+
).model_dump(),
|
|
189
|
+
*converted_messages,
|
|
24
190
|
]
|
|
25
191
|
|
|
26
|
-
async def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
192
|
+
async def completion(self, messages: RunnerMessages, record_to_file: Path | None = None) -> AsyncGenerator[AgentChunk, None]:
|
|
193
|
+
# Apply message transfer callback if provided
|
|
194
|
+
processed_messages = messages
|
|
195
|
+
if self.message_transfer:
|
|
196
|
+
logger.debug(f"Applying message transfer callback for agent {self.name}")
|
|
197
|
+
processed_messages = self.message_transfer(messages)
|
|
198
|
+
|
|
199
|
+
self.message_histories = self.prepare_completion_messages(processed_messages)
|
|
200
|
+
tools = self.fc.get_tools(target="completion")
|
|
201
|
+
resp = await self.client.completion(
|
|
31
202
|
messages=self.message_histories,
|
|
32
203
|
tools=tools,
|
|
33
|
-
tool_choice="auto",
|
|
34
|
-
|
|
204
|
+
tool_choice="auto", # TODO: make this configurable
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Ensure resp is a CustomStreamWrapper
|
|
208
|
+
if isinstance(resp, CustomStreamWrapper):
|
|
209
|
+
return litellm_stream_handler(resp, record_to=record_to_file)
|
|
210
|
+
msg = "Response is not a CustomStreamWrapper, cannot stream chunks."
|
|
211
|
+
raise TypeError(msg)
|
|
212
|
+
|
|
213
|
+
async def list_require_confirm_tools(self, tool_calls: Sequence[ToolCall] | None) -> Sequence[ToolCall]:
|
|
214
|
+
if not tool_calls:
|
|
215
|
+
return []
|
|
216
|
+
results = []
|
|
217
|
+
for tool_call in tool_calls:
|
|
218
|
+
tool_func = self.fc.function_registry.get(tool_call.function.name)
|
|
219
|
+
if not tool_func:
|
|
220
|
+
logger.warning("Tool function %s not found in registry", tool_call.function.name)
|
|
221
|
+
continue
|
|
222
|
+
tool_meta = self.fc.get_tool_meta(tool_call.function.name)
|
|
223
|
+
if tool_meta["require_confirm"]:
|
|
224
|
+
logger.debug('Tool call "%s" requires confirmation', tool_call.id)
|
|
225
|
+
results.append(tool_call)
|
|
226
|
+
return results
|
|
227
|
+
|
|
228
|
+
async def handle_tool_calls(self, tool_calls: Sequence[ToolCall] | None, context: Any | None = None) -> AsyncGenerator[ToolCallChunk | ToolCallResultChunk, None]: # noqa: ANN401
|
|
229
|
+
if not tool_calls:
|
|
230
|
+
return
|
|
231
|
+
if tool_calls:
|
|
232
|
+
for tool_call in tool_calls:
|
|
233
|
+
tool_func = self.fc.function_registry.get(tool_call.function.name)
|
|
234
|
+
if not tool_func:
|
|
235
|
+
logger.warning("Tool function %s not found in registry", tool_call.function.name)
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
for tool_call in tool_calls:
|
|
239
|
+
try:
|
|
240
|
+
yield ToolCallChunk(
|
|
241
|
+
type="tool_call",
|
|
242
|
+
name=tool_call.function.name,
|
|
243
|
+
arguments=tool_call.function.arguments or "",
|
|
244
|
+
)
|
|
245
|
+
content = await self.fc.call_function_async(tool_call.function.name, tool_call.function.arguments or "", context)
|
|
246
|
+
yield ToolCallResultChunk(
|
|
247
|
+
type="tool_call_result",
|
|
248
|
+
tool_call_id=tool_call.id,
|
|
249
|
+
name=tool_call.function.name,
|
|
250
|
+
content=str(content),
|
|
251
|
+
)
|
|
252
|
+
except Exception as e: # noqa: PERF203
|
|
253
|
+
logger.exception("Tool call %s failed", tool_call.id)
|
|
254
|
+
yield ToolCallResultChunk(
|
|
255
|
+
type="tool_call_result",
|
|
256
|
+
tool_call_id=tool_call.id,
|
|
257
|
+
name=tool_call.function.name,
|
|
258
|
+
content=str(e),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def _convert_responses_to_completions_format(self, messages: RunnerMessages) -> list[dict]:
|
|
262
|
+
"""Convert messages from responses API format to completions API format."""
|
|
263
|
+
converted_messages = []
|
|
264
|
+
i = 0
|
|
265
|
+
|
|
266
|
+
while i < len(messages):
|
|
267
|
+
message = messages[i]
|
|
268
|
+
message_dict = message.model_dump() if isinstance(message, BaseModel) else message
|
|
269
|
+
|
|
270
|
+
message_type = message_dict.get("type")
|
|
271
|
+
role = message_dict.get("role")
|
|
272
|
+
|
|
273
|
+
if role == "assistant":
|
|
274
|
+
# Look ahead for function_call messages
|
|
275
|
+
tool_calls = []
|
|
276
|
+
j = i + 1
|
|
277
|
+
|
|
278
|
+
while j < len(messages):
|
|
279
|
+
next_message = messages[j]
|
|
280
|
+
next_dict = next_message.model_dump() if isinstance(next_message, BaseModel) else next_message
|
|
281
|
+
|
|
282
|
+
if next_dict.get("type") == "function_call":
|
|
283
|
+
tool_call = {
|
|
284
|
+
"id": next_dict["function_call_id"], # type: ignore
|
|
285
|
+
"type": "function",
|
|
286
|
+
"function": {
|
|
287
|
+
"name": next_dict["name"], # type: ignore
|
|
288
|
+
"arguments": next_dict["arguments"], # type: ignore
|
|
289
|
+
},
|
|
290
|
+
"index": len(tool_calls),
|
|
291
|
+
}
|
|
292
|
+
tool_calls.append(tool_call)
|
|
293
|
+
j += 1
|
|
294
|
+
else:
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
# Create assistant message with tool_calls if any
|
|
298
|
+
assistant_msg = message_dict.copy()
|
|
299
|
+
if tool_calls:
|
|
300
|
+
assistant_msg["tool_calls"] = tool_calls # type: ignore
|
|
301
|
+
|
|
302
|
+
converted_messages.append(assistant_msg)
|
|
303
|
+
i = j # Skip the function_call messages we've processed
|
|
304
|
+
|
|
305
|
+
elif message_type == "function_call_output":
|
|
306
|
+
# Convert to tool message
|
|
307
|
+
converted_messages.append(
|
|
308
|
+
{
|
|
309
|
+
"role": "tool",
|
|
310
|
+
"tool_call_id": message_dict["call_id"], # type: ignore
|
|
311
|
+
"content": message_dict["output"], # type: ignore
|
|
312
|
+
},
|
|
313
|
+
)
|
|
314
|
+
i += 1
|
|
315
|
+
|
|
316
|
+
elif message_type == "function_call":
|
|
317
|
+
# This should have been processed with the assistant message
|
|
318
|
+
# Skip it if we encounter it standalone
|
|
319
|
+
i += 1
|
|
320
|
+
|
|
321
|
+
else:
|
|
322
|
+
# Regular message (user, system)
|
|
323
|
+
converted_msg = message_dict.copy()
|
|
324
|
+
|
|
325
|
+
# Handle new Response API format for user messages
|
|
326
|
+
content = message_dict.get("content")
|
|
327
|
+
if role == "user" and isinstance(content, list):
|
|
328
|
+
converted_msg["content"] = self._convert_user_content_to_completions_format(content) # type: ignore
|
|
329
|
+
|
|
330
|
+
converted_messages.append(converted_msg)
|
|
331
|
+
i += 1
|
|
332
|
+
|
|
333
|
+
return converted_messages
|
|
334
|
+
|
|
335
|
+
def _convert_user_content_to_completions_format(self, content: list) -> list:
|
|
336
|
+
"""Convert user message content from Response API format to Completion API format."""
|
|
337
|
+
# Handle the case where content might not actually be a list due to test mocking
|
|
338
|
+
if type(content) is not list: # Use type() instead of isinstance() to avoid test mocking issues
|
|
339
|
+
return content
|
|
340
|
+
|
|
341
|
+
converted_content = []
|
|
342
|
+
for item in content:
|
|
343
|
+
if isinstance(item, dict):
|
|
344
|
+
item_type = item.get("type")
|
|
345
|
+
if item_type == "input_text":
|
|
346
|
+
# Convert ResponseInputText to completion API format
|
|
347
|
+
converted_content.append(
|
|
348
|
+
{
|
|
349
|
+
"type": "text",
|
|
350
|
+
"text": item["text"],
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
elif item_type == "input_image":
|
|
354
|
+
# Convert ResponseInputImage to completion API format
|
|
355
|
+
if item.get("file_id"):
|
|
356
|
+
msg = "File ID input is not supported for Completion API. Please use image_url instead of file_id for image input."
|
|
357
|
+
raise ValueError(msg)
|
|
358
|
+
|
|
359
|
+
if not item.get("image_url"):
|
|
360
|
+
msg = "ResponseInputImage must have either file_id or image_url, but image_url is required for Completion API."
|
|
361
|
+
raise ValueError(msg)
|
|
362
|
+
|
|
363
|
+
# Build image_url object with detail inside
|
|
364
|
+
image_data = {"url": item["image_url"]}
|
|
365
|
+
detail = item.get("detail", "auto")
|
|
366
|
+
if detail: # Include detail if provided
|
|
367
|
+
image_data["detail"] = detail
|
|
368
|
+
|
|
369
|
+
converted_content.append(
|
|
370
|
+
{
|
|
371
|
+
"type": "image_url",
|
|
372
|
+
"image_url": image_data,
|
|
373
|
+
},
|
|
374
|
+
)
|
|
375
|
+
else:
|
|
376
|
+
# Keep existing format (text, image_url)
|
|
377
|
+
converted_content.append(item)
|
|
378
|
+
else:
|
|
379
|
+
# Handle non-dict items (shouldn't happen, but just in case)
|
|
380
|
+
converted_content.append(item)
|
|
381
|
+
|
|
382
|
+
return converted_content
|
|
383
|
+
|
|
384
|
+
def set_message_transfer(self, message_transfer: Callable[[RunnerMessages], RunnerMessages] | None) -> None:
|
|
385
|
+
"""Set or update the message transfer callback function.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
message_transfer: A callback function that takes RunnerMessages as input
|
|
389
|
+
and returns RunnerMessages as output. This function will be
|
|
390
|
+
called before making API calls to allow preprocessing of messages.
|
|
391
|
+
"""
|
|
392
|
+
self.message_transfer = message_transfer
|
|
393
|
+
|
|
394
|
+
def _add_wait_for_user_tool(self) -> None:
|
|
395
|
+
"""Add wait_for_user tool for agents with completion_condition='call'.
|
|
396
|
+
|
|
397
|
+
This tool allows the agent to signal when it has completed its task.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
def wait_for_user_handler() -> str:
|
|
401
|
+
"""Handler for wait_for_user function."""
|
|
402
|
+
return "Waiting for user input."
|
|
403
|
+
|
|
404
|
+
# Add dynamic tool for task completion
|
|
405
|
+
self.fc.add_dynamic_tool(
|
|
406
|
+
name="wait_for_user",
|
|
407
|
+
description="Call this function when you have completed your assigned task or need more information from the user.",
|
|
408
|
+
parameters={},
|
|
409
|
+
required=[],
|
|
410
|
+
handler=wait_for_user_handler,
|
|
35
411
|
)
|
|
36
|
-
return chunk_handler(resp, self.fc)
|
lite_agent/client.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import litellm
|
|
5
|
+
from openai.types.chat import ChatCompletionToolParam
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseLLMClient(abc.ABC):
|
|
9
|
+
"""Base class for LLM clients."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, *, model: str, api_key: str | None = None, api_base: str | None = None, api_version: str | None = None):
|
|
12
|
+
self.model = model
|
|
13
|
+
self.api_key = api_key
|
|
14
|
+
self.api_base = api_base
|
|
15
|
+
self.api_version = api_version
|
|
16
|
+
|
|
17
|
+
@abc.abstractmethod
|
|
18
|
+
async def completion(self, messages: list[Any], tools: list[ChatCompletionToolParam] | None = None, tool_choice: str = "auto") -> Any: # noqa: ANN401
|
|
19
|
+
"""Perform a completion request to the LLM."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LiteLLMClient(BaseLLMClient):
|
|
23
|
+
async def completion(self, messages: list[Any], tools: list[ChatCompletionToolParam] | None = None, tool_choice: str = "auto") -> Any: # noqa: ANN401
|
|
24
|
+
"""Perform a completion request to the Litellm API."""
|
|
25
|
+
return await litellm.acompletion(
|
|
26
|
+
model=self.model,
|
|
27
|
+
messages=messages,
|
|
28
|
+
tools=tools,
|
|
29
|
+
tool_choice=tool_choice,
|
|
30
|
+
api_version=self.api_version,
|
|
31
|
+
api_key=self.api_key,
|
|
32
|
+
api_base=self.api_base,
|
|
33
|
+
stream=True,
|
|
34
|
+
)
|
lite_agent/loggers.py
CHANGED
|
@@ -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
|