aient 1.1.94__tar.gz → 1.1.96__tar.gz
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-1.1.94 → aient-1.1.96}/PKG-INFO +1 -1
- {aient-1.1.94 → aient-1.1.96}/aient/architext/architext/core.py +59 -7
- {aient-1.1.94 → aient-1.1.96}/aient/architext/test/test.py +102 -3
- {aient-1.1.94 → aient-1.1.96}/aient.egg-info/PKG-INFO +1 -1
- {aient-1.1.94 → aient-1.1.96}/pyproject.toml +1 -1
- {aient-1.1.94 → aient-1.1.96}/LICENSE +0 -0
- {aient-1.1.94 → aient-1.1.96}/README.md +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/__init__.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/architext/architext/__init__.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/architext/test/openai_client.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/architext/test/test_save_load.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/core/__init__.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/core/log_config.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/core/models.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/core/request.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/core/response.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/core/test/test_base_api.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/core/test/test_geminimask.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/core/test/test_image.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/core/test/test_payload.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/core/utils.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/models/__init__.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/models/audio.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/models/base.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/models/chatgpt.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/__init__.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/arXiv.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/config.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/excute_command.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/get_time.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/image.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/list_directory.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/read_file.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/read_image.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/readonly.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/registry.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/run_python.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/websearch.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/plugins/write_file.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/utils/__init__.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/utils/prompt.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient/utils/scripts.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient.egg-info/SOURCES.txt +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient.egg-info/dependency_links.txt +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient.egg-info/requires.txt +0 -0
- {aient-1.1.94 → aient-1.1.96}/aient.egg-info/top_level.txt +0 -0
- {aient-1.1.94 → aient-1.1.96}/setup.cfg +0 -0
- {aient-1.1.94 → aient-1.1.96}/test/test_Web_crawler.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/test/test_ddg_search.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/test/test_google_search.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/test/test_ollama.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/test/test_plugin.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/test/test_search.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/test/test_url.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/test/test_whisper.py +0 -0
- {aient-1.1.94 → aient-1.1.96}/test/test_yjh.py +0 -0
@@ -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:
|
@@ -63,15 +86,23 @@ class Texts(ContextProvider):
|
|
63
86
|
|
64
87
|
async def render(self) -> Optional[str]:
|
65
88
|
if self._is_dynamic:
|
66
|
-
|
67
|
-
|
89
|
+
# Ensure dynamic content returns a string, even if empty
|
90
|
+
result = self._text()
|
91
|
+
return result if result is not None else ""
|
92
|
+
# Ensure static content returns a string, even if empty
|
93
|
+
return self._text if self._text is not None else ""
|
68
94
|
|
69
95
|
class Tools(ContextProvider):
|
70
|
-
def __init__(self, tools_json: List[Dict]
|
96
|
+
def __init__(self, tools_json: Optional[List[Dict]] = None):
|
97
|
+
super().__init__("tools")
|
98
|
+
self._tools_json = tools_json or []
|
71
99
|
def update(self, tools_json: List[Dict]):
|
72
100
|
self._tools_json = tools_json
|
73
101
|
self.mark_stale()
|
74
|
-
async def render(self) -> str:
|
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>"
|
75
106
|
|
76
107
|
class Files(ContextProvider):
|
77
108
|
def __init__(self, *paths: Union[str, List[str]]):
|
@@ -176,7 +207,23 @@ class Message(ABC):
|
|
176
207
|
processed_items = []
|
177
208
|
for item in initial_items:
|
178
209
|
if isinstance(item, str):
|
179
|
-
|
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
|
+
|
180
227
|
elif isinstance(item, Message):
|
181
228
|
processed_items.extend(item.providers())
|
182
229
|
elif isinstance(item, ContextProvider):
|
@@ -201,8 +248,13 @@ class Message(ABC):
|
|
201
248
|
self._parent_messages: Optional['Messages'] = None
|
202
249
|
|
203
250
|
def _render_content(self) -> str:
|
204
|
-
|
205
|
-
|
251
|
+
final_parts = []
|
252
|
+
for item in self._items:
|
253
|
+
block = item.get_content_block()
|
254
|
+
if block and block.content is not None:
|
255
|
+
final_parts.append(block.content)
|
256
|
+
|
257
|
+
return "".join(final_parts)
|
206
258
|
|
207
259
|
def pop(self, name: str) -> Optional[ContextProvider]:
|
208
260
|
popped_item = None
|
@@ -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
|
@@ -884,7 +885,11 @@ class TestContextManagement(unittest.IsolatedAsyncioTestCase):
|
|
884
885
|
# If there's no content, the message itself might not be rendered.
|
885
886
|
# Let's assume an empty provider results in the message not rendering.
|
886
887
|
await deferred_text_provider.refresh()
|
887
|
-
|
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
|
+
|
888
893
|
|
889
894
|
rendered_initial = await messages.render_latest()
|
890
895
|
self.assertEqual(len(rendered_initial), 0)
|
@@ -899,6 +904,100 @@ class TestContextManagement(unittest.IsolatedAsyncioTestCase):
|
|
899
904
|
self.assertEqual(len(rendered_updated), 1)
|
900
905
|
self.assertEqual(rendered_updated[0]['content'], "This is the new content.")
|
901
906
|
|
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 的内容应该为空
|
925
|
+
rendered_initial = await messages.render_latest()
|
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 = (
|
930
|
+
"<user_info>\n"
|
931
|
+
"The user's OS version is .\n"
|
932
|
+
"Tools: \n"
|
933
|
+
"Files: \n"
|
934
|
+
"Current time: \n"
|
935
|
+
"</user_info>"
|
936
|
+
)
|
937
|
+
self.assertEqual(rendered_initial[0]['content'].strip(), expected_initial.strip())
|
938
|
+
|
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")
|
943
|
+
|
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]
|
997
|
+
|
998
|
+
# 验证两次渲染的时间戳不同
|
999
|
+
self.assertNotEqual(time1_str_part, time2_str_part, "f-string 中的动态 lambda 内容在两次渲染之间应该更新")
|
1000
|
+
|
902
1001
|
|
903
1002
|
# ==============================================================================
|
904
1003
|
# 6. 演示
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|