lite-agent 0.5.0__py3-none-any.whl → 0.8.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/agent.py +188 -49
- lite_agent/chat_display.py +22 -14
- lite_agent/client.py +65 -3
- lite_agent/constants.py +30 -0
- lite_agent/message_transfers.py +3 -3
- lite_agent/processors/completion_event_processor.py +14 -20
- lite_agent/processors/response_event_processor.py +21 -15
- lite_agent/response_handlers/__init__.py +11 -0
- lite_agent/response_handlers/base.py +54 -0
- lite_agent/response_handlers/completion.py +78 -0
- lite_agent/response_handlers/responses.py +76 -0
- lite_agent/runner.py +312 -247
- lite_agent/types/__init__.py +2 -0
- lite_agent/types/messages.py +6 -5
- lite_agent/utils/__init__.py +0 -0
- lite_agent/utils/message_builder.py +211 -0
- lite_agent/utils/metrics.py +50 -0
- {lite_agent-0.5.0.dist-info → lite_agent-0.8.0.dist-info}/METADATA +2 -1
- lite_agent-0.8.0.dist-info/RECORD +31 -0
- lite_agent-0.5.0.dist-info/RECORD +0 -23
- {lite_agent-0.5.0.dist-info → lite_agent-0.8.0.dist-info}/WHEEL +0 -0
lite_agent/agent.py
CHANGED
|
@@ -5,12 +5,23 @@ from typing import Any, Optional
|
|
|
5
5
|
|
|
6
6
|
from funcall import Funcall
|
|
7
7
|
from jinja2 import Environment, FileSystemLoader
|
|
8
|
-
from litellm import CustomStreamWrapper
|
|
9
8
|
|
|
10
9
|
from lite_agent.client import BaseLLMClient, LiteLLMClient, ReasoningConfig
|
|
10
|
+
from lite_agent.constants import CompletionMode, ToolName
|
|
11
11
|
from lite_agent.loggers import logger
|
|
12
|
-
from lite_agent.
|
|
13
|
-
from lite_agent.types import
|
|
12
|
+
from lite_agent.response_handlers import CompletionResponseHandler, ResponsesAPIHandler
|
|
13
|
+
from lite_agent.types import (
|
|
14
|
+
AgentChunk,
|
|
15
|
+
AssistantTextContent,
|
|
16
|
+
AssistantToolCall,
|
|
17
|
+
AssistantToolCallResult,
|
|
18
|
+
FunctionCallEvent,
|
|
19
|
+
FunctionCallOutputEvent,
|
|
20
|
+
RunnerMessages,
|
|
21
|
+
ToolCall,
|
|
22
|
+
message_to_llm_dict,
|
|
23
|
+
system_message_to_llm_dict,
|
|
24
|
+
)
|
|
14
25
|
from lite_agent.types.messages import NewAssistantMessage, NewSystemMessage, NewUserMessage
|
|
15
26
|
|
|
16
27
|
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
@@ -22,7 +33,7 @@ WAIT_FOR_USER_INSTRUCTIONS_TEMPLATE = jinja_env.get_template("wait_for_user_inst
|
|
|
22
33
|
|
|
23
34
|
|
|
24
35
|
class Agent:
|
|
25
|
-
def __init__(
|
|
36
|
+
def __init__(
|
|
26
37
|
self,
|
|
27
38
|
*,
|
|
28
39
|
model: str | BaseLLMClient,
|
|
@@ -33,10 +44,24 @@ class Agent:
|
|
|
33
44
|
message_transfer: Callable[[RunnerMessages], RunnerMessages] | None = None,
|
|
34
45
|
completion_condition: str = "stop",
|
|
35
46
|
reasoning: ReasoningConfig = None,
|
|
47
|
+
stop_before_tools: list[str] | list[Callable] | None = None,
|
|
36
48
|
) -> None:
|
|
37
49
|
self.name = name
|
|
38
50
|
self.instructions = instructions
|
|
39
51
|
self.reasoning = reasoning
|
|
52
|
+
# Convert stop_before_functions to function names
|
|
53
|
+
if stop_before_tools:
|
|
54
|
+
self.stop_before_functions = set()
|
|
55
|
+
for func in stop_before_tools:
|
|
56
|
+
if isinstance(func, str):
|
|
57
|
+
self.stop_before_functions.add(func)
|
|
58
|
+
elif callable(func):
|
|
59
|
+
self.stop_before_functions.add(func.__name__)
|
|
60
|
+
else:
|
|
61
|
+
msg = f"stop_before_functions must contain strings or callables, got {type(func)}"
|
|
62
|
+
raise TypeError(msg)
|
|
63
|
+
else:
|
|
64
|
+
self.stop_before_functions = set()
|
|
40
65
|
|
|
41
66
|
if isinstance(model, BaseLLMClient):
|
|
42
67
|
# If model is a BaseLLMClient instance, use it directly
|
|
@@ -55,7 +80,7 @@ class Agent:
|
|
|
55
80
|
self.fc = Funcall(tools)
|
|
56
81
|
|
|
57
82
|
# Add wait_for_user tool if completion condition is "call"
|
|
58
|
-
if completion_condition ==
|
|
83
|
+
if completion_condition == CompletionMode.CALL:
|
|
59
84
|
self._add_wait_for_user_tool()
|
|
60
85
|
|
|
61
86
|
# Set parent for handoff agents
|
|
@@ -100,7 +125,7 @@ class Agent:
|
|
|
100
125
|
|
|
101
126
|
# Add single dynamic tool for all transfers
|
|
102
127
|
self.fc.add_dynamic_tool(
|
|
103
|
-
name=
|
|
128
|
+
name=ToolName.TRANSFER_TO_AGENT,
|
|
104
129
|
description="Transfer conversation to another agent.",
|
|
105
130
|
parameters={
|
|
106
131
|
"name": {
|
|
@@ -130,7 +155,7 @@ class Agent:
|
|
|
130
155
|
|
|
131
156
|
# Add dynamic tool for parent transfer
|
|
132
157
|
self.fc.add_dynamic_tool(
|
|
133
|
-
name=
|
|
158
|
+
name=ToolName.TRANSFER_TO_PARENT,
|
|
134
159
|
description="Transfer conversation back to parent agent when current task is completed or cannot be solved by current agent",
|
|
135
160
|
parameters={},
|
|
136
161
|
required=[],
|
|
@@ -161,7 +186,7 @@ class Agent:
|
|
|
161
186
|
try:
|
|
162
187
|
# Try to remove the existing transfer tool
|
|
163
188
|
if hasattr(self.fc, "remove_dynamic_tool"):
|
|
164
|
-
self.fc.remove_dynamic_tool(
|
|
189
|
+
self.fc.remove_dynamic_tool(ToolName.TRANSFER_TO_AGENT)
|
|
165
190
|
except Exception as e:
|
|
166
191
|
# If removal fails, log and continue anyway
|
|
167
192
|
logger.debug(f"Failed to remove existing transfer tool: {e}")
|
|
@@ -206,31 +231,30 @@ class Agent:
|
|
|
206
231
|
for message in messages:
|
|
207
232
|
if isinstance(message, NewAssistantMessage):
|
|
208
233
|
for item in message.content:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
)
|
|
234
|
+
if isinstance(item, AssistantTextContent):
|
|
235
|
+
res.append(
|
|
236
|
+
{
|
|
237
|
+
"role": "assistant",
|
|
238
|
+
"content": item.text,
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
elif isinstance(item, AssistantToolCall):
|
|
242
|
+
res.append(
|
|
243
|
+
{
|
|
244
|
+
"type": "function_call",
|
|
245
|
+
"call_id": item.call_id,
|
|
246
|
+
"name": item.name,
|
|
247
|
+
"arguments": item.arguments,
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
elif isinstance(item, AssistantToolCallResult):
|
|
251
|
+
res.append(
|
|
252
|
+
{
|
|
253
|
+
"type": "function_call_output",
|
|
254
|
+
"call_id": item.call_id,
|
|
255
|
+
"output": item.output,
|
|
256
|
+
},
|
|
257
|
+
)
|
|
234
258
|
elif isinstance(message, NewSystemMessage):
|
|
235
259
|
res.append(
|
|
236
260
|
{
|
|
@@ -270,9 +294,6 @@ class Agent:
|
|
|
270
294
|
"content": contents,
|
|
271
295
|
},
|
|
272
296
|
)
|
|
273
|
-
# Handle dict messages (legacy format)
|
|
274
|
-
elif isinstance(message, dict):
|
|
275
|
-
res.append(message)
|
|
276
297
|
return res
|
|
277
298
|
|
|
278
299
|
async def completion(
|
|
@@ -280,6 +301,8 @@ class Agent:
|
|
|
280
301
|
messages: RunnerMessages,
|
|
281
302
|
record_to_file: Path | None = None,
|
|
282
303
|
reasoning: ReasoningConfig = None,
|
|
304
|
+
*,
|
|
305
|
+
streaming: bool = True,
|
|
283
306
|
) -> AsyncGenerator[AgentChunk, None]:
|
|
284
307
|
# Apply message transfer callback if provided - always use legacy format for LLM compatibility
|
|
285
308
|
processed_messages = messages
|
|
@@ -296,19 +319,20 @@ class Agent:
|
|
|
296
319
|
tools=tools,
|
|
297
320
|
tool_choice="auto", # TODO: make this configurable
|
|
298
321
|
reasoning=reasoning,
|
|
322
|
+
streaming=streaming,
|
|
299
323
|
)
|
|
300
324
|
|
|
301
|
-
#
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
msg = "Response is not a CustomStreamWrapper, cannot stream chunks."
|
|
305
|
-
raise TypeError(msg)
|
|
325
|
+
# Use response handler for unified processing
|
|
326
|
+
handler = CompletionResponseHandler()
|
|
327
|
+
return handler.handle(resp, streaming=streaming, record_to=record_to_file)
|
|
306
328
|
|
|
307
329
|
async def responses(
|
|
308
330
|
self,
|
|
309
331
|
messages: RunnerMessages,
|
|
310
332
|
record_to_file: Path | None = None,
|
|
311
333
|
reasoning: ReasoningConfig = None,
|
|
334
|
+
*,
|
|
335
|
+
streaming: bool = True,
|
|
312
336
|
) -> AsyncGenerator[AgentChunk, None]:
|
|
313
337
|
# Apply message transfer callback if provided - always use legacy format for LLM compatibility
|
|
314
338
|
processed_messages = messages
|
|
@@ -324,21 +348,33 @@ class Agent:
|
|
|
324
348
|
tools=tools,
|
|
325
349
|
tool_choice="auto", # TODO: make this configurable
|
|
326
350
|
reasoning=reasoning,
|
|
351
|
+
streaming=streaming,
|
|
327
352
|
)
|
|
328
|
-
|
|
353
|
+
# Use response handler for unified processing
|
|
354
|
+
handler = ResponsesAPIHandler()
|
|
355
|
+
return handler.handle(resp, streaming=streaming, record_to=record_to_file)
|
|
329
356
|
|
|
330
357
|
async def list_require_confirm_tools(self, tool_calls: Sequence[ToolCall] | None) -> Sequence[ToolCall]:
|
|
331
358
|
if not tool_calls:
|
|
332
359
|
return []
|
|
333
360
|
results = []
|
|
334
361
|
for tool_call in tool_calls:
|
|
335
|
-
|
|
362
|
+
function_name = tool_call.function.name
|
|
363
|
+
|
|
364
|
+
# Check if function is in dynamic stop_before_functions list
|
|
365
|
+
if function_name in self.stop_before_functions:
|
|
366
|
+
logger.debug('Tool call "%s" requires confirmation (stop_before_functions)', tool_call.id)
|
|
367
|
+
results.append(tool_call)
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
# Check decorator-based require_confirmation
|
|
371
|
+
tool_func = self.fc.function_registry.get(function_name)
|
|
336
372
|
if not tool_func:
|
|
337
|
-
logger.warning("Tool function %s not found in registry",
|
|
373
|
+
logger.warning("Tool function %s not found in registry", function_name)
|
|
338
374
|
continue
|
|
339
|
-
tool_meta = self.fc.get_tool_meta(
|
|
375
|
+
tool_meta = self.fc.get_tool_meta(function_name)
|
|
340
376
|
if tool_meta["require_confirm"]:
|
|
341
|
-
logger.debug('Tool call "%s" requires confirmation', tool_call.id)
|
|
377
|
+
logger.debug('Tool call "%s" requires confirmation (decorator)', tool_call.id)
|
|
342
378
|
results.append(tool_call)
|
|
343
379
|
return results
|
|
344
380
|
|
|
@@ -393,10 +429,42 @@ class Agent:
|
|
|
393
429
|
role = message_dict.get("role")
|
|
394
430
|
|
|
395
431
|
if role == "assistant":
|
|
396
|
-
#
|
|
432
|
+
# Extract tool_calls from content if present
|
|
397
433
|
tool_calls = []
|
|
434
|
+
content = message_dict.get("content", [])
|
|
435
|
+
|
|
436
|
+
# Handle both string and array content
|
|
437
|
+
if isinstance(content, list):
|
|
438
|
+
# Extract tool_calls from content array and filter out non-text content
|
|
439
|
+
filtered_content = []
|
|
440
|
+
for item in content:
|
|
441
|
+
if isinstance(item, dict):
|
|
442
|
+
if item.get("type") == "tool_call":
|
|
443
|
+
tool_call = {
|
|
444
|
+
"id": item.get("call_id", ""),
|
|
445
|
+
"type": "function",
|
|
446
|
+
"function": {
|
|
447
|
+
"name": item.get("name", ""),
|
|
448
|
+
"arguments": item.get("arguments", "{}"),
|
|
449
|
+
},
|
|
450
|
+
"index": len(tool_calls),
|
|
451
|
+
}
|
|
452
|
+
tool_calls.append(tool_call)
|
|
453
|
+
elif item.get("type") == "text":
|
|
454
|
+
filtered_content.append(item)
|
|
455
|
+
# Skip tool_call_result - they should be handled by separate function_call_output messages
|
|
456
|
+
|
|
457
|
+
# Update content to only include text items
|
|
458
|
+
if filtered_content:
|
|
459
|
+
message_dict = message_dict.copy()
|
|
460
|
+
message_dict["content"] = filtered_content
|
|
461
|
+
elif tool_calls:
|
|
462
|
+
# If we have tool_calls but no text content, set content to None per OpenAI API spec
|
|
463
|
+
message_dict = message_dict.copy()
|
|
464
|
+
message_dict["content"] = None
|
|
465
|
+
|
|
466
|
+
# Look ahead for function_call messages (legacy support)
|
|
398
467
|
j = i + 1
|
|
399
|
-
|
|
400
468
|
while j < len(messages):
|
|
401
469
|
next_message = messages[j]
|
|
402
470
|
next_dict = message_to_llm_dict(next_message) if isinstance(next_message, (NewUserMessage, NewSystemMessage, NewAssistantMessage)) else next_message
|
|
@@ -421,6 +489,13 @@ class Agent:
|
|
|
421
489
|
if tool_calls:
|
|
422
490
|
assistant_msg["tool_calls"] = tool_calls # type: ignore
|
|
423
491
|
|
|
492
|
+
# Convert content format for OpenAI API compatibility
|
|
493
|
+
content = assistant_msg.get("content", [])
|
|
494
|
+
if isinstance(content, list):
|
|
495
|
+
# Extract text content and convert to string using list comprehension
|
|
496
|
+
text_parts = [item.get("text", "") for item in content if isinstance(item, dict) and item.get("type") == "text"]
|
|
497
|
+
assistant_msg["content"] = " ".join(text_parts) if text_parts else None
|
|
498
|
+
|
|
424
499
|
converted_messages.append(assistant_msg)
|
|
425
500
|
i = j # Skip the function_call messages we've processed
|
|
426
501
|
|
|
@@ -533,9 +608,73 @@ class Agent:
|
|
|
533
608
|
|
|
534
609
|
# Add dynamic tool for task completion
|
|
535
610
|
self.fc.add_dynamic_tool(
|
|
536
|
-
name=
|
|
611
|
+
name=ToolName.WAIT_FOR_USER,
|
|
537
612
|
description="Call this function when you have completed your assigned task or need more information from the user.",
|
|
538
613
|
parameters={},
|
|
539
614
|
required=[],
|
|
540
615
|
handler=wait_for_user_handler,
|
|
541
616
|
)
|
|
617
|
+
|
|
618
|
+
def set_stop_before_functions(self, functions: list[str] | list[Callable]) -> None:
|
|
619
|
+
"""Set the list of functions that require confirmation before execution.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
functions: List of function names (str) or callable objects
|
|
623
|
+
"""
|
|
624
|
+
self.stop_before_functions = set()
|
|
625
|
+
for func in functions:
|
|
626
|
+
if isinstance(func, str):
|
|
627
|
+
self.stop_before_functions.add(func)
|
|
628
|
+
elif callable(func):
|
|
629
|
+
self.stop_before_functions.add(func.__name__)
|
|
630
|
+
else:
|
|
631
|
+
msg = f"stop_before_functions must contain strings or callables, got {type(func)}"
|
|
632
|
+
raise TypeError(msg)
|
|
633
|
+
logger.debug(f"Set stop_before_functions to: {self.stop_before_functions}")
|
|
634
|
+
|
|
635
|
+
def add_stop_before_function(self, function: str | Callable) -> None:
|
|
636
|
+
"""Add a function to the stop_before_functions list.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
function: Function name (str) or callable object to add
|
|
640
|
+
"""
|
|
641
|
+
if isinstance(function, str):
|
|
642
|
+
function_name = function
|
|
643
|
+
elif callable(function):
|
|
644
|
+
function_name = function.__name__
|
|
645
|
+
else:
|
|
646
|
+
msg = f"function must be a string or callable, got {type(function)}"
|
|
647
|
+
raise TypeError(msg)
|
|
648
|
+
|
|
649
|
+
self.stop_before_functions.add(function_name)
|
|
650
|
+
logger.debug(f"Added '{function_name}' to stop_before_functions")
|
|
651
|
+
|
|
652
|
+
def remove_stop_before_function(self, function: str | Callable) -> None:
|
|
653
|
+
"""Remove a function from the stop_before_functions list.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
function: Function name (str) or callable object to remove
|
|
657
|
+
"""
|
|
658
|
+
if isinstance(function, str):
|
|
659
|
+
function_name = function
|
|
660
|
+
elif callable(function):
|
|
661
|
+
function_name = function.__name__
|
|
662
|
+
else:
|
|
663
|
+
msg = f"function must be a string or callable, got {type(function)}"
|
|
664
|
+
raise TypeError(msg)
|
|
665
|
+
|
|
666
|
+
self.stop_before_functions.discard(function_name)
|
|
667
|
+
logger.debug(f"Removed '{function_name}' from stop_before_functions")
|
|
668
|
+
|
|
669
|
+
def clear_stop_before_functions(self) -> None:
|
|
670
|
+
"""Clear all function names from the stop_before_functions list."""
|
|
671
|
+
self.stop_before_functions.clear()
|
|
672
|
+
logger.debug("Cleared all stop_before_functions")
|
|
673
|
+
|
|
674
|
+
def get_stop_before_functions(self) -> set[str]:
|
|
675
|
+
"""Get the current set of function names that require confirmation.
|
|
676
|
+
|
|
677
|
+
Returns:
|
|
678
|
+
Set of function names
|
|
679
|
+
"""
|
|
680
|
+
return self.stop_before_functions.copy()
|
lite_agent/chat_display.py
CHANGED
|
@@ -26,6 +26,8 @@ from lite_agent.types import (
|
|
|
26
26
|
AgentSystemMessage,
|
|
27
27
|
AgentUserMessage,
|
|
28
28
|
AssistantMessageMeta,
|
|
29
|
+
AssistantToolCall,
|
|
30
|
+
AssistantToolCallResult,
|
|
29
31
|
BasicMessageMeta,
|
|
30
32
|
FlexibleRunnerMessage,
|
|
31
33
|
LLMResponseMeta,
|
|
@@ -228,9 +230,9 @@ def _update_message_counts(message: FlexibleRunnerMessage, counts: dict[str, int
|
|
|
228
230
|
counts["Assistant"] += 1
|
|
229
231
|
# Count tool calls and outputs within the assistant message
|
|
230
232
|
for content_item in message.content:
|
|
231
|
-
if content_item
|
|
233
|
+
if isinstance(content_item, AssistantToolCall):
|
|
232
234
|
counts["Function Call"] += 1
|
|
233
|
-
elif content_item
|
|
235
|
+
elif isinstance(content_item, AssistantToolCallResult):
|
|
234
236
|
counts["Function Output"] += 1
|
|
235
237
|
elif isinstance(message, NewSystemMessage):
|
|
236
238
|
counts["System"] += 1
|
|
@@ -295,10 +297,18 @@ def _process_object_meta(meta: BasicMessageMeta | LLMResponseMeta | AssistantMes
|
|
|
295
297
|
"""处理对象类型的 meta 数据。"""
|
|
296
298
|
# LLMResponseMeta 和 AssistantMessageMeta 都有这些字段
|
|
297
299
|
if isinstance(meta, (LLMResponseMeta, AssistantMessageMeta)):
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
# For AssistantMessageMeta, use the structured usage field
|
|
301
|
+
if isinstance(meta, AssistantMessageMeta) and meta.usage is not None:
|
|
302
|
+
if meta.usage.input_tokens is not None:
|
|
303
|
+
total_input += int(meta.usage.input_tokens)
|
|
304
|
+
if meta.usage.output_tokens is not None:
|
|
305
|
+
total_output += int(meta.usage.output_tokens)
|
|
306
|
+
# For LLMResponseMeta, use the flat fields
|
|
307
|
+
elif isinstance(meta, LLMResponseMeta):
|
|
308
|
+
if hasattr(meta, "input_tokens") and meta.input_tokens is not None:
|
|
309
|
+
total_input += int(meta.input_tokens)
|
|
310
|
+
if hasattr(meta, "output_tokens") and meta.output_tokens is not None:
|
|
311
|
+
total_output += int(meta.output_tokens)
|
|
302
312
|
if hasattr(meta, "latency_ms") and meta.latency_ms is not None:
|
|
303
313
|
total_latency += int(meta.latency_ms)
|
|
304
314
|
if hasattr(meta, "output_time_ms") and meta.output_time_ms is not None:
|
|
@@ -363,11 +373,9 @@ def display_chat_summary(messages: RunnerMessages, *, console: Console | None =
|
|
|
363
373
|
messages: 要汇总的消息列表
|
|
364
374
|
console: Rich Console 实例,如果为 None 则创建新的
|
|
365
375
|
"""
|
|
366
|
-
|
|
367
|
-
console = Console()
|
|
368
|
-
|
|
376
|
+
active_console = console or Console()
|
|
369
377
|
summary_table = build_chat_summary_table(messages)
|
|
370
|
-
|
|
378
|
+
active_console.print(summary_table)
|
|
371
379
|
|
|
372
380
|
|
|
373
381
|
def display_messages(
|
|
@@ -437,7 +445,7 @@ def display_messages(
|
|
|
437
445
|
)
|
|
438
446
|
|
|
439
447
|
|
|
440
|
-
def _display_single_message_compact(
|
|
448
|
+
def _display_single_message_compact(
|
|
441
449
|
message: FlexibleRunnerMessage,
|
|
442
450
|
*,
|
|
443
451
|
index: int | None = None,
|
|
@@ -577,9 +585,9 @@ def _display_assistant_message_compact_v2(message: AgentAssistantMessage, contex
|
|
|
577
585
|
meta_parts.append(f"Latency:{message.meta.latency_ms}ms")
|
|
578
586
|
if message.meta.output_time_ms is not None:
|
|
579
587
|
meta_parts.append(f"Output:{message.meta.output_time_ms}ms")
|
|
580
|
-
if message.meta.input_tokens is not None and message.meta.output_tokens is not None:
|
|
581
|
-
total_tokens = message.meta.input_tokens + message.meta.output_tokens
|
|
582
|
-
meta_parts.append(f"Tokens:↑{message.meta.input_tokens}↓{message.meta.output_tokens}={total_tokens}")
|
|
588
|
+
if message.meta.usage and message.meta.usage.input_tokens is not None and message.meta.usage.output_tokens is not None:
|
|
589
|
+
total_tokens = message.meta.usage.input_tokens + message.meta.usage.output_tokens
|
|
590
|
+
meta_parts.append(f"Tokens:↑{message.meta.usage.input_tokens}↓{message.meta.usage.output_tokens}={total_tokens}")
|
|
583
591
|
|
|
584
592
|
if meta_parts:
|
|
585
593
|
meta_info = f" [dim]({' | '.join(meta_parts)})[/dim]"
|
lite_agent/client.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import Any, Literal
|
|
|
5
5
|
import litellm
|
|
6
6
|
from openai.types.chat import ChatCompletionToolParam
|
|
7
7
|
from openai.types.responses import FunctionToolParam
|
|
8
|
+
from pydantic import BaseModel
|
|
8
9
|
|
|
9
10
|
ReasoningEffort = Literal["minimal", "low", "medium", "high"]
|
|
10
11
|
ThinkingConfig = dict[str, Any] | None
|
|
@@ -18,6 +19,17 @@ ReasoningConfig = (
|
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
class LLMConfig(BaseModel):
|
|
23
|
+
"""LLM generation parameters configuration."""
|
|
24
|
+
|
|
25
|
+
temperature: float | None = None
|
|
26
|
+
max_tokens: int | None = None
|
|
27
|
+
top_p: float | None = None
|
|
28
|
+
frequency_penalty: float | None = None
|
|
29
|
+
presence_penalty: float | None = None
|
|
30
|
+
stop: list[str] | str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
21
33
|
def parse_reasoning_config(reasoning: ReasoningConfig) -> tuple[ReasoningEffort | None, ThinkingConfig]:
|
|
22
34
|
"""
|
|
23
35
|
解析统一的推理配置,返回 reasoning_effort 和 thinking_config。
|
|
@@ -36,7 +48,10 @@ def parse_reasoning_config(reasoning: ReasoningConfig) -> tuple[ReasoningEffort
|
|
|
36
48
|
return None, None
|
|
37
49
|
if isinstance(reasoning, str):
|
|
38
50
|
# 字符串类型,使用 reasoning_effort
|
|
39
|
-
|
|
51
|
+
# 确保字符串是有效的 ReasoningEffort 值
|
|
52
|
+
if reasoning in ("minimal", "low", "medium", "high"):
|
|
53
|
+
return reasoning, None # type: ignore[return-value]
|
|
54
|
+
return None, None
|
|
40
55
|
if isinstance(reasoning, dict):
|
|
41
56
|
# 字典类型,使用 thinking_config
|
|
42
57
|
return None, reasoning
|
|
@@ -58,13 +73,24 @@ class BaseLLMClient(abc.ABC):
|
|
|
58
73
|
api_base: str | None = None,
|
|
59
74
|
api_version: str | None = None,
|
|
60
75
|
reasoning: ReasoningConfig = None,
|
|
76
|
+
llm_config: LLMConfig | None = None,
|
|
77
|
+
**llm_params: Any, # noqa: ANN401
|
|
61
78
|
):
|
|
62
79
|
self.model = model
|
|
63
80
|
self.api_key = api_key
|
|
64
81
|
self.api_base = api_base
|
|
65
82
|
self.api_version = api_version
|
|
66
83
|
|
|
84
|
+
# 处理 LLM 生成参数
|
|
85
|
+
if llm_config is not None:
|
|
86
|
+
self.llm_config = llm_config
|
|
87
|
+
else:
|
|
88
|
+
# 从 **llm_params 创建配置
|
|
89
|
+
self.llm_config = LLMConfig(**llm_params)
|
|
90
|
+
|
|
67
91
|
# 处理推理配置
|
|
92
|
+
self.reasoning_effort: ReasoningEffort | None
|
|
93
|
+
self.thinking_config: ThinkingConfig
|
|
68
94
|
self.reasoning_effort, self.thinking_config = parse_reasoning_config(reasoning)
|
|
69
95
|
|
|
70
96
|
@abc.abstractmethod
|
|
@@ -74,6 +100,8 @@ class BaseLLMClient(abc.ABC):
|
|
|
74
100
|
tools: list[ChatCompletionToolParam] | None = None,
|
|
75
101
|
tool_choice: str = "auto",
|
|
76
102
|
reasoning: ReasoningConfig = None,
|
|
103
|
+
*,
|
|
104
|
+
streaming: bool = True,
|
|
77
105
|
**kwargs: Any, # noqa: ANN401
|
|
78
106
|
) -> Any: # noqa: ANN401
|
|
79
107
|
"""Perform a completion request to the LLM."""
|
|
@@ -85,6 +113,8 @@ class BaseLLMClient(abc.ABC):
|
|
|
85
113
|
tools: list[FunctionToolParam] | None = None,
|
|
86
114
|
tool_choice: Literal["none", "auto", "required"] = "auto",
|
|
87
115
|
reasoning: ReasoningConfig = None,
|
|
116
|
+
*,
|
|
117
|
+
streaming: bool = True,
|
|
88
118
|
**kwargs: Any, # noqa: ANN401
|
|
89
119
|
) -> Any: # noqa: ANN401
|
|
90
120
|
"""Perform a response request to the LLM."""
|
|
@@ -108,6 +138,8 @@ class LiteLLMClient(BaseLLMClient):
|
|
|
108
138
|
tools: list[ChatCompletionToolParam] | None = None,
|
|
109
139
|
tool_choice: str = "auto",
|
|
110
140
|
reasoning: ReasoningConfig = None,
|
|
141
|
+
*,
|
|
142
|
+
streaming: bool = True,
|
|
111
143
|
**kwargs: Any, # noqa: ANN401
|
|
112
144
|
) -> Any: # noqa: ANN401
|
|
113
145
|
"""Perform a completion request to the Litellm API."""
|
|
@@ -126,10 +158,24 @@ class LiteLLMClient(BaseLLMClient):
|
|
|
126
158
|
"api_version": self.api_version,
|
|
127
159
|
"api_key": self.api_key,
|
|
128
160
|
"api_base": self.api_base,
|
|
129
|
-
"stream":
|
|
161
|
+
"stream": streaming,
|
|
130
162
|
**kwargs,
|
|
131
163
|
}
|
|
132
164
|
|
|
165
|
+
# Add LLM generation parameters if specified
|
|
166
|
+
if self.llm_config.temperature is not None:
|
|
167
|
+
completion_params["temperature"] = self.llm_config.temperature
|
|
168
|
+
if self.llm_config.max_tokens is not None:
|
|
169
|
+
completion_params["max_tokens"] = self.llm_config.max_tokens
|
|
170
|
+
if self.llm_config.top_p is not None:
|
|
171
|
+
completion_params["top_p"] = self.llm_config.top_p
|
|
172
|
+
if self.llm_config.frequency_penalty is not None:
|
|
173
|
+
completion_params["frequency_penalty"] = self.llm_config.frequency_penalty
|
|
174
|
+
if self.llm_config.presence_penalty is not None:
|
|
175
|
+
completion_params["presence_penalty"] = self.llm_config.presence_penalty
|
|
176
|
+
if self.llm_config.stop is not None:
|
|
177
|
+
completion_params["stop"] = self.llm_config.stop
|
|
178
|
+
|
|
133
179
|
# Add reasoning parameters if specified
|
|
134
180
|
if final_reasoning_effort is not None:
|
|
135
181
|
completion_params["reasoning_effort"] = final_reasoning_effort
|
|
@@ -144,6 +190,8 @@ class LiteLLMClient(BaseLLMClient):
|
|
|
144
190
|
tools: list[FunctionToolParam] | None = None,
|
|
145
191
|
tool_choice: Literal["none", "auto", "required"] = "auto",
|
|
146
192
|
reasoning: ReasoningConfig = None,
|
|
193
|
+
*,
|
|
194
|
+
streaming: bool = True,
|
|
147
195
|
**kwargs: Any, # noqa: ANN401
|
|
148
196
|
) -> Any: # type: ignore[return] # noqa: ANN401
|
|
149
197
|
"""Perform a response request to the Litellm API."""
|
|
@@ -164,11 +212,25 @@ class LiteLLMClient(BaseLLMClient):
|
|
|
164
212
|
"api_version": self.api_version,
|
|
165
213
|
"api_key": self.api_key,
|
|
166
214
|
"api_base": self.api_base,
|
|
167
|
-
"stream":
|
|
215
|
+
"stream": streaming,
|
|
168
216
|
"store": False,
|
|
169
217
|
**kwargs,
|
|
170
218
|
}
|
|
171
219
|
|
|
220
|
+
# Add LLM generation parameters if specified
|
|
221
|
+
if self.llm_config.temperature is not None:
|
|
222
|
+
response_params["temperature"] = self.llm_config.temperature
|
|
223
|
+
if self.llm_config.max_tokens is not None:
|
|
224
|
+
response_params["max_tokens"] = self.llm_config.max_tokens
|
|
225
|
+
if self.llm_config.top_p is not None:
|
|
226
|
+
response_params["top_p"] = self.llm_config.top_p
|
|
227
|
+
if self.llm_config.frequency_penalty is not None:
|
|
228
|
+
response_params["frequency_penalty"] = self.llm_config.frequency_penalty
|
|
229
|
+
if self.llm_config.presence_penalty is not None:
|
|
230
|
+
response_params["presence_penalty"] = self.llm_config.presence_penalty
|
|
231
|
+
if self.llm_config.stop is not None:
|
|
232
|
+
response_params["stop"] = self.llm_config.stop
|
|
233
|
+
|
|
172
234
|
# Add reasoning parameters if specified
|
|
173
235
|
if final_reasoning_effort is not None:
|
|
174
236
|
response_params["reasoning_effort"] = final_reasoning_effort
|
lite_agent/constants.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CompletionMode:
|
|
5
|
+
"""Agent completion modes."""
|
|
6
|
+
|
|
7
|
+
STOP: Literal["stop"] = "stop" # Traditional completion until model decides to stop
|
|
8
|
+
CALL: Literal["call"] = "call" # Completion until specific tool is called
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ToolName:
|
|
12
|
+
"""System tool names."""
|
|
13
|
+
|
|
14
|
+
TRANSFER_TO_AGENT = "transfer_to_agent"
|
|
15
|
+
TRANSFER_TO_PARENT = "transfer_to_parent"
|
|
16
|
+
WAIT_FOR_USER = "wait_for_user"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StreamIncludes:
|
|
20
|
+
"""Default stream includes configuration."""
|
|
21
|
+
|
|
22
|
+
DEFAULT_INCLUDES = (
|
|
23
|
+
"completion_raw",
|
|
24
|
+
"usage",
|
|
25
|
+
"function_call",
|
|
26
|
+
"function_call_output",
|
|
27
|
+
"content_delta",
|
|
28
|
+
"function_call_delta",
|
|
29
|
+
"assistant_message",
|
|
30
|
+
)
|
lite_agent/message_transfers.py
CHANGED
|
@@ -5,7 +5,7 @@ This module provides common message transfer functions that can be used
|
|
|
5
5
|
with agents to preprocess messages before sending them to the API.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from lite_agent.types import RunnerMessages
|
|
8
|
+
from lite_agent.types import NewUserMessage, RunnerMessages, UserTextContent
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def consolidate_history_transfer(messages: RunnerMessages) -> RunnerMessages:
|
|
@@ -43,8 +43,8 @@ def consolidate_history_transfer(messages: RunnerMessages) -> RunnerMessages:
|
|
|
43
43
|
# Create the consolidated message
|
|
44
44
|
consolidated_content = "以下是目前发生的所有交互:\n\n" + "\n".join(xml_content) + "\n\n接下来该做什么?"
|
|
45
45
|
|
|
46
|
-
# Return a single user message
|
|
47
|
-
return [
|
|
46
|
+
# Return a single user message using NewMessage format
|
|
47
|
+
return [NewUserMessage(content=[UserTextContent(text=consolidated_content)])]
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def _process_message_to_xml(message: dict | object) -> list[str]:
|