beswarm 0.2.82__py3-none-any.whl → 0.2.83__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.
- beswarm/agents/planact.py +24 -71
- beswarm/aient/aient/architext/architext/core.py +314 -56
- beswarm/aient/aient/architext/test/test.py +511 -28
- beswarm/aient/aient/models/chatgpt.py +9 -8
- beswarm/prompt.py +44 -17
- {beswarm-0.2.82.dist-info → beswarm-0.2.83.dist-info}/METADATA +1 -1
- {beswarm-0.2.82.dist-info → beswarm-0.2.83.dist-info}/RECORD +9 -9
- {beswarm-0.2.82.dist-info → beswarm-0.2.83.dist-info}/WHEEL +0 -0
- {beswarm-0.2.82.dist-info → beswarm-0.2.83.dist-info}/top_level.txt +0 -0
@@ -4,9 +4,51 @@ 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
|
-
from typing import List, Dict, Any, Optional, Union
|
11
|
+
from typing import List, Dict, Any, Optional, Union, Callable
|
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
|
+
|
37
|
+
# Global, thread-safe registry for providers created within f-strings
|
38
|
+
_fstring_provider_registry = {}
|
39
|
+
_registry_lock = threading.Lock()
|
40
|
+
|
41
|
+
def _register_provider(provider: 'ContextProvider') -> str:
|
42
|
+
"""Registers a provider and returns a unique placeholder."""
|
43
|
+
with _registry_lock:
|
44
|
+
provider_id = f"__provider_placeholder_{uuid.uuid4().hex}__"
|
45
|
+
_fstring_provider_registry[provider_id] = provider
|
46
|
+
return provider_id
|
47
|
+
|
48
|
+
def _retrieve_provider(placeholder: str) -> Optional['ContextProvider']:
|
49
|
+
"""Retrieves a provider from the registry."""
|
50
|
+
with _registry_lock:
|
51
|
+
return _fstring_provider_registry.pop(placeholder, None)
|
10
52
|
|
11
53
|
# 1. 核心数据结构: ContentBlock
|
12
54
|
@dataclass
|
@@ -17,8 +59,30 @@ class ContentBlock:
|
|
17
59
|
# 2. 上下文提供者 (带缓存)
|
18
60
|
class ContextProvider(ABC):
|
19
61
|
def __init__(self, name: str):
|
20
|
-
self.name = name
|
62
|
+
self.name = name
|
63
|
+
self._cached_content: Optional[str] = None
|
64
|
+
self._is_stale: bool = True
|
65
|
+
self._visible: bool = True
|
66
|
+
|
67
|
+
def __str__(self):
|
68
|
+
# This allows the object to be captured when used inside an f-string.
|
69
|
+
return _register_provider(self)
|
70
|
+
|
21
71
|
def mark_stale(self): self._is_stale = True
|
72
|
+
|
73
|
+
@property
|
74
|
+
def visible(self) -> bool:
|
75
|
+
"""Gets the visibility of the provider."""
|
76
|
+
return self._visible
|
77
|
+
|
78
|
+
@visible.setter
|
79
|
+
def visible(self, value: bool):
|
80
|
+
"""Sets the visibility of the provider."""
|
81
|
+
if self._visible != value:
|
82
|
+
self._visible = value
|
83
|
+
# Content needs to be re-evaluated, but the source data hasn't changed,
|
84
|
+
# so just marking it stale is enough for the renderer to reconsider it.
|
85
|
+
self.mark_stale()
|
22
86
|
async def refresh(self):
|
23
87
|
if self._is_stale:
|
24
88
|
self._cached_content = await self.render()
|
@@ -28,35 +92,113 @@ class ContextProvider(ABC):
|
|
28
92
|
@abstractmethod
|
29
93
|
def update(self, *args, **kwargs): raise NotImplementedError
|
30
94
|
def get_content_block(self) -> Optional[ContentBlock]:
|
31
|
-
if self._cached_content is not None:
|
95
|
+
if self.visible and self._cached_content is not None:
|
96
|
+
return ContentBlock(self.name, self._cached_content)
|
32
97
|
return None
|
33
98
|
|
34
99
|
class Texts(ContextProvider):
|
35
|
-
def __init__(self, text: str, name: Optional[str] = None):
|
36
|
-
|
100
|
+
def __init__(self, text: Optional[Union[str, Callable[[], str]]] = None, name: Optional[str] = None):
|
101
|
+
if text is None and name is None:
|
102
|
+
raise ValueError("Either 'text' or 'name' must be provided.")
|
103
|
+
|
104
|
+
# Ensure that non-callable inputs are treated as strings
|
105
|
+
if not callable(text):
|
106
|
+
self._text = str(text) if text is not None else None
|
107
|
+
else:
|
108
|
+
self._text = text
|
109
|
+
|
110
|
+
self._is_dynamic = callable(self._text)
|
111
|
+
|
37
112
|
if name is None:
|
38
|
-
|
39
|
-
|
113
|
+
if self._is_dynamic:
|
114
|
+
import uuid
|
115
|
+
_name = f"dynamic_text_{uuid.uuid4().hex[:8]}"
|
116
|
+
else:
|
117
|
+
# Handle the case where text is None during initialization
|
118
|
+
h = hashlib.sha1(self._text.encode() if self._text else b'').hexdigest()
|
119
|
+
_name = f"text_{h[:8]}"
|
40
120
|
else:
|
41
121
|
_name = name
|
42
122
|
super().__init__(_name)
|
43
123
|
|
44
|
-
def
|
124
|
+
async def refresh(self):
|
125
|
+
if self._is_dynamic:
|
126
|
+
self._is_stale = True
|
127
|
+
await super().refresh()
|
128
|
+
|
129
|
+
def update(self, text: Union[str, Callable[[], str]]):
|
45
130
|
self._text = text
|
131
|
+
self._is_dynamic = callable(self._text)
|
46
132
|
self.mark_stale()
|
47
133
|
|
48
|
-
|
134
|
+
@property
|
135
|
+
def content(self) -> Optional[str]:
|
136
|
+
"""
|
137
|
+
Synchronously retrieves the raw text content as a property.
|
138
|
+
If the content is dynamic (a callable), it executes the callable.
|
139
|
+
"""
|
140
|
+
if self._is_dynamic:
|
141
|
+
# Ensure dynamic content returns a string, even if empty
|
142
|
+
result = self._text()
|
143
|
+
return result if result is not None else ""
|
144
|
+
# Ensure static content returns a string, even if empty
|
145
|
+
return self._text if self._text is not None else ""
|
146
|
+
|
147
|
+
async def render(self) -> Optional[str]:
|
148
|
+
return self.content
|
149
|
+
|
150
|
+
def __getstate__(self):
|
151
|
+
"""Custom state for pickling."""
|
152
|
+
state = self.__dict__.copy()
|
153
|
+
if self._is_dynamic:
|
154
|
+
# For dynamic content, we snapshot its current value for serialization.
|
155
|
+
# The lambda function itself cannot be pickled.
|
156
|
+
try:
|
157
|
+
# Evaluate the lambda and store it as a static string
|
158
|
+
state['_text'] = self.content
|
159
|
+
# Mark it as no longer dynamic in the pickled state
|
160
|
+
state['_is_dynamic'] = False
|
161
|
+
except Exception as e:
|
162
|
+
# If the lambda fails for some reason, store an error message.
|
163
|
+
logging.error(f"Error evaluating dynamic text '{self.name}' during pickling: {e}")
|
164
|
+
state['_text'] = f"[Error: Could not evaluate dynamic content during save: {e}]"
|
165
|
+
state['_is_dynamic'] = False
|
166
|
+
return state
|
167
|
+
|
168
|
+
def __setstate__(self, state):
|
169
|
+
"""Custom state for unpickling."""
|
170
|
+
# Just restore the dictionary. The transformation is one-way.
|
171
|
+
self.__dict__.update(state)
|
172
|
+
|
173
|
+
def __eq__(self, other):
|
174
|
+
if not isinstance(other, Texts):
|
175
|
+
return NotImplemented
|
176
|
+
# If either object is dynamic, they are only equal if they are the exact same object.
|
177
|
+
if self._is_dynamic or (hasattr(other, '_is_dynamic') and other._is_dynamic):
|
178
|
+
return self is other
|
179
|
+
# For static content, compare the actual content.
|
180
|
+
return self.content == other.content
|
49
181
|
|
50
182
|
class Tools(ContextProvider):
|
51
|
-
def __init__(self, tools_json: List[Dict]
|
183
|
+
def __init__(self, tools_json: Optional[List[Dict]] = None, name: str = "tools"):
|
184
|
+
super().__init__(name)
|
185
|
+
self._tools_json = tools_json or []
|
52
186
|
def update(self, tools_json: List[Dict]):
|
53
187
|
self._tools_json = tools_json
|
54
188
|
self.mark_stale()
|
55
|
-
async def render(self) -> str:
|
189
|
+
async def render(self) -> Optional[str]:
|
190
|
+
if not self._tools_json:
|
191
|
+
return None
|
192
|
+
return f"<tools>{str(self._tools_json)}</tools>"
|
193
|
+
|
194
|
+
def __eq__(self, other):
|
195
|
+
if not isinstance(other, Tools):
|
196
|
+
return NotImplemented
|
197
|
+
return self._tools_json == other._tools_json
|
56
198
|
|
57
199
|
class Files(ContextProvider):
|
58
|
-
def __init__(self, *paths: Union[str, List[str]]):
|
59
|
-
super().__init__(
|
200
|
+
def __init__(self, *paths: Union[str, List[str]], name: str = "files"):
|
201
|
+
super().__init__(name)
|
60
202
|
self._files: Dict[str, str] = {}
|
61
203
|
|
62
204
|
file_paths: List[str] = []
|
@@ -130,6 +272,11 @@ class Files(ContextProvider):
|
|
130
272
|
if not self._files: return None
|
131
273
|
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>"
|
132
274
|
|
275
|
+
def __eq__(self, other):
|
276
|
+
if not isinstance(other, Files):
|
277
|
+
return NotImplemented
|
278
|
+
return self._files == other._files
|
279
|
+
|
133
280
|
class Images(ContextProvider):
|
134
281
|
def __init__(self, url: str, name: Optional[str] = None):
|
135
282
|
super().__init__(name or url)
|
@@ -150,6 +297,11 @@ class Images(ContextProvider):
|
|
150
297
|
logging.warning(f"Image file not found: {self.url}. Skipping.")
|
151
298
|
return None # Or handle error appropriately
|
152
299
|
|
300
|
+
def __eq__(self, other):
|
301
|
+
if not isinstance(other, Images):
|
302
|
+
return NotImplemented
|
303
|
+
return self.url == other.url
|
304
|
+
|
153
305
|
# 3. 消息类 (已合并 MessageContent)
|
154
306
|
class Message(ABC):
|
155
307
|
def __init__(self, role: str, *initial_items: Union[ContextProvider, str, list]):
|
@@ -157,9 +309,25 @@ class Message(ABC):
|
|
157
309
|
processed_items = []
|
158
310
|
for item in initial_items:
|
159
311
|
if isinstance(item, str):
|
160
|
-
|
312
|
+
# Check if the string contains placeholders from f-string rendering
|
313
|
+
import re
|
314
|
+
placeholder_pattern = re.compile(r'(__provider_placeholder_[a-f0-9]{32}__)')
|
315
|
+
parts = placeholder_pattern.split(item)
|
316
|
+
|
317
|
+
if len(parts) > 1: # Placeholders were found
|
318
|
+
for part in parts:
|
319
|
+
if not part: continue
|
320
|
+
if placeholder_pattern.match(part):
|
321
|
+
provider = _retrieve_provider(part)
|
322
|
+
if provider:
|
323
|
+
processed_items.append(provider)
|
324
|
+
else:
|
325
|
+
processed_items.append(Texts(text=part))
|
326
|
+
else: # No placeholders, just a regular string
|
327
|
+
processed_items.append(Texts(text=item))
|
328
|
+
|
161
329
|
elif isinstance(item, Message):
|
162
|
-
processed_items.extend(item.
|
330
|
+
processed_items.extend(item.provider())
|
163
331
|
elif isinstance(item, ContextProvider):
|
164
332
|
processed_items.append(item)
|
165
333
|
elif isinstance(item, list):
|
@@ -181,9 +349,24 @@ class Message(ABC):
|
|
181
349
|
self._items: List[ContextProvider] = processed_items
|
182
350
|
self._parent_messages: Optional['Messages'] = None
|
183
351
|
|
352
|
+
@property
|
353
|
+
def content(self) -> Optional[Union[str, List[Dict[str, Any]]]]:
|
354
|
+
"""
|
355
|
+
Renders the message content.
|
356
|
+
For simple text messages, returns a string.
|
357
|
+
For multimodal messages, returns a list of content blocks.
|
358
|
+
"""
|
359
|
+
rendered_dict = self.to_dict()
|
360
|
+
return rendered_dict.get('content') if rendered_dict else None
|
361
|
+
|
184
362
|
def _render_content(self) -> str:
|
185
|
-
|
186
|
-
|
363
|
+
final_parts = []
|
364
|
+
for item in self._items:
|
365
|
+
block = item.get_content_block()
|
366
|
+
if block and block.content is not None:
|
367
|
+
final_parts.append(block.content)
|
368
|
+
|
369
|
+
return "".join(final_parts)
|
187
370
|
|
188
371
|
def pop(self, name: str) -> Optional[ContextProvider]:
|
189
372
|
popped_item = None
|
@@ -205,14 +388,24 @@ class Message(ABC):
|
|
205
388
|
if self._parent_messages:
|
206
389
|
self._parent_messages._notify_provider_added(item, self)
|
207
390
|
|
208
|
-
def
|
391
|
+
def provider(self, name: Optional[str] = None) -> Optional[Union[ContextProvider, ProviderGroup, List[ContextProvider]]]:
|
392
|
+
if name is None:
|
393
|
+
return self._items
|
394
|
+
|
395
|
+
named_providers = [p for p in self._items if hasattr(p, 'name') and p.name == name]
|
396
|
+
|
397
|
+
if not named_providers:
|
398
|
+
return None
|
399
|
+
if len(named_providers) == 1:
|
400
|
+
return named_providers[0]
|
401
|
+
return ProviderGroup(named_providers)
|
209
402
|
|
210
403
|
def __add__(self, other):
|
211
404
|
if isinstance(other, str):
|
212
405
|
new_items = self._items + [Texts(text=other)]
|
213
406
|
return type(self)(*new_items)
|
214
407
|
if isinstance(other, Message):
|
215
|
-
new_items = self._items + other.
|
408
|
+
new_items = self._items + other.provider()
|
216
409
|
return type(self)(*new_items)
|
217
410
|
return NotImplemented
|
218
411
|
|
@@ -221,30 +414,40 @@ class Message(ABC):
|
|
221
414
|
new_items = [Texts(text=other)] + self._items
|
222
415
|
return type(self)(*new_items)
|
223
416
|
if isinstance(other, Message):
|
224
|
-
new_items = other.
|
417
|
+
new_items = other.provider() + self._items
|
225
418
|
return type(self)(*new_items)
|
226
419
|
return NotImplemented
|
227
420
|
|
228
|
-
def __getitem__(self, key: str) -> Any:
|
421
|
+
def __getitem__(self, key: Union[str, int]) -> Any:
|
229
422
|
"""
|
230
|
-
使得 Message 对象支持字典风格的访问 (e.g., message['content'])
|
423
|
+
使得 Message 对象支持字典风格的访问 (e.g., message['content'])
|
424
|
+
和列表风格的索引访问 (e.g., message[-1])。
|
231
425
|
"""
|
232
|
-
if key
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
426
|
+
if isinstance(key, str):
|
427
|
+
if key == 'role':
|
428
|
+
return self.role
|
429
|
+
elif key == 'content':
|
430
|
+
# 直接调用 to_dict 并提取 'content',确保逻辑一致
|
431
|
+
rendered_dict = self.to_dict()
|
432
|
+
return rendered_dict.get('content') if rendered_dict else None
|
433
|
+
# 对于 tool_calls 等特殊属性,也通过 to_dict 获取
|
434
|
+
elif hasattr(self, key):
|
435
|
+
rendered_dict = self.to_dict()
|
436
|
+
if rendered_dict and key in rendered_dict:
|
437
|
+
return rendered_dict[key]
|
438
|
+
|
439
|
+
# 如果在对象本身或其 to_dict() 中都找不到,则引发 KeyError
|
440
|
+
if hasattr(self, key):
|
441
|
+
return getattr(self, key)
|
442
|
+
raise KeyError(f"'{key}'")
|
443
|
+
elif isinstance(key, int):
|
444
|
+
return self._items[key]
|
445
|
+
else:
|
446
|
+
raise TypeError(f"Message indices must be integers or strings, not {type(key).__name__}")
|
447
|
+
|
448
|
+
def __len__(self) -> int:
|
449
|
+
"""返回消息中 provider 的数量。"""
|
450
|
+
return len(self._items)
|
248
451
|
|
249
452
|
def __repr__(self): return f"Message(role='{self.role}', items={[i.name for i in self._items]})"
|
250
453
|
def __bool__(self) -> bool:
|
@@ -323,15 +526,17 @@ class ToolCalls(Message):
|
|
323
526
|
class ToolResults(Message):
|
324
527
|
"""Represents a tool message with the result of a single tool call."""
|
325
528
|
def __init__(self, tool_call_id: str, content: str):
|
326
|
-
|
529
|
+
# We pass a Texts provider to the parent so it can be rendered,
|
530
|
+
# but the primary way to access content for ToolResults is via its dict representation.
|
531
|
+
super().__init__("tool", Texts(text=content))
|
327
532
|
self.tool_call_id = tool_call_id
|
328
|
-
self.
|
533
|
+
self._content = content
|
329
534
|
|
330
535
|
def to_dict(self) -> Dict[str, Any]:
|
331
536
|
return {
|
332
537
|
"role": self.role,
|
333
538
|
"tool_call_id": self.tool_call_id,
|
334
|
-
"content": self.
|
539
|
+
"content": self._content
|
335
540
|
}
|
336
541
|
|
337
542
|
# 4. 顶层容器: Messages
|
@@ -339,22 +544,40 @@ class Messages:
|
|
339
544
|
def __init__(self, *initial_messages: Message):
|
340
545
|
from typing import Tuple
|
341
546
|
self._messages: List[Message] = []
|
342
|
-
self._providers_index: Dict[str, Tuple[ContextProvider, Message]] = {}
|
547
|
+
self._providers_index: Dict[str, List[Tuple[ContextProvider, Message]]] = {}
|
343
548
|
if initial_messages:
|
344
549
|
for msg in initial_messages:
|
345
550
|
self.append(msg)
|
346
551
|
|
347
552
|
def _notify_provider_added(self, provider: ContextProvider, message: Message):
|
348
553
|
if provider.name not in self._providers_index:
|
349
|
-
self._providers_index[provider.name] =
|
554
|
+
self._providers_index[provider.name] = []
|
555
|
+
self._providers_index[provider.name].append((provider, message))
|
350
556
|
|
351
557
|
def _notify_provider_removed(self, provider: ContextProvider):
|
352
558
|
if provider.name in self._providers_index:
|
353
|
-
|
559
|
+
# Create a new list excluding the provider to be removed.
|
560
|
+
# Comparing by object identity (`is`) is crucial here.
|
561
|
+
providers_list = self._providers_index[provider.name]
|
562
|
+
new_list = [(p, m) for p, m in providers_list if p is not provider]
|
563
|
+
|
564
|
+
if not new_list:
|
565
|
+
# If the list becomes empty, remove the key from the dictionary.
|
566
|
+
del self._providers_index[provider.name]
|
567
|
+
else:
|
568
|
+
# Otherwise, update the dictionary with the new list.
|
569
|
+
self._providers_index[provider.name] = new_list
|
570
|
+
|
571
|
+
def provider(self, name: str) -> Optional[Union[ContextProvider, ProviderGroup]]:
|
572
|
+
indexed_list = self._providers_index.get(name)
|
573
|
+
if not indexed_list:
|
574
|
+
return None
|
354
575
|
|
355
|
-
|
356
|
-
|
357
|
-
|
576
|
+
providers = [p for p, m in indexed_list]
|
577
|
+
if len(providers) == 1:
|
578
|
+
return providers[0]
|
579
|
+
else:
|
580
|
+
return ProviderGroup(providers)
|
358
581
|
|
359
582
|
def pop(self, key: Optional[Union[str, int]] = None) -> Union[Optional[ContextProvider], Optional[Message]]:
|
360
583
|
# If no key is provided, pop the last message.
|
@@ -362,10 +585,13 @@ class Messages:
|
|
362
585
|
key = len(self._messages) - 1
|
363
586
|
|
364
587
|
if isinstance(key, str):
|
365
|
-
|
366
|
-
if not
|
588
|
+
indexed_list = self._providers_index.get(key)
|
589
|
+
if not indexed_list:
|
367
590
|
return None
|
368
|
-
|
591
|
+
# Pop the first one found, which is consistent with how pop usually works
|
592
|
+
_provider, parent_message = indexed_list[0]
|
593
|
+
# The actual removal from _providers_index happens in _notify_provider_removed
|
594
|
+
# which is called by message.pop()
|
369
595
|
return parent_message.pop(key)
|
370
596
|
elif isinstance(key, int):
|
371
597
|
try:
|
@@ -375,7 +601,7 @@ class Messages:
|
|
375
601
|
return None
|
376
602
|
popped_message = self._messages.pop(key)
|
377
603
|
popped_message._parent_messages = None
|
378
|
-
for provider in popped_message.
|
604
|
+
for provider in popped_message.provider():
|
379
605
|
self._notify_provider_removed(provider)
|
380
606
|
return popped_message
|
381
607
|
except IndexError:
|
@@ -384,7 +610,10 @@ class Messages:
|
|
384
610
|
return None
|
385
611
|
|
386
612
|
async def refresh(self):
|
387
|
-
tasks = [
|
613
|
+
tasks = []
|
614
|
+
for provider_list in self._providers_index.values():
|
615
|
+
for provider, _ in provider_list:
|
616
|
+
tasks.append(provider.refresh())
|
388
617
|
await asyncio.gather(*tasks)
|
389
618
|
|
390
619
|
def render(self) -> List[Dict[str, Any]]:
|
@@ -398,12 +627,12 @@ class Messages:
|
|
398
627
|
def append(self, message: Message):
|
399
628
|
if self._messages and self._messages[-1].role == message.role:
|
400
629
|
last_message = self._messages[-1]
|
401
|
-
for provider in message.
|
630
|
+
for provider in message.provider():
|
402
631
|
last_message.append(provider)
|
403
632
|
else:
|
404
633
|
message._parent_messages = self
|
405
634
|
self._messages.append(message)
|
406
|
-
for p in message.
|
635
|
+
for p in message.provider():
|
407
636
|
self._notify_provider_added(p, message)
|
408
637
|
|
409
638
|
def save(self, file_path: str):
|
@@ -425,12 +654,41 @@ class Messages:
|
|
425
654
|
with open(file_path, 'rb') as f:
|
426
655
|
return pickle.load(f)
|
427
656
|
except FileNotFoundError:
|
428
|
-
logging.warning(f"File not found at {file_path}, returning empty Messages.")
|
657
|
+
# logging.warning(f"File not found at {file_path}, returning empty Messages.")
|
429
658
|
return cls()
|
430
659
|
except (pickle.UnpicklingError, EOFError) as e:
|
431
660
|
logging.error(f"Could not deserialize file {file_path}: {e}")
|
432
661
|
return cls()
|
433
662
|
|
434
|
-
def __getitem__(self, index: int) -> Message
|
663
|
+
def __getitem__(self, index: Union[int, slice]) -> Union[Message, 'Messages']:
|
664
|
+
if isinstance(index, slice):
|
665
|
+
return Messages(*self._messages[index])
|
666
|
+
return self._messages[index]
|
667
|
+
|
668
|
+
def __setitem__(self, index: slice, value: 'Messages'):
|
669
|
+
if not isinstance(index, slice) or not isinstance(value, Messages):
|
670
|
+
raise TypeError("Unsupported operand type(s) for slice assignment")
|
671
|
+
|
672
|
+
# Basic slice assignment logic.
|
673
|
+
# A more robust implementation would handle step and negative indices.
|
674
|
+
start, stop, step = index.indices(len(self._messages))
|
675
|
+
|
676
|
+
if step != 1:
|
677
|
+
raise ValueError("Slice assignment with step is not supported.")
|
678
|
+
|
679
|
+
# Remove old providers from the index
|
680
|
+
for i in range(start, stop):
|
681
|
+
for provider in self._messages[i].provider():
|
682
|
+
self._notify_provider_removed(provider)
|
683
|
+
|
684
|
+
# Replace the slice in the list
|
685
|
+
self._messages[start:stop] = value._messages
|
686
|
+
|
687
|
+
# Add new providers to the index and set parent
|
688
|
+
for msg in value:
|
689
|
+
msg._parent_messages = self
|
690
|
+
for provider in msg.provider():
|
691
|
+
self._notify_provider_added(provider, msg)
|
692
|
+
|
435
693
|
def __len__(self) -> int: return len(self._messages)
|
436
694
|
def __iter__(self): return iter(self._messages)
|