beswarm 0.3.12__tar.gz → 0.3.14__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.
Files changed (138) hide show
  1. {beswarm-0.3.12 → beswarm-0.3.14}/PKG-INFO +1 -1
  2. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/architext/architext/core.py +92 -41
  3. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/architext/test/test.py +127 -0
  4. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/core/request.py +10 -7
  5. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/core/utils.py +1 -1
  6. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/models/chatgpt.py +18 -1
  7. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/read_file.py +15 -23
  8. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm.egg-info/PKG-INFO +1 -1
  9. {beswarm-0.3.12 → beswarm-0.3.14}/pyproject.toml +1 -1
  10. {beswarm-0.3.12 → beswarm-0.3.14}/MANIFEST.in +0 -0
  11. {beswarm-0.3.12 → beswarm-0.3.14}/README.md +0 -0
  12. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/__init__.py +0 -0
  13. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/agents/chatgroup.py +0 -0
  14. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/agents/planact.py +0 -0
  15. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/__init__.py +0 -0
  16. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/architext/architext/__init__.py +0 -0
  17. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/architext/test/openai_client.py +0 -0
  18. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/architext/test/test_save_load.py +0 -0
  19. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/core/__init__.py +0 -0
  20. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/core/log_config.py +0 -0
  21. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/core/models.py +0 -0
  22. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/core/response.py +0 -0
  23. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/core/test/test_base_api.py +0 -0
  24. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/core/test/test_geminimask.py +0 -0
  25. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/core/test/test_image.py +0 -0
  26. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/core/test/test_payload.py +0 -0
  27. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/models/__init__.py +0 -0
  28. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/models/audio.py +0 -0
  29. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/models/base.py +0 -0
  30. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/plugins/__init__.py +0 -0
  31. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/plugins/arXiv.py +0 -0
  32. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/plugins/config.py +0 -0
  33. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/plugins/excute_command.py +0 -0
  34. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/plugins/get_time.py +0 -0
  35. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/plugins/image.py +0 -0
  36. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/plugins/list_directory.py +0 -0
  37. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/plugins/read_image.py +0 -0
  38. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/plugins/readonly.py +0 -0
  39. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/plugins/registry.py +0 -0
  40. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/plugins/run_python.py +0 -0
  41. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/plugins/websearch.py +0 -0
  42. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/utils/__init__.py +0 -0
  43. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/utils/prompt.py +0 -0
  44. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/aient/utils/scripts.py +0 -0
  45. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/test/test_Web_crawler.py +0 -0
  46. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/test/test_ddg_search.py +0 -0
  47. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/test/test_google_search.py +0 -0
  48. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/test/test_ollama.py +0 -0
  49. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/test/test_plugin.py +0 -0
  50. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/test/test_url.py +0 -0
  51. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/aient/test/test_whisper.py +0 -0
  52. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/bemcp/bemcp/__init__.py +0 -0
  53. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/bemcp/bemcp/decorator.py +0 -0
  54. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/bemcp/bemcp/main.py +0 -0
  55. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/bemcp/bemcp/utils.py +0 -0
  56. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/bemcp/test/client.py +0 -0
  57. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/bemcp/test/server.py +0 -0
  58. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/broker.py +0 -0
  59. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/cli.py +0 -0
  60. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/core.py +0 -0
  61. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/knowledge_graph.py +0 -0
  62. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/prompt.py +0 -0
  63. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/README.md +0 -0
  64. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/arduino-tags.scm +0 -0
  65. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/c-tags.scm +0 -0
  66. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/chatito-tags.scm +0 -0
  67. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/commonlisp-tags.scm +0 -0
  68. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/cpp-tags.scm +0 -0
  69. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/csharp-tags.scm +0 -0
  70. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/d-tags.scm +0 -0
  71. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/dart-tags.scm +0 -0
  72. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/elisp-tags.scm +0 -0
  73. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/elixir-tags.scm +0 -0
  74. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/elm-tags.scm +0 -0
  75. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/gleam-tags.scm +0 -0
  76. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/go-tags.scm +0 -0
  77. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/java-tags.scm +0 -0
  78. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/javascript-tags.scm +0 -0
  79. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/lua-tags.scm +0 -0
  80. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/pony-tags.scm +0 -0
  81. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/properties-tags.scm +0 -0
  82. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/python-tags.scm +0 -0
  83. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/r-tags.scm +0 -0
  84. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/racket-tags.scm +0 -0
  85. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/ruby-tags.scm +0 -0
  86. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/rust-tags.scm +0 -0
  87. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/solidity-tags.scm +0 -0
  88. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/swift-tags.scm +0 -0
  89. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-language-pack/udev-tags.scm +0 -0
  90. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/README.md +0 -0
  91. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/c-tags.scm +0 -0
  92. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/c_sharp-tags.scm +0 -0
  93. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/cpp-tags.scm +0 -0
  94. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/dart-tags.scm +0 -0
  95. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/elisp-tags.scm +0 -0
  96. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/elixir-tags.scm +0 -0
  97. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/elm-tags.scm +0 -0
  98. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/go-tags.scm +0 -0
  99. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/hcl-tags.scm +0 -0
  100. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/java-tags.scm +0 -0
  101. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/javascript-tags.scm +0 -0
  102. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/kotlin-tags.scm +0 -0
  103. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/ocaml-tags.scm +0 -0
  104. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/php-tags.scm +0 -0
  105. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/python-tags.scm +0 -0
  106. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/ql-tags.scm +0 -0
  107. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/ruby-tags.scm +0 -0
  108. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/rust-tags.scm +0 -0
  109. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/scala-tags.scm +0 -0
  110. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/queries/tree-sitter-languages/typescript-tags.scm +0 -0
  111. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/taskmanager.py +0 -0
  112. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/__init__.py +0 -0
  113. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/click.py +0 -0
  114. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/completion.py +0 -0
  115. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/deep_search.py +0 -0
  116. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/edit_file.py +0 -0
  117. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/graph.py +0 -0
  118. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/planner.py +0 -0
  119. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/repomap.py +0 -0
  120. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/request_input.py +0 -0
  121. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/screenshot.py +0 -0
  122. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/search_arxiv.py +0 -0
  123. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/search_web.py +0 -0
  124. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/subtasks.py +0 -0
  125. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/worker.py +0 -0
  126. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/write_csv.py +0 -0
  127. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/tools/write_file.py +0 -0
  128. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm/utils.py +0 -0
  129. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm.egg-info/SOURCES.txt +0 -0
  130. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm.egg-info/dependency_links.txt +0 -0
  131. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm.egg-info/entry_points.txt +0 -0
  132. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm.egg-info/requires.txt +0 -0
  133. {beswarm-0.3.12 → beswarm-0.3.14}/beswarm.egg-info/top_level.txt +0 -0
  134. {beswarm-0.3.12 → beswarm-0.3.14}/setup.cfg +0 -0
  135. {beswarm-0.3.12 → beswarm-0.3.14}/test/test_TaskManager.py +0 -0
  136. {beswarm-0.3.12 → beswarm-0.3.14}/test/test_broker.py +0 -0
  137. {beswarm-0.3.12 → beswarm-0.3.14}/test/test_graph.py +0 -0
  138. {beswarm-0.3.12 → beswarm-0.3.14}/test/test_new_TaskManager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: beswarm
