Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/aevatar-console-web/src/shared/studio/document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,42 @@ describe('studio document helpers', () => {
);
});

it('inserts http_request with direct HTTP defaults and no role binding', () => {
const document: StudioWorkflowDocument = {
name: 'workspace-demo',
roles: [{ id: 'assistant' }],
steps: [],
};

const result = insertStepByType(document, 'http_request', {
targetRoleId: 'assistant',
});

expect(result.nodeId).toBe('step:http_request_step');
expect(result.document.steps?.[0]).toEqual(
expect.objectContaining({
id: 'http_request_step',
type: 'http_request',
originalType: 'http_request',
targetRole: null,
parameters: {
authentication: { scheme: 'bearer', secret_ref: '' },
body: '',
body_mode: 'none',
headers: {},
max_request_bytes: '65536',
max_response_bytes: '65536',
method: 'GET',
on_error: 'fail',
query: {},
retry: '0',
timeout_ms: '30000',
url: '',
},
}),
);
});

it('updates role fields and rewrites step role bindings', () => {
const document: StudioWorkflowDocument = {
name: 'workspace-demo',
Expand Down
15 changes: 15 additions & 0 deletions apps/aevatar-console-web/src/shared/studio/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ const DEFAULT_PARAMETERS_BY_STEP_TYPE: Record<string, Record<string, unknown>> =
retry: '0',
on_error: 'fail',
},
http_request: {
method: 'GET',
url: '',
query: {},
headers: {},
body_mode: 'none',
body: '',
authentication: { scheme: 'bearer', secret_ref: '' },
timeout_ms: '30000',
max_request_bytes: '65536',
max_response_bytes: '65536',
retry: '0',
on_error: 'fail',
},
emit: { event_type: 'workflow.completed', payload: '$input' },
human_input: {
prompt: 'Please provide the missing input.',
Expand All @@ -97,6 +111,7 @@ const DEFAULT_STEP_ID_BASE_BY_STEP_TYPE: Record<string, string> = {
dynamic_workflow: 'dynamic_workflow_step',
human_approval: 'approval_step',
human_input: 'human_input_step',
http_request: 'http_request_step',
llm_call: 'llm_step',
map_reduce: 'map_reduce_step',
retrieve_facts: 'retrieve_facts_step',
Expand Down
3 changes: 2 additions & 1 deletion apps/aevatar-console-web/src/shared/studio/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const STUDIO_GRAPH_CATEGORIES: readonly StudioGraphPrimitiveCategory[] =
key: 'integration',
label: 'Integration',
color: '#10B981',
items: ['connector_call', 'emit'],
items: ['connector_call', 'http_request', 'emit'],
},
{
key: 'human',
Expand Down Expand Up @@ -166,6 +166,7 @@ const STUDIO_STEP_TYPE_LABELS: Record<string, string> = {
guard: 'Guard',
human_approval: 'Human approval',
human_input: 'Human input',
http_request: 'HTTP Request',
llm_call: 'LLM call',
map_reduce: 'Map reduce',
parallel: 'Parallel',
Expand Down
206 changes: 206 additions & 0 deletions apps/aevatar-console-web/src/shared/studio/nodeConfigFieldSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,13 @@ const STEP_TYPE_OPTIONS = [
),
value: 'connector_call',
},
{
label: message(
'shared.studio.nodeConfiguration.stepType.option.httpRequest',
'HTTP Request',
),
value: 'http_request',
},
{
label: message(
'shared.studio.nodeConfiguration.stepType.option.emit',
Expand Down Expand Up @@ -523,6 +530,205 @@ const SCHEMAS_BY_STEP_TYPE: Record<string, StudioNodeConfigurationSchemaDefiniti
},
],
},
http_request: {
stepType: 'http_request',
fields: [
{
kind: 'select',
label: message(
'shared.studio.nodeConfiguration.httpRequest.method.label',
'Method',
),
name: 'method',
options: [
{
label: message(
'shared.studio.nodeConfiguration.httpRequest.method.option.get',
'GET',
),
value: 'GET',
},
{
label: message(
'shared.studio.nodeConfiguration.httpRequest.method.option.post',
'POST',
),
value: 'POST',
},
{
label: message(
'shared.studio.nodeConfiguration.httpRequest.method.option.put',
'PUT',
),
value: 'PUT',
},
{
label: message(
'shared.studio.nodeConfiguration.httpRequest.method.option.patch',
'PATCH',
),
value: 'PATCH',
},
{
label: message(
'shared.studio.nodeConfiguration.httpRequest.method.option.delete',
'DELETE',
),
value: 'DELETE',
},
],
parameterName: 'method',
required: true,
},
{
kind: 'single-line',
label: message(
'shared.studio.nodeConfiguration.httpRequest.url.label',
'URL',
),
name: 'url',
parameterName: 'url',
placeholder: message(
'shared.studio.nodeConfiguration.httpRequest.url.placeholder',
'https://api.example.com/items',
),
required: true,
},
{
kind: 'object',
label: message(
'shared.studio.nodeConfiguration.httpRequest.query.label',
'Query parameters',
),
name: 'query',
parameterName: 'query',
},
{
kind: 'object',
label: message(
'shared.studio.nodeConfiguration.httpRequest.headers.label',
'Headers',
),
name: 'headers',
parameterName: 'headers',
},
{
kind: 'select',
label: message(
'shared.studio.nodeConfiguration.httpRequest.bodyMode.label',
'Body mode',
),
name: 'bodyMode',
options: [
{
label: message(
'shared.studio.nodeConfiguration.httpRequest.bodyMode.option.none',
'None',
),
value: 'none',
},
{
label: message(
'shared.studio.nodeConfiguration.httpRequest.bodyMode.option.raw',
'Raw body',
),
value: 'raw',
},
{
label: message(
'shared.studio.nodeConfiguration.httpRequest.bodyMode.option.input',
'Workflow input',
),
value: 'input',
},
],
parameterName: 'body_mode',
},
{
kind: 'multi-line',
label: message(
'shared.studio.nodeConfiguration.httpRequest.body.label',
'Body',
),
name: 'body',
parameterName: 'body',
},
{
kind: 'object',
label: message(
'shared.studio.nodeConfiguration.httpRequest.authentication.label',
'Authentication',
),
name: 'authentication',
parameterName: 'authentication',
},
{
kind: 'number',
label: message(
'shared.studio.nodeConfiguration.httpRequest.timeout.label',
'Timeout ms',
),
name: 'timeoutMs',
parameterName: 'timeout_ms',
validation: { integer: true, min: 100, max: 300000 },
},
{
kind: 'number',
label: message(
'shared.studio.nodeConfiguration.httpRequest.maxResponseBytes.label',
'Max response bytes',
),
name: 'maxResponseBytes',
parameterName: 'max_response_bytes',
validation: { integer: true, min: 1, max: 10485760 },
},
{
kind: 'number',
label: message(
'shared.studio.nodeConfiguration.httpRequest.maxRequestBytes.label',
'Max request bytes',
),
name: 'maxRequestBytes',
parameterName: 'max_request_bytes',
validation: { integer: true, min: 1, max: 10485760 },
},
{
kind: 'number',
label: message(
'shared.studio.nodeConfiguration.httpRequest.retry.label',
'Retries',
),
name: 'retry',
parameterName: 'retry',
validation: { integer: true, min: 0, max: 5 },
},
{
kind: 'select',
label: message(
'shared.studio.nodeConfiguration.httpRequest.onError.label',
'On error',
),
name: 'onError',
options: [
{
label: message(
'shared.studio.nodeConfiguration.httpRequest.onError.option.fail',
'Fail the run',
),
value: 'fail',
},
{
label: message(
'shared.studio.nodeConfiguration.httpRequest.onError.option.continue',
'Continue with input',
),
value: 'continue',
},
],
parameterName: 'on_error',
},
],
},
delay: {
stepType: 'delay',
fields: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,89 @@ describe('studio node configuration semantics', () => {
});
});

it('presents http_request as a first-class direct HTTP node with secret-backed auth fields', () => {
const schema = getStudioNodeConfigurationSchema('http_request');
const values = readStudioNodeConfigurationValues('http_request', {
method: 'GET',
url: 'https://api.example.com/q1000',
authentication: { scheme: 'bearer', secret_ref: 'q1000-token' },
timeout_ms: '20000',
max_request_bytes: '4096',
max_response_bytes: '65536',
});

expect(schema.fields.map((field) => field.parameterName)).toEqual(
expect.arrayContaining([
'method',
'url',
'query',
'headers',
'body_mode',
'body',
'authentication',
'timeout_ms',
'max_request_bytes',
'max_response_bytes',
'retry',
'on_error',
]),
);
expect(schema.fields.find((field) => field.name === 'url')).toEqual(
expect.objectContaining({
label: expect.objectContaining({ defaultMessage: 'URL' }),
required: true,
}),
);
expect(schema.fields.find((field) => field.name === 'authentication')).toEqual(
expect.objectContaining({
kind: 'object',
parameterName: 'authentication',
}),
);
expect(values).toEqual(
expect.objectContaining({
authentication: '{\n "scheme": "bearer",\n "secret_ref": "q1000-token"\n}',
maxResponseBytes: '65536',
method: 'GET',
timeoutMs: '20000',
url: 'https://api.example.com/q1000',
}),
);

expect(
applyStudioNodeConfigurationValues(
'http_request',
{ method: 'GET', url: 'https://api.example.com/q1000' },
{
authentication: '{ "scheme": "bearer", "secret_ref": "q1000-token" }',
body: '',
bodyMode: 'none',
headers: '{}',
maxRequestBytes: '4096',
maxResponseBytes: '65536',
method: 'POST',
onError: 'fail',
query: '{}',
retry: '1',
timeoutMs: '20000',
url: 'https://api.example.com/q1000',
},
),
).toEqual({
authentication: { scheme: 'bearer', secret_ref: 'q1000-token' },
body_mode: 'none',
headers: {},
max_request_bytes: 4096,
max_response_bytes: 65536,
method: 'POST',
on_error: 'fail',
query: {},
retry: 1,
timeout_ms: 20000,
url: 'https://api.example.com/q1000',
});
});

it('presents llm_call prompt_prefix as an Instruction field while preserving runtime parameters', () => {
const schema = getStudioNodeConfigurationSchema('llm_call');
const values = readStudioNodeConfigurationValues('llm_call', {
Expand Down
Loading
Loading