aient 1.1.89__tar.gz → 1.1.91__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.89 → aient-1.1.91}/PKG-INFO +1 -1
- aient-1.1.91/aient/architext/architext/__init__.py +1 -0
- aient-1.1.91/aient/architext/architext/core.py +237 -0
- aient-1.1.91/aient/architext/test.py +226 -0
- {aient-1.1.89 → aient-1.1.91}/aient.egg-info/PKG-INFO +1 -1
- {aient-1.1.89 → aient-1.1.91}/aient.egg-info/SOURCES.txt +3 -0
- {aient-1.1.89 → aient-1.1.91}/pyproject.toml +1 -1
- {aient-1.1.89 → aient-1.1.91}/LICENSE +0 -0
- {aient-1.1.89 → aient-1.1.91}/README.md +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/__init__.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/core/__init__.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/core/log_config.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/core/models.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/core/request.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/core/response.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/core/test/test_base_api.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/core/test/test_geminimask.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/core/test/test_image.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/core/test/test_payload.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/core/utils.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/models/__init__.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/models/audio.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/models/base.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/models/chatgpt.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/__init__.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/arXiv.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/config.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/excute_command.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/get_time.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/image.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/list_directory.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/read_file.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/read_image.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/readonly.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/registry.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/run_python.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/websearch.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/plugins/write_file.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/utils/__init__.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/utils/prompt.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient/utils/scripts.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient.egg-info/dependency_links.txt +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient.egg-info/requires.txt +0 -0
- {aient-1.1.89 → aient-1.1.91}/aient.egg-info/top_level.txt +0 -0
- {aient-1.1.89 → aient-1.1.91}/setup.cfg +0 -0
- {aient-1.1.89 → aient-1.1.91}/test/test_Web_crawler.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/test/test_ddg_search.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/test/test_google_search.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/test/test_ollama.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/test/test_plugin.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/test/test_search.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/test/test_url.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/test/test_whisper.py +0 -0
- {aient-1.1.89 → aient-1.1.91}/test/test_yjh.py +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
from .core import *
|
@@ -0,0 +1,237 @@
|
|
1
|
+
import base64
|
2
|
+
import asyncio
|
3
|
+
import logging
|
4
|
+
import mimetypes
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from abc import ABC, abstractmethod
|
7
|
+
from typing import List, Dict, Any, Optional
|
8
|
+
|
9
|
+
# 1. 核心数据结构: ContentBlock
|
10
|
+
@dataclass
|
11
|
+
class ContentBlock:
|
12
|
+
name: str
|
13
|
+
content: str
|
14
|
+
|
15
|
+
# 2. 上下文提供者 (带缓存)
|
16
|
+
class ContextProvider(ABC):
|
17
|
+
def __init__(self, name: str):
|
18
|
+
self.name = name; self._cached_content: Optional[str] = None; self._is_stale: bool = True
|
19
|
+
def mark_stale(self): self._is_stale = True
|
20
|
+
async def refresh(self):
|
21
|
+
if self._is_stale:
|
22
|
+
self._cached_content = await self._fetch_content()
|
23
|
+
self._is_stale = False
|
24
|
+
@abstractmethod
|
25
|
+
async def _fetch_content(self) -> Optional[str]: raise NotImplementedError
|
26
|
+
def get_content_block(self) -> Optional[ContentBlock]:
|
27
|
+
if self._cached_content is not None: return ContentBlock(self.name, self._cached_content)
|
28
|
+
return None
|
29
|
+
|
30
|
+
class Texts(ContextProvider):
|
31
|
+
def __init__(self, name: str, text: str): super().__init__(name); self._text = text
|
32
|
+
async def _fetch_content(self) -> str: return self._text
|
33
|
+
|
34
|
+
class Tools(ContextProvider):
|
35
|
+
def __init__(self, tools_json: List[Dict]): super().__init__("tools"); self._tools_json = tools_json
|
36
|
+
async def _fetch_content(self) -> str: return f"<tools>{str(self._tools_json)}</tools>"
|
37
|
+
|
38
|
+
class Files(ContextProvider):
|
39
|
+
def __init__(self): super().__init__("files"); self._files: Dict[str, str] = {}
|
40
|
+
def update(self, path: str, content: str): self._files[path] = content; self.mark_stale()
|
41
|
+
async def _fetch_content(self) -> str:
|
42
|
+
if not self._files: return None
|
43
|
+
return "<files>\n" + "\n".join([f"<file path='{p}'>{c}...</file>" for p, c in self._files.items()]) + "\n</files>"
|
44
|
+
|
45
|
+
class Images(ContextProvider):
|
46
|
+
def __init__(self, image_path: str, name: Optional[str] = None):
|
47
|
+
super().__init__(name or image_path)
|
48
|
+
self.image_path = image_path
|
49
|
+
async def _fetch_content(self) -> Optional[str]:
|
50
|
+
try:
|
51
|
+
with open(self.image_path, "rb") as image_file:
|
52
|
+
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
|
53
|
+
mime_type, _ = mimetypes.guess_type(self.image_path)
|
54
|
+
if not mime_type: mime_type = "application/octet-stream" # Fallback
|
55
|
+
return f"data:{mime_type};base64,{encoded_string}"
|
56
|
+
except FileNotFoundError:
|
57
|
+
logging.warning(f"Image file not found: {self.image_path}. Skipping.")
|
58
|
+
return None # Or handle error appropriately
|
59
|
+
|
60
|
+
# 3. 消息类 (已合并 MessageContent)
|
61
|
+
class Message(ABC):
|
62
|
+
def __init__(self, role: str, *initial_items: ContextProvider):
|
63
|
+
self.role = role
|
64
|
+
self._items: List[ContextProvider] = list(initial_items)
|
65
|
+
self._parent_messages: Optional['Messages'] = None
|
66
|
+
|
67
|
+
def _render_content(self) -> str:
|
68
|
+
blocks = [item.get_content_block() for item in self._items]
|
69
|
+
return "\n\n".join(b.content for b in blocks if b and b.content)
|
70
|
+
|
71
|
+
def pop(self, name: str) -> Optional[ContextProvider]:
|
72
|
+
popped_item = None
|
73
|
+
for i, item in enumerate(self._items):
|
74
|
+
if hasattr(item, 'name') and item.name == name:
|
75
|
+
popped_item = self._items.pop(i)
|
76
|
+
break
|
77
|
+
if popped_item and self._parent_messages:
|
78
|
+
self._parent_messages._notify_provider_removed(popped_item)
|
79
|
+
return popped_item
|
80
|
+
|
81
|
+
def insert(self, index: int, item: ContextProvider):
|
82
|
+
self._items.insert(index, item)
|
83
|
+
if self._parent_messages:
|
84
|
+
self._parent_messages._notify_provider_added(item, self)
|
85
|
+
|
86
|
+
def append(self, item: ContextProvider):
|
87
|
+
self._items.append(item)
|
88
|
+
if self._parent_messages:
|
89
|
+
self._parent_messages._notify_provider_added(item, self)
|
90
|
+
|
91
|
+
def providers(self) -> List[ContextProvider]: return self._items
|
92
|
+
def __repr__(self): return f"Message(role='{self.role}', items={[i.name for i in self._items]})"
|
93
|
+
def to_dict(self) -> Optional[Dict[str, Any]]:
|
94
|
+
is_multimodal = any(isinstance(p, Images) for p in self._items)
|
95
|
+
|
96
|
+
if not is_multimodal:
|
97
|
+
rendered_content = self._render_content()
|
98
|
+
if not rendered_content: return None
|
99
|
+
return {"role": self.role, "content": rendered_content}
|
100
|
+
else:
|
101
|
+
content_list = []
|
102
|
+
for item in self._items:
|
103
|
+
block = item.get_content_block()
|
104
|
+
if not block or not block.content: continue
|
105
|
+
if isinstance(item, Images):
|
106
|
+
content_list.append({"type": "image_url", "image_url": {"url": block.content}})
|
107
|
+
else:
|
108
|
+
content_list.append({"type": "text", "text": block.content})
|
109
|
+
if not content_list: return None
|
110
|
+
return {"role": self.role, "content": content_list}
|
111
|
+
|
112
|
+
class SystemMessage(Message):
|
113
|
+
def __init__(self, *items): super().__init__("system", *items)
|
114
|
+
class UserMessage(Message):
|
115
|
+
def __init__(self, *items): super().__init__("user", *items)
|
116
|
+
|
117
|
+
# 4. 顶层容器: Messages
|
118
|
+
class Messages:
|
119
|
+
def __init__(self, *initial_messages: Message):
|
120
|
+
from typing import Tuple
|
121
|
+
self._messages: List[Message] = []
|
122
|
+
self._providers_index: Dict[str, Tuple[ContextProvider, Message]] = {}
|
123
|
+
if initial_messages:
|
124
|
+
for msg in initial_messages:
|
125
|
+
self.append(msg)
|
126
|
+
|
127
|
+
def _notify_provider_added(self, provider: ContextProvider, message: Message):
|
128
|
+
if provider.name not in self._providers_index:
|
129
|
+
self._providers_index[provider.name] = (provider, message)
|
130
|
+
|
131
|
+
def _notify_provider_removed(self, provider: ContextProvider):
|
132
|
+
if provider.name in self._providers_index:
|
133
|
+
del self._providers_index[provider.name]
|
134
|
+
|
135
|
+
def provider(self, name: str) -> Optional[ContextProvider]:
|
136
|
+
indexed = self._providers_index.get(name)
|
137
|
+
return indexed[0] if indexed else None
|
138
|
+
|
139
|
+
def pop(self, name: str) -> Optional[ContextProvider]:
|
140
|
+
indexed = self._providers_index.get(name)
|
141
|
+
if not indexed:
|
142
|
+
return None
|
143
|
+
_provider, parent_message = indexed
|
144
|
+
return parent_message.pop(name)
|
145
|
+
|
146
|
+
async def refresh(self):
|
147
|
+
tasks = [provider.refresh() for provider, _ in self._providers_index.values()]
|
148
|
+
await asyncio.gather(*tasks)
|
149
|
+
|
150
|
+
def render(self) -> List[Dict[str, Any]]:
|
151
|
+
results = [msg.to_dict() for msg in self._messages]
|
152
|
+
return [res for res in results if res]
|
153
|
+
|
154
|
+
async def render_latest(self) -> List[Dict[str, Any]]:
|
155
|
+
await self.refresh()
|
156
|
+
return self.render()
|
157
|
+
|
158
|
+
def append(self, message: Message):
|
159
|
+
if self._messages and self._messages[-1].role == message.role:
|
160
|
+
last_message = self._messages[-1]
|
161
|
+
for provider in message.providers():
|
162
|
+
last_message.append(provider)
|
163
|
+
else:
|
164
|
+
message._parent_messages = self
|
165
|
+
self._messages.append(message)
|
166
|
+
for p in message.providers():
|
167
|
+
self._notify_provider_added(p, message)
|
168
|
+
|
169
|
+
def __getitem__(self, index: int) -> Message: return self._messages[index]
|
170
|
+
def __len__(self) -> int: return len(self._messages)
|
171
|
+
def __iter__(self): return iter(self._messages)
|
172
|
+
|
173
|
+
# ==============================================================================
|
174
|
+
# 6. 演示
|
175
|
+
# ==============================================================================
|
176
|
+
async def run_demo():
|
177
|
+
# --- 1. 初始化提供者 ---
|
178
|
+
system_prompt_provider = Texts("system_prompt", "你是一个AI助手。")
|
179
|
+
tools_provider = Tools(tools_json=[{"name": "read_file"}])
|
180
|
+
files_provider = Files()
|
181
|
+
|
182
|
+
# --- 2. 演示新功能:优雅地构建 Messages ---
|
183
|
+
print("\n>>> 场景 A: 使用新的、优雅的构造函数直接初始化 Messages")
|
184
|
+
messages = Messages(
|
185
|
+
SystemMessage(system_prompt_provider, tools_provider),
|
186
|
+
UserMessage(files_provider, Texts("user_input", "这是我的初始问题。")),
|
187
|
+
UserMessage(Texts("user_input2", "这是我的初始问题2。"))
|
188
|
+
)
|
189
|
+
|
190
|
+
print("\n--- 渲染后的初始 Messages (首次渲染,全部刷新) ---")
|
191
|
+
for msg_dict in await messages.render_latest(): print(msg_dict)
|
192
|
+
print("-" * 40)
|
193
|
+
|
194
|
+
# --- 3. 演示穿透更新 ---
|
195
|
+
print("\n>>> 场景 B: 穿透更新 File Provider,渲染时自动刷新")
|
196
|
+
files_provider_instance = messages.provider("files")
|
197
|
+
if isinstance(files_provider_instance, Files):
|
198
|
+
files_provider_instance.update("file1.py", "这是新的文件内容!")
|
199
|
+
|
200
|
+
print("\n--- 再次渲染 Messages (只有文件提供者会刷新) ---")
|
201
|
+
for msg_dict in await messages.render_latest(): print(msg_dict)
|
202
|
+
print("-" * 40)
|
203
|
+
|
204
|
+
# --- 4. 演示全局 Pop 和通过索引 Insert ---
|
205
|
+
print("\n>>> 场景 C: 全局 Pop 工具提供者,并 Insert 到 UserMessage 中")
|
206
|
+
popped_tools_provider = messages.pop("tools")
|
207
|
+
if popped_tools_provider:
|
208
|
+
messages[1].insert(0, popped_tools_provider)
|
209
|
+
print(f"\n已成功将 '{popped_tools_provider.name}' 提供者移动到用户消息。")
|
210
|
+
|
211
|
+
print("\n--- Pop 和 Insert 后渲染的 Messages (验证移动效果) ---")
|
212
|
+
for msg_dict in messages.render(): print(msg_dict)
|
213
|
+
print("-" * 40)
|
214
|
+
|
215
|
+
# --- 5. 演示多模态渲染 ---
|
216
|
+
print("\n>>> 场景 D: 演示多模态 (文本+图片) 渲染")
|
217
|
+
with open("dummy_image.png", "w") as f:
|
218
|
+
f.write("This is a dummy image file.")
|
219
|
+
|
220
|
+
multimodal_message = Messages(
|
221
|
+
UserMessage(
|
222
|
+
Texts("prompt", "What do you see in this image?"),
|
223
|
+
Images("dummy_image.png")
|
224
|
+
)
|
225
|
+
)
|
226
|
+
print("\n--- 渲染后的多模态 Message ---")
|
227
|
+
for msg_dict in await multimodal_message.render_latest():
|
228
|
+
if isinstance(msg_dict['content'], list):
|
229
|
+
for item in msg_dict['content']:
|
230
|
+
if item['type'] == 'image_url':
|
231
|
+
item['image_url']['url'] = item['image_url']['url'][:80] + "..."
|
232
|
+
print(msg_dict)
|
233
|
+
print("-" * 40)
|
234
|
+
|
235
|
+
|
236
|
+
if __name__ == "__main__":
|
237
|
+
asyncio.run(run_demo())
|
@@ -0,0 +1,226 @@
|
|
1
|
+
import unittest
|
2
|
+
from unittest.mock import AsyncMock
|
3
|
+
from architext import *
|
4
|
+
|
5
|
+
# ==============================================================================
|
6
|
+
# 单元测试部分
|
7
|
+
# ==============================================================================
|
8
|
+
class TestContextManagement(unittest.IsolatedAsyncioTestCase):
|
9
|
+
|
10
|
+
def setUp(self):
|
11
|
+
"""在每个测试前设置环境"""
|
12
|
+
self.system_prompt_provider = Texts("system_prompt", "你是一个AI助手。")
|
13
|
+
self.tools_provider = Tools(tools_json=[{"name": "read_file"}])
|
14
|
+
self.files_provider = Files()
|
15
|
+
|
16
|
+
async def test_a_initial_construction_and_render(self):
|
17
|
+
"""测试优雅的初始化和首次渲染"""
|
18
|
+
messages = Messages(
|
19
|
+
SystemMessage(self.system_prompt_provider, self.tools_provider),
|
20
|
+
UserMessage(self.files_provider, Texts("user_input", "这是我的初始问题。"))
|
21
|
+
)
|
22
|
+
|
23
|
+
self.assertEqual(len(messages), 2)
|
24
|
+
rendered = await messages.render_latest()
|
25
|
+
|
26
|
+
self.assertEqual(len(rendered), 2)
|
27
|
+
self.assertIn("<tools>", rendered[0]['content'])
|
28
|
+
self.assertNotIn("<files>", rendered[1]['content'])
|
29
|
+
|
30
|
+
async def test_b_provider_passthrough_and_refresh(self):
|
31
|
+
"""测试通过 mock 验证缓存和刷新逻辑"""
|
32
|
+
# 我们真正关心的是 _fetch_content 是否被不必要地调用
|
33
|
+
# 所以我们 mock 底层的它,而不是 refresh 方法
|
34
|
+
original_fetch_content = self.files_provider._fetch_content
|
35
|
+
self.files_provider._fetch_content = AsyncMock(side_effect=original_fetch_content)
|
36
|
+
|
37
|
+
messages = Messages(UserMessage(self.files_provider))
|
38
|
+
|
39
|
+
# 1. 首次刷新
|
40
|
+
self.files_provider.update("path1", "content1")
|
41
|
+
await messages.refresh()
|
42
|
+
# _fetch_content 应该被调用了 1 次
|
43
|
+
self.assertEqual(self.files_provider._fetch_content.call_count, 1)
|
44
|
+
|
45
|
+
# 2. 再次刷新,内容未变,不应再次调用 _fetch_content
|
46
|
+
await messages.refresh()
|
47
|
+
# 调用次数应该仍然是 1,证明缓存生效
|
48
|
+
self.assertEqual(self.files_provider._fetch_content.call_count, 1)
|
49
|
+
|
50
|
+
# 3. 更新文件内容,这会标记 provider 为 stale
|
51
|
+
self.files_provider.update("path2", "content2")
|
52
|
+
|
53
|
+
# 4. 再次刷新,现在应该会重新调用 _fetch_content
|
54
|
+
await messages.refresh()
|
55
|
+
rendered = messages.render()
|
56
|
+
# 调用次数应该变为 2
|
57
|
+
self.assertEqual(self.files_provider._fetch_content.call_count, 2)
|
58
|
+
# 并且渲染结果包含了新内容
|
59
|
+
self.assertIn("content2", rendered[0]['content'])
|
60
|
+
|
61
|
+
async def test_c_global_pop_and_indexed_insert(self):
|
62
|
+
"""测试全局pop和通过索引insert的功能"""
|
63
|
+
messages = Messages(
|
64
|
+
SystemMessage(self.system_prompt_provider, self.tools_provider),
|
65
|
+
UserMessage(self.files_provider)
|
66
|
+
)
|
67
|
+
|
68
|
+
# 验证初始状态
|
69
|
+
initial_rendered = await messages.render_latest()
|
70
|
+
self.assertTrue(any("<tools>" in msg['content'] for msg in initial_rendered if msg['role'] == 'system'))
|
71
|
+
|
72
|
+
# 全局弹出 'tools' Provider
|
73
|
+
popped_tools_provider = messages.pop("tools")
|
74
|
+
self.assertIs(popped_tools_provider, self.tools_provider)
|
75
|
+
|
76
|
+
# 验证 pop 后的状态
|
77
|
+
rendered_after_pop = messages.render()
|
78
|
+
self.assertFalse(any("<tools>" in msg['content'] for msg in rendered_after_pop if msg['role'] == 'system'))
|
79
|
+
|
80
|
+
# 通过索引将弹出的provider插入到UserMessage的开头
|
81
|
+
messages[1].insert(0, popped_tools_provider)
|
82
|
+
|
83
|
+
# 验证 insert 后的状态
|
84
|
+
rendered_after_insert = messages.render()
|
85
|
+
user_message_content = next(msg['content'] for msg in rendered_after_insert if msg['role'] == 'user')
|
86
|
+
self.assertTrue(user_message_content.startswith("<tools>"))
|
87
|
+
|
88
|
+
async def test_d_multimodal_rendering(self):
|
89
|
+
"""测试多模态(文本+图片)渲染"""
|
90
|
+
# Create a dummy image file for the test
|
91
|
+
dummy_image_path = "test_dummy_image.png"
|
92
|
+
with open(dummy_image_path, "w") as f:
|
93
|
+
f.write("dummy content")
|
94
|
+
|
95
|
+
messages = Messages(
|
96
|
+
UserMessage(
|
97
|
+
Texts("prompt", "Describe the image."),
|
98
|
+
Images(dummy_image_path) # Test with optional name
|
99
|
+
)
|
100
|
+
)
|
101
|
+
|
102
|
+
rendered = await messages.render_latest()
|
103
|
+
self.assertEqual(len(rendered), 1)
|
104
|
+
|
105
|
+
content = rendered[0]['content']
|
106
|
+
self.assertIsInstance(content, list)
|
107
|
+
self.assertEqual(len(content), 2)
|
108
|
+
|
109
|
+
# Check text part
|
110
|
+
self.assertEqual(content[0]['type'], 'text')
|
111
|
+
self.assertEqual(content[0]['text'], 'Describe the image.')
|
112
|
+
|
113
|
+
# Check image part
|
114
|
+
self.assertEqual(content[1]['type'], 'image_url')
|
115
|
+
self.assertIn('data:image/png;base64,', content[1]['image_url']['url'])
|
116
|
+
|
117
|
+
# Clean up the dummy file
|
118
|
+
import os
|
119
|
+
os.remove(dummy_image_path)
|
120
|
+
|
121
|
+
async def test_e_multimodal_type_switching(self):
|
122
|
+
"""测试多模态消息在pop图片后是否能正确回退到字符串渲染"""
|
123
|
+
dummy_image_path = "test_dummy_image_2.png"
|
124
|
+
with open(dummy_image_path, "w") as f:
|
125
|
+
f.write("dummy content")
|
126
|
+
|
127
|
+
messages = Messages(
|
128
|
+
UserMessage(
|
129
|
+
Texts("prefix", "Look at this:"),
|
130
|
+
Images(dummy_image_path, name="image"), # Explicit name for popping
|
131
|
+
Texts("suffix", "Any thoughts?")
|
132
|
+
)
|
133
|
+
)
|
134
|
+
|
135
|
+
# 1. Initial multimodal render
|
136
|
+
rendered_multi = await messages.render_latest()
|
137
|
+
content_multi = rendered_multi[0]['content']
|
138
|
+
self.assertIsInstance(content_multi, list)
|
139
|
+
self.assertEqual(len(content_multi), 3) # prefix, image, suffix
|
140
|
+
|
141
|
+
# 2. Pop the image
|
142
|
+
popped_image = messages.pop("image")
|
143
|
+
self.assertIsNotNone(popped_image)
|
144
|
+
|
145
|
+
# 3. Render again, should fall back to string content
|
146
|
+
rendered_str = messages.render() # No refresh needed
|
147
|
+
content_str = rendered_str[0]['content']
|
148
|
+
self.assertIsInstance(content_str, str)
|
149
|
+
self.assertEqual(content_str, "Look at this:\n\nAny thoughts?")
|
150
|
+
|
151
|
+
# Clean up
|
152
|
+
import os
|
153
|
+
os.remove(dummy_image_path)
|
154
|
+
|
155
|
+
def test_f_message_merging(self):
|
156
|
+
"""测试初始化和追加时自动合并消息的功能"""
|
157
|
+
# 1. Test merging during initialization
|
158
|
+
messages = Messages(
|
159
|
+
UserMessage(Texts("part1", "Hello,")),
|
160
|
+
UserMessage(Texts("part2", "world!")),
|
161
|
+
SystemMessage(Texts("system", "System prompt.")),
|
162
|
+
UserMessage(Texts("part3", "How are you?"))
|
163
|
+
)
|
164
|
+
# Should be merged into: User, System, User
|
165
|
+
self.assertEqual(len(messages), 3)
|
166
|
+
self.assertEqual(len(messages[0]._items), 2) # First UserMessage has 2 items
|
167
|
+
self.assertEqual(messages[0]._items[1].name, "part2")
|
168
|
+
self.assertEqual(messages[1].role, "system")
|
169
|
+
self.assertEqual(messages[2].role, "user")
|
170
|
+
|
171
|
+
# 2. Test merging during append
|
172
|
+
messages.append(UserMessage(Texts("part4", "I am fine.")))
|
173
|
+
self.assertEqual(len(messages), 3) # Still 3 messages
|
174
|
+
self.assertEqual(len(messages[2]._items), 2) # Last UserMessage now has 2 items
|
175
|
+
self.assertEqual(messages[2]._items[1].name, "part4")
|
176
|
+
|
177
|
+
# 3. Test appending a different role
|
178
|
+
messages.append(SystemMessage(Texts("system2", "Another prompt.")))
|
179
|
+
self.assertEqual(len(messages), 4) # Should not merge
|
180
|
+
self.assertEqual(messages[3].role, "system")
|
181
|
+
|
182
|
+
async def test_g_state_inconsistency_on_direct_message_modification(self):
|
183
|
+
"""
|
184
|
+
测试当直接在 Message 对象上执行 pop 操作时,
|
185
|
+
顶层 Messages 对象的 _providers_index 是否会产生不一致。
|
186
|
+
"""
|
187
|
+
messages = Messages(
|
188
|
+
SystemMessage(self.system_prompt_provider, self.tools_provider),
|
189
|
+
UserMessage(self.files_provider)
|
190
|
+
)
|
191
|
+
|
192
|
+
# 0. 先刷新一次,确保所有 provider 的 cache 都已填充
|
193
|
+
await messages.refresh()
|
194
|
+
|
195
|
+
# 1. 初始状态:'tools' 提供者应该在索引中
|
196
|
+
self.assertIsNotNone(messages.provider("tools"), "初始状态下 'tools' 提供者应该能被找到")
|
197
|
+
self.assertIs(messages.provider("tools"), self.tools_provider)
|
198
|
+
|
199
|
+
# 2. 直接在子消息对象上执行 pop 操作
|
200
|
+
system_message = messages[0]
|
201
|
+
popped_provider = system_message.pop("tools")
|
202
|
+
|
203
|
+
# 验证是否真的从 Message 对象中弹出了
|
204
|
+
self.assertIs(popped_provider, self.tools_provider, "应该从 SystemMessage 中成功弹出 provider")
|
205
|
+
self.assertNotIn(self.tools_provider, system_message.providers(), "provider 不应再存在于 SystemMessage 的 providers 列表中")
|
206
|
+
|
207
|
+
# 3. 核心问题:检查顶层 Messages 的索引
|
208
|
+
# 在理想情况下,直接修改子消息应该同步更新顶层索引。
|
209
|
+
# 因此,我们断言 provider 现在应该是找不到的。这个测试现在应该会失败。
|
210
|
+
provider_after_pop = messages.provider("tools")
|
211
|
+
self.assertIsNone(provider_after_pop, "BUG: 直接从子消息中 pop 后,顶层索引未同步,仍然可以找到 provider")
|
212
|
+
|
213
|
+
# 4. 进一步验证:渲染结果和索引内容不一致
|
214
|
+
# 渲染结果应该不再包含 tools 内容,因为 Message 对象本身是正确的
|
215
|
+
rendered_messages = messages.render()
|
216
|
+
self.assertGreater(len(rendered_messages), 0, "渲染后的消息列表不应为空")
|
217
|
+
rendered_content = rendered_messages[0]['content']
|
218
|
+
self.assertNotIn("<tools>", rendered_content, "渲染结果中不应再包含 'tools' 的内容,证明数据源已更新")
|
219
|
+
|
220
|
+
|
221
|
+
if __name__ == '__main__':
|
222
|
+
# 为了在普通脚本环境中运行,添加这两行
|
223
|
+
suite = unittest.TestSuite()
|
224
|
+
suite.addTest(unittest.makeSuite(TestContextManagement))
|
225
|
+
runner = unittest.TextTestRunner()
|
226
|
+
runner.run(suite)
|
@@ -7,6 +7,9 @@ aient.egg-info/SOURCES.txt
|
|
7
7
|
aient.egg-info/dependency_links.txt
|
8
8
|
aient.egg-info/requires.txt
|
9
9
|
aient.egg-info/top_level.txt
|
10
|
+
aient/architext/test.py
|
11
|
+
aient/architext/architext/__init__.py
|
12
|
+
aient/architext/architext/core.py
|
10
13
|
aient/core/__init__.py
|
11
14
|
aient/core/log_config.py
|
12
15
|
aient/core/models.py
|
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
|