3
- Version: 0.3.12
3
+ Version: 0.3.14
4
4
  Summary: MAS
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -233,73 +233,116 @@ class Files(ContextProvider):
233
233
  def __init__(self, *paths: Union[str, List[str]], name: str = "files", visible: bool = True):
234
234
  super().__init__(name, visible=visible)
235
235
  self._files: Dict[str, str] = {}
236
+ self._file_sources: Dict[str, Dict] = {}
236
237
 
237
238
  file_paths: List[str] = []
238
239
  if paths:
239
- # Handle the case where the first argument is a list of paths, e.g., Files(['a', 'b'])
240
240
  if len(paths) == 1 and isinstance(paths[0], list):
241
241
  file_paths.extend(paths[0])
242
- # Handle the case where arguments are individual string paths, e.g., Files('a', 'b')
243
242
  else:
244
243
  file_paths.extend(paths)
245
244
 
246
245
  if file_paths:
247
246
  for path in file_paths:
248
- try:
249
- with open(path, 'r', encoding='utf-8') as f:
250
- self._files[path] = f.read()
251
- except FileNotFoundError:
252
- logging.warning(f"File not found during initialization: {path}. Skipping.")
253
- except Exception as e:
254
- logging.error(f"Error reading file {path} during initialization: {e}")
247
+ self.update(path)
248
+
249
+ def _read_from_disk(self, path: str, head: Optional[int] = None) -> str:
250
+ """Reads content from a file on disk, respecting the head parameter."""
251
+ try:
252
+ with open(path, 'r', encoding='utf-8') as f:
253
+ if head is not None and head > 0:
254
+ lines = []
255
+ for _ in range(head):
256
+ try:
257
+ lines.append(next(f))
258
+ except StopIteration:
259
+ break
260
+ return "".join(lines).rstrip('\n')
261
+ else:
262
+ return f.read()
263
+ except FileNotFoundError:
264
+ raise
265
+ except Exception as e:
266
+ logging.error(f"Error reading file {path}: {e}")
267
+ return f"[Error: Could not read file at path '{path}': {e}]"
255
268
 
