agt-agent 0.1.0__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.
- agt_agent-0.1.0.dist-info/METADATA +159 -0
- agt_agent-0.1.0.dist-info/RECORD +25 -0
- agt_agent-0.1.0.dist-info/WHEEL +5 -0
- agt_agent-0.1.0.dist-info/entry_points.txt +3 -0
- agt_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- agt_agent-0.1.0.dist-info/top_level.txt +1 -0
- src/__init__.py +6 -0
- src/agent.py +375 -0
- src/agent_config.py +107 -0
- src/chat.py +147 -0
- src/commands.py +391 -0
- src/config.py +102 -0
- src/llm_client.py +221 -0
- src/lsp_server.py +111 -0
- src/mcp_client.py +167 -0
- src/multiagent.py +100 -0
- src/plan_tools.py +52 -0
- src/prompts.py +74 -0
- src/real_tools.py +530 -0
- src/session.py +296 -0
- src/snapshots.py +71 -0
- src/tools.py +154 -0
- src/web.py +526 -0
- src/wiki.py +170 -0
- src/workflow.py +1334 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agt-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI Agent 框架——多模型 ReAct 引擎 + MCP 集成 + Coze 工作流 + WebUI + 可视化编辑器
|
|
5
|
+
Author-email: 马建强 <vgp7758@github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vgp7758/Agt
|
|
8
|
+
Project-URL: Repository, https://github.com/vgp7758/Agt
|
|
9
|
+
Project-URL: Issues, https://github.com/vgp7758/Agt/issues
|
|
10
|
+
Keywords: ai,agent,llm,mcp,workflow,coze
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: openai>=1.0.0
|
|
24
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
25
|
+
Requires-Dist: ddgs>=9.0.0
|
|
26
|
+
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Requires-Dist: requests>=2.28.0
|
|
28
|
+
Requires-Dist: mcp>=1.0.0
|
|
29
|
+
Requires-Dist: pyyaml>=6.0
|
|
30
|
+
Requires-Dist: jedi>=0.19
|
|
31
|
+
Requires-Dist: fastapi>=0.100
|
|
32
|
+
Requires-Dist: uvicorn[standard]>=0.20
|
|
33
|
+
Requires-Dist: websockets>=12.0
|
|
34
|
+
Requires-Dist: python-docx>=0.8
|
|
35
|
+
Requires-Dist: openpyxl>=3.0
|
|
36
|
+
Requires-Dist: PyPDF2>=3.0
|
|
37
|
+
Requires-Dist: pymupdf>=1.23
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# Agt — AI Agent 框架
|
|
44
|
+
|
|
45
|
+
> 多模型 ReAct 引擎 + MCP 工具 + Coze 工作流 + WebUI + 可视化编辑器。不依赖 LangChain / LlamaIndex / AutoGen,每个模块手写。
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install agt-agent
|
|
49
|
+
agt # CLI 对话
|
|
50
|
+
agt-web # WebUI → http://localhost:8000
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 功能
|
|
56
|
+
|
|
57
|
+
| 模块 | 说明 |
|
|
58
|
+
|---|---|
|
|
59
|
+
| **ReAct 引擎** | 思考 → 工具调用 → 观察 → 循环,最大步数 + Token 预算 + Ctrl+C 中断 |
|
|
60
|
+
| **多模型** | DeepSeek / Qwen / GLM / MiniMax / StepFun / 任意 OpenAI 兼容,热切换 + 回退链 |
|
|
61
|
+
| **MCP** | Model Context Protocol 客户端,自动发现工具 |
|
|
62
|
+
| **工作流** | Coze 原生画布 JSON 执行器,13 类节点,每轮扫描注册为 Agent 工具 |
|
|
63
|
+
| **可视化编辑器** | SVG 画布 + 节点拖拽 + 流程线/变量线(彩色) + 属性面板,零依赖 |
|
|
64
|
+
| **多 Agent** | SubAgent 并行调度,工具继承,防递归 |
|
|
65
|
+
| **WebUI** | FastAPI + WebSocket,多客户端广播,断线续连 |
|
|
66
|
+
| **会话** | 分层上下文(全局摘要 + 最近窗口融合)+ 存档/恢复 |
|
|
67
|
+
| **自主模式** | 定时/目标驱动,消息队列 |
|
|
68
|
+
| **Wiki** | `.agent/wiki/` 知识库 CRUD + 子 Agent 自动维护 |
|
|
69
|
+
| **技能** | `.agent/skills/` YAML 渐进式披露 |
|
|
70
|
+
| **Agentic RAG** | 工作流设 `auto:true`,消息自动预取注入 |
|
|
71
|
+
| **批处理** | 任意节点对数组逐元素执行,输出 all/filtered/nth |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 快速开始
|
|
76
|
+
|
|
77
|
+
### 1. 安装
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install agt-agent
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2. 配置模型
|
|
84
|
+
|
|
85
|
+
启动后点 ⚙ 设置 → 添加模型(base_url / api_token / model id),保存到 `~/.agt/models.json`。
|
|
86
|
+
|
|
87
|
+
或手动:复制 `models.example.py` → `models.py`,填 token。
|
|
88
|
+
|
|
89
|
+
### 3. 启动
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
agt # CLI
|
|
93
|
+
agt-web # WebUI(浏览器打开 http://localhost:8000)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 工作流
|
|
99
|
+
|
|
100
|
+
1. 打开 `http://localhost:8000/editor`(或 WebUI 点 ✏ 编辑器)
|
|
101
|
+
2. 左侧调色板拖放节点 → 连线 → 右侧面板编辑属性 → Ctrl+S 保存
|
|
102
|
+
3. 文件存入 `.agent/workflows/<名>.json`
|
|
103
|
+
4. Agent 每轮自动扫描 → 注册为 `wf_<名>` 工具,模型可调用
|
|
104
|
+
|
|
105
|
+
**自动工作流(Agentic RAG)**:编辑器勾选 ✅ 自动 + 填写参数名,保存后 `meta` 写入 `{"auto":true,"auto_param":"query"}`。之后每次发消息,Agent 先跑该工作流取上下文再处理。
|
|
106
|
+
|
|
107
|
+
### 节点类型(13 类)
|
|
108
|
+
|
|
109
|
+
开始/结束 · LLM · 代码 · 选择器(分支) · 循环 · 批处理 · 意图识别 · JSON 解析/构造 · 文本处理 · HTTP 请求 · 子工作流 · 插件 · 变量聚合/赋值
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 项目结构
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
├── pyproject.toml # pip install 配置(entry: agt, agt-web)
|
|
117
|
+
├── src/ # 源代码
|
|
118
|
+
│ ├── chat.py # CLI
|
|
119
|
+
│ ├── web.py # WebUI + API
|
|
120
|
+
│ ├── agent.py # ReAct 引擎
|
|
121
|
+
│ ├── workflow.py # Coze 工作流执行器
|
|
122
|
+
│ ├── tools.py # Tool/Toolbox
|
|
123
|
+
│ ├── real_tools.py # 内置工具(Python/文件读写/grep/搜索/shell)
|
|
124
|
+
│ ├── llm_client.py # 多模型客户端
|
|
125
|
+
│ ├── session.py # 分层上下文会话
|
|
126
|
+
│ ├── mcp_client.py # MCP 协议
|
|
127
|
+
│ ├── multiagent.py # 多 Agent
|
|
128
|
+
│ └── ...
|
|
129
|
+
├── static/ # WebUI 前端
|
|
130
|
+
│ ├── index.html # 聊天界面
|
|
131
|
+
│ └── workflow_editor.html # 工作流编辑器
|
|
132
|
+
├── .agent/ # 工作区(workflows/rules/skills/wiki)
|
|
133
|
+
└── examples/ # step0-7 教程
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 开发
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
git clone https://github.com/vgp7758/Agt.git
|
|
142
|
+
cd Agt
|
|
143
|
+
pip install -e .
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
运行测试:`python examples/step*.py`
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 卸载
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pip uninstall agt-agent
|
|
154
|
+
rm -rf ~/.agt
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
agt_agent-0.1.0.dist-info/licenses/LICENSE,sha256=-KJMHdqC1ZDgedOTvF8O2Y4zDWD2uAJa_3LA9pFUn_w,1076
|
|
2
|
+
src/__init__.py,sha256=fva579HQAQGoek4upG_431R0TH5ZGWda5cE24gJouR0,247
|
|
3
|
+
src/agent.py,sha256=bIZuQh538dGDkn0alWt5sv5QKefcTz7FxT2sFWubnBM,19797
|
|
4
|
+
src/agent_config.py,sha256=0DOOoB8ZNtz1aQIXrizXKbTL1LbwRsoK1DtrpPIcAyM,4358
|
|
5
|
+
src/chat.py,sha256=-E3Ruklf60V5YpeNQFPq0tLxVhEVYVSaZ_HQKTK6Qac,7107
|
|
6
|
+
src/commands.py,sha256=kX9ZkMK5hsv_Q3NZyCsMyHgxxHoPZznzm3-AXodsefQ,15722
|
|
7
|
+
src/config.py,sha256=QIm0TocbVGgvoQTo3gfiZ2mvt-FTsnM4K9_Iz_vHjPk,3778
|
|
8
|
+
src/llm_client.py,sha256=77KY4dsYHOoVbN6wHzf9LjIpzuHpMYk4S6sEmnDL6VE,9542
|
|
9
|
+
src/lsp_server.py,sha256=kUZz-Elw7755u4C5ATl8_AhRTk5QeZnTmo2vbntwiws,3773
|
|
10
|
+
src/mcp_client.py,sha256=Wj0uZJbhS7xWabwZoG9fVg1-SXYt3wNJnluDI-DsKSw,7428
|
|
11
|
+
src/multiagent.py,sha256=qiP3iXnDriMvcWgFF9yD9KuSkYcRQ3La-zlMiUnMf2U,5103
|
|
12
|
+
src/plan_tools.py,sha256=tETRKs1dthYvjybNRCXkK_DQH9UZiRz3Osb4-hpuDqE,2254
|
|
13
|
+
src/prompts.py,sha256=SRg6XISTKJX0sN9X6NEdmgBiZFmez2VCSM2GnutrYLo,2812
|
|
14
|
+
src/real_tools.py,sha256=GkW09B46fgnJDtiJE4CcaAj6WQt6LioAtLOrTRGdNMI,22355
|
|
15
|
+
src/session.py,sha256=3rOo1gMzCxxsINHfAIzeCgkGLlsaZPXJjJ5RBZ0Qahw,13705
|
|
16
|
+
src/snapshots.py,sha256=mDs75RxmOH1cNYg_ZKSxEyDoHQ-5m1bbYwbz4OS-lq0,3677
|
|
17
|
+
src/tools.py,sha256=-wK7taMICTMwg3T-oal4n_btYD2vcJ7lpfDusNYzElU,5428
|
|
18
|
+
src/web.py,sha256=fQvZkS_REWCcYwGisedQwBxhL1vGV97QYgh5SKQJvuM,19555
|
|
19
|
+
src/wiki.py,sha256=rpKLkynAg1NAEUtS4Do8A7rhmNjaJhiPCnNJ8Be6U_I,7341
|
|
20
|
+
src/workflow.py,sha256=DAh99-_c3MXIOapL2ZSb9Z9DUnZ9SHT6KSt90063NR0,56270
|
|
21
|
+
agt_agent-0.1.0.dist-info/METADATA,sha256=3rOUZtPdQgcYKUlhTUkgp6kpIqCQUmIoBNKzvabImdk,5491
|
|
22
|
+
agt_agent-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
23
|
+
agt_agent-0.1.0.dist-info/entry_points.txt,sha256=ZFQEL-Mqr5l2a6ZCumuHTQKRnuQl-sUi2OdqUrEygoY,61
|
|
24
|
+
agt_agent-0.1.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
|
|
25
|
+
agt_agent-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 马建强 (vgp7758)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
src
|
src/__init__.py
ADDED
src/agent.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""agent.py —— 自主 Agent(事件化输出,CLI 与 Web 各取所需)。
|
|
2
|
+
|
|
3
|
+
输出抽象成结构化事件流:`_emit(event)`。若设了 `on_event`(如 Web 后端)则回调它;
|
|
4
|
+
同时若 `verbose=True` 则 `_print_event` 复刻原控制台格式。故 `chat.py`(不设 on_event、
|
|
5
|
+
verbose=True)输出与之前完全一致;`web.py` 设 on_event 把事件推给浏览器。
|
|
6
|
+
|
|
7
|
+
能力:ReAct 主循环、长程自主、单步并行工具、软 token 预算、Ctrl+C 优雅打断、
|
|
8
|
+
多模型热切换、多 Agent(self.sub_agents)、定时纯自主模式。
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import config
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Callable, Optional, List
|
|
16
|
+
|
|
17
|
+
from llm_client import LLMClient
|
|
18
|
+
from session import Session, Step, ToolCall
|
|
19
|
+
from tools import Toolbox
|
|
20
|
+
|
|
21
|
+
GRAY, RESET = "\033[90m", "\033[0m"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Agent:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
system: str,
|
|
28
|
+
tools: Toolbox,
|
|
29
|
+
*,
|
|
30
|
+
enable_thinking: bool = True,
|
|
31
|
+
max_steps: int = 50,
|
|
32
|
+
token_budget: int = 80000,
|
|
33
|
+
temperature: float = 0.7,
|
|
34
|
+
verbose: bool = True,
|
|
35
|
+
recent_window_turns: int = 4,
|
|
36
|
+
max_steps_per_turn: int = 80,
|
|
37
|
+
model_name: Optional[str] = None,
|
|
38
|
+
on_event: Optional[Callable[[dict], None]] = None,
|
|
39
|
+
snapshot_manager=None,
|
|
40
|
+
):
|
|
41
|
+
self.base_system = system
|
|
42
|
+
self.tools = tools
|
|
43
|
+
self.max_steps = max_steps
|
|
44
|
+
self.token_budget = token_budget
|
|
45
|
+
self.verbose = verbose
|
|
46
|
+
self.on_event = on_event
|
|
47
|
+
self.snapshot_manager = snapshot_manager
|
|
48
|
+
self.model_name = model_name or config.DEFAULT_MODEL
|
|
49
|
+
|
|
50
|
+
self.llm = LLMClient(model_name=self.model_name,
|
|
51
|
+
temperature=temperature, enable_thinking=enable_thinking)
|
|
52
|
+
self.session = Session(system, llm=self.llm, recent_window_turns=recent_window_turns,
|
|
53
|
+
max_steps_per_turn=max_steps_per_turn)
|
|
54
|
+
self.cumulative_tokens = 0
|
|
55
|
+
self.sub_agents: dict = {} # 多 Agent 协作:name -> SubAgent
|
|
56
|
+
self.plan: list = [] # 计划清单(create_plan/update_plan 维护)
|
|
57
|
+
# 纯自主模式状态
|
|
58
|
+
self.autonomous_mode: bool = False
|
|
59
|
+
self.autonomous_end_time: Optional[datetime] = None
|
|
60
|
+
self.autonomous_prompt: str = "当前为纯自主模式,请继续按照要求完成更多工作"
|
|
61
|
+
self.pending_messages: List[str] = [] # 用户插入的消息队列
|
|
62
|
+
self.goal_check_script: str = "" # 目标达成验证脚本(Python,输出 PASS=达成)
|
|
63
|
+
|
|
64
|
+
# ========== 事件输出 ==========
|
|
65
|
+
def _print_only_emit(self, event: dict):
|
|
66
|
+
"""CLI 模式的流式回调:tool_stream/tool_progress 直接打印。"""
|
|
67
|
+
t = event.get("type")
|
|
68
|
+
if t == "tool_stream":
|
|
69
|
+
print(f"{GRAY}{event.get('text', '')}{RESET}", end="", flush=True)
|
|
70
|
+
elif t == "tool_progress":
|
|
71
|
+
print(f"{GRAY}⏳ {event['name']} 已运行 {event['elapsed']}s,{event.get('lines', 0)} 行输出{RESET}")
|
|
72
|
+
|
|
73
|
+
def _emit(self, event: dict):
|
|
74
|
+
"""发一个事件:回调 on_event(Web);verbose 时打印(CLI)。"""
|
|
75
|
+
if self.on_event:
|
|
76
|
+
try:
|
|
77
|
+
self.on_event(event)
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
if self.verbose:
|
|
81
|
+
self._print_event(event)
|
|
82
|
+
|
|
83
|
+
def _print_event(self, e: dict):
|
|
84
|
+
"""复刻原控制台输出格式(保证 CLI 行为不变)。"""
|
|
85
|
+
t = e.get("type")
|
|
86
|
+
if t == "user":
|
|
87
|
+
print(f"\n🧑 用户:{e['text']}")
|
|
88
|
+
elif t == "step":
|
|
89
|
+
print(f"\n{GRAY}━━━ 第 {e['n']} 步 (累计 {e['tokens']} token) ━━━{RESET}")
|
|
90
|
+
elif t == "warn":
|
|
91
|
+
print(f"{GRAY}{e['text']}{RESET}")
|
|
92
|
+
elif t == "budget_hit":
|
|
93
|
+
print(f"\n⚠️ token 预算 ({self.token_budget}) 已用尽,强制收尾。")
|
|
94
|
+
elif t == "thinking":
|
|
95
|
+
print(f"{GRAY}[思考] {e['text']}{RESET}")
|
|
96
|
+
elif t == "parallel":
|
|
97
|
+
print(f"{GRAY}⚡ 并行执行 {e['count']} 个工具调用{RESET}")
|
|
98
|
+
elif t == "tool_call":
|
|
99
|
+
print(f"🔧 调用 {e['name']}({e['arguments']})")
|
|
100
|
+
elif t == "tool_result":
|
|
101
|
+
prefix = f" → [{e['name']}]" if e.get("parallel") else " →"
|
|
102
|
+
print(f"{prefix} {e['result']}")
|
|
103
|
+
elif t == "answer":
|
|
104
|
+
print(f"\n🤖 最终回答:{e['text'].strip()}")
|
|
105
|
+
print(f"{GRAY}[本次累计 token: {e.get('tokens', self.cumulative_tokens)}]{RESET}")
|
|
106
|
+
elif t == "wrap_up":
|
|
107
|
+
print(f"\n⚠️ 达到最大步数 {self.max_steps},强制收尾。")
|
|
108
|
+
elif t == "wrap_answer":
|
|
109
|
+
print(f"\n🤖 收尾回答:{e['text'].strip()}")
|
|
110
|
+
elif t == "interrupted":
|
|
111
|
+
print("\n\n⏹ 已中断(已完成的轮次保留在会话中,可用 /save 保存)。")
|
|
112
|
+
elif t == "autonomous_status":
|
|
113
|
+
if e.get("active"):
|
|
114
|
+
print(f"\n🔁 纯自主模式已开启,持续到 {e['end_time']}")
|
|
115
|
+
else:
|
|
116
|
+
print("\n🔁 纯自主模式已关闭")
|
|
117
|
+
elif t == "autonomous_continue":
|
|
118
|
+
print(f"\n{GRAY}🔁 自主继续:{e['text']}{RESET}")
|
|
119
|
+
elif t == "autonomous_next":
|
|
120
|
+
print(f"{GRAY}🔁 准备自主继续:{e['text']}{RESET}")
|
|
121
|
+
elif t == "tool_stream":
|
|
122
|
+
# CLI 模式:流式输出直接 print,不加换行(全靠子进程自己控制)
|
|
123
|
+
pass # _print_only_emit 已在流式回调中处理
|
|
124
|
+
elif t == "tool_progress":
|
|
125
|
+
print(f"{GRAY}⏳ {e['name']} 运行中 {e['elapsed']}s,{e.get('lines',0)} 行输出{RESET}")
|
|
126
|
+
elif t == "auto_wf":
|
|
127
|
+
print(f"{GRAY}🔍 自动工作流[{e['name']}]: {e['text'][:120]}{RESET}")
|
|
128
|
+
elif t == "message_queued":
|
|
129
|
+
print(f"{GRAY}📨 消息已入队(队列大小:{e['queue_size']}){RESET}")
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def _truncate(s, n=500):
|
|
133
|
+
s = str(s)
|
|
134
|
+
return s if len(s) <= n else s[:n] + f"...(+{len(s) - n}字)"
|
|
135
|
+
|
|
136
|
+
def _run_tools_parallel(self, calls: list) -> list:
|
|
137
|
+
"""并行执行一组工具调用,按原顺序返回结果。"""
|
|
138
|
+
results = [None] * len(calls)
|
|
139
|
+
with ThreadPoolExecutor(max_workers=len(calls)) as ex:
|
|
140
|
+
fut_to_idx = {ex.submit(self.tools.call, tc["name"], tc["arguments"]): i
|
|
141
|
+
for i, tc in enumerate(calls)}
|
|
142
|
+
for fut in as_completed(fut_to_idx):
|
|
143
|
+
i = fut_to_idx[fut]
|
|
144
|
+
try:
|
|
145
|
+
results[i] = fut.result()
|
|
146
|
+
except Exception as e:
|
|
147
|
+
results[i] = f"[执行出错] {type(e).__name__}: {e}"
|
|
148
|
+
return results
|
|
149
|
+
|
|
150
|
+
def switch_model(self, name: str):
|
|
151
|
+
"""热切换模型。Session 共用 self.llm,故摘要调用也跟着切。"""
|
|
152
|
+
self.llm.switch_model(name)
|
|
153
|
+
self.model_name = name
|
|
154
|
+
|
|
155
|
+
def set_autonomous_mode(self, end_time: datetime, prompt: str = None):
|
|
156
|
+
"""设置纯自主模式:到 end_time 之前,任务完成后自动继续。
|
|
157
|
+
prompt: 自动继续时使用的提示词(默认使用预设提示)。"""
|
|
158
|
+
self.autonomous_mode = True
|
|
159
|
+
self.autonomous_end_time = end_time
|
|
160
|
+
if prompt:
|
|
161
|
+
self.autonomous_prompt = prompt
|
|
162
|
+
self._emit({"type": "autonomous_status", "active": True, "end_time": end_time.isoformat(),
|
|
163
|
+
"prompt": self.autonomous_prompt})
|
|
164
|
+
|
|
165
|
+
def exit_autonomous_mode(self):
|
|
166
|
+
"""退出纯自主模式。"""
|
|
167
|
+
self.autonomous_mode = False
|
|
168
|
+
self.autonomous_end_time = None
|
|
169
|
+
self._emit({"type": "autonomous_status", "active": False})
|
|
170
|
+
|
|
171
|
+
def is_autonomous_active(self) -> bool:
|
|
172
|
+
"""检查纯自主模式是否仍有效(未超时且未被手动关闭)。"""
|
|
173
|
+
if not self.autonomous_mode:
|
|
174
|
+
return False
|
|
175
|
+
if self.autonomous_end_time and datetime.now() > self.autonomous_end_time:
|
|
176
|
+
self.exit_autonomous_mode()
|
|
177
|
+
return False
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
def queue_user_message(self, text: str):
|
|
181
|
+
"""在自主模式下,将用户消息加入队列(等当前任务完成后注入)。"""
|
|
182
|
+
if self.autonomous_mode:
|
|
183
|
+
self.pending_messages.append(text)
|
|
184
|
+
self._emit({"type": "message_queued", "text": text, "queue_size": len(self.pending_messages)})
|
|
185
|
+
return True
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
def get_next_message(self) -> Optional[str]:
|
|
189
|
+
"""获取下一条要处理的消息(优先队列中的用户消息,否则用自主提示)。"""
|
|
190
|
+
if self.pending_messages:
|
|
191
|
+
return self.pending_messages.pop(0)
|
|
192
|
+
if self.is_autonomous_active():
|
|
193
|
+
return self.autonomous_prompt
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def run_goal_check(self) -> str:
|
|
197
|
+
"""运行目标验证脚本(独立子进程),返回输出。'PASS' 表示目标达成。"""
|
|
198
|
+
if not self.goal_check_script:
|
|
199
|
+
return ""
|
|
200
|
+
import subprocess, sys, tempfile, os
|
|
201
|
+
with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False, encoding="utf-8") as f:
|
|
202
|
+
f.write(self.goal_check_script)
|
|
203
|
+
tmp = f.name
|
|
204
|
+
try:
|
|
205
|
+
proc = subprocess.run([sys.executable, tmp], capture_output=True,
|
|
206
|
+
text=True, timeout=30, cwd=os.getcwd())
|
|
207
|
+
return (proc.stdout or "").strip()
|
|
208
|
+
except subprocess.TimeoutExpired:
|
|
209
|
+
return "[目标检查超时]"
|
|
210
|
+
finally:
|
|
211
|
+
try:
|
|
212
|
+
os.unlink(tmp)
|
|
213
|
+
except OSError:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
# ========== ReAct 主循环 ==========
|
|
217
|
+
def run(self, user_message: str, images: Optional[list] = None, _autonomous_continue: bool = False) -> str:
|
|
218
|
+
"""
|
|
219
|
+
:param user_message: 用户消息(自主继续时为自动生成的提示)
|
|
220
|
+
:param images: 图片列表
|
|
221
|
+
:param _autonomous_continue: 内部标记,表示这是自主继续的一轮(用于事件区分)
|
|
222
|
+
"""
|
|
223
|
+
# 用循环替代递归:自主继续时走下一轮迭代而不是 self.run() 递归
|
|
224
|
+
msg, auto_flag, imgs = user_message, _autonomous_continue, images
|
|
225
|
+
while True:
|
|
226
|
+
self._stop_flag = False
|
|
227
|
+
self.session.start_turn(msg, imgs)
|
|
228
|
+
# 自动工作流(agentic RAG):auto:true 的工作流用当前消息作为输入预执行
|
|
229
|
+
auto_ctx = ""
|
|
230
|
+
try:
|
|
231
|
+
from real_tools import WORKSPACE as _ws2
|
|
232
|
+
from workflow import get_auto_workflows, execute as _wf_execute
|
|
233
|
+
for aw in get_auto_workflows(_ws2):
|
|
234
|
+
try:
|
|
235
|
+
result = _wf_execute(aw["canvas"], {aw["auto_param"]: msg},
|
|
236
|
+
tools=self.tools, llm=self.llm, workspace=_ws2)
|
|
237
|
+
auto_ctx += f"\n\n[自动工作流「{aw['name']}」的预取结果](参数 {aw['auto_param']}={msg[:80]}):\n{result}"
|
|
238
|
+
self._emit({"type": "auto_wf", "name": aw["name"], "text": str(result)[:300]})
|
|
239
|
+
except Exception as e2:
|
|
240
|
+
auto_ctx += f"\n\n[自动工作流「{aw['name']}」执行失败:{e2}]"
|
|
241
|
+
except Exception:
|
|
242
|
+
pass # 自动工作流不影响主循环
|
|
243
|
+
if auto_ctx and not auto_flag:
|
|
244
|
+
msg = auto_ctx.strip() + "\n\n---\n用户消息:" + msg
|
|
245
|
+
if not auto_flag:
|
|
246
|
+
self._emit({"type": "user", "text": msg, "image_count": len(imgs or [])})
|
|
247
|
+
else:
|
|
248
|
+
self._emit({"type": "autonomous_continue", "text": msg})
|
|
249
|
+
if self.snapshot_manager is not None:
|
|
250
|
+
try:
|
|
251
|
+
sha = self.snapshot_manager.snapshot()
|
|
252
|
+
self.session._current.snapshot_sha = sha
|
|
253
|
+
self._emit({"type": "checkpoint", "sha": sha})
|
|
254
|
+
except Exception as e:
|
|
255
|
+
self._emit({"type": "warn", "text": f"快照失败:{type(e).__name__}: {e}"})
|
|
256
|
+
# 每轮扫描 .agent/workflows/,把工作流刷新成工具(新增/改动的工作流即时生效)
|
|
257
|
+
try:
|
|
258
|
+
from real_tools import WORKSPACE as _ws
|
|
259
|
+
from workflow import refresh_workflow_tools
|
|
260
|
+
refresh_workflow_tools(self.tools, _ws, self)
|
|
261
|
+
except Exception:
|
|
262
|
+
pass # 工作流刷新绝不影响主循环
|
|
263
|
+
tool_schemas = self.tools.schemas()
|
|
264
|
+
continue_loop = False
|
|
265
|
+
try:
|
|
266
|
+
for step_num in range(1, self.max_steps + 1):
|
|
267
|
+
if self._stop_flag:
|
|
268
|
+
self._emit({"type": "interrupted"})
|
|
269
|
+
self.session.abort_current_turn("(被用户停止)")
|
|
270
|
+
return ""
|
|
271
|
+
if self.cumulative_tokens >= self.token_budget:
|
|
272
|
+
self._emit({"type": "budget_hit"})
|
|
273
|
+
return self._wrap_up()
|
|
274
|
+
|
|
275
|
+
self._emit({"type": "step", "n": step_num, "tokens": self.cumulative_tokens,
|
|
276
|
+
"model": self.llm.model_name})
|
|
277
|
+
resp = self.llm.chat(self.session.messages_for_llm(), tools=tool_schemas)
|
|
278
|
+
if resp.usage:
|
|
279
|
+
self.cumulative_tokens += resp.usage.get("total_tokens", 0)
|
|
280
|
+
|
|
281
|
+
if self.cumulative_tokens >= self.token_budget * 0.8:
|
|
282
|
+
self._emit({"type": "warn", "text": "⚠️ 预算已用 80%+,即将触顶收尾"})
|
|
283
|
+
|
|
284
|
+
if resp.reasoning:
|
|
285
|
+
snippet = resp.reasoning[:200].replace("\n", " ")
|
|
286
|
+
if len(resp.reasoning) > 200:
|
|
287
|
+
snippet += "..."
|
|
288
|
+
self._emit({"type": "thinking", "text": snippet})
|
|
289
|
+
|
|
290
|
+
# 不再调用工具 → 最终答案
|
|
291
|
+
if not resp.tool_calls:
|
|
292
|
+
self.session.finish_turn(resp.content, resp.reasoning)
|
|
293
|
+
self._emit({"type": "answer", "text": resp.content,
|
|
294
|
+
"tokens": self.cumulative_tokens})
|
|
295
|
+
# 目标检查:跑验证脚本,PASS 则结束自主模式
|
|
296
|
+
if self.goal_check_script:
|
|
297
|
+
result = self.run_goal_check()
|
|
298
|
+
if result and result.startswith("PASS"):
|
|
299
|
+
self._emit({"type": "system", "text": f"🎯 目标达成:{result}"})
|
|
300
|
+
self.exit_autonomous_mode()
|
|
301
|
+
# 纯自主模式:完成后检查是否继续
|
|
302
|
+
if self.is_autonomous_active():
|
|
303
|
+
next_msg = self.get_next_message()
|
|
304
|
+
if next_msg:
|
|
305
|
+
self._emit({"type": "autonomous_next", "text": next_msg})
|
|
306
|
+
msg, auto_flag, imgs, continue_loop = next_msg, True, None, True
|
|
307
|
+
break
|
|
308
|
+
return resp.content
|
|
309
|
+
|
|
310
|
+
# 执行工具
|
|
311
|
+
calls = resp.tool_calls
|
|
312
|
+
step = Step(reasoning=resp.reasoning)
|
|
313
|
+
# 设置流式回调(run_python/run_shell 通过它推 tool_stream/tool_progress)
|
|
314
|
+
import real_tools as _rt
|
|
315
|
+
_rt._tool_emit = self.on_event if self.on_event else (self._print_only_emit if self.verbose else None)
|
|
316
|
+
if len(calls) == 1:
|
|
317
|
+
tc = calls[0]
|
|
318
|
+
self._emit({"type": "tool_call", "name": tc["name"], "arguments": tc["arguments"]})
|
|
319
|
+
result = self.tools.call(tc["name"], tc["arguments"])
|
|
320
|
+
self._emit({"type": "tool_result", "name": tc["name"],
|
|
321
|
+
"result": self._truncate(result), "parallel": False})
|
|
322
|
+
step.tool_calls.append(ToolCall(id=tc.get("id", ""), name=tc["name"],
|
|
323
|
+
arguments=tc["arguments"], result=result))
|
|
324
|
+
else:
|
|
325
|
+
self._emit({"type": "parallel", "count": len(calls)})
|
|
326
|
+
for tc in calls:
|
|
327
|
+
self._emit({"type": "tool_call", "name": tc["name"], "arguments": tc["arguments"]})
|
|
328
|
+
results = self._run_tools_parallel(calls)
|
|
329
|
+
for tc, result in zip(calls, results):
|
|
330
|
+
self._emit({"type": "tool_result", "name": tc["name"],
|
|
331
|
+
"result": self._truncate(result), "parallel": True})
|
|
332
|
+
step.tool_calls.append(ToolCall(id=tc.get("id", ""), name=tc["name"],
|
|
333
|
+
arguments=tc["arguments"], result=result))
|
|
334
|
+
_rt._tool_emit = None # 清理
|
|
335
|
+
self.session.add_step(step)
|
|
336
|
+
# 自主模式下:工具执行完后检查是否有用户插入消息,附加到结果里让 Agent 立刻看到
|
|
337
|
+
if self.autonomous_mode and self.pending_messages:
|
|
338
|
+
inject = ";".join(self.pending_messages)
|
|
339
|
+
self.pending_messages.clear()
|
|
340
|
+
self._emit({"type": "message_injected", "text": inject})
|
|
341
|
+
# 在下一步发给 LLM 的上下文里,通过 system 消息注入用户提示
|
|
342
|
+
self.session._current._user_hint = inject
|
|
343
|
+
|
|
344
|
+
if continue_loop:
|
|
345
|
+
continue
|
|
346
|
+
self._emit({"type": "wrap_up"})
|
|
347
|
+
if self.is_autonomous_active():
|
|
348
|
+
next_msg = self.get_next_message()
|
|
349
|
+
if next_msg:
|
|
350
|
+
self._emit({"type": "autonomous_next", "text": next_msg})
|
|
351
|
+
msg, auto_flag, imgs, continue_loop = next_msg, True, None, True
|
|
352
|
+
continue
|
|
353
|
+
return self._wrap_up()
|
|
354
|
+
|
|
355
|
+
except KeyboardInterrupt:
|
|
356
|
+
self._emit({"type": "interrupted"})
|
|
357
|
+
self.session.abort_current_turn("(被用户中断)")
|
|
358
|
+
return ""
|
|
359
|
+
|
|
360
|
+
def _wrap_up(self) -> str:
|
|
361
|
+
"""预算/步数到顶时,做一次无工具的总结性收尾。"""
|
|
362
|
+
msgs = self.session.messages_for_llm() + [{
|
|
363
|
+
"role": "system",
|
|
364
|
+
"content": "token 预算或步数已达上限。请基于目前已有的工具结果,直接给出最终总结性回答,不要再调用工具。"
|
|
365
|
+
}]
|
|
366
|
+
try:
|
|
367
|
+
resp = self.llm.chat(msgs)
|
|
368
|
+
answer = resp.content
|
|
369
|
+
if resp.usage:
|
|
370
|
+
self.cumulative_tokens += resp.usage.get("total_tokens", 0)
|
|
371
|
+
except Exception as e:
|
|
372
|
+
answer = f"(收尾调用失败:{e})"
|
|
373
|
+
self.session.finish_turn(answer)
|
|
374
|
+
self._emit({"type": "wrap_answer", "text": answer})
|
|
375
|
+
return answer
|