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
13 changes: 13 additions & 0 deletions apps/docs/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2041,6 +2041,19 @@ export function Mem0Icon(props: SVGProps<SVGSVGElement>) {
)
}

export function ExtendIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 33 18' fill='none'>
<path
clipRule='evenodd'
d='M16.2893 0C16.6984 1.91708e-05 17.1074 0.0970011 17.5103 0.293745C22.3018 2.63326 27.0841 4.98521 31.8693 7.33722C32.3003 7.54649 32.5721 7.9868 32.5721 8.46461V9.51422C32.5721 9.99522 32.3004 10.4357 31.8693 10.645C31.8693 10.645 19.5816 16.6732 17.5542 17.6634C17.1357 17.8696 16.692 17.9727 16.2859 17.9727C15.8799 17.9727 15.4707 17.8758 15.0615 17.6759C12.8124 16.5795 1.9646 11.2604 0.705842 10.6419C0.274826 10.4295 2.31482e-05 9.99216 0 9.51117V8.46461C4.59913e-05 7.98366 0.271816 7.54656 0.702792 7.33417C5.8977 4.7819 15.0599 0.301869 15.1021 0.281239C15.4957 0.0938275 15.8801 0 16.2893 0ZM16.2859 2.96124C16.1516 2.96126 16.0173 2.98909 15.8924 3.05153L4.28874 8.77696C4.11382 8.86442 4.11382 9.10831 4.28874 9.19577L15.8924 14.9209C16.0173 14.9802 16.1516 15.0115 16.2859 15.0115C16.4202 15.0115 16.5548 14.9802 16.6797 14.9209L28.2864 9.19577C28.4582 9.10831 28.4582 8.86442 28.2864 8.77696L16.6797 3.05153C16.5548 2.98906 16.4202 2.96124 16.2859 2.96124Z'
fill='currentColor'
fillRule='evenodd'
/>
</svg>
)
}

export function EvernoteIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='#7fce2c'>
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/components/ui/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
EnrichSoIcon,
EvernoteIcon,
ExaAIIcon,
ExtendIcon,
EyeIcon,
FathomIcon,
FirecrawlIcon,
Expand Down Expand Up @@ -222,6 +223,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
enrich: EnrichSoIcon,
evernote: EvernoteIcon,
exa: ExaAIIcon,
extend_v2: ExtendIcon,
fathom: FathomIcon,
file_v3: DocumentIcon,
firecrawl: FirecrawlIcon,
Expand Down
39 changes: 39 additions & 0 deletions apps/docs/content/docs/en/tools/extend.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: Extend
description: Parse and extract content from documents
---

import { BlockInfoCard } from "@/components/ui/block-info-card"

<BlockInfoCard
type="extend_v2"
color="#000000"
/>

## Usage Instructions

Integrate Extend AI into the workflow. Parse and extract structured content from documents or file references.



## Tools

### `extend_parser`

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filePath` | string | No | URL to a document to be processed |
| `file` | file | No | Document file to be processed |
| `fileUpload` | object | No | File upload data from file-upload component |
| `outputFormat` | string | No | Target output format \(markdown or spatial\). Defaults to markdown. |
| `chunking` | string | No | Chunking strategy \(page, document, or section\). Defaults to page. |
| `engine` | string | No | Parsing engine \(parse_performance or parse_light\). Defaults to parse_performance. |
| `apiKey` | string | Yes | Extend API key |

#### Output

This tool does not produce any outputs.


1 change: 1 addition & 0 deletions apps/docs/content/docs/en/tools/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"enrich",
"evernote",
"exa",
"extend",
"fathom",
"file",
"firecrawl",
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/app/(landing)/integrations/data/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
EnrichSoIcon,
EvernoteIcon,
ExaAIIcon,
ExtendIcon,
EyeIcon,
FathomIcon,
FirecrawlIcon,
Expand Down Expand Up @@ -222,6 +223,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
enrich: EnrichSoIcon,
evernote: EvernoteIcon,
exa: ExaAIIcon,
extend_v2: ExtendIcon,
fathom: FathomIcon,
file_v3: DocumentIcon,
firecrawl: FirecrawlIcon,
Expand Down
18 changes: 18 additions & 0 deletions apps/sim/app/(landing)/integrations/data/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -2978,6 +2978,24 @@
"integrationType": "search",
"tags": ["web-scraping", "enrichment"]
},
{
"type": "extend_v2",
"slug": "extend",
"name": "Extend",
"description": "Parse and extract content from documents",
"longDescription": "Integrate Extend AI into the workflow. Parse and extract structured content from documents or file references.",
"bgColor": "#000000",
"iconName": "ExtendIcon",
"docsUrl": "https://docs.sim.ai/tools/extend",
"operations": [],
"operationCount": 0,
"triggers": [],
"triggerCount": 0,
"authType": "api-key",
"category": "tools",
"integrationType": "ai",
"tags": ["document-processing", "ocr"]
},
{
"type": "fathom",
"slug": "fathom",
Expand Down
188 changes: 188 additions & 0 deletions apps/sim/app/api/tools/extend/parse/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
secureFetchWithPinnedIP,
validateUrlWithDNS,
} from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
import { isInternalFileUrl } from '@/lib/uploads/utils/file-utils'
import { resolveFileInputToUrl } from '@/lib/uploads/utils/file-utils.server'

export const dynamic = 'force-dynamic'

const logger = createLogger('ExtendParseAPI')

const ExtendParseSchema = z.object({
apiKey: z.string().min(1, 'API key is required'),
filePath: z.string().optional(),
file: RawFileInputSchema.optional(),
outputFormat: z.enum(['markdown', 'spatial']).optional(),
chunking: z.enum(['page', 'document', 'section']).optional(),
engine: z.enum(['parse_performance', 'parse_light']).optional(),
})

export async function POST(request: NextRequest) {
const requestId = generateRequestId()

try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })

if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Extend parse attempt`, {
error: authResult.error || 'Missing userId',
})
return NextResponse.json(
{
success: false,
error: authResult.error || 'Unauthorized',
},
{ status: 401 }
)
}