256
269
  async def refresh(self):
257
270
  """
258
- Overrides the default refresh behavior. It synchronizes the content of
259
- all tracked files with the file system. If a file is not found, its
260
- content is updated to reflect the error.
271
+ Synchronizes content for files sourced from disk.
272
+ Content set manually is overwritten if the file exists, but preserved if it does not.
261
273
  """
262
274
  is_changed = False
263
- for path in list(self._files.keys()):
264
- try:
265
- with open(path, 'r', encoding='utf-8') as f:
266
- new_content = f.read()
267
- if self._files.get(path) != new_content:
268
- self._files[path] = new_content
269
- is_changed = True
270
- except FileNotFoundError:
271
- error_msg = f"[Error: File not found at path '{path}']"
272
- if self._files.get(path) != error_msg:
273
- self._files[path] = error_msg
274
- is_changed = True
275
- except Exception as e:
276
- error_msg = f"[Error: Could not read file at path '{path}': {e}]"
277
- if self._files.get(path) != error_msg:
278
- self._files[path] = error_msg
279
- is_changed = True
275
+ for path, spec in list(self._file_sources.items()):
276
+ if spec.get('source') == 'disk':
277
+ try:
278
+ head = spec.get('head')
279
+ new_content = self._read_from_disk(path, head)
280
+ if self._files.get(path) != new_content:
281
+ self._files[path] = new_content
282
+ is_changed = True
283
+ except FileNotFoundError:
284
+ error_msg = f"[Error: File not found at path '{path}']"
285
+ if self._files.get(path) != error_msg:
286
+ self._files[path] = error_msg
287
+ is_changed = True
288
+ elif spec.get('source') == 'manual':
289
+ try:
290
+ # File exists, so we must overwrite manual content.
291
+ head = spec.get('head')
292
+ new_content = self._read_from_disk(path, head)
293
+ if self._files.get(path) != new_content:
294
+ self._files[path] = new_content
295
+ is_changed = True
296
+ # If we are here, manual content was overwritten by disk content,
297
+ # so we should update the source.
298
+ self._file_sources[path] = {'source': 'disk'}
299
+
300
+ except FileNotFoundError:
301
+ # File does not exist, so we keep the manual content. No change.
302
+ pass
280
303
 
281
304
  if is_changed:
282
305
  self.mark_stale()
283
-
284
306
  await super().refresh()
285
307
 
286
- def update(self, path: str, content: Optional[str] = None):
308
+ def update(self, path: str, content: Optional[str] = None, head: Optional[Union[int, str]] = None):
287
309
  """
288
- Updates a single file. If content is provided, it updates the file in
289
- memory. If content is None, it reads the file from disk.
310
+ Updates a single file's content and its source specification.
290
311
  """
312
+ if head is not None:
313
+ try:
314
+ head = int(head)
315
+ except (ValueError, TypeError):
316
+ logging.warning(f"Invalid 'head' parameter for file '{path}': {head}. Must be an integer. Ignoring.")
317
+ head = None
318
+
291
319
  if content is not None:
