{
- selectedDatasetsMode.allEconomic && (
+ selectedDatasetsMode.allEconomic && !selectedDatasetsMode.mixtureInternalAndExternal && (
{
let errMsg = ''
if (tempDataSetConfigs.retrieval_model === RETRIEVE_TYPE.multiWay) {
- if (!tempDataSetConfigs.reranking_model?.reranking_model_name && (rerankDefaultModel && !isRerankDefaultModelValid))
+ if (tempDataSetConfigs.reranking_enable
+ && tempDataSetConfigs.reranking_mode === RerankingModeEnum.RerankingModel
+ && !isRerankDefaultModelValid
+ )
errMsg = t('appDebug.datasetConfig.rerankModelRequired')
}
if (errMsg) {
@@ -62,7 +66,9 @@ const ParamsConfig = ({
if (!isValid())
return
const config = { ...tempDataSetConfigs }
- if (config.retrieval_model === RETRIEVE_TYPE.multiWay && !config.reranking_model) {
+ if (config.retrieval_model === RETRIEVE_TYPE.multiWay
+ && config.reranking_mode === RerankingModeEnum.RerankingModel
+ && !config.reranking_model) {
config.reranking_model = {
reranking_provider_name: rerankDefaultModel?.provider?.provider,
reranking_model_name: rerankDefaultModel?.model,
diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx
index 12ee7d75ad..639cb2fad1 100644
--- a/web/app/components/app/configuration/index.tsx
+++ b/web/app/components/app/configuration/index.tsx
@@ -253,12 +253,18 @@ const Configuration: FC = () => {
}
hideSelectDataSet()
const {
- allEconomic,
+ allExternal,
+ allInternal,
+ mixtureInternalAndExternal,
mixtureHighQualityAndEconomic,
inconsistentEmbeddingModel,
} = getSelectedDatasetsMode(newDatasets)
- if (allEconomic || mixtureHighQualityAndEconomic || inconsistentEmbeddingModel)
+ if (
+ (allInternal && (mixtureHighQualityAndEconomic || inconsistentEmbeddingModel))
+ || mixtureInternalAndExternal
+ || allExternal
+ )
setRerankSettingModalOpen(true)
const { datasets, retrieval_model, score_threshold_enabled, ...restConfigs } = datasetConfigs
diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx
index 22585aa678..4c12cab581 100644
--- a/web/app/components/app/log/list.tsx
+++ b/web/app/components/app/log/list.tsx
@@ -36,6 +36,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import TextGeneration from '@/app/components/app/text-generate/item'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import MessageLogModal from '@/app/components/base/message-log-modal'
+import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useAppContext } from '@/context/app-context'
import useTimestamp from '@/hooks/use-timestamp'
@@ -168,11 +169,13 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const { userProfile: { timezone } } = useAppContext()
const { formatTime } = useTimestamp()
const { onClose, appDetail } = useContext(DrawerContext)
- const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
+ const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
currentLogItem: state.currentLogItem,
setCurrentLogItem: state.setCurrentLogItem,
showMessageLogModal: state.showMessageLogModal,
setShowMessageLogModal: state.setShowMessageLogModal,
+ showPromptLogModal: state.showPromptLogModal,
+ setShowPromptLogModal: state.setShowPromptLogModal,
currentLogModalActiveTab: state.currentLogModalActiveTab,
})))
const { t } = useTranslation()
@@ -192,8 +195,8 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
conversation_id: detail.id,
limit: 10,
}
- if (allChatItems.at(-1)?.id)
- params.first_id = allChatItems.at(-1)?.id.replace('question-', '')
+ if (allChatItems[0]?.id)
+ params.first_id = allChatItems[0]?.id.replace('question-', '')
const messageRes = await fetchChatMessages({
url: `/apps/${appDetail?.id}/chat-messages`,
params,
@@ -557,6 +560,16 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
defaultTab={currentLogModalActiveTab}
/>
)}
+ {showPromptLogModal && (
+
{
+ setCurrentLogItem()
+ setShowPromptLogModal(false)
+ }}
+ />
+ )}
)
}
diff --git a/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap b/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap
index 070975bfa7..7da09c4529 100644
--- a/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap
+++ b/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap
@@ -1804,6 +1804,280 @@ exports[`build chat item tree and get thread messages should get thread messages
]
`;
+exports[`build chat item tree and get thread messages should work with partial messages 1`] = `
+[
+ {
+ "children": [
+ {
+ "agent_thoughts": [
+ {
+ "chain_id": null,
+ "created_at": 1726105809,
+ "files": [],
+ "id": "1019cd79-d141-4f9f-880a-fc1441cfd802",
+ "message_id": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
+ "observation": "",
+ "position": 1,
+ "thought": "Sure! My number is 54. Your turn!",
+ "tool": "",
+ "tool_input": "",
+ "tool_labels": {},
+ },
+ ],
+ "children": [
+ {
+ "children": [
+ {
+ "agent_thoughts": [
+ {
+ "chain_id": null,
+ "created_at": 1726105822,
+ "files": [],
+ "id": "0773bec7-b992-4a53-92b2-20ebaeae8798",
+ "message_id": "324bce32-c98c-435d-a66b-bac974ebb5ed",
+ "observation": "",
+ "position": 1,
+ "thought": "My number is 4729. Your turn!",
+ "tool": "",
+ "tool_input": "",
+ "tool_labels": {},
+ },
+ ],
+ "children": [],
+ "content": "My number is 4729. Your turn!",
+ "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80",
+ "feedbackDisabled": false,
+ "id": "324bce32-c98c-435d-a66b-bac974ebb5ed",
+ "input": {
+ "inputs": {},
+ "query": "3306",
+ },
+ "isAnswer": true,
+ "log": [
+ {
+ "files": [],
+ "role": "user",
+ "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "Sure! My number is 54. Your turn!",
+ },
+ {
+ "files": [],
+ "role": "user",
+ "text": "3306",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "My number is 4729. Your turn!",
+ },
+ ],
+ "message_files": [],
+ "more": {
+ "latency": "1.30",
+ "time": "09/11/2024 09:50 PM",
+ "tokens": 66,
+ },
+ "parentMessageId": "question-324bce32-c98c-435d-a66b-bac974ebb5ed",
+ "siblingIndex": 0,
+ "workflow_run_id": null,
+ },
+ ],
+ "content": "3306",
+ "id": "question-324bce32-c98c-435d-a66b-bac974ebb5ed",
+ "isAnswer": false,
+ "message_files": [],
+ "parentMessageId": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
+ },
+ {
+ "children": [
+ {
+ "agent_thoughts": [
+ {
+ "chain_id": null,
+ "created_at": 1726107812,
+ "files": [],
+ "id": "5ca650f3-982c-4399-8b95-9ea241c76707",
+ "message_id": "684b5396-4e91-4043-88e9-aabe48b21acc",
+ "observation": "",
+ "position": 1,
+ "thought": "My number is 4821. Your turn!",
+ "tool": "",
+ "tool_input": "",
+ "tool_labels": {},
+ },
+ ],
+ "children": [
+ {
+ "children": [
+ {
+ "agent_thoughts": [
+ {
+ "chain_id": null,
+ "created_at": 1726111024,
+ "files": [],
+ "id": "095cacab-afad-4387-a41d-1662578b8b13",
+ "message_id": "19904a7b-7494-4ed8-b72c-1d18668cea8c",
+ "observation": "",
+ "position": 1,
+ "thought": "My number is 1456. Your turn!",
+ "tool": "",
+ "tool_input": "",
+ "tool_labels": {},
+ },
+ ],
+ "children": [],
+ "content": "My number is 1456. Your turn!",
+ "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80",
+ "feedbackDisabled": false,
+ "id": "19904a7b-7494-4ed8-b72c-1d18668cea8c",
+ "input": {
+ "inputs": {},
+ "query": "1003",
+ },
+ "isAnswer": true,
+ "log": [
+ {
+ "files": [],
+ "role": "user",
+ "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "Sure! My number is 54. Your turn!",
+ },
+ {
+ "files": [],
+ "role": "user",
+ "text": "3306",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "My number is 4821. Your turn!",
+ },
+ {
+ "files": [],
+ "role": "user",
+ "text": "1003",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "My number is 1456. Your turn!",
+ },
+ ],
+ "message_files": [],
+ "more": {
+ "latency": "1.38",
+ "time": "09/11/2024 11:17 PM",
+ "tokens": 86,
+ },
+ "parentMessageId": "question-19904a7b-7494-4ed8-b72c-1d18668cea8c",
+ "siblingIndex": 0,
+ "workflow_run_id": null,
+ },
+ ],
+ "content": "1003",
+ "id": "question-19904a7b-7494-4ed8-b72c-1d18668cea8c",
+ "isAnswer": false,
+ "message_files": [],
+ "parentMessageId": "684b5396-4e91-4043-88e9-aabe48b21acc",
+ },
+ ],
+ "content": "My number is 4821. Your turn!",
+ "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80",
+ "feedbackDisabled": false,
+ "id": "684b5396-4e91-4043-88e9-aabe48b21acc",
+ "input": {
+ "inputs": {},
+ "query": "3306",
+ },
+ "isAnswer": true,
+ "log": [
+ {
+ "files": [],
+ "role": "user",
+ "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "Sure! My number is 54. Your turn!",
+ },
+ {
+ "files": [],
+ "role": "user",
+ "text": "3306",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "My number is 4821. Your turn!",
+ },
+ ],
+ "message_files": [],
+ "more": {
+ "latency": "1.48",
+ "time": "09/11/2024 10:23 PM",
+ "tokens": 66,
+ },
+ "parentMessageId": "question-684b5396-4e91-4043-88e9-aabe48b21acc",
+ "siblingIndex": 1,
+ "workflow_run_id": null,
+ },
+ ],
+ "content": "3306",
+ "id": "question-684b5396-4e91-4043-88e9-aabe48b21acc",
+ "isAnswer": false,
+ "message_files": [],
+ "parentMessageId": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
+ },
+ ],
+ "content": "Sure! My number is 54. Your turn!",
+ "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80",
+ "feedbackDisabled": false,
+ "id": "cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
+ "input": {
+ "inputs": {},
+ "query": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
+ },
+ "isAnswer": true,
+ "log": [
+ {
+ "files": [],
+ "role": "user",
+ "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
+ },
+ {
+ "files": [],
+ "role": "assistant",
+ "text": "Sure! My number is 54. Your turn!",
+ },
+ ],
+ "message_files": [],
+ "more": {
+ "latency": "1.52",
+ "time": "09/11/2024 09:50 PM",
+ "tokens": 46,
+ },
+ "parentMessageId": "question-cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
+ "siblingIndex": 0,
+ "workflow_run_id": null,
+ },
+ ],
+ "content": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38",
+ "id": "question-cd5affb0-7bc2-4a6f-be7e-25e74595c9dd",
+ "isAnswer": false,
+ "message_files": [],
+ },
+]
+`;
+
exports[`build chat item tree and get thread messages should work with real world messages 1`] = `
[
{
diff --git a/web/app/components/base/chat/__tests__/utils.spec.ts b/web/app/components/base/chat/__tests__/utils.spec.ts
index c602ac8a99..1dead1c949 100644
--- a/web/app/components/base/chat/__tests__/utils.spec.ts
+++ b/web/app/components/base/chat/__tests__/utils.spec.ts
@@ -255,4 +255,10 @@ describe('build chat item tree and get thread messages', () => {
const threadMessages6_2 = getThreadMessages(tree6, 'ff4c2b43-48a5-47ad-9dc5-08b34ddba61b')
expect(threadMessages6_2).toMatchSnapshot()
})
+
+ const partialMessages = (realWorldMessages as ChatItemInTree[]).slice(-10)
+ const tree7 = buildChatItemTree(partialMessages)
+ it('should work with partial messages', () => {
+ expect(tree7).toMatchSnapshot()
+ })
})
diff --git a/web/app/components/base/chat/utils.ts b/web/app/components/base/chat/utils.ts
index 16357361cf..61dfaecffc 100644
--- a/web/app/components/base/chat/utils.ts
+++ b/web/app/components/base/chat/utils.ts
@@ -134,6 +134,12 @@ function buildChatItemTree(allMessages: IChatItem[]): ChatItemInTree[] {
}
}
+ // If no messages have parentMessageId=null (indicating a root node),
+ // then we likely have a partial chat history. In this case,
+ // use the first available message as the root node.
+ if (rootNodes.length === 0 && allMessages.length > 0)
+ rootNodes.push(map[allMessages[0]!.id]!)
+
return rootNodes
}
diff --git a/web/app/components/base/search-input/index.tsx b/web/app/components/base/search-input/index.tsx
index 4b3821da5a..89345fbe32 100644
--- a/web/app/components/base/search-input/index.tsx
+++ b/web/app/components/base/search-input/index.tsx
@@ -1,5 +1,5 @@
import type { FC } from 'react'
-import { useState } from 'react'
+import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiSearchLine } from '@remixicon/react'
import cn from '@/utils/classnames'
@@ -12,6 +12,7 @@ type SearchInputProps = {
onChange: (v: string) => void
white?: boolean
}
+
const SearchInput: FC
= ({
placeholder,
className,
@@ -21,6 +22,7 @@ const SearchInput: FC = ({
}) => {
const { t } = useTranslation()
const [focus, setFocus] = useState(false)
+ const isComposing = useRef(false)
return (
= ({
placeholder={placeholder || t('common.operation.search')!}
value={value}
onChange={(e) => {
- onChange(e.target.value)
+ if (!isComposing.current)
+ onChange(e.target.value)
+ }}
+ onCompositionStart={() => {
+ isComposing.current = true
+ }}
+ onCompositionEnd={() => {
+ isComposing.current = false
}}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
diff --git a/web/app/components/develop/md.tsx b/web/app/components/develop/md.tsx
index 87f7b35aaf..26b4007c87 100644
--- a/web/app/components/develop/md.tsx
+++ b/web/app/components/develop/md.tsx
@@ -39,6 +39,7 @@ export const Heading = function H2({
}
return (
<>
+
{method}
{/*
*/}
diff --git a/web/app/components/develop/template/template_advanced_chat.en.mdx b/web/app/components/develop/template/template_advanced_chat.en.mdx
index 7d80367ce4..6642c5cedc 100644
--- a/web/app/components/develop/template/template_advanced_chat.en.mdx
+++ b/web/app/components/develop/template/template_advanced_chat.en.mdx
@@ -656,6 +656,11 @@ Chat applications support session persistence, allowing previous chat history to
Return only pinned conversations as `true`, only non-pinned as `false`
+
+ Sorting Field (Optional), Default: -updated_at (sorted in descending order by update time)
+ - Available Values: created_at, -created_at, updated_at, -updated_at
+ - The symbol before the field represents the order or reverse, "-" represents reverse order.
+
### Response
diff --git a/web/app/components/develop/template/template_advanced_chat.zh.mdx b/web/app/components/develop/template/template_advanced_chat.zh.mdx
index 690d700f05..8e64d63ac5 100755
--- a/web/app/components/develop/template/template_advanced_chat.zh.mdx
+++ b/web/app/components/develop/template/template_advanced_chat.zh.mdx
@@ -691,6 +691,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
只返回置顶 true,只返回非置顶 false
+
+ 排序字段(选题),默认 -updated_at(按更新时间倒序排列)
+ - 可选值:created_at, -created_at, updated_at, -updated_at
+ - 字段前面的符号代表顺序或倒序,-代表倒序
+
### Response
diff --git a/web/app/components/develop/template/template_chat.en.mdx b/web/app/components/develop/template/template_chat.en.mdx
index 907a1ab0b4..a94016ca3a 100644
--- a/web/app/components/develop/template/template_chat.en.mdx
+++ b/web/app/components/develop/template/template_chat.en.mdx
@@ -690,6 +690,11 @@ Chat applications support session persistence, allowing previous chat history to
Return only pinned conversations as `true`, only non-pinned as `false`
+
+ Sorting Field (Optional), Default: -updated_at (sorted in descending order by update time)
+ - Available Values: created_at, -created_at, updated_at, -updated_at
+ - The symbol before the field represents the order or reverse, "-" represents reverse order.
+
### Response
diff --git a/web/app/components/develop/template/template_chat.zh.mdx b/web/app/components/develop/template/template_chat.zh.mdx
index f6dc7daa1e..92b13b2c7d 100644
--- a/web/app/components/develop/template/template_chat.zh.mdx
+++ b/web/app/components/develop/template/template_chat.zh.mdx
@@ -705,6 +705,11 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
只返回置顶 true,只返回非置顶 false
+
+ 排序字段(选题),默认 -updated_at(按更新时间倒序排列)
+ - 可选值:created_at, -created_at, updated_at, -updated_at
+ - 字段前面的符号代表顺序或倒序,-代表倒序
+
### Response
diff --git a/web/app/components/workflow/nodes/_base/components/editor/base.tsx b/web/app/components/workflow/nodes/_base/components/editor/base.tsx
index cca565c39d..44930427ae 100644
--- a/web/app/components/workflow/nodes/_base/components/editor/base.tsx
+++ b/web/app/components/workflow/nodes/_base/components/editor/base.tsx
@@ -26,7 +26,7 @@ type Props = {
isFocus: boolean
isInNode?: boolean
onGenerated?: (prompt: string) => void
- codeLanguages: CodeLanguage
+ codeLanguages?: CodeLanguage
fileList?: FileEntity[]
showFileList?: boolean
showCodeGenerator?: boolean
@@ -78,7 +78,7 @@ const Base: FC
= ({
e.stopPropagation()
}}>
{headerRight}
- {showCodeGenerator && (
+ {showCodeGenerator && codeLanguages && (
diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx
index a31cde2c3c..28d07936d3 100644
--- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx
+++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx
@@ -31,6 +31,7 @@ export type Props = {
noWrapper?: boolean
isExpand?: boolean
showFileList?: boolean
+ onGenerated?: (value: string) => void
showCodeGenerator?: boolean
}
@@ -64,6 +65,7 @@ const CodeEditor: FC = ({
noWrapper,
isExpand,
showFileList,
+ onGenerated,
showCodeGenerator = false,
}) => {
const [isFocus, setIsFocus] = React.useState(false)
@@ -151,9 +153,6 @@ const CodeEditor: FC = ({
return isFocus ? 'focus-theme' : 'blur-theme'
})()
- const handleGenerated = (code: string) => {
- handleEditorChange(code)
- }
const main = (
<>
@@ -205,7 +204,7 @@ const CodeEditor: FC = ({
isFocus={isFocus && !readOnly}
minHeight={minHeight}
isInNode={isInNode}
- onGenerated={handleGenerated}
+ onGenerated={onGenerated}
codeLanguages={language}
fileList={fileList}
showFileList={showFileList}
diff --git a/web/app/components/workflow/nodes/code/code-parser.spec.ts b/web/app/components/workflow/nodes/code/code-parser.spec.ts
new file mode 100644
index 0000000000..b5d28dd136
--- /dev/null
+++ b/web/app/components/workflow/nodes/code/code-parser.spec.ts
@@ -0,0 +1,326 @@
+import { VarType } from '../../types'
+import { extractFunctionParams, extractReturnType } from './code-parser'
+import { CodeLanguage } from './types'
+
+const SAMPLE_CODES = {
+ python3: {
+ noParams: 'def main():',
+ singleParam: 'def main(param1):',
+ multipleParams: `def main(param1, param2, param3):
+ return {"result": param1}`,
+ withTypes: `def main(param1: str, param2: int, param3: List[str]):
+ result = process_data(param1, param2)
+ return {"output": result}`,
+ withDefaults: `def main(param1: str = "default", param2: int = 0):
+ return {"data": param1}`,
+ },
+ javascript: {
+ noParams: 'function main() {',
+ singleParam: 'function main(param1) {',
+ multipleParams: `function main(param1, param2, param3) {
+ return { result: param1 }
+ }`,
+ withComments: `// Main function
+ function main(param1, param2) {
+ // Process data
+ return { output: process(param1, param2) }
+ }`,
+ withSpaces: 'function main( param1 , param2 ) {',
+ },
+}
+
+describe('extractFunctionParams', () => {
+ describe('Python3', () => {
+ test('handles no parameters', () => {
+ const result = extractFunctionParams(SAMPLE_CODES.python3.noParams, CodeLanguage.python3)
+ expect(result).toEqual([])
+ })
+
+ test('extracts single parameter', () => {
+ const result = extractFunctionParams(SAMPLE_CODES.python3.singleParam, CodeLanguage.python3)
+ expect(result).toEqual(['param1'])
+ })
+
+ test('extracts multiple parameters', () => {
+ const result = extractFunctionParams(SAMPLE_CODES.python3.multipleParams, CodeLanguage.python3)
+ expect(result).toEqual(['param1', 'param2', 'param3'])
+ })
+
+ test('handles type hints', () => {
+ const result = extractFunctionParams(SAMPLE_CODES.python3.withTypes, CodeLanguage.python3)
+ expect(result).toEqual(['param1', 'param2', 'param3'])
+ })
+
+ test('handles default values', () => {
+ const result = extractFunctionParams(SAMPLE_CODES.python3.withDefaults, CodeLanguage.python3)
+ expect(result).toEqual(['param1', 'param2'])
+ })
+ })
+
+ // JavaScriptのテストケース
+ describe('JavaScript', () => {
+ test('handles no parameters', () => {
+ const result = extractFunctionParams(SAMPLE_CODES.javascript.noParams, CodeLanguage.javascript)
+ expect(result).toEqual([])
+ })
+
+ test('extracts single parameter', () => {
+ const result = extractFunctionParams(SAMPLE_CODES.javascript.singleParam, CodeLanguage.javascript)
+ expect(result).toEqual(['param1'])
+ })
+
+ test('extracts multiple parameters', () => {
+ const result = extractFunctionParams(SAMPLE_CODES.javascript.multipleParams, CodeLanguage.javascript)
+ expect(result).toEqual(['param1', 'param2', 'param3'])
+ })
+
+ test('handles comments in code', () => {
+ const result = extractFunctionParams(SAMPLE_CODES.javascript.withComments, CodeLanguage.javascript)
+ expect(result).toEqual(['param1', 'param2'])
+ })
+
+ test('handles whitespace', () => {
+ const result = extractFunctionParams(SAMPLE_CODES.javascript.withSpaces, CodeLanguage.javascript)
+ expect(result).toEqual(['param1', 'param2'])
+ })
+ })
+})
+
+const RETURN_TYPE_SAMPLES = {
+ python3: {
+ singleReturn: `
+def main(param1):
+ return {"result": "value"}`,
+
+ multipleReturns: `
+def main(param1, param2):
+ return {"result": "value", "status": "success"}`,
+
+ noReturn: `
+def main():
+ print("Hello")`,
+
+ complexReturn: `
+def main():
+ data = process()
+ return {"result": data, "count": 42, "messages": ["hello"]}`,
+ nestedObject: `
+ def main(name, age, city):
+ return {
+ 'personal_info': {
+ 'name': name,
+ 'age': age,
+ 'city': city
+ },
+ 'timestamp': int(time.time()),
+ 'status': 'active'
+ }`,
+ },
+
+ javascript: {
+ singleReturn: `
+function main(param1) {
+ return { result: "value" }
+}`,
+
+ multipleReturns: `
+function main(param1) {
+ return { result: "value", status: "success" }
+}`,
+
+ withParentheses: `
+function main() {
+ return ({ result: "value", status: "success" })
+}`,
+
+ noReturn: `
+function main() {
+ console.log("Hello")
+}`,
+
+ withQuotes: `
+function main() {
+ return { "result": 'value', 'status': "success" }
+}`,
+ nestedObject: `
+function main(name, age, city) {
+ return {
+ personal_info: {
+ name: name,
+ age: age,
+ city: city
+ },
+ timestamp: Date.now(),
+ status: 'active'
+ }
+}`,
+ withJSDoc: `
+/**
+ * Creates a user profile with personal information and metadata
+ * @param {string} name - The user's name
+ * @param {number} age - The user's age
+ * @param {string} city - The user's city of residence
+ * @returns {Object} An object containing the user profile
+ */
+function main(name, age, city) {
+ return {
+ result: {
+ personal_info: {
+ name: name,
+ age: age,
+ city: city
+ },
+ timestamp: Date.now(),
+ status: 'active'
+ }
+ };
+}`,
+
+ },
+}
+
+describe('extractReturnType', () => {
+ // Python3のテスト
+ describe('Python3', () => {
+ test('extracts single return value', () => {
+ const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.singleReturn, CodeLanguage.python3)
+ expect(result).toEqual({
+ result: {
+ type: VarType.string,
+ children: null,
+ },
+ })
+ })
+
+ test('extracts multiple return values', () => {
+ const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.multipleReturns, CodeLanguage.python3)
+ expect(result).toEqual({
+ result: {
+ type: VarType.string,
+ children: null,
+ },
+ status: {
+ type: VarType.string,
+ children: null,
+ },
+ })
+ })
+
+ test('returns empty object when no return statement', () => {
+ const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.noReturn, CodeLanguage.python3)
+ expect(result).toEqual({})
+ })
+
+ test('handles complex return statement', () => {
+ const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.complexReturn, CodeLanguage.python3)
+ expect(result).toEqual({
+ result: {
+ type: VarType.string,
+ children: null,
+ },
+ count: {
+ type: VarType.string,
+ children: null,
+ },
+ messages: {
+ type: VarType.string,
+ children: null,
+ },
+ })
+ })
+ test('handles nested object structure', () => {
+ const result = extractReturnType(RETURN_TYPE_SAMPLES.python3.nestedObject, CodeLanguage.python3)
+ expect(result).toEqual({
+ personal_info: {
+ type: VarType.string,
+ children: null,
+ },
+ timestamp: {
+ type: VarType.string,
+ children: null,
+ },
+ status: {
+ type: VarType.string,
+ children: null,
+ },
+ })
+ })
+ })
+
+ // JavaScriptのテスト
+ describe('JavaScript', () => {
+ test('extracts single return value', () => {
+ const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.singleReturn, CodeLanguage.javascript)
+ expect(result).toEqual({
+ result: {
+ type: VarType.string,
+ children: null,
+ },
+ })
+ })
+
+ test('extracts multiple return values', () => {
+ const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.multipleReturns, CodeLanguage.javascript)
+ expect(result).toEqual({
+ result: {
+ type: VarType.string,
+ children: null,
+ },
+ status: {
+ type: VarType.string,
+ children: null,
+ },
+ })
+ })
+
+ test('handles return with parentheses', () => {
+ const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.withParentheses, CodeLanguage.javascript)
+ expect(result).toEqual({
+ result: {
+ type: VarType.string,
+ children: null,
+ },
+ status: {
+ type: VarType.string,
+ children: null,
+ },
+ })
+ })
+
+ test('returns empty object when no return statement', () => {
+ const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.noReturn, CodeLanguage.javascript)
+ expect(result).toEqual({})
+ })
+
+ test('handles quoted keys', () => {
+ const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.withQuotes, CodeLanguage.javascript)
+ expect(result).toEqual({
+ result: {
+ type: VarType.string,
+ children: null,
+ },
+ status: {
+ type: VarType.string,
+ children: null,
+ },
+ })
+ })
+ test('handles nested object structure', () => {
+ const result = extractReturnType(RETURN_TYPE_SAMPLES.javascript.nestedObject, CodeLanguage.javascript)
+ expect(result).toEqual({
+ personal_info: {
+ type: VarType.string,
+ children: null,
+ },
+ timestamp: {
+ type: VarType.string,
+ children: null,
+ },
+ status: {
+ type: VarType.string,
+ children: null,
+ },
+ })
+ })
+ })
+})
diff --git a/web/app/components/workflow/nodes/code/code-parser.ts b/web/app/components/workflow/nodes/code/code-parser.ts
new file mode 100644
index 0000000000..e1b0928f14
--- /dev/null
+++ b/web/app/components/workflow/nodes/code/code-parser.ts
@@ -0,0 +1,86 @@
+import { VarType } from '../../types'
+import type { OutputVar } from './types'
+import { CodeLanguage } from './types'
+
+export const extractFunctionParams = (code: string, language: CodeLanguage) => {
+ if (language === CodeLanguage.json)
+ return []
+
+ const patterns: Record, RegExp> = {
+ [CodeLanguage.python3]: /def\s+main\s*\((.*?)\)/,
+ [CodeLanguage.javascript]: /function\s+main\s*\((.*?)\)/,
+ }
+ const match = code.match(patterns[language])
+ const params: string[] = []
+
+ if (match?.[1]) {
+ params.push(...match[1].split(',')
+ .map(p => p.trim())
+ .filter(Boolean)
+ .map(p => p.split(':')[0].trim()),
+ )
+ }
+
+ return params
+}
+export const extractReturnType = (code: string, language: CodeLanguage): OutputVar => {
+ const codeWithoutComments = code.replace(/\/\*\*[\s\S]*?\*\//, '')
+ console.log(codeWithoutComments)
+
+ const returnIndex = codeWithoutComments.indexOf('return')
+ if (returnIndex === -1)
+ return {}
+
+ // returnから始まる部分文字列を取得
+ const codeAfterReturn = codeWithoutComments.slice(returnIndex)
+
+ let bracketCount = 0
+ let startIndex = codeAfterReturn.indexOf('{')
+
+ if (language === CodeLanguage.javascript && startIndex === -1) {
+ const parenStart = codeAfterReturn.indexOf('(')
+ if (parenStart !== -1)
+ startIndex = codeAfterReturn.indexOf('{', parenStart)
+ }
+
+ if (startIndex === -1)
+ return {}
+
+ let endIndex = -1
+
+ for (let i = startIndex; i < codeAfterReturn.length; i++) {
+ if (codeAfterReturn[i] === '{')
+ bracketCount++
+ if (codeAfterReturn[i] === '}') {
+ bracketCount--
+ if (bracketCount === 0) {
+ endIndex = i + 1
+ break
+ }
+ }
+ }
+
+ if (endIndex === -1)
+ return {}
+
+ const returnContent = codeAfterReturn.slice(startIndex + 1, endIndex - 1)
+ console.log(returnContent)
+
+ const result: OutputVar = {}
+
+ const keyRegex = /['"]?(\w+)['"]?\s*:(?![^{]*})/g
+ const matches = returnContent.matchAll(keyRegex)
+
+ for (const match of matches) {
+ console.log(`Found key: "${match[1]}" from match: "${match[0]}"`)
+ const key = match[1]
+ result[key] = {
+ type: VarType.string,
+ children: null,
+ }
+ }
+
+ console.log(result)
+
+ return result
+}
diff --git a/web/app/components/workflow/nodes/code/panel.tsx b/web/app/components/workflow/nodes/code/panel.tsx
index d3e5e58634..08fc565836 100644
--- a/web/app/components/workflow/nodes/code/panel.tsx
+++ b/web/app/components/workflow/nodes/code/panel.tsx
@@ -5,6 +5,7 @@ import RemoveEffectVarConfirm from '../_base/components/remove-effect-var-confir
import useConfig from './use-config'
import type { CodeNodeType } from './types'
import { CodeLanguage } from './types'
+import { extractFunctionParams, extractReturnType } from './code-parser'
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
import OutputVarList from '@/app/components/workflow/nodes/_base/components/variable/output-var-list'
import AddButton from '@/app/components/base/button/add-button'
@@ -12,10 +13,9 @@ import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
-import type { NodePanelProps } from '@/app/components/workflow/types'
+import { type NodePanelProps } from '@/app/components/workflow/types'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
-
const i18nPrefix = 'workflow.nodes.code'
const codeLanguages = [
@@ -38,6 +38,7 @@ const Panel: FC> = ({
readOnly,
inputs,
outputKeyOrders,
+ handleCodeAndVarsChange,
handleVarListChange,
handleAddVariable,
handleRemoveVariable,
@@ -61,6 +62,18 @@ const Panel: FC> = ({
setInputVarValues,
} = useConfig(id, data)
+ const handleGeneratedCode = (value: string) => {
+ const params = extractFunctionParams(value, inputs.code_language)
+ const codeNewInput = params.map((p) => {
+ return {
+ variable: p,
+ value_selector: [],
+ }
+ })
+ const returnTypes = extractReturnType(value, inputs.code_language)
+ handleCodeAndVarsChange(value, codeNewInput, returnTypes)
+ }
+
return (
@@ -92,6 +105,7 @@ const Panel: FC> = ({
language={inputs.code_language}
value={inputs.code}
onChange={handleCodeChange}
+ onGenerated={handleGeneratedCode}
showCodeGenerator={true}
/>
diff --git a/web/app/components/workflow/nodes/code/use-config.ts b/web/app/components/workflow/nodes/code/use-config.ts
index 07fe85aa0f..c53c07a28e 100644
--- a/web/app/components/workflow/nodes/code/use-config.ts
+++ b/web/app/components/workflow/nodes/code/use-config.ts
@@ -3,7 +3,7 @@ import produce from 'immer'
import useVarList from '../_base/hooks/use-var-list'
import useOutputVarList from '../_base/hooks/use-output-var-list'
import { BlockEnum, VarType } from '../../types'
-import type { Var } from '../../types'
+import type { Var, Variable } from '../../types'
import { useStore } from '../../store'
import type { CodeNodeType, OutputVar } from './types'
import { CodeLanguage } from './types'
@@ -136,7 +136,15 @@ const useConfig = (id: string, payload: CodeNodeType) => {
const setInputVarValues = useCallback((newPayload: Record
) => {
setRunInputData(newPayload)
}, [setRunInputData])
-
+ const handleCodeAndVarsChange = useCallback((code: string, inputVariables: Variable[], outputVariables: OutputVar) => {
+ const newInputs = produce(inputs, (draft) => {
+ draft.code = code
+ draft.variables = inputVariables
+ draft.outputs = outputVariables
+ })
+ setInputs(newInputs)
+ syncOutputKeyOrders(outputVariables)
+ }, [inputs, setInputs, syncOutputKeyOrders])
return {
readOnly,
inputs,
@@ -163,6 +171,7 @@ const useConfig = (id: string, payload: CodeNodeType) => {
inputVarValues,
setInputVarValues,
runResult,
+ handleCodeAndVarsChange,
}
}
diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts b/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts
index d280a2d63e..288a718aa2 100644
--- a/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts
+++ b/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts
@@ -240,7 +240,7 @@ const useConfig = (id: string, payload: KnowledgeRetrievalNodeType) => {
if (
(allInternal && (mixtureHighQualityAndEconomic || inconsistentEmbeddingModel))
|| mixtureInternalAndExternal
- || (allExternal && newDatasets.length > 1)
+ || allExternal
)
setRerankModelOpen(true)
}, [inputs, setInputs, payload.retrieval_mode, selectedDatasets, currentRerankModel])