@roll-agent/octopus-agent 0.0.10 → 0.0.11

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 CHANGED
@@ -74,6 +74,7 @@ roll run octopus-agent query_sponge --json \
74
74
  ```
75
75
 
76
76
  `includeSql=true` 时会返回 `generatedSql`、`normalizedSql`、`repairedSql` 和 `validation`。
77
+ 所有模式都会返回 `needsClarification`。当 `needsClarification=true` 时,上层 Agent 必须原样展示 `answer` 并等待用户确认,不得自行选择候选项或再次调用 `query_sponge`。
77
78
 
78
79
  ### 查询链路规范
79
80
 
package/SKILL.md CHANGED
@@ -30,6 +30,25 @@ metadata:
30
30
  `query_sponge` 不接收用户或设备标识;权限由 Sponge MCP Server 根据 `Authorization` token 解析。
31
31
  Agent 只生成业务 SQL,不生成 `:scope.project_ids`、`:scope.brand_ids` 等权限占位符。
32
32
  调试时可传 `includeSql=true` 返回 `generatedSql`、`normalizedSql`、`repairedSql` 和 `validation`;默认不返回 SQL。
33
+ `finalAnswerOnly` 默认是 `true`,并返回 `structuredContent.responseMode=verbatim`;上层 Agent 必须把 `content[0].text` 或 `structuredContent.answer` 原样作为最终用户可见答复,不要只放在可折叠工具步骤里,也不要改写成摘要。
34
+ 当返回 `needsClarification=true` 时,必须立即停止,原样展示 `structuredContent.answer` 给用户并等待确认;用户未确认前不得再次调用 `query_sponge`,不得自行选择候选项,不得把品牌+地点拼成项目名。
35
+ 用户确认品牌/项目后,不要重新发现工具,不要调用 `diagnostic_status` 或 `refresh_schema`,不要解释 schema,不要自行判断“schema 不支持项目维度”。必须直接再次调用 `query_sponge`,并传 `clarificationSelection`。
36
+ `clarificationSelection` 必须包含:`baseQuestion`(原始问题)、`entityType`(brand=品牌,project=项目)、`entityId`(候选实体 ID)、`entityName`(候选实体名称)。优先从上一轮 `structuredContent.clarificationContext` 和 `structuredContent.clarificationPayload` 取值。
37
+
38
+ 示例:
39
+
40
+ ```json
41
+ {
42
+ "question": "项目",
43
+ "originalQuestion": "项目",
44
+ "clarificationSelection": {
45
+ "baseQuestion": "海绵系统上陈香贵上海有哪些岗位",
46
+ "entityType": "project",
47
+ "entityId": 768,
48
+ "entityName": "陈香贵"
49
+ }
50
+ }
51
+ ```
33
52
 
34
53
  ## 上层调用规则
35
54
 
@@ -43,7 +62,9 @@ Agent 只生成业务 SQL,不生成 `:scope.project_ids`、`:scope.brand_ids`
43
62
  8. LLM 由 roll-core 通过 MCP Sampling 代调,本 Agent 不读取模型 API Key。
44
63
  9. 对“匹配效果”“分析报告”“找出报名多但入职少/在招多但报名少/候选人多但岗位少”等报告类问题,只能调用一次 `query_sponge` 处理完整原始问题,不要拆成多个品牌/项目/城市子查询。
45
64
  10. `query_sponge` 返回后,最终回复正文必须完整复制工具返回的 `answer`,不要只把报告放在执行命令、思考过程、工具详情或可折叠步骤里。
46
- 11. 如果 `answer` 已包含分析报告,最终回复顺序必须保持:完整分析报告 -> 总结关键发现;不要自行改写成摘要替代报告。
65
+ 11. 如果 `structuredContent.needsClarification=true`,最终回复只能展示 `structuredContent.answer`,等待用户确认;用户未确认前不得二次查询。
66
+ 12. 用户确认品牌/项目后,必须直接带 `clarificationSelection` 再调用 `query_sponge`;不要重新发现工具、不要解释 schema、不要输出“schema 不支持项目维度”这类自行判断。
67
+ 13. 如果 `answer` 已包含分析报告,最终回复顺序必须保持:完整分析报告 -> 总结关键发现;不要自行改写成摘要替代报告。
47
68
 
48
69
  ## 查询链路规则
49
70
 
@@ -53,6 +74,9 @@ Agent 只生成业务 SQL,不生成 `:scope.project_ids`、`:scope.brand_ids`
53
74
  4. 如果 `execute_sql` 成功但结果异常,例如 `rowCount=0`、关键字段大量为空、结果明显不符合问题预期,不自动改口径重跑。
54
75
  5. 出现结果异常时,先向用户说明异常点、可能原因、建议口径和影响范围,等待用户确认后再用新口径查询。
55
76
  6. 如果只是 SQL 校验失败,可以修正 SQL 后重新校验;涉及业务口径变化时必须先确认。
77
+ 7. `originalQuestion` 只传**当前这一轮**用户原话,不要把同一会话里上一轮问题拼进来;`sessionId` 仅用于 MCP 审计与澄清续答,**不会**自动继承上一轮的项目/品牌/门店筛选。
78
+ 8. 用户从「查岗位列表」转为「查操作日志 / 算历史在招数」等新问题时,仍复用 `sessionId` 即可,但 `originalQuestion` 必须是新问题本身,例如「你得查岗位上下架操作日志,给我算一下6月21日有多少在招岗位」。
79
+ 9. 品牌/项目澄清续答与问题隔离不冲突:用户确认后必须带 `clarificationSelection` 再调 `query_sponge`;Agent 会把 `baseQuestion` 拼成 `…,按项目「xxx」(id: 941)查询` 这类完整问题,**不会**再对这类续答做子句隔离。
56
80
 
57
81
  ## 持久化
58
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roll-agent/octopus-agent",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "丸子Agent(Octopus Agent)",
5
5
  "license": "UNLICENSED",
6
6
  "keywords": [
package/pyproject.toml CHANGED
@@ -1,10 +1,12 @@
1
1
  [project]
2
2
  name = "octopus-skill"
3
- version = "0.0.10"
3
+ version = "0.0.11"
4
4
  description = "丸子Agent(Octopus Agent)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
7
- dependencies = []
7
+ dependencies = [
8
+ "rapidfuzz>=3.0",
9
+ ]
8
10
 
9
11
  [project.scripts]
10
12
  octopus-agent = "octopus_skill.main:main"
@@ -6,3 +6,20 @@ env:
6
6
  SPONGE_MCP_ACCESS_TOKEN:
7
7
  required: true
8
8
  purpose: Sponge MCP Server 固定访问 Token
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"
@@ -0,0 +1,67 @@
1
+ """One-off: refresh the entity dictionary cache via the MCP server.
2
+
3
+ Replicates Agent._ensure_dictionary's refresh path: calls get_entity_dictionary
4
+ with the configured entity_types and writes the snapshot to the cache file.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+
10
+ from octopus_skill.config import load_config
11
+ from octopus_skill.context import new_trace_id
12
+ from octopus_skill.entity_dictionary import EntityDictionaryCache
13
+ from octopus_skill.mcp_client import SpongeMcpClient
14
+
15
+
16
+ def main() -> int:
17
+ config = load_config()
18
+ if config.dictionary is None:
19
+ print("dictionary config is None; nothing to refresh")
20
+ return 1
21
+
22
+ client = SpongeMcpClient(
23
+ base_url=config.sponge_mcp.base_url,
24
+ access_token=config.sponge_mcp.access_token,
25
+ timeout_ms=config.sponge_mcp.timeout_ms,
26
+ )
27
+ cache = EntityDictionaryCache(config.dictionary.cache_path)
28
+ cached = cache.load()
29
+ cached_version = (
30
+ str(cached.get("dictVersion")) if cached and cached.get("dictVersion") else None
31
+ )
32
+
33
+ print(f"base_url={config.sponge_mcp.base_url}")
34
+ print(f"token_set={bool(config.sponge_mcp.access_token)}")
35
+ print(f"cached_version={cached_version}")
36
+ print(f"entity_types={config.dictionary.entity_types}")
37
+
38
+ trace_id = new_trace_id("trace_dict")
39
+ response = client.get_entity_dictionary(
40
+ trace_id=trace_id,
41
+ dict_version=cached_version,
42
+ entity_types=config.dictionary.entity_types,
43
+ )
44
+
45
+ changed = response.get("changed")
46
+ print(f"response changed={changed} dictVersion={response.get('dictVersion')}")
47
+
48
+ if changed is False and cached is not None:
49
+ print("server reports no change; cache kept as-is")
50
+ return 0
51
+
52
+ if "entities" in response:
53
+ cache.save(response)
54
+ ent = response["entities"]
55
+ for key, value in ent.items():
56
+ items = value.get("items") if isinstance(value, dict) else None
57
+ n = len(items) if isinstance(items, list) else "n/a"
58
+ print(f" saved {key}: items={n}")
59
+ print(f"cache written to {config.dictionary.cache_path}")
60
+ return 0
61
+
62
+ print("response had no 'entities'; cache not updated")
63
+ return 2
64
+
65
+
66
+ if __name__ == "__main__":
67
+ sys.exit(main())
@@ -0,0 +1,47 @@
1
+ """One-off: verify the brand/location/project binding chain end-to-end.
2
+
3
+ Builds the resolver from the persisted dictionary cache (same path the agent
4
+ uses) and runs bind_entities on a few real questions, printing what resolved.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+
10
+ from octopus_skill.config import load_config
11
+ from octopus_skill.entity_binding import bind_entities
12
+ from octopus_skill.entity_resolver import EntityResolver, load_geo_aliases
13
+
14
+
15
+ def show(question: str, resolver: EntityResolver) -> None:
16
+ bindings = bind_entities(question, resolver)
17
+ print(f"\nQ: {question}")
18
+ print(" brands :", [(e.id, e.canonical, e.matched_via) for e in bindings.brands])
19
+ print(" projects:", [(e.id, e.canonical, e.matched_via) for e in bindings.projects])
20
+ print(
21
+ " location:",
22
+ {lvl: (e.id, e.canonical) for lvl, e in bindings.location.items()},
23
+ )
24
+
25
+
26
+ def main() -> int:
27
+ config = load_config()
28
+ assert config.dictionary is not None
29
+ dictionary = json.loads(config.dictionary.cache_path.read_text(encoding="utf-8"))
30
+ geo = load_geo_aliases(config.dictionary.geo_alias_path)
31
+ resolver = EntityResolver.from_payload(dictionary, geo_aliases=geo)
32
+
33
+ print("dictVersion:", dictionary.get("dictVersion"))
34
+ for t in ("brand", "project", "city", "region", "province"):
35
+ idx = resolver.indexes.get(t)
36
+ print(f" index {t}: {len(idx.items) if idx else 0} items")
37
+
38
+ # Real project names pulled from the refreshed dictionary.
39
+ show("西贝莜面村这个项目在上海有多少在招岗位", resolver)
40
+ show("查一下哈根达斯项目的情况", resolver)
41
+ show("北京肯德基项目最近招聘多少人", resolver)
42
+ show("CoCo都可茶饮在南京宝山的门店", resolver)
43
+ return 0
44
+
45
+
46
+ if __name__ == "__main__":
47
+ raise SystemExit(main())
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.0.10"
3
+ __version__ = "0.0.11"