Skip to content
Merged
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
4 changes: 2 additions & 2 deletions credentials/UnstractApi.credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class UnstractApi implements ICredentialType {
displayName = 'Unstract API';
documentationUrl = 'https://docs.unstract.com/unstract/index.html';
icon = 'file:llmWhisperer.svg' as const;

properties: INodeProperties[] = [
{
displayName: 'API Key',
Expand All @@ -36,7 +36,7 @@ export class UnstractApi implements ICredentialType {
type: 'generic',
properties: {
headers: {
'Authorization': '=Bearer apiKey',
'Authorization': '=Bearer {{$credentials.apiKey}}',
},
},
};
Expand Down
4 changes: 2 additions & 2 deletions credentials/UnstractHITLApi.credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class UnstractHITLApi implements ICredentialType {
displayName = 'Unstract HITL API';
documentationUrl = 'https://docs.unstract.com/unstract/index.html';
icon = 'file:llmWhisperer.svg' as const;

properties: INodeProperties[] = [
{
displayName: 'HITL Key',
Expand All @@ -36,7 +36,7 @@ export class UnstractHITLApi implements ICredentialType {
type: 'generic',
properties: {
headers: {
'Authorization': '=Bearer apiKey',
'Authorization': '=Bearer {{$credentials.HITLKey}}',
},
},
};
Expand Down
26 changes: 17 additions & 9 deletions nodes/LlmWhisperer/LlmWhisperer.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export class LlmWhisperer implements INodeType {
const returnData: INodeExecutionData[] = [];

try {
const { helpers, logger } = this;
const { helpers } = this;

for (let i = 0; i < items.length; i++) {
const fileContents = this.getNodeParameter('file_contents', i) as string;
Expand Down Expand Up @@ -250,20 +250,25 @@ export class LlmWhisperer implements INodeType {
accept: 'application/json',
};

logger.info('Making API request to LLMWhisperer API...');

let result: any;
try {
result = await helpers.httpRequestWithAuthentication.call(this, 'llmWhispererApi', requestOptions);
} catch (requestError) {
logger.error('Error during LLMWhisperer API request:', requestError);
} catch (requestError: any) {
throw requestError;
}

if (result.status && result.status !== 202) {
throw new NodeOperationError(this.getNode(), result.body);
// httpRequestWithAuthentication returns already-parsed JSON if Content-Type is application/json
const resultContent = typeof result === 'string' ? JSON.parse(result) : result;


if (!resultContent.whisper_hash) {
throw new NodeOperationError(
this.getNode(),
`Invalid API response: ${resultContent.message || JSON.stringify(resultContent)}`,
);
}

const resultContent = JSON.parse(result) as any;
const whisperHash = resultContent.whisper_hash;

let status = 'processing';
Expand All @@ -281,7 +286,8 @@ export class LlmWhisperer implements INodeType {
},
});

resultContentX = JSON.parse(statusResult);
// httpRequestWithAuthentication returns already-parsed JSON if Content-Type is application/json
resultContentX = typeof statusResult === 'string' ? JSON.parse(statusResult) : statusResult;
status = resultContentX.status;

const currentTime = Date.now();
Expand Down Expand Up @@ -313,7 +319,9 @@ export class LlmWhisperer implements INodeType {
},
});

const retrieveResultContent = JSON.parse(retrieveResult);
// httpRequestWithAuthentication returns already-parsed JSON if Content-Type is application/json
const retrieveResultContent = typeof retrieveResult === 'string' ? JSON.parse(retrieveResult) : retrieveResult;

delete retrieveResultContent.metadata;
delete retrieveResultContent.webhook_metadata;
returnData.push({
Expand Down
108 changes: 83 additions & 25 deletions nodes/Unstract/Unstract.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ export class Unstract implements INodeType {

try {
const credentials = await this.getCredentials('unstractApi');
const apiKey = credentials.apiKey as string;
const orgId = credentials.orgId as string;
const { helpers, logger } = this;
const { helpers } = this;

for (let i = 0; i < items.length; i++) {
const binaryPropertyName = this.getNodeParameter('file_contents', i) as string;
Expand All @@ -136,34 +137,75 @@ export class Unstract implements INodeType {
const tags = this.getNodeParameter('tags', i) as string;
const useFileHistory = this.getNodeParameter('use_file_history', i) as boolean;

const formData: any = {
files: {
value: fileBuffer,
options: {
filename: binaryData.fileName,
contentType: binaryData.mimeType,
},
},
timeout: 1,
// Manual multipart/form-data construction (cloud-compatible, no external dependencies)
// Workaround for n8n issue #18271 where httpRequestWithAuthentication doesn't properly
// handle formData objects. See: https://github.com/n8n-io/n8n/issues/18271
const boundary = `----n8nFormBoundary${Date.now()}`;
const CRLF = '\r\n';

// Build multipart body parts
const parts: Buffer[] = [];

// File field
parts.push(Buffer.from(
`--${boundary}${CRLF}` +
`Content-Disposition: form-data; name="files"; filename="${binaryData.fileName}"${CRLF}` +
`Content-Type: ${binaryData.mimeType}${CRLF}${CRLF}`
));
parts.push(fileBuffer);
parts.push(Buffer.from(CRLF));

// Other fields
const fields = {
timeout: '1',
include_metrics: includeMetrics.toString(),
include_metadata: includeMetadata.toString(),
use_file_history: useFileHistory.toString(),
...(tags && { tags }),
};

if (tags) {
formData.tags = tags;
for (const [name, value] of Object.entries(fields)) {
if (value) {
parts.push(Buffer.from(
`--${boundary}${CRLF}` +
`Content-Disposition: form-data; name="${name}"${CRLF}${CRLF}` +
`${value}${CRLF}`
));
}
}

const requestOptions = {
// Closing boundary
parts.push(Buffer.from(`--${boundary}--${CRLF}`));

// Combine all parts
const body = Buffer.concat(parts);

const requestOptions: any = {
method: 'POST' as IHttpRequestMethods,
url: `${host}/deployment/api/${orgId}/${deploymentName}/`,
formData,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': body.length.toString(),
},
body,
timeout: 5 * 60 * 1000,
};

logger.info('Making API request to Unstract API...');

const result = await helpers.httpRequestWithAuthentication.call(this, 'unstractApi', requestOptions);
let resultContent = JSON.parse(result).message;
// httpRequestWithAuthentication returns already-parsed JSON if Content-Type is application/json
const resultData = typeof result === 'string' ? JSON.parse(result) : result;


if (!resultData || !resultData.message) {
throw new NodeOperationError(
this.getNode(),
`Unexpected API response structure: ${JSON.stringify(resultData)}`,
);
}

let resultContent = resultData.message;
let executionStatus = resultContent.execution_status;

if (executionStatus === 'PENDING' || executionStatus === 'EXECUTING') {
Expand All @@ -173,24 +215,34 @@ export class Unstract implements INodeType {
while (executionStatus !== 'COMPLETED') {
await sleep(2000);

const statusRequestOptions = {
const statusRequestOptions: any = {
method: 'GET' as IHttpRequestMethods,
url: `${host}${statusApi}`,
headers: {
'Authorization': `Bearer ${apiKey}`,
},
timeout: 5 * 60 * 1000,
};


try {
const statusResult = await helpers.httpRequestWithAuthentication.call(this, 'unstractApi', statusRequestOptions);
resultContent = JSON.parse(statusResult);
const statusResult = await helpers.httpRequest(statusRequestOptions);
resultContent = typeof statusResult === 'string' ? JSON.parse(statusResult) : statusResult;
executionStatus = resultContent.status;
} catch (error: any) {
if (error.response && error.response.statusCode === 400) {
throw new NodeOperationError(this.getNode(), `Error: ${error}`);
// HTTP 422 indicates execution still in progress - this is expected
if (error.response?.status === 422 && error.response?.data) {
resultContent = typeof error.response.data === 'string' ? JSON.parse(error.response.data) : error.response.data;
executionStatus = resultContent.status;
} else {
// Actual error - log and rethrow
if (error.response?.status) {
}
throw new NodeOperationError(
this.getNode(),
`Failed to check execution status: ${error.message}`,
);
}
const jsonResponse = error.message.split(' - ')[1];
const cleanJson = jsonResponse.replace(/\\"/g, '"').slice(1, -1);
resultContent = JSON.parse(cleanJson);
executionStatus = resultContent.status;
}

const t2 = new Date();
Expand All @@ -216,6 +268,12 @@ export class Unstract implements INodeType {

return [returnData];
} catch (error: any) {
if (error.response?.data) {
}
if (error.response?.status) {
}
if (error.context?.data) {
}
if (error.message) {
throw new NodeOperationError(this.getNode(), error.message);
}
Expand Down
Loading
Loading