beswarm 0.2.16__py3-none-any.whl → 0.2.18__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/tools/__init__.py +3 -1
- beswarm/tools/search_web.py +1 -7
- beswarm/tools/taskmanager.py +197 -0
- beswarm/tools/worker.py +46 -28
- {beswarm-0.2.16.dist-info → beswarm-0.2.18.dist-info}/METADATA +1 -1
- {beswarm-0.2.16.dist-info → beswarm-0.2.18.dist-info}/RECORD +8 -7
- {beswarm-0.2.16.dist-info → beswarm-0.2.18.dist-info}/WHEEL +0 -0
- {beswarm-0.2.16.dist-info → beswarm-0.2.18.dist-info}/top_level.txt +0 -0
beswarm/tools/__init__.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from .edit_file import edit_file
|
2
|
-
from .worker import worker, worker_gen
|
2
|
+
from .worker import worker, worker_gen, create_task, get_task_result
|
3
3
|
from .screenshot import save_screenshot_to_file
|
4
4
|
from .request_input import request_admin_input
|
5
5
|
|
@@ -49,4 +49,6 @@ __all__ = [
|
|
49
49
|
"save_screenshot_to_file",
|
50
50
|
"set_readonly_path",
|
51
51
|
"request_admin_input",
|
52
|
+
"create_task",
|
53
|
+
"get_task_result",
|
52
54
|
]
|
beswarm/tools/search_web.py
CHANGED
@@ -183,13 +183,7 @@ async def search_web(query: str):
|
|
183
183
|
web_contents_raw = []
|
184
184
|
if results and isinstance(results, list) and len(results) > 0:
|
185
185
|
# print(f"Fetching content for {len(results)} URLs...")
|
186
|
-
|
187
|
-
# for url in url_set_list:
|
188
|
-
# # url_search_thread = ThreadWithReturnValue(target=jina_ai_Web_crawler, args=(url,True,))
|
189
|
-
# url_search_thread = ThreadWithReturnValue(target=get_url_content, args=(url,))
|
190
|
-
# # url_search_thread = ThreadWithReturnValue(target=Web_crawler, args=(url,True,))
|
191
|
-
# url_search_thread.start()
|
192
|
-
# threads.append(url_search_thread)
|
186
|
+
|
193
187
|
threads = []
|
194
188
|
for i, link in enumerate(results):
|
195
189
|
print(f"Processing URL {i + 1}/{len(results)}: {link}")
|
@@ -0,0 +1,197 @@
|
|
1
|
+
import os
|
2
|
+
import uuid
|
3
|
+
import asyncio
|
4
|
+
from enum import Enum
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
class TaskStatus(Enum):
|
8
|
+
"""任务状态枚举"""
|
9
|
+
PENDING = "PENDING"
|
10
|
+
RUNNING = "RUNNING"
|
11
|
+
DONE = "DONE"
|
12
|
+
CANCELLED = "CANCELLED"
|
13
|
+
ERROR = "ERROR"
|
14
|
+
NOT_FOUND = "NOT_FOUND"
|
15
|
+
|
16
|
+
|
17
|
+
class TaskManager:
|
18
|
+
"""一个简单的异步任务管理器"""
|
19
|
+
def __init__(self):
|
20
|
+
self.tasks = {} # 使用字典来存储任务,key是task_id, value是task对象
|
21
|
+
self.results_queue = asyncio.Queue()
|
22
|
+
self.root_path = Path(os.getcwd())
|
23
|
+
|
24
|
+
def create_tasks(self, task_coro, tasks_params):
|
25
|
+
"""
|
26
|
+
批量创建并注册任务。
|
27
|
+
|
28
|
+
Args:
|
29
|
+
task_coro: 用于创建任务的协程函数。
|
30
|
+
tasks_params (list): 包含任务参数的列表。
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
list: 创建的任务ID列表。
|
34
|
+
"""
|
35
|
+
task_ids = []
|
36
|
+
for args in tasks_params:
|
37
|
+
coro = task_coro(**args)
|
38
|
+
task_id = self.create_task(coro)
|
39
|
+
task_ids.append(task_id)
|
40
|
+
return task_ids
|
41
|
+
|
42
|
+
def create_task(self, coro):
|
43
|
+
"""
|
44
|
+
创建并注册一个新任务。
|
45
|
+
|
46
|
+
Args:
|
47
|
+
coro: 要执行的协程。
|
48
|
+
name (str, optional): 任务的可读名称。 Defaults to None.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
str: 任务的唯一ID。
|
52
|
+
"""
|
53
|
+
task_id = str(uuid.uuid4())
|
54
|
+
task_name = f"Task-{task_id[:8]}"
|
55
|
+
|
56
|
+
# 使用 asyncio.create_task() 创建任务
|
57
|
+
task = asyncio.create_task(coro, name=task_name)
|
58
|
+
|
59
|
+
# 将任务存储在管理器中
|
60
|
+
# 当任务完成时,通过回调函数将结果放入队列
|
61
|
+
task.add_done_callback(
|
62
|
+
lambda t: self._on_task_done(task_id, t)
|
63
|
+
)
|
64
|
+
self.tasks[task_id] = task
|
65
|
+
print(f"任务已创建: ID={task_id}, Name={task_name}")
|
66
|
+
return task_id
|
67
|
+
|
68
|
+
def get_task_status(self, task_id):
|
69
|
+
"""
|
70
|
+
查询特定任务的状态。
|
71
|
+
|
72
|
+
Args:
|
73
|
+
task_id (str): 要查询的任务ID。
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
TaskStatus: 任务的当前状态。
|
77
|
+
"""
|
78
|
+
task = self.tasks.get(task_id)
|
79
|
+
if not task:
|
80
|
+
return TaskStatus.NOT_FOUND
|
81
|
+
|
82
|
+
if task.done():
|
83
|
+
if task.cancelled():
|
84
|
+
return TaskStatus.CANCELLED
|
85
|
+
elif task.exception() is not None:
|
86
|
+
return TaskStatus.ERROR
|
87
|
+
else:
|
88
|
+
return TaskStatus.DONE
|
89
|
+
|
90
|
+
# asyncio.Task 没有直接的 'RUNNING' 状态。
|
91
|
+
# 如果任务还没有完成,它要么是等待执行(PENDING),要么是正在执行(RUNNING)。
|
92
|
+
# 这里我们简化处理,认为未完成的就是运行中。
|
93
|
+
return TaskStatus.RUNNING
|
94
|
+
|
95
|
+
def get_task_result(self, task_id):
|
96
|
+
"""获取已完成任务的结果,如果任务未完成或出错则返回相应信息。"""
|
97
|
+
task = self.tasks.get(task_id)
|
98
|
+
if self.get_task_status(task_id) == TaskStatus.DONE:
|
99
|
+
return task.result()
|
100
|
+
elif self.get_task_status(task_id) == TaskStatus.ERROR:
|
101
|
+
return task.exception()
|
102
|
+
return None
|
103
|
+
|
104
|
+
def _on_task_done(self, task_id, task):
|
105
|
+
"""私有回调函数,在任务完成时将结果放入队列。"""
|
106
|
+
try:
|
107
|
+
# 将元组 (task_id, status, result) 放入队列
|
108
|
+
self.results_queue.put_nowait(
|
109
|
+
(task_id, TaskStatus.DONE, task.result())
|
110
|
+
)
|
111
|
+
except asyncio.CancelledError:
|
112
|
+
self.results_queue.put_nowait(
|
113
|
+
(task_id, TaskStatus.CANCELLED, None)
|
114
|
+
)
|
115
|
+
except Exception as e:
|
116
|
+
self.results_queue.put_nowait(
|
117
|
+
(task_id, TaskStatus.ERROR, e)
|
118
|
+
)
|
119
|
+
|
120
|
+
async def get_next_result(self):
|
121
|
+
"""
|
122
|
+
等待并返回下一个完成的任务结果。
|
123
|
+
|
124
|
+
如果所有任务都已提交,但没有任务完成,此方法将异步等待。
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
tuple: 一个包含 (task_id, status, result) 的元组。
|
128
|
+
"""
|
129
|
+
return await self.results_queue.get()
|
130
|
+
|
131
|
+
def get_task_index(self, task_id):
|
132
|
+
"""
|
133
|
+
获取任务在任务字典中的插入顺序索引。
|
134
|
+
|
135
|
+
Args:
|
136
|
+
task_id (str): 要查询的任务ID。
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
int: 任务的索引(从0开始),如果未找到则返回-1。
|
140
|
+
"""
|
141
|
+
try:
|
142
|
+
# 将字典的键转换为列表并查找索引
|
143
|
+
task_ids_list = list(self.tasks.keys())
|
144
|
+
return task_ids_list.index(task_id)
|
145
|
+
except ValueError:
|
146
|
+
# 如果任务ID不存在,则返回-1
|
147
|
+
return -1
|
148
|
+
|
149
|
+
def set_root_path(self, root_path):
|
150
|
+
self.root_path = Path(root_path)
|
151
|
+
|
152
|
+
async def main():
|
153
|
+
manager = TaskManager()
|
154
|
+
from worker import worker
|
155
|
+
|
156
|
+
# --- 任务提交阶段 ---
|
157
|
+
print("--- 任务提交 ---")
|
158
|
+
|
159
|
+
tasks_to_run = [
|
160
|
+
{"goal": ""},
|
161
|
+
{"goal": 1},
|
162
|
+
{"goal": 5},
|
163
|
+
{"goal": 2},
|
164
|
+
{"goal": 4},
|
165
|
+
]
|
166
|
+
task_ids = manager.create_tasks(worker, tasks_to_run)
|
167
|
+
print(f"\n主程序: {len(task_ids)} 个任务已提交,现在开始等待结果...\n")
|
168
|
+
|
169
|
+
# --- 结果处理阶段 ---
|
170
|
+
# 使用 get_next_result() 逐个获取已完成任务的结果
|
171
|
+
print("--- 结果处理 ---")
|
172
|
+
for i in range(len(task_ids)):
|
173
|
+
print(f"等待第 {i + 1} 个任务完成...")
|
174
|
+
# 此处会异步等待,直到队列中有可用的结果
|
175
|
+
task_id, status, result = await manager.get_next_result()
|
176
|
+
|
177
|
+
# 从管理器中获取任务名称(如果需要)
|
178
|
+
task_name = manager.tasks[task_id].get_name()
|
179
|
+
|
180
|
+
print(f"-> 收到结果: 任务 {task_name}, index: {manager.get_task_index(task_id)}")
|
181
|
+
print(f" - 状态: {status.value}")
|
182
|
+
|
183
|
+
if status == TaskStatus.DONE:
|
184
|
+
print(f" - 结果: '{result}'")
|
185
|
+
elif status == TaskStatus.ERROR:
|
186
|
+
print(f" - 错误: {result}")
|
187
|
+
elif status == TaskStatus.CANCELLED:
|
188
|
+
print(" - 结果: 任务被取消")
|
189
|
+
print("-" * 20)
|
190
|
+
|
191
|
+
print("\n--- 所有任务的结果都已处理完毕 ---")
|
192
|
+
|
193
|
+
|
194
|
+
# 运行主协程
|
195
|
+
if __name__ == "__main__":
|
196
|
+
asyncio.run(main())
|
197
|
+
print("\n主程序: 所有任务都已完成并处理。")
|
beswarm/tools/worker.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import os
|
2
2
|
import re
|
3
3
|
import sys
|
4
|
+
import ast
|
4
5
|
import copy
|
5
6
|
import json
|
6
7
|
import difflib
|
@@ -26,29 +27,56 @@ from ..aient.src.aient.plugins import register_tool, get_function_call_list, reg
|
|
26
27
|
from ..prompt import worker_system_prompt, instruction_system_prompt
|
27
28
|
from ..utils import extract_xml_content, get_current_screen_image_message, replace_xml_content, register_mcp_tools
|
28
29
|
from ..bemcp.bemcp import MCPClient, convert_tool_format, MCPManager
|
30
|
+
from .taskmanager import TaskManager
|
29
31
|
|
30
32
|
manager = MCPManager()
|
33
|
+
task_manager = TaskManager()
|
31
34
|
|
32
35
|
@register_tool()
|
33
|
-
|
36
|
+
def create_task(goal, tools, work_dir):
|
34
37
|
"""
|
35
|
-
|
38
|
+
启动一个子任务来自动完成指定的任务目标 (`goal`)。
|
36
39
|
|
37
|
-
|
40
|
+
这个子任务接收一个清晰的任务描述、一组可供调用的工具 (`tools`),以及一个工作目录 (`work_dir`)。
|
38
41
|
它会利用语言模型的能力,结合可用的工具,自主规划并逐步执行必要的操作,直到最终完成指定的任务目标。
|
39
42
|
核心功能是根据输入的目标,驱动整个任务执行流程。
|
40
43
|
|
41
44
|
Args:
|
42
|
-
goal (str):
|
43
|
-
tools (list[str]):
|
44
|
-
work_dir (str):
|
45
|
+
goal (str): 需要完成的具体任务目标描述。子任务将围绕此目标进行工作。必须清晰、具体。
|
46
|
+
tools (list[str]): 一个包含可用工具函数对象的列表。子任务在执行任务时可能会调用这些工具来与环境交互(例如读写文件、执行命令等)。
|
47
|
+
work_dir (str): 工作目录的绝对路径。子任务将在此目录上下文中执行操作。子任务的工作目录位置在主任务的工作目录的子目录。
|
45
48
|
|
46
49
|
Returns:
|
47
50
|
str: 当任务成功完成时,返回字符串 "任务已完成"。
|
48
51
|
"""
|
52
|
+
tasks_params = [
|
53
|
+
{"goal": goal, "tools": ast.literal_eval(tools), "work_dir": work_dir, "cache_messages": True}
|
54
|
+
]
|
55
|
+
task_ids = task_manager.create_tasks(worker, tasks_params)
|
56
|
+
return task_ids
|
57
|
+
|
58
|
+
@register_tool()
|
59
|
+
async def get_task_result():
|
60
|
+
"""
|
61
|
+
等待并获取子任务的执行结果。
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
str: 子任务的执行结果。
|
65
|
+
"""
|
66
|
+
task_id, status, result = await task_manager.get_next_result()
|
67
|
+
return result
|
68
|
+
|
69
|
+
@register_tool()
|
70
|
+
async def worker(goal, tools, work_dir, cache_messages=None):
|
71
|
+
cache_dir = Path(work_dir) / ".beswarm"
|
72
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
73
|
+
cache_file = cache_dir / "work_agent_conversation_history.json"
|
74
|
+
if not cache_file.exists():
|
75
|
+
cache_file.write_text("[]", encoding="utf-8")
|
76
|
+
|
49
77
|
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "t", "yes")
|
50
78
|
if DEBUG:
|
51
|
-
log_file = open(
|
79
|
+
log_file = open(cache_dir / "history.log", "a", encoding="utf-8")
|
52
80
|
log_file.write(f"========== {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ==========\n")
|
53
81
|
original_stdout = sys.stdout
|
54
82
|
original_stderr = sys.stderr
|
@@ -94,11 +122,7 @@ async def worker(goal, tools, work_dir, cache_messages=None):
|
|
94
122
|
}
|
95
123
|
if cache_messages:
|
96
124
|
if isinstance(cache_messages, bool) and cache_messages == True:
|
97
|
-
cache_messages =
|
98
|
-
cache_file_path = Path(work_dir) / ".beswarm" / "work_agent_conversation_history.json"
|
99
|
-
if cache_file_path.exists():
|
100
|
-
with cache_file_path.open("r", encoding="utf-8") as f:
|
101
|
-
cache_messages = json.load(f)
|
125
|
+
cache_messages = json.loads(cache_file.read_text(encoding="utf-8"))
|
102
126
|
if cache_messages and isinstance(cache_messages, list) and len(cache_messages) > 1:
|
103
127
|
old_goal = extract_xml_content(cache_messages[1]["content"], "goal")
|
104
128
|
if old_goal.strip() != goal.strip():
|
@@ -144,11 +168,7 @@ async def worker(goal, tools, work_dir, cache_messages=None):
|
|
144
168
|
instruction_agent = chatgpt(**instruction_agent_config)
|
145
169
|
conversation_history = copy.deepcopy(work_agent.conversation["default"])
|
146
170
|
|
147
|
-
|
148
|
-
os.makedirs(cache_dir, exist_ok=True)
|
149
|
-
cache_file = os.path.join(cache_dir, "work_agent_conversation_history.json")
|
150
|
-
with open(cache_file, "w", encoding="utf-8") as f:
|
151
|
-
f.write(json.dumps(conversation_history, ensure_ascii=False, indent=4))
|
171
|
+
cache_file.write_text(json.dumps(conversation_history, ensure_ascii=False, indent=4), encoding="utf-8")
|
152
172
|
|
153
173
|
work_agent_system_prompt = conversation_history.pop(0)
|
154
174
|
if conversation_history:
|
@@ -248,9 +268,15 @@ async def worker(goal, tools, work_dir, cache_messages=None):
|
|
248
268
|
return "任务已完成"
|
249
269
|
|
250
270
|
async def worker_gen(goal, tools, work_dir, cache_messages=None):
|
271
|
+
cache_dir = Path(work_dir) / ".beswarm"
|
272
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
273
|
+
cache_file = cache_dir / "work_agent_conversation_history.json"
|
274
|
+
if not cache_file.exists():
|
275
|
+
cache_file.write_text("[]", encoding="utf-8")
|
276
|
+
|
251
277
|
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "t", "yes")
|
252
278
|
if DEBUG:
|
253
|
-
log_file = open(
|
279
|
+
log_file = open(cache_dir / "history.log", "a", encoding="utf-8")
|
254
280
|
log_file.write(f"========== {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ==========\n")
|
255
281
|
original_stdout = sys.stdout
|
256
282
|
original_stderr = sys.stderr
|
@@ -296,11 +322,7 @@ async def worker_gen(goal, tools, work_dir, cache_messages=None):
|
|
296
322
|
}
|
297
323
|
if cache_messages:
|
298
324
|
if isinstance(cache_messages, bool) and cache_messages == True:
|
299
|
-
cache_messages =
|
300
|
-
cache_file_path = Path(work_dir) / ".beswarm" / "work_agent_conversation_history.json"
|
301
|
-
if cache_file_path.exists():
|
302
|
-
with cache_file_path.open("r", encoding="utf-8") as f:
|
303
|
-
cache_messages = json.load(f)
|
325
|
+
cache_messages = json.loads(cache_file.read_text(encoding="utf-8"))
|
304
326
|
if cache_messages and isinstance(cache_messages, list) and len(cache_messages) > 1:
|
305
327
|
old_goal = extract_xml_content(cache_messages[1]["content"], "goal")
|
306
328
|
if old_goal.strip() != goal.strip():
|
@@ -346,11 +368,7 @@ async def worker_gen(goal, tools, work_dir, cache_messages=None):
|
|
346
368
|
instruction_agent = chatgpt(**instruction_agent_config)
|
347
369
|
conversation_history = copy.deepcopy(work_agent.conversation["default"])
|
348
370
|
|
349
|
-
|
350
|
-
os.makedirs(cache_dir, exist_ok=True)
|
351
|
-
cache_file = os.path.join(cache_dir, "work_agent_conversation_history.json")
|
352
|
-
with open(cache_file, "w", encoding="utf-8") as f:
|
353
|
-
f.write(json.dumps(conversation_history, ensure_ascii=False, indent=4))
|
371
|
+
cache_file.write_text(json.dumps(conversation_history, ensure_ascii=False, indent=4), encoding="utf-8")
|
354
372
|
|
355
373
|
work_agent_system_prompt = conversation_history.pop(0)
|
356
374
|
if conversation_history:
|
@@ -125,7 +125,7 @@ beswarm/queries/tree-sitter-languages/ruby-tags.scm,sha256=vIidsCeE2A0vdFN18yXKq
|
|
125
125
|
beswarm/queries/tree-sitter-languages/rust-tags.scm,sha256=9ljM1nzhfPs_ZTRw7cr2P9ToOyhGcKkCoN4_HPXSWi4,1451
|
126
126
|
beswarm/queries/tree-sitter-languages/scala-tags.scm,sha256=UxQjz80JIrrJ7Pm56uUnQyThfmQNvwk7aQzPNypB-Ao,1761
|
127
127
|
beswarm/queries/tree-sitter-languages/typescript-tags.scm,sha256=OMdCeedPiA24ky82DpgTMKXK_l2ySTuF2zrQ2fJAi9E,1253
|
128
|
-
beswarm/tools/__init__.py,sha256=
|
128
|
+
beswarm/tools/__init__.py,sha256=egPX4B021Pf8dIaOodJ2yxQ_YCG-giI4cHiXsbX70BA,1306
|
129
129
|
beswarm/tools/click.py,sha256=TygaekCXTmU3fIu6Uom7ZcyzEgYMlCC_GX-5SmWHuLI,20762
|
130
130
|
beswarm/tools/edit_file.py,sha256=iwWl7a8sTVq4vj0e1ny3H6UGcHfYnxALRGcLuk5hZS8,9155
|
131
131
|
beswarm/tools/planner.py,sha256=lguBCS6kpwNPoXQvqH-WySabVubT82iyWOkJnjt6dXw,1265
|
@@ -133,9 +133,10 @@ beswarm/tools/repomap.py,sha256=YsTPq5MXfn_Ds5begcvHDnY_Xp2d4jH-xmWqNMHnNHY,4523
|
|
133
133
|
beswarm/tools/request_input.py,sha256=gXNAJPOJektMqxJVyzNTFOeMQ7xUkO-wWMYH-r2Rdwk,942
|
134
134
|
beswarm/tools/screenshot.py,sha256=u6t8FCgW5YHJ_Oc4coo8e0F3wTusWE_-H8dFh1rBq9Q,1011
|
135
135
|
beswarm/tools/search_arxiv.py,sha256=caVIUOzMhFu-r_gVgJZrH2EO9xI5iV_qLAg0b3Ie9Xg,8095
|
136
|
-
beswarm/tools/search_web.py,sha256=
|
137
|
-
beswarm/tools/
|
138
|
-
beswarm
|
139
|
-
beswarm-0.2.
|
140
|
-
beswarm-0.2.
|
141
|
-
beswarm-0.2.
|
136
|
+
beswarm/tools/search_web.py,sha256=Zxt2Mpeuqa40sKttCxeI71O_TWnoANw7na7CdlGknvg,11659
|
137
|
+
beswarm/tools/taskmanager.py,sha256=oB_768qy6Lb58JNIcSLVgbPrgNB3duIq9DawbVHRbrg,6270
|
138
|
+
beswarm/tools/worker.py,sha256=W-NqcO8T6Y_gsgVvNaKk4hbpqA8_odz18v6qb17s64w,23674
|
139
|
+
beswarm-0.2.18.dist-info/METADATA,sha256=hYbFl-3T_3aou4zXfHLa96ltL4xoDIfafI-8f_Van7s,3847
|
140
|
+
beswarm-0.2.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
141
|
+
beswarm-0.2.18.dist-info/top_level.txt,sha256=pJw4O87wvt5882smuSO6DfByJz7FJ8SxxT8h9fHCmpo,8
|
142
|
+
beswarm-0.2.18.dist-info/RECORD,,
|
File without changes
|
File without changes
|