mirror of
https://github.com/langgenius/dify.git
synced 2026-01-27 07:15:51 +08:00
Compare commits
6 Commits
yanli/api-
...
refactor/d
| Author | SHA1 | Date | |
|---|---|---|---|
| 38d68ac4e3 | |||
| 4e48a1c4c3 | |||
| fb9a6bbc9f | |||
| a86765e2b6 | |||
| 5eaf0c733a | |||
| f561656a89 |
0
agent-notes/.gitkeep
Normal file
0
agent-notes/.gitkeep
Normal file
@ -0,0 +1,27 @@
|
||||
# Notes: `large_language_model.py`
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides the base `LargeLanguageModel` implementation used by the model runtime to invoke plugin-backed LLMs and to
|
||||
bridge plugin daemon streaming semantics back into API-layer entities (`LLMResult`, `LLMResultChunk`).
|
||||
|
||||
## Key behaviors / invariants
|
||||
|
||||
- `invoke(..., stream=False)` still calls the plugin in streaming mode and then synthesizes a single `LLMResult` from
|
||||
the first yielded `LLMResultChunk`.
|
||||
- Plugin invocation is wrapped by `_invoke_llm_via_plugin(...)`, and `stream=False` normalization is handled by
|
||||
`_normalize_non_stream_plugin_result(...)` / `_build_llm_result_from_first_chunk(...)`.
|
||||
- Tool call deltas are merged incrementally via `_increase_tool_call(...)` to support multiple provider chunking
|
||||
patterns (IDs anchored to first chunk, every chunk, or missing entirely).
|
||||
- A tool-call delta with an empty `id` requires at least one existing tool call; otherwise we raise `ValueError` to
|
||||
surface invalid delta sequences explicitly.
|
||||
- Callback invocation is centralized in `_run_callbacks(...)` to ensure consistent error handling/logging.
|
||||
- For compatibility with dify issue `#17799`, `prompt_messages` may be removed by the plugin daemon in chunks and must
|
||||
be re-attached in this layer before callbacks/consumers use them.
|
||||
- Callback hooks (`on_before_invoke`, `on_new_chunk`, `on_after_invoke`, `on_invoke_error`) must not break invocation
|
||||
unless `callback.raise_error` is true.
|
||||
|
||||
## Test focus
|
||||
|
||||
- `api/tests/unit_tests/core/model_runtime/__base/test_increase_tool_call.py` validates tool-call delta merging and
|
||||
patches `_gen_tool_call_id` for deterministic IDs.
|
||||
120
api/AGENTS.md
120
api/AGENTS.md
@ -1,47 +1,97 @@
|
||||
# API Agent Guide
|
||||
|
||||
## Notes for Agent (must-check)
|
||||
## Agent Notes (must-check)
|
||||
|
||||
Before changing any backend code under `api/`, you MUST read the surrounding docstrings and comments. These notes contain required context (invariants, edge cases, trade-offs) and are treated as part of the spec.
|
||||
Before you start work on any backend file under `api/`, you MUST check whether a related note exists under:
|
||||
|
||||
Look for:
|
||||
- `agent-notes/<same-relative-path-as-target-file>.md`
|
||||
|
||||
- The module (file) docstring at the top of a source code file
|
||||
- Docstrings on classes and functions/methods
|
||||
- Paragraph/block comments for non-obvious logic
|
||||
Rules:
|
||||
|
||||
### What to write where
|
||||
- **Path mapping**: for a target file `<path>/<name>.py`, the note must be `agent-notes/<path>/<name>.py.md` (same folder structure, same filename, plus `.md`).
|
||||
- **Before working**:
|
||||
- If the note exists, read it first and follow any constraints/decisions recorded there.
|
||||
- If the note conflicts with the current code, or references an "origin" file/path that has been deleted, renamed, or migrated, treat the **code as the single source of truth** and update the note to match reality.
|
||||
- If the note does not exist, create it with a short architecture/intent summary and any relevant invariants/edge cases.
|
||||
- **During working**:
|
||||
- Keep the note in sync as you discover constraints, make decisions, or change approach.
|
||||
- If you move/rename a file, migrate its note to the new mapped path (and fix any outdated references inside the note).
|
||||
- Record non-obvious edge cases, trade-offs, and the test/verification plan as you go (not just at the end).
|
||||
- Keep notes **coherent**: integrate new findings into the relevant sections and rewrite for clarity; avoid append-only “recent fix” / changelog-style additions unless the note is explicitly intended to be a changelog.
|
||||
- **When finishing work**:
|
||||
- Update the related note(s) to reflect what changed, why, and any new edge cases/tests.
|
||||
- If a file is deleted, remove or clearly deprecate the corresponding note so it cannot be mistaken as current guidance.
|
||||
- Keep notes concise and accurate; they are meant to prevent repeated rediscovery.
|
||||
|
||||
- Keep notes scoped: module notes cover module-wide context, class notes cover class-wide context, function/method notes cover behavioural contracts, and paragraph/block comments cover local “why”. Avoid duplicating the same content across scopes unless repetition prevents misuse.
|
||||
- **Module (file) docstring**: purpose, boundaries, key invariants, and “gotchas” that a new reader must know before editing.
|
||||
- Include cross-links to the key collaborators (modules/services) when discovery is otherwise hard.
|
||||
- Prefer stable facts (invariants, contracts) over ephemeral “today we…” notes.
|
||||
- **Class docstring**: responsibility, lifecycle, invariants, and how it should be used (or not used).
|
||||
- If the class is intentionally stateful, note what state exists and what methods mutate it.
|
||||
- If concurrency/async assumptions matter, state them explicitly.
|
||||
- **Function/method docstring**: behavioural contract.
|
||||
- Document arguments, return shape, side effects (DB writes, external I/O, task dispatch), and raised domain exceptions.
|
||||
- Add examples only when they prevent misuse.
|
||||
- **Paragraph/block comments**: explain *why* (trade-offs, historical constraints, surprising edge cases), not what the code already states.
|
||||
- Keep comments adjacent to the logic they justify; delete or rewrite comments that no longer match reality.
|
||||
## Skill Index
|
||||
|
||||
### Rules (must follow)
|
||||
Start with the section that best matches your need. Each entry lists the problems it solves plus key files/concepts so you know what to expect before opening it.
|
||||
|
||||
In this section, “notes” means module/class/function docstrings plus any relevant paragraph/block comments.
|
||||
### Platform Foundations
|
||||
|
||||
- **Before working**
|
||||
- Read the notes in the area you’ll touch; treat them as part of the spec.
|
||||
- If a docstring or comment conflicts with the current code, treat the **code as the single source of truth** and update the docstring or comment to match reality.
|
||||
- If important intent/invariants/edge cases are missing, add them in the closest docstring or comment (module for overall scope, function for behaviour).
|
||||
- **During working**
|
||||
- Keep the notes in sync as you discover constraints, make decisions, or change approach.
|
||||
- If you move/rename responsibilities across modules/classes, update the affected docstrings and comments so readers can still find the “why” and the invariants.
|
||||
- Record non-obvious edge cases, trade-offs, and the test/verification plan in the nearest docstring or comment that will stay correct.
|
||||
- Keep the notes **coherent**: integrate new findings into the relevant docstrings and comments; avoid append-only “recent fix” / changelog-style additions.
|
||||
- **When finishing**
|
||||
- Update the notes to reflect what changed, why, and any new edge cases/tests.
|
||||
- Remove or rewrite any comments that could be mistaken as current guidance but no longer apply.
|
||||
- Keep docstrings and comments concise and accurate; they are meant to prevent repeated rediscovery.
|
||||
#### [Infrastructure Overview](agent_skills/infra.md)
|
||||
|
||||
- **When to read this**
|
||||
- You need to understand where a feature belongs in the architecture.
|
||||
- You’re wiring storage, Redis, vector stores, or OTEL.
|
||||
- You’re about to add CLI commands or async jobs.
|
||||
- **What it covers**
|
||||
- Configuration stack (`configs/app_config.py`, remote settings)
|
||||
- Storage entry points (`extensions/ext_storage.py`, `core/file/file_manager.py`)
|
||||
- Redis conventions (`extensions/ext_redis.py`)
|
||||
- Plugin runtime topology
|
||||
- Vector-store factory (`core/rag/datasource/vdb/*`)
|
||||
- Observability hooks
|
||||
- SSRF proxy usage
|
||||
- Core CLI commands
|
||||
|
||||
### Plugin & Extension Development
|
||||
|
||||
#### [Plugin Systems](agent_skills/plugin.md)
|
||||
|
||||
- **When to read this**
|
||||
- You’re building or debugging a marketplace plugin.
|
||||
- You need to know how manifests, providers, daemons, and migrations fit together.
|
||||
- **What it covers**
|
||||
- Plugin manifests (`core/plugin/entities/plugin.py`)
|
||||
- Installation/upgrade flows (`services/plugin/plugin_service.py`, CLI commands)
|
||||
- Runtime adapters (`core/plugin/impl/*` for tool/model/datasource/trigger/endpoint/agent)
|
||||
- Daemon coordination (`core/plugin/entities/plugin_daemon.py`)
|
||||
- How provider registries surface capabilities to the rest of the platform
|
||||
|
||||
#### [Plugin OAuth](agent_skills/plugin_oauth.md)
|
||||
|
||||
- **When to read this**
|
||||
- You must integrate OAuth for a plugin or datasource.
|
||||
- You’re handling credential encryption or refresh flows.
|
||||
- **Topics**
|
||||
- Credential storage
|
||||
- Encryption helpers (`core/helper/provider_encryption.py`)
|
||||
- OAuth client bootstrap (`services/plugin/oauth_service.py`, `services/plugin/plugin_parameter_service.py`)
|
||||
- How console/API layers expose the flows
|
||||
|
||||
### Workflow Entry & Execution
|
||||
|
||||
#### [Trigger Concepts](agent_skills/trigger.md)
|
||||
|
||||
- **When to read this**
|
||||
- You’re debugging why a workflow didn’t start.
|
||||
- You’re adding a new trigger type or hook.
|
||||
- You need to trace async execution, draft debugging, or webhook/schedule pipelines.
|
||||
- **Details**
|
||||
- Start-node taxonomy
|
||||
- Webhook & schedule internals (`core/workflow/nodes/trigger_*`, `services/trigger/*`)
|
||||
- Async orchestration (`services/async_workflow_service.py`, Celery queues)
|
||||
- Debug event bus
|
||||
- Storage/logging interactions
|
||||
|
||||
## General Reminders
|
||||
|
||||
- All skill docs assume you follow the coding style rules below—run the lint/type/test commands before submitting changes.
|
||||
- When you cannot find an answer in these briefs, search the codebase using the paths referenced (e.g., `core/plugin/impl/tool.py`, `services/dataset_service.py`).
|
||||
- If you run into cross-cutting concerns (tenancy, configuration, storage), check the infrastructure guide first; it links to most supporting modules.
|
||||
- Keep multi-tenancy and configuration central: everything flows through `configs.dify_config` and `tenant_id`.
|
||||
- When touching plugins or triggers, consult both the system overview and the specialised doc to ensure you adjust lifecycle, storage, and observability consistently.
|
||||
|
||||
## Coding Style
|
||||
|
||||
@ -176,7 +226,7 @@ Before opening a PR / submitting:
|
||||
|
||||
- Controllers: parse input via Pydantic, invoke services, return serialised responses; no business logic.
|
||||
- Services: coordinate repositories, providers, background tasks; keep side effects explicit.
|
||||
- Document non-obvious behaviour with concise docstrings and comments.
|
||||
- Document non-obvious behaviour with concise comments.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
|
||||
@ -11,7 +11,9 @@ from controllers.service_api.wraps import DatasetApiResource, cloud_edition_bill
|
||||
from fields.dataset_fields import dataset_metadata_fields
|
||||
from services.dataset_service import DatasetService
|
||||
from services.entities.knowledge_entities.knowledge_entities import (
|
||||
DocumentMetadataOperation,
|
||||
MetadataArgs,
|
||||
MetadataDetail,
|
||||
MetadataOperationData,
|
||||
)
|
||||
from services.metadata_service import MetadataService
|
||||
@ -22,7 +24,13 @@ class MetadataUpdatePayload(BaseModel):
|
||||
|
||||
|
||||
register_schema_model(service_api_ns, MetadataUpdatePayload)
|
||||
register_schema_models(service_api_ns, MetadataArgs, MetadataOperationData)
|
||||
register_schema_models(
|
||||
service_api_ns,
|
||||
MetadataArgs,
|
||||
MetadataDetail,
|
||||
DocumentMetadataOperation,
|
||||
MetadataOperationData,
|
||||
)
|
||||
|
||||
|
||||
@service_api_ns.route("/datasets/<uuid:dataset_id>/metadata")
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import * as React from 'react'
|
||||
|
||||
declare const require: any
|
||||
|
||||
type IconComponent = React.ComponentType<Record<string, unknown>>
|
||||
type IconModule = { default: IconComponent }
|
||||
|
||||
type IconEntry = {
|
||||
name: string
|
||||
@ -12,18 +12,16 @@ type IconEntry = {
|
||||
Component: IconComponent
|
||||
}
|
||||
|
||||
const iconContext = require.context('./src', true, /\.tsx$/)
|
||||
const iconModules: Record<string, IconModule> = import.meta.glob('./src/**/*.tsx', { eager: true })
|
||||
|
||||
const iconEntries: IconEntry[] = iconContext
|
||||
.keys()
|
||||
.filter((key: string) => !key.endsWith('.stories.tsx') && !key.endsWith('.spec.tsx'))
|
||||
.map((key: string) => {
|
||||
const mod = iconContext(key)
|
||||
const Component = mod.default as IconComponent | undefined
|
||||
const iconEntries: IconEntry[] = Object.entries(iconModules)
|
||||
.filter(([key]) => !key.endsWith('.stories.tsx') && !key.endsWith('.spec.tsx'))
|
||||
.map(([key, mod]) => {
|
||||
const Component = mod.default
|
||||
if (!Component)
|
||||
return null
|
||||
|
||||
const relativePath = key.replace(/^\.\//, '')
|
||||
const relativePath = key.replace(/^\.\/src\//, '')
|
||||
const path = `app/components/base/icons/src/${relativePath}`
|
||||
const parts = relativePath.split('/')
|
||||
const fileName = parts.pop() || ''
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
useWorkflowReadOnly,
|
||||
} from '../hooks'
|
||||
import { useStore, useWorkflowStore } from '../store'
|
||||
import { BlockEnum, ControlMode } from '../types'
|
||||
import { BlockEnum, ControlMode, WorkflowRunningStatus } from '../types'
|
||||
import {
|
||||
getLayoutByDagre,
|
||||
getLayoutForChildNodes,
|
||||
@ -36,12 +36,17 @@ export const useWorkflowInteractions = () => {
|
||||
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
|
||||
|
||||
const handleCancelDebugAndPreviewPanel = useCallback(() => {
|
||||
const { workflowRunningData } = workflowStore.getState()
|
||||
const runningStatus = workflowRunningData?.result?.status
|
||||
const isActiveRun = runningStatus === WorkflowRunningStatus.Running || runningStatus === WorkflowRunningStatus.Waiting
|
||||
workflowStore.setState({
|
||||
showDebugAndPreviewPanel: false,
|
||||
workflowRunningData: undefined,
|
||||
...(isActiveRun ? {} : { workflowRunningData: undefined }),
|
||||
})
|
||||
handleNodeCancelRunningStatus()
|
||||
handleEdgeCancelRunningStatus()
|
||||
if (!isActiveRun) {
|
||||
handleNodeCancelRunningStatus()
|
||||
handleEdgeCancelRunningStatus()
|
||||
}
|
||||
}, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus])
|
||||
|
||||
return {
|
||||
|
||||
@ -21,7 +21,7 @@ import {
|
||||
import { BlockEnum } from '../../types'
|
||||
import ConversationVariableModal from './conversation-variable-modal'
|
||||
import Empty from './empty'
|
||||
import { useChat } from './hooks'
|
||||
import { useChat } from './hooks/use-chat'
|
||||
import UserInput from './user-input'
|
||||
|
||||
type ChatWrapperProps = {
|
||||
|
||||
@ -1,516 +0,0 @@
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type {
|
||||
ChatItem,
|
||||
ChatItemInTree,
|
||||
Inputs,
|
||||
} from '@/app/components/base/chat/types'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import { uniqBy } from 'es-toolkit/compat'
|
||||
import { produce, setAutoFreeze } from 'immer'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
getProcessedInputs,
|
||||
processOpeningStatement,
|
||||
} from '@/app/components/base/chat/chat/utils'
|
||||
import { getThreadMessages } from '@/app/components/base/chat/utils'
|
||||
import {
|
||||
getProcessedFiles,
|
||||
getProcessedFilesFromResponse,
|
||||
} from '@/app/components/base/file-uploader/utils'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { DEFAULT_ITER_TIMES, DEFAULT_LOOP_TIMES } from '../../constants'
|
||||
import {
|
||||
useSetWorkflowVarsWithValue,
|
||||
useWorkflowRun,
|
||||
} from '../../hooks'
|
||||
import { useHooksStore } from '../../hooks-store'
|
||||
import { useWorkflowStore } from '../../store'
|
||||
import { NodeRunningStatus, WorkflowRunningStatus } from '../../types'
|
||||
|
||||
type GetAbortController = (abortController: AbortController) => void
|
||||
type SendCallback = {
|
||||
onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<any>
|
||||
}
|
||||
export const useChat = (
|
||||
config: any,
|
||||
formSettings?: {
|
||||
inputs: Inputs
|
||||
inputsForm: InputForm[]
|
||||
},
|
||||
prevChatTree?: ChatItemInTree[],
|
||||
stopChat?: (taskId: string) => void,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { handleRun } = useWorkflowRun()
|
||||
const hasStopResponded = useRef(false)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const conversationId = useRef('')
|
||||
const taskIdRef = useRef('')
|
||||
const [isResponding, setIsResponding] = useState(false)
|
||||
const isRespondingRef = useRef(false)
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
||||
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
|
||||
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
|
||||
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
|
||||
const {
|
||||
setIterTimes,
|
||||
setLoopTimes,
|
||||
} = workflowStore.getState()
|
||||
|
||||
const handleResponding = useCallback((isResponding: boolean) => {
|
||||
setIsResponding(isResponding)
|
||||
isRespondingRef.current = isResponding
|
||||
}, [])
|
||||
|
||||
const [chatTree, setChatTree] = useState<ChatItemInTree[]>(prevChatTree || [])
|
||||
const chatTreeRef = useRef<ChatItemInTree[]>(chatTree)
|
||||
const [targetMessageId, setTargetMessageId] = useState<string>()
|
||||
const threadMessages = useMemo(() => getThreadMessages(chatTree, targetMessageId), [chatTree, targetMessageId])
|
||||
|
||||
const getIntroduction = useCallback((str: string) => {
|
||||
return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
|
||||
}, [formSettings?.inputs, formSettings?.inputsForm])
|
||||
|
||||
/** Final chat list that will be rendered */
|
||||
const chatList = useMemo(() => {
|
||||
const ret = [...threadMessages]
|
||||
if (config?.opening_statement) {
|
||||
const index = threadMessages.findIndex(item => item.isOpeningStatement)
|
||||
|
||||
if (index > -1) {
|
||||
ret[index] = {
|
||||
...ret[index],
|
||||
content: getIntroduction(config.opening_statement),
|
||||
suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)),
|
||||
}
|
||||
}
|
||||
else {
|
||||
ret.unshift({
|
||||
id: `${Date.now()}`,
|
||||
content: getIntroduction(config.opening_statement),
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)),
|
||||
})
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}, [threadMessages, config?.opening_statement, getIntroduction, config?.suggested_questions])
|
||||
|
||||
useEffect(() => {
|
||||
setAutoFreeze(false)
|
||||
return () => {
|
||||
setAutoFreeze(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** Find the target node by bfs and then operate on it */
|
||||
const produceChatTreeNode = useCallback((targetId: string, operation: (node: ChatItemInTree) => void) => {
|
||||
return produce(chatTreeRef.current, (draft) => {
|
||||
const queue: ChatItemInTree[] = [...draft]
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!
|
||||
if (current.id === targetId) {
|
||||
operation(current)
|
||||
break
|
||||
}
|
||||
if (current.children)
|
||||
queue.push(...current.children)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
hasStopResponded.current = true
|
||||
handleResponding(false)
|
||||
if (stopChat && taskIdRef.current)
|
||||
stopChat(taskIdRef.current)
|
||||
setIterTimes(DEFAULT_ITER_TIMES)
|
||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||
if (suggestedQuestionsAbortControllerRef.current)
|
||||
suggestedQuestionsAbortControllerRef.current.abort()
|
||||
}, [handleResponding, setIterTimes, setLoopTimes, stopChat])
|
||||
|
||||
const handleRestart = useCallback(() => {
|
||||
conversationId.current = ''
|
||||
taskIdRef.current = ''
|
||||
handleStop()
|
||||
setIterTimes(DEFAULT_ITER_TIMES)
|
||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||
setChatTree([])
|
||||
setSuggestQuestions([])
|
||||
}, [
|
||||
handleStop,
|
||||
setIterTimes,
|
||||
setLoopTimes,
|
||||
])
|
||||
|
||||
const updateCurrentQAOnTree = useCallback(({
|
||||
parentId,
|
||||
responseItem,
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
}: {
|
||||
parentId?: string
|
||||
responseItem: ChatItem
|
||||
placeholderQuestionId: string
|
||||
questionItem: ChatItem
|
||||
}) => {
|
||||
let nextState: ChatItemInTree[]
|
||||
const currentQA = { ...questionItem, children: [{ ...responseItem, children: [] }] }
|
||||
if (!parentId && !chatTree.some(item => [placeholderQuestionId, questionItem.id].includes(item.id))) {
|
||||
// QA whose parent is not provided is considered as a first message of the conversation,
|
||||
// and it should be a root node of the chat tree
|
||||
nextState = produce(chatTree, (draft) => {
|
||||
draft.push(currentQA)
|
||||
})
|
||||
}
|
||||
else {
|
||||
// find the target QA in the tree and update it; if not found, insert it to its parent node
|
||||
nextState = produceChatTreeNode(parentId!, (parentNode) => {
|
||||
const questionNodeIndex = parentNode.children!.findIndex(item => [placeholderQuestionId, questionItem.id].includes(item.id))
|
||||
if (questionNodeIndex === -1)
|
||||
parentNode.children!.push(currentQA)
|
||||
else
|
||||
parentNode.children![questionNodeIndex] = currentQA
|
||||
})
|
||||
}
|
||||
setChatTree(nextState)
|
||||
chatTreeRef.current = nextState
|
||||
}, [chatTree, produceChatTreeNode])
|
||||
|
||||
const handleSend = useCallback((
|
||||
params: {
|
||||
query: string
|
||||
files?: FileEntity[]
|
||||
parent_message_id?: string
|
||||
[key: string]: any
|
||||
},
|
||||
{
|
||||
onGetSuggestedQuestions,
|
||||
}: SendCallback,
|
||||
) => {
|
||||
if (isRespondingRef.current) {
|
||||
notify({ type: 'info', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) })
|
||||
return false
|
||||
}
|
||||
|
||||
const parentMessage = threadMessages.find(item => item.id === params.parent_message_id)
|
||||
|
||||
const placeholderQuestionId = `question-${Date.now()}`
|
||||
const questionItem = {
|
||||
id: placeholderQuestionId,
|
||||
content: params.query,
|
||||
isAnswer: false,
|
||||
message_files: params.files,
|
||||
parentMessageId: params.parent_message_id,
|
||||
}
|
||||
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
const placeholderAnswerItem = {
|
||||
id: placeholderAnswerId,
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
parentMessageId: questionItem.id,
|
||||
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
|
||||
}
|
||||
|
||||
setTargetMessageId(parentMessage?.id)
|
||||
updateCurrentQAOnTree({
|
||||
parentId: params.parent_message_id,
|
||||
responseItem: placeholderAnswerItem,
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
})
|
||||
|
||||
// answer
|
||||
const responseItem: ChatItem = {
|
||||
id: placeholderAnswerId,
|
||||
content: '',
|
||||
agent_thoughts: [],
|
||||
message_files: [],
|
||||
isAnswer: true,
|
||||
parentMessageId: questionItem.id,
|
||||
siblingIndex: parentMessage?.children?.length ?? chatTree.length,
|
||||
}
|
||||
|
||||
handleResponding(true)
|
||||
|
||||
const { files, inputs, ...restParams } = params
|
||||
const bodyParams = {
|
||||
files: getProcessedFiles(files || []),
|
||||
inputs: getProcessedInputs(inputs || {}, formSettings?.inputsForm || []),
|
||||
...restParams,
|
||||
}
|
||||
if (bodyParams?.files?.length) {
|
||||
bodyParams.files = bodyParams.files.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
let hasSetResponseId = false
|
||||
|
||||
handleRun(
|
||||
bodyParams,
|
||||
{
|
||||
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
|
||||
responseItem.content = responseItem.content + message
|
||||
|
||||
if (messageId && !hasSetResponseId) {
|
||||
questionItem.id = `question-${messageId}`
|
||||
responseItem.id = messageId
|
||||
responseItem.parentMessageId = questionItem.id
|
||||
hasSetResponseId = true
|
||||
}
|
||||
|
||||
if (isFirstMessage && newConversationId)
|
||||
conversationId.current = newConversationId
|
||||
|
||||
taskIdRef.current = taskId
|
||||
if (messageId)
|
||||
responseItem.id = messageId
|
||||
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
},
|
||||
async onCompleted(hasError?: boolean, errorMessage?: string) {
|
||||
handleResponding(false)
|
||||
fetchInspectVars({})
|
||||
invalidAllLastRun()
|
||||
|
||||
if (hasError) {
|
||||
if (errorMessage) {
|
||||
responseItem.content = errorMessage
|
||||
responseItem.isError = true
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
|
||||
try {
|
||||
const { data }: any = await onGetSuggestedQuestions(
|
||||
responseItem.id,
|
||||
newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController,
|
||||
)
|
||||
setSuggestQuestions(data)
|
||||
}
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
catch (error) {
|
||||
setSuggestQuestions([])
|
||||
}
|
||||
}
|
||||
},
|
||||
onMessageEnd: (messageEnd) => {
|
||||
responseItem.citation = messageEnd.metadata?.retriever_resources || []
|
||||
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
|
||||
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
|
||||
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
},
|
||||
onMessageReplace: (messageReplace) => {
|
||||
responseItem.content = messageReplace.answer
|
||||
},
|
||||
onError() {
|
||||
handleResponding(false)
|
||||
},
|
||||
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
|
||||
taskIdRef.current = task_id
|
||||
responseItem.workflow_run_id = workflow_run_id
|
||||
responseItem.workflowProcess = {
|
||||
status: WorkflowRunningStatus.Running,
|
||||
tracing: [],
|
||||
}
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
},
|
||||
onWorkflowFinished: ({ data }) => {
|
||||
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
},
|
||||
onIterationStart: ({ data }) => {
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
})
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
},
|
||||
onIterationFinish: ({ data }) => {
|
||||
const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id)
|
||||
if (currentTracingIndex > -1) {
|
||||
responseItem.workflowProcess!.tracing[currentTracingIndex] = {
|
||||
...responseItem.workflowProcess!.tracing[currentTracingIndex],
|
||||
...data,
|
||||
}
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
}
|
||||
},
|
||||
onLoopStart: ({ data }) => {
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
})
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
},
|
||||
onLoopFinish: ({ data }) => {
|
||||
const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id)
|
||||
if (currentTracingIndex > -1) {
|
||||
responseItem.workflowProcess!.tracing[currentTracingIndex] = {
|
||||
...responseItem.workflowProcess!.tracing[currentTracingIndex],
|
||||
...data,
|
||||
}
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
}
|
||||
},
|
||||
onNodeStarted: ({ data }) => {
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as any)
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
},
|
||||
onNodeRetry: ({ data }) => {
|
||||
responseItem.workflowProcess!.tracing!.push(data)
|
||||
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
},
|
||||
onNodeFinished: ({ data }) => {
|
||||
const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id)
|
||||
if (currentTracingIndex > -1) {
|
||||
responseItem.workflowProcess!.tracing[currentTracingIndex] = {
|
||||
...responseItem.workflowProcess!.tracing[currentTracingIndex],
|
||||
...data,
|
||||
}
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
}
|
||||
},
|
||||
onAgentLog: ({ data }) => {
|
||||
const currentNodeIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
|
||||
if (currentNodeIndex > -1) {
|
||||
const current = responseItem.workflowProcess!.tracing![currentNodeIndex]
|
||||
|
||||
if (current.execution_metadata) {
|
||||
if (current.execution_metadata.agent_log) {
|
||||
const currentLogIndex = current.execution_metadata.agent_log.findIndex(log => log.message_id === data.message_id)
|
||||
if (currentLogIndex > -1) {
|
||||
current.execution_metadata.agent_log[currentLogIndex] = {
|
||||
...current.execution_metadata.agent_log[currentLogIndex],
|
||||
...data,
|
||||
}
|
||||
}
|
||||
else {
|
||||
current.execution_metadata.agent_log.push(data)
|
||||
}
|
||||
}
|
||||
else {
|
||||
current.execution_metadata.agent_log = [data]
|
||||
}
|
||||
}
|
||||
else {
|
||||
current.execution_metadata = {
|
||||
agent_log: [data],
|
||||
} as any
|
||||
}
|
||||
|
||||
responseItem.workflowProcess!.tracing[currentNodeIndex] = {
|
||||
...current,
|
||||
}
|
||||
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}, [threadMessages, chatTree.length, updateCurrentQAOnTree, handleResponding, formSettings?.inputsForm, handleRun, notify, t, config?.suggested_questions_after_answer?.enabled, fetchInspectVars, invalidAllLastRun])
|
||||
|
||||
return {
|
||||
conversationId: conversationId.current,
|
||||
chatList,
|
||||
setTargetMessageId,
|
||||
handleSend,
|
||||
handleStop,
|
||||
handleRestart,
|
||||
isResponding,
|
||||
suggestedQuestions,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import type { ChatItem, ChatItemInTree } from '@/app/components/base/chat/types'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
|
||||
export type ChatConfig = {
|
||||
opening_statement?: string
|
||||
suggested_questions?: string[]
|
||||
suggested_questions_after_answer?: {
|
||||
enabled?: boolean
|
||||
}
|
||||
text_to_speech?: unknown
|
||||
speech_to_text?: unknown
|
||||
retriever_resource?: unknown
|
||||
sensitive_word_avoidance?: unknown
|
||||
file_upload?: unknown
|
||||
}
|
||||
|
||||
export type GetAbortController = (abortController: AbortController) => void
|
||||
|
||||
export type SendCallback = {
|
||||
onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<unknown>
|
||||
}
|
||||
|
||||
export type SendParams = {
|
||||
query: string
|
||||
files?: FileEntity[]
|
||||
parent_message_id?: string
|
||||
inputs?: Record<string, unknown>
|
||||
conversation_id?: string
|
||||
}
|
||||
|
||||
export type UpdateCurrentQAParams = {
|
||||
parentId?: string
|
||||
responseItem: ChatItem
|
||||
placeholderQuestionId: string
|
||||
questionItem: ChatItem
|
||||
}
|
||||
|
||||
export type ChatTreeUpdater = (updater: (chatTree: ChatItemInTree[]) => ChatItemInTree[]) => void
|
||||
@ -0,0 +1,90 @@
|
||||
import { useCallback } from 'react'
|
||||
import { DEFAULT_ITER_TIMES, DEFAULT_LOOP_TIMES } from '../../../constants'
|
||||
import { useEdgesInteractionsWithoutSync } from '../../../hooks/use-edges-interactions-without-sync'
|
||||
import { useNodesInteractionsWithoutSync } from '../../../hooks/use-nodes-interactions-without-sync'
|
||||
import { useStore, useWorkflowStore } from '../../../store'
|
||||
import { WorkflowRunningStatus } from '../../../types'
|
||||
|
||||
type UseChatFlowControlParams = {
|
||||
stopChat?: (taskId: string) => void
|
||||
}
|
||||
|
||||
export function useChatFlowControl({
|
||||
stopChat,
|
||||
}: UseChatFlowControlParams) {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const setIsResponding = useStore(s => s.setIsResponding)
|
||||
const resetChatPreview = useStore(s => s.resetChatPreview)
|
||||
const setActiveTaskId = useStore(s => s.setActiveTaskId)
|
||||
const setHasStopResponded = useStore(s => s.setHasStopResponded)
|
||||
const setSuggestedQuestionsAbortController = useStore(s => s.setSuggestedQuestionsAbortController)
|
||||
const invalidateRun = useStore(s => s.invalidateRun)
|
||||
const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync()
|
||||
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
|
||||
|
||||
const { setIterTimes, setLoopTimes } = workflowStore.getState()
|
||||
|
||||
const handleResponding = useCallback((responding: boolean) => {
|
||||
setIsResponding(responding)
|
||||
}, [setIsResponding])
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
const {
|
||||
activeTaskId,
|
||||
suggestedQuestionsAbortController,
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
const runningStatus = workflowRunningData?.result?.status
|
||||
const isActiveRun = runningStatus === WorkflowRunningStatus.Running || runningStatus === WorkflowRunningStatus.Waiting
|
||||
setHasStopResponded(true)
|
||||
handleResponding(false)
|
||||
if (stopChat && activeTaskId)
|
||||
stopChat(activeTaskId)
|
||||
setIterTimes(DEFAULT_ITER_TIMES)
|
||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||
if (suggestedQuestionsAbortController)
|
||||
suggestedQuestionsAbortController.abort()
|
||||
setSuggestedQuestionsAbortController(null)
|
||||
setActiveTaskId('')
|
||||
invalidateRun()
|
||||
if (isActiveRun && workflowRunningData) {
|
||||
setWorkflowRunningData({
|
||||
...workflowRunningData,
|
||||
result: {
|
||||
...workflowRunningData.result,
|
||||
status: WorkflowRunningStatus.Stopped,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (isActiveRun) {
|
||||
handleNodeCancelRunningStatus()
|
||||
handleEdgeCancelRunningStatus()
|
||||
}
|
||||
}, [
|
||||
handleResponding,
|
||||
setIterTimes,
|
||||
setLoopTimes,
|
||||
stopChat,
|
||||
workflowStore,
|
||||
setHasStopResponded,
|
||||
setSuggestedQuestionsAbortController,
|
||||
setActiveTaskId,
|
||||
invalidateRun,
|
||||
handleNodeCancelRunningStatus,
|
||||
handleEdgeCancelRunningStatus,
|
||||
])
|
||||
|
||||
const handleRestart = useCallback(() => {
|
||||
handleStop()
|
||||
resetChatPreview()
|
||||
setIterTimes(DEFAULT_ITER_TIMES)
|
||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||
}, [handleStop, setIterTimes, setLoopTimes, resetChatPreview])
|
||||
|
||||
return {
|
||||
handleResponding,
|
||||
handleStop,
|
||||
handleRestart,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatItemInTree, Inputs } from '@/app/components/base/chat/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { processOpeningStatement } from '@/app/components/base/chat/chat/utils'
|
||||
import { getThreadMessages } from '@/app/components/base/chat/utils'
|
||||
|
||||
type UseChatListParams = {
|
||||
chatTree: ChatItemInTree[]
|
||||
targetMessageId: string | undefined
|
||||
config: {
|
||||
opening_statement?: string
|
||||
suggested_questions?: string[]
|
||||
} | undefined
|
||||
formSettings?: {
|
||||
inputs: Inputs
|
||||
inputsForm: InputForm[]
|
||||
}
|
||||
}
|
||||
|
||||
export function useChatList({
|
||||
chatTree,
|
||||
targetMessageId,
|
||||
config,
|
||||
formSettings,
|
||||
}: UseChatListParams) {
|
||||
const threadMessages = useMemo(
|
||||
() => getThreadMessages(chatTree, targetMessageId),
|
||||
[chatTree, targetMessageId],
|
||||
)
|
||||
|
||||
const getIntroduction = useCallback((str: string) => {
|
||||
return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
|
||||
}, [formSettings?.inputs, formSettings?.inputsForm])
|
||||
|
||||
const chatList = useMemo(() => {
|
||||
const ret = [...threadMessages]
|
||||
if (config?.opening_statement) {
|
||||
const index = threadMessages.findIndex(item => item.isOpeningStatement)
|
||||
|
||||
if (index > -1) {
|
||||
ret[index] = {
|
||||
...ret[index],
|
||||
content: getIntroduction(config.opening_statement),
|
||||
suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)),
|
||||
}
|
||||
}
|
||||
else {
|
||||
ret.unshift({
|
||||
id: `${Date.now()}`,
|
||||
content: getIntroduction(config.opening_statement),
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true,
|
||||
suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)),
|
||||
})
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}, [threadMessages, config?.opening_statement, getIntroduction, config?.suggested_questions])
|
||||
|
||||
return {
|
||||
threadMessages,
|
||||
chatList,
|
||||
getIntroduction,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,306 @@
|
||||
import type { SendCallback, SendParams, UpdateCurrentQAParams } from './types'
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatItem, ChatItemInTree, Inputs } from '@/app/components/base/chat/types'
|
||||
import { uniqBy } from 'es-toolkit/compat'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getProcessedInputs } from '@/app/components/base/chat/chat/utils'
|
||||
import { getProcessedFiles, getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { useSetWorkflowVarsWithValue, useWorkflowRun } from '../../../hooks'
|
||||
import { useHooksStore } from '../../../hooks-store'
|
||||
import { useStore, useWorkflowStore } from '../../../store'
|
||||
import { createWorkflowEventHandlers } from './use-workflow-event-handlers'
|
||||
|
||||
type UseChatMessageSenderParams = {
|
||||
threadMessages: ChatItemInTree[]
|
||||
config?: {
|
||||
suggested_questions_after_answer?: {
|
||||
enabled?: boolean
|
||||
}
|
||||
}
|
||||
formSettings?: {
|
||||
inputs: Inputs
|
||||
inputsForm: InputForm[]
|
||||
}
|
||||
handleResponding: (responding: boolean) => void
|
||||
updateCurrentQAOnTree: (params: UpdateCurrentQAParams) => void
|
||||
}
|
||||
|
||||
export function useChatMessageSender({
|
||||
threadMessages,
|
||||
config,
|
||||
formSettings,
|
||||
handleResponding,
|
||||
updateCurrentQAOnTree,
|
||||
}: UseChatMessageSenderParams) {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { handleRun } = useWorkflowRun()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
||||
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
|
||||
const setConversationId = useStore(s => s.setConversationId)
|
||||
const setTargetMessageId = useStore(s => s.setTargetMessageId)
|
||||
const setSuggestedQuestions = useStore(s => s.setSuggestedQuestions)
|
||||
const setActiveTaskId = useStore(s => s.setActiveTaskId)
|
||||
const setSuggestedQuestionsAbortController = useStore(s => s.setSuggestedQuestionsAbortController)
|
||||
const startRun = useStore(s => s.startRun)
|
||||
|
||||
const handleSend = useCallback((
|
||||
params: SendParams,
|
||||
{ onGetSuggestedQuestions }: SendCallback,
|
||||
) => {
|
||||
if (workflowStore.getState().isResponding) {
|
||||
notify({ type: 'info', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) })
|
||||
return false
|
||||
}
|
||||
|
||||
const { suggestedQuestionsAbortController } = workflowStore.getState()
|
||||
if (suggestedQuestionsAbortController)
|
||||
suggestedQuestionsAbortController.abort()
|
||||
setSuggestedQuestionsAbortController(null)
|
||||
|
||||
const runId = startRun()
|
||||
const isCurrentRun = () => runId === workflowStore.getState().activeRunId
|
||||
|
||||
const parentMessage = threadMessages.find(item => item.id === params.parent_message_id)
|
||||
|
||||
const placeholderQuestionId = `question-${Date.now()}`
|
||||
const questionItem: ChatItem = {
|
||||
id: placeholderQuestionId,
|
||||
content: params.query,
|
||||
isAnswer: false,
|
||||
message_files: params.files,
|
||||
parentMessageId: params.parent_message_id,
|
||||
}
|
||||
|
||||
const siblingIndex = parentMessage?.children?.length ?? workflowStore.getState().chatTree.length
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
const placeholderAnswerItem: ChatItem = {
|
||||
id: placeholderAnswerId,
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
parentMessageId: questionItem.id,
|
||||
siblingIndex,
|
||||
}
|
||||
|
||||
setTargetMessageId(parentMessage?.id)
|
||||
updateCurrentQAOnTree({
|
||||
parentId: params.parent_message_id,
|
||||
responseItem: placeholderAnswerItem,
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
})
|
||||
|
||||
const responseItem: ChatItem = {
|
||||
id: placeholderAnswerId,
|
||||
content: '',
|
||||
agent_thoughts: [],
|
||||
message_files: [],
|
||||
isAnswer: true,
|
||||
parentMessageId: questionItem.id,
|
||||
siblingIndex,
|
||||
}
|
||||
|
||||
handleResponding(true)
|
||||
|
||||
const { files, inputs, ...restParams } = params
|
||||
const bodyParams = {
|
||||
files: getProcessedFiles(files || []),
|
||||
inputs: getProcessedInputs(inputs || {}, formSettings?.inputsForm || []),
|
||||
...restParams,
|
||||
}
|
||||
if (bodyParams?.files?.length) {
|
||||
bodyParams.files = bodyParams.files.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
return {
|
||||
...item,
|
||||
url: '',
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
let hasSetResponseId = false
|
||||
|
||||
const workflowHandlers = createWorkflowEventHandlers({
|
||||
responseItem,
|
||||
questionItem,
|
||||
placeholderQuestionId,
|
||||
parentMessageId: params.parent_message_id,
|
||||
updateCurrentQAOnTree,
|
||||
})
|
||||
|
||||
handleRun(
|
||||
bodyParams,
|
||||
{
|
||||
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
responseItem.content = responseItem.content + message
|
||||
|
||||
if (messageId && !hasSetResponseId) {
|
||||
questionItem.id = `question-${messageId}`
|
||||
responseItem.id = messageId
|
||||
responseItem.parentMessageId = questionItem.id
|
||||
hasSetResponseId = true
|
||||
}
|
||||
|
||||
if (isFirstMessage && newConversationId)
|
||||
setConversationId(newConversationId)
|
||||
|
||||
if (taskId)
|
||||
setActiveTaskId(taskId)
|
||||
if (messageId)
|
||||
responseItem.id = messageId
|
||||
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
},
|
||||
async onCompleted(hasError?: boolean, errorMessage?: string) {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
handleResponding(false)
|
||||
fetchInspectVars({})
|
||||
invalidAllLastRun()
|
||||
|
||||
if (hasError) {
|
||||
if (errorMessage) {
|
||||
responseItem.content = errorMessage
|
||||
responseItem.isError = true
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (config?.suggested_questions_after_answer?.enabled && !workflowStore.getState().hasStopResponded && onGetSuggestedQuestions) {
|
||||
try {
|
||||
const result = await onGetSuggestedQuestions(
|
||||
responseItem.id,
|
||||
newAbortController => setSuggestedQuestionsAbortController(newAbortController),
|
||||
) as { data: string[] }
|
||||
setSuggestedQuestions(result.data)
|
||||
}
|
||||
catch {
|
||||
setSuggestedQuestions([])
|
||||
}
|
||||
finally {
|
||||
setSuggestedQuestionsAbortController(null)
|
||||
}
|
||||
}
|
||||
},
|
||||
onMessageEnd: (messageEnd) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
responseItem.citation = messageEnd.metadata?.retriever_resources || []
|
||||
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
|
||||
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
|
||||
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
},
|
||||
onMessageReplace: (messageReplace) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
responseItem.content = messageReplace.answer
|
||||
},
|
||||
onError() {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
handleResponding(false)
|
||||
},
|
||||
onWorkflowStarted: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
const taskId = workflowHandlers.onWorkflowStarted(event)
|
||||
if (taskId)
|
||||
setActiveTaskId(taskId)
|
||||
},
|
||||
onWorkflowFinished: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onWorkflowFinished(event)
|
||||
},
|
||||
onIterationStart: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onIterationStart(event)
|
||||
},
|
||||
onIterationFinish: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onIterationFinish(event)
|
||||
},
|
||||
onLoopStart: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onLoopStart(event)
|
||||
},
|
||||
onLoopFinish: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onLoopFinish(event)
|
||||
},
|
||||
onNodeStarted: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onNodeStarted(event)
|
||||
},
|
||||
onNodeRetry: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onNodeRetry(event)
|
||||
},
|
||||
onNodeFinished: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onNodeFinished(event)
|
||||
},
|
||||
onAgentLog: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onAgentLog(event)
|
||||
},
|
||||
},
|
||||
)
|
||||
}, [
|
||||
threadMessages,
|
||||
updateCurrentQAOnTree,
|
||||
handleResponding,
|
||||
formSettings?.inputsForm,
|
||||
handleRun,
|
||||
notify,
|
||||
t,
|
||||
config?.suggested_questions_after_answer?.enabled,
|
||||
setTargetMessageId,
|
||||
setConversationId,
|
||||
setSuggestedQuestions,
|
||||
setActiveTaskId,
|
||||
setSuggestedQuestionsAbortController,
|
||||
startRun,
|
||||
fetchInspectVars,
|
||||
invalidAllLastRun,
|
||||
workflowStore,
|
||||
])
|
||||
|
||||
return { handleSend }
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import type { ChatTreeUpdater, UpdateCurrentQAParams } from './types'
|
||||
import type { ChatItemInTree } from '@/app/components/base/chat/types'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export function useChatTreeOperations(updateChatTree: ChatTreeUpdater) {
|
||||
const produceChatTreeNode = useCallback(
|
||||
(tree: ChatItemInTree[], targetId: string, operation: (node: ChatItemInTree) => void) => {
|
||||
return produce(tree, (draft) => {
|
||||
const queue: ChatItemInTree[] = [...draft]
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!
|
||||
if (current.id === targetId) {
|
||||
operation(current)
|
||||
break
|
||||
}
|
||||
if (current.children)
|
||||
queue.push(...current.children)
|
||||
}
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const updateCurrentQAOnTree = useCallback(({
|
||||
parentId,
|
||||
responseItem,
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
}: UpdateCurrentQAParams) => {
|
||||
const currentQA = { ...questionItem, children: [{ ...responseItem, children: [] }] } as ChatItemInTree
|
||||
updateChatTree((currentChatTree) => {
|
||||
if (!parentId) {
|
||||
const questionIndex = currentChatTree.findIndex(item => [placeholderQuestionId, questionItem.id].includes(item.id))
|
||||
return produce(currentChatTree, (draft) => {
|
||||
if (questionIndex === -1)
|
||||
draft.push(currentQA)
|
||||
else
|
||||
draft[questionIndex] = currentQA
|
||||
})
|
||||
}
|
||||
|
||||
return produceChatTreeNode(currentChatTree, parentId, (parentNode) => {
|
||||
if (!parentNode.children)
|
||||
parentNode.children = []
|
||||
const questionNodeIndex = parentNode.children.findIndex(item => [placeholderQuestionId, questionItem.id].includes(item.id))
|
||||
if (questionNodeIndex === -1)
|
||||
parentNode.children.push(currentQA)
|
||||
else
|
||||
parentNode.children[questionNodeIndex] = currentQA
|
||||
})
|
||||
})
|
||||
}, [produceChatTreeNode, updateChatTree])
|
||||
|
||||
return {
|
||||
produceChatTreeNode,
|
||||
updateCurrentQAOnTree,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
import type { ChatConfig } from './types'
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatItemInTree, Inputs } from '@/app/components/base/chat/types'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useStore } from '../../../store'
|
||||
import { useChatFlowControl } from './use-chat-flow-control'
|
||||
import { useChatList } from './use-chat-list'
|
||||
import { useChatMessageSender } from './use-chat-message-sender'
|
||||
import { useChatTreeOperations } from './use-chat-tree-operations'
|
||||
|
||||
export function useChat(
|
||||
config: ChatConfig | undefined,
|
||||
formSettings?: {
|
||||
inputs: Inputs
|
||||
inputsForm: InputForm[]
|
||||
},
|
||||
prevChatTree?: ChatItemInTree[],
|
||||
stopChat?: (taskId: string) => void,
|
||||
) {
|
||||
const chatTree = useStore(s => s.chatTree)
|
||||
const conversationId = useStore(s => s.conversationId)
|
||||
const isResponding = useStore(s => s.isResponding)
|
||||
const suggestedQuestions = useStore(s => s.suggestedQuestions)
|
||||
const targetMessageId = useStore(s => s.targetMessageId)
|
||||
const updateChatTree = useStore(s => s.updateChatTree)
|
||||
const setTargetMessageId = useStore(s => s.setTargetMessageId)
|
||||
|
||||
const initialChatTreeRef = useRef(prevChatTree)
|
||||
useEffect(() => {
|
||||
const initialChatTree = initialChatTreeRef.current
|
||||
if (!initialChatTree || initialChatTree.length === 0)
|
||||
return
|
||||
updateChatTree(currentChatTree => (currentChatTree.length === 0 ? initialChatTree : currentChatTree))
|
||||
}, [updateChatTree])
|
||||
|
||||
const { updateCurrentQAOnTree } = useChatTreeOperations(updateChatTree)
|
||||
|
||||
const {
|
||||
handleResponding,
|
||||
handleStop,
|
||||
handleRestart,
|
||||
} = useChatFlowControl({
|
||||
stopChat,
|
||||
})
|
||||
|
||||
const {
|
||||
threadMessages,
|
||||
chatList,
|
||||
} = useChatList({
|
||||
chatTree,
|
||||
targetMessageId,
|
||||
config,
|
||||
formSettings,
|
||||
})
|
||||
|
||||
const { handleSend } = useChatMessageSender({
|
||||
threadMessages,
|
||||
config,
|
||||
formSettings,
|
||||
handleResponding,
|
||||
updateCurrentQAOnTree,
|
||||
})
|
||||
|
||||
return {
|
||||
conversationId,
|
||||
chatList,
|
||||
setTargetMessageId,
|
||||
handleSend,
|
||||
handleStop,
|
||||
handleRestart,
|
||||
isResponding,
|
||||
suggestedQuestions,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
import type { UpdateCurrentQAParams } from './types'
|
||||
import type { ChatItem } from '@/app/components/base/chat/types'
|
||||
import type { AgentLogItem, NodeTracing } from '@/types/workflow'
|
||||
import { NodeRunningStatus, WorkflowRunningStatus } from '../../../types'
|
||||
|
||||
type WorkflowEventHandlersContext = {
|
||||
responseItem: ChatItem
|
||||
questionItem: ChatItem
|
||||
placeholderQuestionId: string
|
||||
parentMessageId?: string
|
||||
updateCurrentQAOnTree: (params: UpdateCurrentQAParams) => void
|
||||
}
|
||||
|
||||
type TracingData = Partial<NodeTracing> & { id: string }
|
||||
type AgentLogData = Partial<AgentLogItem> & { node_id: string, message_id: string }
|
||||
|
||||
export function createWorkflowEventHandlers(ctx: WorkflowEventHandlersContext) {
|
||||
const { responseItem, questionItem, placeholderQuestionId, parentMessageId, updateCurrentQAOnTree } = ctx
|
||||
|
||||
const updateTree = () => {
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: parentMessageId,
|
||||
})
|
||||
}
|
||||
|
||||
const updateTracingItem = (data: TracingData) => {
|
||||
const currentTracingIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.id === data.id)
|
||||
if (currentTracingIndex > -1) {
|
||||
responseItem.workflowProcess!.tracing[currentTracingIndex] = {
|
||||
...responseItem.workflowProcess!.tracing[currentTracingIndex],
|
||||
...data,
|
||||
}
|
||||
updateTree()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onWorkflowStarted: ({ workflow_run_id, task_id }: { workflow_run_id: string, task_id: string }) => {
|
||||
responseItem.workflow_run_id = workflow_run_id
|
||||
responseItem.workflowProcess = {
|
||||
status: WorkflowRunningStatus.Running,
|
||||
tracing: [],
|
||||
}
|
||||
updateTree()
|
||||
return task_id
|
||||
},
|
||||
|
||||
onWorkflowFinished: ({ data }: { data: { status: string } }) => {
|
||||
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onIterationStart: ({ data }: { data: Partial<NodeTracing> }) => {
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as NodeTracing)
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onIterationFinish: ({ data }: { data: TracingData }) => {
|
||||
updateTracingItem(data)
|
||||
},
|
||||
|
||||
onLoopStart: ({ data }: { data: Partial<NodeTracing> }) => {
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as NodeTracing)
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onLoopFinish: ({ data }: { data: TracingData }) => {
|
||||
updateTracingItem(data)
|
||||
},
|
||||
|
||||
onNodeStarted: ({ data }: { data: Partial<NodeTracing> }) => {
|
||||
responseItem.workflowProcess!.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as NodeTracing)
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onNodeRetry: ({ data }: { data: NodeTracing }) => {
|
||||
responseItem.workflowProcess!.tracing!.push(data)
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onNodeFinished: ({ data }: { data: TracingData }) => {
|
||||
updateTracingItem(data)
|
||||
},
|
||||
|
||||
onAgentLog: ({ data }: { data: AgentLogData }) => {
|
||||
const currentNodeIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
|
||||
if (currentNodeIndex > -1) {
|
||||
const current = responseItem.workflowProcess!.tracing![currentNodeIndex]
|
||||
|
||||
if (current.execution_metadata) {
|
||||
if (current.execution_metadata.agent_log) {
|
||||
const currentLogIndex = current.execution_metadata.agent_log.findIndex(log => log.message_id === data.message_id)
|
||||
if (currentLogIndex > -1) {
|
||||
current.execution_metadata.agent_log[currentLogIndex] = {
|
||||
...current.execution_metadata.agent_log[currentLogIndex],
|
||||
...data,
|
||||
} as AgentLogItem
|
||||
}
|
||||
else {
|
||||
current.execution_metadata.agent_log.push(data as AgentLogItem)
|
||||
}
|
||||
}
|
||||
else {
|
||||
current.execution_metadata.agent_log = [data as AgentLogItem]
|
||||
}
|
||||
}
|
||||
else {
|
||||
current.execution_metadata = {
|
||||
agent_log: [data as AgentLogItem],
|
||||
} as NodeTracing['execution_metadata']
|
||||
}
|
||||
|
||||
responseItem.workflowProcess!.tracing[currentNodeIndex] = {
|
||||
...current,
|
||||
}
|
||||
|
||||
updateTree()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type { ChatItemInTree } from '@/app/components/base/chat/types'
|
||||
|
||||
type ChatPreviewState = {
|
||||
chatTree: ChatItemInTree[]
|
||||
targetMessageId: string | undefined
|
||||
suggestedQuestions: string[]
|
||||
conversationId: string
|
||||
isResponding: boolean
|
||||
activeRunId: number
|
||||
activeTaskId: string
|
||||
hasStopResponded: boolean
|
||||
suggestedQuestionsAbortController: AbortController | null
|
||||
}
|
||||
|
||||
type ChatPreviewActions = {
|
||||
setChatTree: (chatTree: ChatItemInTree[]) => void
|
||||
updateChatTree: (updater: (chatTree: ChatItemInTree[]) => ChatItemInTree[]) => void
|
||||
setTargetMessageId: (messageId: string | undefined) => void
|
||||
setSuggestedQuestions: (questions: string[]) => void
|
||||
setConversationId: (conversationId: string) => void
|
||||
setIsResponding: (isResponding: boolean) => void
|
||||
setActiveTaskId: (taskId: string) => void
|
||||
setHasStopResponded: (hasStopResponded: boolean) => void
|
||||
setSuggestedQuestionsAbortController: (controller: AbortController | null) => void
|
||||
startRun: () => number
|
||||
invalidateRun: () => number
|
||||
resetChatPreview: () => void
|
||||
}
|
||||
|
||||
export type ChatPreviewSliceShape = ChatPreviewState & ChatPreviewActions
|
||||
|
||||
const initialState: ChatPreviewState = {
|
||||
chatTree: [],
|
||||
targetMessageId: undefined,
|
||||
suggestedQuestions: [],
|
||||
conversationId: '',
|
||||
isResponding: false,
|
||||
activeRunId: 0,
|
||||
activeTaskId: '',
|
||||
hasStopResponded: false,
|
||||
suggestedQuestionsAbortController: null,
|
||||
}
|
||||
|
||||
export const createChatPreviewSlice: StateCreator<ChatPreviewSliceShape> = (set, get) => ({
|
||||
...initialState,
|
||||
|
||||
setChatTree: chatTree => set({ chatTree }),
|
||||
|
||||
updateChatTree: updater => set((state) => {
|
||||
const nextChatTree = updater(state.chatTree)
|
||||
if (nextChatTree === state.chatTree)
|
||||
return state
|
||||
return { chatTree: nextChatTree }
|
||||
}),
|
||||
|
||||
setTargetMessageId: targetMessageId => set({ targetMessageId }),
|
||||
|
||||
setSuggestedQuestions: suggestedQuestions => set({ suggestedQuestions }),
|
||||
|
||||
setConversationId: conversationId => set({ conversationId }),
|
||||
|
||||
setIsResponding: isResponding => set({ isResponding }),
|
||||
|
||||
setActiveTaskId: activeTaskId => set({ activeTaskId }),
|
||||
|
||||
setHasStopResponded: hasStopResponded => set({ hasStopResponded }),
|
||||
|
||||
setSuggestedQuestionsAbortController: suggestedQuestionsAbortController => set({ suggestedQuestionsAbortController }),
|
||||
|
||||
startRun: () => {
|
||||
const activeRunId = get().activeRunId + 1
|
||||
set({
|
||||
activeRunId,
|
||||
activeTaskId: '',
|
||||
hasStopResponded: false,
|
||||
suggestedQuestionsAbortController: null,
|
||||
})
|
||||
return activeRunId
|
||||
},
|
||||
|
||||
invalidateRun: () => {
|
||||
const activeRunId = get().activeRunId + 1
|
||||
set({
|
||||
activeRunId,
|
||||
activeTaskId: '',
|
||||
suggestedQuestionsAbortController: null,
|
||||
})
|
||||
return activeRunId
|
||||
},
|
||||
|
||||
resetChatPreview: () => set(state => ({
|
||||
...initialState,
|
||||
activeRunId: state.activeRunId + 1,
|
||||
})),
|
||||
})
|
||||
@ -1,6 +1,7 @@
|
||||
import type {
|
||||
StateCreator,
|
||||
} from 'zustand'
|
||||
import type { ChatPreviewSliceShape } from './chat-preview-slice'
|
||||
import type { ChatVariableSliceShape } from './chat-variable-slice'
|
||||
import type { InspectVarsSliceShape } from './debug/inspect-vars-slice'
|
||||
import type { EnvVariableSliceShape } from './env-variable-slice'
|
||||
@ -22,6 +23,7 @@ import {
|
||||
} from 'zustand'
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import { WorkflowContext } from '@/app/components/workflow/context'
|
||||
import { createChatPreviewSlice } from './chat-preview-slice'
|
||||
import { createChatVariableSlice } from './chat-variable-slice'
|
||||
import { createInspectVarsSlice } from './debug/inspect-vars-slice'
|
||||
import { createEnvVariableSlice } from './env-variable-slice'
|
||||
@ -42,7 +44,8 @@ export type SliceFromInjection
|
||||
& Partial<RagPipelineSliceShape>
|
||||
|
||||
export type Shape
|
||||
= ChatVariableSliceShape
|
||||
= ChatPreviewSliceShape
|
||||
& ChatVariableSliceShape
|
||||
& EnvVariableSliceShape
|
||||
& FormSliceShape
|
||||
& HelpLineSliceShape
|
||||
@ -67,6 +70,7 @@ export const createWorkflowStore = (params: CreateWorkflowStoreParams) => {
|
||||
const { injectWorkflowStoreSliceFn } = params || {}
|
||||
|
||||
return createStore<Shape>((...args) => ({
|
||||
...createChatPreviewSlice(...args),
|
||||
...createChatVariableSlice(...args),
|
||||
...createEnvVariableSlice(...args),
|
||||
...createFormSlice(...args),
|
||||
|
||||
@ -1085,11 +1085,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/base/icons/icon-gallery.stories.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/base/icons/utils.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
@ -3774,11 +3769,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/workflow/panel/debug-and-preview/hooks.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"app/components/workflow/panel/env-panel/variable-modal.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 4
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
"analyze-component": "node ./scripts/analyze-component.js",
|
||||
"refactor-component": "node ./scripts/refactor-component.js",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"storybook:build": "storybook build",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"analyze": "ANALYZE=true pnpm build",
|
||||
"knip": "knip"
|
||||
|
||||
16
web/vite.config.ts
Normal file
16
web/vite.config.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths(), react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'~@': __dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -1,9 +1,7 @@
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { defineConfig, mergeConfig } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths(), react()],
|
||||
export default mergeConfig(viteConfig, defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
@ -13,4 +11,4 @@ export default defineConfig({
|
||||
reporter: ['text', 'json', 'json-summary'],
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user