Decouple http node from workflows (#13272)

- Added a generic HTTP request tool, allowing agents and workflows to
make HTTP requests to external APIs with configurable method, headers,
and body.
- Decoupled HTTP request workflow nodes from workflow-specific types and
factories, introducing a generic tool interface.
- Updated agent system prompts to include explicit guidance for the HTTP
request tool, including when and how to use it, and how to communicate
limitations.

### Demo

https://github.com/user-attachments/assets/129bc445-a277-4a19-95ab-09f890f8f051
This commit is contained in:
Abdul Rahman
2025-07-18 16:47:19 +02:00
committed by GitHub
parent 45655a39b0
commit dd24fbe4ee
20 changed files with 259 additions and 113 deletions
@@ -10,13 +10,13 @@ import { AiAgentWorkflowAction } from 'src/modules/workflow/workflow-executor/wo
import { CodeWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code.workflow-action';
import { FilterWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter.workflow-action';
import { FormWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form.workflow-action';
import { HttpRequestWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/http-request/http-request.workflow-action';
import { SendEmailWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action';
import { CreateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action';
import { DeleteRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action';
import { FindRecordsWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action';
import { UpdateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import { WorkflowActionAdapter } from 'src/modules/workflow/workflow-executor/workflow-actions/workflow-action-adapter';
@Injectable()
export class WorkflowActionFactory {
@@ -29,7 +29,7 @@ export class WorkflowActionFactory {
private readonly findRecordsWorkflowAction: FindRecordsWorkflowAction,
private readonly formWorkflowAction: FormWorkflowAction,
private readonly filterWorkflowAction: FilterWorkflowAction,
private readonly httpRequestWorkflowAction: HttpRequestWorkflowAction,
private readonly workflowActionAdapter: WorkflowActionAdapter,
private readonly aiAgentWorkflowAction: AiAgentWorkflowAction,
) {}
@@ -52,7 +52,7 @@ export class WorkflowActionFactory {
case WorkflowActionType.FILTER:
return this.filterWorkflowAction;
case WorkflowActionType.HTTP_REQUEST:
return this.httpRequestWorkflowAction;
return this.workflowActionAdapter;
case WorkflowActionType.AI_AGENT:
return this.aiAgentWorkflowAction;
default:
@@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { HttpRequestWorkflowAction } from './http-request.workflow-action';
@Module({
providers: [HttpRequestWorkflowAction],
exports: [HttpRequestWorkflowAction],
})
export class HttpRequestActionModule {}
@@ -1,76 +0,0 @@
import { Injectable } from '@nestjs/common';
import { isString } from '@sniptt/guards';
import axios, { AxiosRequestConfig } from 'axios';
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
import { WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
import { isWorkflowHttpRequestAction } from './guards/is-workflow-http-request-action.guard';
import { WorkflowHttpRequestActionInput } from './types/workflow-http-request-action-input.type';
@Injectable()
export class HttpRequestWorkflowAction implements WorkflowAction {
async execute({
currentStepId,
steps,
context,
}: WorkflowActionInput): Promise<WorkflowActionOutput> {
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new WorkflowStepExecutorException(
'Step not found',
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
);
}
if (!isWorkflowHttpRequestAction(step)) {
throw new WorkflowStepExecutorException(
'Step is not an HTTP Request action',
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
);
}
const workflowActionInput = resolveInput(
step.settings.input,
context,
) as WorkflowHttpRequestActionInput;
const { url, method, headers, body } = workflowActionInput;
try {
const axiosConfig: AxiosRequestConfig = {
url,
method: method,
headers,
};
if (['POST', 'PUT', 'PATCH'].includes(method) && body) {
const parsedBody = isString(body) ? JSON.parse(body) : body;
axiosConfig.data = parsedBody;
}
const response = await axios(axiosConfig);
return { result: response.data };
} catch (error) {
if (axios.isAxiosError(error)) {
return {
error: error.response?.data || error.message || 'HTTP request failed',
};
}
return {
error: error instanceof Error ? error.message : 'HTTP request failed',
};
}
}
}
@@ -0,0 +1,61 @@
import { Injectable } from '@nestjs/common';
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
import { TOOLS } from 'src/engine/core-modules/tool/constants/tools.const';
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
import { ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
@Injectable()
export class WorkflowActionAdapter implements WorkflowAction {
async execute({
currentStepId,
steps,
context,
}: WorkflowActionInput): Promise<WorkflowActionOutput> {
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new Error('Step not found');
}
const toolType = this.mapWorkflowActionTypeToToolType(step.type);
if (!toolType) {
throw new Error(
`No tool mapping found for workflow action type: ${step.type}`,
);
}
const tool = TOOLS.get(toolType);
if (!tool) {
throw new Error(
`Tool for action type ${step.type} not found in registry`,
);
}
const toolInput = resolveInput(step.settings.input, context) as ToolInput;
const toolOutput = await tool.execute(toolInput);
return {
result: toolOutput.result as object,
error: toolOutput.error,
};
}
private mapWorkflowActionTypeToToolType(
actionType: WorkflowActionType,
): ToolType | null {
const mapping: Partial<Record<WorkflowActionType, ToolType>> = {
[WorkflowActionType.HTTP_REQUEST]: ToolType.HTTP_REQUEST,
};
return mapping[actionType] || null;
}
}
@@ -9,9 +9,9 @@ import { AiAgentActionModule } from 'src/modules/workflow/workflow-executor/work
import { CodeActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code-action.module';
import { FilterActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter-action.module';
import { FormActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form-action.module';
import { HttpRequestActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/http-request/http-request-action.module';
import { SendEmailActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email-action.module';
import { RecordCRUDActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud-action.module';
import { WorkflowActionAdapter } from 'src/modules/workflow/workflow-executor/workflow-actions/workflow-action-adapter';
import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service';
import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module';
@@ -25,7 +25,6 @@ import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow
WorkflowRunModule,
BillingModule,
FilterActionModule,
HttpRequestActionModule,
AiAgentActionModule,
FeatureFlagModule,
],
@@ -33,6 +32,7 @@ import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow
WorkflowExecutorWorkspaceService,
ScopedWorkspaceContextFactory,
WorkflowActionFactory,
WorkflowActionAdapter,
],
exports: [WorkflowExecutorWorkspaceService],
})