Skip to content

feat(execution-factory): add a Cypher tool type usable as an object-type logic property source #2670

Closed

Description

@sh00tg0a1

Feature summary

执行工厂新增 Cypher 工具类型:工具的实现就是一条只读 Cypher 语句,语句里的 $参数 就是工具入参。对象类逻辑属性通过现有的 tool 来源引用它,从而可以用 Cypher 沿关系计算属性值;Agent、MCP 也可以直接调用。工具编辑器支持用自然语言生成 Cypher。

设计稿:openbkn-ai/bkn-docs#264(docs/foundry/execution-factory/design/issue-2670-Cypher工具类型-设计.md)

Problem / use case

逻辑属性只支持 tool 来源(BKN 规范 §6.7)。很多逻辑属性要沿关系取值,例如「这个供应商供应的物料数」,一条 Cypher 就能表达。现在只能写函数工具、在 Python 里调 Cypher 接口:

  • 语句藏在代码里,界面上看不到、改不了;
  • 需要沙箱,要自己处理令牌和返回值;
  • 采集关闭时函数工具读 BKN 会失败(execute_tool 不在 executeUntracedBusiness 保留 bkn_context 的例外里)。

Proposed solution (optional)

  • 工具箱与工具新增类型 cypher;工具定义:kn_id、query、parameters(按 $name 自动生成)、result_shape(value / table)。
  • value 形状:只能返回一列标量;0 行为 {"value": null},1 行为 {"value": x},多行或非标量报错;逻辑属性 ResultPath 写 $.value。
  • 起点约定:单主键用 $id,联合主键每个主键一个同名参数。
  • 保存时用 cypher-queries 的 explain: true 校验;执行时执行工厂直接调 bkn-backend 内部 Cypher 接口,不经沙箱。
  • 身份:直接调用用调用人身份;作为逻辑属性时沿用 ontology-query 现有的知识网络代理身份。
  • 自然语言生成复用内置 cypher_generation Agent。
  • 不改 BKN 规范、ontology-query、Context Loader。

Acceptance Criteria (Definition of Done)

  • 可以创建 Cypher 工具箱与 Cypher 工具;保存时语法、只读、对象类/属性、权限与 $name/参数一致性不通过时拒绝并给出原因
  • value 形状:0 行返回 null、1 行返回值、多行与非标量报错;table 形状原样返回 columns、entries、limited
  • Agent / MCP / 工具调试直接调用时以调用人身份执行,行列权限生效
  • 作为逻辑属性经 ontology-query 取值可用(代理身份),单主键与联合主键对象类各验证一次
  • 采集关闭时 Cypher 逻辑属性仍能取值
  • Studio:Cypher 工具编辑、试运行、用描述生成;逻辑属性选中 Cypher 工具时默认 ResultPath = $.value、起点参数绑定主键
  • 测试覆盖:参数提取与校验、返回形状、身份透传
  • CI green

Boundaries / Risk

  • 不给逻辑属性新增 cypher 类型,不改 BKN 规范;不支持写操作。
  • 新增数据库取值与 Cypher 定义存储,迁移脚本放在当时根 VERSION 对应的迁移目录,属于 schema 变更,执行前需人工确认。
  • 待确认:知识网络代理账号对 Cypher 涉及对象类的读权限;explain 是否给出返回列数;cypher_generation 能否稳定遵守单值与起点约定。

Additional context

  • 逻辑属性执行:adp/bkn/ontology-query/server/logics/object_type/object_type_service.go(handleToolProperty)
  • 工具执行分发:adp/execution-factory/operator-integration/server/logics/toolbox/execute.go(executeTool)
  • Cypher 接口:adp/bkn/bkn-backend/server/driveradapters/cypher_handler.go

