mirror of https://github.com/langgenius/dify.git
feat: handle file schem show
This commit is contained in:
parent
ff33d42c55
commit
4cb286c765
|
|
@ -0,0 +1,162 @@
|
|||
import matchTheSchemaType from './match-schema-type'
|
||||
|
||||
describe('match the schema type', () => {
|
||||
it('should return true for identical primitive types', () => {
|
||||
expect(matchTheSchemaType({ type: 'string' }, { type: 'string' })).toBe(true)
|
||||
expect(matchTheSchemaType({ type: 'number' }, { type: 'number' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for different primitive types', () => {
|
||||
expect(matchTheSchemaType({ type: 'string' }, { type: 'number' })).toBe(false)
|
||||
})
|
||||
|
||||
it('should ignore values and only compare types', () => {
|
||||
expect(matchTheSchemaType({ type: 'string', value: 'hello' }, { type: 'string', value: 'world' })).toBe(true)
|
||||
expect(matchTheSchemaType({ type: 'number', value: 42 }, { type: 'number', value: 100 })).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for structural differences but no types', () => {
|
||||
expect(matchTheSchemaType({ type: 'string', other: { b: 'xxx' } }, { type: 'string', other: 'xxx' })).toBe(true)
|
||||
expect(matchTheSchemaType({ type: 'string', other: { b: 'xxx' } }, { type: 'string' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle nested objects with same structure and types', () => {
|
||||
const obj1 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
address: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
street: { type: 'string' },
|
||||
city: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const obj2 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', value: 'Alice' },
|
||||
age: { type: 'number', value: 30 },
|
||||
address: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
street: { type: 'string', value: '123 Main St' },
|
||||
city: { type: 'string', value: 'Wonderland' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
expect(matchTheSchemaType(obj1, obj2)).toBe(true)
|
||||
})
|
||||
it('should return false for nested objects with different structures', () => {
|
||||
const obj1 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
},
|
||||
}
|
||||
const obj2 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
address: { type: 'string' },
|
||||
},
|
||||
}
|
||||
expect(matchTheSchemaType(obj1, obj2)).toBe(false)
|
||||
})
|
||||
|
||||
it('file struct should match file type', () => {
|
||||
const fileSchema = {
|
||||
$id: 'https://dify.ai/schemas/v1/file.json',
|
||||
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||
version: '1.0.0',
|
||||
type: 'object',
|
||||
title: 'File Schema',
|
||||
description: 'Schema for file objects (v1)',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'file name',
|
||||
},
|
||||
size: {
|
||||
type: 'number',
|
||||
description: 'file size',
|
||||
},
|
||||
extension: {
|
||||
type: 'string',
|
||||
description: 'file extension',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'file type',
|
||||
},
|
||||
mime_type: {
|
||||
type: 'string',
|
||||
description: 'file mime type',
|
||||
},
|
||||
transfer_method: {
|
||||
type: 'string',
|
||||
description: 'file transfer method',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'file url',
|
||||
},
|
||||
related_id: {
|
||||
type: 'string',
|
||||
description: 'file related id',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'name',
|
||||
],
|
||||
}
|
||||
const file = {
|
||||
type: 'object',
|
||||
title: 'File',
|
||||
description: 'Schema for file objects (v1)',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'file name',
|
||||
},
|
||||
size: {
|
||||
type: 'number',
|
||||
description: 'file size',
|
||||
},
|
||||
extension: {
|
||||
type: 'string',
|
||||
description: 'file extension',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'file type',
|
||||
},
|
||||
mime_type: {
|
||||
type: 'string',
|
||||
description: 'file mime type',
|
||||
},
|
||||
transfer_method: {
|
||||
type: 'string',
|
||||
description: 'file transfer method',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'file url',
|
||||
},
|
||||
related_id: {
|
||||
type: 'string',
|
||||
description: 'file related id',
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'name',
|
||||
],
|
||||
}
|
||||
expect(matchTheSchemaType(fileSchema, file)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
export type AnyObj = Record<string, any> | null
|
||||
|
||||
const isObj = (x: any): x is object => x !== null && typeof x === 'object'
|
||||
|
||||
// only compare type in object
|
||||
function matchTheSchemaType(scheme: AnyObj, target: AnyObj): boolean {
|
||||
const isMatch = (schema: AnyObj, t: AnyObj): boolean => {
|
||||
const oSchema = isObj(schema)
|
||||
const oT = isObj(t)
|
||||
if(!oSchema)
|
||||
return true
|
||||
if (!oT) { // ignore the object without type
|
||||
// deep find oSchema has type
|
||||
for (const key in schema) {
|
||||
if (key === 'type')
|
||||
return false
|
||||
if (isObj((schema as any)[key]) && !isMatch((schema as any)[key], null))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
// check current `type`
|
||||
const tx = (schema as any).type
|
||||
const ty = (t as any).type
|
||||
const isTypeValueObj = isObj(tx)
|
||||
|
||||
if(!isTypeValueObj) // caution: type can be object, so that it would not be compare by value
|
||||
if (tx !== ty) return false
|
||||
|
||||
// recurse into all keys
|
||||
const keys = new Set([...Object.keys(schema as object), ...Object.keys(t as object)])
|
||||
for (const k of keys) {
|
||||
if (k === 'type' && !isTypeValueObj) continue // already checked
|
||||
if (!isMatch((schema as any)[k], (t as any)[k])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return isMatch(scheme, target)
|
||||
}
|
||||
|
||||
export default matchTheSchemaType
|
||||
|
|
@ -44,7 +44,7 @@ const Field: FC<Props> = ({
|
|||
/>
|
||||
)}
|
||||
<div className={cn('system-sm-medium ml-[7px] h-6 truncate leading-6 text-text-secondary', isRoot && rootClassName)}>{name}</div>
|
||||
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>{getFieldType(payload)}{payload.schemaType && ` (${payload.schemaType})`}</div>
|
||||
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>{getFieldType(payload)}{(payload.schemaType && payload.schemaType !== 'file' && ` (${payload.schemaType})`)}</div>
|
||||
{required && <div className='system-2xs-medium-uppercase ml-3 leading-6 text-text-warning'>{t('app.structOutput.required')}</div>}
|
||||
</div>
|
||||
{payload.description && (
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
import { deepEqualByType } from './use-match-schema-type'
|
||||
|
||||
describe('deepEqualByType', () => {
|
||||
it('should return true for identical primitive types', () => {
|
||||
expect(deepEqualByType({ type: 'string' }, { type: 'string' })).toBe(true)
|
||||
expect(deepEqualByType({ type: 'number' }, { type: 'number' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for different primitive types', () => {
|
||||
expect(deepEqualByType({ type: 'string' }, { type: 'number' })).toBe(false)
|
||||
})
|
||||
|
||||
it('should ignore values and only compare types', () => {
|
||||
expect(deepEqualByType({ type: 'string', value: 'hello' }, { type: 'string', value: 'world' })).toBe(true)
|
||||
expect(deepEqualByType({ type: 'number', value: 42 }, { type: 'number', value: 100 })).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for structural differences but no types', () => {
|
||||
expect(deepEqualByType({ type: 'string', other: { b: 'xxx' } }, { type: 'string', other: 'xxx' })).toBe(true)
|
||||
expect(deepEqualByType({ type: 'string', other: { b: 'xxx' } }, { type: 'string' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle nested objects with same structure and types', () => {
|
||||
const obj1 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
address: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
street: { type: 'string' },
|
||||
city: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const obj2 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', value: 'Alice' },
|
||||
age: { type: 'number', value: 30 },
|
||||
address: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
street: { type: 'string', value: '123 Main St' },
|
||||
city: { type: 'string', value: 'Wonderland' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
expect(deepEqualByType(obj1, obj2)).toBe(true)
|
||||
})
|
||||
it('should return false for nested objects with different structures', () => {
|
||||
const obj1 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
},
|
||||
}
|
||||
const obj2 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
address: { type: 'string' },
|
||||
},
|
||||
}
|
||||
expect(deepEqualByType(obj1, obj2)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,42 +1,12 @@
|
|||
import { useSchemaTypeDefinitions } from '@/service/use-common'
|
||||
|
||||
type AnyObj = Record<string, any> | null
|
||||
|
||||
// only compare type in object
|
||||
export function deepEqualByType(a: AnyObj, b: AnyObj): boolean {
|
||||
const isObj = (x: any): x is object => x !== null && typeof x === 'object'
|
||||
|
||||
const cmp = (x: AnyObj, y: AnyObj): boolean => {
|
||||
const ox = isObj(x)
|
||||
const oy = isObj(y)
|
||||
if (!ox && !oy) return true // both primitives → ignore values
|
||||
if (ox !== oy) { // ignore the object without type
|
||||
if(ox && !('type' in (x as object)))
|
||||
return true
|
||||
return !!(oy && !('type' in (y as object)))
|
||||
}
|
||||
// check current `type`
|
||||
const tx = (x as any).type
|
||||
const ty = (y as any).type
|
||||
if (tx !== ty) return false
|
||||
|
||||
// recurse into all keys
|
||||
const keys = new Set([...Object.keys(x as object), ...Object.keys(y as object)])
|
||||
for (const k of keys) {
|
||||
if (k === 'type') continue // already checked
|
||||
if (!cmp((x as any)[k], (y as any)[k])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return cmp(a, b)
|
||||
}
|
||||
import type { AnyObj } from './match-schema-type'
|
||||
import matchTheSchemaType from './match-schema-type'
|
||||
|
||||
const useMatchSchemaType = () => {
|
||||
const { data: schemaTypeDefinitions } = useSchemaTypeDefinitions()
|
||||
const getMatchedSchemaType = (obj: AnyObj): string => {
|
||||
if(!schemaTypeDefinitions) return ''
|
||||
const matched = schemaTypeDefinitions.find(def => deepEqualByType(obj, def.schema))
|
||||
const matched = schemaTypeDefinitions.find(def => matchTheSchemaType(obj, def.schema))
|
||||
return matched ? matched.name : ''
|
||||
}
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { useStore } from '@/app/components/workflow/store'
|
|||
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import ToolForm from '../tool/components/tool-form'
|
||||
import { wrapStructuredVarItem } from '@/app/components/workflow/utils/tool'
|
||||
import useMatchSchemaType from '../_base/components/variable/use-match-schema-type'
|
||||
|
||||
const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -49,6 +50,7 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
|
|||
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||
const { getMatchedSchemaType } = useMatchSchemaType()
|
||||
|
||||
return (
|
||||
<div >
|
||||
|
|
@ -139,7 +141,7 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
|
|||
{outputItem.value?.type === 'object' ? (
|
||||
<StructureOutputItem
|
||||
rootClassName='code-sm-semibold text-text-secondary'
|
||||
payload={wrapStructuredVarItem(outputItem)} />
|
||||
payload={wrapStructuredVarItem(outputItem, getMatchedSchemaType(outputItem.value))} />
|
||||
) : (
|
||||
<VarItem
|
||||
name={outputItem.name}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const checkNodeValid = (_payload: LLMNodeType) => {
|
|||
|
||||
export const getFieldType = (field: Field) => {
|
||||
const { type, items } = field
|
||||
if(field.schemaType === 'file') return 'file'
|
||||
if (type !== Type.array || !items)
|
||||
return type
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue