autoglm-gui 1.5.0__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.
Files changed (97) hide show
  1. AutoGLM_GUI/__init__.py +1 -1
  2. AutoGLM_GUI/__main__.py +11 -2
  3. AutoGLM_GUI/adb_plus/qr_pair.py +3 -3
  4. AutoGLM_GUI/agents/__init__.py +7 -2
  5. AutoGLM_GUI/agents/factory.py +46 -6
  6. AutoGLM_GUI/agents/glm/agent.py +8 -3
  7. AutoGLM_GUI/agents/glm/async_agent.py +515 -0
  8. AutoGLM_GUI/agents/glm/parser.py +4 -2
  9. AutoGLM_GUI/agents/mai/agent.py +3 -0
  10. AutoGLM_GUI/agents/protocols.py +111 -1
  11. AutoGLM_GUI/agents/stream_runner.py +11 -7
  12. AutoGLM_GUI/api/__init__.py +3 -1
  13. AutoGLM_GUI/api/agents.py +103 -37
  14. AutoGLM_GUI/api/devices.py +72 -0
  15. AutoGLM_GUI/api/history.py +27 -1
  16. AutoGLM_GUI/api/layered_agent.py +9 -8
  17. AutoGLM_GUI/api/mcp.py +6 -4
  18. AutoGLM_GUI/config_manager.py +38 -1
  19. AutoGLM_GUI/device_manager.py +28 -4
  20. AutoGLM_GUI/device_metadata_manager.py +174 -0
  21. AutoGLM_GUI/devices/mock_device.py +8 -1
  22. AutoGLM_GUI/models/history.py +45 -1
  23. AutoGLM_GUI/phone_agent_manager.py +145 -32
  24. AutoGLM_GUI/scheduler_manager.py +52 -6
  25. AutoGLM_GUI/schemas.py +101 -0
  26. AutoGLM_GUI/scrcpy_stream.py +2 -1
  27. AutoGLM_GUI/static/assets/{about-BQm96DAl.js → about-D7r9gCvG.js} +1 -1
  28. AutoGLM_GUI/static/assets/{alert-dialog-B42XxGPR.js → alert-dialog-BKM-yRiQ.js} +1 -1
  29. AutoGLM_GUI/static/assets/chat-k6TTD7PW.js +129 -0
  30. AutoGLM_GUI/static/assets/{circle-alert-D4rSJh37.js → circle-alert-sohSDLhl.js} +1 -1
  31. AutoGLM_GUI/static/assets/{dialog-DZ78cEcj.js → dialog-BgtPh0d5.js} +1 -1
  32. AutoGLM_GUI/static/assets/eye-DLqKbQmg.js +1 -0
  33. AutoGLM_GUI/static/assets/history-Bv1lfGUU.js +1 -0
  34. AutoGLM_GUI/static/assets/index-CV7jGxGm.css +1 -0
  35. AutoGLM_GUI/static/assets/index-CxWwh1VO.js +1 -0
  36. AutoGLM_GUI/static/assets/{index-CssG-3TH.js → index-SysdKciY.js} +5 -5
  37. AutoGLM_GUI/static/assets/label-DTUnzN4B.js +1 -0
  38. AutoGLM_GUI/static/assets/{logs-eoFxn5of.js → logs-BIhnDizW.js} +1 -1
  39. AutoGLM_GUI/static/assets/{popover-DLsuV5Sx.js → popover-CikYqu2P.js} +1 -1
  40. AutoGLM_GUI/static/assets/scheduled-tasks-B-KBsGbl.js +1 -0
  41. AutoGLM_GUI/static/assets/{textarea-BX6y7uM5.js → textarea-knJZrz77.js} +1 -1
  42. AutoGLM_GUI/static/assets/workflows-DzcSYwLZ.js +1 -0
  43. AutoGLM_GUI/static/index.html +2 -2
  44. {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/METADATA +58 -7
  45. autoglm_gui-1.5.2.dist-info/RECORD +119 -0
  46. AutoGLM_GUI/device_adapter.py +0 -263
  47. AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +0 -129
  48. AutoGLM_GUI/static/assets/history-DFBv7TGc.js +0 -1
  49. AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +0 -1
  50. AutoGLM_GUI/static/assets/index-CmZSnDqc.js +0 -1
  51. AutoGLM_GUI/static/assets/label-BCUzE_nm.js +0 -1
  52. AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +0 -1
  53. AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +0 -1
  54. AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +0 -1
  55. autoglm_gui-1.5.0.dist-info/RECORD +0 -157
  56. mai_agent/base.py +0 -137
  57. mai_agent/mai_grounding_agent.py +0 -263
  58. mai_agent/mai_naivigation_agent.py +0 -526
  59. mai_agent/prompt.py +0 -148
  60. mai_agent/unified_memory.py +0 -67
  61. mai_agent/utils.py +0 -73
  62. phone_agent/__init__.py +0 -12
  63. phone_agent/actions/__init__.py +0 -5
  64. phone_agent/actions/handler.py +0 -400
  65. phone_agent/actions/handler_ios.py +0 -278
  66. phone_agent/adb/__init__.py +0 -51
  67. phone_agent/adb/connection.py +0 -358
  68. phone_agent/adb/device.py +0 -253
  69. phone_agent/adb/input.py +0 -108
  70. phone_agent/adb/screenshot.py +0 -108
  71. phone_agent/agent.py +0 -253
  72. phone_agent/agent_ios.py +0 -277
  73. phone_agent/config/__init__.py +0 -53
  74. phone_agent/config/apps.py +0 -227
  75. phone_agent/config/apps_harmonyos.py +0 -256
  76. phone_agent/config/apps_ios.py +0 -339
  77. phone_agent/config/i18n.py +0 -81
  78. phone_agent/config/prompts.py +0 -80
  79. phone_agent/config/prompts_en.py +0 -79
  80. phone_agent/config/prompts_zh.py +0 -82
  81. phone_agent/config/timing.py +0 -167
  82. phone_agent/device_factory.py +0 -166
  83. phone_agent/hdc/__init__.py +0 -53
  84. phone_agent/hdc/connection.py +0 -384
  85. phone_agent/hdc/device.py +0 -269
  86. phone_agent/hdc/input.py +0 -145
  87. phone_agent/hdc/screenshot.py +0 -127
  88. phone_agent/model/__init__.py +0 -5
  89. phone_agent/model/client.py +0 -290
  90. phone_agent/xctest/__init__.py +0 -47
  91. phone_agent/xctest/connection.py +0 -379
  92. phone_agent/xctest/device.py +0 -472
  93. phone_agent/xctest/input.py +0 -311
  94. phone_agent/xctest/screenshot.py +0 -226
  95. {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/WHEEL +0 -0
  96. {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/entry_points.txt +0 -0
  97. {autoglm_gui-1.5.0.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
@@ -98,13 +98,15 @@ class GLMParser:
98
98
 
99
99
  return params
100
100
 
101
- def _parse_value(self, value_str: str) -> Any:
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
@@ -96,6 +96,9 @@ class InternalMAIAgent:
96
96
 
97
97
  if is_first:
98
98
  self.traj_memory.task_goal = task or ""
99
+ elif task:
100
+ # 多轮对话:有新的用户消息,更新 task_goal
101
+ self.traj_memory.task_goal = task
99
102
 
100
103
  return self._execute_step(task, is_first)
101
104
 
@@ -1,10 +1,27 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Protocol
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: ...