lite-agent 0.2.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 +2 -1
- lite_agent/agent.py +109 -28
- lite_agent/client.py +34 -0
- lite_agent/rich_helpers.py +503 -0
- lite_agent/runner.py +162 -17
- 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 +20 -0
- lite_agent/types/messages.py +79 -12
- {lite_agent-0.2.0.dist-info → lite_agent-0.3.0.dist-info}/METADATA +1 -1
- lite_agent-0.3.0.dist-info/RECORD +22 -0
- lite_agent-0.2.0.dist-info/RECORD +0 -17
- {lite_agent-0.2.0.dist-info → lite_agent-0.3.0.dist-info}/WHEEL +0 -0
lite_agent/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from .agent import Agent
|
|
4
4
|
from .message_transfers import consolidate_history_transfer
|
|
5
|
+
from .rich_helpers import print_chat_history, print_chat_summary
|
|
5
6
|
from .runner import Runner
|
|
6
7
|
|
|
7
|
-
__all__ = ["Agent", "Runner", "consolidate_history_transfer"]
|
|
8
|
+
__all__ = ["Agent", "Runner", "consolidate_history_transfer", "print_chat_history", "print_chat_summary"]
|
lite_agent/agent.py
CHANGED
|
@@ -2,51 +2,55 @@ from collections.abc import AsyncGenerator, Callable, Sequence
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import Any, Optional
|
|
4
4
|
|
|
5
|
-
import litellm
|
|
6
5
|
from funcall import Funcall
|
|
6
|
+
from jinja2 import Environment, FileSystemLoader
|
|
7
7
|
from litellm import CustomStreamWrapper
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
+
from lite_agent.client import BaseLLMClient, LiteLLMClient
|
|
10
11
|
from lite_agent.loggers import logger
|
|
11
12
|
from lite_agent.stream_handlers import litellm_stream_handler
|
|
12
13
|
from lite_agent.types import AgentChunk, AgentSystemMessage, RunnerMessages, ToolCall, ToolCallChunk, ToolCallResultChunk
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
16
|
+
jinja_env = Environment(loader=FileSystemLoader(str(TEMPLATES_DIR)), autoescape=True)
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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>"""
|
|
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")
|
|
28
21
|
|
|
29
22
|
|
|
30
23
|
class Agent:
|
|
31
24
|
def __init__( # noqa: PLR0913
|
|
32
25
|
self,
|
|
33
26
|
*,
|
|
34
|
-
model: str,
|
|
27
|
+
model: str | BaseLLMClient,
|
|
35
28
|
name: str,
|
|
36
29
|
instructions: str,
|
|
37
30
|
tools: list[Callable] | None = None,
|
|
38
31
|
handoffs: list["Agent"] | None = None,
|
|
39
32
|
message_transfer: Callable[[RunnerMessages], RunnerMessages] | None = None,
|
|
33
|
+
completion_condition: str = "stop",
|
|
40
34
|
) -> None:
|
|
41
35
|
self.name = name
|
|
42
36
|
self.instructions = instructions
|
|
43
|
-
|
|
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
44
|
self.handoffs = handoffs if handoffs else []
|
|
45
45
|
self._parent: Agent | None = None
|
|
46
46
|
self.message_transfer = message_transfer
|
|
47
47
|
# Initialize Funcall with regular tools
|
|
48
48
|
self.fc = Funcall(tools)
|
|
49
49
|
|
|
50
|
+
# Add wait_for_user tool if completion condition is "call"
|
|
51
|
+
if completion_condition == "call":
|
|
52
|
+
self._add_wait_for_user_tool()
|
|
53
|
+
|
|
50
54
|
# Set parent for handoff agents
|
|
51
55
|
if handoffs:
|
|
52
56
|
for handoff_agent in handoffs:
|
|
@@ -167,11 +171,15 @@ class Agent:
|
|
|
167
171
|
|
|
168
172
|
# Add source instructions if this agent can handoff to others
|
|
169
173
|
if self.handoffs:
|
|
170
|
-
instructions =
|
|
174
|
+
instructions = HANDOFFS_SOURCE_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
171
175
|
|
|
172
176
|
# Add target instructions if this agent can be handed off to (has a parent)
|
|
173
177
|
if self.parent:
|
|
174
|
-
instructions =
|
|
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
|
|
175
183
|
|
|
176
184
|
return [
|
|
177
185
|
AgentSystemMessage(
|
|
@@ -190,12 +198,10 @@ class Agent:
|
|
|
190
198
|
|
|
191
199
|
self.message_histories = self.prepare_completion_messages(processed_messages)
|
|
192
200
|
tools = self.fc.get_tools(target="completion")
|
|
193
|
-
resp = await
|
|
194
|
-
model=self.model,
|
|
201
|
+
resp = await self.client.completion(
|
|
195
202
|
messages=self.message_histories,
|
|
196
203
|
tools=tools,
|
|
197
204
|
tool_choice="auto", # TODO: make this configurable
|
|
198
|
-
stream=True,
|
|
199
205
|
)
|
|
200
206
|
|
|
201
207
|
# Ensure resp is a CustomStreamWrapper
|
|
@@ -275,11 +281,11 @@ class Agent:
|
|
|
275
281
|
|
|
276
282
|
if next_dict.get("type") == "function_call":
|
|
277
283
|
tool_call = {
|
|
278
|
-
"id": next_dict["function_call_id"],
|
|
284
|
+
"id": next_dict["function_call_id"], # type: ignore
|
|
279
285
|
"type": "function",
|
|
280
286
|
"function": {
|
|
281
|
-
"name": next_dict["name"],
|
|
282
|
-
"arguments": next_dict["arguments"],
|
|
287
|
+
"name": next_dict["name"], # type: ignore
|
|
288
|
+
"arguments": next_dict["arguments"], # type: ignore
|
|
283
289
|
},
|
|
284
290
|
"index": len(tool_calls),
|
|
285
291
|
}
|
|
@@ -291,7 +297,7 @@ class Agent:
|
|
|
291
297
|
# Create assistant message with tool_calls if any
|
|
292
298
|
assistant_msg = message_dict.copy()
|
|
293
299
|
if tool_calls:
|
|
294
|
-
assistant_msg["tool_calls"] = tool_calls
|
|
300
|
+
assistant_msg["tool_calls"] = tool_calls # type: ignore
|
|
295
301
|
|
|
296
302
|
converted_messages.append(assistant_msg)
|
|
297
303
|
i = j # Skip the function_call messages we've processed
|
|
@@ -301,8 +307,8 @@ class Agent:
|
|
|
301
307
|
converted_messages.append(
|
|
302
308
|
{
|
|
303
309
|
"role": "tool",
|
|
304
|
-
"tool_call_id": message_dict["call_id"],
|
|
305
|
-
"content": message_dict["output"],
|
|
310
|
+
"tool_call_id": message_dict["call_id"], # type: ignore
|
|
311
|
+
"content": message_dict["output"], # type: ignore
|
|
306
312
|
},
|
|
307
313
|
)
|
|
308
314
|
i += 1
|
|
@@ -314,11 +320,67 @@ class Agent:
|
|
|
314
320
|
|
|
315
321
|
else:
|
|
316
322
|
# Regular message (user, system)
|
|
317
|
-
|
|
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)
|
|
318
331
|
i += 1
|
|
319
332
|
|
|
320
333
|
return converted_messages
|
|
321
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
|
+
|
|
322
384
|
def set_message_transfer(self, message_transfer: Callable[[RunnerMessages], RunnerMessages] | None) -> None:
|
|
323
385
|
"""Set or update the message transfer callback function.
|
|
324
386
|
|
|
@@ -328,3 +390,22 @@ class Agent:
|
|
|
328
390
|
called before making API calls to allow preprocessing of messages.
|
|
329
391
|
"""
|
|
330
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,
|
|
411
|
+
)
|
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
|
+
)
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rich chat history renderer for lite-agent.
|
|
3
|
+
|
|
4
|
+
This module provides utilities to beautifully render chat history using the rich library.
|
|
5
|
+
It supports all message types including user messages, assistant messages, function calls,
|
|
6
|
+
and function call outputs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.syntax import Syntax
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
from lite_agent.types import (
|
|
18
|
+
AgentAssistantMessage,
|
|
19
|
+
AgentFunctionCallOutput,
|
|
20
|
+
AgentFunctionToolCallMessage,
|
|
21
|
+
AgentSystemMessage,
|
|
22
|
+
AgentUserMessage,
|
|
23
|
+
RunnerMessages,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def print_chat_history(
|
|
28
|
+
messages: RunnerMessages,
|
|
29
|
+
*,
|
|
30
|
+
console: Console | None = None,
|
|
31
|
+
show_timestamps: bool = True,
|
|
32
|
+
show_indices: bool = True,
|
|
33
|
+
chat_width: int = 80,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""
|
|
36
|
+
使用 rich 库美观地渲染聊天记录。
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
messages: 要渲染的消息列表
|
|
40
|
+
console: Rich Console 实例,如果为 None 则创建新的
|
|
41
|
+
show_timestamps: 是否显示时间戳
|
|
42
|
+
show_indices: 是否显示消息索引
|
|
43
|
+
chat_width: 聊天气泡的最大宽度
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> from lite_agent.runner import Runner
|
|
47
|
+
>>> from lite_agent.rich_helpers import render_chat_history
|
|
48
|
+
>>>
|
|
49
|
+
>>> runner = Runner(agent=my_agent)
|
|
50
|
+
>>> # ... add some messages ...
|
|
51
|
+
>>> render_chat_history(runner.messages)
|
|
52
|
+
"""
|
|
53
|
+
if console is None:
|
|
54
|
+
console = Console()
|
|
55
|
+
|
|
56
|
+
if not messages:
|
|
57
|
+
console.print("[dim]No messages to display[/dim]")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
console.print(f"\n[bold blue]Chat History[/bold blue] ([dim]{len(messages)} messages[/dim])\n")
|
|
61
|
+
|
|
62
|
+
for i, message in enumerate(messages):
|
|
63
|
+
_render_single_message(
|
|
64
|
+
message,
|
|
65
|
+
index=i if show_indices else None,
|
|
66
|
+
console=console,
|
|
67
|
+
show_timestamp=show_timestamps,
|
|
68
|
+
chat_width=chat_width,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _render_single_message(
|
|
73
|
+
message: object,
|
|
74
|
+
*,
|
|
75
|
+
index: int | None = None,
|
|
76
|
+
console: Console,
|
|
77
|
+
show_timestamp: bool = True,
|
|
78
|
+
chat_width: int = 80,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""渲染单个消息。"""
|
|
81
|
+
timestamp = datetime.now(timezone.utc).strftime("%H:%M:%S") if show_timestamp else None
|
|
82
|
+
|
|
83
|
+
# 处理不同类型的消息
|
|
84
|
+
if isinstance(message, AgentUserMessage):
|
|
85
|
+
_render_user_message(message, index, console, timestamp, chat_width)
|
|
86
|
+
elif isinstance(message, AgentAssistantMessage):
|
|
87
|
+
_render_assistant_message(message, index, console, timestamp, chat_width)
|
|
88
|
+
elif isinstance(message, AgentSystemMessage):
|
|
89
|
+
_render_system_message(message, index, console, timestamp, chat_width)
|
|
90
|
+
elif isinstance(message, AgentFunctionToolCallMessage):
|
|
91
|
+
_render_function_call_message(message, index, console, timestamp, chat_width)
|
|
92
|
+
elif isinstance(message, AgentFunctionCallOutput):
|
|
93
|
+
_render_function_output_message(message, index, console, timestamp, chat_width)
|
|
94
|
+
elif isinstance(message, dict):
|
|
95
|
+
_render_dict_message(message, index, console, timestamp, chat_width)
|
|
96
|
+
else:
|
|
97
|
+
_render_unknown_message(message, index, console, timestamp, chat_width)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _render_user_message(
|
|
101
|
+
message: AgentUserMessage,
|
|
102
|
+
index: int | None,
|
|
103
|
+
console: Console,
|
|
104
|
+
timestamp: str | None,
|
|
105
|
+
chat_width: int,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""渲染用户消息 - 靠右显示的蓝色气泡。"""
|
|
108
|
+
content = str(message.content) # 显示完整内容,不截断
|
|
109
|
+
|
|
110
|
+
title_parts = ["👤 User"]
|
|
111
|
+
if index is not None:
|
|
112
|
+
title_parts.append(f"#{index}")
|
|
113
|
+
if timestamp:
|
|
114
|
+
title_parts.append(f"[dim]{timestamp}[/dim]")
|
|
115
|
+
|
|
116
|
+
title = " ".join(title_parts)
|
|
117
|
+
|
|
118
|
+
# 计算内容的实际宽度,用于气泡大小
|
|
119
|
+
content_width = min(len(content) + 4, chat_width) # +4 for padding
|
|
120
|
+
bubble_width = max(content_width, 20) # 最小宽度
|
|
121
|
+
|
|
122
|
+
# 创建用户消息气泡 - 靠右
|
|
123
|
+
panel = Panel(
|
|
124
|
+
content,
|
|
125
|
+
title=title,
|
|
126
|
+
title_align="left",
|
|
127
|
+
border_style="blue",
|
|
128
|
+
padding=(0, 1),
|
|
129
|
+
width=bubble_width,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# 用户消息靠右
|
|
133
|
+
console.print(panel, justify="right")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _render_assistant_message(
|
|
137
|
+
message: AgentAssistantMessage,
|
|
138
|
+
index: int | None,
|
|
139
|
+
console: Console,
|
|
140
|
+
timestamp: str | None,
|
|
141
|
+
chat_width: int,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""渲染助手消息 - 靠左显示的绿色气泡。"""
|
|
144
|
+
content = message.content # 显示完整内容,不截断
|
|
145
|
+
|
|
146
|
+
title_parts = ["🤖 Assistant"]
|
|
147
|
+
if index is not None:
|
|
148
|
+
title_parts.append(f"#{index}")
|
|
149
|
+
if timestamp:
|
|
150
|
+
title_parts.append(f"[dim]{timestamp}[/dim]")
|
|
151
|
+
|
|
152
|
+
title = " ".join(title_parts)
|
|
153
|
+
|
|
154
|
+
# 计算内容的实际宽度,用于气泡大小
|
|
155
|
+
content_width = min(len(content) + 4, chat_width) # +4 for padding
|
|
156
|
+
bubble_width = max(content_width, 20) # 最小宽度
|
|
157
|
+
|
|
158
|
+
# 创建助手消息气泡 - 靠左
|
|
159
|
+
panel = Panel(
|
|
160
|
+
content,
|
|
161
|
+
title=title,
|
|
162
|
+
title_align="left",
|
|
163
|
+
border_style="green",
|
|
164
|
+
padding=(0, 1),
|
|
165
|
+
width=bubble_width,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# 助手消息靠左
|
|
169
|
+
console.print(panel)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _render_system_message(
|
|
173
|
+
message: AgentSystemMessage,
|
|
174
|
+
index: int | None,
|
|
175
|
+
console: Console,
|
|
176
|
+
timestamp: str | None,
|
|
177
|
+
chat_width: int,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""渲染系统消息 - 居中显示的黄色气泡。"""
|
|
180
|
+
content = message.content # 显示完整内容,不截断
|
|
181
|
+
|
|
182
|
+
title_parts = ["⚙️ System"]
|
|
183
|
+
if index is not None:
|
|
184
|
+
title_parts.append(f"#{index}")
|
|
185
|
+
if timestamp:
|
|
186
|
+
title_parts.append(f"[dim]{timestamp}[/dim]")
|
|
187
|
+
|
|
188
|
+
title = " ".join(title_parts)
|
|
189
|
+
|
|
190
|
+
# 系统消息居中显示,使用较小的宽度
|
|
191
|
+
console.print(
|
|
192
|
+
Panel(
|
|
193
|
+
content,
|
|
194
|
+
title=title,
|
|
195
|
+
title_align="center",
|
|
196
|
+
border_style="yellow",
|
|
197
|
+
padding=(0, 1),
|
|
198
|
+
width=min(len(content) + 10, chat_width),
|
|
199
|
+
),
|
|
200
|
+
justify="center",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _render_function_call_message(
|
|
205
|
+
message: AgentFunctionToolCallMessage,
|
|
206
|
+
index: int | None,
|
|
207
|
+
console: Console,
|
|
208
|
+
timestamp: str | None,
|
|
209
|
+
chat_width: int,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""渲染函数调用消息 - 靠左显示的紫色气泡。"""
|
|
212
|
+
title_parts = ["🛠️ Function Call"]
|
|
213
|
+
if index is not None:
|
|
214
|
+
title_parts.append(f"#{index}")
|
|
215
|
+
if timestamp:
|
|
216
|
+
title_parts.append(f"[dim]{timestamp}[/dim]")
|
|
217
|
+
|
|
218
|
+
title = " ".join(title_parts)
|
|
219
|
+
|
|
220
|
+
# 创建表格显示函数调用详情
|
|
221
|
+
table = Table(show_header=False, box=None, padding=0)
|
|
222
|
+
table.add_column("Field", style="cyan", width=12)
|
|
223
|
+
table.add_column("Value", style="white")
|
|
224
|
+
|
|
225
|
+
table.add_row("Name:", f"[bold]{message.name}[/bold]")
|
|
226
|
+
table.add_row("Call ID:", f"[dim]{message.function_call_id}[/dim]")
|
|
227
|
+
|
|
228
|
+
if message.arguments:
|
|
229
|
+
# 尝试格式化 JSON 参数 - 显示完整内容
|
|
230
|
+
try:
|
|
231
|
+
parsed_args = json.loads(message.arguments)
|
|
232
|
+
formatted_args = json.dumps(parsed_args, indent=2, ensure_ascii=False)
|
|
233
|
+
syntax = Syntax(formatted_args, "json", theme="monokai", line_numbers=False)
|
|
234
|
+
table.add_row("Arguments:", syntax)
|
|
235
|
+
except (json.JSONDecodeError, TypeError):
|
|
236
|
+
table.add_row("Arguments:", message.arguments)
|
|
237
|
+
|
|
238
|
+
# 函数调用消息靠左
|
|
239
|
+
console.print(
|
|
240
|
+
Panel(
|
|
241
|
+
table,
|
|
242
|
+
title=title,
|
|
243
|
+
title_align="left",
|
|
244
|
+
border_style="magenta",
|
|
245
|
+
padding=(0, 1),
|
|
246
|
+
width=min(chat_width, 100),
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _render_function_output_message(
|
|
252
|
+
message: AgentFunctionCallOutput,
|
|
253
|
+
index: int | None,
|
|
254
|
+
console: Console,
|
|
255
|
+
timestamp: str | None,
|
|
256
|
+
chat_width: int,
|
|
257
|
+
) -> None:
|
|
258
|
+
"""渲染函数输出消息 - 靠左显示的青色气泡。"""
|
|
259
|
+
title_parts = ["📤 Function Output"]
|
|
260
|
+
if index is not None:
|
|
261
|
+
title_parts.append(f"#{index}")
|
|
262
|
+
if timestamp:
|
|
263
|
+
title_parts.append(f"[dim]{timestamp}[/dim]")
|
|
264
|
+
|
|
265
|
+
title = " ".join(title_parts)
|
|
266
|
+
|
|
267
|
+
output_content = message.output # 显示完整内容,不截断
|
|
268
|
+
|
|
269
|
+
# 创建表格显示函数输出详情
|
|
270
|
+
table = Table(show_header=False, box=None, padding=0)
|
|
271
|
+
table.add_column("Field", style="cyan", width=12)
|
|
272
|
+
table.add_column("Value", style="white")
|
|
273
|
+
|
|
274
|
+
table.add_row("Call ID:", f"[dim]{message.call_id}[/dim]")
|
|
275
|
+
table.add_row("Output:", output_content)
|
|
276
|
+
|
|
277
|
+
# 函数输出消息靠左
|
|
278
|
+
console.print(
|
|
279
|
+
Panel(
|
|
280
|
+
table,
|
|
281
|
+
title=title,
|
|
282
|
+
title_align="left",
|
|
283
|
+
border_style="cyan",
|
|
284
|
+
padding=(0, 1),
|
|
285
|
+
width=min(chat_width, 100),
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _render_role_based_dict_message( # noqa: PLR0913
|
|
291
|
+
*,
|
|
292
|
+
message: dict[str, object],
|
|
293
|
+
role: str,
|
|
294
|
+
index: int | None,
|
|
295
|
+
console: Console,
|
|
296
|
+
timestamp: str | None,
|
|
297
|
+
chat_width: int,
|
|
298
|
+
) -> None:
|
|
299
|
+
"""渲染基于角色的字典消息。"""
|
|
300
|
+
content = str(message.get("content", "")) # 显示完整内容,不截断
|
|
301
|
+
|
|
302
|
+
title_parts = []
|
|
303
|
+
if role == "user":
|
|
304
|
+
title_parts = ["👤 User"]
|
|
305
|
+
border_style = "blue"
|
|
306
|
+
# 用户消息靠右
|
|
307
|
+
content_width = min(len(content) + 4, chat_width)
|
|
308
|
+
bubble_width = max(content_width, 20)
|
|
309
|
+
if index is not None:
|
|
310
|
+
title_parts.append(f"#{index}")
|
|
311
|
+
if timestamp:
|
|
312
|
+
title_parts.append(f"[dim]{timestamp}[/dim]")
|
|
313
|
+
|
|
314
|
+
panel = Panel(
|
|
315
|
+
content,
|
|
316
|
+
title=" ".join(title_parts),
|
|
317
|
+
title_align="left",
|
|
318
|
+
border_style=border_style,
|
|
319
|
+
padding=(0, 1),
|
|
320
|
+
width=bubble_width,
|
|
321
|
+
)
|
|
322
|
+
console.print(panel, justify="right")
|
|
323
|
+
elif role == "assistant":
|
|
324
|
+
title_parts = ["🤖 Assistant"]
|
|
325
|
+
border_style = "green"
|
|
326
|
+
# 助手消息靠左
|
|
327
|
+
content_width = min(len(content) + 4, chat_width)
|
|
328
|
+
bubble_width = max(content_width, 20)
|
|
329
|
+
if index is not None:
|
|
330
|
+
title_parts.append(f"#{index}")
|
|
331
|
+
if timestamp:
|
|
332
|
+
title_parts.append(f"[dim]{timestamp}[/dim]")
|
|
333
|
+
|
|
334
|
+
panel = Panel(
|
|
335
|
+
content,
|
|
336
|
+
title=" ".join(title_parts),
|
|
337
|
+
title_align="left",
|
|
338
|
+
border_style=border_style,
|
|
339
|
+
padding=(0, 1),
|
|
340
|
+
width=bubble_width,
|
|
341
|
+
)
|
|
342
|
+
# 助手消息靠左
|
|
343
|
+
console.print(panel)
|
|
344
|
+
else: # system
|
|
345
|
+
title_parts = ["⚙️ System"]
|
|
346
|
+
border_style = "yellow"
|
|
347
|
+
if index is not None:
|
|
348
|
+
title_parts.append(f"#{index}")
|
|
349
|
+
if timestamp:
|
|
350
|
+
title_parts.append(f"[dim]{timestamp}[/dim]")
|
|
351
|
+
|
|
352
|
+
# 系统消息居中
|
|
353
|
+
console.print(
|
|
354
|
+
Panel(
|
|
355
|
+
content,
|
|
356
|
+
title=" ".join(title_parts),
|
|
357
|
+
title_align="center",
|
|
358
|
+
border_style=border_style,
|
|
359
|
+
padding=(0, 1),
|
|
360
|
+
width=min(len(content) + 10, chat_width),
|
|
361
|
+
),
|
|
362
|
+
justify="center",
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _render_dict_message(
|
|
367
|
+
message: dict[str, object],
|
|
368
|
+
index: int | None,
|
|
369
|
+
console: Console,
|
|
370
|
+
timestamp: str | None,
|
|
371
|
+
chat_width: int,
|
|
372
|
+
) -> None:
|
|
373
|
+
"""渲染字典格式的消息。"""
|
|
374
|
+
message_type = message.get("type")
|
|
375
|
+
role = message.get("role")
|
|
376
|
+
|
|
377
|
+
if message_type == "function_call":
|
|
378
|
+
# 创建临时 AgentFunctionToolCallMessage 对象进行渲染
|
|
379
|
+
temp_message = AgentFunctionToolCallMessage(
|
|
380
|
+
type="function_call",
|
|
381
|
+
function_call_id=str(message.get("function_call_id", "")),
|
|
382
|
+
name=str(message.get("name", "unknown")),
|
|
383
|
+
arguments=str(message.get("arguments", "")),
|
|
384
|
+
content=str(message.get("content", "")),
|
|
385
|
+
)
|
|
386
|
+
_render_function_call_message(temp_message, index, console, timestamp, chat_width)
|
|
387
|
+
elif message_type == "function_call_output":
|
|
388
|
+
# 创建临时 AgentFunctionCallOutput 对象进行渲染
|
|
389
|
+
temp_message = AgentFunctionCallOutput(
|
|
390
|
+
type="function_call_output",
|
|
391
|
+
call_id=str(message.get("call_id", "")),
|
|
392
|
+
output=str(message.get("output", "")),
|
|
393
|
+
)
|
|
394
|
+
_render_function_output_message(temp_message, index, console, timestamp, chat_width)
|
|
395
|
+
elif role in ["user", "assistant", "system"]:
|
|
396
|
+
_render_role_based_dict_message(
|
|
397
|
+
message=message,
|
|
398
|
+
role=str(role),
|
|
399
|
+
index=index,
|
|
400
|
+
console=console,
|
|
401
|
+
timestamp=timestamp,
|
|
402
|
+
chat_width=chat_width,
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
_render_unknown_message(message, index, console, timestamp, chat_width)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _render_unknown_message(
|
|
409
|
+
message: object,
|
|
410
|
+
index: int | None,
|
|
411
|
+
console: Console,
|
|
412
|
+
timestamp: str | None,
|
|
413
|
+
chat_width: int,
|
|
414
|
+
) -> None:
|
|
415
|
+
"""渲染未知类型的消息 - 居中显示的红色气泡。"""
|
|
416
|
+
title_parts = ["❓ Unknown"]
|
|
417
|
+
if index is not None:
|
|
418
|
+
title_parts.append(f"#{index}")
|
|
419
|
+
if timestamp:
|
|
420
|
+
title_parts.append(f"[dim]{timestamp}[/dim]")
|
|
421
|
+
|
|
422
|
+
title = " ".join(title_parts)
|
|
423
|
+
|
|
424
|
+
# 尝试将消息转换为可读格式 - 显示完整内容
|
|
425
|
+
try:
|
|
426
|
+
content = str(message.model_dump()) if hasattr(message, "model_dump") else str(message) # type: ignore[attr-defined]
|
|
427
|
+
except Exception:
|
|
428
|
+
content = str(message)
|
|
429
|
+
|
|
430
|
+
console.print(
|
|
431
|
+
Panel(
|
|
432
|
+
content,
|
|
433
|
+
title=title,
|
|
434
|
+
title_align="center",
|
|
435
|
+
border_style="red",
|
|
436
|
+
padding=(0, 1),
|
|
437
|
+
width=min(len(content) + 10, chat_width),
|
|
438
|
+
),
|
|
439
|
+
justify="center",
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def create_chat_summary_table(messages: RunnerMessages) -> Table:
|
|
444
|
+
"""
|
|
445
|
+
创建聊天记录摘要表格。
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
messages: 要汇总的消息列表
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Rich Table 对象,包含消息统计信息
|
|
452
|
+
"""
|
|
453
|
+
table = Table(title="Chat Summary")
|
|
454
|
+
table.add_column("Message Type", style="cyan")
|
|
455
|
+
table.add_column("Count", justify="right", style="green")
|
|
456
|
+
|
|
457
|
+
# 统计各种消息类型
|
|
458
|
+
counts = {
|
|
459
|
+
"User": 0,
|
|
460
|
+
"Assistant": 0,
|
|
461
|
+
"System": 0,
|
|
462
|
+
"Function Call": 0,
|
|
463
|
+
"Function Output": 0,
|
|
464
|
+
"Unknown": 0,
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
for message in messages:
|
|
468
|
+
if isinstance(message, AgentUserMessage) or (isinstance(message, dict) and message.get("role") == "user"):
|
|
469
|
+
counts["User"] += 1
|
|
470
|
+
elif isinstance(message, AgentAssistantMessage) or (isinstance(message, dict) and message.get("role") == "assistant"):
|
|
471
|
+
counts["Assistant"] += 1
|
|
472
|
+
elif isinstance(message, AgentSystemMessage) or (isinstance(message, dict) and message.get("role") == "system"):
|
|
473
|
+
counts["System"] += 1
|
|
474
|
+
elif isinstance(message, AgentFunctionToolCallMessage) or (isinstance(message, dict) and message.get("type") == "function_call"):
|
|
475
|
+
counts["Function Call"] += 1
|
|
476
|
+
elif isinstance(message, AgentFunctionCallOutput) or (isinstance(message, dict) and message.get("type") == "function_call_output"):
|
|
477
|
+
counts["Function Output"] += 1
|
|
478
|
+
else:
|
|
479
|
+
counts["Unknown"] += 1
|
|
480
|
+
|
|
481
|
+
# 只显示计数大于0的类型
|
|
482
|
+
for msg_type, count in counts.items():
|
|
483
|
+
if count > 0:
|
|
484
|
+
table.add_row(msg_type, str(count))
|
|
485
|
+
|
|
486
|
+
table.add_row("[bold]Total[/bold]", f"[bold]{len(messages)}[/bold]")
|
|
487
|
+
|
|
488
|
+
return table
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def print_chat_summary(messages: RunnerMessages, *, console: Console | None = None) -> None:
|
|
492
|
+
"""
|
|
493
|
+
打印聊天记录摘要。
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
messages: 要汇总的消息列表
|
|
497
|
+
console: Rich Console 实例,如果为 None 则创建新的
|
|
498
|
+
"""
|
|
499
|
+
if console is None:
|
|
500
|
+
console = Console()
|
|
501
|
+
|
|
502
|
+
summary_table = create_chat_summary_table(messages)
|
|
503
|
+
console.print(summary_table)
|
lite_agent/runner.py
CHANGED
|
@@ -14,10 +14,12 @@ from lite_agent.types import (
|
|
|
14
14
|
AgentFunctionToolCallMessage,
|
|
15
15
|
AgentSystemMessage,
|
|
16
16
|
AgentUserMessage,
|
|
17
|
+
FlexibleRunnerMessage,
|
|
18
|
+
MessageDict,
|
|
17
19
|
RunnerMessage,
|
|
18
|
-
RunnerMessages,
|
|
19
20
|
ToolCall,
|
|
20
21
|
ToolCallFunction,
|
|
22
|
+
UserInput,
|
|
21
23
|
)
|
|
22
24
|
|
|
23
25
|
if TYPE_CHECKING:
|
|
@@ -70,6 +72,7 @@ class Runner:
|
|
|
70
72
|
),
|
|
71
73
|
)
|
|
72
74
|
return # Stop processing other tool calls after transfer
|
|
75
|
+
|
|
73
76
|
return_parent_calls = [tc for tc in tool_calls if tc.function.name == "transfer_to_parent"]
|
|
74
77
|
if return_parent_calls:
|
|
75
78
|
# Handle multiple transfer_to_parent calls (only execute the first one)
|
|
@@ -87,6 +90,7 @@ class Runner:
|
|
|
87
90
|
),
|
|
88
91
|
)
|
|
89
92
|
return # Stop processing other tool calls after transfer
|
|
93
|
+
|
|
90
94
|
async for tool_call_chunk in self.agent.handle_tool_calls(tool_calls, context=context):
|
|
91
95
|
if tool_call_chunk.type == "tool_call" and tool_call_chunk.type in includes:
|
|
92
96
|
yield tool_call_chunk
|
|
@@ -108,7 +112,7 @@ class Runner:
|
|
|
108
112
|
|
|
109
113
|
def run(
|
|
110
114
|
self,
|
|
111
|
-
user_input:
|
|
115
|
+
user_input: UserInput,
|
|
112
116
|
max_steps: int = 20,
|
|
113
117
|
includes: Sequence[AgentChunkType] | None = None,
|
|
114
118
|
context: "Any | None" = None, # noqa: ANN401
|
|
@@ -118,18 +122,32 @@ class Runner:
|
|
|
118
122
|
includes = self._normalize_includes(includes)
|
|
119
123
|
if isinstance(user_input, str):
|
|
120
124
|
self.messages.append(AgentUserMessage(role="user", content=user_input))
|
|
121
|
-
|
|
125
|
+
elif isinstance(user_input, (list, tuple)):
|
|
126
|
+
# Handle sequence of messages
|
|
122
127
|
for message in user_input:
|
|
123
128
|
self.append_message(message)
|
|
129
|
+
else:
|
|
130
|
+
# Handle single message (BaseModel, TypedDict, or dict)
|
|
131
|
+
# Type assertion needed due to the complex union type
|
|
132
|
+
self.append_message(user_input) # type: ignore[arg-type]
|
|
124
133
|
return self._run(max_steps, includes, self._normalize_record_path(record_to), context=context)
|
|
125
134
|
|
|
126
|
-
async def _run(self, max_steps: int, includes: Sequence[AgentChunkType], record_to: Path | None = None, context: "Any | None" = None) -> AsyncGenerator[AgentChunk, None]: # noqa: ANN401
|
|
135
|
+
async def _run(self, max_steps: int, includes: Sequence[AgentChunkType], record_to: Path | None = None, context: "Any | None" = None) -> AsyncGenerator[AgentChunk, None]: # noqa: ANN401, C901
|
|
127
136
|
"""Run the agent and return a RunResponse object that can be asynchronously iterated for each chunk."""
|
|
128
137
|
logger.debug(f"Running agent with messages: {self.messages}")
|
|
129
138
|
steps = 0
|
|
130
139
|
finish_reason = None
|
|
131
140
|
|
|
132
|
-
|
|
141
|
+
# Determine completion condition based on agent configuration
|
|
142
|
+
completion_condition = getattr(self.agent, "completion_condition", "stop")
|
|
143
|
+
|
|
144
|
+
def is_finish() -> bool:
|
|
145
|
+
if completion_condition == "call":
|
|
146
|
+
function_calls = self._find_pending_function_calls()
|
|
147
|
+
return any(getattr(fc, "name", None) == "wait_for_user" for fc in function_calls)
|
|
148
|
+
return finish_reason == "stop"
|
|
149
|
+
|
|
150
|
+
while not is_finish() and steps < max_steps:
|
|
133
151
|
resp = await self.agent.completion(self.messages, record_to_file=record_to)
|
|
134
152
|
async for chunk in resp:
|
|
135
153
|
if chunk.type in includes:
|
|
@@ -202,14 +220,13 @@ class Runner:
|
|
|
202
220
|
msg = "Cannot continue running without a valid last message from the assistant."
|
|
203
221
|
raise ValueError(msg)
|
|
204
222
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
raise ValueError(msg)
|
|
223
|
+
resp = self._run(max_steps=max_steps, includes=includes, record_to=self._normalize_record_path(record_to), context=context)
|
|
224
|
+
async for chunk in resp:
|
|
225
|
+
yield chunk
|
|
209
226
|
|
|
210
227
|
async def run_until_complete(
|
|
211
228
|
self,
|
|
212
|
-
user_input:
|
|
229
|
+
user_input: UserInput,
|
|
213
230
|
max_steps: int = 20,
|
|
214
231
|
includes: list[AgentChunkType] | None = None,
|
|
215
232
|
record_to: PathLike | str | None = None,
|
|
@@ -223,12 +240,13 @@ class Runner:
|
|
|
223
240
|
# The final message from the stream handler might still contain tool_calls
|
|
224
241
|
# We need to convert it to responses format
|
|
225
242
|
if hasattr(message, "tool_calls") and message.tool_calls:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
243
|
+
if message.content:
|
|
244
|
+
# Add the assistant message without tool_calls
|
|
245
|
+
assistant_msg = AgentAssistantMessage(
|
|
246
|
+
role="assistant",
|
|
247
|
+
content=message.content,
|
|
248
|
+
)
|
|
249
|
+
self.messages.append(assistant_msg)
|
|
232
250
|
|
|
233
251
|
# Add function call messages
|
|
234
252
|
for tool_call in message.tool_calls:
|
|
@@ -285,7 +303,134 @@ class Runner:
|
|
|
285
303
|
tool_calls.append(tool_call)
|
|
286
304
|
return tool_calls
|
|
287
305
|
|
|
288
|
-
def
|
|
306
|
+
def set_chat_history(self, messages: Sequence[FlexibleRunnerMessage], root_agent: Agent | None = None) -> None:
|
|
307
|
+
"""Set the entire chat history and track the current agent based on function calls.
|
|
308
|
+
|
|
309
|
+
This method analyzes the message history to determine which agent should be active
|
|
310
|
+
based on transfer_to_agent and transfer_to_parent function calls.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
messages: List of messages to set as the chat history
|
|
314
|
+
root_agent: The root agent to use if no transfers are found. If None, uses self.agent
|
|
315
|
+
"""
|
|
316
|
+
# Clear current messages
|
|
317
|
+
self.messages.clear()
|
|
318
|
+
|
|
319
|
+
# Set initial agent
|
|
320
|
+
current_agent = root_agent if root_agent is not None else self.agent
|
|
321
|
+
|
|
322
|
+
# Add each message and track agent transfers
|
|
323
|
+
for message in messages:
|
|
324
|
+
self.append_message(message)
|
|
325
|
+
current_agent = self._track_agent_transfer_in_message(message, current_agent)
|
|
326
|
+
|
|
327
|
+
# Set the current agent based on the tracked transfers
|
|
328
|
+
self.agent = current_agent
|
|
329
|
+
logger.info(f"Chat history set with {len(self.messages)} messages. Current agent: {self.agent.name}")
|
|
330
|
+
|
|
331
|
+
def get_messages_dict(self) -> list[dict[str, Any]]:
|
|
332
|
+
"""Get the messages in JSONL format."""
|
|
333
|
+
return [msg.model_dump(mode="json") for msg in self.messages]
|
|
334
|
+
|
|
335
|
+
def _track_agent_transfer_in_message(self, message: FlexibleRunnerMessage, current_agent: Agent) -> Agent:
|
|
336
|
+
"""Track agent transfers in a single message.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
message: The message to analyze for transfers
|
|
340
|
+
current_agent: The currently active agent
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
The agent that should be active after processing this message
|
|
344
|
+
"""
|
|
345
|
+
if isinstance(message, dict):
|
|
346
|
+
return self._track_transfer_from_dict_message(message, current_agent)
|
|
347
|
+
|
|
348
|
+
if isinstance(message, AgentFunctionToolCallMessage):
|
|
349
|
+
return self._track_transfer_from_function_call_message(message, current_agent)
|
|
350
|
+
|
|
351
|
+
return current_agent
|
|
352
|
+
|
|
353
|
+
def _track_transfer_from_dict_message(self, message: dict[str, Any] | MessageDict, current_agent: Agent) -> Agent:
|
|
354
|
+
"""Track transfers from dictionary-format messages."""
|
|
355
|
+
message_type = message.get("type")
|
|
356
|
+
if message_type != "function_call":
|
|
357
|
+
return current_agent
|
|
358
|
+
|
|
359
|
+
function_name = message.get("name", "")
|
|
360
|
+
if function_name == "transfer_to_agent":
|
|
361
|
+
return self._handle_transfer_to_agent_tracking(message.get("arguments", ""), current_agent)
|
|
362
|
+
|
|
363
|
+
if function_name == "transfer_to_parent":
|
|
364
|
+
return self._handle_transfer_to_parent_tracking(current_agent)
|
|
365
|
+
|
|
366
|
+
return current_agent
|
|
367
|
+
|
|
368
|
+
def _track_transfer_from_function_call_message(self, message: AgentFunctionToolCallMessage, current_agent: Agent) -> Agent:
|
|
369
|
+
"""Track transfers from AgentFunctionToolCallMessage objects."""
|
|
370
|
+
if message.name == "transfer_to_agent":
|
|
371
|
+
return self._handle_transfer_to_agent_tracking(message.arguments, current_agent)
|
|
372
|
+
|
|
373
|
+
if message.name == "transfer_to_parent":
|
|
374
|
+
return self._handle_transfer_to_parent_tracking(current_agent)
|
|
375
|
+
|
|
376
|
+
return current_agent
|
|
377
|
+
|
|
378
|
+
def _handle_transfer_to_agent_tracking(self, arguments: str | dict, current_agent: Agent) -> Agent:
|
|
379
|
+
"""Handle transfer_to_agent function call tracking."""
|
|
380
|
+
try:
|
|
381
|
+
args_dict = json.loads(arguments) if isinstance(arguments, str) else arguments
|
|
382
|
+
|
|
383
|
+
target_agent_name = args_dict.get("name")
|
|
384
|
+
if target_agent_name:
|
|
385
|
+
target_agent = self._find_agent_by_name(current_agent, target_agent_name)
|
|
386
|
+
if target_agent:
|
|
387
|
+
logger.debug(f"History tracking: Transferring from {current_agent.name} to {target_agent_name}")
|
|
388
|
+
return target_agent
|
|
389
|
+
|
|
390
|
+
logger.warning(f"Target agent '{target_agent_name}' not found in handoffs during history setup")
|
|
391
|
+
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
|
392
|
+
logger.warning(f"Failed to parse transfer_to_agent arguments during history setup: {e}")
|
|
393
|
+
|
|
394
|
+
return current_agent
|
|
395
|
+
|
|
396
|
+
def _handle_transfer_to_parent_tracking(self, current_agent: Agent) -> Agent:
|
|
397
|
+
"""Handle transfer_to_parent function call tracking."""
|
|
398
|
+
if current_agent.parent:
|
|
399
|
+
logger.debug(f"History tracking: Transferring from {current_agent.name} back to parent {current_agent.parent.name}")
|
|
400
|
+
return current_agent.parent
|
|
401
|
+
|
|
402
|
+
logger.warning(f"Agent {current_agent.name} has no parent to transfer back to during history setup")
|
|
403
|
+
return current_agent
|
|
404
|
+
|
|
405
|
+
def _find_agent_by_name(self, root_agent: Agent, target_name: str) -> Agent | None:
|
|
406
|
+
"""Find an agent by name in the handoffs tree starting from root_agent.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
root_agent: The root agent to start searching from
|
|
410
|
+
target_name: The name of the agent to find
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
The agent if found, None otherwise
|
|
414
|
+
"""
|
|
415
|
+
# Check direct handoffs from current agent
|
|
416
|
+
if root_agent.handoffs:
|
|
417
|
+
for agent in root_agent.handoffs:
|
|
418
|
+
if agent.name == target_name:
|
|
419
|
+
return agent
|
|
420
|
+
|
|
421
|
+
# If not found in direct handoffs, check if we need to look in parent's handoffs
|
|
422
|
+
# This handles cases where agents can transfer to siblings
|
|
423
|
+
current = root_agent
|
|
424
|
+
while current.parent is not None:
|
|
425
|
+
current = current.parent
|
|
426
|
+
if current.handoffs:
|
|
427
|
+
for agent in current.handoffs:
|
|
428
|
+
if agent.name == target_name:
|
|
429
|
+
return agent
|
|
430
|
+
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
def append_message(self, message: FlexibleRunnerMessage) -> None:
|
|
289
434
|
if isinstance(message, RunnerMessage):
|
|
290
435
|
self.messages.append(message)
|
|
291
436
|
elif isinstance(message, dict):
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<HandoffsGuide>
|
|
2
|
+
You are a parent agent that can assign tasks to sub-agents.
|
|
3
|
+
|
|
4
|
+
You can transfer conversations to other agents for specific tasks.
|
|
5
|
+
If you need to assign tasks to multiple agents, you should break down the tasks and assign them one by one.
|
|
6
|
+
You need to wait for one sub-agent to finish before assigning the task to the next sub-agent.
|
|
7
|
+
{% if extra_instructions %}
|
|
8
|
+
{{ extra_instructions }}
|
|
9
|
+
{% endif %}
|
|
10
|
+
</HandoffsGuide>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<TransferToParentGuide>
|
|
2
|
+
You are a sub-agent that is assigned to a specific task by your parent agent.
|
|
3
|
+
|
|
4
|
+
Everything you output is intended for your parent agent to read.
|
|
5
|
+
When you finish your task, you should call `transfer_to_parent` to transfer back to parent agent.
|
|
6
|
+
{% if extra_instructions %}
|
|
7
|
+
{{ extra_instructions }}
|
|
8
|
+
{% endif %}
|
|
9
|
+
</TransferToParentGuide>
|
lite_agent/types/__init__.py
CHANGED
|
@@ -18,12 +18,22 @@ from .messages import (
|
|
|
18
18
|
AgentSystemMessage,
|
|
19
19
|
AgentUserMessage,
|
|
20
20
|
AssistantMessage,
|
|
21
|
+
AssistantMessageDict,
|
|
22
|
+
FlexibleRunnerMessage,
|
|
23
|
+
FunctionCallDict,
|
|
24
|
+
FunctionCallOutputDict,
|
|
21
25
|
Message,
|
|
26
|
+
MessageDict,
|
|
27
|
+
ResponseInputImage,
|
|
28
|
+
ResponseInputText,
|
|
22
29
|
RunnerMessage,
|
|
23
30
|
RunnerMessages,
|
|
31
|
+
SystemMessageDict,
|
|
32
|
+
UserInput,
|
|
24
33
|
UserMessageContentItemImageURL,
|
|
25
34
|
UserMessageContentItemImageURLImageURL,
|
|
26
35
|
UserMessageContentItemText,
|
|
36
|
+
UserMessageDict,
|
|
27
37
|
)
|
|
28
38
|
from .tool_calls import ToolCall, ToolCallFunction
|
|
29
39
|
|
|
@@ -37,19 +47,29 @@ __all__ = [
|
|
|
37
47
|
"AgentSystemMessage",
|
|
38
48
|
"AgentUserMessage",
|
|
39
49
|
"AssistantMessage",
|
|
50
|
+
"AssistantMessageDict",
|
|
40
51
|
"CompletionRawChunk",
|
|
41
52
|
"ContentDeltaChunk",
|
|
42
53
|
"FinalMessageChunk",
|
|
54
|
+
"FlexibleRunnerMessage",
|
|
55
|
+
"FunctionCallDict",
|
|
56
|
+
"FunctionCallOutputDict",
|
|
43
57
|
"Message",
|
|
58
|
+
"MessageDict",
|
|
59
|
+
"ResponseInputImage",
|
|
60
|
+
"ResponseInputText",
|
|
44
61
|
"RunnerMessage",
|
|
45
62
|
"RunnerMessages",
|
|
63
|
+
"SystemMessageDict",
|
|
46
64
|
"ToolCall",
|
|
47
65
|
"ToolCallChunk",
|
|
48
66
|
"ToolCallDeltaChunk",
|
|
49
67
|
"ToolCallFunction",
|
|
50
68
|
"ToolCallResultChunk",
|
|
51
69
|
"UsageChunk",
|
|
70
|
+
"UserInput",
|
|
52
71
|
"UserMessageContentItemImageURL",
|
|
53
72
|
"UserMessageContentItemImageURLImageURL",
|
|
54
73
|
"UserMessageContentItemText",
|
|
74
|
+
"UserMessageDict",
|
|
55
75
|
]
|
lite_agent/types/messages.py
CHANGED
|
@@ -1,25 +1,71 @@
|
|
|
1
1
|
from collections.abc import Sequence
|
|
2
|
-
from typing import Literal
|
|
2
|
+
from typing import Any, Literal, NotRequired, TypedDict
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel
|
|
5
|
-
from rich import Any
|
|
6
5
|
|
|
7
6
|
from .tool_calls import ToolCall
|
|
8
7
|
|
|
9
8
|
|
|
10
|
-
class
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
tool_calls: list[ToolCall] | None = None
|
|
9
|
+
class ResponseInputImageDict(TypedDict):
|
|
10
|
+
detail: NotRequired[Literal["low", "high", "auto"]]
|
|
11
|
+
type: Literal["input_image"]
|
|
12
|
+
file_id: str | None
|
|
13
|
+
image_url: str | None
|
|
16
14
|
|
|
17
15
|
|
|
18
|
-
class
|
|
19
|
-
|
|
16
|
+
class ResponseInputTextDict(TypedDict):
|
|
17
|
+
text: str
|
|
18
|
+
type: Literal["input_text"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# TypedDict definitions for better type hints
|
|
22
|
+
class UserMessageDict(TypedDict):
|
|
23
|
+
role: Literal["user"]
|
|
24
|
+
content: str | Sequence[ResponseInputTextDict | ResponseInputImageDict]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AssistantMessageDict(TypedDict):
|
|
28
|
+
role: Literal["assistant"]
|
|
20
29
|
content: str
|
|
21
30
|
|
|
22
31
|
|
|
32
|
+
class SystemMessageDict(TypedDict):
|
|
33
|
+
role: Literal["system"]
|
|
34
|
+
content: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FunctionCallDict(TypedDict):
|
|
38
|
+
type: Literal["function_call"]
|
|
39
|
+
function_call_id: str
|
|
40
|
+
name: str
|
|
41
|
+
arguments: str
|
|
42
|
+
content: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FunctionCallOutputDict(TypedDict):
|
|
46
|
+
type: Literal["function_call_output"]
|
|
47
|
+
call_id: str
|
|
48
|
+
output: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Union type for all supported message dictionary formats
|
|
52
|
+
MessageDict = UserMessageDict | AssistantMessageDict | SystemMessageDict | FunctionCallDict | FunctionCallOutputDict
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Response API format input types
|
|
56
|
+
class ResponseInputText(BaseModel):
|
|
57
|
+
text: str
|
|
58
|
+
type: Literal["input_text"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ResponseInputImage(BaseModel):
|
|
62
|
+
detail: Literal["low", "high", "auto"] = "auto"
|
|
63
|
+
type: Literal["input_image"]
|
|
64
|
+
file_id: str | None = None
|
|
65
|
+
image_url: str | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Compatibility types for old completion API format
|
|
23
69
|
class UserMessageContentItemText(BaseModel):
|
|
24
70
|
type: Literal["text"]
|
|
25
71
|
text: str
|
|
@@ -34,9 +80,23 @@ class UserMessageContentItemImageURL(BaseModel):
|
|
|
34
80
|
image_url: UserMessageContentItemImageURLImageURL
|
|
35
81
|
|
|
36
82
|
|
|
83
|
+
# Legacy types - keeping for compatibility
|
|
84
|
+
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
|
+
|
|
91
|
+
|
|
92
|
+
class Message(BaseModel):
|
|
93
|
+
role: str
|
|
94
|
+
content: str
|
|
95
|
+
|
|
96
|
+
|
|
37
97
|
class AgentUserMessage(BaseModel):
|
|
38
98
|
role: Literal["user"]
|
|
39
|
-
content: str | Sequence[UserMessageContentItemText | UserMessageContentItemImageURL]
|
|
99
|
+
content: str | Sequence[ResponseInputText | ResponseInputImage | UserMessageContentItemText | UserMessageContentItemImageURL]
|
|
40
100
|
|
|
41
101
|
|
|
42
102
|
class AgentAssistantMessage(BaseModel):
|
|
@@ -65,4 +125,11 @@ class AgentFunctionCallOutput(BaseModel):
|
|
|
65
125
|
|
|
66
126
|
RunnerMessage = AgentUserMessage | AgentAssistantMessage | AgentSystemMessage | AgentFunctionToolCallMessage | AgentFunctionCallOutput
|
|
67
127
|
AgentMessage = RunnerMessage | AgentSystemMessage
|
|
68
|
-
|
|
128
|
+
|
|
129
|
+
# Enhanced type definitions for better type hints
|
|
130
|
+
# Supports BaseModel instances, TypedDict, and plain dict
|
|
131
|
+
FlexibleRunnerMessage = RunnerMessage | MessageDict | dict[str, Any]
|
|
132
|
+
RunnerMessages = Sequence[FlexibleRunnerMessage]
|
|
133
|
+
|
|
134
|
+
# Type alias for user input - supports string, single message, or sequence of messages
|
|
135
|
+
UserInput = str | FlexibleRunnerMessage | RunnerMessages
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
lite_agent/__init__.py,sha256=Xaex4kVGxZzg_hhO17b8_tVXf63xFbSzBOlVdhRf-Ng,338
|
|
2
|
+
lite_agent/agent.py,sha256=MsdAnM2pqh9RLTGOFuynowkS52QXUb7vHaMHQyiOpoQ,17627
|
|
3
|
+
lite_agent/client.py,sha256=e_BsXo6KUgleRFkSPSESUoIPvyLXWyJ9E1AzExYXXsk,1236
|
|
4
|
+
lite_agent/loggers.py,sha256=XkNkdqwD_nQGfhQJ-bBWT7koci_mMkNw3aBpyMhOICw,57
|
|
5
|
+
lite_agent/message_transfers.py,sha256=nT7-tID20RK2yoN-rDiEE6sSclluSlhYSkayCzmPwk8,3984
|
|
6
|
+
lite_agent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
lite_agent/rich_helpers.py,sha256=6HdjMwR2U7P5ieZ5KHQNUsQbW-apG67GzQ_nvJm774E,15583
|
|
8
|
+
lite_agent/runner.py,sha256=LWhtmxrYxL4sFi3ZKeWWRymK3tGCvWfIh1-EcWf8P0g,27822
|
|
9
|
+
lite_agent/processors/__init__.py,sha256=X78GKL_IWwW2lg8w4DD6GOFWLzAR2wTfKxHlvOkcuUQ,114
|
|
10
|
+
lite_agent/processors/stream_chunk_processor.py,sha256=nMA_cW7FDpXwJvm4F8vFwBXmHHsSELQFcoNEjH3xvn8,4751
|
|
11
|
+
lite_agent/stream_handlers/__init__.py,sha256=2GSiG0VUgcQlFMl6JkGAqikXMII1a43Hr-J5NIct6dk,115
|
|
12
|
+
lite_agent/stream_handlers/litellm.py,sha256=NNMAl8Bvoc2xe-qWKtfqvJQA2yr3sz1IUU90rQ_9iBw,3976
|
|
13
|
+
lite_agent/templates/handoffs_source_instructions.xml.j2,sha256=2XsXQlBzk38qbxGrfyt8y2b0KlZmsV_1xavLufcdkHc,428
|
|
14
|
+
lite_agent/templates/handoffs_target_instructions.xml.j2,sha256=gSbWVYYcovPKbGpFc0kqGSJ5Y5UC3fOHyUmZfcrDgSE,356
|
|
15
|
+
lite_agent/templates/wait_for_user_instructions.xml.j2,sha256=wXbcYD5Q1FaCGVBm3Hz_Cp7nnoK7KzloP0ao-jYMwPk,231
|
|
16
|
+
lite_agent/types/__init__.py,sha256=8l2RL-55sRHQW-sTmtKkKzCQGLrENaJT7Cgy5iA5xCo,1767
|
|
17
|
+
lite_agent/types/chunks.py,sha256=Ro5BtrrdsYGkKrEekIhs9vIrBM7HljtgOkHherH8B3k,1697
|
|
18
|
+
lite_agent/types/messages.py,sha256=A66YVl2IYMMTlnEdGlbCXqMztSSMSjS9F2yyebBlKR0,3364
|
|
19
|
+
lite_agent/types/tool_calls.py,sha256=Xnut8-2-Ld9vgA2GKJY6BbFlBaAv_n4W7vo7Jx21A-E,260
|
|
20
|
+
lite_agent-0.3.0.dist-info/METADATA,sha256=l4SLUuFQlcrlr-CHOIaWqQ3WOyYnPVIsHjjxKjR_g4E,3455
|
|
21
|
+
lite_agent-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
22
|
+
lite_agent-0.3.0.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
lite_agent/__init__.py,sha256=T-jxY0aHwqPz5gs-ZzH1zvFgFdYVmOd15fnEc5A2H6U,229
|
|
2
|
-
lite_agent/agent.py,sha256=UE06H1Huk4gHGovbh9DuAJAH2zC0hqf1YXgrVa59_Os,13546
|
|
3
|
-
lite_agent/loggers.py,sha256=XkNkdqwD_nQGfhQJ-bBWT7koci_mMkNw3aBpyMhOICw,57
|
|
4
|
-
lite_agent/message_transfers.py,sha256=nT7-tID20RK2yoN-rDiEE6sSclluSlhYSkayCzmPwk8,3984
|
|
5
|
-
lite_agent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
lite_agent/runner.py,sha256=lO8Z5R2RxjjgYzDAeYQbO2niQeieXOEzqUEN484G7QU,21367
|
|
7
|
-
lite_agent/processors/__init__.py,sha256=X78GKL_IWwW2lg8w4DD6GOFWLzAR2wTfKxHlvOkcuUQ,114
|
|
8
|
-
lite_agent/processors/stream_chunk_processor.py,sha256=nMA_cW7FDpXwJvm4F8vFwBXmHHsSELQFcoNEjH3xvn8,4751
|
|
9
|
-
lite_agent/stream_handlers/__init__.py,sha256=2GSiG0VUgcQlFMl6JkGAqikXMII1a43Hr-J5NIct6dk,115
|
|
10
|
-
lite_agent/stream_handlers/litellm.py,sha256=NNMAl8Bvoc2xe-qWKtfqvJQA2yr3sz1IUU90rQ_9iBw,3976
|
|
11
|
-
lite_agent/types/__init__.py,sha256=sc2cdX1tPisfQwu2-apZtUa3u_Q6WDEqzNglfXhwCJo,1295
|
|
12
|
-
lite_agent/types/chunks.py,sha256=Ro5BtrrdsYGkKrEekIhs9vIrBM7HljtgOkHherH8B3k,1697
|
|
13
|
-
lite_agent/types/messages.py,sha256=cmMcj_r1_R9Pgu6ixmBIq2uwwqi5KzIllxGoxpcxF3w,1531
|
|
14
|
-
lite_agent/types/tool_calls.py,sha256=Xnut8-2-Ld9vgA2GKJY6BbFlBaAv_n4W7vo7Jx21A-E,260
|
|
15
|
-
lite_agent-0.2.0.dist-info/METADATA,sha256=egjWZCc9UgPAHMCJqZrjBXlXrG39f9LoW9k6DzntMas,3455
|
|
16
|
-
lite_agent-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
17
|
-
lite_agent-0.2.0.dist-info/RECORD,,
|
|
File without changes
|