aient 1.2.35__py3-none-any.whl → 1.2.36__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.
@@ -324,19 +324,21 @@ class Images(ContextProvider):
324
324
 
325
325
  # 3. 消息类 (已合并 MessageContent)
326
326
  class Message(ABC):
327
- def __init__(self, role: str, *initial_items: Union[ContextProvider, str, list]):
327
+ def __init__(self, role: str, *initial_items: Union[ContextProvider, str, list, 'Message']):
328
328
  self.role = role
329
329
  processed_items = []
330
330
  for item in initial_items:
331
331
  if item is None:
332
332
  continue
333
- if isinstance(item, str):
334
- # Check if the string contains placeholders from f-string rendering
333
+
334
+ # This is the new recursive flattening logic
335
+ if isinstance(item, Message):
336
+ processed_items.extend(item.provider())
337
+ elif isinstance(item, str):
335
338
  import re
336
339
  placeholder_pattern = re.compile(r'(__provider_placeholder_[a-f0-9]{32}__)')
337
340
  parts = placeholder_pattern.split(item)
338
-
339
- if len(parts) > 1: # Placeholders were found
341
+ if len(parts) > 1:
340
342
  for part in parts:
341
343
  if not part: continue
342
344
  if placeholder_pattern.match(part):
@@ -345,18 +347,14 @@ class Message(ABC):
345
347
  processed_items.append(provider)
346
348
  else:
347
349
  processed_items.append(Texts(text=part))
348
- else: # No placeholders, just a regular string
350
+ else:
349
351
  processed_items.append(Texts(text=item))
350
-
351
- elif isinstance(item, Message):
352
- processed_items.extend(item.provider())
353
352
  elif isinstance(item, ContextProvider):
354
353
  processed_items.append(item)
355
354
  elif isinstance(item, list):
356
355
  for sub_item in item:
357
356
  if not isinstance(sub_item, dict) or 'type' not in sub_item:
358
357
  raise ValueError("List items must be dicts with a 'type' key.")
359
-
360
358
  item_type = sub_item['type']
361
359
  if item_type == 'text':
362
360
  processed_items.append(Texts(text=sub_item.get('text', '')))
@@ -519,10 +517,27 @@ class Message(ABC):
519
517
  """提供类似字典的 .get() 方法来访问属性。"""
520
518
  return getattr(self, key, default)
521
519
 
522
- async def render_latest(self) -> Optional[Dict[str, Any]]:
523
- """Refreshes all providers in the message and returns the rendered dictionary."""
520
+ async def refresh(self):
521
+ """刷新此消息中的所有 provider。"""
524
522
  tasks = [provider.refresh() for provider in self._items]
525
523
  await asyncio.gather(*tasks)
524
+
525
+ async def render(self) -> Optional[Dict[str, Any]]:
526
+ """
527
+ 渲染消息为字典。首次调用时会隐式刷新以确保动态内容被加载。
528
+ 后续调用将返回缓存版本,除非手动调用了 refresh()。
529
+ """
530
+ # 检查是否是首次渲染
531
+ is_first_render = not all(hasattr(p, '_cached_content') and p._cached_content is not None for p in self._items if p._is_stale)
532
+
533
+ if is_first_render:
534
+ await self.refresh()
535
+
536
+ return self.to_dict()
537
+
538
+ async def render_latest(self) -> Optional[Dict[str, Any]]:
539
+ """始终刷新并返回最新的渲染结果。"""
540
+ await self.refresh()
526
541
  return self.to_dict()
527
542
 
528
543
  def to_dict(self) -> Optional[Dict[str, Any]]:
@@ -595,12 +610,20 @@ class ToolCalls(Message):
595
610
 
596
611
  class ToolResults(Message):
597
612
  """Represents a tool message with the result of a single tool call."""
