first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const SESSION_MODE = {
|
||||
NEW: 'new',
|
||||
EXISTING: 'existing',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Session related fields
|
||||
*/
|
||||
|
||||
export const sessionIdField: INodeProperties = {
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '={{ $json["sessionId"] }}',
|
||||
description:
|
||||
'The ID of the <a href="https://docs.airtop.ai/guides/how-to/creating-a-session" target="_blank">Session</a> to use',
|
||||
};
|
||||
|
||||
export const windowIdField: INodeProperties = {
|
||||
displayName: 'Window ID',
|
||||
name: 'windowId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '={{ $json["windowId"] }}',
|
||||
description:
|
||||
'The ID of the <a href="https://docs.airtop.ai/guides/how-to/creating-a-session#windows" target="_blank">Window</a> to use',
|
||||
};
|
||||
|
||||
export const profileNameField: INodeProperties = {
|
||||
displayName: 'Profile Name',
|
||||
name: 'profileName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The name of the Airtop profile to load or create',
|
||||
hint: '<a href="https://docs.airtop.ai/guides/how-to/saving-a-profile" target="_blank">Learn more</a> about Airtop profiles',
|
||||
placeholder: 'e.g. my-x-profile',
|
||||
};
|
||||
|
||||
export const urlField: INodeProperties = {
|
||||
displayName: 'URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. https://google.com',
|
||||
description: 'URL to load in the window',
|
||||
};
|
||||
|
||||
/**
|
||||
* Extraction related fields
|
||||
*/
|
||||
|
||||
export const outputSchemaField: INodeProperties = {
|
||||
displayName: 'JSON Output Schema',
|
||||
name: 'outputSchema',
|
||||
description: 'JSON schema defining the structure of the output',
|
||||
hint: 'If you want to force your output to be JSON, provide a valid JSON schema describing the output. You can generate one automatically in the <a href="https://portal.airtop.ai/" target="_blank">Airtop API Playground</a>.',
|
||||
type: 'json',
|
||||
default: '',
|
||||
};
|
||||
|
||||
export const parseJsonOutputField: INodeProperties = {
|
||||
displayName: 'Parse JSON Output',
|
||||
name: 'parseJsonOutput',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
"Whether to parse the model's response to JSON in the output. Requires the 'JSON Output Schema' parameter to be set.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Interaction related fields
|
||||
*/
|
||||
|
||||
export const elementDescriptionField: INodeProperties = {
|
||||
displayName: 'Element Description',
|
||||
name: 'elementDescription',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A specific description of the element to execute the interaction on',
|
||||
placeholder: 'e.g. the search box at the top of the page',
|
||||
};
|
||||
|
||||
export function getSessionModeFields(resource: string, operations: string[]): INodeProperties[] {
|
||||
return [
|
||||
{
|
||||
displayName: 'Session Mode',
|
||||
name: 'sessionMode',
|
||||
type: 'options',
|
||||
default: 'existing',
|
||||
description: 'Choose between creating a new session or using an existing one',
|
||||
options: [
|
||||
{
|
||||
name: 'Automatically Create Session',
|
||||
description: 'Automatically create a new session and window for this operation',
|
||||
value: SESSION_MODE.NEW,
|
||||
},
|
||||
{
|
||||
name: 'Use Existing Session',
|
||||
description: 'Use an existing session and window for this operation',
|
||||
value: SESSION_MODE.EXISTING,
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [resource],
|
||||
operation: operations,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...sessionIdField,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [resource],
|
||||
sessionMode: [SESSION_MODE.EXISTING],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...windowIdField,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [resource],
|
||||
sessionMode: [SESSION_MODE.EXISTING],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...urlField,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [resource],
|
||||
sessionMode: [SESSION_MODE.NEW],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...profileNameField,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [resource],
|
||||
sessionMode: [SESSION_MODE.NEW],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Auto-Terminate Session',
|
||||
name: 'autoTerminateSession',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to terminate the session after the operation is complete. When disabled, you must manually terminate the session. By default, idle sessions timeout after 10 minutes',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [resource],
|
||||
sessionMode: [SESSION_MODE.NEW],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export const includeHiddenElementsField: INodeProperties = {
|
||||
displayName: 'Include Hidden Elements',
|
||||
name: 'includeHiddenElements',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to include hidden elements in the interaction',
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import type { IExecuteFunctions, IDataObject } from 'n8n-workflow';
|
||||
|
||||
import type { IAirtopNodeExecutionData, IAirtopResponse } from '../../transport/types';
|
||||
|
||||
/**
|
||||
* Parse JSON when the 'Parse JSON Output' parameter is enabled
|
||||
* @param this - The execution context
|
||||
* @param index - The index of the node
|
||||
* @param response - The Airtop API response to parse
|
||||
* @returns The parsed output
|
||||
*/
|
||||
export function parseJsonIfPresent(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
response: IAirtopResponse,
|
||||
): IAirtopResponse {
|
||||
const parseJsonOutput = this.getNodeParameter('additionalFields.parseJsonOutput', index, false);
|
||||
const outputJsonSchema = this.getNodeParameter(
|
||||
'additionalFields.outputSchema',
|
||||
index,
|
||||
'',
|
||||
) as string;
|
||||
|
||||
if (!parseJsonOutput || !outputJsonSchema.startsWith('{')) {
|
||||
return response;
|
||||
}
|
||||
|
||||
try {
|
||||
const output = JSON.parse(response.data?.modelResponse ?? '') as IDataObject;
|
||||
return {
|
||||
sessionId: response.sessionId,
|
||||
windowId: response.windowId,
|
||||
output,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(this.getNode(), 'Output is not a valid JSON');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the output when used as a tool
|
||||
* @param output - The output to clean up
|
||||
* @returns The cleaned up output
|
||||
*/
|
||||
export function cleanOutputForToolUse(output: IAirtopNodeExecutionData[]) {
|
||||
const getOutput = (executionData: IAirtopNodeExecutionData) => {
|
||||
// Return error message
|
||||
if (executionData.json?.errors?.length) {
|
||||
const errorMessage = executionData.json?.errors[0].message as string;
|
||||
return {
|
||||
output: `Error: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Return output parsed from JSON
|
||||
if (executionData.json?.output) {
|
||||
return executionData.json?.output;
|
||||
}
|
||||
|
||||
// Return model response
|
||||
if (executionData.json?.data?.modelResponse) {
|
||||
return {
|
||||
output: executionData.json?.data?.modelResponse,
|
||||
};
|
||||
}
|
||||
|
||||
// Return everything else
|
||||
return {
|
||||
output: { ...(executionData.json?.data ?? {}) },
|
||||
};
|
||||
};
|
||||
|
||||
return output.map((executionData) => ({
|
||||
...executionData,
|
||||
json: {
|
||||
...getOutput(executionData),
|
||||
},
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { IExecuteFunctions, IDataObject } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
validateSessionAndWindowId,
|
||||
createSessionAndWindow,
|
||||
shouldCreateNewSession,
|
||||
validateAirtopApiResponse,
|
||||
} from '../../GenericFunctions';
|
||||
import { apiRequest } from '../../transport';
|
||||
import type { IAirtopResponse } from '../../transport/types';
|
||||
|
||||
/**
|
||||
* Execute the node operation. Creates and terminates a new session if needed.
|
||||
* @param this - The execution context
|
||||
* @param index - The index of the node
|
||||
* @param request - The request to execute
|
||||
* @returns The response from the request
|
||||
*/
|
||||
export async function executeRequestWithSessionManagement(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
request: {
|
||||
method: 'POST' | 'DELETE';
|
||||
path: string;
|
||||
body: IDataObject;
|
||||
},
|
||||
): Promise<IAirtopResponse> {
|
||||
let airtopSessionId = '';
|
||||
try {
|
||||
const { sessionId, windowId } = shouldCreateNewSession.call(this, index)
|
||||
? await createSessionAndWindow.call(this, index)
|
||||
: validateSessionAndWindowId.call(this, index);
|
||||
airtopSessionId = sessionId;
|
||||
|
||||
const shouldTerminateSession = this.getNodeParameter('autoTerminateSession', index, false);
|
||||
|
||||
const endpoint = request.path.replace('{sessionId}', sessionId).replace('{windowId}', windowId);
|
||||
const response = await apiRequest.call(this, request.method, endpoint, request.body);
|
||||
|
||||
validateAirtopApiResponse(this.getNode(), response);
|
||||
|
||||
if (shouldTerminateSession) {
|
||||
await apiRequest.call(this, 'DELETE', `/sessions/${sessionId}`);
|
||||
this.logger.info(`[${this.getNode().name}] Session terminated.`);
|
||||
return response;
|
||||
}
|
||||
|
||||
return { sessionId, windowId, ...response };
|
||||
} catch (error) {
|
||||
// terminate session on error
|
||||
if (airtopSessionId) {
|
||||
await apiRequest.call(this, 'DELETE', `/sessions/${airtopSessionId}`);
|
||||
this.logger.info(`[${this.getNode().name}] Session terminated.`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user