lite-agent 0.9.0__py3-none-any.whl → 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lite-agent might be problematic. Click here for more details.
- lite_agent/agent.py +17 -314
- lite_agent/chat_display.py +10 -4
- lite_agent/client.py +53 -20
- lite_agent/runner.py +56 -101
- lite_agent/types/__init__.py +1 -1
- lite_agent/types/messages.py +2 -1
- lite_agent/utils/advanced_message_builder.py +234 -0
- lite_agent/utils/message_builder.py +1 -1
- lite_agent/utils/message_converter.py +232 -0
- lite_agent/utils/message_state_manager.py +152 -0
- {lite_agent-0.9.0.dist-info → lite_agent-0.11.0.dist-info}/METADATA +2 -1
- {lite_agent-0.9.0.dist-info → lite_agent-0.11.0.dist-info}/RECORD +13 -10
- {lite_agent-0.9.0.dist-info → lite_agent-0.11.0.dist-info}/WHEEL +0 -0
lite_agent/agent.py
CHANGED
|
@@ -6,23 +6,20 @@ from typing import Any, Optional
|
|
|
6
6
|
from funcall import Funcall
|
|
7
7
|
from jinja2 import Environment, FileSystemLoader
|
|
8
8
|
|
|
9
|
-
from lite_agent.client import BaseLLMClient, LiteLLMClient
|
|
9
|
+
from lite_agent.client import BaseLLMClient, LiteLLMClient
|
|
10
10
|
from lite_agent.constants import CompletionMode, ToolName
|
|
11
11
|
from lite_agent.loggers import logger
|
|
12
12
|
from lite_agent.response_handlers import CompletionResponseHandler, ResponsesAPIHandler
|
|
13
13
|
from lite_agent.types import (
|
|
14
14
|
AgentChunk,
|
|
15
|
-
AssistantTextContent,
|
|
16
|
-
AssistantToolCall,
|
|
17
|
-
AssistantToolCallResult,
|
|
18
15
|
FunctionCallEvent,
|
|
19
16
|
FunctionCallOutputEvent,
|
|
20
17
|
RunnerMessages,
|
|
21
18
|
ToolCall,
|
|
22
|
-
message_to_llm_dict,
|
|
23
19
|
system_message_to_llm_dict,
|
|
24
20
|
)
|
|
25
|
-
from lite_agent.types.messages import
|
|
21
|
+
from lite_agent.types.messages import NewSystemMessage
|
|
22
|
+
from lite_agent.utils.message_converter import MessageFormatConverter, ResponsesFormatConverter
|
|
26
23
|
|
|
27
24
|
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
28
25
|
jinja_env = Environment(loader=FileSystemLoader(str(TEMPLATES_DIR)), autoescape=True)
|
|
@@ -43,12 +40,10 @@ class Agent:
|
|
|
43
40
|
handoffs: list["Agent"] | None = None,
|
|
44
41
|
message_transfer: Callable[[RunnerMessages], RunnerMessages] | None = None,
|
|
45
42
|
completion_condition: str = "stop",
|
|
46
|
-
reasoning: ReasoningConfig = None,
|
|
47
43
|
stop_before_tools: list[str] | list[Callable] | None = None,
|
|
48
44
|
) -> None:
|
|
49
45
|
self.name = name
|
|
50
46
|
self.instructions = instructions
|
|
51
|
-
self.reasoning = reasoning
|
|
52
47
|
# Convert stop_before_functions to function names
|
|
53
48
|
if stop_before_tools:
|
|
54
49
|
self.stop_before_functions = set()
|
|
@@ -70,7 +65,6 @@ class Agent:
|
|
|
70
65
|
# Otherwise, create a LitellmClient instance
|
|
71
66
|
self.client = LiteLLMClient(
|
|
72
67
|
model=model,
|
|
73
|
-
reasoning=reasoning,
|
|
74
68
|
)
|
|
75
69
|
self.completion_condition = completion_condition
|
|
76
70
|
self.handoffs = handoffs if handoffs else []
|
|
@@ -194,9 +188,8 @@ class Agent:
|
|
|
194
188
|
# Regenerate transfer tools to include the new agent
|
|
195
189
|
self._add_transfer_tools(self.handoffs)
|
|
196
190
|
|
|
197
|
-
def
|
|
198
|
-
"""
|
|
199
|
-
converted_messages = self._convert_responses_to_completions_format(messages)
|
|
191
|
+
def _build_instructions(self) -> str:
|
|
192
|
+
"""Build complete instructions with templates."""
|
|
200
193
|
instructions = self.instructions
|
|
201
194
|
if self.handoffs:
|
|
202
195
|
instructions = HANDOFFS_SOURCE_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
@@ -204,6 +197,12 @@ class Agent:
|
|
|
204
197
|
instructions = HANDOFFS_TARGET_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
205
198
|
if self.completion_condition == "call":
|
|
206
199
|
instructions = WAIT_FOR_USER_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
200
|
+
return instructions
|
|
201
|
+
|
|
202
|
+
def prepare_completion_messages(self, messages: RunnerMessages) -> list[dict]:
|
|
203
|
+
"""Prepare messages for completions API (with conversion)."""
|
|
204
|
+
converted_messages = MessageFormatConverter.to_completion_format(messages)
|
|
205
|
+
instructions = self._build_instructions()
|
|
207
206
|
return [
|
|
208
207
|
system_message_to_llm_dict(
|
|
209
208
|
NewSystemMessage(
|
|
@@ -215,96 +214,24 @@ class Agent:
|
|
|
215
214
|
|
|
216
215
|
def prepare_responses_messages(self, messages: RunnerMessages) -> list[dict[str, Any]]:
|
|
217
216
|
"""Prepare messages for responses API (no conversion, just add system message if needed)."""
|
|
218
|
-
instructions = self.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if self.parent:
|
|
222
|
-
instructions = HANDOFFS_TARGET_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
223
|
-
if self.completion_condition == "call":
|
|
224
|
-
instructions = WAIT_FOR_USER_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
225
|
-
res: list[dict[str, Any]] = [
|
|
217
|
+
instructions = self._build_instructions()
|
|
218
|
+
converted_messages = ResponsesFormatConverter.to_responses_format(messages)
|
|
219
|
+
return [
|
|
226
220
|
{
|
|
227
221
|
"role": "system",
|
|
228
222
|
"content": f"You are {self.name}. {instructions}",
|
|
229
223
|
},
|
|
224
|
+
*converted_messages,
|
|
230
225
|
]
|
|
231
|
-
for message in messages:
|
|
232
|
-
if isinstance(message, NewAssistantMessage):
|
|
233
|
-
for item in message.content:
|
|
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
|
-
)
|
|
258
|
-
elif isinstance(message, NewSystemMessage):
|
|
259
|
-
res.append(
|
|
260
|
-
{
|
|
261
|
-
"role": "system",
|
|
262
|
-
"content": message.content,
|
|
263
|
-
},
|
|
264
|
-
)
|
|
265
|
-
elif isinstance(message, NewUserMessage):
|
|
266
|
-
contents = []
|
|
267
|
-
for item in message.content:
|
|
268
|
-
match item.type:
|
|
269
|
-
case "text":
|
|
270
|
-
contents.append(
|
|
271
|
-
{
|
|
272
|
-
"type": "input_text",
|
|
273
|
-
"text": item.text,
|
|
274
|
-
},
|
|
275
|
-
)
|
|
276
|
-
case "image":
|
|
277
|
-
contents.append(
|
|
278
|
-
{
|
|
279
|
-
"type": "input_image",
|
|
280
|
-
"image_url": item.image_url,
|
|
281
|
-
},
|
|
282
|
-
)
|
|
283
|
-
case "file":
|
|
284
|
-
contents.append(
|
|
285
|
-
{
|
|
286
|
-
"type": "input_file",
|
|
287
|
-
"file_id": item.file_id,
|
|
288
|
-
"file_name": item.file_name,
|
|
289
|
-
},
|
|
290
|
-
)
|
|
291
|
-
res.append(
|
|
292
|
-
{
|
|
293
|
-
"role": message.role,
|
|
294
|
-
"content": contents,
|
|
295
|
-
},
|
|
296
|
-
)
|
|
297
|
-
return res
|
|
298
226
|
|
|
299
227
|
async def completion(
|
|
300
228
|
self,
|
|
301
229
|
messages: RunnerMessages,
|
|
302
230
|
record_to_file: Path | None = None,
|
|
303
|
-
reasoning: ReasoningConfig = None,
|
|
304
231
|
*,
|
|
305
232
|
streaming: bool = True,
|
|
306
233
|
) -> AsyncGenerator[AgentChunk, None]:
|
|
307
|
-
# Apply message transfer callback if provided
|
|
234
|
+
# Apply message transfer callback if provided
|
|
308
235
|
processed_messages = messages
|
|
309
236
|
if self.message_transfer:
|
|
310
237
|
logger.debug(f"Applying message transfer callback for agent {self.name}")
|
|
@@ -318,7 +245,6 @@ class Agent:
|
|
|
318
245
|
messages=self.message_histories,
|
|
319
246
|
tools=tools,
|
|
320
247
|
tool_choice="auto", # TODO: make this configurable
|
|
321
|
-
reasoning=reasoning,
|
|
322
248
|
streaming=streaming,
|
|
323
249
|
)
|
|
324
250
|
|
|
@@ -330,11 +256,10 @@ class Agent:
|
|
|
330
256
|
self,
|
|
331
257
|
messages: RunnerMessages,
|
|
332
258
|
record_to_file: Path | None = None,
|
|
333
|
-
reasoning: ReasoningConfig = None,
|
|
334
259
|
*,
|
|
335
260
|
streaming: bool = True,
|
|
336
261
|
) -> AsyncGenerator[AgentChunk, None]:
|
|
337
|
-
# Apply message transfer callback if provided
|
|
262
|
+
# Apply message transfer callback if provided
|
|
338
263
|
processed_messages = messages
|
|
339
264
|
if self.message_transfer:
|
|
340
265
|
logger.debug(f"Applying message transfer callback for agent {self.name}")
|
|
@@ -347,7 +272,6 @@ class Agent:
|
|
|
347
272
|
messages=self.message_histories,
|
|
348
273
|
tools=tools,
|
|
349
274
|
tool_choice="auto", # TODO: make this configurable
|
|
350
|
-
reasoning=reasoning,
|
|
351
275
|
streaming=streaming,
|
|
352
276
|
)
|
|
353
277
|
# Use response handler for unified processing
|
|
@@ -416,227 +340,6 @@ class Agent:
|
|
|
416
340
|
execution_time_ms=execution_time_ms,
|
|
417
341
|
)
|
|
418
342
|
|
|
419
|
-
def _convert_responses_to_completions_format(self, messages: RunnerMessages) -> list[dict]:
|
|
420
|
-
"""Convert messages from responses API format to completions API format."""
|
|
421
|
-
converted_messages = []
|
|
422
|
-
i = 0
|
|
423
|
-
|
|
424
|
-
while i < len(messages):
|
|
425
|
-
message = messages[i]
|
|
426
|
-
message_dict = message_to_llm_dict(message) if isinstance(message, (NewUserMessage, NewSystemMessage, NewAssistantMessage)) else message
|
|
427
|
-
|
|
428
|
-
message_type = message_dict.get("type")
|
|
429
|
-
role = message_dict.get("role")
|
|
430
|
-
|
|
431
|
-
if role == "assistant":
|
|
432
|
-
# For NewAssistantMessage, extract directly from the message object
|
|
433
|
-
tool_calls = []
|
|
434
|
-
tool_results = []
|
|
435
|
-
|
|
436
|
-
if isinstance(message, NewAssistantMessage):
|
|
437
|
-
# Process content directly from NewAssistantMessage
|
|
438
|
-
for item in message.content:
|
|
439
|
-
if item.type == "tool_call":
|
|
440
|
-
tool_call = {
|
|
441
|
-
"id": item.call_id,
|
|
442
|
-
"type": "function",
|
|
443
|
-
"function": {
|
|
444
|
-
"name": item.name,
|
|
445
|
-
"arguments": item.arguments,
|
|
446
|
-
},
|
|
447
|
-
"index": len(tool_calls),
|
|
448
|
-
}
|
|
449
|
-
tool_calls.append(tool_call)
|
|
450
|
-
elif item.type == "tool_call_result":
|
|
451
|
-
# Collect tool call results to be added as separate tool messages
|
|
452
|
-
tool_results.append({
|
|
453
|
-
"call_id": item.call_id,
|
|
454
|
-
"output": item.output,
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
# Create assistant message with only text content and tool calls
|
|
458
|
-
text_content = " ".join([item.text for item in message.content if item.type == "text"])
|
|
459
|
-
message_dict = {
|
|
460
|
-
"role": "assistant",
|
|
461
|
-
"content": text_content if text_content else None,
|
|
462
|
-
}
|
|
463
|
-
if tool_calls:
|
|
464
|
-
message_dict["tool_calls"] = tool_calls
|
|
465
|
-
else:
|
|
466
|
-
# Legacy handling for dict messages
|
|
467
|
-
content = message_dict.get("content", [])
|
|
468
|
-
# Handle both string and array content
|
|
469
|
-
if isinstance(content, list):
|
|
470
|
-
# Extract tool_calls and tool_call_results from content array and filter out non-text content
|
|
471
|
-
filtered_content = []
|
|
472
|
-
for item in content:
|
|
473
|
-
if isinstance(item, dict):
|
|
474
|
-
if item.get("type") == "tool_call":
|
|
475
|
-
tool_call = {
|
|
476
|
-
"id": item.get("call_id", ""),
|
|
477
|
-
"type": "function",
|
|
478
|
-
"function": {
|
|
479
|
-
"name": item.get("name", ""),
|
|
480
|
-
"arguments": item.get("arguments", "{}"),
|
|
481
|
-
},
|
|
482
|
-
"index": len(tool_calls),
|
|
483
|
-
}
|
|
484
|
-
tool_calls.append(tool_call)
|
|
485
|
-
elif item.get("type") == "tool_call_result":
|
|
486
|
-
# Collect tool call results to be added as separate tool messages
|
|
487
|
-
tool_results.append({
|
|
488
|
-
"call_id": item.get("call_id", ""),
|
|
489
|
-
"output": item.get("output", ""),
|
|
490
|
-
})
|
|
491
|
-
elif item.get("type") == "text":
|
|
492
|
-
filtered_content.append(item)
|
|
493
|
-
|
|
494
|
-
# Update content to only include text items
|
|
495
|
-
if filtered_content:
|
|
496
|
-
message_dict = message_dict.copy()
|
|
497
|
-
message_dict["content"] = filtered_content
|
|
498
|
-
elif tool_calls:
|
|
499
|
-
# If we have tool_calls but no text content, set content to None per OpenAI API spec
|
|
500
|
-
message_dict = message_dict.copy()
|
|
501
|
-
message_dict["content"] = None
|
|
502
|
-
|
|
503
|
-
# Look ahead for function_call messages (legacy support)
|
|
504
|
-
j = i + 1
|
|
505
|
-
while j < len(messages):
|
|
506
|
-
next_message = messages[j]
|
|
507
|
-
next_dict = message_to_llm_dict(next_message) if isinstance(next_message, (NewUserMessage, NewSystemMessage, NewAssistantMessage)) else next_message
|
|
508
|
-
|
|
509
|
-
if next_dict.get("type") == "function_call":
|
|
510
|
-
tool_call = {
|
|
511
|
-
"id": next_dict["call_id"], # type: ignore
|
|
512
|
-
"type": "function",
|
|
513
|
-
"function": {
|
|
514
|
-
"name": next_dict["name"], # type: ignore
|
|
515
|
-
"arguments": next_dict["arguments"], # type: ignore
|
|
516
|
-
},
|
|
517
|
-
"index": len(tool_calls),
|
|
518
|
-
}
|
|
519
|
-
tool_calls.append(tool_call)
|
|
520
|
-
j += 1
|
|
521
|
-
else:
|
|
522
|
-
break
|
|
523
|
-
|
|
524
|
-
# For legacy dict messages, create assistant message with tool_calls if any
|
|
525
|
-
if not isinstance(message, NewAssistantMessage):
|
|
526
|
-
assistant_msg = message_dict.copy()
|
|
527
|
-
if tool_calls:
|
|
528
|
-
assistant_msg["tool_calls"] = tool_calls # type: ignore
|
|
529
|
-
|
|
530
|
-
# Convert content format for OpenAI API compatibility
|
|
531
|
-
content = assistant_msg.get("content", [])
|
|
532
|
-
if isinstance(content, list):
|
|
533
|
-
# Extract text content and convert to string using list comprehension
|
|
534
|
-
text_parts = [item.get("text", "") for item in content if isinstance(item, dict) and item.get("type") == "text"]
|
|
535
|
-
assistant_msg["content"] = " ".join(text_parts) if text_parts else None
|
|
536
|
-
|
|
537
|
-
message_dict = assistant_msg
|
|
538
|
-
|
|
539
|
-
converted_messages.append(message_dict)
|
|
540
|
-
|
|
541
|
-
# Add tool messages for any tool_call_results found in the assistant message
|
|
542
|
-
converted_messages.extend([
|
|
543
|
-
{
|
|
544
|
-
"role": "tool",
|
|
545
|
-
"tool_call_id": tool_result["call_id"],
|
|
546
|
-
"content": tool_result["output"],
|
|
547
|
-
}
|
|
548
|
-
for tool_result in tool_results
|
|
549
|
-
])
|
|
550
|
-
|
|
551
|
-
i = j # Skip the function_call messages we've processed
|
|
552
|
-
|
|
553
|
-
elif message_type == "function_call_output":
|
|
554
|
-
# Convert to tool message
|
|
555
|
-
converted_messages.append(
|
|
556
|
-
{
|
|
557
|
-
"role": "tool",
|
|
558
|
-
"tool_call_id": message_dict["call_id"], # type: ignore
|
|
559
|
-
"content": message_dict["output"], # type: ignore
|
|
560
|
-
},
|
|
561
|
-
)
|
|
562
|
-
i += 1
|
|
563
|
-
|
|
564
|
-
elif message_type == "function_call":
|
|
565
|
-
# This should have been processed with the assistant message
|
|
566
|
-
# Skip it if we encounter it standalone
|
|
567
|
-
i += 1
|
|
568
|
-
|
|
569
|
-
else:
|
|
570
|
-
# Regular message (user, system)
|
|
571
|
-
converted_msg = message_dict.copy()
|
|
572
|
-
|
|
573
|
-
# Handle new Response API format for user messages
|
|
574
|
-
content = message_dict.get("content")
|
|
575
|
-
if role == "user" and isinstance(content, list):
|
|
576
|
-
converted_msg["content"] = self._convert_user_content_to_completions_format(content) # type: ignore
|
|
577
|
-
|
|
578
|
-
converted_messages.append(converted_msg)
|
|
579
|
-
i += 1
|
|
580
|
-
|
|
581
|
-
return converted_messages
|
|
582
|
-
|
|
583
|
-
def _convert_user_content_to_completions_format(self, content: list) -> list:
|
|
584
|
-
"""Convert user message content from Response API format to Completion API format."""
|
|
585
|
-
# Handle the case where content might not actually be a list due to test mocking
|
|
586
|
-
if type(content) is not list: # Use type() instead of isinstance() to avoid test mocking issues
|
|
587
|
-
return content
|
|
588
|
-
|
|
589
|
-
converted_content = []
|
|
590
|
-
for item in content:
|
|
591
|
-
# Convert Pydantic objects to dict first
|
|
592
|
-
if hasattr(item, "model_dump"):
|
|
593
|
-
item_dict = item.model_dump()
|
|
594
|
-
elif hasattr(item, "dict"): # For older Pydantic versions
|
|
595
|
-
item_dict = item.dict()
|
|
596
|
-
elif isinstance(item, dict):
|
|
597
|
-
item_dict = item
|
|
598
|
-
else:
|
|
599
|
-
# Handle non-dict items (shouldn't happen, but just in case)
|
|
600
|
-
converted_content.append(item)
|
|
601
|
-
continue
|
|
602
|
-
|
|
603
|
-
item_type = item_dict.get("type")
|
|
604
|
-
if item_type in ["input_text", "text"]:
|
|
605
|
-
# Convert ResponseInputText or new text format to completion API format
|
|
606
|
-
converted_content.append(
|
|
607
|
-
{
|
|
608
|
-
"type": "text",
|
|
609
|
-
"text": item_dict["text"],
|
|
610
|
-
},
|
|
611
|
-
)
|
|
612
|
-
elif item_type in ["input_image", "image"]:
|
|
613
|
-
# Convert ResponseInputImage to completion API format
|
|
614
|
-
if item_dict.get("file_id"):
|
|
615
|
-
msg = "File ID input is not supported for Completion API"
|
|
616
|
-
raise ValueError(msg)
|
|
617
|
-
|
|
618
|
-
if not item_dict.get("image_url"):
|
|
619
|
-
msg = "ResponseInputImage must have either file_id or image_url"
|
|
620
|
-
raise ValueError(msg)
|
|
621
|
-
|
|
622
|
-
# Build image_url object with detail inside
|
|
623
|
-
image_data = {"url": item_dict["image_url"]}
|
|
624
|
-
detail = item_dict.get("detail", "auto")
|
|
625
|
-
if detail: # Include detail if provided
|
|
626
|
-
image_data["detail"] = detail
|
|
627
|
-
|
|
628
|
-
converted_content.append(
|
|
629
|
-
{
|
|
630
|
-
"type": "image_url",
|
|
631
|
-
"image_url": image_data,
|
|
632
|
-
},
|
|
633
|
-
)
|
|
634
|
-
else:
|
|
635
|
-
# Keep existing format (text, image_url)
|
|
636
|
-
converted_content.append(item_dict)
|
|
637
|
-
|
|
638
|
-
return converted_content
|
|
639
|
-
|
|
640
343
|
def set_message_transfer(self, message_transfer: Callable[[RunnerMessages], RunnerMessages] | None) -> None:
|
|
641
344
|
"""Set or update the message transfer callback function.
|
|
642
345
|
|
lite_agent/chat_display.py
CHANGED
|
@@ -532,12 +532,15 @@ def _display_user_message_with_columns(
|
|
|
532
532
|
lines = content.split("\n")
|
|
533
533
|
for i, line in enumerate(lines):
|
|
534
534
|
if i == 0:
|
|
535
|
-
#
|
|
535
|
+
# 第一行显示 User: 标签
|
|
536
536
|
table.add_row(
|
|
537
537
|
f"[dim]{time_str:8}[/dim]",
|
|
538
538
|
f"[dim]{index_str:4}[/dim]",
|
|
539
|
-
|
|
539
|
+
"[blue]User:[/blue]",
|
|
540
540
|
)
|
|
541
|
+
# 如果有内容,添加内容行
|
|
542
|
+
if line:
|
|
543
|
+
table.add_row("", "", line)
|
|
541
544
|
else:
|
|
542
545
|
# 续行只在内容列显示
|
|
543
546
|
table.add_row("", "", line)
|
|
@@ -631,12 +634,15 @@ def _display_assistant_message_with_columns(
|
|
|
631
634
|
lines = content.split("\n")
|
|
632
635
|
for i, line in enumerate(lines):
|
|
633
636
|
if i == 0:
|
|
634
|
-
#
|
|
637
|
+
# 第一行显示 Assistant: 标签
|
|
635
638
|
table.add_row(
|
|
636
639
|
f"[dim]{time_str:8}[/dim]",
|
|
637
640
|
f"[dim]{index_str:4}[/dim]",
|
|
638
|
-
f"[green]Assistant:[/green]{meta_info}
|
|
641
|
+
f"[green]Assistant:[/green]{meta_info}",
|
|
639
642
|
)
|
|
643
|
+
# 如果有内容,添加内容行
|
|
644
|
+
if line:
|
|
645
|
+
table.add_row("", "", line)
|
|
640
646
|
first_row_added = True
|
|
641
647
|
else:
|
|
642
648
|
# 续行只在内容列显示
|
lite_agent/client.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import os
|
|
3
|
-
from typing import Any, Literal
|
|
3
|
+
from typing import Any, Literal, NotRequired, TypedDict
|
|
4
4
|
|
|
5
5
|
import litellm
|
|
6
6
|
from openai.types.chat import ChatCompletionToolParam
|
|
@@ -8,12 +8,28 @@ from openai.types.responses import FunctionToolParam
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
10
|
ReasoningEffort = Literal["minimal", "low", "medium", "high"]
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ThinkingConfigDict(TypedDict):
|
|
14
|
+
"""Thinking configuration for reasoning models like Claude."""
|
|
15
|
+
|
|
16
|
+
type: Literal["enabled"] # 启用推理
|
|
17
|
+
budget_tokens: NotRequired[int] # 推理令牌预算,可选
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReasoningEffortDict(TypedDict):
|
|
21
|
+
"""Reasoning effort configuration."""
|
|
22
|
+
|
|
23
|
+
effort: ReasoningEffort
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ThinkingConfig = ThinkingConfigDict | None
|
|
12
27
|
|
|
13
28
|
# 统一的推理配置类型
|
|
14
29
|
ReasoningConfig = (
|
|
15
|
-
|
|
16
|
-
|
|
|
30
|
+
ReasoningEffort # "minimal", "low", "medium", "high"
|
|
31
|
+
| ReasoningEffortDict # {"effort": "minimal"}
|
|
32
|
+
| ThinkingConfigDict # {"type": "enabled", "budget_tokens": 2048}
|
|
17
33
|
| bool # True/False 简单开关
|
|
18
34
|
| None # 不启用推理
|
|
19
35
|
)
|
|
@@ -36,8 +52,9 @@ def parse_reasoning_config(reasoning: ReasoningConfig) -> tuple[ReasoningEffort
|
|
|
36
52
|
|
|
37
53
|
Args:
|
|
38
54
|
reasoning: 统一的推理配置
|
|
39
|
-
-
|
|
40
|
-
-
|
|
55
|
+
- ReasoningEffort: "minimal", "low", "medium", "high" -> reasoning_effort
|
|
56
|
+
- ReasoningEffortDict: {"effort": "minimal"} -> reasoning_effort
|
|
57
|
+
- ThinkingConfigDict: {"type": "enabled", "budget_tokens": 2048} -> thinking_config
|
|
41
58
|
- bool: True -> "medium", False -> None
|
|
42
59
|
- None: 不启用推理
|
|
43
60
|
|
|
@@ -46,22 +63,39 @@ def parse_reasoning_config(reasoning: ReasoningConfig) -> tuple[ReasoningEffort
|
|
|
46
63
|
"""
|
|
47
64
|
if reasoning is None:
|
|
48
65
|
return None, None
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
return reasoning, None # type: ignore[return-value]
|
|
54
|
-
return None, None
|
|
55
|
-
if isinstance(reasoning, dict):
|
|
56
|
-
# 字典类型,使用 thinking_config
|
|
57
|
-
return None, reasoning
|
|
66
|
+
|
|
67
|
+
if isinstance(reasoning, str) and reasoning in ("minimal", "low", "medium", "high"):
|
|
68
|
+
return reasoning, None # type: ignore[return-value]
|
|
69
|
+
|
|
58
70
|
if isinstance(reasoning, bool):
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
return ("medium", None) if reasoning else (None, None)
|
|
72
|
+
|
|
73
|
+
if isinstance(reasoning, dict):
|
|
74
|
+
return _parse_dict_reasoning_config(reasoning)
|
|
75
|
+
|
|
76
|
+
# 其他类型或无效格式,默认不启用
|
|
62
77
|
return None, None
|
|
63
78
|
|
|
64
79
|
|
|
80
|
+
def _parse_dict_reasoning_config(reasoning: ReasoningEffortDict | ThinkingConfigDict | dict[str, Any]) -> tuple[ReasoningEffort | None, ThinkingConfig]:
|
|
81
|
+
"""解析字典格式的推理配置。"""
|
|
82
|
+
# 检查是否为 {"effort": "value"} 格式 (ReasoningEffortDict)
|
|
83
|
+
if "effort" in reasoning and len(reasoning) == 1:
|
|
84
|
+
effort = reasoning["effort"]
|
|
85
|
+
if isinstance(effort, str) and effort in ("minimal", "low", "medium", "high"):
|
|
86
|
+
return effort, None # type: ignore[return-value]
|
|
87
|
+
|
|
88
|
+
# 检查是否为 ThinkingConfigDict 格式
|
|
89
|
+
if "type" in reasoning and reasoning.get("type") == "enabled":
|
|
90
|
+
# 验证 ThinkingConfigDict 的结构
|
|
91
|
+
valid_keys = {"type", "budget_tokens"}
|
|
92
|
+
if all(key in valid_keys for key in reasoning):
|
|
93
|
+
return None, reasoning # type: ignore[return-value]
|
|
94
|
+
|
|
95
|
+
# 其他未知字典格式,仍尝试作为 thinking_config
|
|
96
|
+
return None, reasoning # type: ignore[return-value]
|
|
97
|
+
|
|
98
|
+
|
|
65
99
|
class BaseLLMClient(abc.ABC):
|
|
66
100
|
"""Base class for LLM clients."""
|
|
67
101
|
|
|
@@ -233,8 +267,7 @@ class LiteLLMClient(BaseLLMClient):
|
|
|
233
267
|
|
|
234
268
|
# Add reasoning parameters if specified
|
|
235
269
|
if final_reasoning_effort is not None:
|
|
236
|
-
response_params["
|
|
270
|
+
response_params["reasoning"] = {"effort": final_reasoning_effort}
|
|
237
271
|
if final_thinking_config is not None:
|
|
238
272
|
response_params["thinking"] = final_thinking_config
|
|
239
|
-
|
|
240
273
|
return await litellm.aresponses(**response_params) # type: ignore[return-value]
|