From acd72e3ab24dd43660b0161ed1aec12191792cde Mon Sep 17 00:00:00 2001 From: SoaringEthan <118581835+ethan-fly@users.noreply.github.com> Date: Mon, 19 Aug 2024 12:41:56 +0800 Subject: [PATCH 01/16] feat: support xinference's auth system (#7369) --- .../model_providers/xinference/llm/llm.py | 10 +++++++--- .../model_providers/xinference/rerank/rerank.py | 3 ++- .../xinference/speech2text/speech2text.py | 3 ++- .../xinference/text_embedding/text_embedding.py | 12 ++++++++++-- .../model_providers/xinference/tts/tts.py | 9 +++++++-- .../model_providers/xinference/xinference_helper.py | 9 +++++---- 6 files changed, 33 insertions(+), 13 deletions(-) diff --git a/api/core/model_runtime/model_providers/xinference/llm/llm.py b/api/core/model_runtime/model_providers/xinference/llm/llm.py index 988bb0ce44..4760e8f118 100644 --- a/api/core/model_runtime/model_providers/xinference/llm/llm.py +++ b/api/core/model_runtime/model_providers/xinference/llm/llm.py @@ -85,7 +85,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): tools=tools, stop=stop, stream=stream, user=user, extra_model_kwargs=XinferenceHelper.get_xinference_extra_parameter( server_url=credentials['server_url'], - model_uid=credentials['model_uid'] + model_uid=credentials['model_uid'], + api_key=credentials.get('api_key'), ) ) @@ -106,7 +107,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): extra_param = XinferenceHelper.get_xinference_extra_parameter( server_url=credentials['server_url'], - model_uid=credentials['model_uid'] + model_uid=credentials['model_uid'], + api_key=credentials.get('api_key') ) if 'completion_type' not in credentials: if 'chat' in extra_param.model_ability: @@ -396,7 +398,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): else: extra_args = XinferenceHelper.get_xinference_extra_parameter( server_url=credentials['server_url'], - model_uid=credentials['model_uid'] + model_uid=credentials['model_uid'], + api_key=credentials.get('api_key') ) if 'chat' in extra_args.model_ability: @@ -464,6 +467,7 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel): xinference_client = Client( base_url=credentials['server_url'], + api_key=credentials.get('api_key'), ) xinference_model = xinference_client.get_model(credentials['model_uid']) diff --git a/api/core/model_runtime/model_providers/xinference/rerank/rerank.py b/api/core/model_runtime/model_providers/xinference/rerank/rerank.py index 4e7543fd99..d809537479 100644 --- a/api/core/model_runtime/model_providers/xinference/rerank/rerank.py +++ b/api/core/model_runtime/model_providers/xinference/rerank/rerank.py @@ -108,7 +108,8 @@ class XinferenceRerankModel(RerankModel): # initialize client client = Client( - base_url=credentials['server_url'] + base_url=credentials['server_url'], + api_key=credentials.get('api_key'), ) xinference_client = client.get_model(model_uid=credentials['model_uid']) diff --git a/api/core/model_runtime/model_providers/xinference/speech2text/speech2text.py b/api/core/model_runtime/model_providers/xinference/speech2text/speech2text.py index 9ee3621317..62b77f22e5 100644 --- a/api/core/model_runtime/model_providers/xinference/speech2text/speech2text.py +++ b/api/core/model_runtime/model_providers/xinference/speech2text/speech2text.py @@ -52,7 +52,8 @@ class XinferenceSpeech2TextModel(Speech2TextModel): # initialize client client = Client( - base_url=credentials['server_url'] + base_url=credentials['server_url'], + api_key=credentials.get('api_key'), ) xinference_client = client.get_model(model_uid=credentials['model_uid']) diff --git a/api/core/model_runtime/model_providers/xinference/text_embedding/text_embedding.py b/api/core/model_runtime/model_providers/xinference/text_embedding/text_embedding.py index 11f1e29cb3..3a8d704c25 100644 --- a/api/core/model_runtime/model_providers/xinference/text_embedding/text_embedding.py +++ b/api/core/model_runtime/model_providers/xinference/text_embedding/text_embedding.py @@ -110,14 +110,22 @@ class XinferenceTextEmbeddingModel(TextEmbeddingModel): server_url = credentials['server_url'] model_uid = credentials['model_uid'] - extra_args = XinferenceHelper.get_xinference_extra_parameter(server_url=server_url, model_uid=model_uid) + api_key = credentials.get('api_key') + extra_args = XinferenceHelper.get_xinference_extra_parameter( + server_url=server_url, + model_uid=model_uid, + api_key=api_key, + ) if extra_args.max_tokens: credentials['max_tokens'] = extra_args.max_tokens if server_url.endswith('/'): server_url = server_url[:-1] - client = Client(base_url=server_url) + client = Client( + base_url=server_url, + api_key=api_key, + ) try: handle = client.get_model(model_uid=model_uid) diff --git a/api/core/model_runtime/model_providers/xinference/tts/tts.py b/api/core/model_runtime/model_providers/xinference/tts/tts.py index a564a021b1..bfa752df8c 100644 --- a/api/core/model_runtime/model_providers/xinference/tts/tts.py +++ b/api/core/model_runtime/model_providers/xinference/tts/tts.py @@ -81,7 +81,8 @@ class XinferenceText2SpeechModel(TTSModel): extra_param = XinferenceHelper.get_xinference_extra_parameter( server_url=credentials['server_url'], - model_uid=credentials['model_uid'] + model_uid=credentials['model_uid'], + api_key=credentials.get('api_key'), ) if 'text-to-audio' not in extra_param.model_ability: @@ -203,7 +204,11 @@ class XinferenceText2SpeechModel(TTSModel): credentials['server_url'] = credentials['server_url'][:-1] try: - handle = RESTfulAudioModelHandle(credentials['model_uid'], credentials['server_url'], auth_headers={}) + api_key = credentials.get('api_key') + auth_headers = {'Authorization': f'Bearer {api_key}'} if api_key else {} + handle = RESTfulAudioModelHandle( + credentials['model_uid'], credentials['server_url'], auth_headers=auth_headers + ) model_support_voice = [x.get("value") for x in self.get_tts_model_voices(model=model, credentials=credentials)] diff --git a/api/core/model_runtime/model_providers/xinference/xinference_helper.py b/api/core/model_runtime/model_providers/xinference/xinference_helper.py index 7db483a485..75161ad376 100644 --- a/api/core/model_runtime/model_providers/xinference/xinference_helper.py +++ b/api/core/model_runtime/model_providers/xinference/xinference_helper.py @@ -35,13 +35,13 @@ cache_lock = Lock() class XinferenceHelper: @staticmethod - def get_xinference_extra_parameter(server_url: str, model_uid: str) -> XinferenceModelExtraParameter: + def get_xinference_extra_parameter(server_url: str, model_uid: str, api_key: str) -> XinferenceModelExtraParameter: XinferenceHelper._clean_cache() with cache_lock: if model_uid not in cache: cache[model_uid] = { 'expires': time() + 300, - 'value': XinferenceHelper._get_xinference_extra_parameter(server_url, model_uid) + 'value': XinferenceHelper._get_xinference_extra_parameter(server_url, model_uid, api_key) } return cache[model_uid]['value'] @@ -56,7 +56,7 @@ class XinferenceHelper: pass @staticmethod - def _get_xinference_extra_parameter(server_url: str, model_uid: str) -> XinferenceModelExtraParameter: + def _get_xinference_extra_parameter(server_url: str, model_uid: str, api_key: str) -> XinferenceModelExtraParameter: """ get xinference model extra parameter like model_format and model_handle_type """ @@ -70,9 +70,10 @@ class XinferenceHelper: session = Session() session.mount('http://', HTTPAdapter(max_retries=3)) session.mount('https://', HTTPAdapter(max_retries=3)) + headers = {'Authorization': f'Bearer {api_key}'} if api_key else {} try: - response = session.get(url, timeout=10) + response = session.get(url, headers=headers, timeout=10) except (MissingSchema, ConnectionError, Timeout) as e: raise RuntimeError(f'get xinference model extra parameter failed, url: {url}, error: {e}') if response.status_code != 200: From 68dc6d5bc3c1d7756cc2d7f7b223f4a3ac52e0cb Mon Sep 17 00:00:00 2001 From: Bowen Liang Date: Mon, 19 Aug 2024 14:05:41 +0800 Subject: [PATCH 02/16] chore: rearrange api python dependencies (#7391) --- api/poetry.lock | 2 +- api/pyproject.toml | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/api/poetry.lock b/api/poetry.lock index 0527507bff..9bfeec30d7 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -9584,4 +9584,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "165e4af9cfbce83ee831dd0e82159446ef595d7a7850ee8644c8e2d24dd7040d" +content-hash = "a74c7b6a72145d5074aa84581df6e543ea422810caf0ba1561cd2d35497243ca" diff --git a/api/pyproject.toml b/api/pyproject.toml index f0df3e5a0e..82ccd0b202 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -156,6 +156,7 @@ markdown = "~3.5.1" novita-client = "^0.5.6" numpy = "~1.26.4" openai = "~1.29.0" +openpyxl = "~3.1.5" oss2 = "2.18.5" pandas = { version = "~2.2.2", extras = ["performance", "excel"] } psycopg2-binary = "~2.9.6" @@ -173,7 +174,6 @@ readabilipy = "0.2.0" redis = { version = "~5.0.3", extras = ["hiredis"] } replicate = "~0.22.0" resend = "~0.7.0" -safetensors = "~0.4.3" scikit-learn = "^1.5.1" sentry-sdk = { version = "~1.44.1", extras = ["flask"] } sqlalchemy = "~2.0.29" @@ -187,10 +187,16 @@ werkzeug = "~3.0.1" xinference-client = "0.13.3" yarl = "~1.9.4" zhipuai = "1.0.7" -rank-bm25 = "~0.2.2" -openpyxl = "^3.1.5" +# Before adding new dependency, consider place it in alphabet order (a-z) and suitable group. + +############################################################ +# Related transparent dependencies with pinned verion +# required by main implementations +############################################################ +[tool.poetry.group.indriect.dependencies] kaleido = "0.2.1" -elasticsearch = "8.14.0" +rank-bm25 = "~0.2.2" +safetensors = "~0.4.3" ############################################################ # Tool dependencies required by tool implementations @@ -198,6 +204,7 @@ elasticsearch = "8.14.0" [tool.poetry.group.tool.dependencies] arxiv = "2.1.0" +cloudscraper = "1.2.71" matplotlib = "~3.8.2" newspaper3k = "0.2.8" duckduckgo-search = "^6.2.6" @@ -209,26 +216,25 @@ twilio = "~9.0.4" vanna = { version = "0.5.5", extras = ["postgres", "mysql", "clickhouse", "duckdb"] } wikipedia = "1.4.0" yfinance = "~0.2.40" -cloudscraper = "1.2.71" ############################################################ # VDB dependencies required by vector store clients ############################################################ [tool.poetry.group.vdb.dependencies] +alibabacloud_gpdb20160503 = "~3.8.0" +alibabacloud_tea_openapi = "~0.3.9" chromadb = "0.5.1" +clickhouse-connect = "~0.7.16" +elasticsearch = "8.14.0" oracledb = "~2.2.1" pgvecto-rs = { version = "~0.2.1", extras = ['sqlalchemy'] } pgvector = "0.2.5" pymilvus = "~2.4.4" -pymysql = "1.1.1" tcvectordb = "1.3.2" tidb-vector = "0.0.9" qdrant-client = "1.7.3" weaviate-client = "~3.21.0" -alibabacloud_gpdb20160503 = "~3.8.0" -alibabacloud_tea_openapi = "~0.3.9" -clickhouse-connect = "~0.7.16" ############################################################ # Dev dependencies for running tests @@ -252,5 +258,5 @@ pytest-mock = "~3.14.0" optional = true [tool.poetry.group.lint.dependencies] -ruff = "~0.6.1" dotenv-linter = "~0.5.0" +ruff = "~0.6.1" From 8b06105fa1a727285312f01e3644879dc7535b85 Mon Sep 17 00:00:00 2001 From: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:11:11 +0800 Subject: [PATCH 03/16] Feat: shortcut hook (#7385) --- .../workflow/header/run-and-history.tsx | 4 +- .../workflow/header/view-history.tsx | 4 +- web/app/components/workflow/hooks/index.ts | 5 +- .../workflow/hooks/use-nodes-interactions.ts | 53 ++--- .../workflow/hooks/use-nodes-sync-draft.ts | 4 +- .../workflow/hooks/use-shortcuts.ts | 186 ++++++++++++++++++ .../hooks/use-workflow-interactions.ts | 172 +++++++++++++++- .../components/workflow/hooks/use-workflow.ts | 76 ------- web/app/components/workflow/index.tsx | 56 +----- .../components/workflow/operator/control.tsx | 69 +------ .../workflow/operator/zoom-in-out.tsx | 89 --------- web/app/components/workflow/panel/index.tsx | 7 - web/app/components/workflow/store.ts | 4 - web/app/components/workflow/types.ts | 5 + 14 files changed, 402 insertions(+), 332 deletions(-) create mode 100644 web/app/components/workflow/hooks/use-shortcuts.ts diff --git a/web/app/components/workflow/header/run-and-history.tsx b/web/app/components/workflow/header/run-and-history.tsx index 047104912c..d3c5b99964 100644 --- a/web/app/components/workflow/header/run-and-history.tsx +++ b/web/app/components/workflow/header/run-and-history.tsx @@ -35,7 +35,9 @@ const RunMode = memo(() => { 'hover:bg-state-accent-hover cursor-pointer', isRunning && 'bg-state-accent-hover !cursor-not-allowed', )} - onClick={() => handleWorkflowStartRunInWorkflow()} + onClick={() => { + handleWorkflowStartRunInWorkflow() + }} > { isRunning diff --git a/web/app/components/workflow/header/view-history.tsx b/web/app/components/workflow/header/view-history.tsx index 6711ff2589..a6318dbfeb 100644 --- a/web/app/components/workflow/header/view-history.tsx +++ b/web/app/components/workflow/header/view-history.tsx @@ -17,7 +17,7 @@ import { useWorkflowInteractions, useWorkflowRun, } from '../hooks' -import { WorkflowRunningStatus } from '../types' +import { ControlMode, WorkflowRunningStatus } from '../types' import cn from '@/utils/classnames' import { PortalToFollowElem, @@ -58,6 +58,7 @@ const ViewHistory = ({ handleCancelDebugAndPreviewPanel, } = useWorkflowInteractions() const workflowStore = useWorkflowStore() + const setControlMode = useStore(s => s.setControlMode) const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({ appDetail: state.appDetail, setCurrentLogItem: state.setCurrentLogItem, @@ -173,6 +174,7 @@ const ViewHistory = ({ setOpen(false) handleNodesCancelSelected() handleCancelDebugAndPreviewPanel() + setControlMode(ControlMode.Hand) }} > { diff --git a/web/app/components/workflow/hooks/index.ts b/web/app/components/workflow/hooks/index.ts index b2aa958025..463e9b3271 100644 --- a/web/app/components/workflow/hooks/index.ts +++ b/web/app/components/workflow/hooks/index.ts @@ -7,11 +7,12 @@ export * from './use-workflow' export * from './use-workflow-run' export * from './use-workflow-template' export * from './use-checklist' -export * from './use-workflow-mode' -export * from './use-workflow-interactions' export * from './use-selection-interactions' export * from './use-panel-interactions' export * from './use-workflow-start-run' export * from './use-nodes-layout' export * from './use-workflow-history' export * from './use-workflow-variables' +export * from './use-shortcuts' +export * from './use-workflow-interactions' +export * from './use-workflow-mode' diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 914901ebb1..87d1b4de8c 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -48,6 +48,7 @@ import { useHelpline } from './use-helpline' import { useNodesReadOnly, useWorkflow, + useWorkflowReadOnly, } from './use-workflow' import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' @@ -62,6 +63,7 @@ export const useNodesInteractions = () => { getAfterNodesInSameBranch, } = useWorkflow() const { getNodesReadOnly } = useNodesReadOnly() + const { getWorkflowReadOnly } = useWorkflowReadOnly() const { handleSetHelpline } = useHelpline() const { handleNodeIterationChildDrag, @@ -1029,14 +1031,7 @@ export const useNodesInteractions = () => { if (getNodesReadOnly()) return - const { - setClipboardElements, - shortcutsDisabled, - showFeaturesPanel, - } = workflowStore.getState() - - if (shortcutsDisabled || showFeaturesPanel) - return + const { setClipboardElements } = workflowStore.getState() const { getNodes, @@ -1062,14 +1057,9 @@ export const useNodesInteractions = () => { const { clipboardElements, - shortcutsDisabled, - showFeaturesPanel, mousePosition, } = workflowStore.getState() - if (shortcutsDisabled || showFeaturesPanel) - return - const { getNodes, setNodes, @@ -1107,6 +1097,11 @@ export const useNodesInteractions = () => { }) newNode.id = newNode.id + index + // If only the iteration start node is copied, remove the isIterationStart flag + // This new node is movable and can be placed anywhere + if (clipboardElements.length === 1 && newNode.data.isIterationStart) + newNode.data.isIterationStart = false + let newChildren: Node[] = [] if (nodeToPaste.data.type === BlockEnum.Iteration) { newNode.data._children = []; @@ -1145,14 +1140,6 @@ export const useNodesInteractions = () => { if (getNodesReadOnly()) return - const { - shortcutsDisabled, - showFeaturesPanel, - } = workflowStore.getState() - - if (shortcutsDisabled || showFeaturesPanel) - return - const { getNodes, edges, @@ -1175,7 +1162,7 @@ export const useNodesInteractions = () => { if (selectedNode) handleNodeDelete(selectedNode.id) - }, [store, workflowStore, getNodesReadOnly, handleNodeDelete]) + }, [store, getNodesReadOnly, handleNodeDelete]) const handleNodeResize = useCallback((nodeId: string, params: ResizeParamsWithDirection) => { if (getNodesReadOnly()) @@ -1234,14 +1221,7 @@ export const useNodesInteractions = () => { }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory]) const handleHistoryBack = useCallback(() => { - if (getNodesReadOnly()) - return - - const { - shortcutsDisabled, - } = workflowStore.getState() - - if (shortcutsDisabled) + if (getNodesReadOnly() || getWorkflowReadOnly()) return const { setEdges, setNodes } = store.getState() @@ -1253,17 +1233,10 @@ export const useNodesInteractions = () => { setEdges(edges) setNodes(nodes) - }, [store, undo, workflowHistoryStore, workflowStore, getNodesReadOnly]) + }, [store, undo, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly]) const handleHistoryForward = useCallback(() => { - if (getNodesReadOnly()) - return - - const { - shortcutsDisabled, - } = workflowStore.getState() - - if (shortcutsDisabled) + if (getNodesReadOnly() || getWorkflowReadOnly()) return const { setEdges, setNodes } = store.getState() @@ -1275,7 +1248,7 @@ export const useNodesInteractions = () => { setEdges(edges) setNodes(nodes) - }, [redo, store, workflowHistoryStore, workflowStore, getNodesReadOnly]) + }, [redo, store, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly]) return { handleNodeDragStart, diff --git a/web/app/components/workflow/hooks/use-nodes-sync-draft.ts b/web/app/components/workflow/hooks/use-nodes-sync-draft.ts index 06d0113df6..203386f5a7 100644 --- a/web/app/components/workflow/hooks/use-nodes-sync-draft.ts +++ b/web/app/components/workflow/hooks/use-nodes-sync-draft.ts @@ -8,7 +8,9 @@ import { } from '../store' import { BlockEnum } from '../types' import { useWorkflowUpdate } from '../hooks' -import { useNodesReadOnly } from './use-workflow' +import { + useNodesReadOnly, +} from './use-workflow' import { syncWorkflowDraft } from '@/service/workflow' import { useFeaturesStore } from '@/app/components/base/features/hooks' import { API_PREFIX } from '@/config' diff --git a/web/app/components/workflow/hooks/use-shortcuts.ts b/web/app/components/workflow/hooks/use-shortcuts.ts new file mode 100644 index 0000000000..9484f9c16e --- /dev/null +++ b/web/app/components/workflow/hooks/use-shortcuts.ts @@ -0,0 +1,186 @@ +import { useReactFlow } from 'reactflow' +import { useKeyPress } from 'ahooks' +import { useCallback } from 'react' +import { + getKeyboardKeyCodeBySystem, + isEventTargetInputArea, +} from '../utils' +import { useWorkflowHistoryStore } from '../workflow-history-store' +import { useWorkflowStore } from '../store' +import { + useEdgesInteractions, + useNodesInteractions, + useNodesSyncDraft, + useWorkflowMoveMode, + useWorkflowOrganize, + useWorkflowStartRun, +} from '.' + +export const useShortcuts = (): void => { + const { + handleNodesCopy, + handleNodesPaste, + handleNodesDuplicate, + handleNodesDelete, + handleHistoryBack, + handleHistoryForward, + } = useNodesInteractions() + const { handleStartWorkflowRun } = useWorkflowStartRun() + const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore() + const { handleSyncWorkflowDraft } = useNodesSyncDraft() + const { handleEdgeDelete } = useEdgesInteractions() + const workflowStore = useWorkflowStore() + const { + handleModeHand, + handleModePointer, + } = useWorkflowMoveMode() + const { handleLayout } = useWorkflowOrganize() + + const { + zoomIn, + zoomOut, + zoomTo, + fitView, + } = useReactFlow() + + const shouldHandleShortcut = useCallback((e: KeyboardEvent) => { + const { showFeaturesPanel } = workflowStore.getState() + return !showFeaturesPanel && !isEventTargetInputArea(e.target as HTMLElement) + }, [workflowStore]) + + useKeyPress(['delete', 'backspace'], (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + handleNodesDelete() + handleEdgeDelete() + } + }) + + useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + handleNodesCopy() + } + }, { exactMatch: true, useCapture: true }) + + useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.v`, (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + handleNodesPaste() + } + }, { exactMatch: true, useCapture: true }) + + useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.d`, (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + handleNodesDuplicate() + } + }, { exactMatch: true, useCapture: true }) + + useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + handleStartWorkflowRun() + } + }, { exactMatch: true, useCapture: true }) + + useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.z`, (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + workflowHistoryShortcutsEnabled && handleHistoryBack() + } + }, { exactMatch: true, useCapture: true }) + + useKeyPress( + [`${getKeyboardKeyCodeBySystem('ctrl')}.y`, `${getKeyboardKeyCodeBySystem('ctrl')}.shift.z`], + (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + workflowHistoryShortcutsEnabled && handleHistoryForward() + } + }, + { exactMatch: true, useCapture: true }, + ) + + useKeyPress('h', (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + handleModeHand() + } + }, { + exactMatch: true, + useCapture: true, + }) + + useKeyPress('v', (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + handleModePointer() + } + }, { + exactMatch: true, + useCapture: true, + }) + + useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.o`, (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + handleLayout() + } + }, { exactMatch: true, useCapture: true }) + + useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + fitView() + handleSyncWorkflowDraft() + } + }, { + exactMatch: true, + useCapture: true, + }) + + useKeyPress('shift.1', (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + zoomTo(1) + handleSyncWorkflowDraft() + } + }, { + exactMatch: true, + useCapture: true, + }) + + useKeyPress('shift.5', (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + zoomTo(0.5) + handleSyncWorkflowDraft() + } + }, { + exactMatch: true, + useCapture: true, + }) + + useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.dash`, (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + zoomOut() + handleSyncWorkflowDraft() + } + }, { + exactMatch: true, + useCapture: true, + }) + + useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.equalsign`, (e) => { + if (shouldHandleShortcut(e)) { + e.preventDefault() + zoomIn() + handleSyncWorkflowDraft() + } + }, { + exactMatch: true, + useCapture: true, + }) +} diff --git a/web/app/components/workflow/hooks/use-workflow-interactions.ts b/web/app/components/workflow/hooks/use-workflow-interactions.ts index 820d5fe2fa..47b8a30a5a 100644 --- a/web/app/components/workflow/hooks/use-workflow-interactions.ts +++ b/web/app/components/workflow/hooks/use-workflow-interactions.ts @@ -3,17 +3,29 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' -import { useReactFlow } from 'reactflow' -import { useWorkflowStore } from '../store' -import { DSL_EXPORT_CHECK, WORKFLOW_DATA_UPDATE } from '../constants' -import type { WorkflowDataUpdator } from '../types' +import { useReactFlow, useStoreApi } from 'reactflow' +import produce from 'immer' +import { useStore, useWorkflowStore } from '../store' import { + CUSTOM_NODE, DSL_EXPORT_CHECK, + WORKFLOW_DATA_UPDATE, +} from '../constants' +import type { Node, WorkflowDataUpdator } from '../types' +import { ControlMode } from '../types' +import { + getLayoutByDagre, initialEdges, initialNodes, } from '../utils' +import { + useNodesReadOnly, + useSelectionInteractions, + useWorkflowReadOnly, +} from '../hooks' import { useEdgesInteractions } from './use-edges-interactions' import { useNodesInteractions } from './use-nodes-interactions' import { useNodesSyncDraft } from './use-nodes-sync-draft' +import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' import { useEventEmitterContextContext } from '@/context/event-emitter' import { fetchWorkflowDraft } from '@/service/workflow' import { exportAppConfig } from '@/service/apps' @@ -39,6 +51,158 @@ export const useWorkflowInteractions = () => { } } +export const useWorkflowMoveMode = () => { + const setControlMode = useStore(s => s.setControlMode) + const { + getNodesReadOnly, + } = useNodesReadOnly() + const { handleSelectionCancel } = useSelectionInteractions() + + const handleModePointer = useCallback(() => { + if (getNodesReadOnly()) + return + + setControlMode(ControlMode.Pointer) + }, [getNodesReadOnly, setControlMode]) + + const handleModeHand = useCallback(() => { + if (getNodesReadOnly()) + return + + setControlMode(ControlMode.Hand) + handleSelectionCancel() + }, [getNodesReadOnly, setControlMode, handleSelectionCancel]) + + return { + handleModePointer, + handleModeHand, + } +} + +export const useWorkflowOrganize = () => { + const workflowStore = useWorkflowStore() + const store = useStoreApi() + const reactflow = useReactFlow() + const { getNodesReadOnly } = useNodesReadOnly() + const { saveStateToHistory } = useWorkflowHistory() + const { handleSyncWorkflowDraft } = useNodesSyncDraft() + + const handleLayout = useCallback(async () => { + if (getNodesReadOnly()) + return + workflowStore.setState({ nodeAnimation: true }) + const { + getNodes, + edges, + setNodes, + } = store.getState() + const { setViewport } = reactflow + const nodes = getNodes() + const layout = getLayoutByDagre(nodes, edges) + const rankMap = {} as Record + + nodes.forEach((node) => { + if (!node.parentId && node.type === CUSTOM_NODE) { + const rank = layout.node(node.id).rank! + + if (!rankMap[rank]) { + rankMap[rank] = node + } + else { + if (rankMap[rank].position.y > node.position.y) + rankMap[rank] = node + } + } + }) + + const newNodes = produce(nodes, (draft) => { + draft.forEach((node) => { + if (!node.parentId && node.type === CUSTOM_NODE) { + const nodeWithPosition = layout.node(node.id) + + node.position = { + x: nodeWithPosition.x - node.width! / 2, + y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2, + } + } + }) + }) + setNodes(newNodes) + const zoom = 0.7 + setViewport({ + x: 0, + y: 0, + zoom, + }) + saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize) + setTimeout(() => { + handleSyncWorkflowDraft() + }) + }, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory]) + return { + handleLayout, + } +} + +export const useWorkflowZoom = () => { + const { handleSyncWorkflowDraft } = useNodesSyncDraft() + const { getWorkflowReadOnly } = useWorkflowReadOnly() + const { + zoomIn, + zoomOut, + zoomTo, + fitView, + } = useReactFlow() + + const handleFitView = useCallback(() => { + if (getWorkflowReadOnly()) + return + + fitView() + handleSyncWorkflowDraft() + }, [getWorkflowReadOnly, fitView, handleSyncWorkflowDraft]) + + const handleBackToOriginalSize = useCallback(() => { + if (getWorkflowReadOnly()) + return + + zoomTo(1) + handleSyncWorkflowDraft() + }, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft]) + + const handleSizeToHalf = useCallback(() => { + if (getWorkflowReadOnly()) + return + + zoomTo(0.5) + handleSyncWorkflowDraft() + }, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft]) + + const handleZoomOut = useCallback(() => { + if (getWorkflowReadOnly()) + return + + zoomOut() + handleSyncWorkflowDraft() + }, [getWorkflowReadOnly, zoomOut, handleSyncWorkflowDraft]) + + const handleZoomIn = useCallback(() => { + if (getWorkflowReadOnly()) + return + + zoomIn() + handleSyncWorkflowDraft() + }, [getWorkflowReadOnly, zoomIn, handleSyncWorkflowDraft]) + + return { + handleFitView, + handleBackToOriginalSize, + handleSizeToHalf, + handleZoomOut, + handleZoomIn, + } +} + export const useWorkflowUpdate = () => { const reactflow = useReactFlow() const workflowStore = useWorkflowStore() diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 71f0600d39..cfff4220fa 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -7,19 +7,14 @@ import { import dayjs from 'dayjs' import { uniqBy } from 'lodash-es' import { useContext } from 'use-context-selector' -import produce from 'immer' import { getIncomers, getOutgoers, - useReactFlow, useStoreApi, } from 'reactflow' import type { Connection, } from 'reactflow' -import { - getLayoutByDagre, -} from '../utils' import type { Edge, Node, @@ -34,15 +29,12 @@ import { useWorkflowStore, } from '../store' import { - CUSTOM_NODE, SUPPORT_OUTPUT_VARS_NODE, } from '../constants' import { CUSTOM_NOTE_NODE } from '../note-node/constants' import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils' import { useNodesExtraData } from './use-nodes-data' import { useWorkflowTemplate } from './use-workflow-template' -import { useNodesSyncDraft } from './use-nodes-sync-draft' -import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' import { useStore as useAppStore } from '@/app/components/app/store' import { fetchNodesDefaultConfigs, @@ -68,68 +60,13 @@ export const useIsChatMode = () => { export const useWorkflow = () => { const { locale } = useContext(I18n) const store = useStoreApi() - const reactflow = useReactFlow() const workflowStore = useWorkflowStore() const nodesExtraData = useNodesExtraData() - const { handleSyncWorkflowDraft } = useNodesSyncDraft() - const { saveStateToHistory } = useWorkflowHistory() - const setPanelWidth = useCallback((width: number) => { localStorage.setItem('workflow-node-panel-width', `${width}`) workflowStore.setState({ panelWidth: width }) }, [workflowStore]) - const handleLayout = useCallback(async () => { - workflowStore.setState({ nodeAnimation: true }) - const { - getNodes, - edges, - setNodes, - } = store.getState() - const { setViewport } = reactflow - const nodes = getNodes() - const layout = getLayoutByDagre(nodes, edges) - const rankMap = {} as Record - - nodes.forEach((node) => { - if (!node.parentId && node.type === CUSTOM_NODE) { - const rank = layout.node(node.id).rank! - - if (!rankMap[rank]) { - rankMap[rank] = node - } - else { - if (rankMap[rank].position.y > node.position.y) - rankMap[rank] = node - } - } - }) - - const newNodes = produce(nodes, (draft) => { - draft.forEach((node) => { - if (!node.parentId && node.type === CUSTOM_NODE) { - const nodeWithPosition = layout.node(node.id) - - node.position = { - x: nodeWithPosition.x - node.width! / 2, - y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2, - } - } - }) - }) - setNodes(newNodes) - const zoom = 0.7 - setViewport({ - x: 0, - y: 0, - zoom, - }) - saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize) - setTimeout(() => { - handleSyncWorkflowDraft() - }) - }, [workflowStore, store, reactflow, saveStateToHistory, handleSyncWorkflowDraft]) - const getTreeLeafNodes = useCallback((nodeId: string) => { const { getNodes, @@ -392,19 +329,8 @@ export const useWorkflow = () => { return nodes.find(node => node.id === nodeId) || nodes.find(node => node.data.type === BlockEnum.Start) }, [store]) - const enableShortcuts = useCallback(() => { - const { setShortcutsDisabled } = workflowStore.getState() - setShortcutsDisabled(false) - }, [workflowStore]) - - const disableShortcuts = useCallback(() => { - const { setShortcutsDisabled } = workflowStore.getState() - setShortcutsDisabled(true) - }, [workflowStore]) - return { setPanelWidth, - handleLayout, getTreeLeafNodes, getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent, @@ -418,8 +344,6 @@ export const useWorkflow = () => { getNode, getBeforeNodeById, getIterationNodeChildren, - enableShortcuts, - disableShortcuts, } } diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index c06e1b9524..d96faa8677 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -12,7 +12,6 @@ import { import { setAutoFreeze } from 'immer' import { useEventListener, - useKeyPress, } from 'ahooks' import ReactFlow, { Background, @@ -34,6 +33,9 @@ import type { EnvironmentVariable, Node, } from './types' +import { + ControlMode, +} from './types' import { WorkflowContextProvider } from './context' import { useDSL, @@ -43,10 +45,10 @@ import { useNodesSyncDraft, usePanelInteractions, useSelectionInteractions, + useShortcuts, useWorkflow, useWorkflowInit, useWorkflowReadOnly, - useWorkflowStartRun, useWorkflowUpdate, } from './hooks' import Header from './header' @@ -70,10 +72,8 @@ import { useWorkflowStore, } from './store' import { - getKeyboardKeyCodeBySystem, initialEdges, initialNodes, - isEventTargetInputArea, } from './utils' import { CUSTOM_NODE, @@ -81,7 +81,7 @@ import { ITERATION_CHILDREN_Z_INDEX, WORKFLOW_DATA_UPDATE, } from './constants' -import { WorkflowHistoryProvider, useWorkflowHistoryStore } from './workflow-history-store' +import { WorkflowHistoryProvider } from './workflow-history-store' import Loading from '@/app/components/base/loading' import { FeaturesProvider } from '@/app/components/base/features' import type { Features as FeaturesData } from '@/app/components/base/features/types' @@ -225,17 +225,12 @@ const Workflow: FC = memo(({ handleNodeConnectStart, handleNodeConnectEnd, handleNodeContextMenu, - handleNodesCopy, - handleNodesPaste, - handleNodesDuplicate, - handleNodesDelete, handleHistoryBack, handleHistoryForward, } = useNodesInteractions() const { handleEdgeEnter, handleEdgeLeave, - handleEdgeDelete, handleEdgesChange, } = useEdgesInteractions() const { @@ -250,7 +245,6 @@ const Workflow: FC = memo(({ const { isValidConnection, } = useWorkflow() - const { handleStartWorkflowRun } = useWorkflowStartRun() const { exportCheck, handleExportDSL, @@ -262,41 +256,7 @@ const Workflow: FC = memo(({ }, }) - const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore() - - useKeyPress(['delete', 'backspace'], (e) => { - if (isEventTargetInputArea(e.target as HTMLElement)) - return - - handleNodesDelete() - }) - useKeyPress(['delete', 'backspace'], handleEdgeDelete) - useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, (e) => { - if (isEventTargetInputArea(e.target as HTMLElement)) - return - - handleNodesCopy() - }, { exactMatch: true, useCapture: true }) - useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.v`, (e) => { - if (isEventTargetInputArea(e.target as HTMLElement)) - return - - handleNodesPaste() - }, { exactMatch: true, useCapture: true }) - useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.d`, handleNodesDuplicate, { exactMatch: true, useCapture: true }) - useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, handleStartWorkflowRun, { exactMatch: true, useCapture: true }) - useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, handleStartWorkflowRun, { exactMatch: true, useCapture: true }) - useKeyPress( - `${getKeyboardKeyCodeBySystem('ctrl')}.z`, - () => workflowHistoryShortcutsEnabled && handleHistoryBack(), - { exactMatch: true, useCapture: true }, - ) - - useKeyPress( - [`${getKeyboardKeyCodeBySystem('ctrl')}.y`, `${getKeyboardKeyCodeBySystem('ctrl')}.shift.z`], - () => workflowHistoryShortcutsEnabled && handleHistoryForward(), - { exactMatch: true, useCapture: true }, - ) + useShortcuts() const store = useStoreApi() if (process.env.NODE_ENV === 'development') { @@ -388,14 +348,14 @@ const Workflow: FC = memo(({ nodesConnectable={!nodesReadOnly} nodesFocusable={!nodesReadOnly} edgesFocusable={!nodesReadOnly} - panOnDrag={controlMode === 'hand' && !workflowReadOnly} + panOnDrag={controlMode === ControlMode.Hand && !workflowReadOnly} zoomOnPinch={!workflowReadOnly} zoomOnScroll={!workflowReadOnly} zoomOnDoubleClick={!workflowReadOnly} isValidConnection={isValidConnection} selectionKeyCode={null} selectionMode={SelectionMode.Partial} - selectionOnDrag={controlMode === 'pointer' && !workflowReadOnly} + selectionOnDrag={controlMode === ControlMode.Pointer && !workflowReadOnly} minZoom={0.25} > { const { t } = useTranslation() const controlMode = useStore(s => s.controlMode) - const setControlMode = useStore(s => s.setControlMode) - const { handleLayout } = useWorkflow() + const { handleModePointer, handleModeHand } = useWorkflowMoveMode() + const { handleLayout } = useWorkflowOrganize() const { handleAddNote } = useOperator() const { nodesReadOnly, getNodesReadOnly, } = useNodesReadOnly() - const { handleSelectionCancel } = useSelectionInteractions() - - const handleModePointer = useCallback(() => { - if (getNodesReadOnly()) - return - setControlMode('pointer') - }, [getNodesReadOnly, setControlMode]) - const handleModeHand = useCallback(() => { - if (getNodesReadOnly()) - return - setControlMode('hand') - handleSelectionCancel() - }, [getNodesReadOnly, setControlMode, handleSelectionCancel]) - - useKeyPress('h', (e) => { - if (getNodesReadOnly()) - return - - if (isEventTargetInputArea(e.target as HTMLElement)) - return - - e.preventDefault() - handleModeHand() - }, { - exactMatch: true, - useCapture: true, - }) - - useKeyPress('v', (e) => { - if (isEventTargetInputArea(e.target as HTMLElement)) - return - - e.preventDefault() - handleModePointer() - }, { - exactMatch: true, - useCapture: true, - }) - - const goLayout = () => { - if (getNodesReadOnly()) - return - handleLayout() - } - - useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.o`, (e) => { - e.preventDefault() - goLayout() - }, { exactMatch: true, useCapture: true }) const addNote = (e: MouseEvent) => { if (getNodesReadOnly()) @@ -110,7 +61,7 @@ const Control = () => {
{
{ 'flex items-center justify-center w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer', `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`, )} - onClick={goLayout} + onClick={handleLayout} >
diff --git a/web/app/components/workflow/operator/zoom-in-out.tsx b/web/app/components/workflow/operator/zoom-in-out.tsx index 13047cdafc..654097b430 100644 --- a/web/app/components/workflow/operator/zoom-in-out.tsx +++ b/web/app/components/workflow/operator/zoom-in-out.tsx @@ -9,7 +9,6 @@ import { RiZoomInLine, RiZoomOutLine, } from '@remixicon/react' -import { useKeyPress } from 'ahooks' import { useTranslation } from 'react-i18next' import { useReactFlow, @@ -20,9 +19,7 @@ import { useWorkflowReadOnly, } from '../hooks' import { - getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem, - isEventTargetInputArea, } from '../utils' import ShortcutsName from '../shortcuts-name' import TipPopup from './tip-popup' @@ -116,87 +113,6 @@ const ZoomInOut: FC = () => { handleSyncWorkflowDraft() } - useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => { - e.preventDefault() - if (workflowReadOnly) - return - - fitView() - handleSyncWorkflowDraft() - }, { - exactMatch: true, - useCapture: true, - }) - - useKeyPress('shift.1', (e) => { - if (workflowReadOnly) - return - - if (isEventTargetInputArea(e.target as HTMLElement)) - return - - e.preventDefault() - zoomTo(1) - handleSyncWorkflowDraft() - }, { - exactMatch: true, - useCapture: true, - }) - - useKeyPress('shift.2', (e) => { - if (workflowReadOnly) - return - - if (isEventTargetInputArea(e.target as HTMLElement)) - return - - e.preventDefault() - zoomTo(2) - handleSyncWorkflowDraft() - }, { - exactMatch: true, - useCapture: true, - }) - - useKeyPress('shift.5', (e) => { - if (workflowReadOnly) - return - - if (isEventTargetInputArea(e.target as HTMLElement)) - return - - e.preventDefault() - zoomTo(0.5) - handleSyncWorkflowDraft() - }, { - exactMatch: true, - useCapture: true, - }) - - useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.dash`, (e) => { - e.preventDefault() - if (workflowReadOnly) - return - - zoomOut() - handleSyncWorkflowDraft() - }, { - exactMatch: true, - useCapture: true, - }) - - useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.equalsign`, (e) => { - e.preventDefault() - if (workflowReadOnly) - return - - zoomIn() - handleSyncWorkflowDraft() - }, { - exactMatch: true, - useCapture: true, - }) - const handleTrigger = useCallback(() => { if (getWorkflowReadOnly()) return @@ -289,11 +205,6 @@ const ZoomInOut: FC = () => { ) } - { - option.key === ZoomType.zoomTo200 && ( - - ) - }
)) } diff --git a/web/app/components/workflow/panel/index.tsx b/web/app/components/workflow/panel/index.tsx index 864e24aa80..a35bedc929 100644 --- a/web/app/components/workflow/panel/index.tsx +++ b/web/app/components/workflow/panel/index.tsx @@ -7,7 +7,6 @@ import { Panel as NodePanel } from '../nodes' import { useStore } from '../store' import { useIsChatMode, - useWorkflow, } from '../hooks' import DebugAndPreview from './debug-and-preview' import Record from './record' @@ -28,10 +27,6 @@ const Panel: FC = () => { const showEnvPanel = useStore(s => s.showEnvPanel) const showChatVariablePanel = useStore(s => s.showChatVariablePanel) const isRestoring = useStore(s => s.isRestoring) - const { - enableShortcuts, - disableShortcuts, - } = useWorkflow() const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({ currentLogItem: state.currentLogItem, setCurrentLogItem: state.setCurrentLogItem, @@ -44,8 +39,6 @@ const Panel: FC = () => {
{ diff --git a/web/app/components/workflow/store.ts b/web/app/components/workflow/store.ts index 854684e5c3..2e5e774191 100644 --- a/web/app/components/workflow/store.ts +++ b/web/app/components/workflow/store.ts @@ -99,8 +99,6 @@ type Shape = { setWorkflowTools: (tools: ToolWithProvider[]) => void clipboardElements: Node[] setClipboardElements: (clipboardElements: Node[]) => void - shortcutsDisabled: boolean - setShortcutsDisabled: (shortcutsDisabled: boolean) => void showDebugAndPreviewPanel: boolean setShowDebugAndPreviewPanel: (showDebugAndPreviewPanel: boolean) => void showEnvPanel: boolean @@ -217,8 +215,6 @@ export const createWorkflowStore = () => { setWorkflowTools: workflowTools => set(() => ({ workflowTools })), clipboardElements: [], setClipboardElements: clipboardElements => set(() => ({ clipboardElements })), - shortcutsDisabled: false, - setShortcutsDisabled: shortcutsDisabled => set(() => ({ shortcutsDisabled })), showDebugAndPreviewPanel: false, setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })), showEnvPanel: false, diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 03f78ea21b..034376fed5 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -29,6 +29,11 @@ export enum BlockEnum { Assigner = 'assigner', // is now named as VariableAssigner } +export enum ControlMode { + Pointer = 'pointer', + Hand = 'hand', +} + export type Branch = { id: string name: string From bd07e1d2fd6efe5852315d675df49b38e55884a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=86=E8=90=8C=E9=97=B7=E6=B2=B9=E7=93=B6?= <253605712@qq.com> Date: Mon, 19 Aug 2024 18:12:41 +0800 Subject: [PATCH 04/16] fix:start of the period should be YYYY-MM-DD 00:00 (#7371) --- .../app/(appDetailLayout)/[appId]/overview/chartView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx index 74e9140fe5..089d898383 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx @@ -25,7 +25,7 @@ export default function ChartView({ appId }: IChartViewProps) { const appDetail = useAppStore(state => state.appDetail) const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow' const isWorkflow = appDetail?.mode === 'workflow' - const [period, setPeriod] = useState({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } }) + const [period, setPeriod] = useState({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.format(queryDateFormat) } }) const onSelect = (item: Item) => { if (item.value === 'all') { @@ -37,7 +37,7 @@ export default function ChartView({ appId }: IChartViewProps) { setPeriod({ name: item.name, query: { start: startOfToday, end: endOfToday } }) } else { - setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } }) + setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').startOf('day').format(queryDateFormat), end: today.format(queryDateFormat) } }) } } From 0087afc2e38887afec390ba8531cca0c4d2f39f7 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 19 Aug 2024 18:45:30 +0800 Subject: [PATCH 05/16] fix(api/core/model_runtime/model_providers/__base/large_language_model.py): Add TEXT type checker (#7407) --- .../__base/large_language_model.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/api/core/model_runtime/model_providers/__base/large_language_model.py b/api/core/model_runtime/model_providers/__base/large_language_model.py index 02ba0c9410..cfc8942c79 100644 --- a/api/core/model_runtime/model_providers/__base/large_language_model.py +++ b/api/core/model_runtime/model_providers/__base/large_language_model.py @@ -185,7 +185,7 @@ if you are not sure about the structure. stream=stream, user=user ) - + model_parameters.pop("response_format") stop = stop or [] stop.extend(["\n```", "```\n"]) @@ -249,10 +249,10 @@ if you are not sure about the structure. prompt_messages=prompt_messages, input_generator=new_generator() ) - + return response - def _code_block_mode_stream_processor(self, model: str, prompt_messages: list[PromptMessage], + def _code_block_mode_stream_processor(self, model: str, prompt_messages: list[PromptMessage], input_generator: Generator[LLMResultChunk, None, None] ) -> Generator[LLMResultChunk, None, None]: """ @@ -310,7 +310,7 @@ if you are not sure about the structure. ) ) - def _code_block_mode_stream_processor_with_backtick(self, model: str, prompt_messages: list, + def _code_block_mode_stream_processor_with_backtick(self, model: str, prompt_messages: list, input_generator: Generator[LLMResultChunk, None, None]) \ -> Generator[LLMResultChunk, None, None]: """ @@ -470,7 +470,7 @@ if you are not sure about the structure. :return: full response or stream response chunk generator result """ raise NotImplementedError - + @abstractmethod def get_num_tokens(self, model: str, credentials: dict, prompt_messages: list[PromptMessage], tools: Optional[list[PromptMessageTool]] = None) -> int: @@ -792,6 +792,13 @@ if you are not sure about the structure. if not isinstance(parameter_value, str): raise ValueError(f"Model Parameter {parameter_name} should be string.") + # validate options + if parameter_rule.options and parameter_value not in parameter_rule.options: + raise ValueError(f"Model Parameter {parameter_name} should be one of {parameter_rule.options}.") + elif parameter_rule.type == ParameterType.TEXT: + if not isinstance(parameter_value, str): + raise ValueError(f"Model Parameter {parameter_name} should be text.") + # validate options if parameter_rule.options and parameter_value not in parameter_rule.options: raise ValueError(f"Model Parameter {parameter_name} should be one of {parameter_rule.options}.") From 53cf7562072ebb84e56dc6121474aa3441bf8fa4 Mon Sep 17 00:00:00 2001 From: Xiao Ley Date: Mon, 19 Aug 2024 19:14:08 +0800 Subject: [PATCH 06/16] feat: OpenRouter add gpt-4o-2024-08-06 model (#7409) --- .../openrouter/llm/gpt-4o-2024-08-06.yaml | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 api/core/model_runtime/model_providers/openrouter/llm/gpt-4o-2024-08-06.yaml diff --git a/api/core/model_runtime/model_providers/openrouter/llm/gpt-4o-2024-08-06.yaml b/api/core/model_runtime/model_providers/openrouter/llm/gpt-4o-2024-08-06.yaml new file mode 100644 index 0000000000..cf2de0f73a --- /dev/null +++ b/api/core/model_runtime/model_providers/openrouter/llm/gpt-4o-2024-08-06.yaml @@ -0,0 +1,44 @@ +model: gpt-4o-2024-08-06 +label: + zh_Hans: gpt-4o-2024-08-06 + en_US: gpt-4o-2024-08-06 +model_type: llm +features: + - multi-tool-call + - agent-thought + - stream-tool-call + - vision +model_properties: + mode: chat + context_size: 128000 +parameter_rules: + - name: temperature + use_template: temperature + - name: top_p + use_template: top_p + - name: presence_penalty + use_template: presence_penalty + - name: frequency_penalty + use_template: frequency_penalty + - name: max_tokens + use_template: max_tokens + default: 512 + min: 1 + max: 16384 + - name: response_format + label: + zh_Hans: 回复格式 + en_US: response_format + type: string + help: + zh_Hans: 指定模型必须输出的格式 + en_US: specifying the format that the model must output + required: false + options: + - text + - json_object +pricing: + input: '2.50' + output: '10.00' + unit: '0.000001' + currency: USD From 4ff4859036e8253fec7208589766e05c870171c0 Mon Sep 17 00:00:00 2001 From: RookieAgent <42060616+Sakura4036@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:14:20 +0800 Subject: [PATCH 07/16] add CrossRef builtin tool: doi query and title query (#7406) --- .../builtin/crossref/_assets/icon.svg | 49 +++++++ .../provider/builtin/crossref/crossref.py | 20 +++ .../provider/builtin/crossref/crossref.yaml | 29 +++++ .../builtin/crossref/tools/query_doi.py | 25 ++++ .../builtin/crossref/tools/query_doi.yaml | 23 ++++ .../builtin/crossref/tools/query_title.py | 120 ++++++++++++++++++ .../builtin/crossref/tools/query_title.yaml | 105 +++++++++++++++ 7 files changed, 371 insertions(+) create mode 100644 api/core/tools/provider/builtin/crossref/_assets/icon.svg create mode 100644 api/core/tools/provider/builtin/crossref/crossref.py create mode 100644 api/core/tools/provider/builtin/crossref/crossref.yaml create mode 100644 api/core/tools/provider/builtin/crossref/tools/query_doi.py create mode 100644 api/core/tools/provider/builtin/crossref/tools/query_doi.yaml create mode 100644 api/core/tools/provider/builtin/crossref/tools/query_title.py create mode 100644 api/core/tools/provider/builtin/crossref/tools/query_title.yaml diff --git a/api/core/tools/provider/builtin/crossref/_assets/icon.svg b/api/core/tools/provider/builtin/crossref/_assets/icon.svg new file mode 100644 index 0000000000..aa629de7cb --- /dev/null +++ b/api/core/tools/provider/builtin/crossref/_assets/icon.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api/core/tools/provider/builtin/crossref/crossref.py b/api/core/tools/provider/builtin/crossref/crossref.py new file mode 100644 index 0000000000..404e483e0d --- /dev/null +++ b/api/core/tools/provider/builtin/crossref/crossref.py @@ -0,0 +1,20 @@ +from core.tools.errors import ToolProviderCredentialValidationError +from core.tools.provider.builtin.crossref.tools.query_doi import CrossRefQueryDOITool +from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController + + +class CrossRefProvider(BuiltinToolProviderController): + def _validate_credentials(self, credentials: dict) -> None: + try: + CrossRefQueryDOITool().fork_tool_runtime( + runtime={ + "credentials": credentials, + } + ).invoke( + user_id='', + tool_parameters={ + "doi": '10.1007/s00894-022-05373-8', + }, + ) + except Exception as e: + raise ToolProviderCredentialValidationError(str(e)) diff --git a/api/core/tools/provider/builtin/crossref/crossref.yaml b/api/core/tools/provider/builtin/crossref/crossref.yaml new file mode 100644 index 0000000000..da67fbec3a --- /dev/null +++ b/api/core/tools/provider/builtin/crossref/crossref.yaml @@ -0,0 +1,29 @@ +identity: + author: Sakura4036 + name: crossref + label: + en_US: CrossRef + zh_Hans: CrossRef + description: + en_US: Crossref is a cross-publisher reference linking registration query system using DOI technology created in 2000. Crossref establishes cross-database links between the reference list and citation full text of papers, making it very convenient for readers to access the full text of papers. + zh_Hans: Crossref是于2000年创建的使用DOI技术的跨出版商参考文献链接注册查询系统。Crossref建立了在论文的参考文献列表和引文全文之间的跨数据库链接,使得读者能够非常便捷地获取文献全文。 + icon: icon.svg + tags: + - search +credentials_for_provider: + mailto: + type: text-input + required: true + label: + en_US: email address + zh_Hans: email地址 + pt_BR: email address + placeholder: + en_US: Please input your email address + zh_Hans: 请输入你的email地址 + pt_BR: Please input your email address + help: + en_US: According to the requirements of Crossref, an email address is required + zh_Hans: 根据Crossref的要求,需要提供一个邮箱地址 + pt_BR: According to the requirements of Crossref, an email address is required + url: https://api.crossref.org/swagger-ui/index.html diff --git a/api/core/tools/provider/builtin/crossref/tools/query_doi.py b/api/core/tools/provider/builtin/crossref/tools/query_doi.py new file mode 100644 index 0000000000..a43c0989e4 --- /dev/null +++ b/api/core/tools/provider/builtin/crossref/tools/query_doi.py @@ -0,0 +1,25 @@ +from typing import Any, Union + +import requests + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.errors import ToolParameterValidationError +from core.tools.tool.builtin_tool import BuiltinTool + + +class CrossRefQueryDOITool(BuiltinTool): + """ + Tool for querying the metadata of a publication using its DOI. + """ + def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + doi = tool_parameters.get('doi') + if not doi: + raise ToolParameterValidationError('doi is required.') + # doc: https://github.com/CrossRef/rest-api-doc + url = f"https://api.crossref.org/works/{doi}" + response = requests.get(url) + response.raise_for_status() + response = response.json() + message = response.get('message', {}) + + return self.create_json_message(message) diff --git a/api/core/tools/provider/builtin/crossref/tools/query_doi.yaml b/api/core/tools/provider/builtin/crossref/tools/query_doi.yaml new file mode 100644 index 0000000000..9c16da25ed --- /dev/null +++ b/api/core/tools/provider/builtin/crossref/tools/query_doi.yaml @@ -0,0 +1,23 @@ +identity: + name: crossref_query_doi + author: Sakura4036 + label: + en_US: CrossRef Query DOI + zh_Hans: CrossRef DOI 查询 + pt_BR: CrossRef Query DOI +description: + human: + en_US: A tool for searching literature information using CrossRef by DOI. + zh_Hans: 一个使用CrossRef通过DOI获取文献信息的工具。 + pt_BR: A tool for searching literature information using CrossRef by DOI. + llm: A tool for searching literature information using CrossRef by DOI. +parameters: + - name: doi + type: string + required: true + label: + en_US: DOI + zh_Hans: DOI + pt_BR: DOI + llm_description: DOI for searching in CrossRef + form: llm diff --git a/api/core/tools/provider/builtin/crossref/tools/query_title.py b/api/core/tools/provider/builtin/crossref/tools/query_title.py new file mode 100644 index 0000000000..946aa6dc94 --- /dev/null +++ b/api/core/tools/provider/builtin/crossref/tools/query_title.py @@ -0,0 +1,120 @@ +import time +from typing import Any, Union + +import requests + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.tool.builtin_tool import BuiltinTool + + +def convert_time_str_to_seconds(time_str: str) -> int: + """ + Convert a time string to seconds. + example: 1s -> 1, 1m30s -> 90, 1h30m -> 5400, 1h30m30s -> 5430 + """ + time_str = time_str.lower().strip().replace(' ', '') + seconds = 0 + if 'h' in time_str: + hours, time_str = time_str.split('h') + seconds += int(hours) * 3600 + if 'm' in time_str: + minutes, time_str = time_str.split('m') + seconds += int(minutes) * 60 + if 's' in time_str: + seconds += int(time_str.replace('s', '')) + return seconds + + +class CrossRefQueryTitleAPI: + """ + Tool for querying the metadata of a publication using its title. + Crossref API doc: https://github.com/CrossRef/rest-api-doc + """ + query_url_template: str = "https://api.crossref.org/works?query.bibliographic={query}&rows={rows}&offset={offset}&sort={sort}&order={order}&mailto={mailto}" + rate_limit: int = 50 + rate_interval: float = 1 + max_limit: int = 1000 + + def __init__(self, mailto: str): + self.mailto = mailto + + def _query(self, query: str, rows: int = 5, offset: int = 0, sort: str = 'relevance', order: str = 'desc', fuzzy_query: bool = False) -> list[dict]: + """ + Query the metadata of a publication using its title. + :param query: the title of the publication + :param rows: the number of results to return + :param sort: the sort field + :param order: the sort order + :param fuzzy_query: whether to return all items that match the query + """ + url = self.query_url_template.format(query=query, rows=rows, offset=offset, sort=sort, order=order, mailto=self.mailto) + response = requests.get(url) + response.raise_for_status() + rate_limit = int(response.headers['x-ratelimit-limit']) + # convert time string to seconds + rate_interval = convert_time_str_to_seconds(response.headers['x-ratelimit-interval']) + + self.rate_limit = rate_limit + self.rate_interval = rate_interval + + response = response.json() + if response['status'] != 'ok': + return [] + + message = response['message'] + if fuzzy_query: + # fuzzy query return all items + return message['items'] + else: + for paper in message['items']: + title = paper['title'][0] + if title.lower() != query.lower(): + continue + return [paper] + return [] + + def query(self, query: str, rows: int = 5, sort: str = 'relevance', order: str = 'desc', fuzzy_query: bool = False) -> list[dict]: + """ + Query the metadata of a publication using its title. + :param query: the title of the publication + :param rows: the number of results to return + :param sort: the sort field + :param order: the sort order + :param fuzzy_query: whether to return all items that match the query + """ + rows = min(rows, self.max_limit) + if rows > self.rate_limit: + # query multiple times + query_times = rows // self.rate_limit + 1 + results = [] + + for i in range(query_times): + result = self._query(query, rows=self.rate_limit, offset=i * self.rate_limit, sort=sort, order=order, fuzzy_query=fuzzy_query) + if fuzzy_query: + results.extend(result) + else: + # fuzzy_query=False, only one result + if result: + return result + time.sleep(self.rate_interval) + return results + else: + # query once + return self._query(query, rows, sort=sort, order=order, fuzzy_query=fuzzy_query) + + +class CrossRefQueryTitleTool(BuiltinTool): + """ + Tool for querying the metadata of a publication using its title. + """ + def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + query = tool_parameters.get('query') + fuzzy_query = tool_parameters.get('fuzzy_query', False) + rows = tool_parameters.get('rows', 3) + sort = tool_parameters.get('sort', 'relevance') + order = tool_parameters.get('order', 'desc') + mailto = self.runtime.credentials['mailto'] + + result = CrossRefQueryTitleAPI(mailto).query(query, rows, sort, order, fuzzy_query) + + return [self.create_json_message(r) for r in result] diff --git a/api/core/tools/provider/builtin/crossref/tools/query_title.yaml b/api/core/tools/provider/builtin/crossref/tools/query_title.yaml new file mode 100644 index 0000000000..5579c77f52 --- /dev/null +++ b/api/core/tools/provider/builtin/crossref/tools/query_title.yaml @@ -0,0 +1,105 @@ +identity: + name: crossref_query_title + author: Sakura4036 + label: + en_US: CrossRef Title Query + zh_Hans: CrossRef 标题查询 + pt_BR: CrossRef Title Query +description: + human: + en_US: A tool for querying literature information using CrossRef by title. + zh_Hans: 一个使用CrossRef通过标题搜索文献信息的工具。 + pt_BR: A tool for querying literature information using CrossRef by title. + llm: A tool for querying literature information using CrossRef by title. +parameters: + - name: query + type: string + required: true + label: + en_US: 标题 + zh_Hans: 查询语句 + pt_BR: 标题 + human_description: + en_US: Query bibliographic information, useful for citation look up. Includes titles, authors, ISSNs and publication years + zh_Hans: 用于搜索文献信息,有助于查找引用。包括标题,作者,ISSN和出版年份 + pt_BR: Query bibliographic information, useful for citation look up. Includes titles, authors, ISSNs and publication years + llm_description: key words for querying in Web of Science + form: llm + - name: fuzzy_query + type: boolean + default: false + label: + en_US: Whether to fuzzy search + zh_Hans: 是否模糊搜索 + pt_BR: Whether to fuzzy search + human_description: + en_US: used for selecting the query type, fuzzy query returns more results, precise query returns 1 or none + zh_Hans: 用于选择搜索类型,模糊搜索返回更多结果,精确搜索返回1条结果或无 + pt_BR: used for selecting the query type, fuzzy query returns more results, precise query returns 1 or none + form: form + - name: limit + type: number + required: false + label: + en_US: max query number + zh_Hans: 最大搜索数 + pt_BR: max query number + human_description: + en_US: max query number(fuzzy search returns the maximum number of results or precise search the maximum number of matches) + zh_Hans: 最大搜索数(模糊搜索返回的最大结果数或精确搜索最大匹配数) + pt_BR: max query number(fuzzy search returns the maximum number of results or precise search the maximum number of matches) + form: llm + default: 50 + - name: sort + type: select + required: true + options: + - value: relevance + label: + en_US: relevance + zh_Hans: 相关性 + pt_BR: relevance + - value: published + label: + en_US: publication date + zh_Hans: 出版日期 + pt_BR: publication date + - value: references-count + label: + en_US: references-count + zh_Hans: 引用次数 + pt_BR: references-count + default: relevance + label: + en_US: sorting field + zh_Hans: 排序字段 + pt_BR: sorting field + human_description: + en_US: Sorting of query results + zh_Hans: 检索结果的排序字段 + pt_BR: Sorting of query results + form: form + - name: order + type: select + required: true + options: + - value: desc + label: + en_US: descending + zh_Hans: 降序 + pt_BR: descending + - value: asc + label: + en_US: ascending + zh_Hans: 升序 + pt_BR: ascending + default: desc + label: + en_US: Order + zh_Hans: 排序 + pt_BR: Order + human_description: + en_US: Order of query results + zh_Hans: 检索结果的排序方式 + pt_BR: Order of query results + form: form From ab6499e5b7669bee9bb744cf828e1daf8f71d14c Mon Sep 17 00:00:00 2001 From: Yeuoly <45712896+Yeuoly@users.noreply.github.com> Date: Mon, 19 Aug 2024 21:24:15 +0800 Subject: [PATCH 08/16] upgrade: sandbox to 0.2.6 (#7410) --- docker/docker-compose.middleware.yaml | 2 +- docker/docker-compose.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index 3aa84d009e..9a4c40448b 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -34,7 +34,7 @@ services: # The DifySandbox sandbox: - image: langgenius/dify-sandbox:0.2.1 + image: langgenius/dify-sandbox:0.2.6 restart: always environment: # The DifySandbox configurations diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index de3361f073..4f54d0f234 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -272,7 +272,7 @@ services: # The DifySandbox sandbox: - image: langgenius/dify-sandbox:0.2.1 + image: langgenius/dify-sandbox:0.2.6 restart: always environment: # The DifySandbox configurations From 3d27d15f00d025185bf979a90a07f2767297703f Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 19 Aug 2024 21:24:56 +0800 Subject: [PATCH 09/16] chore(*): Bump version 0.7.1 (#7389) --- api/configs/packaging/__init__.py | 2 +- docker-legacy/docker-compose.yaml | 6 +++--- docker/docker-compose.yaml | 6 +++--- web/package.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/configs/packaging/__init__.py b/api/configs/packaging/__init__.py index 247fcde655..a7c5eb15a3 100644 --- a/api/configs/packaging/__init__.py +++ b/api/configs/packaging/__init__.py @@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings): CURRENT_VERSION: str = Field( description='Dify version', - default='0.7.0', + default='0.7.1', ) COMMIT_SHA: str = Field( diff --git a/docker-legacy/docker-compose.yaml b/docker-legacy/docker-compose.yaml index aed2586053..edefd129d5 100644 --- a/docker-legacy/docker-compose.yaml +++ b/docker-legacy/docker-compose.yaml @@ -2,7 +2,7 @@ version: '3' services: # API service api: - image: langgenius/dify-api:0.7.0 + image: langgenius/dify-api:0.7.1 restart: always environment: # Startup mode, 'api' starts the API server. @@ -229,7 +229,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:0.7.0 + image: langgenius/dify-api:0.7.1 restart: always environment: CONSOLE_WEB_URL: '' @@ -400,7 +400,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:0.7.0 + image: langgenius/dify-web:0.7.1 restart: always environment: # The base URL of console application api server, refers to the Console base URL of WEB service if console domain is diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 4f54d0f234..aea16e3817 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -188,7 +188,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:0.7.0 + image: langgenius/dify-api:0.7.1 restart: always environment: # Use the shared environment variables. @@ -208,7 +208,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:0.7.0 + image: langgenius/dify-api:0.7.1 restart: always environment: # Use the shared environment variables. @@ -227,7 +227,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:0.7.0 + image: langgenius/dify-web:0.7.1 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} diff --git a/web/package.json b/web/package.json index f1809177d7..bbbb8f3aa1 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "dify-web", - "version": "0.7.0", + "version": "0.7.1", "private": true, "engines": { "node": ">=18.17.0" From 31f997741154db51dcef73e62df8ec14c8fa1df7 Mon Sep 17 00:00:00 2001 From: Hash Brown Date: Mon, 19 Aug 2024 22:24:21 +0800 Subject: [PATCH 10/16] Web app support sending message using numpad enter (#7414) --- web/app/components/base/chat/chat/chat-input.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/components/base/chat/chat/chat-input.tsx b/web/app/components/base/chat/chat/chat-input.tsx index fcf9b2c23f..0c083157a1 100644 --- a/web/app/components/base/chat/chat/chat-input.tsx +++ b/web/app/components/base/chat/chat/chat-input.tsx @@ -90,7 +90,7 @@ const ChatInput: FC = ({ } const handleKeyUp = (e: React.KeyboardEvent) => { - if (e.code === 'Enter') { + if (e.key === 'Enter') { e.preventDefault() // prevent send message when using input method enter if (!e.shiftKey && !isUseInputMethod.current) @@ -100,7 +100,7 @@ const ChatInput: FC = ({ const handleKeyDown = (e: React.KeyboardEvent) => { isUseInputMethod.current = e.nativeEvent.isComposing - if (e.code === 'Enter' && !e.shiftKey) { + if (e.key === 'Enter' && !e.shiftKey) { setQuery(query.replace(/\n$/, '')) e.preventDefault() } From 1f944c6eeb36a148697531ecb3c4056f18daeed1 Mon Sep 17 00:00:00 2001 From: Chengyu Yan Date: Mon, 19 Aug 2024 22:25:09 +0800 Subject: [PATCH 11/16] feat(api): support wenxin bge-large and tao embedding model. (#7393) --- .../model_providers/wenxin/_common.py | 3 + .../wenxin/text_embedding/bge-large-en.yaml | 9 +++ .../wenxin/text_embedding/bge-large-zh.yaml | 9 +++ .../wenxin/text_embedding/tao-8k.yaml | 9 +++ .../model_runtime/wenxin/test_embedding.py | 61 ++++++++++++++++++- 5 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 api/core/model_runtime/model_providers/wenxin/text_embedding/bge-large-en.yaml create mode 100644 api/core/model_runtime/model_providers/wenxin/text_embedding/bge-large-zh.yaml create mode 100644 api/core/model_runtime/model_providers/wenxin/text_embedding/tao-8k.yaml diff --git a/api/core/model_runtime/model_providers/wenxin/_common.py b/api/core/model_runtime/model_providers/wenxin/_common.py index ee9c34b6ac..0230c78b75 100644 --- a/api/core/model_runtime/model_providers/wenxin/_common.py +++ b/api/core/model_runtime/model_providers/wenxin/_common.py @@ -118,6 +118,9 @@ class _CommonWenxin: 'ernie-4.0-turbo-8k-preview': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-4.0-turbo-8k-preview', 'yi_34b_chat': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/yi_34b_chat', 'embedding-v1': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/embedding-v1', + 'bge-large-en': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/bge_large_en', + 'bge-large-zh': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/bge_large_zh', + 'tao-8k': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/tao_8k', } function_calling_supports = [ diff --git a/api/core/model_runtime/model_providers/wenxin/text_embedding/bge-large-en.yaml b/api/core/model_runtime/model_providers/wenxin/text_embedding/bge-large-en.yaml new file mode 100644 index 0000000000..74fadb7f9d --- /dev/null +++ b/api/core/model_runtime/model_providers/wenxin/text_embedding/bge-large-en.yaml @@ -0,0 +1,9 @@ +model: bge-large-en +model_type: text-embedding +model_properties: + context_size: 512 + max_chunks: 16 +pricing: + input: '0.0005' + unit: '0.001' + currency: RMB diff --git a/api/core/model_runtime/model_providers/wenxin/text_embedding/bge-large-zh.yaml b/api/core/model_runtime/model_providers/wenxin/text_embedding/bge-large-zh.yaml new file mode 100644 index 0000000000..d4af27ec38 --- /dev/null +++ b/api/core/model_runtime/model_providers/wenxin/text_embedding/bge-large-zh.yaml @@ -0,0 +1,9 @@ +model: bge-large-zh +model_type: text-embedding +model_properties: + context_size: 512 + max_chunks: 16 +pricing: + input: '0.0005' + unit: '0.001' + currency: RMB diff --git a/api/core/model_runtime/model_providers/wenxin/text_embedding/tao-8k.yaml b/api/core/model_runtime/model_providers/wenxin/text_embedding/tao-8k.yaml new file mode 100644 index 0000000000..e28f253eb6 --- /dev/null +++ b/api/core/model_runtime/model_providers/wenxin/text_embedding/tao-8k.yaml @@ -0,0 +1,9 @@ +model: tao-8k +model_type: text-embedding +model_properties: + context_size: 8192 + max_chunks: 1 +pricing: + input: '0.0005' + unit: '0.001' + currency: RMB diff --git a/api/tests/integration_tests/model_runtime/wenxin/test_embedding.py b/api/tests/integration_tests/model_runtime/wenxin/test_embedding.py index 60e8036223..d886226cf9 100644 --- a/api/tests/integration_tests/model_runtime/wenxin/test_embedding.py +++ b/api/tests/integration_tests/model_runtime/wenxin/test_embedding.py @@ -5,7 +5,7 @@ from core.model_runtime.entities.text_embedding_entities import TextEmbeddingRes from core.model_runtime.model_providers.wenxin.text_embedding.text_embedding import WenxinTextEmbeddingModel -def test_invoke_embedding_model(): +def test_invoke_embedding_v1(): sleep(3) model = WenxinTextEmbeddingModel() @@ -21,4 +21,61 @@ def test_invoke_embedding_model(): assert isinstance(response, TextEmbeddingResult) assert len(response.embeddings) == 3 - assert isinstance(response.embeddings[0], list) \ No newline at end of file + assert isinstance(response.embeddings[0], list) + + +def test_invoke_embedding_bge_large_en(): + sleep(3) + model = WenxinTextEmbeddingModel() + + response = model.invoke( + model='bge-large-en', + credentials={ + 'api_key': os.environ.get('WENXIN_API_KEY'), + 'secret_key': os.environ.get('WENXIN_SECRET_KEY') + }, + texts=['hello', '你好', 'xxxxx'], + user="abc-123" + ) + + assert isinstance(response, TextEmbeddingResult) + assert len(response.embeddings) == 3 + assert isinstance(response.embeddings[0], list) + + +def test_invoke_embedding_bge_large_zh(): + sleep(3) + model = WenxinTextEmbeddingModel() + + response = model.invoke( + model='bge-large-zh', + credentials={ + 'api_key': os.environ.get('WENXIN_API_KEY'), + 'secret_key': os.environ.get('WENXIN_SECRET_KEY') + }, + texts=['hello', '你好', 'xxxxx'], + user="abc-123" + ) + + assert isinstance(response, TextEmbeddingResult) + assert len(response.embeddings) == 3 + assert isinstance(response.embeddings[0], list) + + +def test_invoke_embedding_tao_8k(): + sleep(3) + model = WenxinTextEmbeddingModel() + + response = model.invoke( + model='tao-8k', + credentials={ + 'api_key': os.environ.get('WENXIN_API_KEY'), + 'secret_key': os.environ.get('WENXIN_SECRET_KEY') + }, + texts=['hello', '你好', 'xxxxx'], + user="abc-123" + ) + + assert isinstance(response, TextEmbeddingResult) + assert len(response.embeddings) == 3 + assert isinstance(response.embeddings[0], list) From 6991a243aaa52a73eea89aa090091c8f7556cf7c Mon Sep 17 00:00:00 2001 From: Nam Vu Date: Tue, 20 Aug 2024 09:20:04 +0700 Subject: [PATCH 12/16] chore: correct _tts_invoke_streaming max length (#7423) --- api/core/model_runtime/model_providers/azure_openai/tts/tts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/model_runtime/model_providers/azure_openai/tts/tts.py b/api/core/model_runtime/model_providers/azure_openai/tts/tts.py index 3d2bac1c31..f9ddd86f68 100644 --- a/api/core/model_runtime/model_providers/azure_openai/tts/tts.py +++ b/api/core/model_runtime/model_providers/azure_openai/tts/tts.py @@ -70,7 +70,7 @@ class AzureOpenAIText2SpeechModel(_CommonAzureOpenAI, TTSModel): # doc: https://platform.openai.com/docs/guides/text-to-speech credentials_kwargs = self._to_credential_kwargs(credentials) client = AzureOpenAI(**credentials_kwargs) - # max font is 4096,there is 3500 limit for each request + # max length is 4096 characters, there is 3500 limit for each request max_length = 3500 if len(content_text) > max_length: sentences = self._split_text_into_sentences(content_text, max_length=max_length) From afd23f7ad8ed2a8c596db2daed8bc735b27fc39e Mon Sep 17 00:00:00 2001 From: Nam Vu Date: Tue, 20 Aug 2024 09:21:24 +0700 Subject: [PATCH 13/16] chore: #7196 i18n (#7416) --- web/i18n/de-DE/app.ts | 2 ++ web/i18n/es-ES/app.ts | 2 ++ web/i18n/fa-IR/app.ts | 2 ++ web/i18n/fr-FR/app.ts | 2 ++ web/i18n/hi-IN/app.ts | 2 ++ web/i18n/it-IT/app.ts | 2 ++ web/i18n/ja-JP/app.ts | 2 ++ web/i18n/ko-KR/app.ts | 2 ++ web/i18n/pl-PL/app.ts | 2 ++ web/i18n/pt-BR/app.ts | 2 ++ web/i18n/ro-RO/app.ts | 2 ++ web/i18n/tr-TR/app.ts | 2 ++ web/i18n/uk-UA/app.ts | 2 ++ web/i18n/vi-VN/app.ts | 2 ++ web/i18n/vi-VN/common.ts | 4 ++-- web/i18n/zh-Hant/app.ts | 2 ++ 16 files changed, 32 insertions(+), 2 deletions(-) diff --git a/web/i18n/de-DE/app.ts b/web/i18n/de-DE/app.ts index 1ebc84d506..55caff2284 100644 --- a/web/i18n/de-DE/app.ts +++ b/web/i18n/de-DE/app.ts @@ -49,6 +49,8 @@ const translation = { iconPicker: { ok: 'OK', cancel: 'Abbrechen', + emoji: 'Emoji', + image: 'Bild', }, switch: 'Zu Workflow-Orchestrierung wechseln', switchTipStart: 'Eine neue App-Kopie wird für Sie erstellt, und die neue Kopie wird zur Workflow-Orchestrierung wechseln. Die neue Kopie wird ', diff --git a/web/i18n/es-ES/app.ts b/web/i18n/es-ES/app.ts index c3d7a8362b..739439ff58 100644 --- a/web/i18n/es-ES/app.ts +++ b/web/i18n/es-ES/app.ts @@ -70,6 +70,8 @@ const translation = { iconPicker: { ok: 'OK', cancel: 'Cancelar', + emoji: 'Emoji', + image: 'Imagen', }, switch: 'Cambiar a Orquestación de Flujo de Trabajo', switchTipStart: 'Se creará una nueva copia de la app para ti y la nueva copia cambiará a Orquestación de Flujo de Trabajo. La nueva copia no permitirá', diff --git a/web/i18n/fa-IR/app.ts b/web/i18n/fa-IR/app.ts index 9f2563709e..b9dd179809 100644 --- a/web/i18n/fa-IR/app.ts +++ b/web/i18n/fa-IR/app.ts @@ -74,6 +74,8 @@ const translation = { iconPicker: { ok: 'باشه', cancel: 'لغو', + emoji: 'ایموجی', + image: 'تصویر', }, switch: 'تغییر به سازماندهی گردش کار', switchTipStart: 'یک نسخه جدید از برنامه برای شما ایجاد خواهد شد و نسخه جدید به سازماندهی گردش کار تغییر خواهد کرد. نسخه جدید ', diff --git a/web/i18n/fr-FR/app.ts b/web/i18n/fr-FR/app.ts index e3268ae137..fab9b98f99 100644 --- a/web/i18n/fr-FR/app.ts +++ b/web/i18n/fr-FR/app.ts @@ -70,6 +70,8 @@ const translation = { iconPicker: { ok: 'OK', cancel: 'Annuler', + emoji: 'Emoji', + image: 'Image', }, switch: 'Passer à l\'orchestration de flux de travail', switchTipStart: 'Une nouvelle copie de l\'application sera créée pour vous, et la nouvelle copie passera à l\'orchestration de flux de travail. La nouvelle copie ne permettra pas le ', diff --git a/web/i18n/hi-IN/app.ts b/web/i18n/hi-IN/app.ts index e2b4412332..3f22b6701b 100644 --- a/web/i18n/hi-IN/app.ts +++ b/web/i18n/hi-IN/app.ts @@ -70,6 +70,8 @@ const translation = { iconPicker: { ok: 'ठीक है', cancel: 'रद्द करें', + emoji: 'इमोजी', + image: 'छवि', }, switch: 'वर्कफ़्लो ऑर्केस्ट्रेट पर स्विच करें', switchTipStart: 'आपके लिए एक नई ऐप कॉपी बनाई जाएगी, और नई कॉपी वर्कफ़्लो ऑर्केस्ट्रेट में स्विच हो जाएगी। नई कॉपी ', diff --git a/web/i18n/it-IT/app.ts b/web/i18n/it-IT/app.ts index 3c23c3ea94..265cb58ec4 100644 --- a/web/i18n/it-IT/app.ts +++ b/web/i18n/it-IT/app.ts @@ -76,6 +76,8 @@ const translation = { iconPicker: { ok: 'OK', cancel: 'Annulla', + emoji: 'Emoji', + image: 'Immagine', }, switch: 'Passa a Orchestrazione del flusso di lavoro', switchTipStart: diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index ece86a442d..55f641f4c3 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -75,6 +75,8 @@ const translation = { iconPicker: { ok: 'OK', cancel: 'キャンセル', + emoji: '絵文字', + image: '画像', }, switch: 'ワークフロー オーケストレートに切り替える', switchTipStart: '新しいアプリのコピーが作成され、新しいコピーがワークフロー オーケストレートに切り替わります。新しいコピーは ', diff --git a/web/i18n/ko-KR/app.ts b/web/i18n/ko-KR/app.ts index 9fbc0095f5..a1fde30e4d 100644 --- a/web/i18n/ko-KR/app.ts +++ b/web/i18n/ko-KR/app.ts @@ -66,6 +66,8 @@ const translation = { iconPicker: { ok: '확인', cancel: '취소', + emoji: '이모지', + image: '이미지', }, switch: '워크플로우 오케스트레이션으로 전환하기', switchTipStart: '새로운 앱의 복사본이 생성되어 새로운 복사본이 워크플로우 오케스트레이션으로 전환됩니다. 새로운 복사본은 ', diff --git a/web/i18n/pl-PL/app.ts b/web/i18n/pl-PL/app.ts index cf2462d012..6a47d43798 100644 --- a/web/i18n/pl-PL/app.ts +++ b/web/i18n/pl-PL/app.ts @@ -76,6 +76,8 @@ const translation = { iconPicker: { ok: 'OK', cancel: 'Anuluj', + emoji: 'Emoji', + image: 'Obraz', }, switch: 'Przełącz na Orkiestrację Przepływu Pracy', switchTipStart: diff --git a/web/i18n/pt-BR/app.ts b/web/i18n/pt-BR/app.ts index 6d728bad47..ef9122b86c 100644 --- a/web/i18n/pt-BR/app.ts +++ b/web/i18n/pt-BR/app.ts @@ -70,6 +70,8 @@ const translation = { iconPicker: { ok: 'OK', cancel: 'Cancelar', + emoji: 'Emoji', + image: 'Imagem', }, switch: 'Mudar para Orquestração de Fluxo de Trabalho', switchTipStart: 'Será criada uma nova cópia do aplicativo para você e a nova cópia mudará para Orquestração de Fluxo de Trabalho. A nova cópia não permitirá a ', diff --git a/web/i18n/ro-RO/app.ts b/web/i18n/ro-RO/app.ts index 053dd47d84..2d13dd4e66 100644 --- a/web/i18n/ro-RO/app.ts +++ b/web/i18n/ro-RO/app.ts @@ -70,6 +70,8 @@ const translation = { iconPicker: { ok: 'OK', cancel: 'Anulează', + emoji: 'Emoji', + image: 'Imagine', }, switch: 'Comută la Orchestrare Flux de Lucru', switchTipStart: 'O nouă copie a aplicației va fi creată pentru tine, iar noua copie va comuta la Orchestrare Flux de Lucru. Noua copie ', diff --git a/web/i18n/tr-TR/app.ts b/web/i18n/tr-TR/app.ts index 336d3567d1..fb1ac36762 100644 --- a/web/i18n/tr-TR/app.ts +++ b/web/i18n/tr-TR/app.ts @@ -70,6 +70,8 @@ const translation = { iconPicker: { ok: 'Tamam', cancel: 'İptal', + emoji: 'Emoji', + image: 'Görsel', }, switch: 'Workflow Orkestrasyonuna Geç', switchTipStart: 'Sizin için yeni bir uygulama kopyası oluşturulacak ve yeni kopya Workflow Orkestrasyonuna geçecektir. Yeni kopya ', diff --git a/web/i18n/uk-UA/app.ts b/web/i18n/uk-UA/app.ts index fe74f5a262..fbe9eea81e 100644 --- a/web/i18n/uk-UA/app.ts +++ b/web/i18n/uk-UA/app.ts @@ -70,6 +70,8 @@ const translation = { iconPicker: { ok: 'OK', cancel: 'Скасувати', + emoji: 'Емодзі', + image: 'Зображення', }, switch: 'Перейти до оркестрації робочого процесу', switchTipStart: 'Для вас буде створена нова копія додатка, і нова копія перейде до оркестрації робочого процесу. Нова копія не дозволить ', diff --git a/web/i18n/vi-VN/app.ts b/web/i18n/vi-VN/app.ts index 86dd4fac6f..4052506f83 100644 --- a/web/i18n/vi-VN/app.ts +++ b/web/i18n/vi-VN/app.ts @@ -70,6 +70,8 @@ const translation = { iconPicker: { ok: 'Đồng ý', cancel: 'Hủy', + emoji: 'Biểu tượng cảm xúc', + image: 'Hình ảnh', }, switch: 'Chuyển sang quản lý quy trình', switchTipStart: 'Một bản sao ứng dụng mới sẽ được tạo và chuyển sang quản lý quy trình. Bản sao mới sẽ ', diff --git a/web/i18n/vi-VN/common.ts b/web/i18n/vi-VN/common.ts index 80570861b5..232148ce74 100644 --- a/web/i18n/vi-VN/common.ts +++ b/web/i18n/vi-VN/common.ts @@ -76,9 +76,9 @@ const translation = { }, model: { params: { - temperature: 'Nhiệt độ', + temperature: 'Độ sáng tạo', temperatureTip: - 'Kiểm soát độ ngẫu nhiên: Giảm nhiệt độ dẫn đến ít kết quả ngẫu nhiên hơn. Khi nhiệt độ gần bằng 0, mô hình sẽ trở nên xác định và lặp lại.', + 'Kiểm soát độ ngẫu nhiên: Giảm độ sáng tạo dẫn đến ít kết quả ngẫu nhiên hơn. Khi độ sáng tạo gần bằng 0, mô hình sẽ trở nên xác định và lặp lại.', top_p: 'Top P', top_pTip: 'Kiểm soát đa dạng thông qua lấy mẫu nhân nhóm: 0.5 có nghĩa là nửa số tùy chọn có khả năng cao được xem xét.', diff --git a/web/i18n/zh-Hant/app.ts b/web/i18n/zh-Hant/app.ts index 41230936e2..4b915b7f2d 100644 --- a/web/i18n/zh-Hant/app.ts +++ b/web/i18n/zh-Hant/app.ts @@ -69,6 +69,8 @@ const translation = { iconPicker: { ok: '確認', cancel: '取消', + emoji: '表情符號', + image: '圖片', }, switch: '遷移為工作流編排', switchTipStart: '將為您建立一個使用工作流編排的新應用。新應用將', From 218380ba43d97c1aff08c395afd1c96023bd5e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=86=E8=90=8C=E9=97=B7=E6=B2=B9=E7=93=B6?= <253605712@qq.com> Date: Tue, 20 Aug 2024 10:57:33 +0800 Subject: [PATCH 14/16] fix:end of day (#7426) --- .../app/(appDetailLayout)/[appId]/overview/chartView.tsx | 4 ++-- web/app/components/app/log/index.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx index 089d898383..ff32a157fc 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/chartView.tsx @@ -25,7 +25,7 @@ export default function ChartView({ appId }: IChartViewProps) { const appDetail = useAppStore(state => state.appDetail) const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow' const isWorkflow = appDetail?.mode === 'workflow' - const [period, setPeriod] = useState({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.format(queryDateFormat) } }) + const [period, setPeriod] = useState({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }) const onSelect = (item: Item) => { if (item.value === 'all') { @@ -37,7 +37,7 @@ export default function ChartView({ appId }: IChartViewProps) { setPeriod({ name: item.name, query: { start: startOfToday, end: endOfToday } }) } else { - setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').startOf('day').format(queryDateFormat), end: today.format(queryDateFormat) } }) + setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }) } } diff --git a/web/app/components/app/log/index.tsx b/web/app/components/app/log/index.tsx index 828004d6f3..e9ad2f43c6 100644 --- a/web/app/components/app/log/index.tsx +++ b/web/app/components/app/log/index.tsx @@ -61,7 +61,7 @@ const Logs: FC = ({ appDetail }) => { ...(queryParams.period !== 'all' ? { start: dayjs().subtract(queryParams.period as number, 'day').startOf('day').format('YYYY-MM-DD HH:mm'), - end: dayjs().format('YYYY-MM-DD HH:mm'), + end: dayjs().endOf('day').format('YYYY-MM-DD HH:mm'), } : {}), ...omit(queryParams, ['period']), From 0223fc6fd57589daf613bf15eff0e4df8f8f3289 Mon Sep 17 00:00:00 2001 From: Byeongjin Kang Date: Tue, 20 Aug 2024 12:01:13 +0900 Subject: [PATCH 15/16] feat: add pgvector full_text_search (#7396) --- .../rag/datasource/vdb/pgvector/pgvector.py | 23 +++++++++++++++++-- .../vdb/pgvector/test_pgvector.py | 4 ---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/api/core/rag/datasource/vdb/pgvector/pgvector.py b/api/core/rag/datasource/vdb/pgvector/pgvector.py index 33ca5bc028..c9f2f35af0 100644 --- a/api/core/rag/datasource/vdb/pgvector/pgvector.py +++ b/api/core/rag/datasource/vdb/pgvector/pgvector.py @@ -152,8 +152,27 @@ class PGVector(BaseVector): return docs def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]: - # do not support bm25 search - return [] + top_k = kwargs.get("top_k", 5) + + with self._get_cursor() as cur: + cur.execute( + f"""SELECT meta, text, ts_rank(to_tsvector(coalesce(text, '')), to_tsquery(%s)) AS score + FROM {self.table_name} + WHERE to_tsvector(text) @@ plainto_tsquery(%s) + ORDER BY score DESC + LIMIT {top_k}""", + # f"'{query}'" is required in order to account for whitespace in query + (f"'{query}'", f"'{query}'"), + ) + + docs = [] + + for record in cur: + metadata, text, score = record + metadata["score"] = score + docs.append(Document(page_content=text, metadata=metadata)) + + return docs def delete(self) -> None: with self._get_cursor() as cur: diff --git a/api/tests/integration_tests/vdb/pgvector/test_pgvector.py b/api/tests/integration_tests/vdb/pgvector/test_pgvector.py index 851599c7ce..c5a986b747 100644 --- a/api/tests/integration_tests/vdb/pgvector/test_pgvector.py +++ b/api/tests/integration_tests/vdb/pgvector/test_pgvector.py @@ -21,10 +21,6 @@ class PGVectorTest(AbstractVectorTest): ), ) - def search_by_full_text(self): - hits_by_full_text: list[Document] = self.vector.search_by_full_text(query=get_example_text()) - assert len(hits_by_full_text) == 0 - def test_pgvector(setup_mock_redis): PGVectorTest().run_all_tests() From 53146ad685eb4801f68754f463b8be40defe65bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Tue, 20 Aug 2024 11:03:55 +0800 Subject: [PATCH 16/16] feat: support line break of tooltip content (#7424) --- .../builtin/jina/tools/jina_tokenizer.yaml | 8 +++++++- .../model-provider-page/model-modal/Form.tsx | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/api/core/tools/provider/builtin/jina/tools/jina_tokenizer.yaml b/api/core/tools/provider/builtin/jina/tools/jina_tokenizer.yaml index 807aeec39b..62a5c7e7ba 100644 --- a/api/core/tools/provider/builtin/jina/tools/jina_tokenizer.yaml +++ b/api/core/tools/provider/builtin/jina/tools/jina_tokenizer.yaml @@ -60,5 +60,11 @@ parameters: label: en_US: Tokenizer human_description: - en_US: cl100k_base - gpt-4,gpt-3.5-turbo,gpt-3.5; o200k_base - gpt-4o,gpt-4o-mini; p50k_base - text-davinci-003,text-davinci-002 + en_US: | + · cl100k_base --- gpt-4, gpt-3.5-turbo, gpt-3.5 + · o200k_base --- gpt-4o, gpt-4o-mini + · p50k_base --- text-davinci-003, text-davinci-002 + · r50k_base --- text-davinci-001, text-curie-001 + · p50k_edit --- text-davinci-edit-001, code-davinci-edit-001 + · gpt2 --- gpt-2 form: form diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx index 6bf7f389fc..3cf3fc513f 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { Fragment, useState } from 'react' import type { FC } from 'react' import { RiQuestionLine, @@ -70,6 +70,16 @@ const Form: FC = ({ onChange({ ...value, [key]: val, ...shouldClearVariable }) } + // convert tooltip '\n' to
+ const renderTooltipContent = (content: string) => { + return content.split('\n').map((line, index, array) => ( + + {line} + {index < array.length - 1 &&
} +
+ )) + } + const renderField = (formSchema: CredentialFormSchema) => { const tooltip = formSchema.tooltip const tooltipContent = (tooltip && ( @@ -77,7 +87,7 @@ const Form: FC = ({ - {tooltip[language] || tooltip.en_US} + {renderTooltipContent(tooltip[language] || tooltip.en_US)}
} >