aient 1.2.0__tar.gz → 1.2.2__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.2.0 → aient-1.2.2}/PKG-INFO +1 -1
- {aient-1.2.0 → aient-1.2.2}/aient/architext/architext/core.py +73 -13
- {aient-1.2.0 → aient-1.2.2}/aient/architext/test/test.py +82 -0
- {aient-1.2.0 → aient-1.2.2}/aient.egg-info/PKG-INFO +1 -1
- {aient-1.2.0 → aient-1.2.2}/pyproject.toml +1 -1
- {aient-1.2.0 → aient-1.2.2}/LICENSE +0 -0
- {aient-1.2.0 → aient-1.2.2}/README.md +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/__init__.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/architext/architext/__init__.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/architext/test/openai_client.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/architext/test/test_save_load.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/core/__init__.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/core/log_config.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/core/models.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/core/request.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/core/response.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/core/test/test_base_api.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/core/test/test_geminimask.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/core/test/test_image.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/core/test/test_payload.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/core/utils.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/models/__init__.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/models/audio.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/models/base.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/models/chatgpt.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/__init__.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/arXiv.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/config.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/excute_command.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/get_time.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/image.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/list_directory.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/read_file.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/read_image.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/readonly.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/registry.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/run_python.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/websearch.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/plugins/write_file.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/utils/__init__.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/utils/prompt.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient/utils/scripts.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient.egg-info/SOURCES.txt +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient.egg-info/dependency_links.txt +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient.egg-info/requires.txt +0 -0
- {aient-1.2.0 → aient-1.2.2}/aient.egg-info/top_level.txt +0 -0
- {aient-1.2.0 → aient-1.2.2}/setup.cfg +0 -0
- {aient-1.2.0 → aient-1.2.2}/test/test_Web_crawler.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/test/test_ddg_search.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/test/test_google_search.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/test/test_ollama.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/test/test_plugin.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/test/test_search.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/test/test_url.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/test/test_whisper.py +0 -0
- {aient-1.2.0 → aient-1.2.2}/test/test_yjh.py +0 -0
@@ -10,6 +10,30 @@ from dataclasses import dataclass
|
|
10
10
|
from abc import ABC, abstractmethod
|
11
11
|
from typing import List, Dict, Any, Optional, Union, Callable
|
12
12
|
|
13
|
+
# A wrapper to manage multiple providers with the same name
|
14
|
+
class ProviderGroup:
|
15
|
+
"""A container for multiple providers that share the same name, allowing for bulk operations."""
|
16
|
+
def __init__(self, providers: List['ContextProvider']):
|
17
|
+
self._providers = providers
|
18
|
+
def __getitem__(self, key: int) -> 'ContextProvider':
|
19
|
+
"""Allows accessing providers by index, e.g., group[-1]."""
|
20
|
+
return self._providers[key]
|
21
|
+
def __iter__(self):
|
22
|
+
"""Allows iterating over the providers."""
|
23
|
+
return iter(self._providers)
|
24
|
+
def __len__(self) -> int:
|
25
|
+
"""Returns the number of providers in the group."""
|
26
|
+
return len(self._providers)
|
27
|
+
@property
|
28
|
+
def visible(self) -> List[bool]:
|
29
|
+
"""Gets the visibility of all providers in the group."""
|
30
|
+
return [p.visible for p in self._providers]
|
31
|
+
@visible.setter
|
32
|
+
def visible(self, value: bool):
|
33
|
+
"""Sets the visibility for all providers in the group."""
|
34
|
+
for p in self._providers:
|
35
|
+
p.visible = value
|
36
|
+
|
13
37
|
# Global, thread-safe registry for providers created within f-strings
|
14
38
|
_fstring_provider_registry = {}
|
15
39
|
_registry_lock = threading.Lock()
|
@@ -273,6 +297,16 @@ class Message(ABC):
|
|
273
297
|
self._items: List[ContextProvider] = processed_items
|
274
298
|
self._parent_messages: Optional['Messages'] = None
|
275
299
|
|
300
|
+
@property
|
301
|
+
def content(self) -> Optional[Union[str, List[Dict[str, Any]]]]:
|
302
|
+
"""
|
303
|
+
Renders the message content.
|
304
|
+
For simple text messages, returns a string.
|
305
|
+
For multimodal messages, returns a list of content blocks.
|
306
|
+
"""
|
307
|
+
rendered_dict = self.to_dict()
|
308
|
+
return rendered_dict.get('content') if rendered_dict else None
|
309
|
+
|
276
310
|
def _render_content(self) -> str:
|
277
311
|
final_parts = []
|
278
312
|
for item in self._items:
|
@@ -420,15 +454,17 @@ class ToolCalls(Message):
|
|
420
454
|
class ToolResults(Message):
|
421
455
|
"""Represents a tool message with the result of a single tool call."""
|
422
456
|
def __init__(self, tool_call_id: str, content: str):
|
423
|
-
|
457
|
+
# We pass a Texts provider to the parent so it can be rendered,
|
458
|
+
# but the primary way to access content for ToolResults is via its dict representation.
|
459
|
+
super().__init__("tool", Texts(text=content))
|
424
460
|
self.tool_call_id = tool_call_id
|
425
|
-
self.
|
461
|
+
self._content = content
|
426
462
|
|
427
463
|
def to_dict(self) -> Dict[str, Any]:
|
428
464
|
return {
|
429
465
|
"role": self.role,
|
430
466
|
"tool_call_id": self.tool_call_id,
|
431
|
-
"content": self.
|
467
|
+
"content": self._content
|
432
468
|
}
|
433
469
|
|
434
470
|
# 4. 顶层容器: Messages
|
@@ -436,22 +472,40 @@ class Messages:
|
|
436
472
|
def __init__(self, *initial_messages: Message):
|
437
473
|
from typing import Tuple
|
438
474
|
self._messages: List[Message] = []
|
439
|
-
self._providers_index: Dict[str, Tuple[ContextProvider, Message]] = {}
|
475
|
+
self._providers_index: Dict[str, List[Tuple[ContextProvider, Message]]] = {}
|
440
476
|
if initial_messages:
|
441
477
|
for msg in initial_messages:
|
442
478
|
self.append(msg)
|
443
479
|
|
444
480
|
def _notify_provider_added(self, provider: ContextProvider, message: Message):
|
445
481
|
if provider.name not in self._providers_index:
|
446
|
-
self._providers_index[provider.name] =
|
482
|
+
self._providers_index[provider.name] = []
|
483
|
+
self._providers_index[provider.name].append((provider, message))
|
447
484
|
|
448
485
|
def _notify_provider_removed(self, provider: ContextProvider):
|
449
486
|
if provider.name in self._providers_index:
|
450
|
-
|
487
|
+
# Create a new list excluding the provider to be removed.
|
488
|
+
# Comparing by object identity (`is`) is crucial here.
|
489
|
+
providers_list = self._providers_index[provider.name]
|
490
|
+
new_list = [(p, m) for p, m in providers_list if p is not provider]
|
491
|
+
|
492
|
+
if not new_list:
|
493
|
+
# If the list becomes empty, remove the key from the dictionary.
|
494
|
+
del self._providers_index[provider.name]
|
495
|
+
else:
|
496
|
+
# Otherwise, update the dictionary with the new list.
|
497
|
+
self._providers_index[provider.name] = new_list
|
451
498
|
|
452
|
-
def provider(self, name: str) -> Optional[ContextProvider]:
|
453
|
-
|
454
|
-
|
499
|
+
def provider(self, name: str) -> Optional[Union[ContextProvider, ProviderGroup]]:
|
500
|
+
indexed_list = self._providers_index.get(name)
|
501
|
+
if not indexed_list:
|
502
|
+
return None
|
503
|
+
|
504
|
+
providers = [p for p, m in indexed_list]
|
505
|
+
if len(providers) == 1:
|
506
|
+
return providers[0]
|
507
|
+
else:
|
508
|
+
return ProviderGroup(providers)
|
455
509
|
|
456
510
|
def pop(self, key: Optional[Union[str, int]] = None) -> Union[Optional[ContextProvider], Optional[Message]]:
|
457
511
|
# If no key is provided, pop the last message.
|
@@ -459,10 +513,13 @@ class Messages:
|
|
459
513
|
key = len(self._messages) - 1
|
460
514
|
|
461
515
|
if isinstance(key, str):
|
462
|
-
|
463
|
-
if not
|
516
|
+
indexed_list = self._providers_index.get(key)
|
517
|
+
if not indexed_list:
|
464
518
|
return None
|
465
|
-
|
519
|
+
# Pop the first one found, which is consistent with how pop usually works
|
520
|
+
_provider, parent_message = indexed_list[0]
|
521
|
+
# The actual removal from _providers_index happens in _notify_provider_removed
|
522
|
+
# which is called by message.pop()
|
466
523
|
return parent_message.pop(key)
|
467
524
|
elif isinstance(key, int):
|
468
525
|
try:
|
@@ -481,7 +538,10 @@ class Messages:
|
|
481
538
|
return None
|
482
539
|
|
483
540
|
async def refresh(self):
|
484
|
-
tasks = [
|
541
|
+
tasks = []
|
542
|
+
for provider_list in self._providers_index.values():
|
543
|
+
for provider, _ in provider_list:
|
544
|
+
tasks.append(provider.refresh())
|
485
545
|
await asyncio.gather(*tasks)
|
486
546
|
|
487
547
|
def render(self) -> List[Dict[str, Any]]:
|
@@ -1053,6 +1053,88 @@ Current time: {Texts(lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"))}
|
|
1053
1053
|
self.assertEqual(len(rendered_visible_again), 1)
|
1054
1054
|
self.assertEqual(rendered_visible_again[0]['content'], "Hello, World!")
|
1055
1055
|
|
1056
|
+
async def test_z8_bulk_provider_visibility_control(self):
|
1057
|
+
"""测试通过名称批量控制和豁免provider的可见性"""
|
1058
|
+
# 1. 创建多个同名 provider
|
1059
|
+
messages = Messages(
|
1060
|
+
UserMessage(
|
1061
|
+
Texts("First explanation.", name="explanation"),
|
1062
|
+
Texts("Second explanation.", name="explanation"),
|
1063
|
+
Texts("Some other text."),
|
1064
|
+
Texts("Third explanation.", name="explanation")
|
1065
|
+
)
|
1066
|
+
)
|
1067
|
+
|
1068
|
+
# 2. 初始渲染,所有 "explanation" 都应该可见
|
1069
|
+
rendered_initial = await messages.render_latest()
|
1070
|
+
self.assertIn("First explanation.", rendered_initial[0]['content'])
|
1071
|
+
self.assertIn("Second explanation.", rendered_initial[0]['content'])
|
1072
|
+
self.assertIn("Third explanation.", rendered_initial[0]['content'])
|
1073
|
+
|
1074
|
+
# 3. 获取所有名为 "explanation" 的 provider
|
1075
|
+
explanation_providers = messages.provider("explanation")
|
1076
|
+
self.assertIsInstance(explanation_providers, ProviderGroup)
|
1077
|
+
self.assertEqual(len(explanation_providers), 3)
|
1078
|
+
|
1079
|
+
# 4. 将所有 "explanation" provider 设置为不可见
|
1080
|
+
# 这是需要实现的新语法
|
1081
|
+
explanation_providers.visible = False
|
1082
|
+
for p in explanation_providers:
|
1083
|
+
self.assertFalse(p.visible)
|
1084
|
+
|
1085
|
+
# 5. 渲染,所有 "explanation" 的内容都应该消失
|
1086
|
+
rendered_hidden = await messages.render_latest()
|
1087
|
+
self.assertNotIn("First explanation.", rendered_hidden[0]['content'])
|
1088
|
+
self.assertNotIn("Second explanation.", rendered_hidden[0]['content'])
|
1089
|
+
self.assertNotIn("Third explanation.", rendered_hidden[0]['content'])
|
1090
|
+
self.assertIn("Some other text.", rendered_hidden[0]['content'])
|
1091
|
+
|
1092
|
+
# 6. 将最后一个 "explanation" provider 设置回可见
|
1093
|
+
# 这是需要实现的另一个新语法
|
1094
|
+
explanation_providers[-1].visible = True
|
1095
|
+
self.assertTrue(explanation_providers[-1].visible)
|
1096
|
+
self.assertFalse(explanation_providers[0].visible)
|
1097
|
+
|
1098
|
+
# 7. 最终渲染,只应看到最后一个 "explanation"
|
1099
|
+
rendered_final = await messages.render_latest()
|
1100
|
+
self.assertNotIn("First explanation.", rendered_final[0]['content'])
|
1101
|
+
self.assertNotIn("Second explanation.", rendered_final[0]['content'])
|
1102
|
+
self.assertIn("Third explanation.", rendered_final[0]['content'])
|
1103
|
+
self.assertIn("Some other text.", rendered_final[0]['content'])
|
1104
|
+
|
1105
|
+
async def test_z9_rolemessage_content_access(self):
|
1106
|
+
"""测试是否支持 RoleMessage.content 来访问渲染好的内容"""
|
1107
|
+
# 1. 创建一个简单的 UserMessage
|
1108
|
+
user_message = UserMessage("你好, Architext!")
|
1109
|
+
# 对于简单的 Texts, refresh 不是必须的, 但这是个好习惯
|
1110
|
+
# Message 类本身没有 refresh, 调用其 providers 的 refresh
|
1111
|
+
for p in user_message.providers():
|
1112
|
+
await p.refresh()
|
1113
|
+
|
1114
|
+
# 2. 直接访问 .content 属性
|
1115
|
+
# 在实现该功能前,这行代码会因 AttributeError 而失败
|
1116
|
+
self.assertEqual(user_message.content, "你好, Architext!")
|
1117
|
+
|
1118
|
+
# 3. 创建一个多模态消息
|
1119
|
+
multimodal_message = AssistantMessage(
|
1120
|
+
"这是一张图片:",
|
1121
|
+
Images(url="data:image/png;base64,FAKE_IMG_DATA")
|
1122
|
+
)
|
1123
|
+
for p in multimodal_message.providers():
|
1124
|
+
await p.refresh()
|
1125
|
+
|
1126
|
+
# 4. 访问多模态消息的 .content 属性,期望返回一个列表
|
1127
|
+
content_list = multimodal_message.content
|
1128
|
+
self.assertIsInstance(content_list, list)
|
1129
|
+
self.assertEqual(len(content_list), 2)
|
1130
|
+
self.assertEqual(content_list[0]['type'], 'text')
|
1131
|
+
self.assertEqual(content_list[1]['type'], 'image_url')
|
1132
|
+
|
1133
|
+
# 5. 测试通过 RoleMessage 工厂创建的消息
|
1134
|
+
role_message = RoleMessage('user', "通过工厂创建的内容")
|
1135
|
+
for p in role_message.providers():
|
1136
|
+
await p.refresh()
|
1137
|
+
self.assertEqual(role_message.content, "通过工厂创建的内容")
|
1056
1138
|
|
1057
1139
|
# ==============================================================================
|
1058
1140
|
# 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
|