Compare commits
4 Commits
5b9920449d
...
e798332828
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e798332828 | ||
|
|
b0d41e0d88 | ||
|
|
02b2d44ccc | ||
|
|
47de3cc376 |
@@ -30,6 +30,7 @@
|
||||
<curator.version>5.1.0</curator.version>
|
||||
<hutool.version>5.8.27</hutool.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<liteflow.version>2.13.2</liteflow.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -153,12 +154,27 @@
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-spring-boot-starter</artifactId>
|
||||
<version>2.13.2</version>
|
||||
<version>${liteflow.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-el-builder</artifactId>
|
||||
<version>2.13.2</version>
|
||||
<version>${liteflow.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-graaljs</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-python</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-lua</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.noear</groupId>
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.freemarker</groupId>
|
||||
<artifactId>freemarker</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.blinkfox</groupId>
|
||||
<artifactId>fenix-spring-boot-starter</artifactId>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.lanyuanxiaoyao.service.ai.web.engine;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext;
|
||||
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowEdge;
|
||||
@@ -47,6 +48,9 @@ public final class FlowGraphRunner {
|
||||
}
|
||||
while (!executionQueue.isEmpty()) {
|
||||
var node = executionQueue.poll();
|
||||
if (ObjectUtil.isNull(node)) {
|
||||
continue;
|
||||
}
|
||||
process(node, flowContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.lanyuanxiaoyao.service.ai.web.engine;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.template.TemplateEngine;
|
||||
import cn.hutool.extra.template.TemplateUtil;
|
||||
import com.lanyuanxiaoyao.service.ai.web.engine.entity.FlowContext;
|
||||
import java.util.Map;
|
||||
import org.eclipse.collections.api.factory.Maps;
|
||||
@@ -11,6 +14,8 @@ import org.eclipse.collections.api.map.ImmutableMap;
|
||||
* @version 20250711
|
||||
*/
|
||||
public class FlowHelper {
|
||||
private static final TemplateEngine TEMPLATE_ENGINE = TemplateUtil.createEngine();
|
||||
|
||||
public static ImmutableMap<String, Object> generateInputVariablesMap(String nodeId, FlowContext context) {
|
||||
var variableMap = Maps.mutable.<String, Object>empty();
|
||||
var currentNodeData = context.get(nodeId);
|
||||
@@ -18,27 +23,39 @@ public class FlowHelper {
|
||||
var inputsMap = (Map<String, Map<String, String>>) currentNodeData.get("inputs");
|
||||
for (String variableName : inputsMap.keySet()) {
|
||||
var expression = inputsMap.get(variableName).get("variable");
|
||||
if (StrUtil.contains(expression, ".")) {
|
||||
var splits = StrUtil.splitTrim(expression, ".", 2);
|
||||
var targetNodeId = splits.get(0);
|
||||
var targetVariableName = splits.get(1);
|
||||
if (!context.getData().containsKey(targetNodeId)) {
|
||||
throw new RuntimeException(StrUtil.format("Target node id not found: {}", targetNodeId));
|
||||
}
|
||||
var targetNodeData = context.getData().get(targetNodeId);
|
||||
if (!targetNodeData.containsKey(targetVariableName)) {
|
||||
throw new RuntimeException(StrUtil.format("Target node variable not found: {}.{}", targetNodeId, targetVariableName));
|
||||
}
|
||||
var targetVariable = targetNodeData.get(targetVariableName);
|
||||
var targetVariable = generateVariable(expression, context);
|
||||
if (ObjectUtil.isNotNull(targetVariable)) {
|
||||
variableMap.put(variableName, targetVariable);
|
||||
} else if (context.getInput().containsKey(expression)) {
|
||||
if (!context.getInput().containsKey(variableName)) {
|
||||
throw new RuntimeException(StrUtil.format("Target variable not found in input {}", variableName));
|
||||
}
|
||||
variableMap.put(variableName, context.getInput().get(variableName));
|
||||
}
|
||||
}
|
||||
}
|
||||
return variableMap.toImmutable();
|
||||
}
|
||||
|
||||
public static Object generateVariable(String expression, FlowContext context) {
|
||||
if (StrUtil.contains(expression, ".")) {
|
||||
var splits = StrUtil.splitTrim(expression, ".", 2);
|
||||
var targetNodeId = splits.get(0);
|
||||
var targetVariableName = splits.get(1);
|
||||
if (!context.getData().containsKey(targetNodeId)) {
|
||||
throw new RuntimeException(StrUtil.format("Target node id not found: {}", targetNodeId));
|
||||
}
|
||||
var targetNodeData = context.getData().get(targetNodeId);
|
||||
if (!targetNodeData.containsKey(targetVariableName)) {
|
||||
throw new RuntimeException(StrUtil.format("Target node variable not found: {}.{}", targetNodeId, targetVariableName));
|
||||
}
|
||||
return targetNodeData.get(targetVariableName);
|
||||
} else if (context.getInput().containsKey(expression)) {
|
||||
if (!context.getInput().containsKey(expression)) {
|
||||
throw new RuntimeException(StrUtil.format("Target variable not found in input {}", expression));
|
||||
}
|
||||
return context.getInput().get(expression);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String renderTemplateText(String templateText, Map<?, ?> data) {
|
||||
var template = TEMPLATE_ENGINE.getTemplate(templateText);
|
||||
return template.render(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.lanyuanxiaoyao.service.ai.web.engine.node;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.lanyuanxiaoyao.service.ai.web.configuration.SpringBeanGetter;
|
||||
import com.lanyuanxiaoyao.service.ai.web.engine.FlowHelper;
|
||||
import com.lanyuanxiaoyao.service.ai.web.engine.FlowNodeRunner;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
@@ -14,10 +17,9 @@ public class LlmNode extends FlowNodeRunner {
|
||||
public void run() {
|
||||
var variableMap = FlowHelper.generateInputVariablesMap(getNodeId(), getContext());
|
||||
log.info("Variable map: {}", variableMap);
|
||||
setData("text", "llm");
|
||||
|
||||
/* var prompt = (String) getData("systemPrompt");
|
||||
if (StrUtil.isNotBlank(prompt)) {
|
||||
var sourcePrompt = (String) getData("systemPrompt");
|
||||
if (StrUtil.isNotBlank(sourcePrompt)) {
|
||||
var prompt = FlowHelper.renderTemplateText(sourcePrompt, variableMap.toMap());
|
||||
var builder = SpringBeanGetter.getBean("chat", ChatClient.Builder.class);
|
||||
var client = builder.build();
|
||||
var content = client.prompt()
|
||||
@@ -25,6 +27,6 @@ public class LlmNode extends FlowNodeRunner {
|
||||
.call()
|
||||
.content();
|
||||
setData("text", content);
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public class OutputNode extends FlowNodeRunner {
|
||||
@Override
|
||||
public void run() {
|
||||
var variableMap = FlowHelper.generateInputVariablesMap(getNodeId(), getContext());
|
||||
log.info("Variable map: {}", variableMap);
|
||||
String expression = getData("output");
|
||||
var targetVariable = FlowHelper.generateVariable(expression, getContext());
|
||||
log.info("Target: {}", targetVariable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {type Edge, getIncomers, type Node} from '@xyflow/react'
|
||||
import {find, has, isEqual, unique} from 'licia'
|
||||
import type {Option} from 'amis/lib/Schema'
|
||||
import {find, has, isEmpty, isEqual, unique} from 'licia'
|
||||
import Queue from 'yocto-queue'
|
||||
import type {InputFormOptions, InputFormOptionsGroup} from './types.ts'
|
||||
|
||||
export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => {
|
||||
let queue = new Queue<Node>()
|
||||
@@ -36,3 +38,47 @@ export const getAllIncomerNodeOutputVariables: (id: string, nodes: Node[], edges
|
||||
}
|
||||
return incomerVariables
|
||||
}
|
||||
|
||||
export const generateAllIncomerOutputVariablesFormOptions: (id: string, inputSchema: Record<string, Record<string, any>>, nodes: Node[], edges: Edge[], data: any) => Option[] = (id, inputSchema, nodes, edges, data) => {
|
||||
let inputSchemaVariables: InputFormOptions[] = Object.keys(inputSchema).map(key => ({
|
||||
label: `${key} (${inputSchema[key]?.label ?? ''})`,
|
||||
value: key,
|
||||
}))
|
||||
|
||||
let incomerIds = getAllIncomerNodeById(id, nodes, edges)
|
||||
let incomerVariables: InputFormOptionsGroup[] = []
|
||||
for (const incomerId of incomerIds) {
|
||||
let nodeData = data[incomerId] ?? {}
|
||||
let group = incomerId
|
||||
if (has(nodeData, 'node') && has(nodeData.node, 'name')) {
|
||||
group = `${nodeData.node.name} ${incomerId}`
|
||||
}
|
||||
if (has(nodeData, 'outputs')) {
|
||||
let outputs = nodeData?.outputs ?? []
|
||||
incomerVariables.push({
|
||||
group: group,
|
||||
variables: Object.keys(outputs).map(key => ({
|
||||
value: `${incomerId}.${key}`,
|
||||
label: key,
|
||||
})),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let inputVariables = [
|
||||
...(isEmpty(inputSchemaVariables) ? [] : [
|
||||
{
|
||||
group: '流程入参',
|
||||
variables: inputSchemaVariables,
|
||||
},
|
||||
]),
|
||||
...incomerVariables,
|
||||
]
|
||||
|
||||
return [
|
||||
...inputVariables.map(item => ({
|
||||
label: item.group,
|
||||
children: item.variables,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,9 +6,32 @@ import KnowledgeNode from './node/KnowledgeNode.tsx'
|
||||
import LlmNode from './node/LlmNode.tsx'
|
||||
import OutputNode from './node/OutputNode.tsx'
|
||||
import SwitchNode from './node/SwitchNode.tsx'
|
||||
import TemplateNode from './node/TemplateNode.tsx'
|
||||
import type {NodeChecker} from './types.ts'
|
||||
|
||||
const inputVariableChecker: NodeChecker = (id, inputSchema, nodes, edges, data) => {
|
||||
const inputSingleVariableChecker: (field: string) => NodeChecker = field => {
|
||||
return (id, inputSchema, nodes, edges, data) => {
|
||||
let nodeData = data[id] ?? {}
|
||||
if (has(nodeData, field)) {
|
||||
let expression = nodeData?.[field] ?? ''
|
||||
if (!isEmpty(expression)) {
|
||||
let outputVariables = new Set([
|
||||
...getAllIncomerNodeOutputVariables(id, nodes, edges, data).map(i => `${i.id}.${i.variable}`),
|
||||
...Object.keys(inputSchema),
|
||||
])
|
||||
if (!outputVariables.has(expression)) {
|
||||
return {
|
||||
error: true,
|
||||
message: `节点 ${id} 存在错误:变量 ${expression} 不存在`,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {error: false}
|
||||
}
|
||||
}
|
||||
|
||||
const inputMultiVariableChecker: NodeChecker = (id, inputSchema, nodes, edges, data) => {
|
||||
let nodeData = data[id] ?? {}
|
||||
if (has(nodeData, 'inputs')) {
|
||||
let inputs = nodeData?.inputs ?? {}
|
||||
@@ -49,7 +72,7 @@ export const NodeRegistry: NodeDefine[] = [
|
||||
icon: <i className="fa fa-message"/>,
|
||||
description: '使用大模型对话',
|
||||
component: LlmNode,
|
||||
checkers: [inputVariableChecker],
|
||||
checkers: [inputMultiVariableChecker],
|
||||
},
|
||||
{
|
||||
key: 'knowledge-node',
|
||||
@@ -58,7 +81,7 @@ export const NodeRegistry: NodeDefine[] = [
|
||||
icon: <i className="fa fa-book-bookmark"/>,
|
||||
description: '',
|
||||
component: KnowledgeNode,
|
||||
checkers: [inputVariableChecker],
|
||||
checkers: [inputMultiVariableChecker],
|
||||
},
|
||||
{
|
||||
key: 'code-node',
|
||||
@@ -67,7 +90,16 @@ export const NodeRegistry: NodeDefine[] = [
|
||||
icon: <i className="fa fa-code"/>,
|
||||
description: '执行自定义的处理代码',
|
||||
component: CodeNode,
|
||||
checkers: [inputVariableChecker],
|
||||
checkers: [inputMultiVariableChecker],
|
||||
},
|
||||
{
|
||||
key: 'template-node',
|
||||
group: '普通节点',
|
||||
name: '模板替换',
|
||||
icon: <i className="fa fa-pen-nib"/>,
|
||||
description: '使用模板聚合转换变量表示',
|
||||
component: TemplateNode,
|
||||
checkers: [inputMultiVariableChecker],
|
||||
},
|
||||
{
|
||||
key: 'switch-node',
|
||||
@@ -85,7 +117,7 @@ export const NodeRegistry: NodeDefine[] = [
|
||||
icon: <i className="fa fa-file"/>,
|
||||
description: '定义输出变量',
|
||||
component: OutputNode,
|
||||
checkers: [inputVariableChecker],
|
||||
checkers: [inputSingleVariableChecker('output')],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@ import {CopyFilled, DeleteFilled, EditFilled} from '@ant-design/icons'
|
||||
import {type Edge, Handle, type Node, type NodeProps, Position} from '@xyflow/react'
|
||||
import type {Schema} from 'amis'
|
||||
import {Button, Card, Drawer} from 'antd'
|
||||
import {has, isEmpty} from 'licia'
|
||||
import {type JSX, useCallback, useState} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.tsx'
|
||||
import {getAllIncomerNodeById} from '../Helper.tsx'
|
||||
import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx'
|
||||
import {useDataStore} from '../store/DataStore.ts'
|
||||
import {useFlowStore} from '../store/FlowStore.ts'
|
||||
import type {InputFormOptions, InputFormOptionsGroup} from '../types.ts'
|
||||
|
||||
export function inputsFormColumns(
|
||||
nodeId: string,
|
||||
@@ -18,41 +16,6 @@ export function inputsFormColumns(
|
||||
edges: Edge[],
|
||||
data: any,
|
||||
): Schema[] {
|
||||
let inputSchemaVariables: InputFormOptions[] = Object.keys(inputSchema).map(key => ({
|
||||
label: `${key} (${inputSchema[key]?.label ?? ''})`,
|
||||
value: key,
|
||||
}))
|
||||
|
||||
let incomerIds = getAllIncomerNodeById(nodeId, nodes, edges)
|
||||
let incomerVariables: InputFormOptionsGroup[] = []
|
||||
for (const incomerId of incomerIds) {
|
||||
let nodeData = data[incomerId] ?? {}
|
||||
let group = incomerId
|
||||
if (has(nodeData, 'node') && has(nodeData.node, 'name')) {
|
||||
group = `${nodeData.node.name} ${incomerId}`
|
||||
}
|
||||
if (has(nodeData, 'outputs')) {
|
||||
let outputs = nodeData?.outputs ?? []
|
||||
incomerVariables.push({
|
||||
group: group,
|
||||
variables: Object.keys(outputs).map(key => ({
|
||||
value: `${incomerId}.${key}`,
|
||||
label: key,
|
||||
})),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let inputVariables = [
|
||||
...(isEmpty(inputSchemaVariables) ? [] : [
|
||||
{
|
||||
group: '流程入参',
|
||||
variables: inputSchemaVariables,
|
||||
},
|
||||
]),
|
||||
...incomerVariables,
|
||||
]
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'input-kvs',
|
||||
@@ -72,12 +35,13 @@ export function inputsFormColumns(
|
||||
label: '变量',
|
||||
required: true,
|
||||
selectMode: 'group',
|
||||
options: [
|
||||
...inputVariables.map(item => ({
|
||||
label: item.group,
|
||||
children: item.variables,
|
||||
})),
|
||||
],
|
||||
options: generateAllIncomerOutputVariablesFormOptions(
|
||||
nodeId,
|
||||
inputSchema,
|
||||
nodes,
|
||||
edges,
|
||||
data,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
import type {NodeProps} from '@xyflow/react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx'
|
||||
import {useContextStore} from '../store/ContextStore.ts'
|
||||
import {useDataStore} from '../store/DataStore.ts'
|
||||
import {useFlowStore} from '../store/FlowStore.ts'
|
||||
import AmisNode, {EndNodeHandler, inputsFormColumns} from './AmisNode.tsx'
|
||||
import AmisNode, {EndNodeHandler} from './AmisNode.tsx'
|
||||
|
||||
const OutputNode = (props: NodeProps) => {
|
||||
const {getNodes, getEdges} = useFlowStore()
|
||||
const {getData} = useDataStore()
|
||||
const {getInputSchema} = useContextStore()
|
||||
|
||||
const columnsSchema = useCallback(
|
||||
() => inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()),
|
||||
() => [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'output',
|
||||
label: '输出变量',
|
||||
required: true,
|
||||
selectMode: 'group',
|
||||
options: generateAllIncomerOutputVariablesFormOptions(
|
||||
props.id,
|
||||
getInputSchema(),
|
||||
getNodes(),
|
||||
getEdges(),
|
||||
getData(),
|
||||
),
|
||||
},
|
||||
],
|
||||
[props.id],
|
||||
)
|
||||
|
||||
|
||||
79
service-web/client/src/components/flow/node/TemplateNode.tsx
Normal file
79
service-web/client/src/components/flow/node/TemplateNode.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import type {NodeProps} from '@xyflow/react'
|
||||
import {Tag} from 'antd'
|
||||
import React, {useCallback} from 'react'
|
||||
import {useContextStore} from '../store/ContextStore.ts'
|
||||
import {useDataStore} from '../store/DataStore.ts'
|
||||
import {useFlowStore} from '../store/FlowStore.ts'
|
||||
import AmisNode, {EndNodeHandler, inputsFormColumns} from './AmisNode.tsx'
|
||||
|
||||
const typeMap: Record<string, string> = {
|
||||
default: '默认',
|
||||
json: 'JSON',
|
||||
'template-markdown': 'Markdown',
|
||||
'template-rich-text': '富文本',
|
||||
}
|
||||
|
||||
const TemplateNode = (props: NodeProps) => {
|
||||
const {getNodes, getEdges} = useFlowStore()
|
||||
const {getData, getDataById} = useDataStore()
|
||||
const {getInputSchema} = useContextStore()
|
||||
|
||||
const nodeData = getDataById(props.id)
|
||||
|
||||
const columnsSchema = useCallback(
|
||||
() => [
|
||||
...inputsFormColumns(props.id, getInputSchema(), getNodes(), getEdges(), getData()),
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'type',
|
||||
label: '模板类型',
|
||||
required: true,
|
||||
selectFirst: true,
|
||||
options: Object.keys(typeMap).map(key => ({label: typeMap[key], value: key})),
|
||||
},
|
||||
{
|
||||
visibleOn: 'type === \'template-markdown\'',
|
||||
type: 'editor',
|
||||
required: true,
|
||||
label: '模板内容',
|
||||
name: 'template',
|
||||
language: 'markdown',
|
||||
options: {
|
||||
wordWrap: 'bounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
visibleOn: 'type === \'template-rich-text\'',
|
||||
type: 'input-rich-text',
|
||||
required: true,
|
||||
name: 'template',
|
||||
label: '模板内容',
|
||||
options: {
|
||||
min_height: 500,
|
||||
},
|
||||
},
|
||||
],
|
||||
[props.id],
|
||||
)
|
||||
|
||||
return (
|
||||
<AmisNode
|
||||
nodeProps={props}
|
||||
extraNodeDescription={
|
||||
nodeData?.type
|
||||
? <div className="mt-2 flex justify-between">
|
||||
<span>模板类型</span>
|
||||
<Tag className="m-0" color="blue">{typeMap[nodeData.type]}</Tag>
|
||||
</div>
|
||||
: <></>
|
||||
}
|
||||
columnSchema={columnsSchema}
|
||||
handler={<EndNodeHandler/>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TemplateNode)
|
||||
Reference in New Issue
Block a user