beswarm 0.2.35__py3-none-any.whl → 0.2.37__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.

Potentially problematic release.


This version of beswarm might be problematic. Click here for more details.

@@ -0,0 +1,339 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import copy
5
+ import json
6
+ import difflib
7
+ import asyncio
8
+ import platform
9
+ from pathlib import Path
10
+ from datetime import datetime
11
+ from typing import List, Dict, Union
12
+
13
+ from ..broker import MessageBroker
14
+ from ..aient.src.aient.models import chatgpt
15
+ from ..aient.src.aient.plugins import get_function_call_list, registry
16
+ from ..prompt import worker_system_prompt, instruction_system_prompt
17
+ from ..utils import extract_xml_content, get_current_screen_image_message, replace_xml_content, register_mcp_tools
18
+
19
+ class BaseAgent:
20
+ """Base class for agents, handling common initialization and disposal."""
21
+ def __init__(self, goal: str, tools_json: List, agent_config: Dict, work_dir: str, cache_messages: Union[bool, List[Dict]], broker: MessageBroker, listen_topic: str, publish_topic: str, status_topic: str):
22
+ self.goal = goal
23
+ self.tools_json = tools_json
24
+ self.work_dir = work_dir
25
+ self.cache_file = Path(work_dir) / ".beswarm" / "work_agent_conversation_history.json"
26
+ self.config = agent_config
27
+ self.cache_messages = cache_messages
28
+ if cache_messages and isinstance(cache_messages, bool) and cache_messages == True:
29
+ self.cache_messages = json.loads(self.cache_file.read_text(encoding="utf-8"))
30
+ self.broker = broker
31
+ self.listen_topic = listen_topic
32
+ self.error_topic = listen_topic + ".error"
33
+ self.publish_topic = publish_topic
34
+ self.status_topic = status_topic
35
+ self._subscription = self.broker.subscribe(self.handle_message, [self.listen_topic, self.error_topic])
36
+
37
+ async def handle_message(self, message: Dict):
38
+ """Process incoming messages. Must be implemented by subclasses."""
39
+ raise NotImplementedError
40
+
41
+ def dispose(self):
42
+ """Cancels the subscription and cleans up resources."""
43
+ if self._subscription:
44
+ self._subscription.dispose()
45
+
46
+
47
+ class InstructionAgent(BaseAgent):
48
+ """Generates instructions and publishes them to a message broker."""
49
+ def __init__(self, goal: str, tools_json: List, agent_config: Dict, work_dir: str, cache_messages: Union[bool, List[Dict]], broker: MessageBroker, listen_topic: str, publish_topic: str, status_topic: str):
50
+ super().__init__(goal, tools_json, agent_config, work_dir, cache_messages, broker, listen_topic, publish_topic, status_topic)
51
+
52
+ self.last_instruction = None
53
+ self.agent = chatgpt(**self.config)
54
+
55
+ self.goal_diff = None
56
+
57
+ if self.cache_messages and isinstance(self.cache_messages, list) and len(self.cache_messages) > 1:
58
+ old_goal = extract_xml_content(self.cache_messages[1]["content"], "goal")
59
+ if old_goal.strip() != goal.strip():
60
+ diff_generator = difflib.ndiff(old_goal.splitlines(), goal.splitlines())
61
+ changed_lines = []
62
+ for line in diff_generator:
63
+ if (line.startswith('+ ') or line.startswith('- ')) and line[2:].strip():
64
+ changed_lines.append(line)
65
+ self.goal_diff = '\n'.join(changed_lines).strip()
66
+
67
+ def get_conversation_history(self, conversation_history: List[Dict]):
68
+ conversation_history = copy.deepcopy(conversation_history)
69
+
70
+ self.cache_file.write_text(json.dumps(conversation_history, ensure_ascii=False, indent=4), encoding="utf-8")
71
+
72
+ work_agent_system_prompt = conversation_history.pop(0)
73
+ if conversation_history:
74
+ original_content = work_agent_system_prompt["content"]
75
+ regex = r"<latest_file_content>(.*?)</latest_file_content>"
76
+ match = re.search(regex, original_content, re.DOTALL)
77
+ if match:
78
+ extracted_content = f"<latest_file_content>{match.group(1)}</latest_file_content>\n\n"
79
+ else:
80
+ extracted_content = ""
81
+ if isinstance(conversation_history[0]["content"], str):
82
+ conversation_history[0]["content"] = extracted_content + conversation_history[0]["content"]
83
+ elif isinstance(conversation_history[0]["content"], list) and extracted_content:
84
+ conversation_history[0]["content"].append({"type": "text", "text": extracted_content})
85
+
86
+ return conversation_history
87
+
88
+ async def handle_message(self, message: Dict):
89
+ """Receives a worker response, generates the next instruction, and publishes it."""
90
+
91
+ if len(message["conversation"]) > 1 and message["conversation"][-2]["role"] == "user" \
92
+ and "<task_complete_message>" in message["conversation"][-2]["content"]:
93
+ task_complete_message = extract_xml_content(message["conversation"][-2]["content"], "task_complete_message")
94
+ self.broker.publish({"status": "finished", "result": task_complete_message}, self.status_topic)
95
+ return
96
+
97
+ instruction_prompt = "".join([
98
+ "</work_agent_conversation_end>\n\n",
99
+ f"任务目标: {self.goal}\n\n",
100
+ f"任务目标新变化:\n{self.goal_diff}\n\n" if self.goal_diff else "",
101
+ "在 tag <work_agent_conversation_start>...</work_agent_conversation_end> 之前的对话历史都是工作智能体的对话历史。\n\n",
102
+ "根据以上对话历史和目标,请生成下一步指令。如果任务已完成,指示工作智能体调用task_complete工具。\n\n",
103
+ ])
104
+ if self.last_instruction and 'fetch_gpt_response_stream HTTP Error' not in self.last_instruction:
105
+ instruction_prompt = (
106
+ f"{instruction_prompt}\n\n"
107
+ "你生成的指令格式错误,必须把给assistant的指令放在<instructions>...</instructions>标签内。请重新生成格式正确的指令。"
108
+ f"这是你上次给assistant的错误格式的指令:\n{self.last_instruction}"
109
+ )
110
+
111
+ self.agent.conversation["default"][1:] = self.get_conversation_history(message["conversation"])
112
+
113
+ if "find_and_click_element" in json.dumps(self.tools_json):
114
+ instruction_prompt = await get_current_screen_image_message(instruction_prompt)
115
+
116
+ raw_response = await self.agent.ask_async(instruction_prompt)
117
+
118
+ if "fetch_gpt_response_stream HTTP Error', 'status_code': 404" in raw_response:
119
+ raise Exception(f"Model: {self.config['engine']} not found!")
120
+ if "'status_code': 413" in raw_response or \
121
+ "'status_code': 400" in raw_response:
122
+ self.broker.publish({"status": "error", "result": raw_response}, self.status_topic)
123
+ return
124
+
125
+ self.broker.publish({"status": "new_message", "result": "\n🤖 指令智能体:\n" + raw_response}, self.status_topic)
126
+
127
+ self.last_instruction = raw_response
128
+ instruction = extract_xml_content(raw_response, "instructions")
129
+ if instruction:
130
+ if len(message["conversation"]) == 1:
131
+ instruction = (
132
+ "任务描述:\n"
133
+ f"<goal>{self.goal}</goal>\n\n"
134
+ "你作为指令的**执行者**,而非任务的**规划师**,你必须严格遵循以下单步工作流程:\n"
135
+ "**执行指令**\n"
136
+ " - **严格遵从:** 只执行我当前下达的明确指令。在我明确给出下一步指令前,绝不擅自行动或推测、执行任何未明确要求的后续步骤。\n"
137
+ " - **严禁越权:** 禁止执行任何我未指定的步骤。`<goal>` 标签中的内容仅为背景信息,不得据此进行任务规划或推测。\n"
138
+ "**汇报结果**\n"
139
+ " - **聚焦单步:** 指令完成后,仅汇报该步骤的执行结果与产出。\n"
140
+ "**暂停等待**\n"
141
+ " - **原地待命:** 汇报后,任务暂停。在收到我新的指令前,严禁发起任何新的工具调用或操作。\n"
142
+ " - **请求指令:** 回复的最后必须明确请求我提供下一步指令。\n"
143
+ "**注意:** 禁止完成超出下面我未规定的步骤,`<goal>` 标签中的内容仅为背景信息。"
144
+ "现在开始执行第一步:\n"
145
+ f"{instruction}"
146
+ )
147
+ self.broker.publish({"instruction": instruction, "conversation": message["conversation"]}, self.publish_topic)
148
+ else:
149
+ print("\n❌ 指令智能体生成的指令不符合要求,正在重新生成。")
150
+ self.broker.publish(message, self.error_topic)
151
+
152
+
153
+ class WorkerAgent(BaseAgent):
154
+ """Executes instructions and publishes results to a message broker."""
155
+ def __init__(self, goal: str, tools_json: List, agent_config: Dict, work_dir: str, cache_messages: Union[bool, List[Dict]], broker: MessageBroker, listen_topic: str, publish_topic: str, status_topic: str):
156
+ super().__init__(goal, tools_json, agent_config, work_dir, cache_messages, broker, listen_topic, publish_topic, status_topic)
157
+
158
+ if self.cache_messages and isinstance(self.cache_messages, list) and len(self.cache_messages) > 1:
159
+ first_user_message = replace_xml_content(self.cache_messages[1]["content"], "goal", goal)
160
+ self.config["cache_messages"] = self.cache_messages[0:1] + [{"role": "user", "content": first_user_message}] + self.cache_messages[2:]
161
+
162
+ self.agent = chatgpt(**self.config)
163
+
164
+ async def handle_message(self, message: Dict):
165
+ """Receives an instruction, executes it, and publishes the response."""
166
+
167
+ if message.get("instruction") == "Initial kickoff":
168
+ self.broker.publish({
169
+ "conversation": self.agent.conversation["default"]
170
+ }, self.publish_topic)
171
+ return
172
+
173
+ instruction = message["instruction"]
174
+ if "find_and_click_element" in json.dumps(self.tools_json):
175
+ instruction = await get_current_screen_image_message(instruction)
176
+ response = await self.agent.ask_async(instruction)
177
+
178
+ if response.strip() == '':
179
+ print("\n❌ 工作智能体回复为空,请重新生成指令。")
180
+ self.broker.publish(message, self.error_topic)
181
+ else:
182
+ self.broker.publish({"status": "new_message", "result": "\n✅ 工作智能体:\n" + response}, self.status_topic)
183
+ self.broker.publish({
184
+ "conversation": self.agent.conversation["default"]
185
+ }, self.publish_topic)
186
+
187
+ class Tee:
188
+ def __init__(self, *files):
189
+ self.files = files
190
+
191
+ def write(self, obj):
192
+ for f in self.files:
193
+ f.write(obj)
194
+ f.flush()
195
+
196
+ def flush(self):
197
+ for f in self.files:
198
+ f.flush()
199
+
200
+ class BrokerWorker:
201
+ """The 'glue' class that orchestrates agents via a MessageBroker."""
202
+ def __init__(self, goal: str, tools: List[Union[str, Dict]], work_dir: str, cache_messages: Union[bool, List[Dict]] = None, broker = None, mcp_manager = None, task_manager = None):
203
+ self.goal = goal
204
+ self.tools = tools
205
+ self.work_dir = Path(work_dir)
206
+ self.cache_messages = cache_messages
207
+
208
+ self.broker = broker
209
+ self.mcp_manager = mcp_manager
210
+ self.task_manager = task_manager
211
+ self.task_completion_event = asyncio.Event()
212
+ self.final_result = None
213
+ self._status_subscription = None
214
+ self.setup()
215
+
216
+ self.channel = self.broker.request_channel()
217
+ self.INSTRUCTION_TOPIC = self.channel + ".instructions"
218
+ self.WORKER_RESPONSE_TOPIC = self.channel + ".worker_responses"
219
+ self.TASK_STATUS_TOPIC =self.channel + ".task_status"
220
+
221
+ def setup(self):
222
+ cache_dir = self.work_dir / ".beswarm"
223
+ cache_dir.mkdir(parents=True, exist_ok=True)
224
+ self.task_manager.set_root_path(self.work_dir)
225
+ self.cache_file = cache_dir / "work_agent_conversation_history.json"
226
+ if not self.cache_file.exists():
227
+ self.cache_file.write_text("[]", encoding="utf-8")
228
+
229
+ DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "t", "yes")
230
+ if DEBUG:
231
+ log_file = open(cache_dir / "history.log", "a", encoding="utf-8")
232
+ log_file.write(f"========== {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ==========\n")
233
+ original_stdout = sys.stdout
234
+ original_stderr = sys.stderr
235
+ sys.stdout = Tee(original_stdout, log_file)
236
+ sys.stderr = Tee(original_stderr, log_file)
237
+
238
+ async def _configure_tools(self):
239
+ mcp_list = [item for item in self.tools if isinstance(item, dict)]
240
+ if mcp_list:
241
+ for mcp_item in mcp_list:
242
+ mcp_name, mcp_config = list(mcp_item.items())[0]
243
+ await self.mcp_manager.add_server(mcp_name, mcp_config)
244
+ client = self.mcp_manager.clients.get(mcp_name)
245
+ await register_mcp_tools(client, registry)
246
+ all_mcp_tools = await self.mcp_manager.get_all_tools()
247
+ self.tools.extend([tool.name for tool in sum(all_mcp_tools.values(), [])])
248
+ self.tools = [item for item in self.tools if not isinstance(item, dict)]
249
+ if "task_complete" not in self.tools: self.tools.append("task_complete")
250
+ self.tools_json = [value for _, value in get_function_call_list(self.tools).items()]
251
+
252
+ def _task_status_subscriber(self, message: Dict):
253
+ """Subscriber for task status changes."""
254
+ if message.get("status") == "finished":
255
+ self.final_result = message.get("result")
256
+ self.task_completion_event.set()
257
+
258
+ if message.get("status") == "error":
259
+ raise Exception(message.get("result"))
260
+
261
+ if message.get("status") == "new_message":
262
+ print(message.get("result"))
263
+
264
+ def _setup_agents(self):
265
+ instruction_agent_config = {
266
+ "api_key": os.getenv("API_KEY"), "api_url": os.getenv("BASE_URL"),
267
+ "engine": os.getenv("MODEL"),
268
+ "system_prompt": instruction_system_prompt.format(
269
+ os_version=platform.platform(), tools_list=self.tools_json,
270
+ workspace_path=self.work_dir, current_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
271
+ ),
272
+ "print_log": os.getenv("DEBUG", "false").lower() in ("true", "1", "t", "yes"),
273
+ "temperature": 0.7, "use_plugins": False
274
+ }
275
+
276
+ worker_agent_config = {
277
+ "api_key": os.getenv("API_KEY"), "api_url": os.getenv("BASE_URL"),
278
+ "engine": os.getenv("FAST_MODEL") or os.getenv("MODEL"),
279
+ "system_prompt": worker_system_prompt.format(
280
+ os_version=platform.platform(), workspace_path=self.work_dir,
281
+ shell=os.getenv('SHELL', 'Unknown'), current_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
282
+ tools_list=self.tools_json
283
+ ),
284
+ "print_log": True, "temperature": 0.5, "function_call_max_loop": 100
285
+ }
286
+
287
+ instruction_agent = InstructionAgent(
288
+ goal=self.goal, tools_json=self.tools_json, agent_config=instruction_agent_config, work_dir=self.work_dir, cache_messages=self.cache_messages,
289
+ broker=self.broker, listen_topic=self.WORKER_RESPONSE_TOPIC,
290
+ publish_topic=self.INSTRUCTION_TOPIC, status_topic=self.TASK_STATUS_TOPIC
291
+ )
292
+
293
+ worker_agent = WorkerAgent(
294
+ goal=self.goal, tools_json=self.tools_json, agent_config=worker_agent_config, work_dir=self.work_dir, cache_messages=self.cache_messages,
295
+ broker=self.broker, listen_topic=self.INSTRUCTION_TOPIC,
296
+ publish_topic=self.WORKER_RESPONSE_TOPIC, status_topic=self.TASK_STATUS_TOPIC
297
+ )
298
+ return instruction_agent, worker_agent
299
+
300
+ async def run(self):
301
+ """Sets up subscriptions and starts the workflow."""
302
+ os.chdir(self.work_dir.absolute())
303
+ await self._configure_tools()
304
+
305
+ instruction_agent, worker_agent = self._setup_agents()
306
+
307
+ self.broker.publish({"instruction": "Initial kickoff"}, self.INSTRUCTION_TOPIC)
308
+
309
+ self._status_subscription = self.broker.subscribe(self._task_status_subscriber, self.TASK_STATUS_TOPIC)
310
+ await self.task_completion_event.wait()
311
+
312
+ instruction_agent.dispose()
313
+ worker_agent.dispose()
314
+ self._status_subscription.dispose()
315
+ await self.mcp_manager.cleanup()
316
+ return self.final_result
317
+
318
+ async def stream_run(self):
319
+ """Runs the workflow and yields status messages."""
320
+ os.chdir(self.work_dir.absolute())
321
+ await self._configure_tools()
322
+
323
+ instruction_agent, worker_agent = self._setup_agents()
324
+
325
+ self.broker.publish({"instruction": "Initial kickoff"}, self.INSTRUCTION_TOPIC)
326
+
327
+ try:
328
+ async for message in self.broker.iter_topic(self.TASK_STATUS_TOPIC):
329
+ if message.get("status") == "new_message":
330
+ yield message.get("result")
331
+ elif message.get("status") == "finished":
332
+ yield message.get("result")
333
+ break
334
+ elif message.get("status") == "error":
335
+ raise Exception(message.get("result"))
336
+ finally:
337
+ instruction_agent.dispose()
338
+ worker_agent.dispose()
339
+ await self.mcp_manager.cleanup()
beswarm/aient/setup.py CHANGED
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
4
4
 
