Skip to content

Commit dbc9c0f

Browse files
committed
fix(textract): forward URL documents and fix ambiguous error status
- textract_analyze_expense/textract_analyze_id now fall back to filePath/filePathBack when the document input is a URL string rather than an uploaded file object, so advanced "File reference" URL inputs actually reach the API (Cursor Bugbot) - mapTextractSdkError defaults to 500 (not 400) when the AWS SDK error has no HTTP status, since that implies a server-side/network failure rather than a bad request (Greptile)
1 parent 1fd00ed commit dbc9c0f

7 files changed

Lines changed: 96 additions & 19 deletions

File tree

apps/sim/app/api/tools/textract/shared.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ describe('mapTextractSdkError', () => {
7878
expect(mapped.status).toBe(500)
7979
})
8080

81-
it('defaults to 400 when the SDK gives no http status', () => {
81+
it('defaults to 500 when the SDK gives no http status, since that implies a server-side failure', () => {
8282
const mapped = mapTextractSdkError({ message: 'Unknown failure' }, false)
83-
expect(mapped.status).toBe(400)
83+
expect(mapped.status).toBe(500)
8484
})
8585
})
8686

apps/sim/app/api/tools/textract/shared.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ export function mapTextractSdkError(
7575
return new TextractRouteError(`This document format is not supported.${hint}`, 400)
7676
}
7777

78-
const status = err.$metadata?.httpStatusCode || 400
78+
// No status at all (e.g. a network failure before AWS responded) is a server-side problem, not a
79+
// bad request — default to 500 so tool-execution retry logic still treats it as retryable.
80+
const status = err.$metadata?.httpStatusCode ?? 500
7981
return new TextractRouteError(err.message || 'Textract API error', status)
8082
}
8183

apps/sim/blocks/blocks/textract.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,18 @@ export const TextractBlock: BlockConfig<TextractParserOutput> = {
199199
},
200200
}
201201

202+
/**
203+
* Resolves a canonical document input to either an uploaded file object or a plain URL string.
204+
* `normalizeFileInput` only recognizes file objects (or JSON-serialized file references) — a raw
205+
* URL typed into the "File reference" advanced field falls through to `filePath` instead.
206+
*/
207+
function resolveDocumentParam(value: unknown): { file?: object; filePath?: string } {
208+
const file = normalizeFileInput(value, { single: true })
209+
if (file) return { file }
210+
if (typeof value === 'string' && value.trim() !== '') return { filePath: value.trim() }
211+
return {}
212+
}
213+
202214
function requireAwsCredentials(params: Record<string, unknown>) {
203215
const accessKeyId = typeof params.accessKeyId === 'string' ? params.accessKeyId.trim() : ''
204216
const secretAccessKey =
@@ -350,17 +362,18 @@ export const TextractV2Block: BlockConfig<TextractParserOutput> = {
350362
const operation = params.operation || 'analyze_document'
351363

352364
if (operation === 'analyze_id') {
353-
const file = normalizeFileInput(params.document, { single: true })
354-
if (!file) throw new Error('Identity document is required')
365+
const front = resolveDocumentParam(params.document)
366+
if (!front.file && !front.filePath) throw new Error('Identity document is required')
355367

356368
const parameters: Record<string, unknown> = {
357369
accessKeyId,
358370
secretAccessKey,
359371
region,
360-
file,
372+
...front,
361373
}
362-
const fileBack = normalizeFileInput(params.documentBack, { single: true })
363-
if (fileBack) parameters.fileBack = fileBack
374+
const back = resolveDocumentParam(params.documentBack)
375+
if (back.file) parameters.fileBack = back.file
376+
else if (back.filePath) parameters.filePathBack = back.filePath
364377
return parameters
365378
}
366379

@@ -377,7 +390,13 @@ export const TextractV2Block: BlockConfig<TextractParserOutput> = {
377390
throw new Error('S3 URI is required for multi-page processing')
378391
}
379392
parameters.s3Uri = params.s3Uri.trim()
393+
} else if (operation === 'analyze_expense') {
394+
// textract_analyze_expense accepts filePath as a fallback for URL-based documents.
395+
const resolved = resolveDocumentParam(params.document)
396+
if (!resolved.file && !resolved.filePath) throw new Error('Document file is required')
397+
Object.assign(parameters, resolved)
380398
} else {
399+
// textract_parser_v2 (analyze_document) has no filePath param — file only.
381400
const file = normalizeFileInput(params.document, { single: true })
382401
if (!file) throw new Error('Document file is required')
383402
parameters.file = file

apps/sim/tools/textract/analyze-expense.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ export const textractAnalyzeExpenseTool: ToolConfig<
9999
visibility: 'hidden',
100100
description: 'Invoice or receipt to be processed (JPEG, PNG, or single-page PDF).',
101101
},
102+
filePath: {
103+
type: 'string',
104+
required: false,
105+
visibility: 'hidden',
106+
description: 'URL to an invoice or receipt to be processed, if not uploaded directly.',
107+
},
102108
s3Uri: {
103109
type: 'string',
104110
required: false,
@@ -125,11 +131,12 @@ export const textractAnalyzeExpenseTool: ToolConfig<
125131

126132
if (processingMode === 'async') {
127133
requestBody.s3Uri = params.s3Uri?.trim()
128-
} else {
129-
if (!params.file || typeof params.file !== 'object') {
130-
throw new Error('Document file is required for single-page processing')
131-
}
134+
} else if (params.file && typeof params.file === 'object') {
132135
requestBody.file = params.file
136+
} else if (params.filePath && params.filePath.trim() !== '') {
137+
requestBody.filePath = params.filePath.trim()
138+
} else {
139+
throw new Error('Document is required for single-page processing')
133140
}
134141

135142
return requestBody

apps/sim/tools/textract/analyze-id.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,24 @@ export const textractAnalyzeIdTool: ToolConfig<TextractAnalyzeIdV2Input, Textrac
3737
visibility: 'hidden',
3838
description: 'Front of the identity document (JPEG, PNG, or PDF).',
3939
},
40+
filePath: {
41+
type: 'string',
42+
required: false,
43+
visibility: 'hidden',
44+
description: 'URL to the front of the identity document, if not uploaded directly.',
45+
},
4046
fileBack: {
4147
type: 'file',
4248
required: false,
4349
visibility: 'hidden',
4450
description: 'Back of the identity document, if applicable (JPEG, PNG, or PDF).',
4551
},
52+
filePathBack: {
53+
type: 'string',
54+
required: false,
55+
visibility: 'hidden',
56+
description: 'URL to the back of the identity document, if not uploaded directly.',
57+
},
4658
},
4759

