@volc-emr/emr-cli 0.1.0-beta.0
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.
- package/LICENSE +21 -0
- package/README.md +336 -0
- package/dist/agent/agent.js +19 -0
- package/dist/agent/executor.js +25 -0
- package/dist/agent/llmPlanner.js +131 -0
- package/dist/agent/planner.js +12 -0
- package/dist/agent/types.js +2 -0
- package/dist/index.js +182 -0
- package/dist/runtime/config.js +73 -0
- package/dist/runtime/confirm.js +92 -0
- package/dist/runtime/createClusterMemory.js +56 -0
- package/dist/runtime/llm.js +64 -0
- package/dist/runtime/logger.js +8 -0
- package/dist/runtime/memory.js +4 -0
- package/dist/services/emrApi.js +181 -0
- package/dist/services/volcApi.js +53 -0
- package/dist/tools/base.js +2 -0
- package/dist/tools/emr/createCluster.js +335 -0
- package/dist/tools/emr/deleteCluster.js +15 -0
- package/dist/tools/emr/findClustersToCleanup.js +18 -0
- package/dist/tools/emr/index.js +15 -0
- package/dist/tools/emr/listClusters.js +68 -0
- package/dist/tools/registry.js +11 -0
- package/dist/utils/prompt.js +9 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 volc-emr-agent contributors
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# volc-emr-agent
|
|
2
|
+
|
|
3
|
+
> 一个 **可控、可审计、可 dry-run** 的 Volcengine EMR CLI Agent。
|
|
4
|
+
>
|
|
5
|
+
> Tool-first · Plan → Execute · LLM Planner(可选)· 真实 OpenAPI 调用 · 本地凭证 / 偏好记忆。
|
|
6
|
+
|
|
7
|
+
你用一句自然语言描述任务(例如"列出所有运行中的集群"、"清理一周前已关停的集群"、"帮我建一个 3.7.0 的 Hadoop 集群"),Agent 会:
|
|
8
|
+
|
|
9
|
+
1. 由 LLM 生成一份结构化 `Plan`
|
|
10
|
+
2. 高风险步骤前弹确认(或 `-y` 跳过)
|
|
11
|
+
3. 交由 Executor 依次调用 Tool
|
|
12
|
+
4. Tool 用 Zod 校验 LLM 输入 → 调用真实 EMR OpenAPI → 把结果写回 stdout
|
|
13
|
+
5. 创建集群这类复合 Tool,会**再次**交互式向你追问缺失字段并回填常用默认值
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 已对接的 EMR OpenAPI(Version=2023-08-15)
|
|
18
|
+
|
|
19
|
+
| Tool | OpenAPI Action | 风险级别 | 说明 |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| `listClusters` | [ListClusters](https://www.volcengine.com/docs/6491/1208305) | low | 支持全字段过滤 / 分页 / 中文状态别名归一化 |
|
|
22
|
+
| `findClustersToCleanup` | ListClusters | low | 组合查询:超过 N 天 + 处于 SHUTDOWN 类状态 |
|
|
23
|
+
| `deleteCluster` | [ReleaseCluster](https://www.volcengine.com/docs/6491/1208304) | **high** | 自动识别归档 / 已释放 / 不存在 → 幂等跳过而非中断 |
|
|
24
|
+
| `createCluster` | [CreateCluster](https://www.volcengine.com/docs/6491/1208307) | **high** | 交互式补全 13 类必填字段、PRE/POST 计费分支、节点组向导 |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 通过 npm 全局安装(推荐)
|
|
32
|
+
npm install -g @volc-emr/emr-cli@beta
|
|
33
|
+
|
|
34
|
+
# 验证(CLI 命令名仍是 volc-emr-agent)
|
|
35
|
+
volc-emr-agent --help
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
或者本地开发模式:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone <repo>
|
|
42
|
+
cd console-cli
|
|
43
|
+
npm install
|
|
44
|
+
npm run build
|
|
45
|
+
npm link # 注册 `volc-emr-agent` 到 PATH
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**环境要求**:Node.js ≥ 18
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 快速上手(三步)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# 1) 初始化:填 AK/SK、地域,可选一起填 LLM
|
|
56
|
+
volc-emr-agent init
|
|
57
|
+
|
|
58
|
+
# 2) 跑第一条命令(只打印 Plan,不执行)
|
|
59
|
+
volc-emr-agent run "列出所有运行中的集群" --dry-run
|
|
60
|
+
|
|
61
|
+
# 3) 真正执行
|
|
62
|
+
volc-emr-agent run "列出所有运行中的集群"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 凭证与配置
|
|
68
|
+
|
|
69
|
+
优先级统一为:**CLI 参数 > 环境变量 > 本地配置文件**。
|
|
70
|
+
|
|
71
|
+
### 本地配置文件位置
|
|
72
|
+
|
|
73
|
+
| 文件 | 内容 | 权限 |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `~/.volc-emr/config.json` | Volcengine AK/SK/Region + LLM endpoint/apiKey/model | `0600` |
|
|
76
|
+
| `~/.volc-emr/create-cluster-memory.json` | 创建集群向导的常用偏好(VPC/子网/可用区/机型等) | `0600` |
|
|
77
|
+
|
|
78
|
+
可通过 `VOLC_EMR_CONFIG_DIR` 环境变量整体改位置(测试 / 多环境友好)。
|
|
79
|
+
|
|
80
|
+
### 写配置
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 交互式向导(支持同时配 LLM)
|
|
84
|
+
volc-emr-agent init
|
|
85
|
+
|
|
86
|
+
# 已有配置?加 --force 跳过确认
|
|
87
|
+
volc-emr-agent init --force
|
|
88
|
+
|
|
89
|
+
# 非交互(CI 友好)
|
|
90
|
+
volc-emr-agent init \
|
|
91
|
+
--access-key "$VOLC_ACCESSKEY" \
|
|
92
|
+
--secret-key "$VOLC_SECRETKEY" \
|
|
93
|
+
--region cn-beijing
|
|
94
|
+
|
|
95
|
+
# 单独写凭证
|
|
96
|
+
volc-emr-agent config set-credentials \
|
|
97
|
+
--access-key xxx --secret-key yyy --region cn-beijing
|
|
98
|
+
|
|
99
|
+
# 单独写 LLM
|
|
100
|
+
volc-emr-agent config set-llm \
|
|
101
|
+
--endpoint https://ark.cn-beijing.volces.com/api/v3/chat/completions \
|
|
102
|
+
--api-key ark-xxx \
|
|
103
|
+
--model doubao-seed-character-251128
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 环境变量
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
export VOLC_ACCESSKEY=xxx
|
|
110
|
+
export VOLC_SECRETKEY=yyy
|
|
111
|
+
export VOLC_REGION=cn-beijing
|
|
112
|
+
|
|
113
|
+
# LLM(可选;未配置时 Planner 会直接报错,不做 rule-based 兜底)
|
|
114
|
+
export VOLC_LLM_ENDPOINT=https://ark.cn-beijing.volces.com/api/v3/chat/completions
|
|
115
|
+
export VOLC_LLM_API_KEY=ark-xxx
|
|
116
|
+
export VOLC_LLM_MODEL=doubao-seed-character-251128
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 查看 / 重定向
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# 查看已保存的本地配置(secrets 自动脱敏)
|
|
123
|
+
volc-emr-agent config show
|
|
124
|
+
|
|
125
|
+
# 改用自定义目录
|
|
126
|
+
VOLC_EMR_CONFIG_DIR=/tmp/volc-test volc-emr-agent init
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 运行示例
|
|
132
|
+
|
|
133
|
+
### 列出集群
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
volc-emr-agent run "列出所有运行中的集群"
|
|
137
|
+
volc-emr-agent run "列出 cn-beijing 最近创建的 10 个 Hadoop 集群"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 清理老集群
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# 看 Plan、不执行
|
|
144
|
+
volc-emr-agent run "清理一周前已关停的集群" --dry-run
|
|
145
|
+
|
|
146
|
+
# 批量执行(自动确认所有 high-risk 步骤)
|
|
147
|
+
volc-emr-agent run "清理一周前已关停的集群" -y
|
|
148
|
+
|
|
149
|
+
# 自定义天数
|
|
150
|
+
volc-emr-agent run "清理 14 天前已关停的集群"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
> 已被 EMR 归档(`marked archived`)的集群会被自动识别并**跳过**,不会让整个清理流程中断。
|
|
154
|
+
|
|
155
|
+
### 创建集群(交互式向导)
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
volc-emr-agent run "帮我创建一个 3.7.0 的 Hadoop 集群"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
LLM 会先从你的描述里预填能猜到的字段(`ClusterType=Hadoop, ReleaseVersion=3.7.0`),Tool 接着启动向导:
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
=== 创建 EMR 集群:补全配置 ===
|
|
165
|
+
(已加载上次记忆,路径: /Users/you/.volc-emr/create-cluster-memory.json)
|
|
166
|
+
|
|
167
|
+
集群类型 (可选: Hadoop / Presto / ...) [Hadoop]: ⏎
|
|
168
|
+
集群版本 (ReleaseVersion) [3.7.0]: ⏎
|
|
169
|
+
集群名称 [OpenApiHadoop3.7.0-a7x2m]: ⏎
|
|
170
|
+
付费类型 (可选: POST / PRE) [POST]: ⏎
|
|
171
|
+
VpcId (如 vpc-xxx) [vpc-abc123]: ⏎ ← 来自上次记忆
|
|
172
|
+
SecurityGroupId (如 sg-xxx) [sg-def456]: ⏎ ← 来自上次记忆
|
|
173
|
+
可用区 ZoneId [cn-beijing-b]: ⏎
|
|
174
|
+
|
|
175
|
+
-- 节点组配置 (共 2 组) --
|
|
176
|
+
[节点组 1]
|
|
177
|
+
节点组类型 (可选: MASTER / CORE / TASK / GATEWAY) [MASTER]: ⏎
|
|
178
|
+
节点数 [3]: ⏎
|
|
179
|
+
SubnetIds (逗号分隔) [subnet-xyz]: ⏎
|
|
180
|
+
EcsInstanceTypes [ecs.g3i.2xlarge]: ⏎
|
|
181
|
+
...
|
|
182
|
+
|
|
183
|
+
=== 即将创建集群,配置如下 ===
|
|
184
|
+
{ ...完整 JSON... }
|
|
185
|
+
确认使用以上配置创建集群? (y/n): y
|
|
186
|
+
|
|
187
|
+
(已更新常用默认值记忆: /Users/you/.volc-emr/create-cluster-memory.json)
|
|
188
|
+
✓ 集群创建成功: ClusterId=emr-xxx, OperationId=op-xxx
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**特性**:
|
|
192
|
+
|
|
193
|
+
- **记忆**:用户确认后立刻写 `create-cluster-memory.json`。即使 OpenAPI 随后返回错误,下次依然能复用你填过的网络/节点偏好,不用再填一遍
|
|
194
|
+
- **中文状态 / 大小写自动归一化**:LLM 写成 "已关停" / "Running" 都能被翻译成官方枚举
|
|
195
|
+
- **MASTER + CORE 自动补齐**:Hadoop 类集群强制至少两个节点组
|
|
196
|
+
- **ChargeType=PRE 自动展开包年包月子向导**(周期 / 自动续费)
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 命令总览
|
|
201
|
+
|
|
202
|
+
| 命令 | 作用 |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `volc-emr-agent init [--force]` | 交互式写入 AK/SK、可选一起配 LLM |
|
|
205
|
+
| `volc-emr-agent config set-credentials --access-key ... --secret-key ...` | 单独更新凭证 |
|
|
206
|
+
| `volc-emr-agent config set-llm --endpoint ... [--api-key ...] [--model ...]` | 单独更新 LLM |
|
|
207
|
+
| `volc-emr-agent config show` | 查看本地配置(脱敏) |
|
|
208
|
+
| `volc-emr-agent run <task> [--dry-run] [-y] [--region ...]` | 执行一条自然语言任务 |
|
|
209
|
+
|
|
210
|
+
运行选项:
|
|
211
|
+
|
|
212
|
+
- `--dry-run`:只打印 Plan,不调用任何 OpenAPI
|
|
213
|
+
- `-y, --yes`:对 `riskLevel: "high"` 的 Tool 自动确认
|
|
214
|
+
- `--region <region>`:临时覆盖当前会话的区域(不写配置)
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 目录结构
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
src/
|
|
222
|
+
├── index.ts # CLI 入口(commander)
|
|
223
|
+
├── agent/
|
|
224
|
+
│ ├── agent.ts # 对外 Agent.run(task, options)
|
|
225
|
+
│ ├── planner.ts # LLM-only Planner,无 LLM 时直接报错
|
|
226
|
+
│ ├── llmPlanner.ts # 把 Zod 输入翻成 JSON Schema 喂给 LLM
|
|
227
|
+
│ ├── executor.ts # 依次执行 Plan,触发高风险确认
|
|
228
|
+
│ └── types.ts
|
|
229
|
+
├── services/
|
|
230
|
+
│ ├── volcApi.ts # @volcengine/openapi 签名 + 错误解析
|
|
231
|
+
│ └── emrApi.ts # listClusters / releaseCluster / createCluster ...
|
|
232
|
+
├── tools/
|
|
233
|
+
│ ├── base.ts # Tool 定义 (name/description/input/riskLevel/execute)
|
|
234
|
+
│ ├── registry.ts # 聚合所有业务 Tool + API
|
|
235
|
+
│ └── emr/
|
|
236
|
+
│ ├── index.ts
|
|
237
|
+
│ ├── listClusters.ts
|
|
238
|
+
│ ├── findClustersToCleanup.ts
|
|
239
|
+
│ ├── deleteCluster.ts # → ReleaseCluster
|
|
240
|
+
│ └── createCluster.ts # 交互式向导 + 记忆
|
|
241
|
+
├── runtime/
|
|
242
|
+
│ ├── config.ts # AK/SK + LLM 持久化
|
|
243
|
+
│ ├── createClusterMemory.ts # 创建集群专用的偏好记忆(独立文件)
|
|
244
|
+
│ ├── confirm.ts # TTY / pipe 双模安全的 prompt
|
|
245
|
+
│ ├── llm.ts # OpenAI-compatible Chat Completions 客户端
|
|
246
|
+
│ ├── memory.ts # 进程内简易 KV
|
|
247
|
+
│ └── logger.ts
|
|
248
|
+
└── utils/prompt.ts
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 关键设计
|
|
254
|
+
|
|
255
|
+
### 1. Tool-first
|
|
256
|
+
|
|
257
|
+
所有业务能力都是 Tool(`name / description / input (Zod) / riskLevel / execute`)。LLM 看到的只有 Tool 目录(由 `zod-to-json-schema` 自动生成),**不能直接执行副作用**。
|
|
258
|
+
|
|
259
|
+
### 2. Plan → Execute 分离
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
用户自然语言
|
|
263
|
+
↓
|
|
264
|
+
Planner(LLM, Zod 校验)→ PlanStep[]
|
|
265
|
+
↓
|
|
266
|
+
Executor(读 registry, 拦截 high-risk, 调用 Tool)
|
|
267
|
+
↓
|
|
268
|
+
真实 Volcengine OpenAPI
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 3. 枚举三层防御(以 `ClusterStates` 为例)
|
|
272
|
+
|
|
273
|
+
- **Schema 层**:Tool input 用 `z.nativeEnum(EmrClusterState)`,LLM 收到带 `enum` 的 JSON Schema
|
|
274
|
+
- **Prompt 层**:Tool description 里显式印合法枚举清单
|
|
275
|
+
- **执行层**:Tool `execute()` 内部再做一次大小写 + 中文别名兜底
|
|
276
|
+
|
|
277
|
+
### 4. OpenAPI 错误可读化
|
|
278
|
+
|
|
279
|
+
[volcApi.ts](src/services/volcApi.ts) 对火山返回做结构化解析,错误消息会带 `Code / RequestId / body` 等排障必需的上下文,而不是一句干巴巴的 `Internal error`。
|
|
280
|
+
|
|
281
|
+
### 5. 记忆分文件,互不污染
|
|
282
|
+
|
|
283
|
+
- `config.json`:**身份**(AK/SK、LLM token)。敏感,少变。
|
|
284
|
+
- `create-cluster-memory.json`:**偏好**(VPC/子网/ZoneId/机型)。常变,可分享模板。
|
|
285
|
+
|
|
286
|
+
写入时机在 **确认后立刻写**(而不是 API 成功后),哪怕 OpenAPI 挂了,你下次也不用重填网络信息。
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## LLM 配置提示
|
|
291
|
+
|
|
292
|
+
Planner 要求必须配置 LLM(OpenAI-compatible `/chat/completions`)。已验证可用的:
|
|
293
|
+
|
|
294
|
+
- **火山方舟 Ark**:
|
|
295
|
+
```
|
|
296
|
+
endpoint: https://ark.cn-beijing.volces.com/api/v3/chat/completions
|
|
297
|
+
model: doubao-seed-character-251128(或其他 doubao 模型)
|
|
298
|
+
```
|
|
299
|
+
- **OpenAI 官方**:
|
|
300
|
+
```
|
|
301
|
+
endpoint: https://api.openai.com/v1/chat/completions
|
|
302
|
+
model: gpt-4o-mini
|
|
303
|
+
```
|
|
304
|
+
- **任何兼容 OpenAI 协议的网关**(Together / Groq / Fireworks / 本地 vLLM 等)
|
|
305
|
+
|
|
306
|
+
如果模型不支持 `response_format=json_object`,Planner 会退化到 "字符串 JSON + markdown fence 清洗" 的 robust 模式,无需额外改配置。
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## 故障排查
|
|
311
|
+
|
|
312
|
+
| 现象 | 原因 / 处理 |
|
|
313
|
+
|---|---|
|
|
314
|
+
| `[ListClusters] The request is missing Version parameter` | `@volcengine/openapi` 需要 `Version` 而非 `version` — 现已修好 |
|
|
315
|
+
| `[ListClusters] Invalid request {0}` | LLM 传了非法字段或类型,Tool 层错误消息会附上 body 预览 |
|
|
316
|
+
| `[CreateCluster] Internal error` | 5xx;抓取 `RequestId` 后去[火山工单](https://console.volcengine.com/workorder/create/),或用相同参数在 [API Explorer](https://api.volcengine.com/api-explorer/?action=CreateCluster&serviceCode=emr&version=2023-08-15) 复现 |
|
|
317
|
+
| `LLM is not configured` | Planner 没有 rule-based 兜底,请先 `volc-emr-agent config set-llm` |
|
|
318
|
+
| 清理时个别集群报 `marked archived` | 会被 `deleteCluster` 内部捕获,返回 `{ Skipped: true, Reason: "ARCHIVED" }` 并作为该步 `Done` 结果打印,不影响其它集群清理 |
|
|
319
|
+
|
|
320
|
+
想看更详细的请求/响应?可以临时编辑 [volcApi.ts](src/services/volcApi.ts) 在 `fetchApi(body)` 前后加 `console.error` 打印 body 和 response。
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## 后续规划
|
|
325
|
+
|
|
326
|
+
- [ ] 支持 `CreateCluster` 的 `--config file.json` 无交互模式(CI 友好)
|
|
327
|
+
- [ ] 更多 EMR Action:`GetCluster` / `ScaleOutNodeGroup` / `UpdateClusterAttribute`
|
|
328
|
+
- [ ] 多 profile 记忆(`--profile prod/test`)
|
|
329
|
+
- [ ] Tool catalog 热插拔、跨业务目录(非 EMR)
|
|
330
|
+
- [ ] Executor 级别的 retry / rollback / 流式输出
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## License
|
|
335
|
+
|
|
336
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Agent = void 0;
|
|
4
|
+
const planner_1 = require("./planner");
|
|
5
|
+
const executor_1 = require("./executor");
|
|
6
|
+
class Agent {
|
|
7
|
+
constructor(ctx) {
|
|
8
|
+
this.ctx = ctx;
|
|
9
|
+
}
|
|
10
|
+
async run(task, options = {}) {
|
|
11
|
+
const plan = await (0, planner_1.planTask)(task);
|
|
12
|
+
console.log("\n📋 Plan:");
|
|
13
|
+
console.log(plan);
|
|
14
|
+
if (options.dryRun)
|
|
15
|
+
return;
|
|
16
|
+
await (0, executor_1.executePlan)(plan, this.ctx, { autoApprove: options.autoApprove });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.Agent = Agent;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executePlan = executePlan;
|
|
4
|
+
const registry_1 = require("../tools/registry");
|
|
5
|
+
const confirm_1 = require("../runtime/confirm");
|
|
6
|
+
const logger_1 = require("../runtime/logger");
|
|
7
|
+
async function executePlan(plan, ctx, options = {}) {
|
|
8
|
+
for (const [i, step] of plan.entries()) {
|
|
9
|
+
const tool = registry_1.toolList.find((t) => t.name === step.tool);
|
|
10
|
+
if (!tool)
|
|
11
|
+
throw new Error(`Tool not found: ${step.tool}`);
|
|
12
|
+
if (tool.riskLevel === "high") {
|
|
13
|
+
const ok = options.autoApprove
|
|
14
|
+
? true
|
|
15
|
+
: await (0, confirm_1.confirm)(`⚠️ 执行 ${tool.name} (input=${JSON.stringify(step.input)})?`);
|
|
16
|
+
if (!ok) {
|
|
17
|
+
(0, logger_1.logStep)(i, `Skipped ${tool.name}`);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
(0, logger_1.logStep)(i, `Running ${tool.name}`, step.input);
|
|
22
|
+
const result = await tool.execute(step.input, ctx);
|
|
23
|
+
(0, logger_1.logStep)(i, `Done ${tool.name}`, result);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.llmPlanTask = llmPlanTask;
|
|
4
|
+
const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
5
|
+
const registry_1 = require("../tools/registry");
|
|
6
|
+
const emr_1 = require("../tools/emr");
|
|
7
|
+
const llm_1 = require("../runtime/llm");
|
|
8
|
+
function buildToolCatalog() {
|
|
9
|
+
return registry_1.toolList.map((t) => ({
|
|
10
|
+
name: t.name,
|
|
11
|
+
description: t.description,
|
|
12
|
+
riskLevel: t.riskLevel || "low",
|
|
13
|
+
inputSchema: (0, zod_to_json_schema_1.zodToJsonSchema)(t.input, t.name)
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
function buildSystemPrompt() {
|
|
17
|
+
const catalog = buildToolCatalog();
|
|
18
|
+
return [
|
|
19
|
+
"You are a CLI Agent Planner for Volcengine EMR.",
|
|
20
|
+
"You MUST NOT execute anything directly.",
|
|
21
|
+
"Your only job is to output an ordered plan of tool calls.",
|
|
22
|
+
"",
|
|
23
|
+
"Available tools (JSON catalog):",
|
|
24
|
+
JSON.stringify(catalog, null, 2),
|
|
25
|
+
"",
|
|
26
|
+
"Response format:",
|
|
27
|
+
'You MUST output ONLY a JSON object with shape {"steps":[{"tool":"<name>","input":{...}}]}.',
|
|
28
|
+
"Do NOT include any prose, markdown fences, or explanations.",
|
|
29
|
+
"The very first character of your reply MUST be `{` and the last MUST be `}`.",
|
|
30
|
+
"Rules:",
|
|
31
|
+
"- Only use tools listed above.",
|
|
32
|
+
"- `input` MUST conform to each tool's inputSchema.",
|
|
33
|
+
"- For destructive actions (riskLevel=high), only plan them if the user task clearly asks for it.",
|
|
34
|
+
"- If the task is ambiguous, return at most one safe, read-only step (e.g. listClusters)."
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
function buildUserPrompt(task, context) {
|
|
38
|
+
const ctx = context ? `\n\nContext:\n${JSON.stringify(context, null, 2)}` : "";
|
|
39
|
+
return `Task: ${task}${ctx}`;
|
|
40
|
+
}
|
|
41
|
+
function extractJsonObject(raw) {
|
|
42
|
+
const stripped = raw
|
|
43
|
+
.trim()
|
|
44
|
+
.replace(/^```(?:json)?\s*/i, "")
|
|
45
|
+
.replace(/```\s*$/i, "")
|
|
46
|
+
.trim();
|
|
47
|
+
if (stripped.startsWith("{"))
|
|
48
|
+
return stripped;
|
|
49
|
+
const start = stripped.indexOf("{");
|
|
50
|
+
if (start === -1)
|
|
51
|
+
return stripped;
|
|
52
|
+
let depth = 0;
|
|
53
|
+
let inString = false;
|
|
54
|
+
let escape = false;
|
|
55
|
+
for (let i = start; i < stripped.length; i++) {
|
|
56
|
+
const ch = stripped[i];
|
|
57
|
+
if (inString) {
|
|
58
|
+
if (escape) {
|
|
59
|
+
escape = false;
|
|
60
|
+
}
|
|
61
|
+
else if (ch === "\\") {
|
|
62
|
+
escape = true;
|
|
63
|
+
}
|
|
64
|
+
else if (ch === '"') {
|
|
65
|
+
inString = false;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (ch === '"') {
|
|
70
|
+
inString = true;
|
|
71
|
+
}
|
|
72
|
+
else if (ch === "{") {
|
|
73
|
+
depth++;
|
|
74
|
+
}
|
|
75
|
+
else if (ch === "}") {
|
|
76
|
+
depth--;
|
|
77
|
+
if (depth === 0)
|
|
78
|
+
return stripped.slice(start, i + 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return stripped.slice(start);
|
|
82
|
+
}
|
|
83
|
+
function tryParsePlan(raw) {
|
|
84
|
+
const jsonText = extractJsonObject(raw);
|
|
85
|
+
const parsed = JSON.parse(jsonText);
|
|
86
|
+
const steps = Array.isArray(parsed) ? parsed : parsed?.steps;
|
|
87
|
+
if (!Array.isArray(steps)) {
|
|
88
|
+
throw new Error("LLM plan has no `steps` array");
|
|
89
|
+
}
|
|
90
|
+
return steps.map((s, i) => {
|
|
91
|
+
if (!s || typeof s.tool !== "string") {
|
|
92
|
+
throw new Error(`Step ${i} is missing "tool"`);
|
|
93
|
+
}
|
|
94
|
+
return { tool: s.tool, input: s.input ?? {} };
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function validatePlanAgainstTools(steps) {
|
|
98
|
+
return steps.map((step) => {
|
|
99
|
+
const tool = registry_1.toolList.find((t) => t.name === step.tool);
|
|
100
|
+
if (!tool)
|
|
101
|
+
throw new Error(`LLM chose unknown tool: ${step.tool}`);
|
|
102
|
+
const result = tool.input.safeParse(step.input ?? {});
|
|
103
|
+
if (!result.success) {
|
|
104
|
+
throw new Error(`LLM produced invalid input for ${step.tool}: ${result.error.message}`);
|
|
105
|
+
}
|
|
106
|
+
return { tool: step.tool, input: result.data };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function llmPlanTask(task, llm) {
|
|
110
|
+
const messages = [
|
|
111
|
+
{ role: "system", content: buildSystemPrompt() },
|
|
112
|
+
{ role: "user", content: buildUserPrompt(task) }
|
|
113
|
+
];
|
|
114
|
+
const content = await (0, llm_1.chatCompletion)(llm, { messages });
|
|
115
|
+
const steps = tryParsePlan(content);
|
|
116
|
+
const validated = validatePlanAgainstTools(steps);
|
|
117
|
+
const expanded = [];
|
|
118
|
+
for (const step of validated) {
|
|
119
|
+
expanded.push(step);
|
|
120
|
+
if (step.tool === "findClustersToCleanup") {
|
|
121
|
+
const found = await emr_1.emrApi.findClustersToCleanup(step.input || {});
|
|
122
|
+
for (const c of found) {
|
|
123
|
+
expanded.push({
|
|
124
|
+
tool: "deleteCluster",
|
|
125
|
+
input: { ClusterId: c.ClusterId }
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return expanded;
|
|
131
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.planTask = planTask;
|
|
4
|
+
const config_1 = require("../runtime/config");
|
|
5
|
+
const llmPlanner_1 = require("./llmPlanner");
|
|
6
|
+
async function planTask(task, _options = {}) {
|
|
7
|
+
const llm = (0, config_1.resolveLlmConfig)();
|
|
8
|
+
if (!llm) {
|
|
9
|
+
throw new Error("LLM is not configured. Run `volc-emr-agent config set-llm` or set VOLC_LLM_ENDPOINT first.");
|
|
10
|
+
}
|
|
11
|
+
return (0, llmPlanner_1.llmPlanTask)(task, llm);
|
|
12
|
+
}
|