598
- def __init__(self, tool_call_id: str, content: str):
599
- # We pass a Texts provider to the parent so it can be rendered,
600
- # but the primary way to access content for ToolResults is via its dict representation.
601
- super().__init__("tool", Texts(text=content))
613
+ def __init__(self, tool_call_id: str, content: Union[str, Message]):
614
+ # The base Message class now handles the absorption of a Message object.
615
+ # We just need to pass the content to the parent __init__.
616
+ # For ToolResults, we primarily care about the textual content.
617
+ if isinstance(content, Message):
618
+ # Extract only text-like providers to pass to the parent
619
+ text_providers = [p for p in content.provider() if not isinstance(p, Images)]
620
+ super().__init__("tool", *text_providers)
621
+ else:
622
+ super().__init__("tool", content)
623
+
602
624
  self.tool_call_id = tool_call_id
603
- self._content = content
625
+ # After initialization, render the content to a simple string for _content.
626
+ self._content = self._render_content()
604
627
 
605
628
  def to_dict(self) -> Dict[str, Any]:
606
629
  return {
@@ -780,7 +803,8 @@ class Messages:
780
803
 
781
804
  def __len__(self) -> int: return len(self._messages)
782
805
  def __iter__(self): return iter(self._messages)
783
-
806
+ def __repr__(self):
807
+ return f"Messages({repr(self._messages)})"
784
808
  def __contains__(self, item: Any) -> bool:
785
809
  """Checks if a Message or ContextProvider is in the collection."""
786
810
  if isinstance(item, Message):
@@ -1598,6 +1598,117 @@ Files: {Files(visible=True, name="files")}
1598
1598
  self.assertEqual(content[1]['type'], 'image_url')
1599
1599
  self.assertEqual(content[1]['image_url']['url'], "data:image/png;base64,FAKE")
1600
1600
 
1601
+ async def test_zae_messages_representation(self):
1602
+ """测试 Messages 对象的 __repr__ 方法是否提供可读的输出"""
1603
+ messages = Messages(
1604
+ UserMessage("Hello"),
1605
+ AssistantMessage("Hi there!")
1606
+ )
1607
+
1608
+ actual_repr = repr(messages)
1609
+
1610
+ # 一个可读的字符串形式应该像 Messages([...]) 这样,并包含其内部 message 的 repr
1611
+ self.assertTrue(actual_repr.startswith("Messages(["), f"期望输出以 'Messages([' 开头,但得到 '{actual_repr}'")
1612
+ self.assertTrue(actual_repr.endswith("])"), f"期望输出以 '])' 结尾,但得到 '{actual_repr}'")
1613
+ self.assertIn("Message(role='user', items=", actual_repr)
1614
+ self.assertIn("Message(role='assistant', items=", actual_repr)
1615
+
1616
+ async def test_zaf_message_absorption(self):
1617
+ """测试Message对象是否能吸收嵌套的Message对象作为其内容"""
1618
+ # 1. ToolResults吸收UserMessage
1619
+ tool_results_1 = ToolResults(tool_call_id="call_1", content=UserMessage("hi"))
1620
+ rendered_1 = await tool_results_1.render_latest()
1621
+ self.assertEqual(rendered_1['content'], "hi")
1622
+ self.assertEqual(rendered_1['tool_call_id'], "call_1")
1623
+
1624
+ # 2. UserMessage吸收AssistantMessage
1625
+ user_message_1 = UserMessage("prefix", AssistantMessage("absorbed content"))
1626
+ rendered_user_1 = await user_message_1.render_latest()
1627
+ self.assertEqual(rendered_user_1['content'], "prefixabsorbed content")
1628
+ self.assertEqual(len(user_message_1.provider()), 2) # Should be flattened
1629
+
1630
+ # 3. 复杂嵌套
1631
+ final_message = ToolResults(tool_call_id="call_final", content=UserMessage("A", AssistantMessage("B", UserMessage("C"))))
1632
+ rendered_final = await final_message.render_latest()
1633
+ self.assertEqual(rendered_final['content'], "ABC")
1634
+
1635
+ # 4. 组合情况: ToolResults(UserMessage(Texts("a"), Texts("b"))) -> content="ab"
1636
+ tool_results_2 = ToolResults(tool_call_id="call_2", content=UserMessage(Texts("a"), Texts("b")))
1637
+ rendered_2 = await tool_results_2.render_latest()
1638
+ self.assertEqual(rendered_2['content'], "ab")
1639
+
1640
+ # 5. 包含多模态内容的情况 (ToolResults应该只提取文本)
1641
+ tool_results_3 = ToolResults(tool_call_id="call_3", content=UserMessage("text part", Images(url="some_url")))
1642
+ rendered_3 = await tool_results_3.render_latest()
1643
+ self.assertEqual(rendered_3['content'], "text part") # Images should be ignored
1644
+
1645
+ # 6. 直接传入字符串的情况应保持不变
1646
+ tool_results_4 = ToolResults(tool_call_id="call_4", content="just a string")
1647
+ rendered_4 = await tool_results_4.render_latest()
1648
+ self.assertEqual(rendered_4['content'], "just a string")
1649
+
1650
+ # 7. 传入一个空的 Message
1651
+ tool_results_5 = ToolResults(tool_call_id="call_5", content=UserMessage())
1652
+ rendered_5 = await tool_results_5.render_latest()
1653
+ self.assertEqual(rendered_5['content'], "")
1654
+
1655
+ async def test_zzb_final_message_render_logic(self):
1656
+ """
1657
+ 最终版测试:
1658
+ - render() 首次调用保证获取完整结果。
1659
+ - 后续 render() 调用返回缓存结果,不会自动刷新。
1660
+ - refresh() 显式刷新内容。
1661
+ - render_latest() 总是获取最新内容。
1662
+ """
1663
+ from datetime import datetime
1664
+ import time
1665
+
1666
+ # 1. 创建带有动态 provider 的 Message
1667
+ timestamp_provider = Texts(lambda: str(datetime.now().timestamp()))
1668
+ message = UserMessage("Time: ", timestamp_provider)
1669
+
1670
+ # 2. 第一次调用 render() - 应该刷新并返回完整内容
1671
+ rendered_1 = await message.render()
1672
+ content1 = rendered_1['content']
1673
+ timestamp1_str = content1.replace("Time: ", "")
1674
+ self.assertTrue(timestamp1_str, "render() 第一次调用应返回完整动态内容")
1675
+
1676
+ # 3. 第二次调用 render() - 不应自动刷新,返回缓存的内容
1677
+ time.sleep(1)
1678
+ rendered_2 = await message.render()
1679
+ content2 = rendered_2['content']
1680
+ self.assertEqual(content1, content2, "第二次调用 render() 应返回缓存内容,不应刷新")
1681
+
1682
+ # 4. 调用 refresh() - 显式刷新
1683
+ time.sleep(1)
1684
+ await message.refresh()
1685
+
1686
+ # 5. refresh() 后调用 render() - 应该返回刚刚刷新的新内容
1687
+ rendered_3 = await message.render()
1688
+ content3 = rendered_3['content']
1689
+ timestamp3_str = content3.replace("Time: ", "")
1690
+ self.assertNotEqual(content2, content3, "refresh() 后 render() 应返回新内容")
1691
+
1692
+ # 6. 调用 render_latest() - 总是获取最新内容
1693
+ time.sleep(1)
1694
+ rendered_latest = await message.render_latest()
1695
+ content_latest = rendered_latest['content']
1696
+ timestamp_latest_str = content_latest.replace("Time: ", "")
1697
+ self.assertNotEqual(content3, content_latest, "render_latest() 应总是获取最新内容")
1698
+
1699
+ # 7. 测试 ToolResults
1700
+ tool_results_msg = ToolResults(tool_call_id="call_123", content="Result from tool")
1701
+ # ToolResults's content is static, so render() should always return the same full content
1702
+ rendered_tool_1 = await tool_results_msg.render()
1703
+ self.assertEqual(rendered_tool_1, {
1704
+ "role": "tool",
1705
+ "tool_call_id": "call_123",
1706
+ "content": "Result from tool"
1707
+ })
1708
+ # Subsequent calls should also work
1709
+ rendered_tool_2 = await tool_results_msg.render()
1710
+ self.assertEqual(rendered_tool_1, rendered_tool_2)
1711
+
1601
1712
 
1602
1713
  # ==============================================================================
1603
1714
  # 6. 演示
aient/core/response.py CHANGED
@@ -45,8 +45,8 @@ async def gemini_json_poccess(response_json):
45
45
  if is_thinking:
46
46
  content = safe_get(json_data, "parts", 1, "text", default="")
47
47
 
48
- function_call_name = safe_get(json_data, "functionCall", "name", default=None)
49
- function_full_response = safe_get(json_data, "functionCall", "args", default="")
48
+ function_call_name = safe_get(json_data, "parts", 0, "functionCall", "name", default=None)
49
+ function_full_response = safe_get(json_data, "parts", 0, "functionCall", "args", default="")
50
50
  function_full_response = await asyncio.to_thread(json.dumps, function_full_response) if function_full_response else None
51
51
 
52
52
  blockReason = safe_get(json_data, 0, "promptFeedback", "blockReason", default=None)
aient/models/chatgpt.py CHANGED
@@ -435,7 +435,7 @@ class chatgpt(BaseLLM):
435
435
  for chunk in process_sync():
436
436
  yield chunk
437
437
 
438
- if not full_response.strip():
438
+ if not full_response.strip() and not need_function_call:
439
439
  raise EmptyResponseError("Response is empty")
440
440
 
441
441
  if self.print_log:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aient
3
- Version: 1.2.35
3
+ Version: 1.2.36
4
4
  Summary: Aient: The Awakening of Agent.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,14 +1,14 @@
1
1
  aient/__init__.py,sha256=SRfF7oDVlOOAi6nGKiJIUK6B_arqYLO9iSMp-2IZZps,21
2
2
  aient/architext/architext/__init__.py,sha256=79Ih1151rfcqZdr7F8HSZSTs_iT2SKd1xCkehMsXeXs,19
3
- aient/architext/architext/core.py,sha256=XKMYDLdaTP6OWmNZLEF-b24KVDkMeM-xyVptmpXoBsU,33112
3
+ aient/architext/architext/core.py,sha256=k5Sza5mrIuv_34T0qUmnFGqAZMFwWhudO96qVlnyA8c,34135
4
4
  aient/architext/test/openai_client.py,sha256=Dqtbmubv6vwF8uBqcayG0kbsiO65of7sgU2-DRBi-UM,4590
5
- aient/architext/test/test.py,sha256=jxmEfZaeJWh3dq0f67TBTPMgT0FMK7eiTs9XcyAtFFg,73944
5
+ aient/architext/test/test.py,sha256=pYDGN0Dvy5teeMcuglP4cQaESfjYdKf2Zx5m67cRPjA,79539
6
6
  aient/architext/test/test_save_load.py,sha256=o8DqH6gDYZkFkQy-a7blqLtJTRj5e4a-Lil48pJ0V3g,3260
7
7
  aient/core/__init__.py,sha256=NxjebTlku35S4Dzr16rdSqSTWUvvwEeACe8KvHJnjPg,34
8
8
  aient/core/log_config.py,sha256=kz2_yJv1p-o3lUQOwA3qh-LSc3wMHv13iCQclw44W9c,274
9
9
  aient/core/models.py,sha256=KMlCRLjtq1wQHZTJGqnbWhPS2cHq6eLdnk7peKDrzR8,7490
10
10
  aient/core/request.py,sha256=-KEBd4jWLVC9QYUhb1ZfgkLf4nKE7HKL0A58iULkY7o,76757
11
- aient/core/response.py,sha256=Z9geTfh2LkGHKAqjelgeleQtfOAYIyM82t9AVB4xsgE,36407
11
+ aient/core/response.py,sha256=nwJcqThjqUdcbLnmE-2xfPCetAwo3US4nYHLQjnzNv4,36431
12
12
  aient/core/utils.py,sha256=Z8vTH9w3uS8uubBa65c_aJ11A3OKGYEzm4q0brNZDSk,31594
13
13
  aient/core/test/test_base_api.py,sha256=pWnycRJbuPSXKKU9AQjWrMAX1wiLC_014Qc9hh5C2Pw,524
14
14
  aient/core/test/test_geminimask.py,sha256=HFX8jDbNg_FjjgPNxfYaR-0-roUrOO-ND-FVsuxSoiw,13254
@@ -17,7 +17,7 @@ aient/core/test/test_payload.py,sha256=8jBiJY1uidm1jzL-EiK0s6UGmW9XkdsuuKFGrwFhF
17
17
  aient/models/__init__.py,sha256=ZTiZgbfBPTjIPSKURE7t6hlFBVLRS9lluGbmqc1WjxQ,43
18
18
  aient/models/audio.py,sha256=FNW4lxG1IhxOU7L8mvcbaeC1nXk_lpUZQlg9ijQ0h_Q,1937
19
19
  aient/models/base.py,sha256=HWIGfa2A7OTccvHK0wG1-UlHB-yaWRC7hbi4oR1Mu1Y,7228
20
- aient/models/chatgpt.py,sha256=cRz_6ocQbEoXREAlr9HteddfIRicE0c8lV_fnWhTXcA,43574
20
+ aient/models/chatgpt.py,sha256=DOm0_tieWj8V_xIQCZy_33PGCebZ8nvKIXXYpzYWw6Y,43601
21
21
  aient/plugins/__init__.py,sha256=p3KO6Aa3Lupos4i2SjzLQw1hzQTigOAfEHngsldrsyk,986
22
22
  aient/plugins/arXiv.py,sha256=yHjb6PS3GUWazpOYRMKMzghKJlxnZ5TX8z9F6UtUVow,1461
23
23
  aient/plugins/config.py,sha256=TGgZ5SnNKZ8MmdznrZ-TEq7s2ulhAAwTSKH89bci3dA,7079
@@ -35,8 +35,8 @@ aient/plugins/write_file.py,sha256=Jt8fOEwqhYiSWpCbwfAr1xoi_BmFnx3076GMhuL06uI,3
35
35
  aient/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  aient/utils/prompt.py,sha256=ZvGAt_ImJ_CGbDnWgpsWskfSV5fCkpFKRpNQjYL7M7s,11100
37
37
  aient/utils/scripts.py,sha256=Q0tS7E9AmdikO7GeDBd_3Ii5opXHCvKjDGqHsXen6_A,40622
38
- aient-1.2.35.dist-info/licenses/LICENSE,sha256=XNdbcWldt0yaNXXWB_Bakoqnxb3OVhUft4MgMA_71ds,1051
39
- aient-1.2.35.dist-info/METADATA,sha256=Gi2MON1pLGbUcWLcKsE9bdI_3yKomVlbvoOAdWVgDCI,4842
40
- aient-1.2.35.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
- aient-1.2.35.dist-info/top_level.txt,sha256=3oXzrP5sAVvyyqabpeq8A2_vfMtY554r4bVE-OHBrZk,6
42
- aient-1.2.35.dist-info/RECORD,,
38
+ aient-1.2.36.dist-info/licenses/LICENSE,sha256=XNdbcWldt0yaNXXWB_Bakoqnxb3OVhUft4MgMA_71ds,1051
39
+ aient-1.2.36.dist-info/METADATA,sha256=saoXdXmCNTcRpt6pY1xgr_SpqNHFfglwjZIBzjVmmAs,4842
40
+ aient-1.2.36.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
+ aient-1.2.36.dist-info/top_level.txt,sha256=3oXzrP5sAVvyyqabpeq8A2_vfMtY554r4bVE-OHBrZk,6
42
+ aient-1.2.36.dist-info/RECORD,,
File without changes