diff --git a/entrypoint/flow-schema-extraction.ts b/entrypoint/flow-schema-extraction.ts index 8050078..8fb659c 100644 --- a/entrypoint/flow-schema-extraction.ts +++ b/entrypoint/flow-schema-extraction.ts @@ -2,7 +2,7 @@ import {readSingleValidation} from "./readSingle"; import {mapToFlowValidation} from "./mapper"; import {getFlowSchemas} from "@code0-tech/triangulum/server"; import type {JsonSchema, SchematizedSubFlowValue} from "@code0-tech/triangulum/server"; -import type {Scalars} from "@code0-tech/sagittarius-graphql-types"; +import type {InlineReferenceValue, NodeParameter, Scalars} from "@code0-tech/sagittarius-graphql-types"; const data = await readSingleValidation(console); const validationInput = mapToFlowValidation(data); @@ -10,16 +10,30 @@ const validationInput = mapToFlowValidation(data); const result = getFlowSchemas(validationInput.flow, validationInput.functions, validationInput.dataTypes) if (result) { + type SchematizedSubFlowParameter = { + id: Scalars['NodeParameterID']['output'], + inputSchema: JsonSchema, + outputSchema: JsonSchema, + } + + /** + * A sub-flow that does not fill a parameter itself but sits inline in a + * literal's `references`, addressed via `${signature}`. It is identified by + * the id of the inline reference holding it. + */ + type SchematizedSubFlowReference = { + id: Scalars['InlineReferenceValueID']['output'], + inputSchema: JsonSchema, + outputSchema: JsonSchema, + } + type MappedResult = { flow: { inputSchema: JsonSchema, outputSchema: JsonSchema, }, - subflowParameters: { - id: Scalars['NodeParameterID']['output'], - inputSchema: JsonSchema, - outputSchema: JsonSchema, - }[], + subflowParameters: SchematizedSubFlowParameter[], + subflowReferences: SchematizedSubFlowReference[], } const mappedResult: MappedResult = { @@ -28,20 +42,52 @@ if (result) { outputSchema: result.outputSchema, }, subflowParameters: [], + subflowReferences: [], } - result.nodes?.nodes?.forEach(node => - node?.parameters?.nodes?.forEach(parameter => { - const value = parameter?.value; - if (value?.__typename === 'SubFlowValue') { + const collectInlineReferences = ( + references: (InlineReferenceValue | null | undefined)[] | null | undefined, + ): void => { + references?.forEach(reference => { + const value = reference?.value; + if (!value) return; + + if (value.__typename === 'SubFlowValue') { + // `database_id` is optional in the proto. Without it the sub-flow + // cannot be addressed, and emitting a null id would make the + // consumer match the wrong rows, so it is left out entirely. + if (!reference?.id) return; + const schemaValue = value as SchematizedSubFlowValue; - mappedResult.subflowParameters.push({ - id: parameter?.id!, + mappedResult.subflowReferences.push({ + id: reference.id, inputSchema: schemaValue.inputSchema, outputSchema: schemaValue.outputSchema, }) + } else if (value.__typename === 'LiteralValue') { + collectInlineReferences(value.references) } - })) + }) + } + + const collectParameter = (parameter: NodeParameter | null | undefined): void => { + const value = parameter?.value; + if (!value) return; + + if (value.__typename === 'SubFlowValue') { + const schemaValue = value as SchematizedSubFlowValue; + mappedResult.subflowParameters.push({ + id: parameter?.id!, + inputSchema: schemaValue.inputSchema, + outputSchema: schemaValue.outputSchema, + }) + } else if (value.__typename === 'LiteralValue') { + collectInlineReferences(value.references) + } + } + + result.nodes?.nodes?.forEach(node => + node?.parameters?.nodes?.forEach(collectParameter)) console.info(JSON.stringify(mappedResult)); } else { diff --git a/entrypoint/mapper.ts b/entrypoint/mapper.ts index bab8496..bcd3039 100644 --- a/entrypoint/mapper.ts +++ b/entrypoint/mapper.ts @@ -3,6 +3,7 @@ import { DataType, Flow, FlowSetting, FunctionDefinition, + InlineReferenceValue, NodeFunction, NodeParameter, NodeParameterValue, @@ -21,6 +22,7 @@ import { SubFlow as TucanaSubFlow, SubFlowParameters as TucanaSubFlowParameters, NodeValue as TucanaNodeValue, + InlineReferenceValue as TucanaInlineReferenceValue, } from "@code0-tech/tucana/shared"; import {toAllowedValue} from "@code0-tech/tucana/helpers"; @@ -98,10 +100,7 @@ function mapNodeValue(tucanaNodeValue: TucanaNodeValue): NodeParameterValue { value = { __typename: 'LiteralValue', value: toAllowedValue(nodeValue.literalValue.value!), - references: nodeValue.literalValue.references.map(r => ({ - signature: r.signature, - value: mapNodeValue(r.value!) - })), + references: nodeValue.literalValue.references.map(mapInlineReference), }; } else if (nodeValue?.oneofKind === 'referenceValue') { const target = nodeValue.referenceValue.target; @@ -129,6 +128,17 @@ function mapNodeValue(tucanaNodeValue: TucanaNodeValue): NodeParameterValue { return value; } +function mapInlineReference(reference: TucanaInlineReferenceValue): InlineReferenceValue { + return { + __typename: 'InlineReferenceValue', + id: reference.databaseId !== undefined + ? gid('InlineReferenceValue', reference.databaseId) as InlineReferenceValue['id'] + : undefined, + signature: reference.signature, + value: mapNodeValue(reference.value!) + }; +} + function mapFunctionDefinition(functionDefinition: TucanaFunctionDefinition): FunctionDefinition { return { identifier: functionDefinition.runtimeName, diff --git a/entrypoint/package-lock.json b/entrypoint/package-lock.json index 78bb8a5..83b649a 100644 --- a/entrypoint/package-lock.json +++ b/entrypoint/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@code0-tech/triangulum": "file:../", - "@code0-tech/tucana": "0.0.83", + "@code0-tech/tucana": "0.0.85", "@protobuf-ts/runtime": "^2.11.1" }, "devDependencies": { @@ -43,9 +43,9 @@ "link": true }, "node_modules/@code0-tech/tucana": { - "version": "0.0.83", - "resolved": "https://registry.npmjs.org/@code0-tech/tucana/-/tucana-0.0.83.tgz", - "integrity": "sha512-scBOjgf3oIQF8pUlskqPHqfzPvgCJgG1+/tt9RAKWZ1IdCdSqTRG768uEVeTjpuryiGRiO0p8r4bCXNEQNRtRg==", + "version": "0.0.85", + "resolved": "https://registry.npmjs.org/@code0-tech/tucana/-/tucana-0.0.85.tgz", + "integrity": "sha512-udz6RA4GwOqXtLEHc7Ed8LRyzXczn2GiMVl7Y85rX0bdny4vp1kH4mQkPSQuppiq6tKbYfmOCu5OQr2kPFJHDQ==", "license": "MIT" }, "node_modules/@protobuf-ts/runtime": { diff --git a/entrypoint/package.json b/entrypoint/package.json index 659fad9..360521f 100644 --- a/entrypoint/package.json +++ b/entrypoint/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@code0-tech/triangulum": "file:../", - "@code0-tech/tucana": "0.0.83", + "@code0-tech/tucana": "0.0.85", "@protobuf-ts/runtime": "^2.11.1" }, "devDependencies": { diff --git a/gem/Gemfile.lock b/gem/Gemfile.lock index 05a1cea..fda1149 100644 --- a/gem/Gemfile.lock +++ b/gem/Gemfile.lock @@ -5,7 +5,7 @@ PATH base64 (~> 0.3) json (~> 2.19) open3 (~> 0.2) - tucana (~> 0.0, >= 0.0.82) + tucana (~> 0.0, >= 0.0.85) GEM remote: https://rubygems.org/ @@ -101,7 +101,7 @@ GEM rubyzip (2.4.1) stringio (3.2.0) tsort (0.2.0) - tucana (0.0.82) + tucana (0.0.85) grpc (~> 1.64) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) diff --git a/gem/lib/triangulum/flow_schema_extraction.rb b/gem/lib/triangulum/flow_schema_extraction.rb index b248284..e7b2d13 100644 --- a/gem/lib/triangulum/flow_schema_extraction.rb +++ b/gem/lib/triangulum/flow_schema_extraction.rb @@ -6,7 +6,7 @@ module Triangulum class FlowSchemaExtraction include Executor - Result = Struct.new(:flow, :subflow_parameters, keyword_init: true) + Result = Struct.new(:flow, :subflow_parameters, :subflow_references, keyword_init: true) SchematizedObject = Struct.new(:id, :input_schema, :output_schema, keyword_init: true) ENTRYPOINT = File.expand_path('js/flow-schema-extraction.js', __dir__) @@ -64,6 +64,13 @@ def parse_output(output) input_schema: param[:inputSchema], output_schema: param[:outputSchema] ) + end, + subflow_references: json[:subflowReferences].map do |reference| + SchematizedObject.new( + id: reference[:id], + input_schema: reference[:inputSchema], + output_schema: reference[:outputSchema] + ) end ) end diff --git a/gem/spec/support/protobuf_factories.rb b/gem/spec/support/protobuf_factories.rb index d273c17..aaa18c4 100644 --- a/gem/spec/support/protobuf_factories.rb +++ b/gem/spec/support/protobuf_factories.rb @@ -22,6 +22,10 @@ def literal_value(value, references = []) ) end + def inline_reference(signature:, value:, id: nil) + Tucana::Shared::InlineReferenceValue.new(database_id: id, signature: signature, value: value) + end + def reference_node(node_id: nil, input_type: nil, paths: []) Tucana::Shared::NodeValue.new( reference_value: Tucana::Shared::ReferenceValue.new( diff --git a/gem/spec/triangulum/flow_schema_extraction_spec.rb b/gem/spec/triangulum/flow_schema_extraction_spec.rb index 6734739..5862d7c 100644 --- a/gem/spec/triangulum/flow_schema_extraction_spec.rb +++ b/gem/spec/triangulum/flow_schema_extraction_spec.rb @@ -26,6 +26,7 @@ output_schema: { type: 'null' } ) expect(result.subflow_parameters).to be_empty + expect(result.subflow_references).to be_empty end it 'extracts subflow schemas' do @@ -74,6 +75,107 @@ output_schema: { type: 'null' } ) ) + expect(result.subflow_references).to be_empty + end + + it 'extracts schemas of sub-flows sitting inline in a literal' do + # `tools: ["${item_0}"]` -- a list of sub-flows can only be filled through a + # literal carrying inline references, so the sub-flow has no parameter slot of + # its own and is addressed by the id of the inline reference holding it. + tools_function = Tucana::Shared::FunctionDefinition.new( + runtime_name: 'custom::ai::agent', + signature: '(tools: LIST>): void' + ) + + flow = build_flow( + node_functions: [ + node( + id: 1, + function_id: 'custom::ai::agent', + parameters: [ + param( + id: 1, + runtime_parameter_id: 'tools', + value: literal_value( + ['${item_0}'], + [inline_reference(id: 7, signature: 'item_0', value: node_function_value(2))] + ) + ) + ] + ), + node( + id: 2, + function_id: 'std::math::add', + parameters: [ + param(id: 2, runtime_parameter_id: 'a', value: literal_value(1)), + param(id: 3, runtime_parameter_id: 'b', value: reference_node( + input_type: reference_input_type(node_id: 1, parameter_index: 0, input_index: 0) + )) + ] + ) + ] + ) + + result = described_class.new(flow, functions + [tools_function], data_types).extract + + expect(result.subflow_parameters).to be_empty + expect(result.subflow_references).to contain_exactly( + described_class::SchematizedObject.new( + id: 'gid://sagittarius/InlineReferenceValue/7', + input_schema: { + type: 'object', + properties: { + item: { + type: 'number' + } + }, + required: ['item'], + additionalProperties: false + }, + output_schema: { type: 'null' } + ) + ) + end + + it 'skips inline sub-flows whose reference carries no database id' do + # `database_id` is optional in the proto. An inline sub-flow without one cannot + # be addressed by the consumer, so it is left out rather than reported with a + # null id, which would match the wrong rows. + tools_function = Tucana::Shared::FunctionDefinition.new( + runtime_name: 'custom::ai::agent', + signature: '(tools: LIST>): void' + ) + + flow = build_flow( + node_functions: [ + node( + id: 1, + function_id: 'custom::ai::agent', + parameters: [ + param( + id: 1, + runtime_parameter_id: 'tools', + value: literal_value( + ['${item_0}'], + [inline_reference(signature: 'item_0', value: node_function_value(2))] + ) + ) + ] + ), + node( + id: 2, + function_id: 'std::math::add', + parameters: [ + param(id: 2, runtime_parameter_id: 'a', value: literal_value(1)), + param(id: 3, runtime_parameter_id: 'b', value: literal_value(2)) + ] + ) + ] + ) + + result = described_class.new(flow, functions + [tools_function], data_types).extract + + expect(result.subflow_references).to be_empty end end end diff --git a/gem/triangulum.gemspec b/gem/triangulum.gemspec index 7553642..b9db339 100644 --- a/gem/triangulum.gemspec +++ b/gem/triangulum.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'base64', '~> 0.3' spec.add_dependency 'json', '~> 2.19' spec.add_dependency 'open3', '~> 0.2' - spec.add_dependency 'tucana', '~> 0.0', '>= 0.0.82' + spec.add_dependency 'tucana', '~> 0.0', '>= 0.0.85' spec.add_development_dependency 'irb', '~> 1.17' spec.add_development_dependency 'rake', '~> 13.0'