aient 1.1.93__py3-none-any.whl → 1.1.95__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 +43 -7
- aient/architext/test/test.py +91 -2
- {aient-1.1.93.dist-info → aient-1.1.95.dist-info}/METADATA +1 -1
- {aient-1.1.93.dist-info → aient-1.1.95.dist-info}/RECORD +7 -7
- {aient-1.1.93.dist-info → aient-1.1.95.dist-info}/WHEEL +0 -0
- {aient-1.1.93.dist-info → aient-1.1.95.dist-info}/licenses/LICENSE +0 -0
- {aient-1.1.93.dist-info → aient-1.1.95.dist-info}/top_level.txt +0 -0
@@ -32,7 +32,10 @@ class ContextProvider(ABC):
|
|
32
32
|
return None
|
33
33
|
|
34
34
|
class Texts(ContextProvider):
|
35
|
-
def __init__(self, text: Union[str, Callable[[], str]], name: Optional[str] = None):
|
35
|
+
def __init__(self, text: Optional[Union[str, Callable[[], str]]] = None, name: Optional[str] = None):
|
36
|
+
if text is None and name is None:
|
37
|
+
raise ValueError("Either 'text' or 'name' must be provided.")
|
38
|
+
|
36
39
|
self._text = text
|
37
40
|
self._is_dynamic = callable(self._text)
|
38
41
|
|
@@ -41,7 +44,8 @@ class Texts(ContextProvider):
|
|
41
44
|
import uuid
|
42
45
|
_name = f"dynamic_text_{uuid.uuid4().hex[:8]}"
|
43
46
|
else:
|
44
|
-
|
47
|
+
# Handle the case where text is None during initialization
|
48
|
+
h = hashlib.sha1(self._text.encode() if self._text else b'').hexdigest()
|
45
49
|
_name = f"text_{h[:8]}"
|
46
50
|
else:
|
47
51
|
_name = name
|
@@ -57,10 +61,13 @@ class Texts(ContextProvider):
|
|
57
61
|
self._is_dynamic = callable(self._text)
|
58
62
|
self.mark_stale()
|
59
63
|
|
60
|
-
async def render(self) -> str:
|
64
|
+
async def render(self) -> Optional[str]:
|
61
65
|
if self._is_dynamic:
|
62
|
-
|
63
|
-
|
66
|
+
# Ensure dynamic content returns a string, even if empty
|
67
|
+
result = self._text()
|
68
|
+
return result if result is not None else ""
|
69
|
+
# Ensure static content returns a string, even if empty
|
70
|
+
return self._text if self._text is not None else ""
|
64
71
|
|
65
72
|
class Tools(ContextProvider):
|
66
73
|
def __init__(self, tools_json: List[Dict]): super().__init__("tools"); self._tools_json = tools_json
|
@@ -197,8 +204,37 @@ class Message(ABC):
|
|
197
204
|
self._parent_messages: Optional['Messages'] = None
|
198
205
|
|
199
206
|
def _render_content(self) -> str:
|
200
|
-
blocks
|
201
|
-
|
207
|
+
# Get all blocks and their provider types first
|
208
|
+
blocks_with_types = []
|
209
|
+
for item in self._items:
|
210
|
+
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)
|
236
|
+
|
237
|
+
return "".join(final_content_parts)
|
202
238
|
|
203
239
|
def pop(self, name: str) -> Optional[ContextProvider]:
|
204
240
|
popped_item = None
|
aient/architext/test/test.py
CHANGED
@@ -143,11 +143,12 @@ class TestContextManagement(unittest.IsolatedAsyncioTestCase):
|
|
143
143
|
popped_image = messages.pop("image")
|
144
144
|
self.assertIsNotNone(popped_image)
|
145
145
|
|
146
|
-
# 3. Render again, should fall back to string content
|
146
|
+
# 3. Render again, should fall back to string content and seamlessly join texts
|
147
147
|
rendered_str = messages.render() # No refresh needed
|
148
148
|
content_str = rendered_str[0]['content']
|
149
149
|
self.assertIsInstance(content_str, str)
|
150
|
-
|
150
|
+
# With the new render logic, adjacent texts are joined with ""
|
151
|
+
self.assertEqual(content_str, "Look at this:Any thoughts?")
|
151
152
|
|
152
153
|
# Clean up
|
153
154
|
import os
|
@@ -872,6 +873,94 @@ class TestContextManagement(unittest.IsolatedAsyncioTestCase):
|
|
872
873
|
# 5. 验证两次内容不同(因为时间戳变了)
|
873
874
|
self.assertNotEqual(content1, content2, "包含静态前缀的动态 provider 内容应该更新")
|
874
875
|
|
876
|
+
async def test_z3_deferred_text_update_via_provider(self):
|
877
|
+
"""测试 Texts(name=...) 初始化, 然后通过 provider 更新内容"""
|
878
|
+
# This test is expected to fail with a TypeError on the next line
|
879
|
+
# because the current Texts.__init__ requires 'text'.
|
880
|
+
deferred_text_provider = Texts(name="deferred_content")
|
881
|
+
|
882
|
+
messages = Messages(UserMessage(deferred_text_provider))
|
883
|
+
|
884
|
+
# Initial render: with no text, it should probably render to an empty string.
|
885
|
+
# If there's no content, the message itself might not be rendered.
|
886
|
+
# Let's assume an empty provider results in the message not rendering.
|
887
|
+
await deferred_text_provider.refresh()
|
888
|
+
# With the new logic, it should return a block with an empty string
|
889
|
+
content_block = deferred_text_provider.get_content_block()
|
890
|
+
self.assertIsNotNone(content_block)
|
891
|
+
self.assertEqual(content_block.content, "")
|
892
|
+
|
893
|
+
|
894
|
+
rendered_initial = await messages.render_latest()
|
895
|
+
self.assertEqual(len(rendered_initial), 0)
|
896
|
+
|
897
|
+
# 3. Get provider and update content
|
898
|
+
provider = messages.provider("deferred_content")
|
899
|
+
self.assertIsNotNone(provider)
|
900
|
+
provider.update("This is the new content.")
|
901
|
+
|
902
|
+
# 4. Re-render and validate
|
903
|
+
rendered_updated = await messages.render_latest()
|
904
|
+
self.assertEqual(len(rendered_updated), 1)
|
905
|
+
self.assertEqual(rendered_updated[0]['content'], "This is the new content.")
|
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 应该为空字符串
|
942
|
+
rendered_initial = await messages.render_latest()
|
943
|
+
expected_initial_content = (
|
944
|
+
"<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"
|
947
|
+
"</user_info>"
|
948
|
+
)
|
949
|
+
self.assertEqual(rendered_initial[0]['content'], expected_initial_content)
|
950
|
+
|
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")
|
955
|
+
|
956
|
+
# 4. 再次渲染,验证 placeholders 是否已填充
|
957
|
+
rendered_final = await messages.render_latest()
|
958
|
+
final_content = rendered_final[0]['content']
|
959
|
+
|
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)
|
963
|
+
|
875
964
|
|
876
965
|
# ==============================================================================
|
877
966
|
# 6. 演示
|
@@ -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=
|
3
|
+
aient/architext/architext/core.py,sha256=arBodL7N3o-YDDgX0I_ZtGlFoQDTXal0L6t9TCP_924,20028
|
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=oF7FyRpfZIMNYbDE9vuQv9uTeV6Q_Hsv9-QoyI0YIRI,47326
|
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.
|
39
|
-
aient-1.1.
|
40
|
-
aient-1.1.
|
41
|
-
aient-1.1.
|
42
|
-
aient-1.1.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|