292
- self._files[path] = content
320
+ # New logic: if head is also provided, decide what to do with content.
321
+ if head is not None and head > 0:
322
+ try:
323
+ # If file exists, prioritize reading from disk.
324
+ self._files[path] = self._read_from_disk(path, head)
325
+ self._file_sources[path] = {'source': 'disk', 'head': head}
326
+ except FileNotFoundError:
327
+ # If file does not exist, use the provided content's head.
328
+ lines = content.split('\n')
329
+ self._files[path] = "\n".join(lines[:head])
330
+ self._file_sources[path] = {'source': 'manual', 'head': head}
331
+ else:
332
+ # Original logic for when only content is provided.
333
+ self._files[path] = content
334
+ self._file_sources[path] = {'source': 'manual'}
293
335
  else:
336
+ # Original logic for when only path (and optional head) is provided.
294
337
  try:
295
- with open(path, 'r', encoding='utf-8') as f:
296
- self._files[path] = f.read()
338
+ self._files[path] = self._read_from_disk(path, head)
339
+ spec = {'source': 'disk'}
340
+ if head is not None and head > 0:
341
+ spec['head'] = head
342
+ self._file_sources[path] = spec
297
343
  except FileNotFoundError:
298
- logging.error(f"File not found for update: {path}.")
299
344
  self._files[path] = f"[Error: File not found at path '{path}']"
300
- except Exception as e:
301
- logging.error(f"Error reading file for update {path}: {e}.")
302
- self._files[path] = f"[Error: Could not read file at path '{path}': {e}]"
345
+ self._file_sources[path] = {'source': 'disk'}
303
346
  self.mark_stale()
304
347
  async def render(self) -> str:
305
348
  if not self._files: return None
@@ -380,8 +423,16 @@ class Message(ABC):
380
423
  processed_items.append(Images(url=image_url))
381
424
  else:
382
425
  raise ValueError(f"Unsupported item type in list: {item_type}")
426
+ elif isinstance(item, dict):
427
+ item_type = item.get('type')
428
+ if item_type == 'image_url':
429
+ image_url = item.get('image_url', {}).get('url')
430
+ if image_url:
431
+ processed_items.append(Images(url=image_url))
432
+ else:
433
+ raise ValueError(f"Unsupported dict item type: {item_type}")
383
434
  else:
384
- raise TypeError(f"Unsupported item type: {type(item)}. Must be str, ContextProvider, or list.")
435
+ raise TypeError(f"Unsupported item type: {type(item)}. Must be str, ContextProvider, list, or dict.")
385
436
  self._items: List[ContextProvider] = processed_items
386
437
  self._parent_messages: Optional['Messages'] = None
387
438
 
@@ -1783,6 +1783,133 @@ Files: {Files(visible=True, name="files")}
1783
1783
  self.assertEqual(len(rendered_separated), 1, "被空消息隔开的同角色消息在渲染时应该合并")
1784
1784
  self.assertEqual(rendered_separated[0]['content'], "hihi2", "被空消息隔开的同角色消息合并后内容不正确")
1785
1785
 
