feat: llm support jinja fe (#4260)

This commit is contained in:
Joel
2024-05-10 18:14:05 +08:00
committed by GitHub
parent 6b99075dc8
commit 01555463d2
21 changed files with 621 additions and 177 deletions

View File

@@ -3,33 +3,28 @@ import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useBoolean } from 'ahooks'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import type { Props as EditorProps } from '.'
import Editor from '.'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import type { Variable } from '@/app/components/workflow/types'
import type { NodeOutPutVar, Variable } from '@/app/components/workflow/types'
const TO_WINDOW_OFFSET = 8
type Props = {
nodeId: string
availableVars: NodeOutPutVar[]
varList: Variable[]
onAddVar: (payload: Variable) => void
onAddVar?: (payload: Variable) => void
} & EditorProps
const CodeEditor: FC<Props> = ({
nodeId,
availableVars,
varList,
onAddVar,
...editorProps
}) => {
const { t } = useTranslation()
const { availableVars } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: () => true,
})
const isLeftBraceRef = useRef(false)
const editorRef = useRef(null)
@@ -76,7 +71,8 @@ const CodeEditor: FC<Props> = ({
if (popupPosition.y + height > window.innerHeight - TO_WINDOW_OFFSET)
newPopupPosition.y = window.innerHeight - height - TO_WINDOW_OFFSET
setPopupPosition(newPopupPosition)
if (newPopupPosition.x !== popupPosition.x || newPopupPosition.y !== popupPosition.y)
setPopupPosition(newPopupPosition)
}
}, [isShowVarPicker, popupPosition])
@@ -124,7 +120,7 @@ const CodeEditor: FC<Props> = ({
value_selector: varValue,
}
onAddVar(newVar)
onAddVar?.(newVar)
}
const editor: any = editorRef.current
const monaco: any = monacoRef.current
@@ -143,7 +139,7 @@ const CodeEditor: FC<Props> = ({
}
return (
<div>
<div className={cn(editorProps.isExpand && 'h-full')}>
<Editor
{...editorProps}
onMount={onEditorMounted}

View File

@@ -1,20 +1,23 @@
'use client'
import type { FC } from 'react'
import Editor, { loader } from '@monaco-editor/react'
import React, { useRef } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import cn from 'classnames'
import Base from '../base'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import './style.css'
// load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482
loader.config({ paths: { vs: '/vs' } })
const CODE_EDITOR_LINE_HEIGHT = 18
export type Props = {
value?: string | object
placeholder?: string
onChange?: (value: string) => void
title: JSX.Element
title?: JSX.Element
language: CodeLanguage
headerRight?: JSX.Element
readOnly?: boolean
@@ -22,6 +25,8 @@ export type Props = {
height?: number
isInNode?: boolean
onMount?: (editor: any, monaco: any) => void
noWrapper?: boolean
isExpand?: boolean
}
const languageMap = {
@@ -30,11 +35,20 @@ const languageMap = {
[CodeLanguage.json]: 'json',
}
const DEFAULT_THEME = {
base: 'vs',
inherit: true,
rules: [],
colors: {
'editor.background': '#F2F4F7', // #00000000 transparent. But it will has a blue border
},
}
const CodeEditor: FC<Props> = ({
value = '',
placeholder = '',
onChange = () => { },
title,
title = '',
headerRight,
language,
readOnly,
@@ -42,16 +56,37 @@ const CodeEditor: FC<Props> = ({
height,
isInNode,
onMount,
noWrapper,
isExpand,
}) => {
const [isFocus, setIsFocus] = React.useState(false)
const [isMounted, setIsMounted] = React.useState(false)
const minHeight = height || 200
const [editorContentHeight, setEditorContentHeight] = useState(56)
const valueRef = useRef(value)
useEffect(() => {
valueRef.current = value
}, [value])
const editorRef = useRef<any>(null)
const resizeEditorToContent = () => {
if (editorRef.current) {
const contentHeight = editorRef.current.getContentHeight() // Math.max(, minHeight)
setEditorContentHeight(contentHeight)
}
}
const handleEditorChange = (value: string | undefined) => {
onChange(value || '')
setTimeout(() => {
resizeEditorToContent()
}, 10)
}
const editorRef = useRef(null)
const handleEditorDidMount = (editor: any, monaco: any) => {
editorRef.current = editor
resizeEditorToContent()
editor.onDidFocusEditorText(() => {
setIsFocus(true)
@@ -60,6 +95,8 @@ const CodeEditor: FC<Props> = ({
setIsFocus(false)
})
monaco.editor.defineTheme('default-theme', DEFAULT_THEME)
monaco.editor.defineTheme('blur-theme', {
base: 'vs',
inherit: true,
@@ -78,7 +115,10 @@ const CodeEditor: FC<Props> = ({
},
})
monaco.editor.setTheme('default-theme') // Fix: sometimes not load the default theme
onMount?.(editor, monaco)
setIsMounted(true)
}
const outPutValue = (() => {
@@ -92,43 +132,63 @@ const CodeEditor: FC<Props> = ({
}
})()
return (
<div>
<Base
className='relative'
title={title}
const theme = (() => {
if (noWrapper)
return 'default-theme'
return isFocus ? 'focus-theme' : 'blur-theme'
})()
const main = (
<>
{/* https://www.npmjs.com/package/@monaco-editor/react */}
<Editor
// className='min-h-[100%]' // h-full
// language={language === CodeLanguage.javascript ? 'javascript' : 'python'}
language={languageMap[language] || 'javascript'}
theme={isMounted ? theme : 'default-theme'} // sometimes not load the default theme
value={outPutValue}
headerRight={headerRight}
isFocus={isFocus && !readOnly}
minHeight={height || 200}
isInNode={isInNode}
>
<>
{/* https://www.npmjs.com/package/@monaco-editor/react */}
<Editor
className='h-full'
// language={language === CodeLanguage.javascript ? 'javascript' : 'python'}
language={languageMap[language] || 'javascript'}
theme={isFocus ? 'focus-theme' : 'blur-theme'}
onChange={handleEditorChange}
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
options={{
readOnly,
domReadOnly: true,
quickSuggestions: false,
minimap: { enabled: false },
lineNumbersMinChars: 1, // would change line num width
wordWrap: 'on', // auto line wrap
// lineNumbers: (num) => {
// return <div>{num}</div>
// }
}}
onMount={handleEditorDidMount}
/>
{!outPutValue && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
</>
)
return (
<div className={cn(isExpand && 'h-full')}>
{noWrapper
? <div className='relative no-wrapper' style={{
height: isExpand ? '100%' : (editorContentHeight) / 2 + CODE_EDITOR_LINE_HEIGHT, // In IDE, the last line can always be in lop line. So there is some blank space in the bottom.
minHeight: CODE_EDITOR_LINE_HEIGHT,
}}>
{main}
</div>
: (
<Base
className='relative'
title={title}
value={outPutValue}
onChange={handleEditorChange}
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
options={{
readOnly,
domReadOnly: true,
quickSuggestions: false,
minimap: { enabled: false },
lineNumbersMinChars: 1, // would change line num width
wordWrap: 'on', // auto line wrap
// lineNumbers: (num) => {
// return <div>{num}</div>
// }
}}
onMount={handleEditorDidMount}
/>
{!outPutValue && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
</>
</Base>
headerRight={headerRight}
isFocus={isFocus && !readOnly}
minHeight={minHeight}
isInNode={isInNode}
>
{main}
</Base>
)}
</div>
)
}

View File

@@ -2,6 +2,10 @@
padding-left: 10px;
}
.no-wrapper .margin-view-overlays {
padding-left: 0;
}
/* hide readonly tooltip */
.monaco-editor-overlaymessage {
display: none !important;

View File

@@ -1,16 +1,19 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import React, { useCallback, useRef } from 'react'
import cn from 'classnames'
import copy from 'copy-to-clipboard'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import {
BlockEnum,
type Node,
type NodeOutPutVar,
import { BlockEnum, EditionType } from '../../../../types'
import type {
Node,
NodeOutPutVar,
Variable,
} from '../../../../types'
import Wrap from '../editor/wrap'
import { CodeLanguage } from '../../../code/types'
import ToggleExpandBtn from '@/app/components/workflow/nodes/_base/components/toggle-expand-btn'
import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend'
import PromptEditor from '@/app/components/base/prompt-editor'
@@ -21,6 +24,10 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
import { PROMPT_EDITOR_INSERT_QUICKLY } from '@/app/components/base/prompt-editor/plugins/update-block'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars'
import Switch from '@/app/components/base/switch'
import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
type Props = {
className?: string
headerClassName?: string
@@ -42,6 +49,12 @@ type Props = {
}
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
// for jinja
isSupportJinja?: boolean
editionType?: EditionType
onEditionTypeChange?: (editionType: EditionType) => void
varList?: Variable[]
handleAddVariable?: (payload: any) => void
}
const Editor: FC<Props> = ({
@@ -61,6 +74,11 @@ const Editor: FC<Props> = ({
hasSetBlockStatus,
nodesOutputVars,
availableNodes = [],
isSupportJinja,
editionType,
onEditionTypeChange,
varList = [],
handleAddVariable,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
@@ -85,20 +103,6 @@ const Editor: FC<Props> = ({
setTrue: setFocus,
setFalse: setBlur,
}] = useBoolean(false)
const hideTooltipRunId = useRef(0)
const [isShowInsertToolTip, setIsShowInsertTooltip] = useState(false)
useEffect(() => {
if (isFocus) {
clearTimeout(hideTooltipRunId.current)
setIsShowInsertTooltip(true)
}
else {
hideTooltipRunId.current = setTimeout(() => {
setIsShowInsertTooltip(false)
}, 100) as any
}
}, [isFocus])
const handleInsertVariable = () => {
setFocus()
@@ -116,6 +120,29 @@ const Editor: FC<Props> = ({
<div className='w-px h-3 ml-2 mr-2 bg-gray-200'></div>
{/* Operations */}
<div className='flex items-center space-x-2'>
{isSupportJinja && (
<TooltipPlus
popupContent={
<div>
<div>{t('workflow.common.enableJinja')}</div>
<a className='text-[#155EEF]' target='_blank' href='https://jinja.palletsprojects.com/en/2.10.x/'>{t('workflow.common.learnMore')}</a>
</div>
}
hideArrow
>
<div className={cn(editionType === EditionType.jinja2 && 'border-black/5 bg-white', 'mb-1 flex h-[22px] items-center px-1.5 rounded-[5px] border border-transparent hover:border-black/5 space-x-0.5')}>
<Jinja className='w-6 h-3 text-gray-300' />
<Switch
size='sm'
defaultValue={editionType === EditionType.jinja2}
onChange={(checked) => {
onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic)
}}
/>
</div>
</TooltipPlus>
)}
{!readOnly && (
<TooltipPlus
popupContent={`${t('workflow.common.insertVarTip')}`}
@@ -142,57 +169,75 @@ const Editor: FC<Props> = ({
{/* Min: 80 Max: 560. Header: 24 */}
<div className={cn('pb-2', isExpand && 'flex flex-col grow')}>
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
<PromptEditor
instanceId={instanceId}
compact
className='min-h-[56px]'
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
show: justVar ? false : isShowContext,
selectable: !hasSetBlockStatus?.context,
canNotAddContext: true,
}}
historyBlock={{
show: justVar ? false : isShowHistory,
selectable: !hasSetBlockStatus?.history,
history: {
user: 'Human',
assistant: 'Assistant',
},
}}
queryBlock={{
show: false, // use [sys.query] instead of query block
selectable: false,
}}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
onChange={onChange}
onBlur={setBlur}
onFocus={setFocus}
editable={!readOnly}
/>
{/* to patch Editor not support dynamic change editable status */}
{readOnly && <div className='absolute inset-0 z-10'></div>}
</div>
{!(isSupportJinja && editionType === EditionType.jinja2)
? (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
<PromptEditor
instanceId={instanceId}
compact
className='min-h-[56px]'
style={isExpand ? { height: editorExpandHeight - 5 } : {}}
value={value}
contextBlock={{
show: justVar ? false : isShowContext,
selectable: !hasSetBlockStatus?.context,
canNotAddContext: true,
}}
historyBlock={{
show: justVar ? false : isShowHistory,
selectable: !hasSetBlockStatus?.history,
history: {
user: 'Human',
assistant: 'Assistant',
},
}}
queryBlock={{
show: false, // use [sys.query] instead of query block
selectable: false,
}}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
onChange={onChange}
onBlur={setBlur}
onFocus={setFocus}
editable={!readOnly}
/>
{/* to patch Editor not support dynamic change editable status */}
{readOnly && <div className='absolute inset-0 z-10'></div>}
</div>
)
: (
<div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}>
<CodeEditor
availableVars={nodesOutputVars || []}
varList={varList}
onAddVar={handleAddVariable}
isInNode
readOnly={readOnly}
language={CodeLanguage.python3}
value={value}
onChange={onChange}
noWrapper
isExpand={isExpand}
/>
</div>
)}
</div>
</div>
</div>
</Wrap>

View File

@@ -3,7 +3,8 @@ import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { uniqueId } from 'lodash-es'
import { useTranslation } from 'react-i18next'
import type { PromptItem } from '../../../types'
import type { PromptItem, Variable } from '../../../types'
import { EditionType } from '../../../types'
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import TooltipPlus from '@/app/components/base/tooltip-plus'
@@ -24,6 +25,7 @@ type Props = {
payload: PromptItem
handleChatModeMessageRoleChange: (role: PromptRole) => void
onPromptChange: (p: string) => void
onEditionTypeChange: (editionType: EditionType) => void
onRemove: () => void
isShowContext: boolean
hasSetBlockStatus: {
@@ -33,6 +35,8 @@ type Props = {
}
availableVars: any
availableNodes: any
varList: Variable[]
handleAddVariable: (payload: any) => void
}
const roleOptions = [
@@ -64,17 +68,21 @@ const ConfigPromptItem: FC<Props> = ({
isChatApp,
payload,
onPromptChange,
onEditionTypeChange,
onRemove,
isShowContext,
hasSetBlockStatus,
availableVars,
availableNodes,
varList,
handleAddVariable,
}) => {
const { t } = useTranslation()
const [instanceId, setInstanceId] = useState(uniqueId())
useEffect(() => {
setInstanceId(`${id}-${uniqueId()}`)
}, [id])
return (
<Editor
className={className}
@@ -107,7 +115,7 @@ const ConfigPromptItem: FC<Props> = ({
</TooltipPlus>
</div>
}
value={payload.text}
value={payload.edition_type === EditionType.jinja2 ? (payload.jinja2_text || '') : payload.text}
onChange={onPromptChange}
readOnly={readOnly}
showRemove={canRemove}
@@ -118,6 +126,11 @@ const ConfigPromptItem: FC<Props> = ({
hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={availableVars}
availableNodes={availableNodes}
isSupportJinja
editionType={payload.edition_type}
onEditionTypeChange={onEditionTypeChange}
varList={varList}
handleAddVariable={handleAddVariable}
/>
)
}

View File

@@ -6,8 +6,8 @@ import produce from 'immer'
import { ReactSortable } from 'react-sortablejs'
import { v4 as uuid4 } from 'uuid'
import cn from 'classnames'
import type { PromptItem, ValueSelector, Var } from '../../../types'
import { PromptRole } from '../../../types'
import type { PromptItem, ValueSelector, Var, Variable } from '../../../types'
import { EditionType, PromptRole } from '../../../types'
import useAvailableVarList from '../../_base/hooks/use-available-var-list'
import ConfigPromptItem from './config-prompt-item'
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
@@ -30,6 +30,8 @@ type Props = {
history: boolean
query: boolean
}
varList?: Variable[]
handleAddVariable: (payload: any) => void
}
const ConfigPrompt: FC<Props> = ({
@@ -42,10 +44,12 @@ const ConfigPrompt: FC<Props> = ({
onChange,
isShowContext,
hasSetBlockStatus,
varList = [],
handleAddVariable,
}) => {
const { t } = useTranslation()
const payloadWithIds = (isChatModel && Array.isArray(payload))
? payload.map((item, i) => {
? payload.map((item) => {
const id = uuid4()
return {
id: item.id || id,
@@ -67,7 +71,16 @@ const ConfigPrompt: FC<Props> = ({
const handleChatModePromptChange = useCallback((index: number) => {
return (prompt: string) => {
const newPrompt = produce(payload as PromptItem[], (draft) => {
draft[index].text = prompt
draft[index][draft[index].edition_type === EditionType.jinja2 ? 'jinja2_text' : 'text'] = prompt
})
onChange(newPrompt)
}
}, [onChange, payload])
const handleChatModeEditionTypeChange = useCallback((index: number) => {
return (editionType: EditionType) => {
const newPrompt = produce(payload as PromptItem[], (draft) => {
draft[index].edition_type = editionType
})
onChange(newPrompt)
}
@@ -106,7 +119,14 @@ const ConfigPrompt: FC<Props> = ({
const handleCompletionPromptChange = useCallback((prompt: string) => {
const newPrompt = produce(payload as PromptItem, (draft) => {
draft.text = prompt
draft[draft.edition_type === EditionType.jinja2 ? 'jinja2_text' : 'text'] = prompt
})
onChange(newPrompt)
}, [onChange, payload])
const handleCompletionEditionTypeChange = useCallback((editionType: EditionType) => {
const newPrompt = produce(payload as PromptItem, (draft) => {
draft.edition_type = editionType
})
onChange(newPrompt)
}, [onChange, payload])
@@ -161,11 +181,14 @@ const ConfigPrompt: FC<Props> = ({
isChatApp={isChatApp}
payload={item}
onPromptChange={handleChatModePromptChange(index)}
onEditionTypeChange={handleChatModeEditionTypeChange(index)}
onRemove={handleRemove(index)}
isShowContext={isShowContext}
hasSetBlockStatus={hasSetBlockStatus}
availableVars={availableVars}
availableNodes={availableNodes}
varList={varList}
handleAddVariable={handleAddVariable}
/>
</div>
@@ -187,7 +210,7 @@ const ConfigPrompt: FC<Props> = ({
<Editor
instanceId={`${nodeId}-chat-workflow-llm-prompt-editor`}
title={<span className='capitalize'>{t(`${i18nPrefix}.prompt`)}</span>}
value={(payload as PromptItem).text}
value={(payload as PromptItem).edition_type === EditionType.basic ? (payload as PromptItem).text : ((payload as PromptItem).jinja2_text || '')}
onChange={handleCompletionPromptChange}
readOnly={readOnly}
isChatModel={isChatModel}
@@ -196,6 +219,11 @@ const ConfigPrompt: FC<Props> = ({
hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={availableVars}
availableNodes={availableNodes}
isSupportJinja
editionType={(payload as PromptItem).edition_type}
varList={varList}
onEditionTypeChange={handleCompletionEditionTypeChange}
handleAddVariable={handleAddVariable}
/>
</div>
)}

View File

@@ -1,7 +1,6 @@
import { BlockEnum } from '../../types'
import { type NodeDefault, PromptRole } from '../../types'
import { BlockEnum, EditionType } from '../../types'
import { type NodeDefault, type PromptItem, PromptRole } from '../../types'
import type { LLMNodeType } from './types'
import type { PromptItem } from '@/models/debug'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
const i18nPrefix = 'workflow.errorMsg'
@@ -16,7 +15,6 @@ const nodeDefault: NodeDefault<LLMNodeType> = {
temperature: 0.7,
},
},
variables: [],
prompt_template: [{
role: PromptRole.system,
text: '',
@@ -57,6 +55,23 @@ const nodeDefault: NodeDefault<LLMNodeType> = {
if (isChatModel && !!payload.memory.query_prompt_template && !payload.memory.query_prompt_template.includes('{{#sys.query#}}'))
errorMessages = t('workflow.nodes.llm.sysQueryInUser')
}
if (!errorMessages) {
const isChatModel = payload.model.mode === 'chat'
const isShowVars = (() => {
if (isChatModel)
return (payload.prompt_template as PromptItem[]).some(item => item.edition_type === EditionType.jinja2)
return (payload.prompt_template as PromptItem).edition_type === EditionType.jinja2
})()
if (isShowVars && payload.prompt_config?.jinja2_variables) {
payload.prompt_config?.jinja2_variables.forEach((i) => {
if (!errorMessages && !i.variable)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
if (!errorMessages && !i.value_selector.length)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
})
}
}
return {
isValid: !errorMessages,
errorMessage: errorMessages,

View File

@@ -7,6 +7,8 @@ import useConfig from './use-config'
import ResolutionPicker from './components/resolution-picker'
import type { LLMNodeType } from './types'
import ConfigPrompt from './components/config-prompt'
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
import AddButton2 from '@/app/components/base/button/add-button'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
@@ -44,7 +46,12 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
filterVar,
availableVars,
availableNodes,
isShowVars,
handlePromptChange,
handleAddEmptyVariable,
handleAddVariable,
handleVarListChange,
handleVarNameChange,
handleSyeQueryChange,
handleMemoryChange,
handleVisionResolutionEnabledChange,
@@ -169,9 +176,29 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
payload={inputs.prompt_template}
onChange={handlePromptChange}
hasSetBlockStatus={hasSetBlockStatus}
varList={inputs.prompt_config?.jinja2_variables || []}
handleAddVariable={handleAddVariable}
/>
)}
{isShowVars && (
<Field
title={t('workflow.nodes.templateTransform.inputVars')}
operations={
!readOnly ? <AddButton2 onClick={handleAddEmptyVariable} /> : undefined
}
>
<VarList
nodeId={id}
readonly={readOnly}
list={inputs.prompt_config?.jinja2_variables || []}
onChange={handleVarListChange}
onVarNameChange={handleVarNameChange}
filterVar={filterVar}
/>
</Field>
)}
{/* Memory put place examples. */}
{isChatMode && isChatModel && !!inputs.memory && (
<div className='mt-4'>

View File

@@ -3,8 +3,10 @@ import type { CommonNodeType, Memory, ModelConfig, PromptItem, ValueSelector, Va
export type LLMNodeType = CommonNodeType & {
model: ModelConfig
variables: Variable[]
prompt_template: PromptItem[] | PromptItem
prompt_config?: {
jinja2_variables?: Variable[]
}
memory?: Memory
context: {
enabled: boolean

View File

@@ -1,8 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import produce from 'immer'
import useVarList from '../_base/hooks/use-var-list'
import { VarType } from '../../types'
import type { Memory, ValueSelector, Var } from '../../types'
import { EditionType, VarType } from '../../types'
import type { Memory, PromptItem, ValueSelector, Var, Variable } from '../../types'
import { useStore } from '../../store'
import {
useIsChatMode,
@@ -18,7 +17,6 @@ import {
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
import type { PromptItem } from '@/models/debug'
import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants'
import { checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants'
@@ -29,20 +27,21 @@ const useConfig = (id: string, payload: LLMNodeType) => {
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
const [defaultRolePrefix, setDefaultRolePrefix] = useState<{ user: string; assistant: string }>({ user: '', assistant: '' })
const { inputs, setInputs: doSetInputs } = useNodeCrud<LLMNodeType>(id, payload)
const inputRef = useRef(inputs)
const setInputs = useCallback((newInputs: LLMNodeType) => {
if (newInputs.memory && !newInputs.memory.role_prefix) {
const newPayload = produce(newInputs, (draft) => {
draft.memory!.role_prefix = defaultRolePrefix
})
doSetInputs(newPayload)
inputRef.current = newPayload
return
}
doSetInputs(newInputs)
inputRef.current = newInputs
}, [doSetInputs, defaultRolePrefix])
const inputRef = useRef(inputs)
useEffect(() => {
inputRef.current = inputs
}, [inputs])
// model
const model = inputs.model
const modelMode = inputs.model?.mode
@@ -178,11 +177,80 @@ const useConfig = (id: string, payload: LLMNodeType) => {
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isShowVisionConfig, modelChanged])
// variables
const { handleVarListChange, handleAddVariable } = useVarList<LLMNodeType>({
inputs,
setInputs,
})
const isShowVars = (() => {
if (isChatModel)
return (inputs.prompt_template as PromptItem[]).some(item => item.edition_type === EditionType.jinja2)
return (inputs.prompt_template as PromptItem).edition_type === EditionType.jinja2
})()
const handleAddEmptyVariable = useCallback(() => {
const newInputs = produce(inputRef.current, (draft) => {
if (!draft.prompt_config) {
draft.prompt_config = {
jinja2_variables: [],
}
}
if (!draft.prompt_config.jinja2_variables)
draft.prompt_config.jinja2_variables = []
draft.prompt_config.jinja2_variables.push({
variable: '',
value_selector: [],
})
})
setInputs(newInputs)
}, [setInputs])
const handleAddVariable = useCallback((payload: Variable) => {
const newInputs = produce(inputRef.current, (draft) => {
if (!draft.prompt_config) {
draft.prompt_config = {
jinja2_variables: [],
}
}
if (!draft.prompt_config.jinja2_variables)
draft.prompt_config.jinja2_variables = []
draft.prompt_config.jinja2_variables.push(payload)
})
setInputs(newInputs)
}, [setInputs])
const handleVarListChange = useCallback((newList: Variable[]) => {
const newInputs = produce(inputRef.current, (draft) => {
if (!draft.prompt_config) {
draft.prompt_config = {
jinja2_variables: [],
}
}
if (!draft.prompt_config.jinja2_variables)
draft.prompt_config.jinja2_variables = []
draft.prompt_config.jinja2_variables = newList
})
setInputs(newInputs)
}, [setInputs])
const handleVarNameChange = useCallback((oldName: string, newName: string) => {
const newInputs = produce(inputRef.current, (draft) => {
if (isChatModel) {
const promptTemplate = draft.prompt_template as PromptItem[]
promptTemplate.filter(item => item.edition_type === EditionType.jinja2).forEach((item) => {
item.jinja2_text = (item.jinja2_text || '').replaceAll(`{{ ${oldName} }}`, `{{ ${newName} }}`)
})
}
else {
if ((draft.prompt_template as PromptItem).edition_type !== EditionType.jinja2)
return
const promptTemplate = draft.prompt_template as PromptItem
promptTemplate.jinja2_text = (promptTemplate.jinja2_text || '').replaceAll(`{{ ${oldName} }}`, `{{ ${newName} }}`)
}
})
setInputs(newInputs)
}, [isChatModel, setInputs])
// context
const handleContextVarChange = useCallback((newVar: ValueSelector | string) => {
@@ -194,11 +262,11 @@ const useConfig = (id: string, payload: LLMNodeType) => {
}, [inputs, setInputs])
const handlePromptChange = useCallback((newPrompt: PromptItem[] | PromptItem) => {
const newInputs = produce(inputs, (draft) => {
const newInputs = produce(inputRef.current, (draft) => {
draft.prompt_template = newPrompt
})
setInputs(newInputs)
}, [inputs, setInputs])
}, [setInputs])
const handleMemoryChange = useCallback((newMemory?: Memory) => {
const newInputs = produce(inputs, (draft) => {
@@ -286,6 +354,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
runInputData,
setRunInputData,
runResult,
toVarInputs,
} = useOneStepRun<LLMNodeType>({
id,
data: inputs,
@@ -295,23 +364,6 @@ const useConfig = (id: string, payload: LLMNodeType) => {
},
})
// const handleRun = (submitData: Record<string, any>) => {
// console.log(submitData)
// const res = produce(submitData, (draft) => {
// debugger
// if (draft.contexts) {
// draft['#context#'] = draft.contexts
// delete draft.contexts
// }
// if (draft.visionFiles) {
// draft['#files#'] = draft.visionFiles
// delete draft.visionFiles
// }
// })
// doHandleRun(res)
// }
const inputVarValues = (() => {
const vars: Record<string, any> = {}
Object.keys(runInputData)
@@ -348,7 +400,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
}, [runInputData, setRunInputData])
const allVarStrArr = (() => {
const arr = isChatModel ? (inputs.prompt_template as PromptItem[]).map(item => item.text) : [(inputs.prompt_template as PromptItem).text]
const arr = isChatModel ? (inputs.prompt_template as PromptItem[]).filter(item => item.edition_type !== EditionType.jinja2).map(item => item.text) : [(inputs.prompt_template as PromptItem).text]
if (isChatMode && isChatModel && !!inputs.memory) {
arr.push('{{#sys.query#}}')
arr.push(inputs.memory.query_prompt_template)
@@ -357,7 +409,13 @@ const useConfig = (id: string, payload: LLMNodeType) => {
return arr
})()
const varInputs = getInputVars(allVarStrArr)
const varInputs = (() => {
const vars = getInputVars(allVarStrArr)
if (isShowVars)
return [...vars, ...toVarInputs(inputs.prompt_config?.jinja2_variables || [])]
return vars
})()
return {
readOnly,
@@ -370,8 +428,11 @@ const useConfig = (id: string, payload: LLMNodeType) => {
isShowVisionConfig,
handleModelChanged,
handleCompletionParamsChange,
isShowVars,
handleVarListChange,
handleVarNameChange,
handleAddVariable,
handleAddEmptyVariable,
handleContextVarChange,
filterInputVar,
filterVar,

View File

@@ -26,6 +26,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
const {
readOnly,
inputs,
availableVars,
handleVarListChange,
handleVarNameChange,
handleAddVariable,
@@ -65,7 +66,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
</Field>
<Split />
<CodeEditor
nodeId={id}
availableVars={availableVars}
varList={inputs.variables}
onAddVar={handleAddVariable}
isInNode

View File

@@ -10,6 +10,7 @@ import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-s
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
const useConfig = (id: string, payload: TemplateTransformNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
@@ -22,6 +23,11 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => {
inputsRef.current = newPayload
}, [doSetInputs])
const { availableVars } = useAvailableVarList(id, {
onlyLeafNodeVar: false,
filterVar: () => true,
})
const { handleAddVariable: handleAddEmptyVariable } = useVarList<TemplateTransformNodeType>({
inputs,
setInputs,
@@ -108,6 +114,7 @@ const useConfig = (id: string, payload: TemplateTransformNodeType) => {
return {
readOnly,
inputs,
availableVars,
handleVarListChange,
handleVarNameChange,
handleAddVariable,