improvement: actually follow json-schema format for response format and make structured output enforced for providers

This commit is contained in:
Waleed Latif
2025-03-10 16:38:31 -07:00
parent d477f6d17e
commit 490ba4eb87
11 changed files with 238 additions and 116 deletions
@@ -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>
)}
+36 -4
View File
@@ -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
View File
@@ -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 },
+50 -13
View File
@@ -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) {
+59 -5
View File
@@ -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
+4 -1
View File
@@ -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
+4 -1
View File
@@ -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
+4 -1
View File
@@ -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
+9 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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,
},
}