aient 1.1.95__py3-none-any.whl → 1.1.97__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.
@@ -4,10 +4,28 @@ import asyncio
4
4
  import logging
5
5
  import hashlib
6
6
  import mimetypes
7
+ import uuid
8
+ import threading
7
9
  from dataclasses import dataclass
8
10
  from abc import ABC, abstractmethod
9
11
  from typing import List, Dict, Any, Optional, Union, Callable
10
12
 
13
+ # Global, thread-safe registry for providers created within f-strings
14
+ _fstring_provider_registry = {}
15
+ _registry_lock = threading.Lock()
16
+
17
+ def _register_provider(provider: 'ContextProvider') -> str:
18
+ """Registers a provider and returns a unique placeholder."""
19
+ with _registry_lock:
20
+ provider_id = f"__provider_placeholder_{uuid.uuid4().hex}__"
21
+ _fstring_provider_registry[provider_id] = provider
22
+ return provider_id
23
+
24
+ def _retrieve_provider(placeholder: str) -> Optional['ContextProvider']:
25
+ """Retrieves a provider from the registry."""
26
+ with _registry_lock:
27
+ return _fstring_provider_registry.pop(placeholder, None)
28
+
11
29
  # 1. 核心数据结构: ContentBlock
12
30
  @dataclass
13
31
  class ContentBlock:
@@ -18,6 +36,11 @@ class ContentBlock:
18
36
  class ContextProvider(ABC):
19
37
  def __init__(self, name: str):
20
38
  self.name = name; self._cached_content: Optional[str] = None; self._is_stale: bool = True
39
+
40
+ def __str__(self):
41
+ # This allows the object to be captured when used inside an f-string.
42
+ return _register_provider(self)
43
+
21
44
  def mark_stale(self): self._is_stale = True
22
45
  async def refresh(self):
23
46
  if self._is_stale:
@@ -70,15 +93,20 @@ class Texts(ContextProvider):
70
93
  return self._text if self._text is not None else ""
71
94
 
72
95
  class Tools(ContextProvider):
73
- def __init__(self, tools_json: List[Dict]): super().__init__("tools"); self._tools_json = tools_json
96
+ def __init__(self, tools_json: Optional[List[Dict]] = None, name: str = "tools"):
97
+ super().__init__(name)
98
+ self._tools_json = tools_json or []
74
99
  def update(self, tools_json: List[Dict]):
75
100
  self._tools_json = tools_json
76
101
  self.mark_stale()
77
- async def render(self) -> str: return f"<tools>{str(self._tools_json)}</tools>"
102
+ async def render(self) -> Optional[str]:
103
+ if not self._tools_json:
104
+ return None
105
+ return f"<tools>{str(self._tools_json)}</tools>"
78
106
 
79
107
  class Files(ContextProvider):
80
- def __init__(self, *paths: Union[str, List[str]]):
81
- super().__init__("files")
108
+ def __init__(self, *paths: Union[str, List[str]], name: str = "files"):
109
+ super().__init__(name)
82
110
  self._files: Dict[str, str] = {}
83
111
 
84
112
  file_paths: List[str] = []
@@ -179,7 +207,23 @@ class Message(ABC):
179
207
  processed_items = []
180
208
  for item in initial_items:
181
209
  if isinstance(item, str):
182
- processed_items.append(Texts(text=item))
210
+ # Check if the string contains placeholders from f-string rendering
211
+ import re
212
+ placeholder_pattern = re.compile(r'(__provider_placeholder_[a-f0-9]{32}__)')
213
+ parts = placeholder_pattern.split(item)
214
+
215
+ if len(parts) > 1: # Placeholders were found
216
+ for part in parts:
217
+ if not part: continue
218
+ if placeholder_pattern.match(part):
219
+ provider = _retrieve_provider(part)
220
+ if provider:
221
+ processed_items.append(provider)
222
+ else:
223
+ processed_items.append(Texts(text=part))
224
+ else: # No placeholders, just a regular string
225
+ processed_items.append(Texts(text=item))
226
+
183
227
  elif isinstance(item, Message):
