mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement: actually follow json-schema format for response format and make structured output enforced for providers
This commit is contained in:
+33
-4
@@ -44,18 +44,45 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP
|
||||
setIsConnecting(false)
|
||||
}
|
||||
|
||||
// Helper function to extract fields from JSON Schema
|
||||
const extractFieldsFromSchema = (connection: ConnectedBlock): ResponseField[] => {
|
||||
// Handle legacy format with fields array
|
||||
if (connection.responseFormat?.fields) {
|
||||
return connection.responseFormat.fields
|
||||
}
|
||||
|
||||
// Handle new JSON Schema format
|
||||
const schema = connection.responseFormat?.schema || connection.responseFormat
|
||||
// Safely check if schema and properties exist
|
||||
if (
|
||||
!schema ||
|
||||
typeof schema !== 'object' ||
|
||||
!('properties' in schema) ||
|
||||
typeof schema.properties !== 'object'
|
||||
) {
|
||||
return []
|
||||
}
|
||||
return Object.entries(schema.properties).map(([name, prop]: [string, any]) => ({
|
||||
name,
|
||||
type: Array.isArray(prop) ? 'array' : prop.type || 'string',
|
||||
description: prop.description,
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute -left-[180px] top-0 space-y-2 flex flex-col items-end w-[160px]">
|
||||
{incomingConnections.map((connection) => (
|
||||
<div key={connection.id} className="space-y-2">
|
||||
{Array.isArray(connection.outputType) ? (
|
||||
// Handle array of field names
|
||||
connection.outputType.map((fieldName) => {
|
||||
const field = connection.responseFormat?.fields.find(
|
||||
(f: ResponseField) => f.name === fieldName
|
||||
) || {
|
||||
// Try to find field in response format
|
||||
const fields = extractFieldsFromSchema(connection)
|
||||
const field = fields.find((f) => f.name === fieldName) || {
|
||||
name: fieldName,
|
||||
type: 'string',
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={field.name}
|
||||
@@ -84,7 +111,9 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP
|
||||
<span className="font-medium leading-none">
|
||||
{connection.name.replace(/\s+/g, '').toLowerCase()}
|
||||
</span>
|
||||
<span className="text-muted-foreground">.{connection.outputType}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{typeof connection.outputType === 'string' ? `.${connection.outputType}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -14,10 +14,42 @@ export interface ConnectedBlock {
|
||||
outputType: string | string[]
|
||||
name: string
|
||||
responseFormat?: {
|
||||
fields: Field[]
|
||||
// Support both formats
|
||||
fields?: Field[]
|
||||
name?: string
|
||||
schema?: {
|
||||
type: string
|
||||
properties: Record<string, any>
|
||||
required?: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to extract fields from JSON Schema
|
||||
function extractFieldsFromSchema(schema: any): Field[] {
|
||||
if (!schema || typeof schema !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
// Handle legacy format with fields array
|
||||
if (Array.isArray(schema.fields)) {
|
||||
return schema.fields
|
||||
}
|
||||
|
||||
// Handle new JSON Schema format
|
||||
const schemaObj = schema.schema || schema
|
||||
if (!schemaObj || !schemaObj.properties || typeof schemaObj.properties !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
// Extract fields from schema properties
|
||||
return Object.entries(schemaObj.properties).map(([name, prop]: [string, any]) => ({
|
||||
name,
|
||||
type: prop.type || 'string',
|
||||
description: prop.description,
|
||||
}))
|
||||
}
|
||||
|
||||
export function useBlockConnections(blockId: string) {
|
||||
const { edges, blocks } = useWorkflowStore(
|
||||
(state) => ({
|
||||
@@ -43,7 +75,7 @@ export function useBlockConnections(blockId: string) {
|
||||
responseFormat =
|
||||
typeof responseFormatValue === 'string' && responseFormatValue
|
||||
? JSON.parse(responseFormatValue)
|
||||
: undefined
|
||||
: responseFormatValue // Handle case where it's already an object
|
||||
} catch (e) {
|
||||
console.error('Failed to parse response format:', e)
|
||||
responseFormat = undefined
|
||||
@@ -55,8 +87,8 @@ export function useBlockConnections(blockId: string) {
|
||||
type: 'string',
|
||||
}))
|
||||
|
||||
// If we have a valid response format, use its fields as the output types
|
||||
const outputFields = responseFormat?.fields || defaultOutputs
|
||||
// Extract fields from the response format using our helper function
|
||||
const outputFields = responseFormat ? extractFieldsFromSchema(responseFormat) : defaultOutputs
|
||||
|
||||
return {
|
||||
id: sourceBlock.id,
|
||||
|
||||
+33
-49
@@ -81,6 +81,7 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
|
||||
title: 'Response Format',
|
||||
type: 'code',
|
||||
layout: 'full',
|
||||
placeholder: `Enter JSON schema...`,
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
@@ -115,63 +116,46 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
|
||||
type: 'json',
|
||||
required: false,
|
||||
description:
|
||||
'Define the expected response format. If not provided, returns plain text content.',
|
||||
'Define the expected response format using JSON Schema. If not provided, returns plain text content.',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fields: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'A name for your schema (optional)',
|
||||
},
|
||||
schema: {
|
||||
type: 'object',
|
||||
description: 'The JSON Schema definition',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['object'],
|
||||
description: 'Must be "object" for a valid JSON Schema',
|
||||
},
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name of the field',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['string', 'number', 'boolean', 'array', 'object'],
|
||||
description: 'Type of the field',
|
||||
},
|
||||
isArray: {
|
||||
type: 'boolean',
|
||||
description: 'Whether this field contains multiple values',
|
||||
},
|
||||
items: {
|
||||
type: 'object',
|
||||
description: 'Schema for array items (required when isArray is true)',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['string', 'number', 'boolean', 'object'],
|
||||
},
|
||||
properties: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['string', 'number', 'boolean'],
|
||||
},
|
||||
},
|
||||
required: ['name', 'type'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Description of what this field represents',
|
||||
},
|
||||
type: 'object',
|
||||
description: 'Object containing property definitions',
|
||||
},
|
||||
required: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of required property names',
|
||||
},
|
||||
additionalProperties: {
|
||||
type: 'boolean',
|
||||
description: 'Whether additional properties are allowed',
|
||||
},
|
||||
required: ['name', 'type'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
required: ['type', 'properties'],
|
||||
},
|
||||
strict: {
|
||||
type: 'boolean',
|
||||
description: 'Whether to enforce strict schema validation',
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
required: ['fields'],
|
||||
required: ['schema'],
|
||||
},
|
||||
},
|
||||
temperature: { type: 'number', required: false },
|
||||
|
||||
@@ -30,6 +30,33 @@ interface TagDropdownProps {
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
// Add a helper function to extract fields from JSON Schema
|
||||
const extractFieldsFromSchema = (responseFormat: any): Field[] => {
|
||||
if (!responseFormat) return []
|
||||
|
||||
// Handle legacy format with fields array
|
||||
if (Array.isArray(responseFormat.fields)) {
|
||||
return responseFormat.fields
|
||||
}
|
||||
|
||||
// Handle new JSON Schema format
|
||||
const schema = responseFormat.schema || responseFormat
|
||||
if (
|
||||
!schema ||
|
||||
typeof schema !== 'object' ||
|
||||
!('properties' in schema) ||
|
||||
typeof schema.properties !== 'object'
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.entries(schema.properties).map(([name, prop]: [string, any]) => ({
|
||||
name,
|
||||
type: Array.isArray(prop) ? 'array' : prop.type || 'string',
|
||||
description: prop.description,
|
||||
}))
|
||||
}
|
||||
|
||||
export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
visible,
|
||||
onSelect,
|
||||
@@ -109,13 +136,18 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const responseFormatValue = useSubBlockStore
|
||||
.getState()
|
||||
.getValue(activeSourceBlockId, 'responseFormat')
|
||||
if (typeof responseFormatValue === 'string' && responseFormatValue) {
|
||||
const responseFormat = JSON.parse(responseFormatValue)
|
||||
if (responseFormat?.fields) {
|
||||
return {
|
||||
tags: responseFormat.fields.map(
|
||||
(field: Field) => `${normalizedBlockName}.${field.name}`
|
||||
),
|
||||
if (responseFormatValue) {
|
||||
const responseFormat =
|
||||
typeof responseFormatValue === 'string'
|
||||
? JSON.parse(responseFormatValue)
|
||||
: responseFormatValue
|
||||
|
||||
if (responseFormat) {
|
||||
const fields = extractFieldsFromSchema(responseFormat)
|
||||
if (fields.length > 0) {
|
||||
return {
|
||||
tags: fields.map((field: Field) => `${normalizedBlockName}.${field.name}`),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,12 +176,17 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const responseFormatValue = useSubBlockStore
|
||||
.getState()
|
||||
.getValue(edge.source, 'responseFormat')
|
||||
if (typeof responseFormatValue === 'string' && responseFormatValue) {
|
||||
const responseFormat = JSON.parse(responseFormatValue)
|
||||
if (responseFormat?.fields) {
|
||||
return responseFormat.fields.map(
|
||||
(field: Field) => `${normalizedBlockName}.${field.name}`
|
||||
)
|
||||
if (responseFormatValue) {
|
||||
const responseFormat =
|
||||
typeof responseFormatValue === 'string'
|
||||
? JSON.parse(responseFormatValue)
|
||||
: responseFormatValue
|
||||
|
||||
if (responseFormat) {
|
||||
const fields = extractFieldsFromSchema(responseFormat)
|
||||
if (fields.length > 0) {
|
||||
return fields.map((field: Field) => `${normalizedBlockName}.${field.name}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -100,19 +100,73 @@ export const anthropicProvider: ProviderConfig = {
|
||||
|
||||
// If response format is specified, add strict formatting instructions
|
||||
if (request.responseFormat) {
|
||||
systemPrompt = `${systemPrompt}\n\nIMPORTANT RESPONSE FORMAT INSTRUCTIONS:
|
||||
// Get the schema from the response format
|
||||
const schema = request.responseFormat.schema || request.responseFormat
|
||||
|
||||
// Build a system prompt for structured output based on the JSON schema
|
||||
let schemaInstructions = ''
|
||||
|
||||
if (schema && schema.properties) {
|
||||
// Create a template of the expected JSON structure
|
||||
const jsonTemplate = Object.entries(schema.properties).reduce(
|
||||
(acc: Record<string, any>, [key, prop]: [string, any]) => {
|
||||
let exampleValue
|
||||
const propType = prop.type || 'string'
|
||||
|
||||
// Generate appropriate example values based on type
|
||||
switch (propType) {
|
||||
case 'string':
|
||||
exampleValue = '"value"'
|
||||
break
|
||||
case 'number':
|
||||
exampleValue = '0'
|
||||
break
|
||||
case 'boolean':
|
||||
exampleValue = 'true'
|
||||
break
|
||||
case 'array':
|
||||
exampleValue = '[]'
|
||||
break
|
||||
case 'object':
|
||||
exampleValue = '{}'
|
||||
break
|
||||
default:
|
||||
exampleValue = '"value"'
|
||||
}
|
||||
|
||||
acc[key] = exampleValue
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
// Generate field descriptions
|
||||
const fieldDescriptions = Object.entries(schema.properties)
|
||||
.map(([key, prop]: [string, any]) => {
|
||||
const type = prop.type || 'string'
|
||||
const description = prop.description ? `: ${prop.description}` : ''
|
||||
return `${key} (${type})${description}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
// Format the JSON template as a string
|
||||
const jsonTemplateStr = JSON.stringify(jsonTemplate, null, 2)
|
||||
|
||||
schemaInstructions = `
|
||||
IMPORTANT RESPONSE FORMAT INSTRUCTIONS:
|
||||
1. Your response must be EXACTLY in this format, with no additional fields:
|
||||
{
|
||||
${request.responseFormat.fields.map((field) => ` "${field.name}": ${field.type === 'string' ? '"value"' : field.type === 'array' ? '[]' : field.type === 'object' ? '{}' : field.type === 'number' ? '0' : 'true/false'}`).join(',\n')}
|
||||
}
|
||||
${jsonTemplateStr}
|
||||
|
||||
Field descriptions:
|
||||
${request.responseFormat.fields.map((field) => `${field.name} (${field.type})${field.description ? `: ${field.description}` : ''}`).join('\n')}
|
||||
${fieldDescriptions}
|
||||
|
||||
2. DO NOT include any explanatory text before or after the JSON
|
||||
3. DO NOT wrap the response in an array
|
||||
4. DO NOT add any fields not specified in the schema
|
||||
5. Your response MUST be valid JSON and include all the specified fields with their correct types`
|
||||
}
|
||||
|
||||
systemPrompt = `${systemPrompt}${schemaInstructions}`
|
||||
}
|
||||
|
||||
// Build the request payload
|
||||
|
||||
@@ -67,7 +67,10 @@ export const cerebrasProvider: ProviderConfig = {
|
||||
|
||||
// Add response format for structured output if specified
|
||||
if (request.responseFormat) {
|
||||
payload.response_format = { type: 'json_object' }
|
||||
payload.response_format = {
|
||||
type: 'json_schema',
|
||||
schema: request.responseFormat.schema || request.responseFormat,
|
||||
}
|
||||
}
|
||||
|
||||
// Add tools if provided
|
||||
|
||||
@@ -69,7 +69,10 @@ export const googleProvider: ProviderConfig = {
|
||||
|
||||
// Add response format for structured output if specified
|
||||
if (request.responseFormat) {
|
||||
payload.response_format = { type: 'json_object' }
|
||||
payload.response_format = {
|
||||
type: 'json_schema',
|
||||
schema: request.responseFormat.schema || request.responseFormat,
|
||||
}
|
||||
}
|
||||
|
||||
// Add tools if provided
|
||||
|
||||
@@ -68,7 +68,10 @@ export const groqProvider: ProviderConfig = {
|
||||
|
||||
// Add response format for structured output if specified
|
||||
if (request.responseFormat) {
|
||||
payload.response_format = { type: 'json_object' }
|
||||
payload.response_format = {
|
||||
type: 'json_schema',
|
||||
schema: request.responseFormat.schema || request.responseFormat,
|
||||
}
|
||||
}
|
||||
|
||||
// Add tools if provided
|
||||
|
||||
@@ -68,7 +68,15 @@ export const openaiProvider: ProviderConfig = {
|
||||
|
||||
// Add response format for structured output if specified
|
||||
if (request.responseFormat) {
|
||||
payload.response_format = { type: 'json_object' }
|
||||
// Use OpenAI's JSON schema format
|
||||
payload.response_format = {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: request.responseFormat.name || 'response_schema',
|
||||
schema: request.responseFormat.schema || request.responseFormat,
|
||||
strict: request.responseFormat.strict !== false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Add tools if provided
|
||||
|
||||
+3
-5
@@ -86,11 +86,9 @@ export interface ProviderRequest {
|
||||
apiKey: string
|
||||
messages?: Message[]
|
||||
responseFormat?: {
|
||||
fields: Array<{
|
||||
name: string
|
||||
type: 'string' | 'number' | 'boolean' | 'array' | 'object'
|
||||
description?: string
|
||||
}>
|
||||
name: string
|
||||
schema: any
|
||||
strict?: boolean
|
||||
}
|
||||
local_execution?: boolean
|
||||
}
|
||||
|
||||
+3
-32
@@ -64,38 +64,9 @@ export const xAIProvider: ProviderConfig = {
|
||||
payload.response_format = {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: 'structured_response',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: request.responseFormat.fields.reduce(
|
||||
(acc, field) => ({
|
||||
...acc,
|
||||
[field.name]: {
|
||||
type:
|
||||
field.type === 'array'
|
||||
? 'array'
|
||||
: field.type === 'object'
|
||||
? 'object'
|
||||
: field.type === 'number'
|
||||
? 'number'
|
||||
: field.type === 'boolean'
|
||||
? 'boolean'
|
||||
: 'string',
|
||||
description: field.description || '',
|
||||
...(field.type === 'array' && {
|
||||
items: { type: 'string' },
|
||||
}),
|
||||
...(field.type === 'object' && {
|
||||
additionalProperties: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{}
|
||||
),
|
||||
required: request.responseFormat.fields.map((f) => f.name),
|
||||
additionalProperties: false,
|
||||
},
|
||||
strict: true,
|
||||
name: request.responseFormat.name || 'structured_response',
|
||||
schema: request.responseFormat.schema || request.responseFormat,
|
||||
strict: request.responseFormat.strict !== false,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user