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.
- aient/architext/architext/core.py +42 -18
- aient/architext/test/test.py +111 -0
- aient/core/response.py +2 -2
- aient/models/chatgpt.py +1 -1
- {aient-1.2.35.dist-info → aient-1.2.36.dist-info}/METADATA +1 -1
- {aient-1.2.35.dist-info → aient-1.2.36.dist-info}/RECORD +9 -9
- {aient-1.2.35.dist-info → aient-1.2.36.dist-info}/WHEEL +0 -0
- {aient-1.2.35.dist-info → aient-1.2.36.dist-info}/licenses/LICENSE +0 -0
- {aient-1.2.35.dist-info → aient-1.2.36.dist-info}/top_level.txt +0 -0
@@ -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
|
-
|
334
|
-
|
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:
|
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
|
523
|
-
"""
|
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
|
-
#
|
600
|
-
#
|
601
|
-
|
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
|
-
|
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):
|
aient/architext/test/test.py
CHANGED
@@ -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
@@ -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=
|
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=
|
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=
|
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=
|
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.
|
39
|
-
aient-1.2.
|
40
|
-
aient-1.2.
|
41
|
-
aient-1.2.
|
42
|
-
aient-1.2.
|
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
|
File without changes
|
File without changes
|