1786
+ async def test_zzc_files_update_with_head(self):
1787
+ """测试 Files.update 是否支持 head 参数以及新的 refresh 逻辑"""
1788
+ test_file = "test_file_head.txt"
1789
+ file_content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
1790
+ with open(test_file, "w", encoding='utf-8') as f:
1791
+ f.write(file_content)
1792
+
1793
+ try:
1794
+ messages = Messages(UserMessage(Files(name="files")))
1795
+
1796
+ # 1. Test update with head=3 using the provider from Messages
1797
+ files_provider = messages.provider("files")
1798
+ self.assertIsInstance(files_provider, Files)
1799
+ files_provider.update(path=test_file, head=3)
1800
+
1801
+ rendered_head = await messages.render_latest()
1802
+ expected_head_content = "Line 1\nLine 2\nLine 3"
1803
+ self.assertIn(f"<file_content>{expected_head_content}</file_content>", rendered_head[0]['content'])
1804
+ self.assertNotIn("Line 4", rendered_head[0]['content'])
1805
+
1806
+ # 2. Test update without head (default behavior)
1807
+ files_provider.update(path=test_file)
1808
+ rendered_full = await messages.render_latest()
1809
+ self.assertIn(f"<file_content>{file_content}</file_content>", rendered_full[0]['content'])
1810
+
1811
+ # 3. Test that refresh overwrites manual content when file exists
1812
+ files_provider.update(path=test_file, content="manual content")
1813
+ rendered_overwritten = await messages.render_latest()
1814
+ self.assertIn(f"<file_content>{file_content}</file_content>", rendered_overwritten[0]['content'])
1815
+ self.assertNotIn("manual content", rendered_overwritten[0]['content'])
1816
+
1817
+ # 4. Test that manual content is kept if file does not exist
1818
+ non_existent_file = "non_existent_for_sure.txt"
1819
+ files_provider.update(path=non_existent_file, content="manual content to keep")
1820
+ rendered_kept = await messages.render_latest()
1821
+ self.assertIn("<file_content>manual content to keep</file_content>", rendered_kept[0]['content'])
1822
+
1823
+ finally:
1824
+ if os.path.exists(test_file):
1825
+ os.remove(test_file)
1826
+
1827
+ async def test_zzd_files_update_with_string_head(self):
1828
+ """测试 Files.update 是否能自动将字符串 head 参数转换为整数"""
1829
+ test_file = "test_file_string_head.txt"
1830
+ file_content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
1831
+ with open(test_file, "w", encoding='utf-8') as f:
1832
+ f.write(file_content)
1833
+
1834
+ try:
1835
+ messages = Messages(UserMessage(Files(name="files")))
1836
+
1837
+ # Test update with head="3"
1838
+ files_provider = messages.provider("files")
1839
+ self.assertIsInstance(files_provider, Files)
1840
+ # This is the part that will likely fail before implementation
1841
+ files_provider.update(path=test_file, head="3")
1842
+
1843
+ rendered_head = await messages.render_latest()
1844
+ expected_head_content = "Line 1\nLine 2\nLine 3"
1845
+ self.assertIn(f"<file_content>{expected_head_content}</file_content>", rendered_head[0]['content'])
1846
+ self.assertNotIn("Line 4", rendered_head[0]['content'])
1847
+
1848
+ finally:
1849
+ if os.path.exists(test_file):
1850
+ os.remove(test_file)
1851
+
1852
+ async def test_zzc_files_update_with_head_and_content(self):
1853
+ """测试update同时设置path, content, head时的行为"""
1854
+ existing_file = "test_file_existing.txt"
1855
+ non_existent_file = "test_file_non_existent.txt"
1856
+ existing_content = "Line 1 (from file)\nLine 2 (from file)\nLine 3 (from file)\nLine 4 (from file)"
1857
+ manual_content = "Line 1 (manual)\nLine 2 (manual)\nLine 3 (manual)\nLine 4 (manual)"
1858
+
1859
+ # --- 场景1: 文件存在 ---
1860
+ with open(existing_file, "w", encoding='utf-8') as f:
1861
+ f.write(existing_content)
1862
+ try:
1863
+ messages = Messages(UserMessage(Files(name="files")))
1864
+ files_provider = messages.provider("files")
1865
+ files_provider.update(path=existing_file, content=manual_content, head=2)
1866
+ rendered_existing = await messages.render_latest()
1867
+ expected_existing_head = "Line 1 (from file)\nLine 2 (from file)"
1868
+ self.assertIn(f"<file_content>{expected_existing_head}</file_content>", rendered_existing[0]['content'])
1869
+ self.assertNotIn("manual", rendered_existing[0]['content'])
1870
+ finally:
1871
+ if os.path.exists(existing_file):
1872
+ os.remove(existing_file)
1873
+
1874
+ # --- 场景2: 文件不存在 ---
1875
+ try:
1876
+ messages = Messages(UserMessage(Files(name="files")))
1877
+ files_provider = messages.provider("files")
1878
+ files_provider.update(path=non_existent_file, content=manual_content, head=2)
1879
+ rendered_non_existent = await messages.render_latest()
1880
+ expected_manual_head = "Line 1 (manual)\nLine 2 (manual)"
1881
+ self.assertIn(f"<file_content>{expected_manual_head}</file_content>", rendered_non_existent[0]['content'])
1882
+ self.assertNotIn("from file", rendered_non_existent[0]['content'])
1883
+ finally:
1884
+ if os.path.exists(non_existent_file):
1885
+ os.remove(non_existent_file)
1886
+
1887
+ async def test_auto_convert_image_dict_to_image_provider(self):
1888
+ """测试 UserMessage 是否能自动转换 image_url 字典为 Images provider"""
1889
+ base64_image_data = "/9j/4AAQSkZJRgABAQEAYABgAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAEAAAAAAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9/KKKKAP/2Q=="
1890
+ image_dict = {
1891
+ 'type': 'image_url',
1892
+ 'image_url': {
1893
+ 'url': f'data:image/jpeg;base64,{base64_image_data}'
1894
+ }
1895
+ }
1896
+
1897
+ # This is the functionality we are testing.
1898
+ # The UserMessage should be able to take this dictionary directly.
1899
+ message = UserMessage(image_dict)
1900
+
1901
+ # 1. Verify that the message contains exactly one provider.
1902
+ self.assertEqual(len(message.provider()), 1)
1903
+
1904
+ # 2. Verify that the provider is an instance of the Images class.
1905
+ image_provider = message.provider()[0]
1906
+ self.assertIsInstance(image_provider, Images)
1907
+
1908
+ # 3. Verify that the content of the Images provider is correct.
1909
+ rendered = await message.render_latest()
1910
+ self.assertEqual(rendered['content'][0]['image_url']['url'], image_dict['image_url']['url'])
1911
+
1912
+
1786
1913
  # ==============================================================================
