feat: implement structured output generation with model configuration and error handling

This commit is contained in:
twwu 2025-03-20 14:05:46 +08:00
parent a32bc341fb
commit 066b0deef4
4 changed files with 153 additions and 71 deletions

View File

@ -1,16 +1,23 @@
import React, { type FC, useCallback, useState } from 'react' import React, { type FC, useCallback, useEffect, useState } from 'react'
import { type SchemaRoot, Type } from '../../../types' import type { SchemaRoot } from '../../../types'
import { import {
PortalToFollowElem, PortalToFollowElem,
PortalToFollowElemContent, PortalToFollowElemContent,
PortalToFollowElemTrigger, PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem' } from '@/app/components/base/portal-to-follow-elem'
import useTheme from '@/hooks/use-theme' import useTheme from '@/hooks/use-theme'
import type { CompletionParams, Model } from '@/types/app'
import { ModelModeType } from '@/types/app'
import { Theme } from '@/types/app' import { Theme } from '@/types/app'
import { SchemaGeneratorDark, SchemaGeneratorLight } from './assets' import { SchemaGeneratorDark, SchemaGeneratorLight } from './assets'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { ModelInfo } from './prompt-editor'
import PromptEditor from './prompt-editor' import PromptEditor from './prompt-editor'
import GeneratedResult from './generated-result' 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 = { type JsonSchemaGeneratorProps = {
onApply: (schema: SchemaRoot) => void onApply: (schema: SchemaRoot) => void
@ -29,9 +36,28 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const { theme } = useTheme() const { theme } = useTheme()
const [view, setView] = useState(GeneratorView.promptEditor) 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 [instruction, setInstruction] = useState('')
const [schema, setSchema] = useState<SchemaRoot | null>(null) const [schema, setSchema] = useState<SchemaRoot | null>(null)
const SchemaGenerator = theme === Theme.light ? SchemaGeneratorLight : SchemaGeneratorDark 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>) => { const handleTrigger = useCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.stopPropagation() e.stopPropagation()
@ -42,34 +68,41 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
setOpen(false) setOpen(false)
}, []) }, [])
const generateSchema = useCallback(async () => { const handleModelChange = useCallback((model: ModelInfo) => {
// todo: fetch schema, delete mock data setModel(prev => ({
await new Promise<void>((resolve) => { ...prev,
setTimeout(() => { provider: model.provider,
setSchema({ name: model.modelId,
type: Type.object, mode: model.mode as ModelModeType,
properties: { }))
string_field_1: {
type: Type.string,
description: '可为空可为空可为空可为空可为空可为空可为空可为空可为空可为空',
},
string_field_2: {
type: Type.string,
description: '可为空可为空可为空可为空可为空可为空可为空可为空可为空可为空',
},
},
required: [
'string_field_1',
],
additionalProperties: false,
})
resolve()
}, 1000)
})
}, []) }, [])
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 () => { const handleGenerate = useCallback(async () => {
await generateSchema() const output = await generateSchema()
if (output === undefined) return
setSchema(JSON.parse(output))
setView(GeneratorView.result) setView(GeneratorView.result)
}, [generateSchema]) }, [generateSchema])
@ -78,7 +111,9 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
} }
const handleRegenerate = useCallback(async () => { const handleRegenerate = useCallback(async () => {
await generateSchema() const output = await generateSchema()
if (output === undefined) return
setSchema(JSON.parse(output))
}, [generateSchema]) }, [generateSchema])
const handleApply = () => { const handleApply = () => {
@ -111,9 +146,12 @@ export const JsonSchemaGenerator: FC<JsonSchemaGeneratorProps> = ({
{view === GeneratorView.promptEditor && ( {view === GeneratorView.promptEditor && (
<PromptEditor <PromptEditor
instruction={instruction} instruction={instruction}
model={model}
onInstructionChange={setInstruction} onInstructionChange={setInstruction}
onCompletionParamsChange={handleCompletionParamsChange}
onGenerate={handleGenerate} onGenerate={handleGenerate}
onClose={onClose} onClose={onClose}
onModelChange={handleModelChange}
/> />
)} )}
{view === GeneratorView.result && ( {view === GeneratorView.result && (

View File

@ -1,34 +1,45 @@
import React from 'react' import React, { useCallback } from 'react'
import type { FC } from 'react' import type { FC } from 'react'
import { RiCloseLine, RiSparklingFill } from '@remixicon/react' import { RiCloseLine, RiSparklingFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next' 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 Textarea from '@/app/components/base/textarea'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Button from '@/app/components/base/button' 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 = { type PromptEditorProps = {
instruction: string instruction: string
model: Model
onInstructionChange: (instruction: string) => void onInstructionChange: (instruction: string) => void
onCompletionParamsChange: (newParams: FormValue) => void
onModelChange: (model: ModelInfo) => void
onClose: () => void onClose: () => void
onGenerate: () => void onGenerate: () => void
} }
const PromptEditor: FC<PromptEditorProps> = ({ const PromptEditor: FC<PromptEditorProps> = ({
instruction, instruction,
model,
onInstructionChange, onInstructionChange,
onCompletionParamsChange,
onClose, onClose,
onGenerate, onGenerate,
onModelChange,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { const handleInstructionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
activeTextGenerationModelList, onInstructionChange(e.target.value)
} = useTextGenerationCurrentProviderAndModelAndModelList() }, [onInstructionChange])
const handleChangeModel = () => {
}
return ( 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'> <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'> <div className='flex items-center h-6 text-text-secondary system-sm-semibold-uppercase'>
{t('common.modelProvider.model')} {t('common.modelProvider.model')}
</div> </div>
<ModelSelector <ModelParameterModal
modelList={activeTextGenerationModelList} popupClassName='!w-[448px]'
onSelect={handleChangeModel} 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>
<div className='flex flex-col gap-y-1 px-4 py-2'> <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' className='h-[364px] px-2 py-1 resize-none'
value={instruction} value={instruction}
placeholder={t('workflow.nodes.llm.jsonSchema.promptPlaceholder')} placeholder={t('workflow.nodes.llm.jsonSchema.promptPlaceholder')}
onChange={e => onInstructionChange(e.target.value)} onChange={handleInstructionChange}
/> />
</div> </div>
</div> </div>

View File

@ -1,23 +1,24 @@
import type { I18nText } from '@/i18n/language' import type { I18nText } from '@/i18n/language'
import type { Model } from '@/types/app'
export interface CommonResponse { export type CommonResponse = {
result: 'success' | 'fail' result: 'success' | 'fail'
} }
export interface OauthResponse { export type OauthResponse = {
redirect_url: string redirect_url: string
} }
export interface SetupStatusResponse { export type SetupStatusResponse = {
step: 'finished' | 'not_started' step: 'finished' | 'not_started'
setup_at?: Date setup_at?: Date
} }
export interface InitValidateStatusResponse { export type InitValidateStatusResponse = {
status: 'finished' | 'not_started' status: 'finished' | 'not_started'
} }
export interface UserProfileResponse { export type UserProfileResponse = {
id: string id: string
name: string name: string
email: string email: string
@ -33,13 +34,13 @@ export interface UserProfileResponse {
created_at?: string created_at?: string
} }
export interface UserProfileOriginResponse { export type UserProfileOriginResponse = {
json: () => Promise<UserProfileResponse> json: () => Promise<UserProfileResponse>
bodyUsed: boolean bodyUsed: boolean
headers: any headers: any
} }
export interface LangGeniusVersionResponse { export type LangGeniusVersionResponse = {
current_version: string current_version: string
latest_version: string latest_version: string
version: string version: string
@ -49,7 +50,7 @@ export interface LangGeniusVersionResponse {
current_env: string current_env: string
} }
export interface TenantInfoResponse { export type TenantInfoResponse = {
name: string name: string
created_at: string created_at: string
providers: Array<{ providers: Array<{
@ -80,14 +81,14 @@ export enum ProviderName {
Tongyi = 'tongyi', Tongyi = 'tongyi',
ChatGLM = 'chatglm', ChatGLM = 'chatglm',
} }
export interface ProviderAzureToken { export type ProviderAzureToken = {
openai_api_base?: string openai_api_base?: string
openai_api_key?: string openai_api_key?: string
} }
export interface ProviderAnthropicToken { export type ProviderAnthropicToken = {
anthropic_api_key?: string anthropic_api_key?: string
} }
export interface ProviderTokenType { export type ProviderTokenType = {
[ProviderName.OPENAI]: string [ProviderName.OPENAI]: string
[ProviderName.AZURE_OPENAI]: ProviderAzureToken [ProviderName.AZURE_OPENAI]: ProviderAzureToken
[ProviderName.ANTHROPIC]: ProviderAnthropicToken [ProviderName.ANTHROPIC]: ProviderAnthropicToken
@ -110,14 +111,14 @@ export type ProviderHosted = Provider & {
quota_used: number quota_used: number
} }
export interface AccountIntegrate { export type AccountIntegrate = {
provider: 'google' | 'github' provider: 'google' | 'github'
created_at: number created_at: number
is_bound: boolean is_bound: boolean
link: string link: string
} }
export interface IWorkspace { export type IWorkspace = {
id: string id: string
name: string name: string
plan: string plan: string
@ -137,7 +138,7 @@ export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & {
} }
} }
export interface DataSourceNotionPage { export type DataSourceNotionPage = {
page_icon: null | { page_icon: null | {
type: string | null type: string | null
url: string | null url: string | null
@ -156,7 +157,7 @@ export type NotionPage = DataSourceNotionPage & {
export type DataSourceNotionPageMap = Record<string, DataSourceNotionPage & { workspace_id: string }> export type DataSourceNotionPageMap = Record<string, DataSourceNotionPage & { workspace_id: string }>
export interface DataSourceNotionWorkspace { export type DataSourceNotionWorkspace = {
workspace_name: string workspace_name: string
workspace_id: string workspace_id: string
workspace_icon: string | null workspace_icon: string | null
@ -166,7 +167,7 @@ export interface DataSourceNotionWorkspace {
export type DataSourceNotionWorkspaceMap = Record<string, DataSourceNotionWorkspace> export type DataSourceNotionWorkspaceMap = Record<string, DataSourceNotionWorkspace>
export interface DataSourceNotion { export type DataSourceNotion = {
id: string id: string
provider: string provider: string
is_bound: boolean is_bound: boolean
@ -181,12 +182,12 @@ export enum DataSourceProvider {
jinaReader = 'jinareader', jinaReader = 'jinareader',
} }
export interface FirecrawlConfig { export type FirecrawlConfig = {
api_key: string api_key: string
base_url: string base_url: string
} }
export interface DataSourceItem { export type DataSourceItem = {
id: string id: string
category: DataSourceCategory category: DataSourceCategory
provider: DataSourceProvider provider: DataSourceProvider
@ -195,15 +196,15 @@ export interface DataSourceItem {
updated_at: number updated_at: number
} }
export interface DataSources { export type DataSources = {
sources: DataSourceItem[] sources: DataSourceItem[]
} }
export interface GithubRepo { export type GithubRepo = {
stargazers_count: number stargazers_count: number
} }
export interface PluginProvider { export type PluginProvider = {
tool_name: string tool_name: string
is_enabled: boolean is_enabled: boolean
credentials: { credentials: {
@ -211,7 +212,7 @@ export interface PluginProvider {
} | null } | null
} }
export interface FileUploadConfigResponse { export type FileUploadConfigResponse = {
batch_count_limit: number batch_count_limit: number
image_file_size_limit?: number | string // default is 10MB image_file_size_limit?: number | string // default is 10MB
file_size_limit: number // default is 15MB file_size_limit: number // default is 15MB
@ -234,14 +235,14 @@ export type InvitationResponse = CommonResponse & {
invitation_results: InvitationResult[] invitation_results: InvitationResult[]
} }
export interface ApiBasedExtension { export type ApiBasedExtension = {
id?: string id?: string
name?: string name?: string
api_endpoint?: string api_endpoint?: string
api_key?: string api_key?: string
} }
export interface CodeBasedExtensionForm { export type CodeBasedExtensionForm = {
type: string type: string
label: I18nText label: I18nText
variable: string variable: string
@ -252,17 +253,17 @@ export interface CodeBasedExtensionForm {
max_length?: number max_length?: number
} }
export interface CodeBasedExtensionItem { export type CodeBasedExtensionItem = {
name: string name: string
label: any label: any
form_schema: CodeBasedExtensionForm[] form_schema: CodeBasedExtensionForm[]
} }
export interface CodeBasedExtension { export type CodeBasedExtension = {
module: string module: string
data: CodeBasedExtensionItem[] data: CodeBasedExtensionItem[]
} }
export interface ExternalDataTool { export type ExternalDataTool = {
type?: string type?: string
label?: string label?: string
icon?: string icon?: string
@ -274,7 +275,7 @@ export interface ExternalDataTool {
} & Partial<Record<string, any>> } & Partial<Record<string, any>>
} }
export interface ModerateResponse { export type ModerateResponse = {
flagged: boolean flagged: boolean
text: string text: string
} }
@ -286,3 +287,13 @@ export type ModerationService = (
text: string text: string
} }
) => Promise<ModerateResponse> ) => Promise<ModerateResponse>
export type StructuredOutputRulesRequestBody = {
instruction: string
model_config: Model
}
export type StructuredOutputRulesResponse = {
output: string
error?: string
}

View File

@ -1,8 +1,10 @@
import { get } from './base' import { get, post } from './base'
import type { import type {
FileUploadConfigResponse, FileUploadConfigResponse,
StructuredOutputRulesRequestBody,
StructuredOutputRulesResponse,
} from '@/models/common' } from '@/models/common'
import { useQuery } from '@tanstack/react-query' import { useMutation, useQuery } from '@tanstack/react-query'
const NAME_SPACE = 'common' const NAME_SPACE = 'common'
@ -12,3 +14,15 @@ export const useFileUploadConfig = () => {
queryFn: () => get<FileUploadConfigResponse>('/files/upload'), 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 },
)
},
})
}