184
228
  processed_items.extend(item.providers())
185
229
  elif isinstance(item, ContextProvider):
@@ -204,37 +248,13 @@ class Message(ABC):
204
248
  self._parent_messages: Optional['Messages'] = None
205
249
 
206
250
  def _render_content(self) -> str:
207
- # Get all blocks and their provider types first
208
- blocks_with_types = []
251
+ final_parts = []
209
252
  for item in self._items:
210
253
  block = item.get_content_block()
211
- if block and (block.content or block.content == ""): # Consider blocks with empty string content
212
- # We only care about non-multimodal Texts providers for concatenation
213
- provider_type = Texts if type(item) is Texts else type(item)
214
- blocks_with_types.append((block, provider_type))
215
-
216
- if not blocks_with_types:
217
- return ""
218
-
219
- # Build the final content string
220
- # Start with the first block's content
221
- final_content_parts = [blocks_with_types[0][0].content]
222
-
223
- for i in range(1, len(blocks_with_types)):
224
- current_block, current_type = blocks_with_types[i]
225
- _, prev_type = blocks_with_types[i-1]
226
-
227
- # If both the previous and current rendered blocks are simple text, concatenate them.
228
- # Otherwise, join with newlines.
229
- if prev_type is Texts and current_type is Texts:
230
- separator = ""
231
- else:
232
- separator = "\n\n"
233
-
234
- final_content_parts.append(separator)
235
- final_content_parts.append(current_block.content)
254
+ if block and block.content is not None:
255
+ final_parts.append(block.content)
236
256
 
237
- return "".join(final_content_parts)
257
+ return "".join(final_parts)
238
258
 
239
259
  def pop(self, name: str) -> Optional[ContextProvider]:
240
260
  popped_item = None
@@ -904,62 +904,99 @@ class TestContextManagement(unittest.IsolatedAsyncioTestCase):
904
904
  self.assertEqual(len(rendered_updated), 1)
905
905
  self.assertEqual(rendered_updated[0]['content'], "This is the new content.")
906
906
 
907
- async def test_z4_fstring_deferred_population(self):
908
- """测试 f-string 内容是否可以延迟填充"""
909
- # 1. 初始化一个带有 f-string 占位符的 UserMessage
910
- # 注意:这里的 f-string 会立即求值,所以我们需要模拟一个假的 Texts 对象
911
- # 让它看起来像是 f-string 的一部分。正确的用法是直接在 f-string 中创建 Texts 对象。
912
- user_info_provider = Texts(name="user_info")
913
-
914
- # This is how the user wants to use it. The f-string is evaluated immediately.
915
- # The Texts object inside it is created but has no text yet.
916
- raw_fstring_text = f"""
917
- <user_info>
918
- The user's OS version is {Texts(name="os_version")}. The absolute path of the user's workspace is {Texts(name="workspace_path")} which is also the project root directory. The user's shell is {Texts(name="shell")}.
919
- </user_info>
920
- """
921
- # The above f-string evaluates to a string containing reprs of Texts objects.
922
- # This is not how the library is designed to work.
923
- # A more correct approach is to build the message programmatically.
924
-
925
- # Let's test the intended-like usage pattern.
926
- # We create a template and providers separately.
927
-
928
- os_provider = Texts(name="os_version")
929
- path_provider = Texts(name="workspace_path")
930
- shell_provider = Texts(name="shell")
931
-
932
- messages = Messages(
933
- UserMessage(
934
- "<user_info>\n"
935
- "The user's OS version is ", os_provider, ". The absolute path of the user's workspace is ",
936
- path_provider, " which is also the project root directory. The user's shell is ", shell_provider, ".\n"
937
- "</user_info>"
938
- )
939
- )
940
-
941
- # 2. 初始渲染,此时 placeholders 应该为空字符串
907
+ async def test_z4_direct_fstring_usage(self):
908
+ """直接使用 f-string 语法,并预期其能够被处理"""
909
+
910
+ # 这个测试将直接使用用户期望的 f-string 语法。
911
+ # 由于 Python 的限制,这行代码会立即对 f-string 求值,
912
+ # 导致 providers 的字符串表示形式(而不是 provider 对象本身)被插入。
913
+ # 因此,这个测试最初会失败。
914
+ f_string_message = f"""<user_info>
915
+ The user's OS version is {Texts(name="os_version")}.
916
+ Tools: {Tools()}
917
+ Files: {Files()}
918
+ Current time: {Texts(name="current_time")}
919
+ </user_info>"""
920
+
921
+ # 借助新的 f-string 处理机制,UserMessage 现在可以直接消费 f-string 的结果。
922
+ messages = Messages(UserMessage(f_string_message))
923
+
924
+ # 初始渲染时,provider 的内容应该为空
942
925
  rendered_initial = await messages.render_latest()
