autoglm-gui 1.5.1__py3-none-any.whl → 1.5.2__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.
- AutoGLM_GUI/__init__.py +1 -1
- AutoGLM_GUI/__main__.py +11 -2
- AutoGLM_GUI/adb_plus/qr_pair.py +3 -3
- AutoGLM_GUI/agents/__init__.py +7 -2
- AutoGLM_GUI/agents/factory.py +46 -6
- AutoGLM_GUI/agents/glm/agent.py +2 -2
- AutoGLM_GUI/agents/glm/async_agent.py +515 -0
- AutoGLM_GUI/agents/glm/parser.py +4 -2
- AutoGLM_GUI/agents/protocols.py +111 -1
- AutoGLM_GUI/agents/stream_runner.py +4 -5
- AutoGLM_GUI/api/__init__.py +3 -1
- AutoGLM_GUI/api/agents.py +78 -37
- AutoGLM_GUI/api/devices.py +72 -0
- AutoGLM_GUI/api/layered_agent.py +9 -8
- AutoGLM_GUI/api/mcp.py +6 -4
- AutoGLM_GUI/config_manager.py +38 -1
- AutoGLM_GUI/device_manager.py +28 -4
- AutoGLM_GUI/device_metadata_manager.py +174 -0
- AutoGLM_GUI/devices/mock_device.py +8 -1
- AutoGLM_GUI/phone_agent_manager.py +145 -32
- AutoGLM_GUI/scheduler_manager.py +6 -6
- AutoGLM_GUI/schemas.py +89 -0
- AutoGLM_GUI/scrcpy_stream.py +2 -1
- AutoGLM_GUI/static/assets/{about-CfwX1Cmc.js → about-D7r9gCvG.js} +1 -1
- AutoGLM_GUI/static/assets/{alert-dialog-CtGlN2IJ.js → alert-dialog-BKM-yRiQ.js} +1 -1
- AutoGLM_GUI/static/assets/chat-k6TTD7PW.js +129 -0
- AutoGLM_GUI/static/assets/{circle-alert-t08bEMPO.js → circle-alert-sohSDLhl.js} +1 -1
- AutoGLM_GUI/static/assets/{dialog-FNwZJFwk.js → dialog-BgtPh0d5.js} +1 -1
- AutoGLM_GUI/static/assets/{eye-D0UPWCWC.js → eye-DLqKbQmg.js} +1 -1
- AutoGLM_GUI/static/assets/{history-CRo95B7i.js → history-Bv1lfGUU.js} +1 -1
- AutoGLM_GUI/static/assets/index-CxWwh1VO.js +1 -0
- AutoGLM_GUI/static/assets/{index-CTHbFvKl.js → index-SysdKciY.js} +5 -5
- AutoGLM_GUI/static/assets/label-DTUnzN4B.js +1 -0
- AutoGLM_GUI/static/assets/{logs-RW09DyYY.js → logs-BIhnDizW.js} +1 -1
- AutoGLM_GUI/static/assets/{popover--JTJrE5v.js → popover-CikYqu2P.js} +1 -1
- AutoGLM_GUI/static/assets/scheduled-tasks-B-KBsGbl.js +1 -0
- AutoGLM_GUI/static/assets/{textarea-PRmVnWq5.js → textarea-knJZrz77.js} +1 -1
- AutoGLM_GUI/static/assets/workflows-DzcSYwLZ.js +1 -0
- AutoGLM_GUI/static/index.html +1 -1
- {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.2.dist-info}/METADATA +10 -1
- {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.2.dist-info}/RECORD +44 -43
- AutoGLM_GUI/static/assets/chat-BYa-foUI.js +0 -129
- AutoGLM_GUI/static/assets/index-BaLMSqd3.js +0 -1
- AutoGLM_GUI/static/assets/label-DJFevVmr.js +0 -1
- AutoGLM_GUI/static/assets/scheduled-tasks-DTRKsQXF.js +0 -1
- AutoGLM_GUI/static/assets/square-pen-CPK_K680.js +0 -1
- AutoGLM_GUI/static/assets/workflows-CdcsAoaT.js +0 -1
- {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.2.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.2.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.5.1.dist-info → autoglm_gui-1.5.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""AsyncGLMAgent - 异步 GLM Agent 实现,支持原生流式输出和立即取消。"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import traceback
|
|
6
|
+
from typing import Any, AsyncIterator, Callable
|
|
7
|
+
|
|
8
|
+
from openai import AsyncOpenAI
|
|
9
|
+
|
|
10
|
+
from AutoGLM_GUI.actions import ActionHandler, ActionResult
|
|
11
|
+
from AutoGLM_GUI.config import AgentConfig, ModelConfig, StepResult
|
|
12
|
+
from AutoGLM_GUI.device_protocol import DeviceProtocol
|
|
13
|
+
from AutoGLM_GUI.logger import logger
|
|
14
|
+
from AutoGLM_GUI.prompt_config import get_messages, get_system_prompt
|
|
15
|
+
|
|
16
|
+
from .message_builder import MessageBuilder
|
|
17
|
+
from .parser import GLMParser
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsyncGLMAgent:
|
|
21
|
+
"""异步 GLM Agent 实现。
|
|
22
|
+
|
|
23
|
+
核心特性:
|
|
24
|
+
- 使用 AsyncOpenAI 进行异步 LLM 调用
|
|
25
|
+
- 原生支持流式输出 (async for)
|
|
26
|
+
- 支持立即取消 (asyncio.CancelledError)
|
|
27
|
+
- 使用 asyncio.to_thread 包装同步的设备操作
|
|
28
|
+
|
|
29
|
+
与 GLMAgent 的区别:
|
|
30
|
+
- stream() 方法返回 AsyncIterator,不需要 worker 线程
|
|
31
|
+
- cancel() 可以立即中断 HTTP 请求
|
|
32
|
+
- 不需要 monkey-patch thinking_callback
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
model_config: ModelConfig,
|
|
38
|
+
agent_config: AgentConfig,
|
|
39
|
+
device: DeviceProtocol,
|
|
40
|
+
confirmation_callback: Callable[[str], bool] | None = None,
|
|
41
|
+
takeover_callback: Callable[[str], None] | None = None,
|
|
42
|
+
):
|
|
43
|
+
self.model_config = model_config
|
|
44
|
+
self.agent_config = agent_config
|
|
45
|
+
|
|
46
|
+
# 使用 AsyncOpenAI
|
|
47
|
+
self.openai_client = AsyncOpenAI(
|
|
48
|
+
base_url=model_config.base_url,
|
|
49
|
+
api_key=model_config.api_key,
|
|
50
|
+
timeout=120,
|
|
51
|
+
)
|
|
52
|
+
self.parser = GLMParser()
|
|
53
|
+
|
|
54
|
+
self.device = device
|
|
55
|
+
self.action_handler = ActionHandler(
|
|
56
|
+
device=self.device,
|
|
57
|
+
confirmation_callback=confirmation_callback,
|
|
58
|
+
takeover_callback=takeover_callback,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# 取消机制
|
|
62
|
+
self._cancel_event = asyncio.Event()
|
|
63
|
+
|
|
64
|
+
# 状态
|
|
65
|
+
self._context: list[dict[str, Any]] = []
|
|
66
|
+
self._step_count = 0
|
|
67
|
+
self._is_running = False
|
|
68
|
+
|
|
69
|
+
async def stream(self, task: str) -> AsyncIterator[dict[str, Any]]:
|
|
70
|
+
"""流式执行任务,支持取消。
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
task: 任务描述
|
|
74
|
+
|
|
75
|
+
Yields:
|
|
76
|
+
dict[str, Any]: 事件字典,格式为 {"type": str, "data": dict}
|
|
77
|
+
|
|
78
|
+
事件类型:
|
|
79
|
+
- "thinking": {"chunk": str}
|
|
80
|
+
- "step": {"step": int, "thinking": str, "action": dict, ...}
|
|
81
|
+
- "done": {"message": str, "steps": int, "success": bool}
|
|
82
|
+
- "cancelled": {"message": str}
|
|
83
|
+
- "error": {"message": str}
|
|
84
|
+
"""
|
|
85
|
+
self._context = []
|
|
86
|
+
self._step_count = 0
|
|
87
|
+
self._is_running = True
|
|
88
|
+
self._cancel_event.clear()
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
# 首步执行
|
|
92
|
+
async for event in self._execute_step_async(task, is_first=True):
|
|
93
|
+
yield event
|
|
94
|
+
|
|
95
|
+
# 检查是否完成
|
|
96
|
+
if event["type"] == "step" and event["data"].get("finished"):
|
|
97
|
+
yield {
|
|
98
|
+
"type": "done",
|
|
99
|
+
"data": {
|
|
100
|
+
"message": event["data"].get("message", "Task completed"),
|
|
101
|
+
"steps": self._step_count,
|
|
102
|
+
"success": event["data"].get("success", True),
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
# 后续步骤
|
|
108
|
+
while self._step_count < self.agent_config.max_steps and self._is_running:
|
|
109
|
+
# 检查取消
|
|
110
|
+
if self._cancel_event.is_set():
|
|
111
|
+
raise asyncio.CancelledError()
|
|
112
|
+
|
|
113
|
+
async for event in self._execute_step_async(None, is_first=False):
|
|
114
|
+
yield event
|
|
115
|
+
|
|
116
|
+
# 检查是否完成
|
|
117
|
+
if event["type"] == "step" and event["data"].get("finished"):
|
|
118
|
+
yield {
|
|
119
|
+
"type": "done",
|
|
120
|
+
"data": {
|
|
121
|
+
"message": event["data"].get(
|
|
122
|
+
"message", "Task completed"
|
|
123
|
+
),
|
|
124
|
+
"steps": self._step_count,
|
|
125
|
+
"success": event["data"].get("success", True),
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# 达到最大步数
|
|
131
|
+
yield {
|
|
132
|
+
"type": "done",
|
|
133
|
+
"data": {
|
|
134
|
+
"message": "Max steps reached",
|
|
135
|
+
"steps": self._step_count,
|
|
136
|
+
"success": False,
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
except asyncio.CancelledError:
|
|
141
|
+
yield {
|
|
142
|
+
"type": "cancelled",
|
|
143
|
+
"data": {"message": "Task cancelled by user"},
|
|
144
|
+
}
|
|
145
|
+
raise
|
|
146
|
+
|
|
147
|
+
finally:
|
|
148
|
+
self._is_running = False
|
|
149
|
+
|
|
150
|
+
async def _execute_step_async(
|
|
151
|
+
self, user_prompt: str | None, is_first: bool
|
|
152
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
153
|
+
"""执行单步,支持流式输出和取消。
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
user_prompt: 用户输入(首步必需,后续可选)
|
|
157
|
+
is_first: 是否是首步
|
|
158
|
+
|
|
159
|
+
Yields:
|
|
160
|
+
dict[str, Any]: 事件字典
|
|
161
|
+
"""
|
|
162
|
+
self._step_count += 1
|
|
163
|
+
|
|
164
|
+
# 1. 截图和获取当前应用(使用线程池)
|
|
165
|
+
try:
|
|
166
|
+
screenshot = await asyncio.to_thread(self.device.get_screenshot)
|
|
167
|
+
current_app = await asyncio.to_thread(self.device.get_current_app)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error(f"Failed to get device info: {e}")
|
|
170
|
+
yield {
|
|
171
|
+
"type": "error",
|
|
172
|
+
"data": {"message": f"Device error: {e}"},
|
|
173
|
+
}
|
|
174
|
+
yield {
|
|
175
|
+
"type": "step",
|
|
176
|
+
"data": {
|
|
177
|
+
"step": self._step_count,
|
|
178
|
+
"thinking": "",
|
|
179
|
+
"action": None,
|
|
180
|
+
"success": False,
|
|
181
|
+
"finished": True,
|
|
182
|
+
"message": f"Device error: {e}",
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
# 2. 构建消息
|
|
188
|
+
if is_first:
|
|
189
|
+
system_prompt = self.agent_config.system_prompt
|
|
190
|
+
if system_prompt is None:
|
|
191
|
+
system_prompt = get_system_prompt(self.agent_config.lang)
|
|
192
|
+
|
|
193
|
+
self._context.append(MessageBuilder.create_system_message(system_prompt))
|
|
194
|
+
|
|
195
|
+
screen_info = MessageBuilder.build_screen_info(current_app)
|
|
196
|
+
text_content = f"{user_prompt}\n\n{screen_info}"
|
|
197
|
+
|
|
198
|
+
self._context.append(
|
|
199
|
+
MessageBuilder.create_user_message(
|
|
200
|
+
text=text_content, image_base64=screenshot.base64_data
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
screen_info = MessageBuilder.build_screen_info(current_app)
|
|
205
|
+
if user_prompt:
|
|
206
|
+
text_content = f"{user_prompt}\n\n** Screen Info **\n\n{screen_info}"
|
|
207
|
+
else:
|
|
208
|
+
text_content = f"** Screen Info **\n\n{screen_info}"
|
|
209
|
+
|
|
210
|
+
self._context.append(
|
|
211
|
+
MessageBuilder.create_user_message(
|
|
212
|
+
text=text_content, image_base64=screenshot.base64_data
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# 3. 流式调用 OpenAI(真正的异步,可取消)
|
|
217
|
+
try:
|
|
218
|
+
if self.agent_config.verbose:
|
|
219
|
+
msgs = get_messages(self.agent_config.lang)
|
|
220
|
+
print("\n" + "=" * 50)
|
|
221
|
+
print(f"💭 {msgs['thinking']}:")
|
|
222
|
+
print("-" * 50)
|
|
223
|
+
|
|
224
|
+
thinking_parts = []
|
|
225
|
+
raw_content = ""
|
|
226
|
+
|
|
227
|
+
async for chunk_data in self._stream_openai(self._context):
|
|
228
|
+
# 检查取消
|
|
229
|
+
if self._cancel_event.is_set():
|
|
230
|
+
raise asyncio.CancelledError()
|
|
231
|
+
|
|
232
|
+
if chunk_data["type"] == "thinking":
|
|
233
|
+
thinking_parts.append(chunk_data["content"])
|
|
234
|
+
|
|
235
|
+
# Yield thinking event
|
|
236
|
+
yield {
|
|
237
|
+
"type": "thinking",
|
|
238
|
+
"data": {"chunk": chunk_data["content"]},
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Verbose output
|
|
242
|
+
if self.agent_config.verbose:
|
|
243
|
+
print(chunk_data["content"], end="", flush=True)
|
|
244
|
+
|
|
245
|
+
elif chunk_data["type"] == "raw":
|
|
246
|
+
raw_content += chunk_data["content"]
|
|
247
|
+
|
|
248
|
+
thinking = "".join(thinking_parts)
|
|
249
|
+
|
|
250
|
+
except asyncio.CancelledError:
|
|
251
|
+
logger.info(f"Step {self._step_count} cancelled during LLM call")
|
|
252
|
+
raise
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.error(f"LLM error: {e}")
|
|
256
|
+
if self.agent_config.verbose:
|
|
257
|
+
traceback.print_exc()
|
|
258
|
+
|
|
259
|
+
yield {
|
|
260
|
+
"type": "error",
|
|
261
|
+
"data": {"message": f"Model error: {e}"},
|
|
262
|
+
}
|
|
263
|
+
yield {
|
|
264
|
+
"type": "step",
|
|
265
|
+
"data": {
|
|
266
|
+
"step": self._step_count,
|
|
267
|
+
"thinking": "",
|
|
268
|
+
"action": None,
|
|
269
|
+
"success": False,
|
|
270
|
+
"finished": True,
|
|
271
|
+
"message": f"Model error: {e}",
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
# 4. 解析 action
|
|
277
|
+
_, action_str = self._parse_raw_response(raw_content)
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
action = self.parser.parse(action_str)
|
|
281
|
+
except ValueError as e:
|
|
282
|
+
if self.agent_config.verbose:
|
|
283
|
+
logger.warning(f"Failed to parse action: {e}, treating as finish")
|
|
284
|
+
action = {"_metadata": "finish", "message": action_str}
|
|
285
|
+
|
|
286
|
+
if self.agent_config.verbose:
|
|
287
|
+
msgs = get_messages(self.agent_config.lang)
|
|
288
|
+
print()
|
|
289
|
+
print("-" * 50)
|
|
290
|
+
print(f"🎯 {msgs['action']}:")
|
|
291
|
+
print(json.dumps(action, ensure_ascii=False, indent=2))
|
|
292
|
+
print("=" * 50 + "\n")
|
|
293
|
+
|
|
294
|
+
# 5. 执行 action(使用线程池)
|
|
295
|
+
try:
|
|
296
|
+
result = await asyncio.to_thread(
|
|
297
|
+
self.action_handler.execute, action, screenshot.width, screenshot.height
|
|
298
|
+
)
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.error(f"Action execution error: {e}")
|
|
301
|
+
if self.agent_config.verbose:
|
|
302
|
+
traceback.print_exc()
|
|
303
|
+
result = ActionResult(success=False, should_finish=True, message=str(e))
|
|
304
|
+
|
|
305
|
+
# 6. 更新上下文
|
|
306
|
+
self._context[-1] = MessageBuilder.remove_images_from_message(self._context[-1])
|
|
307
|
+
|
|
308
|
+
self._context.append(
|
|
309
|
+
MessageBuilder.create_assistant_message(
|
|
310
|
+
f"<think>{thinking}</think><answer>{action_str}</answer>"
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# 7. 检查是否完成
|
|
315
|
+
finished = action.get("_metadata") == "finish" or result.should_finish
|
|
316
|
+
|
|
317
|
+
if finished and self.agent_config.verbose:
|
|
318
|
+
msgs = get_messages(self.agent_config.lang)
|
|
319
|
+
print("\n" + "🎉 " + "=" * 48)
|
|
320
|
+
print(
|
|
321
|
+
f"✅ {msgs['task_completed']}: {result.message or action.get('message', msgs['done'])}"
|
|
322
|
+
)
|
|
323
|
+
print("=" * 50 + "\n")
|
|
324
|
+
|
|
325
|
+
# 8. 返回步骤结果
|
|
326
|
+
yield {
|
|
327
|
+
"type": "step",
|
|
328
|
+
"data": {
|
|
329
|
+
"step": self._step_count,
|
|
330
|
+
"thinking": thinking,
|
|
331
|
+
"action": action,
|
|
332
|
+
"success": result.success,
|
|
333
|
+
"finished": finished,
|
|
334
|
+
"message": result.message or action.get("message"),
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async def _stream_openai(
|
|
339
|
+
self, messages: list[dict[str, Any]]
|
|
340
|
+
) -> AsyncIterator[dict[str, str]]:
|
|
341
|
+
"""流式调用 OpenAI,yield thinking chunks。
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
messages: 消息列表
|
|
345
|
+
|
|
346
|
+
Yields:
|
|
347
|
+
dict[str, str]: {"type": "thinking" | "raw", "content": str}
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
asyncio.CancelledError: 任务被取消
|
|
351
|
+
"""
|
|
352
|
+
stream = await self.openai_client.chat.completions.create(
|
|
353
|
+
messages=messages, # type: ignore[arg-type]
|
|
354
|
+
model=self.model_config.model_name,
|
|
355
|
+
max_tokens=self.model_config.max_tokens,
|
|
356
|
+
temperature=self.model_config.temperature,
|
|
357
|
+
top_p=self.model_config.top_p,
|
|
358
|
+
frequency_penalty=self.model_config.frequency_penalty,
|
|
359
|
+
extra_body=self.model_config.extra_body,
|
|
360
|
+
stream=True,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
buffer = ""
|
|
364
|
+
action_markers = ["finish(message=", "do(action="]
|
|
365
|
+
in_action_phase = False
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
async for chunk in stream:
|
|
369
|
+
# 检查取消
|
|
370
|
+
if self._cancel_event.is_set():
|
|
371
|
+
await stream.close() # 关键:关闭 HTTP 连接
|
|
372
|
+
raise asyncio.CancelledError()
|
|
373
|
+
|
|
374
|
+
if len(chunk.choices) == 0:
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
if chunk.choices[0].delta.content is not None:
|
|
378
|
+
content = chunk.choices[0].delta.content
|
|
379
|
+
yield {"type": "raw", "content": content}
|
|
380
|
+
|
|
381
|
+
if in_action_phase:
|
|
382
|
+
continue
|
|
383
|
+
|
|
384
|
+
buffer += content
|
|
385
|
+
|
|
386
|
+
# 检查是否到达 action 标记
|
|
387
|
+
marker_found = False
|
|
388
|
+
for marker in action_markers:
|
|
389
|
+
if marker in buffer:
|
|
390
|
+
thinking_part = buffer.split(marker, 1)[0]
|
|
391
|
+
yield {"type": "thinking", "content": thinking_part}
|
|
392
|
+
in_action_phase = True
|
|
393
|
+
marker_found = True
|
|
394
|
+
break
|
|
395
|
+
|
|
396
|
+
if marker_found:
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
# 检查是否是潜在的 marker 前缀
|
|
400
|
+
is_potential_marker = False
|
|
401
|
+
for marker in action_markers:
|
|
402
|
+
for i in range(1, len(marker)):
|
|
403
|
+
if buffer.endswith(marker[:i]):
|
|
404
|
+
is_potential_marker = True
|
|
405
|
+
break
|
|
406
|
+
if is_potential_marker:
|
|
407
|
+
break
|
|
408
|
+
|
|
409
|
+
if not is_potential_marker and len(buffer) > 0:
|
|
410
|
+
yield {"type": "thinking", "content": buffer}
|
|
411
|
+
buffer = ""
|
|
412
|
+
|
|
413
|
+
finally:
|
|
414
|
+
await stream.close() # 确保资源释放
|
|
415
|
+
|
|
416
|
+
def _parse_raw_response(self, content: str) -> tuple[str, str]:
|
|
417
|
+
"""解析原始响应,提取 thinking 和 action。
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
content: 原始响应内容
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
tuple[str, str]: (thinking, action)
|
|
424
|
+
"""
|
|
425
|
+
if "finish(message=" in content:
|
|
426
|
+
parts = content.split("finish(message=", 1)
|
|
427
|
+
thinking = parts[0].strip()
|
|
428
|
+
action = "finish(message=" + parts[1]
|
|
429
|
+
return thinking, action
|
|
430
|
+
|
|
431
|
+
if "do(action=" in content:
|
|
432
|
+
parts = content.split("do(action=", 1)
|
|
433
|
+
thinking = parts[0].strip()
|
|
434
|
+
action = "do(action=" + parts[1]
|
|
435
|
+
return thinking, action
|
|
436
|
+
|
|
437
|
+
if "<answer>" in content:
|
|
438
|
+
parts = content.split("<answer>", 1)
|
|
439
|
+
thinking = parts[0].replace("<think>", "").replace("</think>", "").strip()
|
|
440
|
+
action = parts[1].replace("</answer>", "").strip()
|
|
441
|
+
return thinking, action
|
|
442
|
+
|
|
443
|
+
return "", content
|
|
444
|
+
|
|
445
|
+
async def cancel(self) -> None:
|
|
446
|
+
"""取消当前执行。
|
|
447
|
+
|
|
448
|
+
设置取消标志,中断正在进行的 HTTP 请求。
|
|
449
|
+
"""
|
|
450
|
+
self._cancel_event.set()
|
|
451
|
+
self._is_running = False
|
|
452
|
+
logger.info("AsyncGLMAgent cancelled by user")
|
|
453
|
+
|
|
454
|
+
def reset(self) -> None:
|
|
455
|
+
"""重置状态。"""
|
|
456
|
+
self._context = []
|
|
457
|
+
self._step_count = 0
|
|
458
|
+
self._is_running = False
|
|
459
|
+
self._cancel_event.clear()
|
|
460
|
+
|
|
461
|
+
async def run(self, task: str) -> str:
|
|
462
|
+
"""运行完整任务(兼容接口)。
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
task: 任务描述
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
str: 最终结果消息
|
|
469
|
+
"""
|
|
470
|
+
final_message = ""
|
|
471
|
+
async for event in self.stream(task):
|
|
472
|
+
if event["type"] == "done":
|
|
473
|
+
final_message = event["data"].get("message", "")
|
|
474
|
+
return final_message
|
|
475
|
+
|
|
476
|
+
async def step(self, task: str | None = None) -> StepResult:
|
|
477
|
+
"""执行单步(兼容接口)。
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
task: 任务描述(首步必需,后续可选)
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
StepResult: 步骤结果
|
|
484
|
+
"""
|
|
485
|
+
is_first = len(self._context) == 0
|
|
486
|
+
if is_first and not task:
|
|
487
|
+
raise ValueError("Task is required for the first step")
|
|
488
|
+
|
|
489
|
+
result = None
|
|
490
|
+
async for event in self._execute_step_async(task, is_first):
|
|
491
|
+
if event["type"] == "step":
|
|
492
|
+
result = StepResult(
|
|
493
|
+
thinking=event["data"]["thinking"],
|
|
494
|
+
action=event["data"]["action"],
|
|
495
|
+
success=event["data"]["success"],
|
|
496
|
+
finished=event["data"]["finished"],
|
|
497
|
+
message=event["data"].get("message"),
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
if result is None:
|
|
501
|
+
raise RuntimeError("Step execution did not produce a result")
|
|
502
|
+
|
|
503
|
+
return result
|
|
504
|
+
|
|
505
|
+
@property
|
|
506
|
+
def step_count(self) -> int:
|
|
507
|
+
return self._step_count
|
|
508
|
+
|
|
509
|
+
@property
|
|
510
|
+
def context(self) -> list[dict[str, Any]]:
|
|
511
|
+
return self._context.copy()
|
|
512
|
+
|
|
513
|
+
@property
|
|
514
|
+
def is_running(self) -> bool:
|
|
515
|
+
return self._is_running
|
AutoGLM_GUI/agents/glm/parser.py
CHANGED
|
@@ -98,13 +98,15 @@ class GLMParser:
|
|
|
98
98
|
|
|
99
99
|
return params
|
|
100
100
|
|
|
101
|
-
def _parse_value(
|
|
101
|
+
def _parse_value(
|
|
102
|
+
self, value_str: str
|
|
103
|
+
) -> str | int | float | bool | list | dict | None:
|
|
102
104
|
value_str = value_str.strip()
|
|
103
105
|
|
|
104
106
|
if not value_str:
|
|
105
107
|
return ""
|
|
106
108
|
|
|
107
109
|
try:
|
|
108
|
-
return ast.literal_eval(value_str)
|
|
110
|
+
return ast.literal_eval(value_str) # type: ignore[no-any-return]
|
|
109
111
|
except (ValueError, SyntaxError):
|
|
110
112
|
return value_str
|
AutoGLM_GUI/agents/protocols.py
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any, AsyncIterator, Protocol
|
|
4
5
|
|
|
5
6
|
from AutoGLM_GUI.config import AgentConfig, ModelConfig, StepResult
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
def is_async_agent(agent: AsyncAgent | BaseAgent) -> bool:
|
|
10
|
+
"""Check if an agent implements the AsyncAgent interface.
|
|
11
|
+
|
|
12
|
+
Uses runtime inspection to detect async capabilities since static
|
|
13
|
+
type narrowing is not possible with Protocol union types.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
agent: Agent instance to check
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
True if agent has async stream() method, False otherwise
|
|
20
|
+
"""
|
|
21
|
+
stream_method = getattr(agent, "stream", None)
|
|
22
|
+
return stream_method is not None and inspect.isasyncgenfunction(stream_method)
|
|
23
|
+
|
|
24
|
+
|
|
8
25
|
class BaseAgent(Protocol):
|
|
9
26
|
model_config: ModelConfig
|
|
10
27
|
agent_config: AgentConfig
|
|
@@ -25,3 +42,96 @@ class BaseAgent(Protocol):
|
|
|
25
42
|
|
|
26
43
|
@property
|
|
27
44
|
def is_running(self) -> bool: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AsyncAgent(Protocol):
|
|
48
|
+
"""异步 Agent 接口,原生支持流式输出和取消。
|
|
49
|
+
|
|
50
|
+
核心特性:
|
|
51
|
+
- stream() 方法返回 AsyncIterator[dict],支持原生 async for
|
|
52
|
+
- cancel() 方法使用 asyncio 取消机制,可立即中断 HTTP 请求
|
|
53
|
+
- 不需要 worker 线程、queue、monkey-patch
|
|
54
|
+
|
|
55
|
+
使用示例:
|
|
56
|
+
async for event in agent.stream("打开微信"):
|
|
57
|
+
if event["type"] == "thinking":
|
|
58
|
+
print(event["data"]["chunk"])
|
|
59
|
+
elif event["type"] == "step":
|
|
60
|
+
print(f"Step {event['data']['step']}")
|
|
61
|
+
elif event["type"] == "done":
|
|
62
|
+
break
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
model_config: ModelConfig
|
|
66
|
+
agent_config: AgentConfig
|
|
67
|
+
|
|
68
|
+
async def run(self, task: str) -> str:
|
|
69
|
+
"""运行完整任务,返回最终结果。
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
task: 任务描述
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
str: 最终结果消息
|
|
76
|
+
"""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
async def step(self, task: str | None = None) -> StepResult:
|
|
80
|
+
"""执行单步,返回步骤结果。
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
task: 任务描述(首步必需,后续可选)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
StepResult: 步骤结果
|
|
87
|
+
"""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
async def stream(self, task: str) -> AsyncIterator[dict[str, Any]]:
|
|
91
|
+
"""流式执行任务,yield 事件字典。
|
|
92
|
+
|
|
93
|
+
这是核心方法,支持:
|
|
94
|
+
- 实时流式输出 (thinking chunks)
|
|
95
|
+
- 立即取消 (通过 asyncio.CancelledError)
|
|
96
|
+
- 不需要额外的线程或队列
|
|
97
|
+
|
|
98
|
+
事件类型:
|
|
99
|
+
- "thinking": {"chunk": str} - 思考过程片段
|
|
100
|
+
- "step": {"step": int, "thinking": str, "action": dict, ...} - 步骤完成
|
|
101
|
+
- "done": {"message": str, "steps": int, "success": bool} - 任务完成
|
|
102
|
+
- "cancelled": {"message": str} - 任务取消
|
|
103
|
+
- "error": {"message": str} - 错误
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
task: 任务描述
|
|
107
|
+
|
|
108
|
+
Yields:
|
|
109
|
+
dict[str, Any]: 事件字典,格式为 {"type": str, "data": dict}
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
asyncio.CancelledError: 任务被取消
|
|
113
|
+
"""
|
|
114
|
+
...
|
|
115
|
+
|
|
116
|
+
async def cancel(self) -> None:
|
|
117
|
+
"""取消当前执行(立即中断网络请求)。
|
|
118
|
+
|
|
119
|
+
使用 asyncio 的取消机制,会:
|
|
120
|
+
1. 设置内部取消标志
|
|
121
|
+
2. 关闭正在进行的 HTTP 连接
|
|
122
|
+
3. 抛出 asyncio.CancelledError
|
|
123
|
+
"""
|
|
124
|
+
...
|
|
125
|
+
|
|
126
|
+
def reset(self) -> None:
|
|
127
|
+
"""重置状态(同步方法,只清理内存)。"""
|
|
128
|
+
...
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def step_count(self) -> int: ...
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def context(self) -> list[dict[str, Any]]: ...
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def is_running(self) -> bool: ...
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import queue
|
|
2
2
|
import threading
|
|
3
|
-
import typing
|
|
4
3
|
from contextlib import contextmanager
|
|
5
|
-
from typing import Any, Callable, Iterator, Optional
|
|
4
|
+
from typing import Any, Callable, Iterator, Optional, TYPE_CHECKING
|
|
6
5
|
|
|
7
6
|
from AutoGLM_GUI.agents.events import AgentEvent, AgentEventType
|
|
8
7
|
|
|
9
|
-
if
|
|
8
|
+
if TYPE_CHECKING:
|
|
10
9
|
from AutoGLM_GUI.agents.protocols import BaseAgent
|
|
11
10
|
|
|
12
11
|
|
|
@@ -76,7 +75,7 @@ class AgentStepStreamer:
|
|
|
76
75
|
def _start_worker(self) -> None:
|
|
77
76
|
"""启动 worker 线程."""
|
|
78
77
|
|
|
79
|
-
def worker():
|
|
78
|
+
def worker() -> None:
|
|
80
79
|
try:
|
|
81
80
|
# 检查停止事件
|
|
82
81
|
if self._stop_event.is_set():
|
|
@@ -87,7 +86,7 @@ class AgentStepStreamer:
|
|
|
87
86
|
# 假设 agent 有 _thinking_callback 属性
|
|
88
87
|
original_callback = getattr(self._agent, "_thinking_callback", None)
|
|
89
88
|
|
|
90
|
-
def on_thinking(chunk: str):
|
|
89
|
+
def on_thinking(chunk: str) -> None:
|
|
91
90
|
self._event_queue.put(
|
|
92
91
|
(AgentEventType.THINKING.value, {"chunk": chunk})
|
|
93
92
|
)
|