5
5
  setup(
6
6
  name="aient",
7
- version="1.1.53",
7
+ version="1.1.54",
8
8
  description="Aient: The Awakening of Agent.",
9
9
  long_description=Path.open(Path("README.md"), encoding="utf-8").read(),
10
10
  long_description_content_type="text/markdown",
@@ -1354,7 +1354,8 @@ async def get_openrouter_payload(request, engine, provider, api_key=None):
1354
1354
 
1355
1355
  messages = []
1356
1356
  for msg in request.messages:
1357
- name = None
1357
+ tool_calls = None
1358
+ tool_call_id = None
1358
1359
  if isinstance(msg.content, list):
1359
1360
  content = []
1360
1361
  for item in msg.content:
@@ -1366,9 +1367,25 @@ async def get_openrouter_payload(request, engine, provider, api_key=None):
1366
1367
  content.append(image_message)
1367
1368
  else:
1368
1369
  content = msg.content
1369
- name = msg.name
1370
- if name:
1371
- messages.append({"role": msg.role, "name": name, "content": content})
1370
+ tool_calls = msg.tool_calls
1371
+ tool_call_id = msg.tool_call_id
1372
+
1373
+ if tool_calls:
1374
+ tool_calls_list = []
1375
+ for tool_call in tool_calls:
1376
+ tool_calls_list.append({
1377
+ "id": tool_call.id,
1378
+ "type": tool_call.type,
1379
+ "function": {
1380
+ "name": tool_call.function.name,
1381
+ "arguments": tool_call.function.arguments
1382
+ }
1383
+ })
1384
+ if provider.get("tools"):
1385
+ messages.append({"role": msg.role, "tool_calls": tool_calls_list})
1386
+ elif tool_call_id:
1387
+ if provider.get("tools"):
1388
+ messages.append({"role": msg.role, "tool_call_id": tool_call_id, "content": content})
1372
1389
  else:
1373
1390
  # print("content", content)
1374
1391
  if isinstance(content, list):
@@ -42,11 +42,12 @@ def gemini_json_poccess(response_str):
42
42
  is_thinking = safe_get(json_data, "parts", 0, "thought", default=False)
43
43
 
44
44
  function_call_name = safe_get(json_data, "functionCall", "name", default=None)
45
- function_full_response = json.dumps(safe_get(json_data, "functionCall", "args", default=""))
45
+ function_full_response = safe_get(json_data, "functionCall", "args", default="")
46
+ function_full_response = json.dumps(function_full_response) if function_full_response else None
46
47
 
47
48
  blockReason = safe_get(json_data, 0, "promptFeedback", "blockReason", default=None)
48
49
 
49
- return is_thinking, content, image_base64, function_call_name, function_full_response, blockReason, promptTokenCount, candidatesTokenCount, totalTokenCount
50
+ return is_thinking, content, image_base64, function_call_name, function_full_response, finishReason, blockReason, promptTokenCount, candidatesTokenCount, totalTokenCount
50
51
 
51
52
  async def fetch_gemini_response_stream(client, url, headers, payload, model):
52
53
  timestamp = int(datetime.timestamp(datetime.now()))
@@ -62,7 +63,6 @@ async def fetch_gemini_response_stream(client, url, headers, payload, model):
62
63
  parts_json = ""
63
64
  async for chunk in response.aiter_text():
64
65
  buffer += chunk
65
- cache_buffer += chunk
66
66
 
67
67
  while "\n" in buffer:
68
68
  line, buffer = buffer.split("\n", 1)
@@ -77,7 +77,7 @@ async def fetch_gemini_response_stream(client, url, headers, payload, model):
77
77
  continue
78
78
 
79
79
  # https://ai.google.dev/api/generate-content?hl=zh-cn#FinishReason
80
- is_thinking, content, image_base64, function_call_name, function_full_response, blockReason, promptTokenCount, candidatesTokenCount, totalTokenCount = gemini_json_poccess(parts_json)
80
+ is_thinking, content, image_base64, function_call_name, function_full_response, finishReason, blockReason, promptTokenCount, candidatesTokenCount, totalTokenCount = gemini_json_poccess(parts_json)
81
81
 
82
82
  if is_thinking:
83
83
  sse_string = await generate_sse_response(timestamp, model, reasoning_content=content)
@@ -99,9 +99,10 @@ async def fetch_gemini_response_stream(client, url, headers, payload, model):
99
99
  if parts_json == "[]" or blockReason == "PROHIBITED_CONTENT":
100
100
  sse_string = await generate_sse_response(timestamp, model, stop="PROHIBITED_CONTENT")
101
101
  yield sse_string
102
- else:
102
+ elif finishReason:
103
103
  sse_string = await generate_sse_response(timestamp, model, stop="stop")
104
104
  yield sse_string
105
+ break
105
106
 
106
107
  parts_json = ""
107
108
 
@@ -187,7 +187,8 @@ class chatgpt(BaseLLM):
187
187
  # print(json.dumps(replaced_text, indent=4, ensure_ascii=False))
188
188
  while message_index < conversation_len:
189
189
  if self.conversation[convo_id][message_index]["role"] == self.conversation[convo_id][message_index + 1]["role"]:
190
- if self.conversation[convo_id][message_index].get("content") and self.conversation[convo_id][message_index + 1].get("content"):
190
+ if self.conversation[convo_id][message_index].get("content") and self.conversation[convo_id][message_index + 1].get("content") \
191
+ and self.conversation[convo_id][message_index].get("content") != self.conversation[convo_id][message_index + 1].get("content"):
191
192
  if type(self.conversation[convo_id][message_index + 1]["content"]) == str \
192
193
  and type(self.conversation[convo_id][message_index]["content"]) == list:
193
194
  self.conversation[convo_id][message_index + 1]["content"] = [{"type": "text", "text": self.conversation[convo_id][message_index + 1]["content"]}]
@@ -754,8 +755,8 @@ class chatgpt(BaseLLM):
754
755
 
755
756
  # 打印日志
756
757
  if self.print_log:
757
- print("api_url", kwargs.get('api_url', self.api_url.chat_url) == url)
758
- print("api_url", kwargs.get('api_url', self.api_url.chat_url))
758
+ # print("api_url", kwargs.get('api_url', self.api_url.chat_url) == url)
759
+ # print("api_url", kwargs.get('api_url', self.api_url.chat_url))
759
760
  print("api_url", url)
760
761
  # print("headers", headers)
761
762
  print("api_key", kwargs.get('api_key', self.api_key))