From 4542346f1816eee49ea92a24df6f7dd9002ab9f7 Mon Sep 17 00:00:00 2001 From: balibabu Date: Mon, 1 Jul 2024 10:27:32 +0800 Subject: [PATCH] feat: get the operator type from id #918 (#1323) ### What problem does this PR solve? feat: get the operator type from id #918 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/pages/flow/headhunter_zh.json | 483 ++++++++++++++++++++++++++ web/src/pages/flow/hooks.ts | 6 +- web/src/pages/flow/list/hooks.ts | 9 +- web/src/pages/flow/store.ts | 11 +- web/src/pages/flow/utils.test.ts | 19 + web/src/pages/flow/utils.ts | 8 +- 6 files changed, 521 insertions(+), 15 deletions(-) create mode 100644 web/src/pages/flow/headhunter_zh.json diff --git a/web/src/pages/flow/headhunter_zh.json b/web/src/pages/flow/headhunter_zh.json new file mode 100644 index 000000000..bcca0af95 --- /dev/null +++ b/web/src/pages/flow/headhunter_zh.json @@ -0,0 +1,483 @@ +{ + "edges": [ + { + "id": "1a75afcd-05ea-4bf1-af5e-adff4f870100", + "label": "", + "source": "begin", + "target": "answer:0", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "a88e567e-1c23-457f-a0db-a90f1ea14d21", + "label": "", + "source": "message:reject", + "target": "answer:0", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "ad5fdb81-568e-46c5-82b1-1f50b625ae23", + "label": "", + "source": "answer:0", + "target": "categorize:0", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "fbb4c147-0d4c-4dd2-9264-3088e02c40d1", + "label": "", + "source": "categorize:0", + "target": "message:introduction", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "40da6d94-7d51-4d77-a5dd-1d583daba147", + "label": "", + "source": "categorize:0", + "target": "generate:casual", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "e4c7f5dc-5ccb-45da-af0d-4fbcb9dbb6f9", + "label": "", + "source": "categorize:0", + "target": "message:reject", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "a8c0b4e5-9597-4335-bded-5b1dd46f7507", + "label": "", + "source": "categorize:0", + "target": "retrieval:0", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "223972c4-08f7-47ae-b9ce-9a52dba044a7", + "label": "", + "source": "message:introduction", + "target": "answer:1", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "ece123ba-a84c-41d6-a843-63df331f945e", + "label": "", + "source": "generate:aboutJob", + "target": "answer:1", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "fa07af7d-29de-4066-89b7-f1bee184e260", + "label": "", + "source": "generate:casual", + "target": "answer:1", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "0d7565d8-673c-4a5e-90f4-f4296384eef0", + "label": "", + "source": "generate:get_wechat", + "target": "answer:1", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "e615e3d2-8ba5-448f-b001-ab926078cca6", + "label": "", + "source": "generate:nowechat", + "target": "answer:1", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "a9cf0f0a-77b5-42a2-a71f-e71086a525f0", + "label": "", + "source": "answer:1", + "target": "categorize:1", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "4e4c5461-cdc6-46c1-afb1-5892cb54104a", + "label": "", + "source": "answer:0", + "target": "categorize:1", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "44c8cb42-a2d9-43df-83bb-175d4870a027", + "label": "", + "source": "categorize:1", + "target": "retrieval:0", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "86d2308d-c91a-4a21-a6d6-081c92fc517c", + "label": "", + "source": "categorize:1", + "target": "generate:casual", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "e5de8fc4-b801-44cf-8240-02f2aac6ae23", + "label": "", + "source": "categorize:1", + "target": "generate:get_wechat", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "a9511914-d696-46b4-90c2-a3a940205b5c", + "label": "", + "source": "categorize:1", + "target": "generate:nowechat", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "07db19d7-e356-4c24-8e0d-353b1e6e6ee4", + "label": "", + "source": "retrieval:0", + "target": "generate:aboutJob", + "markerEnd": { + "type": "arrow" + } + }, + { + "id": "e17eb04f-c773-44b2-8a7b-7f770185c4fa", + "label": "", + "source": "relevant:0", + "target": "generate:aboutJob", + "markerEnd": { + "type": "arrow" + } + } + ], + "nodes": [ + { + "id": "begin", + "type": "beginNode", + "position": { + "x": 0, + "y": 0 + }, + "data": { + "label": "Begin", + "params": { + "prologue": "您好!我是AGI方向的猎头,了解到您是这方面的大佬,然后冒昧的就联系到您。这边有个机会想和您分享,RAGFlow正在招聘您这个岗位的资深的工程师不知道您那边是不是感兴趣?" + }, + "downstream": ["answer:0"], + "upstream": [] + }, + "sourcePosition": "left", + "targetPosition": "right" + }, + { + "id": "answer:0", + "type": "ragNode", + "position": { + "x": 0, + "y": 0 + }, + "data": { + "label": "Answer", + "params": {}, + "downstream": ["categorize:0"], + "upstream": ["begin", "message:reject"] + }, + "sourcePosition": "left", + "targetPosition": "right" + }, + { + "id": "categorize:0", + "type": "categorizeNode", + "position": { + "x": 0, + "y": 0 + }, + "data": { + "label": "Categorize", + "params": { + "llm_id": "deepseek-chat", + "category_description": { + "about_job": { + "description": "该问题关于职位本身或公司的信息。", + "examples": "什么岗位?\n汇报对象是谁?\n公司多少人?\n公司有啥产品?\n具体工作内容是啥?\n地点哪里?\n双休吗?", + "to": "retrieval:0" + }, + "casual": { + "description": "该问题不关于职位本身或公司的信息,属于闲聊。", + "examples": "你好\n好久不见\n你男的女的?\n你是猴子派来的救兵吗?\n上午开会了?\n你叫啥?\n最近市场如何?生意好做吗?", + "to": "generate:casual" + }, + "interested": { + "description": "该回答表示他对于该职位感兴趣。", + "examples": "嗯\n说吧\n说说看\n还好吧\n是的\n哦\nyes\n具体说说", + "to": "message:introduction" + }, + "answer": { + "description": "该回答表示他对于该职位不感兴趣,或感觉受到骚扰。", + "examples": "不需要\n不感兴趣\n暂时不看\n不要\nno\n我已经不干这个了\n我不是这个方向的", + "to": "message:reject" + } + } + }, + "downstream": [ + "message:introduction", + "generate:casual", + "message:reject", + "retrieval:0" + ], + "upstream": ["answer:0"] + }, + "sourcePosition": "left", + "targetPosition": "right" + }, + { + "id": "message:introduction", + "type": "ragNode", + "position": { + "x": 0, + "y": 0 + }, + "data": { + "label": "Message", + "params": { + "messages": [ + "我简单介绍以下:\nRAGFlow 是一款基于深度文档理解构建的开源 RAG(Retrieval-Augmented Generation)引擎。RAGFlow 可以为各种规模的企业及个人提供一套精简的 RAG 工作流程,结合大语言模型(LLM)针对用户各类不同的复杂格式数据提供可靠的问答以及有理有据的引用。https://github.com/infiniflow/ragflow\n您那边还有什么要了解的?" + ] + }, + "downstream": ["answer:1"], + "upstream": ["categorize:0"] + }, + "sourcePosition": "left", + "targetPosition": "right" + }, + { + "id": "answer:1", + "type": "ragNode", + "position": { + "x": 0, + "y": 0 + }, + "data": { + "label": "Answer", + "params": {}, + "downstream": ["categorize:1"], + "upstream": [ + "message:introduction", + "generate:aboutJob", + "generate:casual", + "generate:get_wechat", + "generate:nowechat" + ] + }, + "sourcePosition": "left", + "targetPosition": "right" + }, + { + "id": "categorize:1", + "type": "categorizeNode", + "position": { + "x": 0, + "y": 0 + }, + "data": { + "label": "Categorize", + "params": { + "llm_id": "deepseek-chat", + "category_description": { + "about_job": { + "description": "该问题关于职位本身或公司的信息。", + "examples": "什么岗位?\n汇报对象是谁?\n公司多少人?\n公司有啥产品?\n具体工作内容是啥?\n地点哪里?\n双休吗?", + "to": "retrieval:0" + }, + "casual": { + "description": "该问题不关于职位本身或公司的信息,属于闲聊。", + "examples": "你好\n好久不见\n你男的女的?\n你是猴子派来的救兵吗?\n上午开会了?\n你叫啥?\n最近市场如何?生意好做吗?", + "to": "generate:casual" + }, + "wechat": { + "description": "该回答表示他愿意加微信,或者已经报了微信号。", + "examples": "嗯\n可以\n是的\n哦\nyes\n15002333453\nwindblow_2231", + "to": "generate:get_wechat" + }, + "giveup": { + "description": "该回答表示他不愿意加微信。", + "examples": "不需要\n不感兴趣\n暂时不看\n不要\nno\n不方便\n不知道还要加我微信", + "to": "generate:nowechat" + } + }, + "message_history_window_size": 8 + }, + "downstream": [ + "retrieval:0", + "generate:casual", + "generate:get_wechat", + "generate:nowechat" + ], + "upstream": ["answer:0"] + }, + "sourcePosition": "left", + "targetPosition": "right" + }, + { + "id": "generate:casual", + "type": "ragNode", + "position": { + "x": 0, + "y": 0 + }, + "data": { + "label": "Generate", + "params": { + "llm_id": "deepseek-chat", + "prompt": "你是AGI方向的猎头,现在候选人的聊了和职位无关的话题,请耐心的回应候选人,并将话题往该AGI的职位上带,最好能要到候选人微信号以便后面保持联系。", + "temperature": 0.9, + "message_history_window_size": 12, + "cite": false + }, + "downstream": ["answer:1"], + "upstream": ["categorize:0", "categorize:1"] + }, + "sourcePosition": "left", + "targetPosition": "right" + }, + { + "id": "retrieval:0", + "type": "ragNode", + "position": { + "x": 0, + "y": 0 + }, + "data": { + "label": "Retrieval", + "params": { + "similarity_threshold": 0.2, + "keywords_similarity_weight": 0.3, + "top_n": 6, + "top_k": 1024, + "rerank_id": "BAAI/bge-reranker-v2-m3", + "kb_ids": ["869a236818b811ef91dffa163e197198"] + }, + "downstream": ["generate:aboutJob"], + "upstream": ["categorize:0", "categorize:1"] + }, + "sourcePosition": "left", + "targetPosition": "right" + }, + { + "id": "generate:aboutJob", + "type": "ragNode", + "position": { + "x": 0, + "y": 0 + }, + "data": { + "label": "Generate", + "params": { + "llm_id": "deepseek-chat", + "prompt": "你是AGI方向的猎头,候选人问了有关职位或公司的问题,你根据以下职位信息回答。如果职位信息中不包含候选人的问题就回答不清楚、不知道、有待确认等。回答完后引导候选人加微信号,如:\n - 方便加一下微信吗,我把JD发您看看?\n - 微信号多少,我把详细职位JD发您?\n 职位信息如下:\n {input}\n 职位信息如上。", + "temperature": 0.02 + }, + "downstream": ["answer:1"], + "upstream": ["relevant:0"] + }, + "sourcePosition": "left", + "targetPosition": "right" + }, + { + "id": "generate:get_wechat", + "type": "ragNode", + "position": { + "x": 0, + "y": 0 + }, + "data": { + "label": "Generate", + "params": { + "llm_id": "deepseek-chat", + "prompt": "你是AGI方向的猎头,候选人表示不反感加微信,如果对方已经报了微信号,表示感谢和信任并表示马上会加上;如果没有,则问对方微信号多少。你的微信号是weixin_kevin,E-mail是kkk@ragflow.com。说话不要重复。不要总是您好。", + "temperature": 0.1, + "message_history_window_size": 12, + "cite": false + }, + "downstream": ["answer:1"], + "upstream": ["categorize:1"] + }, + "sourcePosition": "left", + "targetPosition": "right" + }, + { + "id": "generate:nowechat", + "type": "ragNode", + "position": { + "x": 0, + "y": 0 + }, + "data": { + "label": "Generate", + "params": { + "llm_id": "deepseek-chat", + "prompt": "你是AGI方向的猎头,当你提出加微信时对方表示拒绝。你需要耐心礼貌的回应候选人,表示对于保护隐私信息给予理解,也可以询问他对该职位的看法和顾虑。并在恰当的时机再次询问微信联系方式。也可以鼓励候选人主动与你取得联系。你的微信号是weixin_kevin,E-mail是kkk@ragflow.com。说话不要重复。不要总是您好。", + "temperature": 0.1, + "message_history_window_size": 12, + "cite": false + }, + "downstream": ["answer:1"], + "upstream": ["categorize:1"] + }, + "sourcePosition": "left", + "targetPosition": "right" + }, + { + "id": "message:reject", + "type": "ragNode", + "position": { + "x": 0, + "y": 0 + }, + "data": { + "label": "Message", + "params": { + "messages": [ + "好的,祝您生活愉快,工作顺利。", + "哦,好的,感谢您宝贵的时间!" + ] + }, + "downstream": ["answer:0"], + "upstream": ["categorize:0"] + }, + "sourcePosition": "left", + "targetPosition": "right" + } + ] +} diff --git a/web/src/pages/flow/hooks.ts b/web/src/pages/flow/hooks.ts index 0b2d9ff49..6e222253c 100644 --- a/web/src/pages/flow/hooks.ts +++ b/web/src/pages/flow/hooks.ts @@ -27,7 +27,7 @@ import { humanId } from 'human-id'; import { useParams } from 'umi'; import { NodeMap, Operator, RestrictedUpstreamMap } from './constant'; import useGraphStore, { RFState } from './store'; -import { buildDslComponentsByGraph, getOperatorTypeFromId } from './utils'; +import { buildDslComponentsByGraph } from './utils'; const selector = (state: RFState) => ({ nodes: state.nodes, @@ -249,7 +249,7 @@ export const useSetLlmSetting = (form?: FormInstance) => { }; export const useValidateConnection = () => { - const edges = useGraphStore((state) => state.edges); + const { edges, getOperatorTypeFromId } = useGraphStore((state) => state); // restricted lines cannot be connected successfully. const isValidConnection = useCallback( (connection: Connection) => { @@ -265,7 +265,7 @@ export const useValidateConnection = () => { ]?.every((x) => x !== getOperatorTypeFromId(connection.target)); return ret; }, - [edges], + [edges, getOperatorTypeFromId], ); return isValidConnection; diff --git a/web/src/pages/flow/list/hooks.ts b/web/src/pages/flow/list/hooks.ts index 96537d37c..41172071d 100644 --- a/web/src/pages/flow/list/hooks.ts +++ b/web/src/pages/flow/list/hooks.ts @@ -2,7 +2,9 @@ import { useSetModalState } from '@/hooks/commonHooks'; import { useFetchFlowList, useSetFlow } from '@/hooks/flow-hooks'; import { useCallback, useState } from 'react'; import { useNavigate } from 'umi'; -import { dsl } from '../mock'; +// import { dsl } from '../mock'; +import headhunterZhComponents from '../../../../../graph/test/dsl_examples/headhunter_zh.json'; +import headhunter_zh from '../headhunter_zh.json'; export const useFetchDataOnMount = () => { const { data, loading } = useFetchFlowList(); @@ -22,7 +24,10 @@ export const useSaveFlow = () => { const onFlowOk = useCallback( async (title: string) => { - const ret = await setFlow({ title, dsl }); + const ret = await setFlow({ + title, + dsl: { ...headhunterZhComponents, graph: headhunter_zh }, + }); if (ret?.retcode === 0) { hideFlowSettingModal(); diff --git a/web/src/pages/flow/store.ts b/web/src/pages/flow/store.ts index f20cc3134..a438af070 100644 --- a/web/src/pages/flow/store.ts +++ b/web/src/pages/flow/store.ts @@ -20,7 +20,6 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { Operator } from './constant'; import { NodeData } from './interface'; -import { getOperatorTypeFromId } from './utils'; export type RFState = { nodes: Node[]; @@ -35,7 +34,7 @@ export type RFState = { updateNodeForm: (nodeId: string, values: any) => void; onSelectionChange: OnSelectionChangeFunc; addNode: (nodes: Node) => void; - getNode: (id?: string) => Node | undefined; + getNode: (id?: string | null) => Node | undefined; addEdge: (connection: Connection) => void; getEdge: (id: string) => Edge | undefined; deletePreviousEdgeOfClassificationNode: (connection: Connection) => void; @@ -46,6 +45,7 @@ export type RFState = { deleteEdgeBySourceAndSourceHandle: (connection: Partial) => void; findNodeByName: (operatorName: Operator) => Node | undefined; updateMutableNodeFormItem: (id: string, field: string, value: any) => void; + getOperatorTypeFromId: (id?: string | null) => string | undefined; }; // this is our useStore hook that we can use in our components to get parts of the store and call actions @@ -87,9 +87,12 @@ const useGraphStore = create()( addNode: (node: Node) => { set({ nodes: get().nodes.concat(node) }); }, - getNode: (id?: string) => { + getNode: (id?: string | null) => { return get().nodes.find((x) => x.id === id); }, + getOperatorTypeFromId: (id?: string | null) => { + return get().getNode(id)?.data?.label; + }, addEdge: (connection: Connection) => { set({ edges: addEdge(connection, get().edges), @@ -101,7 +104,7 @@ const useGraphStore = create()( }, deletePreviousEdgeOfClassificationNode: (connection: Connection) => { // Delete the edge on the classification node anchor when the anchor is connected to other nodes - const { edges } = get(); + const { edges, getOperatorTypeFromId } = get(); if (getOperatorTypeFromId(connection.source) === Operator.Categorize) { const previousEdge = edges.find( (x) => diff --git a/web/src/pages/flow/utils.test.ts b/web/src/pages/flow/utils.test.ts index 3f93758d0..3bc77b1d3 100644 --- a/web/src/pages/flow/utils.test.ts +++ b/web/src/pages/flow/utils.test.ts @@ -1,3 +1,6 @@ +import fs from 'fs'; +import path from 'path'; +import headhunter_zh from '../../../../graph/test/dsl_examples/headhunter_zh.json'; import { dsl } from './mock'; import { buildNodesAndEdgesFromDSLComponents } from './utils'; @@ -28,3 +31,19 @@ test('buildNodesAndEdgesFromDSLComponents', () => { ]), ); }); + +test('build nodes and edges from dsl', () => { + const { edges, nodes } = buildNodesAndEdgesFromDSLComponents( + headhunter_zh.components, + ); + try { + fs.writeFileSync( + path.join(__dirname, 'headhunter_zh.json'), + JSON.stringify({ edges, nodes }, null, 4), + ); + console.log('JSON data is saved.'); + } catch (error) { + console.warn(error); + } + expect(nodes.length).toEqual(12); +}); diff --git a/web/src/pages/flow/utils.ts b/web/src/pages/flow/utils.ts index e1e1e15cb..4a0faf2e7 100644 --- a/web/src/pages/flow/utils.ts +++ b/web/src/pages/flow/utils.ts @@ -5,7 +5,7 @@ import { curry, isEmpty } from 'lodash'; import pipe from 'lodash/fp/pipe'; import { Edge, MarkerType, Node, Position } from 'reactflow'; import { v4 as uuidv4 } from 'uuid'; -import { Operator, initialFormValuesMap } from './constant'; +import { NodeMap, Operator, initialFormValuesMap } from './constant'; import { NodeData } from './interface'; const buildEdges = ( @@ -41,7 +41,7 @@ export const buildNodesAndEdgesFromDSLComponents = (data: DSLComponents) => { const upstream = [...value.upstream]; nodes.push({ id: key, - type: 'ragNode', + type: NodeMap[value.obj.component_name as Operator] || 'ragNode', position: { x: 0, y: 0 }, data: { label: value.obj.component_name, @@ -162,7 +162,3 @@ export const buildDslComponentsByGraph = ( return components; }; - -export const getOperatorTypeFromId = (id: string | null) => { - return id?.split(':')[0] as Operator | undefined; -};