1787
1914
  # 6. 演示
1788
1915
  # ==============================================================================
@@ -295,7 +295,7 @@ async def get_gemini_payload(request, engine, provider, api_key=None):
295
295
  if key == request.model:
296
296
  for k, v in value.items():
297
297
  payload[k] = v
298
- elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key:
298
+ elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key and " " not in key:
299
299
  payload[key] = value
300
300
 
301
301
  return url, headers, payload
@@ -591,7 +591,7 @@ async def get_vertex_gemini_payload(request, engine, provider, api_key=None):
591
591
  if key == request.model:
592
592
  for k, v in value.items():
593
593
  payload[k] = v
594
- elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key:
594
+ elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key and " " not in key:
595
595
  payload[key] = value
596
596
 
597
597
  return url, headers, payload
@@ -1116,6 +1116,9 @@ async def get_gpt_payload(request, engine, provider, api_key=None):
1116
1116
  if "temperature" in payload:
1117
1117
  payload.pop("temperature")
1118
1118
 
1119
+ if "v1/responses" in url:
1120
+ payload.pop("stream_options", None)
1121
+
1119
1122
  # 代码生成/数学解题  0.0
1120
1123
  # 数据抽取/分析 1.0
1121
1124
  # 通用对话 1.3
@@ -1149,7 +1152,7 @@ async def get_gpt_payload(request, engine, provider, api_key=None):
1149
1152
  if key == request.model:
1150
1153
  for k, v in value.items():
1151
1154
  payload[k] = v
1152
- elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key:
1155
+ elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key and " " not in key:
1153
1156
  payload[key] = value
1154
1157
 
1155
1158
  return url, headers, payload
@@ -1247,7 +1250,7 @@ async def get_azure_payload(request, engine, provider, api_key=None):
1247
1250
  if key == request.model:
1248
1251
  for k, v in value.items():
1249
1252
  payload[k] = v
1250
- elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key:
1253
+ elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key and " " not in key:
1251
1254
  payload[key] = value
1252
1255
 
1253
1256
  return url, headers, payload
@@ -1367,7 +1370,7 @@ async def get_azure_databricks_payload(request, engine, provider, api_key=None):
1367
1370
  if key == request.model:
1368
1371
  for k, v in value.items():
1369
1372
  payload[k] = v
1370
- elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key:
1373
+ elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key and " " not in key:
1371
1374
  payload[key] = value
1372
1375
 
1373
1376
  return url, headers, payload
@@ -1454,7 +1457,7 @@ async def get_openrouter_payload(request, engine, provider, api_key=None):
1454
1457
  if key == request.model:
