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 { 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 && (

View File

@ -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>

View File

@ -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
}

View File

@ -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 },
)
},
})
}