4860
request: {
@@ -53,19 +65,24 @@ export const textractAnalyzeIdTool: ToolConfig<TextractAnalyzeIdV2Input, Textrac
5365
Accept: 'application/json',
5466
}),
5567
body: (params) => {
56-
if (!params.file || typeof params.file !== 'object') {
57-
throw new Error('Identity document is required')
58-
}
59-
6068
const requestBody: Record<string, unknown> = {
6169
accessKeyId: params.accessKeyId?.trim(),
6270
secretAccessKey: params.secretAccessKey?.trim(),
6371
region: params.region?.trim(),
64-
file: params.file,
72+
}
73+
74+
if (params.file && typeof params.file === 'object') {
75+
requestBody.file = params.file
76+
} else if (params.filePath && params.filePath.trim() !== '') {
77+
requestBody.filePath = params.filePath.trim()
78+
} else {
79+
throw new Error('Identity document is required')
6580
}
6681

6782
if (params.fileBack && typeof params.fileBack === 'object') {
6883
requestBody.fileBack = params.fileBack
84+
} else if (params.filePathBack && params.filePathBack.trim() !== '') {
85+
requestBody.filePathBack = params.filePathBack.trim()
6986
}
7087

7188
return requestBody

apps/sim/tools/textract/textract.test.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,25 @@ describe('textract_parser_v2', () => {
114114
describe('textract_analyze_expense', () => {
115115
const body = textractAnalyzeExpenseTool.request.body!
116116

117-
it('throws when no file is provided for sync mode', () => {
117+
it('throws when neither file nor filePath is provided for sync mode', () => {
118118
expect(() =>
119119
body({
120120
accessKeyId: 'key',
121121
secretAccessKey: 'secret',
122122
region: 'us-east-1',
123123
} as never)
124-
).toThrow('Document file is required for single-page processing')
124+
).toThrow('Document is required for single-page processing')
125+
})
126+
127+
it('falls back to filePath when no file object is provided', () => {
128+
expect(
129+
body({
130+
accessKeyId: 'key',
131+
secretAccessKey: 'secret',
132+
region: 'us-east-1',
133+
filePath: ' https://example.com/receipt.pdf ',
134+
} as never)
135+
).toMatchObject({ processingMode: 'sync', filePath: 'https://example.com/receipt.pdf' })
125136
})
126137

127138
it('builds an async body requiring s3Uri', () => {
@@ -191,6 +202,24 @@ describe('textract_analyze_id', () => {
191202
})
192203
})
193204

205+
it('falls back to filePath/filePathBack when no file objects are provided', () => {
206+
expect(
207+
body({
208+
accessKeyId: 'key',
209+
secretAccessKey: 'secret',
210+
region: 'us-east-1',
211+
filePath: ' https://example.com/id-front.png ',
212+
filePathBack: ' https://example.com/id-back.png ',
213+
} as never)
214+
).toEqual({
215+
accessKeyId: 'key',
216+
secretAccessKey: 'secret',
217+
region: 'us-east-1',
218+
filePath: 'https://example.com/id-front.png',
219+
filePathBack: 'https://example.com/id-back.png',
220+
})
221+
})
222+
194223
it('normalizes identity documents from the response', async () => {
195224
const result = await textractAnalyzeIdTool.transformResponse!(
196225
respond({

apps/sim/tools/textract/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export interface TextractAnalyzeExpenseV2Input {
111111
region: string
112112
processingMode?: TextractProcessingMode
113113
file?: UserFile
114+
filePath?: string
114115
s3Uri?: string
115116
}
116117

@@ -171,7 +172,9 @@ export interface TextractAnalyzeIdV2Input {
171172
secretAccessKey: string
172173
region: string
173174
file?: UserFile
175+
filePath?: string
174176
fileBack?: UserFile
177+
filePathBack?: string
175178
}
176179

177180
interface TextractIdFieldValue {

0 commit comments

Comments
 (0)