1455
1458
  for k, v in value.items():
1456
1459
  payload[k] = v
1457
- elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key:
1460
+ elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key and " " not in key:
1458
1461
  payload[key] = value
1459
1462
 
1460
1463
  return url, headers, payload
@@ -1820,7 +1823,7 @@ async def get_claude_payload(request, engine, provider, api_key=None):
1820
1823
  if key == request.model:
1821
1824
  for k, v in value.items():
1822
1825
  payload[k] = v
1823
- elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key:
1826
+ elif all(_model not in request.model.lower() for _model in model_dict.keys()) and "-" not in key and " " not in key:
1824
1827
  payload[key] = value
1825
1828
 
1826
1829
  return url, headers, payload
@@ -52,7 +52,7 @@ class BaseAPI:
52
52
  self.v1_models: str = urlunparse(parsed_url[:2] + (before_v1 + "models",) + ("",) * 3)
53
53
 
54
54
  if "v1/responses" in parsed_url.path:
55
- self.chat_url: str = urlunparse(parsed_url[:2] + ("v1/responses",) + ("",) * 3)
55
+ self.chat_url: str = api_url
56
56
  else:
57
57
  self.chat_url: str = urlunparse(parsed_url[:2] + (before_v1 + "chat/completions",) + ("",) * 3)
58
58
  self.image_url: str = urlunparse(parsed_url[:2] + (before_v1 + "images/generations",) + ("",) * 3)
@@ -89,6 +89,13 @@ class RepetitiveResponseError(Exception):
89
89
  self.count = count
90
90
 
91
91
 
92
+ class AllToolsMissingParametersError(Exception):
93
+ """Custom exception for when all tools are missing required parameters."""
94
+ def __init__(self, message, response_text):
95
+ super().__init__(message)
96
+ self.response_text = response_text
97
+
98
+
92
99
  class chatgpt(BaseLLM):
93
100
  """
94
101
  Official ChatGPT API
@@ -500,6 +507,9 @@ class chatgpt(BaseLLM):
500
507
  missing_required_params.append(f"Error: {tool_name} missing required parameters: {missing_required_params}")
501
508
  function_parameter = valid_function_parameters
502
509
 
510
+ if not function_parameter and missing_required_params:
511
+ raise AllToolsMissingParametersError("\n\n".join(missing_required_params), response_text=full_response)
512
+
503
513
  # 删除 task_complete 跟其他工具一起调用的情况,因为 task_complete 必须单独调用
504
514
  if len(function_parameter) > 1:
505
515
  function_parameter = [tool_dict for tool_dict in function_parameter if tool_dict.get("function_name", "") != "task_complete"]
@@ -615,7 +625,7 @@ class chatgpt(BaseLLM):
615
625
  final_tool_response = tool_response
616
626
  if "<tool_error>" not in tool_response:
617
627
  if tool_name == "read_file":
618
- self.conversation[convo_id].provider("files").update(tool_info['parameter']["file_path"], tool_response)
628
+ self.conversation[convo_id].provider("files").update(tool_info['parameter']["file_path"], tool_response, head=safe_get(tool_info, 'parameter', "head", default=None))
619
629
  final_tool_response = "Read file successfully! The file content has been updated in the tag <latest_file_content>."
620
630
  elif tool_name == "get_knowledge_graph_tree":
621
631
  self.conversation[convo_id].provider("knowledge_graph").visible = True
@@ -810,6 +820,13 @@ class chatgpt(BaseLLM):
810
820
  {"role": "user", "content": "你的消息没有以[done]结尾,请重新输出"}
811
821
  ]
812
822
  continue
823
+ except AllToolsMissingParametersError as e:
824
+ self.logger.warning(f"All tools are missing required parameters: {e}. Retrying with corrective prompt.")
825
+ need_done_prompt = [
826
+ {"role": "assistant", "content": e.response_text},
827
+ {"role": "user", "content": f"{e.message},请重新输出"}
828
+ ]
829
+ continue
813
830
  except EmptyResponseError as e:
814
831
  self.logger.warning(f"{e}, retrying...")
815
832
  continue
@@ -2,7 +2,6 @@ import os
2
2
  import json
3
3
  import chardet
4
4
  from pdfminer.high_level import extract_text
5
-
6
5
  from ..aient.aient.plugins import register_tool
7
6
  from ..core import current_work_dir
8
7
 
@@ -52,9 +51,7 @@ Examples:
52
51
  </read_file>
53
52
  """