const userId = authResult.userId
const body = await request.json()
const validatedData = ExtendParseSchema.parse(body)

logger.info(`[${requestId}] Extend parse request`, {
fileName: validatedData.file?.name,
filePath: validatedData.filePath,
isWorkspaceFile: validatedData.filePath ? isInternalFileUrl(validatedData.filePath) : false,
userId,
})

const resolution = await resolveFileInputToUrl({
file: validatedData.file,
filePath: validatedData.filePath,
userId,
requestId,
logger,
})

if (resolution.error) {
return NextResponse.json(
{ success: false, error: resolution.error.message },
{ status: resolution.error.status }
)
}

const fileUrl = resolution.fileUrl
if (!fileUrl) {
return NextResponse.json({ success: false, error: 'File input is required' }, { status: 400 })
}

const extendBody: Record<string, unknown> = {
file: { fileUrl },
}

const config: Record<string, unknown> = {}

if (validatedData.outputFormat) {
config.target = validatedData.outputFormat
}

if (validatedData.chunking) {
config.chunkingStrategy = { type: validatedData.chunking }
}

if (validatedData.engine) {
config.engine = validatedData.engine
}

if (Object.keys(config).length > 0) {
extendBody.config = config
}

const extendEndpoint = 'https://api.extend.ai/parse'
const extendValidation = await validateUrlWithDNS(extendEndpoint, 'Extend API URL')
if (!extendValidation.isValid) {
logger.error(`[${requestId}] Extend API URL validation failed`, {
error: extendValidation.error,
})
return NextResponse.json(
{
success: false,
error: 'Failed to reach Extend API',
},
{ status: 502 }
)
}

const extendResponse = await secureFetchWithPinnedIP(
extendEndpoint,
extendValidation.resolvedIP!,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${validatedData.apiKey}`,
'x-extend-api-version': '2025-04-21',
},
body: JSON.stringify(extendBody),
}
)

if (!extendResponse.ok) {
const errorText = await extendResponse.text()
logger.error(`[${requestId}] Extend API error:`, errorText)
let clientError = `Extend API error: ${extendResponse.statusText || extendResponse.status}`
try {
const parsedError = JSON.parse(errorText)
if (parsedError?.message || parsedError?.error) {
clientError = (parsedError.message ?? parsedError.error) as string
}
} catch {
// errorText is not JSON; keep generic message
}
return NextResponse.json(
{
success: false,
error: clientError,
},
{ status: extendResponse.status }
)
}

const extendData = (await extendResponse.json()) as Record<string, unknown>

logger.info(`[${requestId}] Extend parse successful`)

return NextResponse.json({
success: true,
output: {
id: extendData.id ?? null,
status: extendData.status ?? 'PROCESSED',
chunks: extendData.chunks ?? [],
blocks: extendData.blocks ?? [],
pageCount: extendData.pageCount ?? extendData.page_count ?? null,
creditsUsed: extendData.creditsUsed ?? extendData.credits_used ?? null,
},
})
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
return NextResponse.json(
{
success: false,
error: 'Invalid request data',
details: error.errors,
},
{ status: 400 }
)
}

logger.error(`[${requestId}] Error in Extend parse:`, error)

return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Internal server error',
},
{ status: 500 }
)
}
}
Loading
Loading