@roll-agent/octopus-agent 0.1.2 → 0.2.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/README.md +26 -125
- package/SKILL.md +27 -102
- package/package.json +12 -18
- package/references/env.yaml +1 -22
- package/scripts/start-octopus-agent.js +1 -125
- package/src/config.js +13 -0
- package/src/context.js +5 -0
- package/src/mcp-client.js +80 -0
- package/src/orchestrator.js +57 -0
- package/src/server-core.js +67 -0
- package/src/server.js +10 -0
- package/src/stdio.js +29 -0
- package/octopus_skill/__init__.py +0 -11
- package/pyproject.toml +0 -19
- package/scripts/refresh_dictionary.py +0 -67
- package/scripts/verify_binding.py +0 -47
- package/src/octopus_skill/__init__.py +0 -5
- package/src/octopus_skill/__main__.py +0 -4
- package/src/octopus_skill/_version.py +0 -3
- package/src/octopus_skill/agent.py +0 -4971
- package/src/octopus_skill/audit_logger.py +0 -29
- package/src/octopus_skill/config.py +0 -129
- package/src/octopus_skill/context.py +0 -17
- package/src/octopus_skill/device_info.py +0 -7
- package/src/octopus_skill/exporter.py +0 -66
- package/src/octopus_skill/llm_result_renderer.py +0 -101
- package/src/octopus_skill/llm_sql_generator.py +0 -197
- package/src/octopus_skill/main.py +0 -26
- package/src/octopus_skill/mcp_client.py +0 -114
- package/src/octopus_skill/octopus_run.py +0 -752
- package/src/octopus_skill/prompt_builder.py +0 -519
- package/src/octopus_skill/question_analyzer.py +0 -399
- package/src/octopus_skill/result_renderer.py +0 -367
- package/src/octopus_skill/schema_cache.py +0 -49
- package/src/octopus_skill/schema_context_retriever.py +0 -1053
- package/src/octopus_skill/sql_generator.py +0 -2788
package/README.md
CHANGED
|
@@ -1,144 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Octopus Agent
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## 能力
|
|
6
|
-
|
|
7
|
-
- Schema Cache(表结构缓存):本地保存 Sponge 返回的 schema。
|
|
8
|
-
- MCP Client(MCP 客户端):按 `get_schema`、`validate_sql`、`execute_sql` 三步调用。
|
|
9
|
-
- MCP Sampling(采样):由 roll-core 代调 LLM 生成和修正 SQL。
|
|
10
|
-
- SQL Guard(SQL 保护规则):只允许单条 `SELECT`,必须带 `LIMIT`。
|
|
11
|
-
- Audit Log(审计日志):记录 trace、session、SQL hash、执行摘要。
|
|
3
|
+
Octopus Agent 是 Node.js 实现的轻量 NL2SQL(自然语言转 SQL)调用客户端。Agent 每次只调用一次 `query_sponge`(查询 Sponge);所有业务判断、SQL 生成、校验、修复、执行和渲染均由 Sponge MCP Server 完成。
|
|
12
4
|
|
|
13
5
|
## 本地运行
|
|
14
6
|
|
|
15
|
-
|
|
16
|
-
python -m octopus_skill "查询我能看到的品牌列表"
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
运行参数由 Agent 内置;用户本地只需要配置 Sponge MCP Server 地址和 Token。
|
|
20
|
-
|
|
21
|
-
## roll-core 接入
|
|
22
|
-
|
|
23
|
-
本仓库支持纯 Python stdio MCP 子 Agent 接入 roll-core。
|
|
24
|
-
|
|
25
|
-
### npm 包安装
|
|
26
|
-
|
|
27
|
-
发布到 npm 后,可通过 `roll agent install` 安装:
|
|
7
|
+
要求 Node.js 20 或更高版本。
|
|
28
8
|
|
|
29
9
|
```bash
|
|
30
|
-
|
|
31
|
-
|
|
10
|
+
npm install
|
|
11
|
+
npm test
|
|
12
|
+
npm start
|
|
32
13
|
```
|
|
33
14
|
|
|
34
|
-
|
|
35
|
-
安装机器上需要有 Python 3.11+。Agent 启动时会自动尝试 `python3`、`python`、`py -3.11`、`py -3`。
|
|
36
|
-
如果 Python 命令不在这些名称里,可以配置 `OCTOPUS_PYTHON_COMMAND`(Python 命令),例如:
|
|
37
|
-
|
|
38
|
-
```powershell
|
|
39
|
-
roll config set agents.env.octopus-agent.OCTOPUS_PYTHON_COMMAND "py -3.11"
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Windows 用户只要安装 Python 3.11+,通常不需要额外创建 `python3` 别名。
|
|
43
|
-
|
|
44
|
-
### 本地源码注册
|
|
15
|
+
## Roll Agent 接入
|
|
45
16
|
|
|
46
17
|
```bash
|
|
47
18
|
roll agent add .
|
|
48
|
-
roll run octopus-agent
|
|
49
|
-
|
|
50
|
-
--input-json '{"traceId":"trace_schema_10001"}'
|
|
51
|
-
roll run octopus-agent query_sponge --json \
|
|
52
|
-
--question "查询我能看到的品牌列表"
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
`refresh_schema` 必填 `traceId`。Agent 会自动读取本地缓存里的 `schemaVersion` 并传给 Sponge。
|
|
56
|
-
`query_sponge` 不接收用户或设备标识;Sponge MCP Server 会根据 `Authorization` token 解析用户权限。
|
|
57
|
-
Agent 只生成业务 SQL,不生成 `:scope.project_ids`、`:scope.brand_ids` 等权限占位符。
|
|
58
|
-
权限范围由 Sponge MCP Server 在 `execute_sql` 阶段统一补全,并写入 `mcp_sql_records.scope_bindings`。
|
|
59
|
-
|
|
60
|
-
指定输出格式:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
roll run octopus-agent query_sponge --json \
|
|
64
|
-
--input-json '{"question":"查询我能看到的品牌列表","resultFormat":"markdown"}'
|
|
19
|
+
roll run octopus-agent query --json \
|
|
20
|
+
--input-json '{"question":"查询本月各门店销售额"}'
|
|
65
21
|
```
|
|
66
22
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
调试 SQL:
|
|
23
|
+
`query`(自然语言查询)入参:
|
|
70
24
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
25
|
+
| 字段 | 必填 | 中文说明 |
|
|
26
|
+
|-|-|-|
|
|
27
|
+
| `question` | 是 | 用户原始问题,不得改写 |
|
|
28
|
+
| `resultFormat` | 否 | 结果格式,支持 text/json/yaml/md/markdown/html,默认 markdown(Markdown 格式) |
|
|
75
29
|
|
|
76
|
-
|
|
77
|
-
所有模式都会返回 `needsClarification`。当 `needsClarification=true` 时,上层 Agent 必须原样展示 `answer` 并等待用户确认,不得自行选择候选项或再次调用 `query_sponge`。
|
|
78
|
-
默认 `requirePlanConfirmation=true`,`query_sponge` 会先返回查询计划:匹配到的表、字段、筛选条件和执行步骤。用户回复“确认”后,再继续生成、校验和执行 SQL;用户提出修改时,会按修改后的口径重新整理查询计划并再次确认。调试或内部直通场景可以显式传 `requirePlanConfirmation=false`。
|
|
30
|
+
## 环境变量
|
|
79
31
|
|
|
80
|
-
|
|
32
|
+
| 环境变量 | 必填 | 中文说明 | 默认值 |
|
|
33
|
+
|-|-|-|-|
|
|
34
|
+
| `SPONGE_MCP_BASE_URL` | 是 | Sponge MCP Server 地址 | `https://sponge-mcp.duliday.com` |
|
|
35
|
+
| `SPONGE_MCP_ACCESS_TOKEN` | 是 | 身份透传 Token(令牌) | 无 |
|
|
81
36
|
|
|
82
|
-
|
|
83
|
-
- `structuredContent.answer`:同样保存纯净业务答案,作为结构化兜底。
|
|
84
|
-
- `structuredContent.responseMode=verbatim` 和 `structuredContent.finalAnswerOnly=true`:提示上层 Agent 应原样透传答案。约束只放在结构化字段,`content` 数组不再附带元指令文本,避免泄露给用户。
|
|
37
|
+
Agent 内置单次请求超时 58 秒、query(查询)总超时 59 秒、网络重试 2 次,无需额外配置。每个新问题生成新的 `requestId`(请求编号),仅网络重试复用该编号。总超时低于当前 Roll 的 60 秒固定限制。
|
|
85
38
|
|
|
86
|
-
|
|
39
|
+
## 安全边界
|
|
87
40
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
-
|
|
93
|
-
- 查询字段:确认后必须尽量出现在 SQL 的 `SELECT` 中。
|
|
94
|
-
- 筛选条件:确认后必须尽量出现在 SQL 的 `WHERE` 中;名称类使用 `LIKE`,状态类使用 `schema.enums` 的枚举值。
|
|
95
|
-
- 执行步骤:从上层调用、意图判断、口径确认、SQL 校验执行到结果渲染。
|
|
96
|
-
|
|
97
|
-
最终答案顶部会先展示“查询字段”和“筛选条件”,再展示查询结果。
|
|
98
|
-
|
|
99
|
-
如果执行成功但结果异常,例如 `rowCount=0`、关键字段大量为空、结果明显不符合预期,Agent 不应自动改口径再跑第二次完整链路。应先说明异常点、可能原因、建议口径和影响范围,等待用户确认后,再用新的 `traceId`、原 `sessionId` 发起新链路。
|
|
100
|
-
|
|
101
|
-
如果只是 SQL 校验失败,可以修正 SQL 后重新校验;只要涉及业务口径变化,必须先确认。
|
|
102
|
-
|
|
103
|
-
`package.json` 中声明了 roll-core 启动方式:
|
|
104
|
-
|
|
105
|
-
```json
|
|
106
|
-
{
|
|
107
|
-
"rollAgent": {
|
|
108
|
-
"runtime": {"ownership": "on-demand", "transport": "stdio"},
|
|
109
|
-
"start": {
|
|
110
|
-
"command": "node",
|
|
111
|
-
"args": ["scripts/start-octopus-agent.js"]
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
环境变量可放到 `roll.config.yaml`:
|
|
118
|
-
|
|
119
|
-
```yaml
|
|
120
|
-
agents:
|
|
121
|
-
env:
|
|
122
|
-
octopus-agent:
|
|
123
|
-
SPONGE_MCP_BASE_URL: "https://sponge-mcp.duliday.com"
|
|
124
|
-
SPONGE_MCP_ACCESS_TOKEN: "xxxx"
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
`SPONGE_MCP_BASE_URL` 只填域名根地址,不要带 `/mcp/admin/health`;Agent 会调用 `/mcp/tools/...`。
|
|
128
|
-
|
|
129
|
-
LLM 配置读取 roll-core 现有 `llm` 配置;Agent 不读取模型 API Key。
|
|
130
|
-
`query_sponge` 会通过 MCP Sampling 请求 roll-core 代调模型。
|
|
131
|
-
|
|
132
|
-
## 本地持久化
|
|
133
|
-
|
|
134
|
-
- `data/sponge-schema-cache.json`:schema 缓存。
|
|
135
|
-
- `data/sponge-clarification-cache.json`:短期确认缓存,保存品牌/项目候选和查询计划确认上下文,默认 10 分钟过期。
|
|
136
|
-
- `logs/octopus-sponge-audit.log`:审计日志。
|
|
137
|
-
|
|
138
|
-
不新建数据库,不保存真实项目 ID、品牌 ID、数据库账号密码。
|
|
139
|
-
|
|
140
|
-
## 自测
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
PYTHONPATH=src python -m unittest discover -s tests
|
|
144
|
-
```
|
|
41
|
+
- Agent 只调用一次 `query_sponge`(查询 Sponge),不调用内部原子能力。
|
|
42
|
+
- Agent 不接收或传入任意 SQL。
|
|
43
|
+
- Agent 不读取 Schema,不调用 LLM,不硬编码业务判断。
|
|
44
|
+
- 认证 Token 仅用于身份透传,数据权限由 Sponge MCP Server 校验。
|
|
45
|
+
- 不新建数据库,不持久化 Schema、SQL 或查询结果。
|
package/SKILL.md
CHANGED
|
@@ -1,127 +1,52 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: octopus-agent
|
|
3
|
-
description: 丸子Agent(Octopus Agent
|
|
3
|
+
description: 丸子Agent(Octopus Agent),单次调用 Sponge MCP Server 完成自然语言数据查询。
|
|
4
4
|
metadata:
|
|
5
5
|
roll-env-file: references/env.yaml
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# 丸子Agent(Octopus Agent)
|
|
9
9
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
本 Agent 只负责海绵数据查询:
|
|
13
|
-
|
|
14
|
-
- 读取和刷新 Schema Cache(表结构缓存)。
|
|
15
|
-
- 基于用户问题和 schema context,通过 MCP Sampling(采样)请求 roll-core 代调 LLM 生成只读 SQL。
|
|
16
|
-
- 按 `get_schema`、`validate_sql`、`execute_sql` 三步调用 Sponge MCP Server。
|
|
17
|
-
- 记录 Audit Log(审计日志)。
|
|
18
|
-
|
|
19
|
-
## Tools
|
|
20
|
-
|
|
21
|
-
| Tool | 中文解释 | 用途 |
|
|
22
|
-
|-|-|-|
|
|
23
|
-
| `diagnostic_status` | 诊断状态 | 检查配置、缓存和日志路径 |
|
|
24
|
-
| `refresh_schema` | 刷新表结构 | 人工刷新本地 schema 缓存 |
|
|
25
|
-
| `query_sponge` | 查询海绵数据 | 自然语言查询入口 |
|
|
26
|
-
|
|
27
|
-
`refresh_schema` 必填 `traceId`;Agent 会自动读取本地缓存里的 `schemaVersion` 并传给 Sponge。
|
|
28
|
-
|
|
29
|
-
`query_sponge` 支持 `resultFormat`:`text`、`json`、`yaml`、`md`、`markdown`、`html`。不传时按用户问题自动识别。
|
|
30
|
-
`query_sponge` 不接收用户或设备标识;权限由 Sponge MCP Server 根据 `Authorization` token 解析。
|
|
31
|
-
Agent 只生成业务 SQL,不生成 `:scope.project_ids`、`:scope.brand_ids` 等权限占位符。
|
|
32
|
-
调试时可传 `includeSql=true` 返回 `generatedSql`、`normalizedSql`、`repairedSql` 和 `validation`;默认不返回 SQL。
|
|
33
|
-
`query_sponge` 不需要用户确认查询计划。工具内部会按 schema 中文语义识别表、字段、筛选条件、枚举和实体,把 `search_entities` 的真实命中项固化进 queryPlan,然后直接生成、校验并执行 SQL,不会在任何节点停下来等待用户确认。
|
|
34
|
-
`finalAnswerOnly` 默认是 `true`,并返回 `structuredContent.responseMode=verbatim`;上层 Agent 必须把 `content[0].text` 或 `structuredContent.answer` 原样作为最终用户可见答复,不要只放在可折叠工具步骤里,也不要改写成摘要。用户可见答复统一为三段式:`1. 查询实体`、`2. 筛选条件` 和 `3. 查询结果`。
|
|
35
|
-
普通业务返回只带一个 `content` 项:`content[0].text` 是三段式用户可见答复。是否原样透传由 `structuredContent.responseMode=verbatim` 和 `finalAnswerOnly=true` 这两个结构化字段承载,不再在 `content` 里附带任何元指令文本,避免元指令泄露给用户。
|
|
36
|
-
`search_entities` 只用于实体别称和模糊召回;品牌、项目等多个实体同时命中时不追问用户,queryPlan 会用同组 OR 条件承载,并在结果中展示各实体名称字段方便用户自行缩小范围。
|
|
37
|
-
状态枚举不单独追问。用户明确提出状态条件时,把状态条件写入 SQL 的 WHERE;用户未提出状态条件或 schema 缺少枚举时,首次查询保持宽泛,但查询结果必须展示相关状态字段的中文含义。
|
|
38
|
-
当 schema 完全匹配不到相关表时,`query_sponge` 会返回 `needsClarification=true`、`clarificationType=schema_no_match`。此时必须立即停止,原样展示 `structuredContent.answer` 给用户;这是"暂不支持该查询"的说明,不是查询计划确认,用户补充明确需求后重新发起查询即可。业务指标口径缺失等其它情况会直接返回普通 `answer`(`needsClarification=false`),上层原样展示即可。
|
|
39
|
-
|
|
40
|
-
## 上层调用规则
|
|
41
|
-
|
|
42
|
-
1. 用户只是说使用、启动、检查、看看丸子Agent,或确认 Agent 是否可用时,只调用 `diagnostic_status`,返回状态后停止;不得主动调用 `query_sponge`,不得编造示例业务问题。
|
|
43
|
-
2. 查询前如需排查环境,先调用 `diagnostic_status`。
|
|
44
|
-
3. schema 过期或缺失时,调用 `refresh_schema`。
|
|
45
|
-
4. 用户提出明确业务数据查询时,调用 `query_sponge`。
|
|
46
|
-
5. 不要求上层提供真实项目 ID 或品牌 ID;权限由 Sponge MCP Server 处理。
|
|
47
|
-
6. 权限范围由 Sponge MCP Server 在 `execute_sql` 阶段统一补全并记录。
|
|
48
|
-
7. 不允许绕过本 Agent 直接连业务数据库。
|
|
49
|
-
8. 运行参数由 Agent 内置;本地只通过环境变量配置 Sponge 地址和 Token。
|
|
50
|
-
9. LLM 由 roll-core 通过 MCP Sampling 代调,本 Agent 不读取模型 API Key。
|
|
51
|
-
10. 对“匹配效果”“分析报告”“找出报名多但入职少/在招多但报名少/候选人多但岗位少”等报告类问题,只能调用一次 `query_sponge` 处理完整原始问题,不要拆成多个品牌/项目/城市子查询。
|
|
52
|
-
11. `query_sponge` 返回后,最终回复正文必须完整复制工具返回的 `answer`,不要只把报告放在执行命令、思考过程、工具详情或可折叠步骤里。
|
|
53
|
-
12. 如果 `structuredContent.needsClarification=true`(`clarificationType=schema_no_match`),最终回复只能展示 `structuredContent.answer`;这是"暂不支持该查询"的说明,用户补充明确需求后重新发起查询即可。
|
|
54
|
-
13. 品牌、项目等实体多候选由 queryPlan 的同组 OR 条件处理;上层不要要求用户先选择品牌或项目。
|
|
55
|
-
14. 如果 `answer` 已包含分析报告,最终回复顺序必须保持:完整分析报告 -> 总结关键发现;不要自行改写成摘要替代报告。
|
|
10
|
+
## Tool
|
|
56
11
|
|
|
57
|
-
|
|
12
|
+
`query`(自然语言查询)是唯一业务入口:
|
|
58
13
|
|
|
59
|
-
|
|
14
|
+
- `question`:必填,本次用于分析的问题。
|
|
15
|
+
- `resultFormat`:选填,结果格式,默认 markdown(Markdown 格式)。
|
|
60
16
|
|
|
61
|
-
|
|
17
|
+
## 调用边界
|
|
62
18
|
|
|
63
|
-
|
|
19
|
+
- 用户询问“介绍丸子”“丸子有什么功能”“怎么使用丸子”“丸子是否可用”等介绍、能力说明或使用方法时,只直接介绍,不调用 `query`(查询)。
|
|
20
|
+
- 介绍场景禁止编造、发起或执行任何示例查询,也不要声称已经执行过查询。
|
|
21
|
+
- 只有用户提出明确的业务数据查询问题时,才调用 `query`。
|
|
22
|
+
- 用户没有给出明确查询对象和查询诉求时,先说明能力,不自行补充示例问题。
|
|
64
23
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```text
|
|
68
|
-
1. 查询实体:
|
|
69
|
-
门店;项目
|
|
70
|
-
|
|
71
|
-
2. 筛选条件:
|
|
72
|
-
创建时间 >= 2025-07-07;创建时间 < 2026-07-08;按创建日期、项目统计门店数量
|
|
73
|
-
|
|
74
|
-
3. 查询结果:
|
|
75
|
-
返回 500 行。
|
|
76
|
-
limited: true
|
|
77
|
-
|
|
78
|
-
|序号|创建日期|项目名称|门店数量|
|
|
79
|
-
|---|---|---|---|
|
|
80
|
-
|1|2026-07-07|示例项目|12|
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
最终回复必须原样展示以上两段式内容;不要改写成“我已查询到近一年数据”,也不要删除第一段。
|
|
84
|
-
|
|
85
|
-
### 示例 2:“所有/全部”不覆盖时间范围
|
|
86
|
-
|
|
87
|
-
用户:查询近一年门店数据,列出所有项目
|
|
24
|
+
## 职责
|
|
88
25
|
|
|
89
|
-
|
|
26
|
+
- 每个新问题生成新的 `requestId`(请求编号)。
|
|
27
|
+
- 原样传入 `question`(问题),只调用一次 `query_sponge`(查询 Sponge)。
|
|
28
|
+
- 成功时原样返回 `answer`(答案),失败时返回服务端错误。
|
|
29
|
+
- 仅网络重试复用原 `requestId`,并控制总超时。
|
|
90
30
|
|
|
91
|
-
|
|
92
|
-
- `所有` 是返回范围或导出诉求,不是取消时间范围。
|
|
93
|
-
- 如果生成 CSV,也必须在两段式答复中保留实体和筛选条件后再展示文件路径。
|
|
31
|
+
## 最终展示规则
|
|
94
32
|
|
|
95
|
-
|
|
33
|
+
上层 Agent 的最终答复必须完全等于 `content[0].text`。该内容带有 `audience: user`(面向用户)和最高优先级注解,禁止总结、删减、改写或重排。必须保留原始换行、Markdown 表格和以下顺序:
|
|
96
34
|
|
|
97
|
-
|
|
35
|
+
1. 查询实体
|
|
36
|
+
2. 筛选条件
|
|
37
|
+
3. 业务逻辑
|
|
38
|
+
4. 查询结果
|
|
98
39
|
|
|
99
|
-
|
|
100
|
-
我查到了门店数量最多的项目,结果如下……
|
|
101
|
-
```
|
|
40
|
+
不得先展示查询结果,也不得在四段内容前后添加分析性结论。
|
|
102
41
|
|
|
103
|
-
|
|
42
|
+
Agent 不调用 LLM(大语言模型),不读取 Schema(表结构),不生成、修改或解析 SQL,不根据自然语言错误做业务判断。
|
|
104
43
|
|
|
105
|
-
|
|
106
|
-
- `2. 筛选条件`
|
|
107
|
-
- `3. 查询结果`
|
|
108
|
-
- 完整表格或 CSV 文件路径说明
|
|
44
|
+
## 查询链路
|
|
109
45
|
|
|
110
|
-
|
|
46
|
+
`query → query_sponge → answer`(查询 → 查询 Sponge → 答案)
|
|
111
47
|
|
|
112
|
-
|
|
113
|
-
2. 同一个用户问题、同一次分析上下文必须复用同一个 `sessionId`(会话 ID)。
|
|
114
|
-
3. 口径修正仍属于同一问题,继续复用原 `sessionId`,但使用新的 `traceId` 发起新链路。
|
|
115
|
-
4. 如果 `execute_sql` 成功但结果异常,例如 `rowCount=0`、关键字段大量为空、结果明显不符合问题预期,不自动改口径重跑。
|
|
116
|
-
5. 出现结果异常时,先向用户说明异常点、可能原因、建议口径和影响范围,等待用户确认后再用新口径查询。
|
|
117
|
-
6. 如果只是 SQL 校验失败,可以修正 SQL 后重新校验;涉及业务口径变化时必须先确认。
|
|
118
|
-
7. `originalQuestion` 只传**当前这一轮**用户原话,不要把同一会话里上一轮问题拼进来;`sessionId` 仅用于 MCP 审计,**不会**自动继承上一轮的项目/品牌/门店筛选。
|
|
119
|
-
8. 用户从「查岗位列表」转为「查操作日志 / 算历史在招数」等新问题时,仍复用 `sessionId` 即可,但 `originalQuestion` 必须是新问题本身,例如「你得查岗位上下架操作日志,给我算一下6月21日有多少在招岗位」。
|
|
120
|
-
9. 品牌/项目等多实体命中不追问用户;Agent 会在查询计划中生成同组 OR 条件并直接执行,结果中展示各实体名称字段方便用户自行缩小范围。
|
|
48
|
+
Agent 不追问、不生成或修复 SQL(结构化查询语言),也不调用服务端内部原子能力。
|
|
121
49
|
|
|
122
50
|
## 持久化
|
|
123
51
|
|
|
124
|
-
|
|
125
|
-
- `logs/octopus-sponge-audit.log`:Audit Log(审计日志)。
|
|
126
|
-
|
|
127
|
-
不新建数据库,不保存真实项目 ID、品牌 ID、数据库账号密码。
|
|
52
|
+
不新建数据库,不缓存 Schema、SQL 或查询结果。链路状态仅保存在单次调用内存中。
|
package/package.json
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roll-agent/octopus-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Octopus Agent:编排 Sponge MCP Server 的 NL2SQL 查询链路",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"license": "UNLICENSED",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"roll-agent",
|
|
8
|
-
"mcp",
|
|
9
|
-
"nl2sql"
|
|
10
|
-
],
|
|
11
|
-
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
13
|
-
},
|
|
14
7
|
"files": [
|
|
15
|
-
"octopus_skill",
|
|
16
8
|
"src",
|
|
17
9
|
"scripts",
|
|
18
10
|
"references",
|
|
19
11
|
"SKILL.md",
|
|
20
|
-
"
|
|
21
|
-
"!**/__pycache__/**",
|
|
22
|
-
"!**/*.pyc",
|
|
23
|
-
"!**/*.egg-info/**"
|
|
12
|
+
"README.md"
|
|
24
13
|
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node scripts/start-octopus-agent.js",
|
|
16
|
+
"test": "node --test"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
25
21
|
"rollAgent": {
|
|
26
22
|
"runtime": {
|
|
27
23
|
"ownership": "on-demand",
|
|
@@ -29,9 +25,7 @@
|
|
|
29
25
|
},
|
|
30
26
|
"start": {
|
|
31
27
|
"command": "node",
|
|
32
|
-
"args": [
|
|
33
|
-
"scripts/start-octopus-agent.js"
|
|
34
|
-
]
|
|
28
|
+
"args": ["scripts/start-octopus-agent.js"]
|
|
35
29
|
}
|
|
36
30
|
}
|
|
37
31
|
}
|
package/references/env.yaml
CHANGED
|
@@ -5,25 +5,4 @@ env:
|
|
|
5
5
|
example: https://sponge-mcp.duliday.com
|
|
6
6
|
SPONGE_MCP_ACCESS_TOKEN:
|
|
7
7
|
required: true
|
|
8
|
-
purpose:
|
|
9
|
-
OCTOPUS_BINDING_SHADOW:
|
|
10
|
-
required: false
|
|
11
|
-
purpose: >-
|
|
12
|
-
实体识别(binding)总开关。false(默认)=识别结果注入 LLM prompt 影响 SQL;
|
|
13
|
-
true=仅计算并写审计日志(entity_binding_shadow),不改变 SQL 生成(用于回滚观察)。
|
|
14
|
-
example: "false"
|
|
15
|
-
OCTOPUS_BINDING_ENABLED:
|
|
16
|
-
required: false
|
|
17
|
-
purpose: >-
|
|
18
|
-
哪些实体类型的识别结果允许注入 prompt,逗号分隔,可选值 brand,location,project。
|
|
19
|
-
默认 brand,location,project(全开)。仅在 OCTOPUS_BINDING_SHADOW=false 时生效;
|
|
20
|
-
显式设为空字符串可关闭全部类型。
|
|
21
|
-
example: brand,location,project
|
|
22
|
-
OCTOPUS_BINDING_MIN_SCORE:
|
|
23
|
-
required: false
|
|
24
|
-
purpose: 实体模糊匹配的最低分数阈值(0~1),低于该分数不视为命中。默认 0.6。
|
|
25
|
-
example: "0.6"
|
|
26
|
-
OCTOPUS_SAMPLING_MAX_TOKENS:
|
|
27
|
-
required: false
|
|
28
|
-
purpose: MCP sampling/createMessage 请求的 maxTokens(LLM 输出 token 上限)。默认 4096。
|
|
29
|
-
example: "4096"
|
|
8
|
+
purpose: 身份透传 Token(令牌)
|
|
@@ -1,126 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const { spawn, spawnSync } = require("node:child_process");
|
|
5
|
-
const path = require("node:path");
|
|
6
|
-
|
|
7
|
-
const MIN_MAJOR = 3;
|
|
8
|
-
const MIN_MINOR = 11;
|
|
9
|
-
const VERSION_CHECK = [
|
|
10
|
-
"import sys",
|
|
11
|
-
`raise SystemExit(0 if sys.version_info >= (${MIN_MAJOR}, ${MIN_MINOR}) else 1)`,
|
|
12
|
-
].join("; ");
|
|
13
|
-
|
|
14
|
-
function splitCommand(value) {
|
|
15
|
-
const parts = [];
|
|
16
|
-
let current = "";
|
|
17
|
-
let quote = null;
|
|
18
|
-
|
|
19
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
20
|
-
const char = value[index];
|
|
21
|
-
if ((char === '"' || char === "'") && quote === null) {
|
|
22
|
-
quote = char;
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
if (char === quote) {
|
|
26
|
-
quote = null;
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
if (/\s/.test(char) && quote === null) {
|
|
30
|
-
if (current) {
|
|
31
|
-
parts.push(current);
|
|
32
|
-
current = "";
|
|
33
|
-
}
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
current += char;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (current) {
|
|
40
|
-
parts.push(current);
|
|
41
|
-
}
|
|
42
|
-
return parts;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function configuredCandidate() {
|
|
46
|
-
const value = process.env.OCTOPUS_PYTHON_COMMAND;
|
|
47
|
-
if (!value) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const [command, ...args] = splitCommand(value);
|
|
52
|
-
if (!command) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
return { command, args, source: "OCTOPUS_PYTHON_COMMAND" };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function pythonCandidates() {
|
|
59
|
-
return [
|
|
60
|
-
configuredCandidate(),
|
|
61
|
-
{ command: "python3", args: [], source: "python3" },
|
|
62
|
-
{ command: "python", args: [], source: "python" },
|
|
63
|
-
{ command: "py", args: ["-3.11"], source: "py -3.11" },
|
|
64
|
-
{ command: "py", args: ["-3"], source: "py -3" },
|
|
65
|
-
].filter(Boolean);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function isUsablePython(candidate) {
|
|
69
|
-
const result = spawnSync(candidate.command, [...candidate.args, "-c", VERSION_CHECK], {
|
|
70
|
-
encoding: "utf8",
|
|
71
|
-
stdio: "ignore",
|
|
72
|
-
});
|
|
73
|
-
return result.status === 0;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function findPython() {
|
|
77
|
-
return pythonCandidates().find(isUsablePython);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const python = findPython();
|
|
81
|
-
if (!python) {
|
|
82
|
-
console.error(
|
|
83
|
-
[
|
|
84
|
-
`Octopus Agent requires Python ${MIN_MAJOR}.${MIN_MINOR}+.`,
|
|
85
|
-
"Install Python 3.11+ and make one of these commands available:",
|
|
86
|
-
"python3, python, py -3.11, py -3",
|
|
87
|
-
"Or set OCTOPUS_PYTHON_COMMAND to the Python command.",
|
|
88
|
-
].join("\n")
|
|
89
|
-
);
|
|
90
|
-
process.exit(1);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const packageRoot = path.resolve(__dirname, "..");
|
|
94
|
-
const PYTHON_BOOTSTRAP = [
|
|
95
|
-
"import runpy",
|
|
96
|
-
"import sys",
|
|
97
|
-
"from pathlib import Path",
|
|
98
|
-
"package_root = Path(sys.argv[1]).resolve()",
|
|
99
|
-
"sys.path.insert(0, str(package_root / 'src'))",
|
|
100
|
-
"sys.path.insert(0, str(package_root))",
|
|
101
|
-
"sys.argv = ['octopus_skill.octopus_run', *sys.argv[2:]]",
|
|
102
|
-
"runpy.run_module('octopus_skill.octopus_run', run_name='__main__', alter_sys=True)",
|
|
103
|
-
].join("; ");
|
|
104
|
-
|
|
105
|
-
const child = spawn(
|
|
106
|
-
python.command,
|
|
107
|
-
[...python.args, "-c", PYTHON_BOOTSTRAP, packageRoot, ...process.argv.slice(2)],
|
|
108
|
-
{
|
|
109
|
-
cwd: packageRoot,
|
|
110
|
-
stdio: "inherit",
|
|
111
|
-
windowsHide: true,
|
|
112
|
-
}
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
child.on("error", (error) => {
|
|
116
|
-
console.error(`Failed to start Python via ${python.source}: ${error.message}`);
|
|
117
|
-
process.exit(1);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
child.on("exit", (code, signal) => {
|
|
121
|
-
if (signal) {
|
|
122
|
-
process.kill(process.pid, signal);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
process.exit(code ?? 0);
|
|
126
|
-
});
|
|
2
|
+
import "../src/server.js";
|
package/src/config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const REQUEST_TIMEOUT_MS = 58_000;
|
|
2
|
+
const TOTAL_TIMEOUT_MS = 59_000;
|
|
3
|
+
const NETWORK_RETRIES = 2;
|
|
4
|
+
|
|
5
|
+
export function loadConfig(env = process.env) {
|
|
6
|
+
return {
|
|
7
|
+
baseUrl: (env.SPONGE_MCP_BASE_URL || "https://sponge-mcp.duliday.com").replace(/\/+$/, ""),
|
|
8
|
+
accessToken: (env.SPONGE_MCP_ACCESS_TOKEN || "").trim(),
|
|
9
|
+
requestTimeoutMs: REQUEST_TIMEOUT_MS,
|
|
10
|
+
totalTimeoutMs: TOTAL_TIMEOUT_MS,
|
|
11
|
+
networkRetries: NETWORK_RETRIES,
|
|
12
|
+
};
|
|
13
|
+
}
|
package/src/context.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
|
2
|
+
|
|
3
|
+
export class SpongeMcpError extends Error {
|
|
4
|
+
constructor(message, options = {}) {
|
|
5
|
+
super(message, options);
|
|
6
|
+
this.name = "SpongeMcpError";
|
|
7
|
+
this.status = options.status;
|
|
8
|
+
this.code = options.code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class SpongeMcpClient {
|
|
13
|
+
constructor({ baseUrl, accessToken, requestTimeoutMs = 30_000, networkRetries = 2, fetchImpl = fetch }) {
|
|
14
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
15
|
+
this.accessToken = accessToken;
|
|
16
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
17
|
+
this.networkRetries = networkRetries;
|
|
18
|
+
this.fetchImpl = fetchImpl;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async call(tool, payload, { signal } = {}) {
|
|
22
|
+
let lastError;
|
|
23
|
+
for (let attempt = 0; attempt <= this.networkRetries; attempt += 1) {
|
|
24
|
+
try {
|
|
25
|
+
return await this.#post(tool, payload, signal);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
lastError = error;
|
|
28
|
+
if (attempt === this.networkRetries || !isRetryable(error)) throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw lastError;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async #post(tool, payload, parentSignal) {
|
|
35
|
+
const timeout = AbortSignal.timeout(this.requestTimeoutMs);
|
|
36
|
+
const signal = parentSignal ? AbortSignal.any([parentSignal, timeout]) : timeout;
|
|
37
|
+
let response;
|
|
38
|
+
try {
|
|
39
|
+
response = await this.fetchImpl(`${this.baseUrl}/mcp/tools/${tool}`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"content-type": "application/json",
|
|
43
|
+
...(this.accessToken ? { authorization: `Bearer ${this.accessToken}` } : {}),
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify(payload),
|
|
46
|
+
signal,
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new SpongeMcpError(`MCP ${tool} 网络调用失败`, { cause: error, code: "NETWORK_ERROR" });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const text = await response.text();
|
|
53
|
+
let body;
|
|
54
|
+
try {
|
|
55
|
+
body = text ? JSON.parse(text) : {};
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new SpongeMcpError(`MCP ${tool} 返回非 JSON 数据`, {
|
|
58
|
+
cause: error,
|
|
59
|
+
status: response.status,
|
|
60
|
+
code: "INVALID_RESPONSE",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const detail = body?.error?.message;
|
|
65
|
+
throw new SpongeMcpError(
|
|
66
|
+
detail || `MCP ${tool} 调用失败:HTTP ${response.status}`,
|
|
67
|
+
{
|
|
68
|
+
status: response.status,
|
|
69
|
+
code: body?.errorCode || body?.error?.code || "HTTP_ERROR",
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return body;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isRetryable(error) {
|
|
78
|
+
return error instanceof SpongeMcpError &&
|
|
79
|
+
(error.code === "NETWORK_ERROR" || RETRYABLE_STATUS.has(error.status));
|
|
80
|
+
}
|