54
53
 
55
- work_dir = current_work_dir.get()
56
- if not work_dir:
57
- return f"<tool_error>关键上下文 'current_work_dir' 未设置,无法确定安全的工作目录。</tool_error>"
54
+ work_dir = current_work_dir.get(os.getcwd())
58
55
 
59
56
  # Determine the final, absolute path for the file operation.
60
57
  if os.path.isabs(file_path):
@@ -62,12 +59,7 @@ Examples:
62
59
  else:
63
60
  final_path = os.path.join(work_dir, file_path)
64
61
 
65
- # Security check: Ensure the final path is within the designated work directory.
66
- # abs_work_dir = os.path.abspath(work_dir)
67
62
  abs_final_path = os.path.abspath(final_path)
68
- # if not abs_final_path.startswith(abs_work_dir):
69
- # return f"<tool_error>路径遍历攻击被阻止。尝试写入的路径 '{file_path}' 解析后超出了允许的工作目录范围 '{abs_work_dir}'。</tool_error>"
70
-
71
63
  file_path = abs_final_path
72
64
 
73
65
  try:
@@ -125,16 +117,23 @@ Examples:
125
117
  # 更新:修改通用文件读取逻辑以支持多种编码
126
118
  # 这部分替换了原有的 else 块内容
127
119
  try:
120
+ # 优化:分块读取以加速 chardet
128
121
  with open(file_path, 'rb') as file: # 以二进制模式读取
129
- raw_data = file.read()
122
+ # 1. 读取用于检测编码的初始块
123
+ detection_chunk = file.read(4096) # Read first 4KB
124
+ # 2. 读取文件的剩余部分
125
+ remaining_data = file.read()
126
+
127
+ # 将两部分数据合并为完整的文件内容
128
+ raw_data = detection_chunk + remaining_data
130
129
 
131
130
  if not raw_data: # 处理空文件
132
131
  text_content = ""
133
132
  else:
134
- detected_info = chardet.detect(raw_data)
133
+ # 3. 仅对初始块运行 chardet 以提高速度
134
+ detected_info = chardet.detect(detection_chunk) # Detect on the small chunk
135
135
  primary_encoding_to_try = detected_info['encoding']
136
136
  confidence = detected_info['confidence']
137
-
138
137
  decoded_successfully = False
139
138
 
140
139
  # 尝试1: 使用检测到的编码 (如果置信度高且编码有效)
@@ -193,14 +192,6 @@ Examples:
193
192
  # Invalid head value, ignore and proceed with normal logic.
194
193
  pass
195
194
 
196
- # if file_path.lower().endswith('.csv'):
197
- # lines = text_content.splitlines(True)
198
- # if len(lines) > 500:
199
- # top_lines = lines[:250]
200
- # bottom_lines = lines[-250:]
201
- # omitted_count = len(lines) - 500
202
- # text_content = "".join(top_lines) + f"\n... (中间省略了 {omitted_count} 行) ...\n" + "".join(bottom_lines)
203
-
204
195
  # 返回文件内容
205
196
  return text_content
206
197
 
@@ -213,7 +204,8 @@ Examples:
213
204
  return f"<tool_error>读取文件时发生错误: {e}</tool_error>"
214
205
 
215
206
  if __name__ == "__main__":
216
- # python -m beswarm.aient.aient.plugins.read_file
217
- result = read_file("./work/cax/Lenia Notebook.ipynb", head=10)
207
+ # python -m beswarm.tools.read_file
208
+ # result = read_file("traindata.csv", head=3)
209
+ result = read_file("testdata.csv", head=3)
218
210
  print(result)
219
- print(len(result))
211
+ print(f"行数: {len(result.splitlines())}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: beswarm
3
- Version: 0.3.12
3
+ Version: 0.3.14
4
4
  Summary: MAS
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "beswarm"
3
- version = "0.3.12"
3
+ version = "0.3.14"
4
4
  description = "MAS"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
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