feat: add configurable response format for AI agents (text/JSON) (#15953)

## Summary
This PR adds configurable response format support for AI agents,
allowing them to return either plain text or structured JSON data based
on a defined schema.

## Key Features

### 1. Agent Response Format Configuration
- Added `AgentResponseFormat` type supporting:
  - `text`: Returns plain text responses (default)
  - `json`: Returns structured JSON based on defined schema
- New `AgentResponseSchema` type moved to `twenty-shared/ai` for sharing
between frontend/backend

### 2. Settings UI
- New `SettingsAgentResponseFormat` component for configuring response
format
- Visual schema builder for defining JSON output structure
- Real-time validation and preview
- Integrated into agent settings tab

### 3. Workflow Integration
- AI Agent workflow action automatically uses agent's configured
response format
- Output schema dynamically generated from agent's response format
- Workflow variable picker shows structured fields for JSON responses
- Backward compatible with existing text-only agents

### 4. Backend Implementation
- Added `convertAgentSchemaToZod` utility to validate JSON responses
- Agent executor service handles both text and JSON generation
- Automatic agent creation/cloning when adding AI agent steps to
workflows
- Unique agent naming with conflict resolution

### 5. Database Migration
- Migration `1763622159656-update-agent-response-format.ts` 
- Sets default `responseFormat` to `{"type":"text"}` for existing agents
- Updated all standard agents with proper response format

## Changes by Module

### Frontend (`twenty-front`)
- 🆕 `AgentResponseFormat` type
- 🆕 `SettingsAgentResponseFormat` component
- ✏️ Updated `WorkflowEditActionAiAgent` to support response format
configuration
- 🗑️ Removed deprecated `useAiAgentOutputSchema` hook and
`AiAgentOutputSchema` type

### Backend (`twenty-server`)
- 🆕 `AgentResponseFormat` type in agent entity
- 🆕 `convertAgentSchemaToZod` utility for schema validation
- ✏️ Updated `AiAgentExecutorService` to handle both text and JSON
generation
- ✏️ Updated `WorkflowSchemaWorkspaceService` to generate output schema
from agent config
- ✏️ Enhanced `WorkflowVersionStepOperationsWorkspaceService` with agent
creation/cloning
- 🆕 Agent naming constants for conflict resolution

### Shared (`twenty-shared`)
- 🆕 `AgentResponseSchema` type
- 🆕 `ModelConfiguration` type moved to shared package
- Updated exports in `ai/index.ts`

## Code Quality
- Removed useless comments following code style guidelines
- All linter checks passed
- Type-safe implementation with proper TypeScript types

## Testing
-  Database migration tested
-  Agent creation/cloning in workflows verified
-  Response format switching (text ↔ JSON) validated
-  Backward compatibility with existing agents confirmed

## Migration Notes
- Existing agents will have `responseFormat: {type: 'text'}` set
automatically
- No breaking changes - all existing functionality preserved
- Agents can be updated to use JSON format through settings UI
This commit is contained in:
Félix Malfait
2025-11-20 18:32:44 +01:00
committed by GitHub
parent 5476879f77
commit a281f2a773
49 changed files with 1207 additions and 641 deletions
@@ -1,19 +1,15 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { getFieldIcon } from '../getFieldIcon';
describe('getFieldIcon', () => {
describe('supported field types', () => {
it('should return IconAbc for TEXT field type', () => {
expect(getFieldIcon(FieldMetadataType.TEXT)).toBe('IconAbc');
it('should return IconAbc for string field type', () => {
expect(getFieldIcon('string')).toBe('IconAbc');
});
it('should return IconText for NUMBER field type', () => {
expect(getFieldIcon(FieldMetadataType.NUMBER)).toBe('IconText');
it('should return IconText for number field type', () => {
expect(getFieldIcon('number')).toBe('IconText');
});
it('should return IconCheckbox for BOOLEAN field type', () => {
expect(getFieldIcon(FieldMetadataType.BOOLEAN)).toBe('IconCheckbox');
});
it('should return IconCalendarEvent for DATE field type', () => {
expect(getFieldIcon(FieldMetadataType.DATE)).toBe('IconCalendarEvent');
it('should return IconCheckbox for boolean field type', () => {
expect(getFieldIcon('boolean')).toBe('IconCheckbox');
});
});
@@ -36,8 +32,8 @@ describe('getFieldIcon', () => {
describe('consistency', () => {
it('should return the same icon for the same field type', () => {
const result1 = getFieldIcon(FieldMetadataType.TEXT);
const result2 = getFieldIcon(FieldMetadataType.TEXT);
const result1 = getFieldIcon('string');
const result2 = getFieldIcon('string');
expect(result1).toBe(result2);
});
});
@@ -1,16 +1,13 @@
import { type InputSchemaPropertyType } from '@/workflow/types/InputSchema';
import { FieldMetadataType } from 'twenty-shared/types';
import { type AgentResponseFieldType } from 'twenty-shared/ai';
export const getFieldIcon = (fieldType?: InputSchemaPropertyType): string => {
export const getFieldIcon = (fieldType?: AgentResponseFieldType): string => {
switch (fieldType) {
case FieldMetadataType.TEXT:
case 'string':
return 'IconAbc';
case FieldMetadataType.NUMBER:
case 'number':
return 'IconText';
case FieldMetadataType.BOOLEAN:
case 'boolean':
return 'IconCheckbox';
case FieldMetadataType.DATE:
return 'IconCalendarEvent';
default:
return 'IconQuestionMark';
}