aient 1.1.91__tar.gz → 1.1.93__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.91 → aient-1.1.93}/PKG-INFO +1 -1
- aient-1.1.93/aient/architext/architext/core.py +451 -0
- aient-1.1.93/aient/architext/test/openai_client.py +146 -0
- aient-1.1.93/aient/architext/test/test.py +980 -0
- aient-1.1.93/aient/architext/test/test_save_load.py +93 -0
- {aient-1.1.91 → aient-1.1.93}/aient/models/chatgpt.py +31 -104
- {aient-1.1.91 → aient-1.1.93}/aient.egg-info/PKG-INFO +1 -1
- {aient-1.1.91 → aient-1.1.93}/aient.egg-info/SOURCES.txt +3 -1
- {aient-1.1.91 → aient-1.1.93}/pyproject.toml +1 -1
- aient-1.1.91/aient/architext/architext/core.py +0 -237
- aient-1.1.91/aient/architext/test.py +0 -226
- {aient-1.1.91 → aient-1.1.93}/LICENSE +0 -0
- {aient-1.1.91 → aient-1.1.93}/README.md +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/__init__.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/architext/architext/__init__.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/core/__init__.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/core/log_config.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/core/models.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/core/request.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/core/response.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/core/test/test_base_api.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/core/test/test_geminimask.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/core/test/test_image.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/core/test/test_payload.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/core/utils.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/models/__init__.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/models/audio.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/models/base.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/__init__.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/arXiv.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/config.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/excute_command.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/get_time.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/image.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/list_directory.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/read_file.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/read_image.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/readonly.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/registry.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/run_python.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/websearch.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/plugins/write_file.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/utils/__init__.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/utils/prompt.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient/utils/scripts.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient.egg-info/dependency_links.txt +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient.egg-info/requires.txt +0 -0
- {aient-1.1.91 → aient-1.1.93}/aient.egg-info/top_level.txt +0 -0
- {aient-1.1.91 → aient-1.1.93}/setup.cfg +0 -0
- {aient-1.1.91 → aient-1.1.93}/test/test_Web_crawler.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/test/test_ddg_search.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/test/test_google_search.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/test/test_ollama.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/test/test_plugin.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/test/test_search.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/test/test_url.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/test/test_whisper.py +0 -0
- {aient-1.1.91 → aient-1.1.93}/test/test_yjh.py +0 -0
@@ -0,0 +1,451 @@
|
|
1
|
+
import pickle
|
2
|
+
import base64
|
3
|
+
import asyncio
|
4
|
+
import logging
|
5
|
+
import hashlib
|
6
|
+
import mimetypes
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from abc import ABC, abstractmethod
|
9
|
+
from typing import List, Dict, Any, Optional, Union, Callable
|
10
|
+
|
11
|
+
# 1. 核心数据结构: ContentBlock
|
12
|
+
@dataclass
|
13
|
+
class ContentBlock:
|
14
|
+
name: str
|
15
|
+
content: str
|
16
|
+
|
17
|
+
# 2. 上下文提供者 (带缓存)
|
18
|
+
class ContextProvider(ABC):
|
19
|
+
def __init__(self, name: str):
|
20
|
+
self.name = name; self._cached_content: Optional[str] = None; self._is_stale: bool = True
|
21
|
+
def mark_stale(self): self._is_stale = True
|
22
|
+
async def refresh(self):
|
23
|
+
if self._is_stale:
|
24
|
+
self._cached_content = await self.render()
|
25
|
+
self._is_stale = False
|
26
|
+
@abstractmethod
|
27
|
+
async def render(self) -> Optional[str]: raise NotImplementedError
|
28
|
+
@abstractmethod
|
29
|
+
def update(self, *args, **kwargs): raise NotImplementedError
|
30
|
+
def get_content_block(self) -> Optional[ContentBlock]:
|
31
|
+
if self._cached_content is not None: return ContentBlock(self.name, self._cached_content)
|
32
|
+
return None
|
33
|
+
|
34
|
+
class Texts(ContextProvider):
|
35
|
+
def __init__(self, text: Union[str, Callable[[], str]], name: Optional[str] = None):
|
36
|
+
self._text = text
|
37
|
+
self._is_dynamic = callable(self._text)
|
38
|
+
|
39
|
+
if name is None:
|
40
|
+
if self._is_dynamic:
|
41
|
+
import uuid
|
42
|
+
_name = f"dynamic_text_{uuid.uuid4().hex[:8]}"
|
43
|
+
else:
|
44
|
+
h = hashlib.sha1(self._text.encode()).hexdigest()
|
45
|
+
_name = f"text_{h[:8]}"
|
46
|
+
else:
|
47
|
+
_name = name
|
48
|
+
super().__init__(_name)
|
49
|
+
|
50
|
+
async def refresh(self):
|
51
|
+
if self._is_dynamic:
|
52
|
+
self._is_stale = True
|
53
|
+
await super().refresh()
|
54
|
+
|
55
|
+
def update(self, text: Union[str, Callable[[], str]]):
|
56
|
+
self._text = text
|
57
|
+
self._is_dynamic = callable(self._text)
|
58
|
+
self.mark_stale()
|
59
|
+
|
60
|
+
async def render(self) -> str:
|
61
|
+
if self._is_dynamic:
|
62
|
+
return self._text()
|
63
|
+
return self._text
|
64
|
+
|
65
|
+
class Tools(ContextProvider):
|
66
|
+
def __init__(self, tools_json: List[Dict]): super().__init__("tools"); self._tools_json = tools_json
|
67
|
+
def update(self, tools_json: List[Dict]):
|
68
|
+
self._tools_json = tools_json
|
69
|
+
self.mark_stale()
|
70
|
+
async def render(self) -> str: return f"<tools>{str(self._tools_json)}</tools>"
|
71
|
+
|
72
|
+
class Files(ContextProvider):
|
73
|
+
def __init__(self, *paths: Union[str, List[str]]):
|
74
|
+
super().__init__("files")
|
75
|
+
self._files: Dict[str, str] = {}
|
76
|
+
|
77
|
+
file_paths: List[str] = []
|
78
|
+
if paths:
|
79
|
+
# Handle the case where the first argument is a list of paths, e.g., Files(['a', 'b'])
|
80
|
+
if len(paths) == 1 and isinstance(paths[0], list):
|
81
|
+
file_paths.extend(paths[0])
|
82
|
+
# Handle the case where arguments are individual string paths, e.g., Files('a', 'b')
|
83
|
+
else:
|
84
|
+
file_paths.extend(paths)
|
85
|
+
|
86
|
+
if file_paths:
|
87
|
+
for path in file_paths:
|
88
|
+
try:
|
89
|
+
with open(path, 'r', encoding='utf-8') as f:
|
90
|
+
self._files[path] = f.read()
|
91
|
+
except FileNotFoundError:
|
92
|
+
logging.warning(f"File not found during initialization: {path}. Skipping.")
|
93
|
+
except Exception as e:
|
94
|
+
logging.error(f"Error reading file {path} during initialization: {e}")
|
95
|
+
|
96
|
+
async def refresh(self):
|
97
|
+
"""
|
98
|
+
Overrides the default refresh behavior. It synchronizes the content of
|
99
|
+
all tracked files with the file system. If a file is not found, its
|
100
|
+
content is updated to reflect the error.
|
101
|
+
"""
|
102
|
+
is_changed = False
|
103
|
+
for path in list(self._files.keys()):
|
104
|
+
try:
|
105
|
+
with open(path, 'r', encoding='utf-8') as f:
|
106
|
+
new_content = f.read()
|
107
|
+
if self._files.get(path) != new_content:
|
108
|
+
self._files[path] = new_content
|
109
|
+
is_changed = True
|
110
|
+
except FileNotFoundError:
|
111
|
+
error_msg = f"[Error: File not found at path '{path}']"
|
112
|
+
if self._files.get(path) != error_msg:
|
113
|
+
self._files[path] = error_msg
|
114
|
+
is_changed = True
|
115
|
+
except Exception as e:
|
116
|
+
error_msg = f"[Error: Could not read file at path '{path}': {e}]"
|
117
|
+
if self._files.get(path) != error_msg:
|
118
|
+
self._files[path] = error_msg
|
119
|
+
is_changed = True
|
120
|
+
|
121
|
+
if is_changed:
|
122
|
+
self.mark_stale()
|
123
|
+
|
124
|
+
await super().refresh()
|
125
|
+
|
126
|
+
def update(self, path: str, content: Optional[str] = None):
|
127
|
+
"""
|
128
|
+
Updates a single file. If content is provided, it updates the file in
|
129
|
+
memory. If content is None, it reads the file from disk.
|
130
|
+
"""
|
131
|
+
if content is not None:
|
132
|
+
self._files[path] = content
|
133
|
+
else:
|
134
|
+
try:
|
135
|
+
with open(path, 'r', encoding='utf-8') as f:
|
136
|
+
self._files[path] = f.read()
|
137
|
+
except FileNotFoundError:
|
138
|
+
logging.error(f"File not found for update: {path}.")
|
139
|
+
self._files[path] = f"[Error: File not found at path '{path}']"
|
140
|
+
except Exception as e:
|
141
|
+
logging.error(f"Error reading file for update {path}: {e}.")
|
142
|
+
self._files[path] = f"[Error: Could not read file at path '{path}': {e}]"
|
143
|
+
self.mark_stale()
|
144
|
+
async def render(self) -> str:
|
145
|
+
if not self._files: return None
|
146
|
+
return "<latest_file_content>" + "\n".join([f"<file><file_path>{p}</file_path><file_content>{c}</file_content></file>" for p, c in self._files.items()]) + "\n</latest_file_content>"
|
147
|
+
|
148
|
+
class Images(ContextProvider):
|
149
|
+
def __init__(self, url: str, name: Optional[str] = None):
|
150
|
+
super().__init__(name or url)
|
151
|
+
self.url = url
|
152
|
+
def update(self, url: str):
|
153
|
+
self.url = url
|
154
|
+
self.mark_stale()
|
155
|
+
async def render(self) -> Optional[str]:
|
156
|
+
if self.url.startswith("data:"):
|
157
|
+
return self.url
|
158
|
+
try:
|
159
|
+
with open(self.url, "rb") as image_file:
|
160
|
+
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
|
161
|
+
mime_type, _ = mimetypes.guess_type(self.url)
|
162
|
+
if not mime_type: mime_type = "application/octet-stream" # Fallback
|
163
|
+
return f"data:{mime_type};base64,{encoded_string}"
|
164
|
+
except FileNotFoundError:
|
165
|
+
logging.warning(f"Image file not found: {self.url}. Skipping.")
|
166
|
+
return None # Or handle error appropriately
|
167
|
+
|
168
|
+
# 3. 消息类 (已合并 MessageContent)
|
169
|
+
class Message(ABC):
|
170
|
+
def __init__(self, role: str, *initial_items: Union[ContextProvider, str, list]):
|
171
|
+
self.role = role
|
172
|
+
processed_items = []
|
173
|
+
for item in initial_items:
|
174
|
+
if isinstance(item, str):
|
175
|
+
processed_items.append(Texts(text=item))
|
176
|
+
elif isinstance(item, Message):
|
177
|
+
processed_items.extend(item.providers())
|
178
|
+
elif isinstance(item, ContextProvider):
|
179
|
+
processed_items.append(item)
|
180
|
+
elif isinstance(item, list):
|
181
|
+
for sub_item in item:
|
182
|
+
if not isinstance(sub_item, dict) or 'type' not in sub_item:
|
183
|
+
raise ValueError("List items must be dicts with a 'type' key.")
|
184
|
+
|
185
|
+
item_type = sub_item['type']
|
186
|
+
if item_type == 'text':
|
187
|
+
processed_items.append(Texts(text=sub_item.get('text', '')))
|
188
|
+
elif item_type == 'image_url':
|
189
|
+
image_url = sub_item.get('image_url', {}).get('url')
|
190
|
+
if image_url:
|
191
|
+
processed_items.append(Images(url=image_url))
|
192
|
+
else:
|
193
|
+
raise ValueError(f"Unsupported item type in list: {item_type}")
|
194
|
+
else:
|
195
|
+
raise TypeError(f"Unsupported item type: {type(item)}. Must be str, ContextProvider, or list.")
|
196
|
+
self._items: List[ContextProvider] = processed_items
|
197
|
+
self._parent_messages: Optional['Messages'] = None
|
198
|
+
|
199
|
+
def _render_content(self) -> str:
|
200
|
+
blocks = [item.get_content_block() for item in self._items]
|
201
|
+
return "\n\n".join(b.content for b in blocks if b and b.content)
|
202
|
+
|
203
|
+
def pop(self, name: str) -> Optional[ContextProvider]:
|
204
|
+
popped_item = None
|
205
|
+
for i, item in enumerate(self._items):
|
206
|
+
if hasattr(item, 'name') and item.name == name:
|
207
|
+
popped_item = self._items.pop(i)
|
208
|
+
break
|
209
|
+
if popped_item and self._parent_messages:
|
210
|
+
self._parent_messages._notify_provider_removed(popped_item)
|
211
|
+
return popped_item
|
212
|
+
|
213
|
+
def insert(self, index: int, item: ContextProvider):
|
214
|
+
self._items.insert(index, item)
|
215
|
+
if self._parent_messages:
|
216
|
+
self._parent_messages._notify_provider_added(item, self)
|
217
|
+
|
218
|
+
def append(self, item: ContextProvider):
|
219
|
+
self._items.append(item)
|
220
|
+
if self._parent_messages:
|
221
|
+
self._parent_messages._notify_provider_added(item, self)
|
222
|
+
|
223
|
+
def providers(self) -> List[ContextProvider]: return self._items
|
224
|
+
|
225
|
+
def __add__(self, other):
|
226
|
+
if isinstance(other, str):
|
227
|
+
new_items = self._items + [Texts(text=other)]
|
228
|
+
return type(self)(*new_items)
|
229
|
+
if isinstance(other, Message):
|
230
|
+
new_items = self._items + other.providers()
|
231
|
+
return type(self)(*new_items)
|
232
|
+
return NotImplemented
|
233
|
+
|
234
|
+
def __radd__(self, other):
|
235
|
+
if isinstance(other, str):
|
236
|
+
new_items = [Texts(text=other)] + self._items
|
237
|
+
return type(self)(*new_items)
|
238
|
+
if isinstance(other, Message):
|
239
|
+
new_items = other.providers() + self._items
|
240
|
+
return type(self)(*new_items)
|
241
|
+
return NotImplemented
|
242
|
+
|
243
|
+
def __getitem__(self, key: str) -> Any:
|
244
|
+
"""
|
245
|
+
使得 Message 对象支持字典风格的访问 (e.g., message['content'])。
|
246
|
+
"""
|
247
|
+
if key == 'role':
|
248
|
+
return self.role
|
249
|
+
elif key == 'content':
|
250
|
+
# 直接调用 to_dict 并提取 'content',确保逻辑一致
|
251
|
+
rendered_dict = self.to_dict()
|
252
|
+
return rendered_dict.get('content') if rendered_dict else None
|
253
|
+
# 对于 tool_calls 等特殊属性,也通过 to_dict 获取
|
254
|
+
elif hasattr(self, key):
|
255
|
+
rendered_dict = self.to_dict()
|
256
|
+
if rendered_dict and key in rendered_dict:
|
257
|
+
return rendered_dict[key]
|
258
|
+
|
259
|
+
# 如果在对象本身或其 to_dict() 中都找不到,则引发 KeyError
|
260
|
+
if hasattr(self, key):
|
261
|
+
return getattr(self, key)
|
262
|
+
raise KeyError(f"'{key}'")
|
263
|
+
|
264
|
+
def __repr__(self): return f"Message(role='{self.role}', items={[i.name for i in self._items]})"
|
265
|
+
def __bool__(self) -> bool:
|
266
|
+
return bool(self._items)
|
267
|
+
def get(self, key: str, default: Any = None) -> Any:
|
268
|
+
"""提供类似字典的 .get() 方法来访问属性。"""
|
269
|
+
return getattr(self, key, default)
|
270
|
+
def to_dict(self) -> Optional[Dict[str, Any]]:
|
271
|
+
is_multimodal = any(isinstance(p, Images) for p in self._items)
|
272
|
+
|
273
|
+
if not is_multimodal:
|
274
|
+
rendered_content = self._render_content()
|
275
|
+
if not rendered_content: return None
|
276
|
+
return {"role": self.role, "content": rendered_content}
|
277
|
+
else:
|
278
|
+
content_list = []
|
279
|
+
for item in self._items:
|
280
|
+
block = item.get_content_block()
|
281
|
+
if not block or not block.content: continue
|
282
|
+
if isinstance(item, Images):
|
283
|
+
content_list.append({"type": "image_url", "image_url": {"url": block.content}})
|
284
|
+
else:
|
285
|
+
content_list.append({"type": "text", "text": block.content})
|
286
|
+
if not content_list: return None
|
287
|
+
return {"role": self.role, "content": content_list}
|
288
|
+
|
289
|
+
class SystemMessage(Message):
|
290
|
+
def __init__(self, *items): super().__init__("system", *items)
|
291
|
+
class UserMessage(Message):
|
292
|
+
def __init__(self, *items): super().__init__("user", *items)
|
293
|
+
class AssistantMessage(Message):
|
294
|
+
def __init__(self, *items): super().__init__("assistant", *items)
|
295
|
+
|
296
|
+
class RoleMessage:
|
297
|
+
"""A factory class that creates a specific message type based on the role."""
|
298
|
+
def __new__(cls, role: str, *items):
|
299
|
+
if role == 'system':
|
300
|
+
return SystemMessage(*items)
|
301
|
+
elif role == 'user':
|
302
|
+
return UserMessage(*items)
|
303
|
+
elif role == 'assistant':
|
304
|
+
return AssistantMessage(*items)
|
305
|
+
else:
|
306
|
+
raise ValueError(f"Invalid role: {role}. Must be 'system', 'user', or 'assistant'.")
|
307
|
+
|
308
|
+
class ToolCalls(Message):
|
309
|
+
"""Represents an assistant message that requests tool calls."""
|
310
|
+
def __init__(self, tool_calls: List[Any]):
|
311
|
+
super().__init__("assistant")
|
312
|
+
self.tool_calls = tool_calls
|
313
|
+
|
314
|
+
def to_dict(self) -> Dict[str, Any]:
|
315
|
+
# Duck-typing serialization for OpenAI's tool_call objects
|
316
|
+
serialized_calls = []
|
317
|
+
for tc in self.tool_calls:
|
318
|
+
try:
|
319
|
+
# Attempt to serialize based on openai-python > 1.0 tool_call structure
|
320
|
+
func = tc.function
|
321
|
+
serialized_calls.append({
|
322
|
+
"id": tc.id,
|
323
|
+
"type": tc.type,
|
324
|
+
"function": { "name": func.name, "arguments": func.arguments }
|
325
|
+
})
|
326
|
+
except AttributeError:
|
327
|
+
if isinstance(tc, dict):
|
328
|
+
serialized_calls.append(tc) # Assume it's already a serializable dict
|
329
|
+
else:
|
330
|
+
raise TypeError(f"Unsupported tool_call type: {type(tc)}. It should be an OpenAI tool_call object or a dict.")
|
331
|
+
|
332
|
+
return {
|
333
|
+
"role": self.role,
|
334
|
+
"tool_calls": serialized_calls,
|
335
|
+
"content": None
|
336
|
+
}
|
337
|
+
|
338
|
+
class ToolResults(Message):
|
339
|
+
"""Represents a tool message with the result of a single tool call."""
|
340
|
+
def __init__(self, tool_call_id: str, content: str):
|
341
|
+
super().__init__("tool")
|
342
|
+
self.tool_call_id = tool_call_id
|
343
|
+
self.content = content
|
344
|
+
|
345
|
+
def to_dict(self) -> Dict[str, Any]:
|
346
|
+
return {
|
347
|
+
"role": self.role,
|
348
|
+
"tool_call_id": self.tool_call_id,
|
349
|
+
"content": self.content
|
350
|
+
}
|
351
|
+
|
352
|
+
# 4. 顶层容器: Messages
|
353
|
+
class Messages:
|
354
|
+
def __init__(self, *initial_messages: Message):
|
355
|
+
from typing import Tuple
|
356
|
+
self._messages: List[Message] = []
|
357
|
+
self._providers_index: Dict[str, Tuple[ContextProvider, Message]] = {}
|
358
|
+
if initial_messages:
|
359
|
+
for msg in initial_messages:
|
360
|
+
self.append(msg)
|
361
|
+
|
362
|
+
def _notify_provider_added(self, provider: ContextProvider, message: Message):
|
363
|
+
if provider.name not in self._providers_index:
|
364
|
+
self._providers_index[provider.name] = (provider, message)
|
365
|
+
|
366
|
+
def _notify_provider_removed(self, provider: ContextProvider):
|
367
|
+
if provider.name in self._providers_index:
|
368
|
+
del self._providers_index[provider.name]
|
369
|
+
|
370
|
+
def provider(self, name: str) -> Optional[ContextProvider]:
|
371
|
+
indexed = self._providers_index.get(name)
|
372
|
+
return indexed[0] if indexed else None
|
373
|
+
|
374
|
+
def pop(self, key: Optional[Union[str, int]] = None) -> Union[Optional[ContextProvider], Optional[Message]]:
|
375
|
+
# If no key is provided, pop the last message.
|
376
|
+
if key is None:
|
377
|
+
key = len(self._messages) - 1
|
378
|
+
|
379
|
+
if isinstance(key, str):
|
380
|
+
indexed = self._providers_index.get(key)
|
381
|
+
if not indexed:
|
382
|
+
return None
|
383
|
+
_provider, parent_message = indexed
|
384
|
+
return parent_message.pop(key)
|
385
|
+
elif isinstance(key, int):
|
386
|
+
try:
|
387
|
+
if key < 0: # Handle negative indices like -1
|
388
|
+
key += len(self._messages)
|
389
|
+
if not (0 <= key < len(self._messages)):
|
390
|
+
return None
|
391
|
+
popped_message = self._messages.pop(key)
|
392
|
+
popped_message._parent_messages = None
|
393
|
+
for provider in popped_message.providers():
|
394
|
+
self._notify_provider_removed(provider)
|
395
|
+
return popped_message
|
396
|
+
except IndexError:
|
397
|
+
return None
|
398
|
+
|
399
|
+
return None
|
400
|
+
|
401
|
+
async def refresh(self):
|
402
|
+
tasks = [provider.refresh() for provider, _ in self._providers_index.values()]
|
403
|
+
await asyncio.gather(*tasks)
|
404
|
+
|
405
|
+
def render(self) -> List[Dict[str, Any]]:
|
406
|
+
results = [msg.to_dict() for msg in self._messages]
|
407
|
+
return [res for res in results if res]
|
408
|
+
|
409
|
+
async def render_latest(self) -> List[Dict[str, Any]]:
|
410
|
+
await self.refresh()
|
411
|
+
return self.render()
|
412
|
+
|
413
|
+
def append(self, message: Message):
|
414
|
+
if self._messages and self._messages[-1].role == message.role:
|
415
|
+
last_message = self._messages[-1]
|
416
|
+
for provider in message.providers():
|
417
|
+
last_message.append(provider)
|
418
|
+
else:
|
419
|
+
message._parent_messages = self
|
420
|
+
self._messages.append(message)
|
421
|
+
for p in message.providers():
|
422
|
+
self._notify_provider_added(p, message)
|
423
|
+
|
424
|
+
def save(self, file_path: str):
|
425
|
+
"""
|
426
|
+
Saves the entire Messages object to a file using pickle.
|
427
|
+
Warning: Deserializing data with pickle from an untrusted source is insecure.
|
428
|
+
"""
|
429
|
+
with open(file_path, 'wb') as f:
|
430
|
+
pickle.dump(self, f)
|
431
|
+
|
432
|
+
@classmethod
|
433
|
+
def load(cls, file_path: str) -> Optional['Messages']:
|
434
|
+
"""
|
435
|
+
Loads a Messages object from a file using pickle.
|
436
|
+
Returns the loaded object, or None if the file is not found or an error occurs.
|
437
|
+
Warning: Only load files from a trusted source.
|
438
|
+
"""
|
439
|
+
try:
|
440
|
+
with open(file_path, 'rb') as f:
|
441
|
+
return pickle.load(f)
|
442
|
+
except FileNotFoundError:
|
443
|
+
logging.warning(f"File not found at {file_path}, returning empty Messages.")
|
444
|
+
return cls()
|
445
|
+
except (pickle.UnpicklingError, EOFError) as e:
|
446
|
+
logging.error(f"Could not deserialize file {file_path}: {e}")
|
447
|
+
return cls()
|
448
|
+
|
449
|
+
def __getitem__(self, index: int) -> Message: return self._messages[index]
|
450
|
+
def __len__(self) -> int: return len(self._messages)
|
451
|
+
def __iter__(self): return iter(self._messages)
|
@@ -0,0 +1,146 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
import asyncio
|
4
|
+
from openai import AsyncOpenAI
|
5
|
+
|
6
|
+
# 从我们设计的 architext 库中导入消息类
|
7
|
+
from architext.core import (
|
8
|
+
Messages,
|
9
|
+
SystemMessage,
|
10
|
+
UserMessage,
|
11
|
+
AssistantMessage,
|
12
|
+
ToolCalls,
|
13
|
+
ToolResults,
|
14
|
+
Texts,
|
15
|
+
)
|
16
|
+
|
17
|
+
def _add_tool(a: int, b: int) -> int:
|
18
|
+
"""(工具函数) 计算两个整数的和。"""
|
19
|
+
print(f"Executing tool: add(a={a}, b={b})")
|
20
|
+
return a + b
|
21
|
+
|
22
|
+
async def main():
|
23
|
+
"""
|
24
|
+
一个简化的、函数式的流程,用于处理单个包含工具调用的用户查询。
|
25
|
+
"""
|
26
|
+
print("Starting simplified Tool Use demonstration...")
|
27
|
+
|
28
|
+
# --- 1. 初始化 ---
|
29
|
+
# 确保环境变量已设置
|
30
|
+
if not os.getenv("API_KEY"):
|
31
|
+
print("\nERROR: API_KEY environment variable not set.")
|
32
|
+
return
|
33
|
+
|
34
|
+
client = AsyncOpenAI(base_url=os.getenv("BASE_URL"), api_key=os.getenv("API_KEY"))
|
35
|
+
model = os.getenv("MODEL", "gpt-4o-mini")
|
36
|
+
|
37
|
+
# 定义工具
|
38
|
+
tool_executors = { "add": _add_tool }
|
39
|
+
tools_definition = [{
|
40
|
+
"type": "function", "function": {
|
41
|
+
"name": "add", "description": "Calculate the sum of two integers.",
|
42
|
+
"parameters": {
|
43
|
+
"type": "object",
|
44
|
+
"properties": {
|
45
|
+
"a": {"type": "integer", "description": "The first integer."},
|
46
|
+
"b": {"type": "integer", "description": "The second integer."},
|
47
|
+
}, "required": ["a", "b"],
|
48
|
+
},
|
49
|
+
},
|
50
|
+
}]
|
51
|
+
|
52
|
+
# --- 2. 处理查询 ---
|
53
|
+
# 初始消息
|
54
|
+
messages = Messages(
|
55
|
+
SystemMessage(Texts("system_prompt", "You are a helpful assistant. You must use the provided tools to answer questions.")),
|
56
|
+
UserMessage(Texts("user_question", "What is the sum of 5 and 10?"))
|
57
|
+
)
|
58
|
+
|
59
|
+
# 第一次 API 调用
|
60
|
+
print("\n--- [Step 1] Calling OpenAI with tools...")
|
61
|
+
response = await client.chat.completions.create(
|
62
|
+
model=model,
|
63
|
+
messages=await messages.render_latest(),
|
64
|
+
tools=tools_definition,
|
65
|
+
tool_choice="auto",
|
66
|
+
)
|
67
|
+
response_message = response.choices[0].message
|
68
|
+
|
69
|
+
# 检查是否需要工具调用
|
70
|
+
if not response_message.tool_calls:
|
71
|
+
final_content = response_message.content or ""
|
72
|
+
messages.append(AssistantMessage(Texts("assistant_response", final_content)))
|
73
|
+
else:
|
74
|
+
# 执行工具调用
|
75
|
+
print("--- [Step 2] Assistant requested tool calls. Executing them...")
|
76
|
+
messages.append(ToolCalls(response_message.tool_calls))
|
77
|
+
|
78
|
+
for tool_call in response_message.tool_calls:
|
79
|
+
if tool_call.function is None: continue
|
80
|
+
|
81
|
+
executor = tool_executors.get(tool_call.function.name)
|
82
|
+
if not executor: continue
|
83
|
+
|
84
|
+
try:
|
85
|
+
args = json.loads(tool_call.function.arguments)
|
86
|
+
result = executor(**args)
|
87
|
+
messages.append(ToolResults(tool_call_id=tool_call.id, content=str(result)))
|
88
|
+
print(f" - Executed '{tool_call.function.name}'. Result: {result}")
|
89
|
+
except (json.JSONDecodeError, TypeError) as e:
|
90
|
+
print(f" - Error processing tool call '{tool_call.function.name}': {e}")
|
91
|
+
|
92
|
+
# 第二次 API 调用
|
93
|
+
print("--- [Step 3] Calling OpenAI with tool results for final answer...")
|
94
|
+
final_response = await client.chat.completions.create(
|
95
|
+
model=model,
|
96
|
+
messages=await messages.render_latest(),
|
97
|
+
)
|
98
|
+
final_content = final_response.choices[0].message.content or ""
|
99
|
+
messages.append(AssistantMessage(Texts("final_response", final_content)))
|
100
|
+
|
101
|
+
# --- 3. 显示结果 ---
|
102
|
+
print("\n--- Final request body sent to OpenAI: ---")
|
103
|
+
print(json.dumps(await messages.render_latest(), indent=2, ensure_ascii=False))
|
104
|
+
|
105
|
+
print("\n--- Final Assistant Answer ---")
|
106
|
+
print(final_content)
|
107
|
+
print("\nDemonstration finished.")
|
108
|
+
|
109
|
+
if __name__ == "__main__":
|
110
|
+
asyncio.run(main())
|
111
|
+
|
112
|
+
"""
|
113
|
+
[
|
114
|
+
{
|
115
|
+
"role": "system",
|
116
|
+
"content": "You are a helpful assistant. You must use the provided tools to answer questions."
|
117
|
+
},
|
118
|
+
{
|
119
|
+
"role": "user",
|
120
|
+
"content": "What is the sum of 5 and 10?"
|
121
|
+
},
|
122
|
+
{
|
123
|
+
"role": "assistant",
|
124
|
+
"tool_calls": [
|
125
|
+
{
|
126
|
+
"id": "call_rddWXkDikIxllRgbPrR6XjtMVSBPv",
|
127
|
+
"type": "function",
|
128
|
+
"function": {
|
129
|
+
"name": "add",
|
130
|
+
"arguments": "{\"b\": 10, \"a\": 5}"
|
131
|
+
}
|
132
|
+
}
|
133
|
+
],
|
134
|
+
"content": null
|
135
|
+
},
|
136
|
+
{
|
137
|
+
"role": "tool",
|
138
|
+
"tool_call_id": "call_rddWXkDikIxllRgbPrR6XjtMVSBPv",
|
139
|
+
"content": "15"
|
140
|
+
},
|
141
|
+
{
|
142
|
+
"role": "assistant",
|
143
|
+
"content": "The sum of 5 and 10 is 15."
|
144
|
+
}
|
145
|
+
]
|
146
|
+
"""
|