943
- expected_initial_content = (
926
+
927
+ # With the new simplest rendering logic, the output should match the f-string exactly,
928
+ # with empty strings for the providers and no leading whitespace.
929
+ expected_initial = (
944
930
  "<user_info>\n"
945
- "The user's OS version is . The absolute path of the user's workspace is "
946
- " which is also the project root directory. The user's shell is .\n"
931
+ "The user's OS version is .\n"
932
+ "Tools: \n"
933
+ "Files: \n"
934
+ "Current time: \n"
947
935
  "</user_info>"
948
936
  )
949
- self.assertEqual(rendered_initial[0]['content'], expected_initial_content)
937
+ self.assertEqual(rendered_initial[0]['content'].strip(), expected_initial.strip())
950
938
 
951
- # 3. 获取 providers 并更新它们的值
952
- messages.provider("os_version").update("macOS 14.5")
953
- messages.provider("workspace_path").update("/Users/yanyuming/Downloads/GitHub/architext")
954
- messages.provider("shell").update("/bin/zsh")
939
+ # 现在,尝试通过 provider 更新内容。这应该会成功。
940
+ messages.provider("os_version").update("TestOS")
941
+ messages.provider("tools").update([{"name": "test_tool"}])
942
+ messages.provider("current_time").update("2025-12-25")
955
943
 
956
- # 4. 再次渲染,验证 placeholders 是否已填充
957
- rendered_final = await messages.render_latest()
958
- final_content = rendered_final[0]['content']
944
+ test_file = "fstring_test.txt"
945
+ with open(test_file, "w") as f: f.write("content from f-string test")
946
+
947
+ try:
948
+ messages.provider("files").update(test_file)
949
+
950
+ rendered_final = await messages.render_latest()
951
+ final_content = rendered_final[0]['content']
952
+
953
+ # 断言内容已经被成功更新
954
+ tools_str = "<tools>[{'name': 'test_tool'}]</tools>"
955
+ files_str = f"<latest_file_content><file><file_path>fstring_test.txt</file_path><file_content>content from f-string test</file_content></file>\n</latest_file_content>"
956
+
957
+ expected_final = f"""<user_info>
958
+ The user's OS version is TestOS.
959
+ Tools: {tools_str}
960
+ Files: {files_str}
961
+ Current time: 2025-12-25
962
+ </user_info>"""
963
+ self.assertEqual(final_content.strip(), expected_final.strip())
964
+ finally:
965
+ if os.path.exists(test_file):
966
+ os.remove(test_file)
967
+
968
+ async def test_z5_fstring_with_dynamic_lambda(self):
969
+ """测试 f-string 消息是否支持动态 lambda 函数"""
970
+ from datetime import datetime
971
+ import time
972
+
973
+ # 这个测试将验证 f-string 是否能正确处理包含 lambda 的动态 provider
974
+ f_string_message = f"""<user_info>
975
+ The user's OS version is {Texts(name="os_version")}.
976
+ Current time: {Texts(lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"))}
977
+ </user_info>"""
978
+
979
+ messages = Messages(UserMessage(f_string_message))
980
+ messages.provider("os_version").update("TestOS")
981
+
982
+ # 第一次渲染
983
+ rendered1 = await messages.render_latest()
984
+ content1 = rendered1[0]['content']
985
+ self.assertIn("TestOS", content1)
986
+
987
+ time1_str_part = content1.split("Current time:")[1].strip().split("\n")[0]
988
+
989
+
990
+ # 等待一秒
991
+ time.sleep(1)
992
+
993
+ # 第二次渲染
994
+ rendered2 = await messages.render_latest()
995
+ content2 = rendered2[0]['content']
996
+ time2_str_part = content2.split("Current time:")[1].strip().split("\n")[0]
959
997
 
960
- self.assertIn("The user's OS version is macOS 14.5.", final_content)
961
- self.assertIn("The absolute path of the user's workspace is /Users/yanyuming/Downloads/GitHub/architext which is also the project root directory.", final_content)
962
- self.assertIn("The user's shell is /bin/zsh.", final_content)
998
+ # 验证两次渲染的时间戳不同
999
+ self.assertNotEqual(time1_str_part, time2_str_part, "f-string 中的动态 lambda 内容在两次渲染之间应该更新")
963
1000
 
964
1001
 
965
1002
  # ==============================================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aient
3
- Version: 1.1.95
3
+ Version: 1.1.97
4
4
  Summary: Aient: The Awakening of Agent.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,8 +1,8 @@
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=arBodL7N3o-YDDgX0I_ZtGlFoQDTXal0L6t9TCP_924,20028
3
+ aient/architext/architext/core.py,sha256=-uu6CelQyKJQ7e6xTZWRxnC-yvxeLiIl8siQdXcL8RI,20727
4
4
  aient/architext/test/openai_client.py,sha256=Dqtbmubv6vwF8uBqcayG0kbsiO65of7sgU2-DRBi-UM,4590
5
- aient/architext/test/test.py,sha256=oF7FyRpfZIMNYbDE9vuQv9uTeV6Q_Hsv9-QoyI0YIRI,47326
5
+ aient/architext/test/test.py,sha256=URVDaUfxxPU4yobojJC7SO1cBsM0Hzz96ZW3lYYMWGo,48176
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
@@ -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=UcSzKkFE4-h_1b6NofI6xgk3GoleqALRKY8VBaXLjmI,11311
37
37
  aient/utils/scripts.py,sha256=VqtK4RFEx7KxkmcqG3lFDS1DxoNlFFGErEjopVcc8IE,40974
38
- aient-1.1.95.dist-info/licenses/LICENSE,sha256=XNdbcWldt0yaNXXWB_Bakoqnxb3OVhUft4MgMA_71ds,1051
39
- aient-1.1.95.dist-info/METADATA,sha256=RqDv7-DjbGD_PUbKUqrEqcYbLQoXvOyRM7xafg-hrKQ,4842
40
- aient-1.1.95.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
- aient-1.1.95.dist-info/top_level.txt,sha256=3oXzrP5sAVvyyqabpeq8A2_vfMtY554r4bVE-OHBrZk,6
42
- aient-1.1.95.dist-info/RECORD,,
38
+ aient-1.1.97.dist-info/licenses/LICENSE,sha256=XNdbcWldt0yaNXXWB_Bakoqnxb3OVhUft4MgMA_71ds,1051
39
+ aient-1.1.97.dist-info/METADATA,sha256=iNrH1fwUGshVwM3UmFmun1CV7qPH61FPl1XpUmMemyo,4842
40
+ aient-1.1.97.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
+ aient-1.1.97.dist-info/top_level.txt,sha256=3oXzrP5sAVvyyqabpeq8A2_vfMtY554r4bVE-OHBrZk,6
42
+ aient-1.1.97.dist-info/RECORD,,
File without changes