mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-04-21 13:19:43 +08:00
feat: implement structured output generation with model configuration and error handling
This commit is contained in:
parent
a32bc341fb
commit
066b0deef4
@ -1,16 +1,23 @@
|
||||
import React, { type FC, useCallback, useState } from 'react'
|
||||
import { type SchemaRoot, Type } from '../../../types'
|
||||
import React, { type FC, useCallback, useEffect, useState } from 'react'
|
||||
import type { SchemaRoot } from '../../../types'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import type { CompletionParams, Model } from '@/types/app'
|
||||
import { ModelModeType } from '@/types/app'
|
||||
import { Theme } from '@/types/app'
|
||||
import { SchemaGeneratorDark, SchemaGeneratorLight } from './assets'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { ModelInfo } from './prompt-editor'
|
||||
import PromptEditor from './prompt-editor'
|
||||
import GeneratedResult from './generated-result'
|
||||
import { useGenerateStructuredOutputRules } from '@/service/use-common'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { type FormValue, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
|
||||
type JsonSchemaGeneratorProps = {
|
||||
onApply: (schema: SchemaRoot) => void
|
||||
@ -29,9 +36,28 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
||||
const [open, setOpen] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
const [view, setView] = useState(GeneratorView.promptEditor)
|
||||
const [model, setModel] = useState<Model>({
|
||||
name: '',
|
||||
provider: '',
|
||||
mode: ModelModeType.completion,
|
||||
completion_params: {} as CompletionParams,
|
||||
})
|
||||
const [instruction, setInstruction] = useState('')
|
||||
const [schema, setSchema] = useState<SchemaRoot | null>(null)
|
||||
const SchemaGenerator = theme === Theme.light ? SchemaGeneratorLight : SchemaGeneratorDark
|
||||
const {
|
||||
defaultModel,
|
||||
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultModel) {
|
||||
setModel(prev => ({
|
||||
...prev,
|
||||
name: defaultModel.model,
|
||||
provider: defaultModel.provider.provider,
|
||||
}))
|
||||
}
|
||||
}, [defaultModel])
|
||||
|
||||
const handleTrigger = useCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
e.stopPropagation()
|
||||
@ -42,34 +68,41 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
||||
setOpen(false)
|
||||
}, [])
|
||||
|
||||
const generateSchema = useCallback(async () => {
|
||||
// todo: fetch schema, delete mock data
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
setSchema({
|
||||
type: Type.object,
|
||||
properties: {
|
||||
string_field_1: {
|
||||
type: Type.string,
|
||||
description: '可为空可为空可为空可为空可为空可为空可为空可为空可为空可为空',
|
||||
},
|
||||
string_field_2: {
|
||||
type: Type.string,
|
||||
description: '可为空可为空可为空可为空可为空可为空可为空可为空可为空可为空',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'string_field_1',
|
||||
],
|
||||
additionalProperties: false,
|
||||
})
|
||||
resolve()
|
||||
}, 1000)
|
||||
})
|
||||
const handleModelChange = useCallback((model: ModelInfo) => {
|
||||
setModel(prev => ({
|
||||
...prev,
|
||||
provider: model.provider,
|
||||
name: model.modelId,
|
||||
mode: model.mode as ModelModeType,
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const handleCompletionParamsChange = useCallback((newParams: FormValue) => {
|
||||
setModel(prev => ({
|
||||
...prev,
|
||||
completion_params: newParams as CompletionParams,
|
||||
}),
|
||||
)
|
||||
}, [])
|
||||
|
||||
const { mutateAsync: generateStructuredOutputRules } = useGenerateStructuredOutputRules()
|
||||
|
||||
const generateSchema = useCallback(async () => {
|
||||
const { output, error } = await generateStructuredOutputRules({ instruction, model_config: model! })
|
||||
if (error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error,
|
||||
})
|
||||
return
|
||||
}
|
||||
return output
|
||||
}, [instruction, model, generateStructuredOutputRules])
|
||||
|
||||
const handleGenerate = useCallback(async () => {
|
||||
await generateSchema()
|
||||
const output = await generateSchema()
|
||||
if (output === undefined) return
|
||||
setSchema(JSON.parse(output))
|
||||
setView(GeneratorView.result)
|
||||
}, [generateSchema])
|
||||
|
||||
@ -78,7 +111,9 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
||||
}
|
||||
|
||||
const handleRegenerate = useCallback(async () => {
|
||||
await generateSchema()
|
||||
const output = await generateSchema()
|
||||
if (output === undefined) return
|
||||
setSchema(JSON.parse(output))
|
||||
}, [generateSchema])
|
||||
|
||||
const handleApply = () => {
|
||||
@ -111,9 +146,12 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
|
||||
{view === GeneratorView.promptEditor && (
|
||||
<PromptEditor
|
||||
instruction={instruction}
|
||||
model={model}
|
||||
onInstructionChange={setInstruction}
|
||||
onCompletionParamsChange={handleCompletionParamsChange}
|
||||
onGenerate={handleGenerate}
|
||||
onClose={onClose}
|
||||
onModelChange={handleModelChange}
|
||||
/>
|
||||
)}
|
||||
{view === GeneratorView.result && (
|
||||
|
@ -1,34 +1,45 @@
|
||||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { RiCloseLine, RiSparklingFill } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import type { Model } from '@/types/app'
|
||||
|
||||
export type ModelInfo = {
|
||||
modelId: string
|
||||
provider: string
|
||||
mode?: string
|
||||
features?: string[]
|
||||
}
|
||||
|
||||
type PromptEditorProps = {
|
||||
instruction: string
|
||||
model: Model
|
||||
onInstructionChange: (instruction: string) => void
|
||||
onCompletionParamsChange: (newParams: FormValue) => void
|
||||
onModelChange: (model: ModelInfo) => void
|
||||
onClose: () => void
|
||||
onGenerate: () => void
|
||||
}
|
||||
|
||||
const PromptEditor: FC<PromptEditorProps> = ({
|
||||
instruction,
|
||||
model,
|
||||
onInstructionChange,
|
||||
onCompletionParamsChange,
|
||||
onClose,
|
||||
onGenerate,
|
||||
onModelChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
activeTextGenerationModelList,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList()
|
||||
|
||||
const handleChangeModel = () => {
|
||||
}
|
||||
const handleInstructionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onInstructionChange(e.target.value)
|
||||
}, [onInstructionChange])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col relative w-[480px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-2xl shadow-shadow-shadow-9'>
|
||||
@ -49,9 +60,17 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
<div className='flex items-center h-6 text-text-secondary system-sm-semibold-uppercase'>
|
||||
{t('common.modelProvider.model')}
|
||||
</div>
|
||||
<ModelSelector
|
||||
modelList={activeTextGenerationModelList}
|
||||
onSelect={handleChangeModel}
|
||||
<ModelParameterModal
|
||||
popupClassName='!w-[448px]'
|
||||
portalToFollowElemContentClassName='z-[1000]'
|
||||
isAdvancedMode={true}
|
||||
provider={model.provider}
|
||||
mode={model.mode}
|
||||
completionParams={model.completion_params}
|
||||
modelId={model.name}
|
||||
setModel={onModelChange}
|
||||
onCompletionParamsChange={onCompletionParamsChange}
|
||||
hideDebugWithMultipleModel
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-y-1 px-4 py-2'>
|
||||
@ -64,7 +83,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
className='h-[364px] px-2 py-1 resize-none'
|
||||
value={instruction}
|
||||
placeholder={t('workflow.nodes.llm.jsonSchema.promptPlaceholder')}
|
||||
onChange={e => onInstructionChange(e.target.value)}
|
||||
onChange={handleInstructionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,23 +1,24 @@
|
||||
import type { I18nText } from '@/i18n/language'
|
||||
import type { Model } from '@/types/app'
|
||||
|
||||
export interface CommonResponse {
|
||||
export type CommonResponse = {
|
||||
result: 'success' | 'fail'
|
||||
}
|
||||
|
||||
export interface OauthResponse {
|
||||
export type OauthResponse = {
|
||||
redirect_url: string
|
||||
}
|
||||
|
||||
export interface SetupStatusResponse {
|
||||
export type SetupStatusResponse = {
|
||||
step: 'finished' | 'not_started'
|
||||
setup_at?: Date
|
||||
}
|
||||
|
||||
export interface InitValidateStatusResponse {
|
||||
export type InitValidateStatusResponse = {
|
||||
status: 'finished' | 'not_started'
|
||||
}
|
||||
|
||||
export interface UserProfileResponse {
|
||||
export type UserProfileResponse = {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
@ -33,13 +34,13 @@ export interface UserProfileResponse {
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
export interface UserProfileOriginResponse {
|
||||
export type UserProfileOriginResponse = {
|
||||
json: () => Promise<UserProfileResponse>
|
||||
bodyUsed: boolean
|
||||
headers: any
|
||||
}
|
||||
|
||||
export interface LangGeniusVersionResponse {
|
||||
export type LangGeniusVersionResponse = {
|
||||
current_version: string
|
||||
latest_version: string
|
||||
version: string
|
||||
@ -49,7 +50,7 @@ export interface LangGeniusVersionResponse {
|
||||
current_env: string
|
||||
}
|
||||
|
||||
export interface TenantInfoResponse {
|
||||
export type TenantInfoResponse = {
|
||||
name: string
|
||||
created_at: string
|
||||
providers: Array<{
|
||||
@ -80,14 +81,14 @@ export enum ProviderName {
|
||||
Tongyi = 'tongyi',
|
||||
ChatGLM = 'chatglm',
|
||||
}
|
||||
export interface ProviderAzureToken {
|
||||
export type ProviderAzureToken = {
|
||||
openai_api_base?: string
|
||||
openai_api_key?: string
|
||||
}
|
||||
export interface ProviderAnthropicToken {
|
||||
export type ProviderAnthropicToken = {
|
||||
anthropic_api_key?: string
|
||||
}
|
||||
export interface ProviderTokenType {
|
||||
export type ProviderTokenType = {
|
||||
[ProviderName.OPENAI]: string
|
||||
[ProviderName.AZURE_OPENAI]: ProviderAzureToken
|
||||
[ProviderName.ANTHROPIC]: ProviderAnthropicToken
|
||||
@ -110,14 +111,14 @@ export type ProviderHosted = Provider & {
|
||||
quota_used: number
|
||||
}
|
||||
|
||||
export interface AccountIntegrate {
|
||||
export type AccountIntegrate = {
|
||||
provider: 'google' | 'github'
|
||||
created_at: number
|
||||
is_bound: boolean
|
||||
link: string
|
||||
}
|
||||
|
||||
export interface IWorkspace {
|
||||
export type IWorkspace = {
|
||||
id: string
|
||||
name: string
|
||||
plan: string
|
||||
@ -137,7 +138,7 @@ export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & {
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataSourceNotionPage {
|
||||
export type DataSourceNotionPage = {
|
||||
page_icon: null | {
|
||||
type: string | null
|
||||
url: string | null
|
||||
@ -156,7 +157,7 @@ export type NotionPage = DataSourceNotionPage & {
|
||||
|
||||
export type DataSourceNotionPageMap = Record<string, DataSourceNotionPage & { workspace_id: string }>
|
||||
|
||||
export interface DataSourceNotionWorkspace {
|
||||
export type DataSourceNotionWorkspace = {
|
||||
workspace_name: string
|
||||
workspace_id: string
|
||||
workspace_icon: string | null
|
||||
@ -166,7 +167,7 @@ export interface DataSourceNotionWorkspace {
|
||||
|
||||
export type DataSourceNotionWorkspaceMap = Record<string, DataSourceNotionWorkspace>
|
||||
|
||||
export interface DataSourceNotion {
|
||||
export type DataSourceNotion = {
|
||||
id: string
|
||||
provider: string
|
||||
is_bound: boolean
|
||||
@ -181,12 +182,12 @@ export enum DataSourceProvider {
|
||||
jinaReader = 'jinareader',
|
||||
}
|
||||
|
||||
export interface FirecrawlConfig {
|
||||
export type FirecrawlConfig = {
|
||||
api_key: string
|
||||
base_url: string
|
||||
}
|
||||
|
||||
export interface DataSourceItem {
|
||||
export type DataSourceItem = {
|
||||
id: string
|
||||
category: DataSourceCategory
|
||||
provider: DataSourceProvider
|
||||
@ -195,15 +196,15 @@ export interface DataSourceItem {
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export interface DataSources {
|
||||
export type DataSources = {
|
||||
sources: DataSourceItem[]
|
||||
}
|
||||
|
||||
export interface GithubRepo {
|
||||
export type GithubRepo = {
|
||||
stargazers_count: number
|
||||
}
|
||||
|
||||
export interface PluginProvider {
|
||||
export type PluginProvider = {
|
||||
tool_name: string
|
||||
is_enabled: boolean
|
||||
credentials: {
|
||||
@ -211,7 +212,7 @@ export interface PluginProvider {
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface FileUploadConfigResponse {
|
||||
export type FileUploadConfigResponse = {
|
||||
batch_count_limit: number
|
||||
image_file_size_limit?: number | string // default is 10MB
|
||||
file_size_limit: number // default is 15MB
|
||||
@ -234,14 +235,14 @@ export type InvitationResponse = CommonResponse & {
|
||||
invitation_results: InvitationResult[]
|
||||
}
|
||||
|
||||
export interface ApiBasedExtension {
|
||||
export type ApiBasedExtension = {
|
||||
id?: string
|
||||
name?: string
|
||||
api_endpoint?: string
|
||||
api_key?: string
|
||||
}
|
||||
|
||||
export interface CodeBasedExtensionForm {
|
||||
export type CodeBasedExtensionForm = {
|
||||
type: string
|
||||
label: I18nText
|
||||
variable: string
|
||||
@ -252,17 +253,17 @@ export interface CodeBasedExtensionForm {
|
||||
max_length?: number
|
||||
}
|
||||
|
||||
export interface CodeBasedExtensionItem {
|
||||
export type CodeBasedExtensionItem = {
|
||||
name: string
|
||||
label: any
|
||||
form_schema: CodeBasedExtensionForm[]
|
||||
}
|
||||
export interface CodeBasedExtension {
|
||||
export type CodeBasedExtension = {
|
||||
module: string
|
||||
data: CodeBasedExtensionItem[]
|
||||
}
|
||||
|
||||
export interface ExternalDataTool {
|
||||
export type ExternalDataTool = {
|
||||
type?: string
|
||||
label?: string
|
||||
icon?: string
|
||||
@ -274,7 +275,7 @@ export interface ExternalDataTool {
|
||||
} & Partial<Record<string, any>>
|
||||
}
|
||||
|
||||
export interface ModerateResponse {
|
||||
export type ModerateResponse = {
|
||||
flagged: boolean
|
||||
text: string
|
||||
}
|
||||
@ -286,3 +287,13 @@ export type ModerationService = (
|
||||
text: string
|
||||
}
|
||||
) => Promise<ModerateResponse>
|
||||
|
||||
export type StructuredOutputRulesRequestBody = {
|
||||
instruction: string
|
||||
model_config: Model
|
||||
}
|
||||
|
||||
export type StructuredOutputRulesResponse = {
|
||||
output: string
|
||||
error?: string
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { get } from './base'
|
||||
import { get, post } from './base'
|
||||
import type {
|
||||
FileUploadConfigResponse,
|
||||
StructuredOutputRulesRequestBody,
|
||||
StructuredOutputRulesResponse,
|
||||
} from '@/models/common'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
|
||||
const NAME_SPACE = 'common'
|
||||
|
||||
@ -12,3 +14,15 @@ export const useFileUploadConfig = () => {
|
||||
queryFn: () => get<FileUploadConfigResponse>('/files/upload'),
|
||||
})
|
||||
}
|
||||
|
||||
export const useGenerateStructuredOutputRules = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'generate-structured-output-rules'],
|
||||
mutationFn: (body: StructuredOutputRulesRequestBody) => {
|
||||
return post<StructuredOutputRulesResponse>(
|
||||
'/rule-structured-output-generate',
|
||||
{ body },
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user