Activity

  1. added this to the 0.3.0 milestone on Oct 29, 2026
  2. self-assigned this
    on Nov 10, 2026
  3. sh00tg0a1 commented on Nov 10, 2026

    @sh00tg0a1
    MemberAuthor

    PRs are open:

    Both were verified on the dev VM. Not yet covered: a non-admin caller with row-level permissions.

  4. sh00tg0a1 commented on Nov 11, 2026

    @sh00tg0a1
    MemberAuthor

    Verification on the supply chain sample

    Setup. Verification environment, network supply_ontology_hand (the bkn-samples supply chain sample: suppliers, purchase orders, purchase requests, MRP, materials, inventory, products, BOM, sales orders). One function toolbox, 29 functions attempted (14 saved, 15 meant to be refused), 45 calls through /debug and /proxy. Statements below drop the supply_ontology_hand_ prefix from object and relation type names.

    Result. Saving, calling, editing, timeouts and type checks behave as documented. Ten problems came up. The first is a wrong result returned without an error; the next five are statements a function author will naturally write that the Cypher subset refuses. Most of them sit in bkn-backend's Cypher compiler, not in execution-factory.

    Problems

    1. A date property compared with a string that is not a date matches everything, with no error.

    MATCH (o:salesorder) WHERE o.signing_date >= $since RETURN count(o)
    $since rows
    2026-04-20 419
    not-a-date, 2026年4月20日, 下周一, 2026-13-45, 2026-04, "" 800 (all)
    not-a-date with < or = 0

    The unparsable value behaves like the earliest possible time. A function is called by an LLM or a script, so a value like 2026年4月20日 is a realistic input, and the caller gets a plausible but wrong count. Wrapping the parameter as datetime($since) or date($since) already rejects the same values with a clear 400; only the bare comparison skips the check. The save-time explain cannot catch this, because it runs with the valid example value.
    Suggested: validate a string compared with a date or datetime property the same way datetime() does. Until then, the docs and the cypher_generation prompt should write datetime($x).

    2. IN $list with an empty list fails the call.
    WHERE m.material_code IN $codes with codes: [] returns status 400: "IN over nothing matches nothing, so leave the condition out instead". A function author cannot leave the condition out, and the caller expects an empty table (or null / 0 for a value function). Suggested: treat an empty list as matching nothing and return no rows.

    3. LIMIT $n and SKIP $n are refused at save time.
    "LIMIT takes an integer literal". A "top N" or paged function needs a separate function for each N. Suggested: accept an integer parameter, checked against the 10000 cap at call time.

    4. Optional filters cannot be written.
    WHERE ($code IS NULL OR p.supplier_number = $code) is refused ("IN and IS NULL apply to variable.property"), and so is ($only_open = false OR p.rowclosestatus_title <> '已关闭') ("a comparison must read a property"). Every input of a Cypher function is also required. Together this means one function per combination of filters, and a boolean input can only be compared with a property.

    5. Arithmetic across two WITH stages is refused with a misleading message.

    MATCH (m:material {material_code:'725-000334'})-[:mat2inv]->(i:inventory)
    WITH m, sum(i.available_inventory_qty) AS avail
    WITH m.purchase_fixedleadtime AS lt, avail
    RETURN lt - avail AS d

    Refused: "d" reads purchase_fixedleadtime outside an aggregate. Projecting lt in the first WITH works, and so does returning lt, avail without the subtraction, so the statement is valid. This blocks the natural shortage query: sales order → product → BOM → material, then inventory, then need - avail.

    6. Function support is narrow, and the refusal does not say which function.
    round(), toUpper() and size() all come back as "function calls is not supported". coalesce(), CASE, date() and datetime() work. Also refused: collect() next to sum() ("run the count as a query of its own"), and list slicing collect(x)[0..3]. Suggested: name the function in the message and list the supported ones in the Cypher manual.

    7. Numbers come back in two forms.
    sum() of a decimal property returns the string "13833141.000000", while sum(a) * 100.0 / sum(b) returns the float 72.8975147437592. The value function's output schema leaves the type open ("A string, a number, a boolean or null"). A consumer that compares values or does arithmetic has to handle both forms. Keeping decimals as strings protects precision, so this is mainly a documentation gap: the call guide and the output schema should state it.

    8. Multi-hop queries come close to the 55 s statement limit.

    • Shortage for one sales order (4 hops plus an OPTIONAL MATCH on inventory, 507 materials): 39 s.
    • BOM size per product over all products (3 hops): 20 s with a STARTS WITH $prefix filter, and timed out at 55 s without one.

    The caller timeout works (timeout: 3 and 5 give 504 on time), but a caller using the default timeout waits close to a minute before it fails.

    9. Error details mix two languages and wrap twice.
    Example: the cypher statement was refused: Cypher 查询使用了本接口不支持的语法。请改写为受支持的只读子集。: line 1:213: .... The useful part is the last clause; the Chinese sentence in the middle repeats bkn-backend's description.

    10. Editing without script_type names the wrong cause.
    An edit that omits script_type gets "a cypher function cannot become a python function; create a new function instead". This is documented (the field defaults to python), but the message reads as if the caller asked to switch languages. Suggested: default to the stored type, or say that script_type: cypher is missing.

    What passed

    45 calls, grouped
    Group Case Result
    value supplier name by code; unknown code; x' OR '1'='1 name; null; null (treated as a value)
    value PO lines per supplier over po2sup 240, matches a direct count
    value decimal sum, decimal property, property that is null "13833141.000000", "839.999964", null
    value count of sales orders by status 784 / 16, matches the data
    table 240 PO lines, no LIMIT 240 rows, limited: false
    table 5645 BOM lines, no LIMIT 1000 rows, limited: true
    table IN $codes with two codes; a string instead of a list 2 rows; 400 "must be a list"
    table >= $since with 2026-04-20 and with 2026-04-20T00:00:00+08:00 same rows
    table number input: 1000, "1000", "abc", missing rows, rows (converted), 400, 400 "missing required parameters"
    complex WITH aggregate + ORDER BY (material mix per supplier) OK, 0.03 s
    complex PO → purchase request → MRP → material, 3 hops, DISTINCT OK, 0.24 s
    complex CASE over two aggregates, coalesce() OK
    complex WITH … WHERE avail > … after an aggregate OK
    runtime value function matching 229 rows 400 "must return at most one row"
    runtime caller timeout 3 s / 5 s on a 20 s query 504 on time
    proxy value, IN, string-to-number conversion, extra body field, 229 rows, timeout same as /debug
    proxy 20 concurrent calls of the 3-hop function all 200, same 20 rows, slowest 1.5 s
    refused at save write (SET, DELETE), unknown property, unknown label, unknown network, syntax error 400 / 404 with bkn-backend's reason
    refused at save input not in statement, $name with no input, input without description 400 naming the parameter
    refused at save value shape with 2 columns, value shape returning a node 400 suggesting the table shape
    edit change statement; to a write; to 2 columns (value); new $nm without input; to python saved and new result returned; the rest refused

    Proxy calls also need the toolbox published and the tool enabled; new functions start disabled. /debug needs neither. That is the same for Python functions.

    Not covered

    • A non-admin caller with row or column permissions.
    • Calls through Context Loader execute_tool and through a logic property binding.
    • Studio UI. All of the above went through the HTTP API.

    Problems 1–6 are in bkn-backend's Cypher compiler, problems 9–10 in execution-factory, and 7–8 need docs more than code. I can open separate issues if that is easier to track.

  5. sh00tg0a1 commented on Nov 11, 2026

    @sh00tg0a1
    MemberAuthor

    Fixes for the verification report above are up:

    All three were run against supply_ontology_hand on the verification environment beside the deployed services. Suggested merge order: #3075, #3076, #3077.

    Not addressed here: problem 4 (optional filters) needs its own design; new functions such as round() (problem 6) are a feature; problems 7 and 8 are documentation.

  6. sh00tg0a1 commented on Nov 11, 2026

    @sh00tg0a1
    MemberAuthor

    Follow-up on the supply chain verification report (#2670 (comment)). With these PRs, every problem in the report has a fix or a doc note.

    Problem PR State
    4. Optional filters #3097 (bkn-backend + execution-factory), openbkn-ai/bkn-studio#1316 (workbench) Open, verified on the dev VM
    round() / toUpper() refused #3098 (bkn-backend), openbkn-ai/bkn-studio#1318 (reference) Open, verified on the dev VM
    7. Two number forms #3094 (docs and output schema); #3098 adds toFloat() / round() to get numbers Open
    8. Multi-hop close to 55 s #3094 (docs) Open
    Studio follow-up to #3075/#3076/#3077 openbkn-ai/bkn-studio#1315 (drops the stale LIMIT $n warning, reference update) Open

    What a caller can now write:

    // Optional filters: pass status / since, or leave them out
    MATCH (o:salesorder)
    WHERE ($status IS NULL OR o.order_status = $status)
      AND ($since IS NULL OR o.signing_date >= $since)
    RETURN count(o) AS n
    
    // A rounded ratio, returned as a number
    MATCH (p:po)-[:po2sup]->(s:supplier {supplier_code: $code})
    RETURN round(sum(p.actqty) * 100.0 / sum(p.qty), 2) AS received_pct

    On the VM, the first statement, saved as a function with two required: false inputs, returns 4, 16, 800 and 800 for both inputs given, one given, none, and both null. The second returns 72.9 for supplier 59202002.

    Merge order:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions