@librechat/agents 3.2.63 → 3.2.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/graphs/Graph.cjs +3 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +73 -9
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +41 -9
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +3 -1
- package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/stream_events.cjs +337 -0
- package/dist/cjs/llm/anthropic/utils/stream_events.cjs.map +1 -0
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +82 -34
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/tools/search/crw-scraper.cjs +165 -0
- package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -0
- package/dist/cjs/tools/search/crw-search.cjs +105 -0
- package/dist/cjs/tools/search/crw-search.cjs.map +1 -0
- package/dist/cjs/tools/search/search.cjs +4 -2
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +15 -3
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +3 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +73 -9
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/types.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +41 -9
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs +3 -2
- package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/stream_events.mjs +337 -0
- package/dist/esm/llm/anthropic/utils/stream_events.mjs.map +1 -0
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +82 -34
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/tools/search/crw-scraper.mjs +163 -0
- package/dist/esm/tools/search/crw-scraper.mjs.map +1 -0
- package/dist/esm/tools/search/crw-search.mjs +103 -0
- package/dist/esm/tools/search/crw-search.mjs.map +1 -0
- package/dist/esm/tools/search/search.mjs +4 -2
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +15 -3
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/types/llm/anthropic/index.d.ts +2 -0
- package/dist/types/llm/anthropic/types.d.ts +2 -0
- package/dist/types/llm/anthropic/utils/message_outputs.d.ts +1 -2
- package/dist/types/llm/anthropic/utils/stream_events.d.ts +25 -0
- package/dist/types/tools/search/crw-scraper.d.ts +41 -0
- package/dist/types/tools/search/crw-search.d.ts +4 -0
- package/dist/types/tools/search/types.d.ts +88 -3
- package/package.json +1 -1
- package/src/graphs/Graph.ts +15 -0
- package/src/llm/anthropic/index.ts +134 -10
- package/src/llm/anthropic/inherited-content-utils.spec.ts +10 -5
- package/src/llm/anthropic/inherited-stream-events.spec.ts +513 -9
- package/src/llm/anthropic/llm.spec.ts +100 -16
- package/src/llm/anthropic/types.ts +3 -0
- package/src/llm/anthropic/utils/message_inputs.ts +60 -3
- package/src/llm/anthropic/utils/message_outputs.ts +10 -2
- package/src/llm/anthropic/utils/stream_events.ts +471 -0
- package/src/llm/bedrock/utils/message_inputs.test.ts +276 -1
- package/src/llm/bedrock/utils/message_inputs.ts +121 -73
- package/src/tools/search/crw-scraper.ts +244 -0
- package/src/tools/search/crw-search.ts +167 -0
- package/src/tools/search/crw.test.ts +836 -0
- package/src/tools/search/search.ts +7 -1
- package/src/tools/search/tool.ts +23 -3
- package/src/tools/search/types.ts +103 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message_inputs.mjs","names":[],"sources":["../../../../../src/llm/bedrock/utils/message_inputs.ts"],"sourcesContent":["/**\n * Utility functions for converting LangChain messages to Bedrock Converse messages.\n * Ported from @langchain/aws common.js\n */\nimport {\n type BaseMessage,\n isAIMessage,\n type Data,\n parseBase64DataUrl,\n parseMimeType,\n MessageContentComplex,\n type StandardContentBlockConverter,\n convertToProviderContentBlock,\n isDataContentBlock,\n} from '@langchain/core/messages';\nimport type {\n AudioFormat,\n AudioSource,\n DocumentFormat,\n DocumentSource,\n VideoFormat,\n VideoSource,\n} from '@aws-sdk/client-bedrock-runtime';\nimport type {\n BedrockMessage,\n BedrockSystemContentBlock,\n BedrockContentBlock,\n MessageContentReasoningBlock,\n} from '../types';\n\n/**\n * Reasoning blocks from other providers, relative to Bedrock. Bedrock's native\n * reasoning format is `reasoning_content`; these carry provider-specific\n * signatures Bedrock cannot validate, so they are dropped on a cross-provider\n * handoff (e.g. Anthropic → Bedrock) rather than crashing the conversion.\n */\nconst FOREIGN_REASONING_TYPES = [\n 'thinking',\n 'redacted_thinking',\n 'reasoning',\n 'think',\n];\n\n/**\n * Bedrock Converse rejects assistant messages with no content blocks. When\n * filtering (e.g. dropping foreign reasoning) empties an assistant turn that\n * also has no tool calls, fall back to this placeholder text.\n */\nconst BEDROCK_EMPTY_TEXT_PLACEHOLDER = '_';\n\n/**\n * Convert a LangChain reasoning block to a Bedrock reasoning block.\n */\nexport function langchainReasoningBlockToBedrockReasoningBlock(\n content: MessageContentReasoningBlock\n): {\n reasoningText?: { text?: string; signature?: string };\n redactedContent?: Uint8Array;\n} {\n if (content.reasoningText != null) {\n return {\n reasoningText: content.reasoningText,\n };\n }\n if (content.redactedContent != null && content.redactedContent !== '') {\n return {\n redactedContent: new Uint8Array(\n Buffer.from(content.redactedContent, 'base64')\n ),\n };\n }\n throw new Error('Invalid reasoning content');\n}\n\n/**\n * Whether a reasoning block can be serialized to a valid Bedrock\n * `reasoningContent`. Bedrock Converse rejects `reasoningText` with a null/empty\n * `text` (e.g. a signature-only block that never merged with its text), so such\n * blocks must be dropped rather than sent.\n */\nfunction isSerializableBedrockReasoningBlock(\n content: MessageContentReasoningBlock\n): boolean {\n if (content.reasoningText != null) {\n const text = content.reasoningText.text;\n return text != null && text !== '';\n }\n return content.redactedContent != null && content.redactedContent !== '';\n}\n\n/**\n * Concatenate consecutive reasoning blocks in content array.\n */\nexport function concatenateLangchainReasoningBlocks(\n content: Array<MessageContentComplex | MessageContentReasoningBlock>\n): Array<MessageContentComplex | MessageContentReasoningBlock> {\n const result: Array<MessageContentComplex | MessageContentReasoningBlock> =\n [];\n\n for (const block of content) {\n if (block.type === 'reasoning_content') {\n const currentReasoning = block as MessageContentReasoningBlock;\n const lastIndex = result.length - 1;\n\n // Check if we can merge with the previous block\n if (lastIndex >= 0) {\n const lastBlock = result[lastIndex];\n if (\n lastBlock.type === 'reasoning_content' &&\n (lastBlock as MessageContentReasoningBlock).reasoningText != null &&\n currentReasoning.reasoningText != null\n ) {\n const lastReasoning = lastBlock as MessageContentReasoningBlock;\n // Merge consecutive reasoning text blocks\n const lastText = lastReasoning.reasoningText?.text;\n const currentText = currentReasoning.reasoningText.text;\n if (\n lastText != null &&\n lastText !== '' &&\n currentText != null &&\n currentText !== ''\n ) {\n lastReasoning.reasoningText!.text = lastText + currentText;\n } else if (\n currentReasoning.reasoningText.signature != null &&\n currentReasoning.reasoningText.signature !== ''\n ) {\n lastReasoning.reasoningText!.signature =\n currentReasoning.reasoningText.signature;\n }\n continue;\n }\n }\n\n result.push({ ...block } as MessageContentReasoningBlock);\n } else {\n result.push(block);\n }\n }\n\n return result;\n}\n\n/**\n * Extract image info from a base64 string or URL.\n */\nexport function extractImageInfo(base64: string): BedrockContentBlock {\n // Extract the format from the base64 string\n const formatMatch = base64.match(/^data:image\\/(\\w+);base64,/);\n let format: 'gif' | 'jpeg' | 'png' | 'webp' | undefined;\n if (formatMatch) {\n const extractedFormat = formatMatch[1].toLowerCase();\n if (['gif', 'jpeg', 'png', 'webp'].includes(extractedFormat)) {\n format = extractedFormat as typeof format;\n }\n }\n\n // Remove the data URL prefix if present\n const base64Data = base64.replace(/^data:image\\/\\w+;base64,/, '');\n\n // Convert base64 to Uint8Array\n const binaryString = atob(base64Data);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i += 1) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n\n return {\n image: {\n format,\n source: {\n bytes,\n },\n },\n };\n}\n\ntype MediaContentBlock = MessageContentComplex & {\n data?: string | Uint8Array;\n url?: string;\n fileId?: string;\n mimeType?: string;\n};\n\nconst mimeTypeToVideoFormat: Record<string, VideoFormat> = {\n 'video/flv': 'flv',\n 'video/mkv': 'mkv',\n 'video/mov': 'mov',\n 'video/mp4': 'mp4',\n 'video/mpeg': 'mpeg',\n 'video/mpg': 'mpg',\n 'video/three_gp': 'three_gp',\n 'video/webm': 'webm',\n 'video/wmv': 'wmv',\n};\n\nconst mimeTypeToAudioFormat: Record<string, AudioFormat> = {\n 'audio/aac': 'aac',\n 'audio/flac': 'flac',\n 'audio/m4a': 'm4a',\n 'audio/mka': 'mka',\n 'audio/mkv': 'mkv',\n 'audio/mp3': 'mp3',\n 'audio/mp4': 'mp4',\n 'audio/mpeg': 'mpeg',\n 'audio/mpga': 'mpga',\n 'audio/ogg': 'ogg',\n 'audio/opus': 'opus',\n 'audio/pcm': 'pcm',\n 'audio/wav': 'wav',\n 'audio/webm': 'webm',\n 'audio/x-aac': 'x-aac',\n};\n\nconst mimeTypeToDocumentFormat: Partial<Record<string, DocumentFormat>> = {\n 'text/csv': 'csv',\n 'application/msword': 'doc',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':\n 'docx',\n 'text/html': 'html',\n 'text/markdown': 'md',\n 'application/pdf': 'pdf',\n 'text/plain': 'txt',\n 'application/vnd.ms-excel': 'xls',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',\n};\n\nfunction base64ToBytes(data: string): Uint8Array {\n return Uint8Array.from(atob(data), (char) => char.charCodeAt(0));\n}\n\nfunction getMediaFormat<T extends string>(\n mimeType: string | undefined,\n formatMap: Record<string, T>\n): T | undefined {\n if (mimeType == null || mimeType === '') {\n return undefined;\n }\n return formatMap[mimeType] ?? (parseMimeType(mimeType).subtype as T);\n}\n\nfunction resolveMediaSource(\n block: MediaContentBlock\n): AudioSource | VideoSource {\n if (typeof block.data === 'string') {\n return { bytes: base64ToBytes(block.data) };\n }\n if (block.data instanceof Uint8Array) {\n return { bytes: block.data };\n }\n if (typeof block.url === 'string') {\n const parsedData = parseBase64DataUrl({\n dataUrl: block.url,\n asTypedArray: true,\n });\n if (parsedData != null) {\n return { bytes: parsedData.data as Uint8Array };\n }\n throw new Error(\n `Only base64 data URLs are supported for ${block.type} blocks with 'url' field with ChatBedrockConverse.`\n );\n }\n if (typeof block.fileId === 'string') {\n return { s3Location: { uri: block.fileId } };\n }\n throw new Error(\n `${block.type} block must include one of: 'data' (base64 string or Uint8Array), 'url' (base64 data URL), or 'fileId' (S3 URI).`\n );\n}\n\nfunction convertMultimodalVideoBlock(\n block: MediaContentBlock\n): BedrockContentBlock {\n return {\n video: {\n format: getMediaFormat(block.mimeType, mimeTypeToVideoFormat),\n source: resolveMediaSource(block) as VideoSource,\n },\n } as BedrockContentBlock;\n}\n\nfunction convertMultimodalAudioBlock(\n block: MediaContentBlock\n): BedrockContentBlock {\n return {\n audio: {\n format: getMediaFormat(block.mimeType, mimeTypeToAudioFormat),\n source: resolveMediaSource(block) as AudioSource,\n },\n } as BedrockContentBlock;\n}\n\nfunction getDocumentName(block: Data.StandardFileBlock): string {\n return (\n (block.metadata?.name as string | undefined) ??\n (block.metadata?.filename as string | undefined) ??\n (block.metadata?.title as string | undefined) ??\n globalThis.crypto.randomUUID().replace(/-/g, '').slice(0, 12)\n );\n}\n\nfunction getDocumentFormat(\n mimeType: string | undefined\n): DocumentFormat | undefined {\n if (mimeType == null || mimeType === '') {\n return undefined;\n }\n const parsedMimeType = parseMimeType(mimeType);\n const format =\n mimeTypeToDocumentFormat[\n `${parsedMimeType.type}/${parsedMimeType.subtype}`\n ];\n if (format === undefined) {\n throw new Error(\n `Unsupported file mime type: \"${mimeType}\" ChatBedrockConverse only supports ${Object.keys(\n mimeTypeToDocumentFormat\n ).join(', ')} formats.`\n );\n }\n return format;\n}\n\nconst standardContentBlockConverter: StandardContentBlockConverter<{\n text: BedrockContentBlock;\n image: BedrockContentBlock;\n file: BedrockContentBlock;\n}> = {\n providerName: 'ChatBedrockConverse',\n\n fromStandardTextBlock(block: Data.StandardTextBlock): BedrockContentBlock {\n return { text: block.text };\n },\n\n fromStandardImageBlock(block: Data.StandardImageBlock): BedrockContentBlock {\n if (block.source_type === 'url') {\n const parsedData = parseBase64DataUrl({\n dataUrl: block.url,\n asTypedArray: true,\n });\n if (parsedData == null) {\n throw new Error(\n [\n 'Only base64 data URLs are supported for image blocks with source type ',\n 'url',\n ' with ChatBedrockConverse.',\n ].join(String.fromCharCode(39))\n );\n }\n return {\n image: {\n format: parseMimeType(parsedData.mime_type).subtype as\n | 'gif'\n | 'jpeg'\n | 'png'\n | 'webp',\n source: { bytes: parsedData.data as Uint8Array },\n },\n };\n }\n if (block.source_type === 'base64') {\n let format: 'gif' | 'jpeg' | 'png' | 'webp' | undefined;\n if (block.mime_type != null && block.mime_type !== '') {\n format = parseMimeType(block.mime_type).subtype as typeof format;\n }\n if (format != null && !['gif', 'jpeg', 'png', 'webp'].includes(format)) {\n throw new Error(\n `Unsupported image mime type: \"${block.mime_type}\" ChatBedrockConverse only supports \"image/gif\", \"image/jpeg\", \"image/png\", and \"image/webp\" formats.`\n );\n }\n return {\n image: {\n format,\n source: { bytes: base64ToBytes(block.data) },\n },\n };\n }\n throw new Error(\n `Image source type '${block.source_type}' not supported with ChatBedrockConverse.`\n );\n },\n\n fromStandardFileBlock(block: Data.StandardFileBlock): BedrockContentBlock {\n const name = getDocumentName(block);\n if (block.source_type === 'text') {\n return {\n document: {\n name,\n format: 'txt',\n source: { bytes: new TextEncoder().encode(block.text) },\n },\n } as BedrockContentBlock;\n }\n if (block.source_type === 'url') {\n const parsedData = parseBase64DataUrl({\n dataUrl: block.url,\n asTypedArray: true,\n });\n if (parsedData == null) {\n throw new Error(\n [\n 'Only base64 data URLs are supported for file blocks with source type ',\n 'url',\n ' with ChatBedrockConverse.',\n ].join(String.fromCharCode(39))\n );\n }\n return {\n document: {\n name,\n format: getDocumentFormat(parsedData.mime_type),\n source: { bytes: parsedData.data as Uint8Array } as DocumentSource,\n },\n } as BedrockContentBlock;\n }\n if (block.source_type === 'base64') {\n return {\n document: {\n name,\n format: getDocumentFormat(block.mime_type),\n source: { bytes: base64ToBytes(block.data) } as DocumentSource,\n },\n } as BedrockContentBlock;\n }\n throw new Error(\n `File source type '${block.source_type}' not supported with ChatBedrockConverse.`\n );\n },\n};\n\ntype BedrockPromptCacheTtl = '5m' | '1h';\ntype NormalizedBedrockCachePoint = {\n type: 'default';\n ttl?: BedrockPromptCacheTtl;\n};\n\n/**\n * Check if a block has a default cache point and return its normalized form,\n * preserving an optional extended-TTL `ttl` (`'5m'` | `'1h'`). Returns\n * `undefined` when the block is not a default cache point.\n */\nfunction getDefaultCachePoint(\n block: unknown\n): NormalizedBedrockCachePoint | undefined {\n if (typeof block !== 'object' || block === null) {\n return undefined;\n }\n if (!('cachePoint' in block)) {\n return undefined;\n }\n const cachePoint = (block as { cachePoint?: unknown }).cachePoint;\n if (typeof cachePoint !== 'object' || cachePoint === null) {\n return undefined;\n }\n if (!('type' in cachePoint)) {\n return undefined;\n }\n if ((cachePoint as { type?: string }).type !== 'default') {\n return undefined;\n }\n const ttl = (cachePoint as { ttl?: unknown }).ttl;\n return ttl === '5m' || ttl === '1h'\n ? { type: 'default', ttl }\n : { type: 'default' };\n}\n\n/**\n * Convert a LangChain content block to a Bedrock Converse content block.\n */\nfunction convertLangChainContentBlockToConverseContentBlock({\n block,\n onUnknown = 'throw',\n}: {\n block: string | MessageContentComplex;\n onUnknown?: 'throw' | 'passthrough';\n}): BedrockContentBlock {\n if (typeof block === 'string') {\n return { text: block };\n }\n\n if (isDataContentBlock(block)) {\n return convertToProviderContentBlock(block, standardContentBlockConverter);\n }\n\n if (block.type === 'text') {\n return { text: (block as { text: string }).text };\n }\n\n if (block.type === 'image_url') {\n const imageUrl =\n typeof (block as { image_url: string | { url: string } }).image_url ===\n 'string'\n ? (block as { image_url: string }).image_url\n : (block as { image_url: { url: string } }).image_url.url;\n return extractImageInfo(imageUrl);\n }\n\n if (block.type === 'image') {\n // Handle standard image block format\n const imageBlock = block as {\n source_type?: string;\n url?: string;\n data?: string;\n mime_type?: string;\n };\n if (\n imageBlock.source_type === 'url' &&\n imageBlock.url != null &&\n imageBlock.url !== ''\n ) {\n const parsedData = parseBase64DataUrl({\n dataUrl: imageBlock.url,\n asTypedArray: true,\n });\n if (parsedData != null) {\n const parsedMimeType = parseMimeType(parsedData.mime_type);\n return {\n image: {\n format: parsedMimeType.subtype as 'gif' | 'jpeg' | 'png' | 'webp',\n source: {\n bytes: parsedData.data as Uint8Array,\n },\n },\n };\n }\n } else if (\n imageBlock.source_type === 'base64' &&\n imageBlock.data != null &&\n imageBlock.data !== ''\n ) {\n let format: 'gif' | 'jpeg' | 'png' | 'webp' | undefined;\n if (imageBlock.mime_type != null && imageBlock.mime_type !== '') {\n const parsedMimeType = parseMimeType(imageBlock.mime_type);\n format = parsedMimeType.subtype as typeof format;\n }\n return {\n image: {\n format,\n source: {\n bytes: Uint8Array.from(atob(imageBlock.data), (c) =>\n c.charCodeAt(0)\n ),\n },\n },\n };\n }\n // If it already has the Bedrock image structure, pass through\n if ((block as { image?: unknown }).image !== undefined) {\n return {\n image: (block as { image: unknown }).image,\n } as BedrockContentBlock;\n }\n }\n\n if (\n block.type === 'video' &&\n (block as { video?: unknown }).video !== undefined\n ) {\n return {\n video: (block as { video: unknown }).video,\n } as BedrockContentBlock;\n }\n\n if (block.type === 'video') {\n return convertMultimodalVideoBlock(block as MediaContentBlock);\n }\n\n if (\n block.type === 'audio' &&\n (block as { audio?: unknown }).audio !== undefined\n ) {\n return {\n audio: (block as { audio: unknown }).audio,\n } as BedrockContentBlock;\n }\n\n if (block.type === 'audio') {\n return convertMultimodalAudioBlock(block as MediaContentBlock);\n }\n\n if (\n block.type === 'document' &&\n (block as { document?: unknown }).document !== undefined\n ) {\n return {\n document: (block as { document: unknown }).document,\n } as BedrockContentBlock;\n }\n\n const cachePoint = getDefaultCachePoint(block);\n if (cachePoint != null) {\n return {\n cachePoint,\n } as BedrockContentBlock;\n }\n\n if (onUnknown === 'throw') {\n throw new Error(`Unsupported content block type: ${block.type}`);\n } else {\n return block as unknown as BedrockContentBlock;\n }\n}\n\n/**\n * Convert a system message to Bedrock system content blocks.\n */\nfunction convertSystemMessageToConverseMessage(\n msg: BaseMessage\n): BedrockSystemContentBlock[] {\n if (typeof msg.content === 'string') {\n return [{ text: msg.content }];\n } else if (Array.isArray(msg.content) && msg.content.length > 0) {\n const contentBlocks: BedrockSystemContentBlock[] = [];\n for (const block of msg.content) {\n if (\n typeof block === 'object' &&\n block.type === 'text' &&\n typeof (block as { text?: string }).text === 'string'\n ) {\n contentBlocks.push({\n text: (block as { text: string }).text,\n });\n } else {\n const cachePoint = getDefaultCachePoint(block);\n if (cachePoint == null) {\n break;\n }\n contentBlocks.push({\n cachePoint,\n } as BedrockSystemContentBlock);\n }\n }\n if (msg.content.length === contentBlocks.length) {\n return contentBlocks;\n }\n }\n throw new Error(\n 'System message content must be either a string, or an array of text blocks, optionally including a cache point.'\n );\n}\n\n/**\n * Convert an AI message to a Bedrock message.\n */\nfunction convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {\n // Check for v1 format from other providers (PR #9766 fix)\n const responseMetadata = msg.response_metadata as\n | { output_version?: string }\n | undefined;\n if (responseMetadata?.output_version === 'v1') {\n return convertFromV1ToChatBedrockConverseMessage(msg);\n }\n\n const assistantMsg: BedrockMessage = {\n role: 'assistant',\n content: [],\n };\n\n if (typeof msg.content === 'string' && msg.content !== '') {\n assistantMsg.content?.push({ text: msg.content });\n } else if (Array.isArray(msg.content)) {\n const concatenatedBlocks = concatenateLangchainReasoningBlocks(\n msg.content as Array<MessageContentComplex | MessageContentReasoningBlock>\n );\n const contentBlocks: BedrockContentBlock[] = [];\n\n concatenatedBlocks.forEach((block) => {\n if (block.type === 'text') {\n const text = (block as { text?: string }).text ?? '';\n // Skip completely empty text blocks (common in AI messages with tool_use blocks)\n if (text === '') {\n return;\n }\n // Merge whitespace/newlines with previous text blocks to avoid validation errors.\n const cleanedText = text.replace(/\\n/g, '').trim();\n if (cleanedText === '') {\n if (contentBlocks.length > 0) {\n const lastBlock = contentBlocks[contentBlocks.length - 1];\n if ('text' in lastBlock) {\n const mergedTextContent = `${lastBlock.text}${text}`;\n (lastBlock as { text: string }).text = mergedTextContent;\n }\n }\n } else {\n contentBlocks.push({ text });\n }\n } else if (block.type === 'reasoning_content') {\n const reasoningBlock = block as MessageContentReasoningBlock;\n // Bedrock Converse rejects reasoningContent whose reasoningText.text is\n // null/empty (a signature-only block that never merged with its text).\n // Drop it rather than emit an invalid request; the empty-turn\n // placeholder below covers a turn left with no content.\n if (!isSerializableBedrockReasoningBlock(reasoningBlock)) {\n return;\n }\n contentBlocks.push({\n reasoningContent:\n langchainReasoningBlockToBedrockReasoningBlock(reasoningBlock),\n } as BedrockContentBlock);\n } else {\n const cachePoint = getDefaultCachePoint(block);\n if (cachePoint != null) {\n contentBlocks.push({\n cachePoint,\n } as BedrockContentBlock);\n } else if (FOREIGN_REASONING_TYPES.some((t) => t === block.type)) {\n // Reasoning from another provider (Anthropic `thinking`/\n // `redacted_thinking`, Google `reasoning`, LibreChat `think`).\n // Bedrock's native reasoning is `reasoning_content` (handled above); a\n // foreign block carries a signature Bedrock cannot validate, so drop\n // it on a cross-provider handoff (e.g. Anthropic → Bedrock) rather\n // than crash. The Bedrock model produces its own reasoning. Anything\n // else unknown still throws below — real content must be surfaced.\n return;\n } else {\n const blockValues = Object.fromEntries(\n Object.entries(block).filter(([key]) => key !== 'type')\n );\n throw new Error(\n `Unsupported content block type: ${block.type} with content of ${JSON.stringify(blockValues, null, 2)}`\n );\n }\n }\n });\n\n assistantMsg.content = [...(assistantMsg.content ?? []), ...contentBlocks];\n }\n\n // Important: this must be placed after any reasoning content blocks\n if (isAIMessage(msg) && msg.tool_calls != null && msg.tool_calls.length > 0) {\n const toolUseBlocks = msg.tool_calls.map((tc) => ({\n toolUse: {\n toolUseId: tc.id,\n name: tc.name,\n input: tc.args as Record<string, unknown>,\n },\n }));\n assistantMsg.content = [\n ...(assistantMsg.content ?? []),\n ...toolUseBlocks,\n ] as BedrockContentBlock[];\n }\n\n // Bedrock rejects an assistant message with no content blocks; if filtering\n // (e.g. dropping foreign reasoning) left it empty, emit a placeholder.\n if (assistantMsg.content == null || assistantMsg.content.length === 0) {\n assistantMsg.content = [{ text: BEDROCK_EMPTY_TEXT_PLACEHOLDER }];\n }\n\n return assistantMsg;\n}\n\n/**\n * Convert a v1 format message from other providers to Bedrock format.\n * This handles messages with standard content blocks like tool_call and reasoning.\n * (Implements PR #9766 fix for output_version v1 detection)\n */\nfunction convertFromV1ToChatBedrockConverseMessage(\n msg: BaseMessage\n): BedrockMessage {\n const assistantMsg: BedrockMessage = {\n role: 'assistant',\n content: [],\n };\n\n if (Array.isArray(msg.content)) {\n for (const block of msg.content as Array<\n MessageContentComplex | MessageContentReasoningBlock\n >) {\n if (typeof block === 'string') {\n assistantMsg.content?.push({ text: block });\n } else if (block.type === 'text') {\n assistantMsg.content?.push({ text: (block as { text: string }).text });\n } else if (block.type === 'tool_call') {\n const toolCall = block as {\n id: string;\n name: string;\n args: Record<string, unknown>;\n };\n assistantMsg.content?.push({\n toolUse: {\n toolUseId: toolCall.id,\n name: toolCall.name,\n input: toolCall.args as Record<string, unknown>,\n },\n } as BedrockContentBlock);\n } else if (block.type === 'reasoning') {\n const reasoning = block as { reasoning: string };\n assistantMsg.content?.push({\n reasoningContent: {\n reasoningText: { text: reasoning.reasoning },\n },\n } as BedrockContentBlock);\n } else if (block.type === 'reasoning_content') {\n assistantMsg.content?.push({\n reasoningContent: langchainReasoningBlockToBedrockReasoningBlock(\n block as MessageContentReasoningBlock\n ),\n } as BedrockContentBlock);\n }\n }\n } else if (typeof msg.content === 'string' && msg.content !== '') {\n assistantMsg.content?.push({ text: msg.content });\n }\n\n // Also handle tool_calls from the message\n if (isAIMessage(msg) && msg.tool_calls != null && msg.tool_calls.length > 0) {\n // Check if tool calls are already in content\n const existingToolUseIds = new Set(\n assistantMsg.content\n ?.filter((c) => 'toolUse' in c)\n .map(\n (c) => (c as { toolUse: { toolUseId: string } }).toolUse.toolUseId\n ) ?? []\n );\n\n for (const tc of msg.tool_calls) {\n if (!existingToolUseIds.has(tc.id ?? '')) {\n assistantMsg.content?.push({\n toolUse: {\n toolUseId: tc.id,\n name: tc.name,\n input: tc.args as Record<string, unknown>,\n },\n } as BedrockContentBlock);\n }\n }\n }\n\n return assistantMsg;\n}\n\n/**\n * Convert a human message to a Bedrock message.\n */\nfunction convertHumanMessageToConverseMessage(\n msg: BaseMessage\n): BedrockMessage {\n const userMessage: BedrockMessage = {\n role: 'user',\n content: [],\n };\n\n if (typeof msg.content === 'string') {\n userMessage.content = [{ text: msg.content }];\n } else if (Array.isArray(msg.content)) {\n userMessage.content = msg.content.map((block) =>\n convertLangChainContentBlockToConverseContentBlock({ block })\n );\n }\n\n return userMessage;\n}\n\n/**\n * Convert a tool message to a Bedrock message.\n */\nfunction convertToolMessageToConverseMessage(msg: BaseMessage): BedrockMessage {\n const toolCallId = (msg as { tool_call_id?: string }).tool_call_id;\n\n let content: BedrockContentBlock[];\n if (typeof msg.content === 'string') {\n content = [{ text: msg.content }];\n } else if (Array.isArray(msg.content)) {\n content = msg.content.map((block) =>\n convertLangChainContentBlockToConverseContentBlock({\n block,\n onUnknown: 'passthrough',\n })\n );\n } else {\n content = [{ text: String(msg.content) }];\n }\n\n // A `cachePoint` is a message-level ContentBlock — it is NOT a valid\n // ToolResultContentBlock. A tail prompt-cache breakpoint that anchors on a\n // tool result therefore ends up nested inside `toolResult.content`, which\n // Bedrock silently ignores (no cache write, no cache read). Hoist any\n // cachePoint(s) out of the tool result body so they sit as siblings after\n // it, which is the only position Bedrock honors.\n const toolResultContent: BedrockContentBlock[] = [];\n const trailingCachePoints: BedrockContentBlock[] = [];\n for (const block of content) {\n const cachePoint = getDefaultCachePoint(block);\n if (cachePoint != null) {\n trailingCachePoints.push({\n cachePoint,\n } as BedrockContentBlock);\n } else {\n toolResultContent.push(block);\n }\n }\n\n return {\n role: 'user',\n content: [\n {\n toolResult: {\n toolUseId: toolCallId,\n content: toolResultContent as { text: string }[],\n },\n },\n ...trailingCachePoints,\n ],\n };\n}\n\n/**\n * Convert LangChain messages to Bedrock Converse messages.\n */\nexport function convertToConverseMessages(messages: BaseMessage[]): {\n converseMessages: BedrockMessage[];\n converseSystem: BedrockSystemContentBlock[];\n} {\n const converseSystem = messages\n .filter((msg) => msg._getType() === 'system')\n .flatMap((msg) => convertSystemMessageToConverseMessage(msg));\n\n const converseMessages = messages\n .filter((msg) => msg._getType() !== 'system')\n .map((msg) => {\n if (msg._getType() === 'ai') {\n return convertAIMessageToConverseMessage(msg);\n } else if (msg._getType() === 'human' || msg._getType() === 'generic') {\n return convertHumanMessageToConverseMessage(msg);\n } else if (msg._getType() === 'tool') {\n return convertToolMessageToConverseMessage(msg);\n } else {\n throw new Error(`Unsupported message type: ${msg._getType()}`);\n }\n });\n\n // Combine consecutive user tool result messages into a single message\n const combinedConverseMessages = converseMessages.reduce<BedrockMessage[]>(\n (acc, curr) => {\n if (acc.length === 0) {\n acc.push(curr);\n return acc;\n }\n const lastMessage = acc[acc.length - 1];\n const lastHasToolResult =\n lastMessage.content?.some((c) => 'toolResult' in c) === true;\n const currHasToolResult =\n curr.content?.some((c) => 'toolResult' in c) === true;\n if (\n lastMessage.role === 'user' &&\n lastHasToolResult &&\n curr.role === 'user' &&\n currHasToolResult\n ) {\n lastMessage.content = lastMessage.content?.concat(curr.content ?? []);\n } else {\n acc.push(curr);\n }\n return acc;\n },\n []\n );\n\n return { converseMessages: combinedConverseMessages, converseSystem };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"message_inputs.mjs","names":[],"sources":["../../../../../src/llm/bedrock/utils/message_inputs.ts"],"sourcesContent":["/**\n * Utility functions for converting LangChain messages to Bedrock Converse messages.\n * Ported from @langchain/aws common.js\n */\nimport {\n type BaseMessage,\n isAIMessage,\n type Data,\n parseBase64DataUrl,\n parseMimeType,\n MessageContentComplex,\n type StandardContentBlockConverter,\n convertToProviderContentBlock,\n isDataContentBlock,\n} from '@langchain/core/messages';\nimport type {\n AudioFormat,\n AudioSource,\n DocumentFormat,\n DocumentSource,\n VideoFormat,\n VideoSource,\n} from '@aws-sdk/client-bedrock-runtime';\nimport type {\n BedrockMessage,\n BedrockSystemContentBlock,\n BedrockContentBlock,\n MessageContentReasoningBlock,\n} from '../types';\n\n/**\n * Reasoning blocks from other providers, relative to Bedrock. Bedrock's native\n * reasoning format is `reasoning_content`; these carry provider-specific\n * signatures Bedrock cannot validate, so they are dropped on a cross-provider\n * handoff (e.g. Anthropic → Bedrock) rather than crashing the conversion.\n */\nconst FOREIGN_REASONING_TYPES = [\n 'thinking',\n 'redacted_thinking',\n 'reasoning',\n 'think',\n];\n\n/**\n * Bedrock Converse rejects assistant messages with no content blocks. When\n * filtering (e.g. dropping foreign reasoning) empties an assistant turn that\n * also has no tool calls, fall back to this placeholder text.\n */\nconst BEDROCK_EMPTY_TEXT_PLACEHOLDER = '_';\n\n/**\n * Convert a LangChain reasoning block to a Bedrock reasoning block.\n */\nexport function langchainReasoningBlockToBedrockReasoningBlock(\n content: MessageContentReasoningBlock\n): {\n reasoningText?: { text?: string; signature?: string };\n redactedContent?: Uint8Array;\n} {\n if (content.reasoningText != null) {\n return {\n reasoningText: content.reasoningText,\n };\n }\n if (content.redactedContent != null && content.redactedContent !== '') {\n return {\n redactedContent: new Uint8Array(\n Buffer.from(content.redactedContent, 'base64')\n ),\n };\n }\n throw new Error('Invalid reasoning content');\n}\n\n/**\n * Whether a reasoning block can be serialized to a valid Bedrock\n * `reasoningContent`. Bedrock Converse rejects `reasoningText` with a null/empty\n * `text` (e.g. a signature-only block that never merged with its text), so such\n * blocks must be dropped rather than sent.\n */\nfunction isSerializableBedrockReasoningBlock(\n content: MessageContentReasoningBlock\n): boolean {\n if (content.reasoningText != null) {\n const text = content.reasoningText.text;\n return text != null && text !== '';\n }\n return content.redactedContent != null && content.redactedContent !== '';\n}\n\nfunction appendSerializableBedrockTextBlock(\n contentBlocks: BedrockContentBlock[],\n text: string\n): boolean {\n if (text === '') {\n return false;\n }\n const cleanedText = text.replace(/\\n/g, '').trim();\n if (cleanedText !== '') {\n contentBlocks.push({ text });\n return true;\n }\n const lastBlock = contentBlocks[contentBlocks.length - 1] as\n | BedrockContentBlock\n | undefined;\n if (lastBlock == null || !('text' in lastBlock)) {\n return false;\n }\n const mergedTextContent = `${lastBlock.text}${text}`;\n (lastBlock as { text: string }).text = mergedTextContent;\n return true;\n}\n\n/**\n * Concatenate consecutive reasoning blocks in content array.\n */\nexport function concatenateLangchainReasoningBlocks(\n content: Array<MessageContentComplex | MessageContentReasoningBlock>\n): Array<MessageContentComplex | MessageContentReasoningBlock> {\n const result: Array<MessageContentComplex | MessageContentReasoningBlock> =\n [];\n let pendingReasoning: MessageContentReasoningBlock | undefined;\n\n const flushPendingReasoning = (): void => {\n if (pendingReasoning != null) {\n result.push(pendingReasoning);\n pendingReasoning = undefined;\n }\n };\n\n for (const block of content) {\n if (block.type !== 'reasoning_content') {\n flushPendingReasoning();\n result.push(block);\n continue;\n }\n\n const currentReasoning = block as MessageContentReasoningBlock;\n if (currentReasoning.reasoningText != null) {\n const previousText = pendingReasoning?.reasoningText?.text;\n const previousSignature = pendingReasoning?.reasoningText?.signature;\n const { text, signature } = currentReasoning.reasoningText;\n const mergedReasoningText: { text?: string; signature?: string } = {};\n if (previousText !== undefined || text !== undefined) {\n mergedReasoningText.text = (previousText ?? '') + (text ?? '');\n }\n if (previousSignature !== undefined || signature !== undefined) {\n mergedReasoningText.signature =\n (previousSignature ?? '') + (signature ?? '');\n }\n pendingReasoning = {\n type: 'reasoning_content',\n reasoningText: mergedReasoningText,\n };\n\n // A signature seals the accumulated reasoning text. Any following\n // reasoning block starts a new independently signed Bedrock block.\n if ('signature' in currentReasoning.reasoningText) {\n flushPendingReasoning();\n }\n }\n\n if (currentReasoning.redactedContent != null) {\n flushPendingReasoning();\n result.push({\n type: 'reasoning_content',\n redactedContent: currentReasoning.redactedContent,\n });\n }\n }\n\n flushPendingReasoning();\n return result;\n}\n\n/**\n * Extract image info from a base64 string or URL.\n */\nexport function extractImageInfo(base64: string): BedrockContentBlock {\n // Extract the format from the base64 string\n const formatMatch = base64.match(/^data:image\\/(\\w+);base64,/);\n let format: 'gif' | 'jpeg' | 'png' | 'webp' | undefined;\n if (formatMatch) {\n const extractedFormat = formatMatch[1].toLowerCase();\n if (['gif', 'jpeg', 'png', 'webp'].includes(extractedFormat)) {\n format = extractedFormat as typeof format;\n }\n }\n\n // Remove the data URL prefix if present\n const base64Data = base64.replace(/^data:image\\/\\w+;base64,/, '');\n\n // Convert base64 to Uint8Array\n const binaryString = atob(base64Data);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i += 1) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n\n return {\n image: {\n format,\n source: {\n bytes,\n },\n },\n };\n}\n\ntype MediaContentBlock = MessageContentComplex & {\n data?: string | Uint8Array;\n url?: string;\n fileId?: string;\n mimeType?: string;\n};\n\nconst mimeTypeToVideoFormat: Record<string, VideoFormat> = {\n 'video/flv': 'flv',\n 'video/mkv': 'mkv',\n 'video/mov': 'mov',\n 'video/mp4': 'mp4',\n 'video/mpeg': 'mpeg',\n 'video/mpg': 'mpg',\n 'video/three_gp': 'three_gp',\n 'video/webm': 'webm',\n 'video/wmv': 'wmv',\n};\n\nconst mimeTypeToAudioFormat: Record<string, AudioFormat> = {\n 'audio/aac': 'aac',\n 'audio/flac': 'flac',\n 'audio/m4a': 'm4a',\n 'audio/mka': 'mka',\n 'audio/mkv': 'mkv',\n 'audio/mp3': 'mp3',\n 'audio/mp4': 'mp4',\n 'audio/mpeg': 'mpeg',\n 'audio/mpga': 'mpga',\n 'audio/ogg': 'ogg',\n 'audio/opus': 'opus',\n 'audio/pcm': 'pcm',\n 'audio/wav': 'wav',\n 'audio/webm': 'webm',\n 'audio/x-aac': 'x-aac',\n};\n\nconst mimeTypeToDocumentFormat: Partial<Record<string, DocumentFormat>> = {\n 'text/csv': 'csv',\n 'application/msword': 'doc',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':\n 'docx',\n 'text/html': 'html',\n 'text/markdown': 'md',\n 'application/pdf': 'pdf',\n 'text/plain': 'txt',\n 'application/vnd.ms-excel': 'xls',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',\n};\n\nfunction base64ToBytes(data: string): Uint8Array {\n return Uint8Array.from(atob(data), (char) => char.charCodeAt(0));\n}\n\nfunction getMediaFormat<T extends string>(\n mimeType: string | undefined,\n formatMap: Record<string, T>\n): T | undefined {\n if (mimeType == null || mimeType === '') {\n return undefined;\n }\n return formatMap[mimeType] ?? (parseMimeType(mimeType).subtype as T);\n}\n\nfunction resolveMediaSource(\n block: MediaContentBlock\n): AudioSource | VideoSource {\n if (typeof block.data === 'string') {\n return { bytes: base64ToBytes(block.data) };\n }\n if (block.data instanceof Uint8Array) {\n return { bytes: block.data };\n }\n if (typeof block.url === 'string') {\n const parsedData = parseBase64DataUrl({\n dataUrl: block.url,\n asTypedArray: true,\n });\n if (parsedData != null) {\n return { bytes: parsedData.data as Uint8Array };\n }\n throw new Error(\n `Only base64 data URLs are supported for ${block.type} blocks with 'url' field with ChatBedrockConverse.`\n );\n }\n if (typeof block.fileId === 'string') {\n return { s3Location: { uri: block.fileId } };\n }\n throw new Error(\n `${block.type} block must include one of: 'data' (base64 string or Uint8Array), 'url' (base64 data URL), or 'fileId' (S3 URI).`\n );\n}\n\nfunction convertMultimodalVideoBlock(\n block: MediaContentBlock\n): BedrockContentBlock {\n return {\n video: {\n format: getMediaFormat(block.mimeType, mimeTypeToVideoFormat),\n source: resolveMediaSource(block) as VideoSource,\n },\n } as BedrockContentBlock;\n}\n\nfunction convertMultimodalAudioBlock(\n block: MediaContentBlock\n): BedrockContentBlock {\n return {\n audio: {\n format: getMediaFormat(block.mimeType, mimeTypeToAudioFormat),\n source: resolveMediaSource(block) as AudioSource,\n },\n } as BedrockContentBlock;\n}\n\nfunction getDocumentName(block: Data.StandardFileBlock): string {\n return (\n (block.metadata?.name as string | undefined) ??\n (block.metadata?.filename as string | undefined) ??\n (block.metadata?.title as string | undefined) ??\n globalThis.crypto.randomUUID().replace(/-/g, '').slice(0, 12)\n );\n}\n\nfunction getDocumentFormat(\n mimeType: string | undefined\n): DocumentFormat | undefined {\n if (mimeType == null || mimeType === '') {\n return undefined;\n }\n const parsedMimeType = parseMimeType(mimeType);\n const format =\n mimeTypeToDocumentFormat[\n `${parsedMimeType.type}/${parsedMimeType.subtype}`\n ];\n if (format === undefined) {\n throw new Error(\n `Unsupported file mime type: \"${mimeType}\" ChatBedrockConverse only supports ${Object.keys(\n mimeTypeToDocumentFormat\n ).join(', ')} formats.`\n );\n }\n return format;\n}\n\nconst standardContentBlockConverter: StandardContentBlockConverter<{\n text: BedrockContentBlock;\n image: BedrockContentBlock;\n file: BedrockContentBlock;\n}> = {\n providerName: 'ChatBedrockConverse',\n\n fromStandardTextBlock(block: Data.StandardTextBlock): BedrockContentBlock {\n return { text: block.text };\n },\n\n fromStandardImageBlock(block: Data.StandardImageBlock): BedrockContentBlock {\n if (block.source_type === 'url') {\n const parsedData = parseBase64DataUrl({\n dataUrl: block.url,\n asTypedArray: true,\n });\n if (parsedData == null) {\n throw new Error(\n [\n 'Only base64 data URLs are supported for image blocks with source type ',\n 'url',\n ' with ChatBedrockConverse.',\n ].join(String.fromCharCode(39))\n );\n }\n return {\n image: {\n format: parseMimeType(parsedData.mime_type).subtype as\n | 'gif'\n | 'jpeg'\n | 'png'\n | 'webp',\n source: { bytes: parsedData.data as Uint8Array },\n },\n };\n }\n if (block.source_type === 'base64') {\n let format: 'gif' | 'jpeg' | 'png' | 'webp' | undefined;\n if (block.mime_type != null && block.mime_type !== '') {\n format = parseMimeType(block.mime_type).subtype as typeof format;\n }\n if (format != null && !['gif', 'jpeg', 'png', 'webp'].includes(format)) {\n throw new Error(\n `Unsupported image mime type: \"${block.mime_type}\" ChatBedrockConverse only supports \"image/gif\", \"image/jpeg\", \"image/png\", and \"image/webp\" formats.`\n );\n }\n return {\n image: {\n format,\n source: { bytes: base64ToBytes(block.data) },\n },\n };\n }\n throw new Error(\n `Image source type '${block.source_type}' not supported with ChatBedrockConverse.`\n );\n },\n\n fromStandardFileBlock(block: Data.StandardFileBlock): BedrockContentBlock {\n const name = getDocumentName(block);\n if (block.source_type === 'text') {\n return {\n document: {\n name,\n format: 'txt',\n source: { bytes: new TextEncoder().encode(block.text) },\n },\n } as BedrockContentBlock;\n }\n if (block.source_type === 'url') {\n const parsedData = parseBase64DataUrl({\n dataUrl: block.url,\n asTypedArray: true,\n });\n if (parsedData == null) {\n throw new Error(\n [\n 'Only base64 data URLs are supported for file blocks with source type ',\n 'url',\n ' with ChatBedrockConverse.',\n ].join(String.fromCharCode(39))\n );\n }\n return {\n document: {\n name,\n format: getDocumentFormat(parsedData.mime_type),\n source: { bytes: parsedData.data as Uint8Array } as DocumentSource,\n },\n } as BedrockContentBlock;\n }\n if (block.source_type === 'base64') {\n return {\n document: {\n name,\n format: getDocumentFormat(block.mime_type),\n source: { bytes: base64ToBytes(block.data) } as DocumentSource,\n },\n } as BedrockContentBlock;\n }\n throw new Error(\n `File source type '${block.source_type}' not supported with ChatBedrockConverse.`\n );\n },\n};\n\ntype BedrockPromptCacheTtl = '5m' | '1h';\ntype NormalizedBedrockCachePoint = {\n type: 'default';\n ttl?: BedrockPromptCacheTtl;\n};\n\n/**\n * Check if a block has a default cache point and return its normalized form,\n * preserving an optional extended-TTL `ttl` (`'5m'` | `'1h'`). Returns\n * `undefined` when the block is not a default cache point.\n */\nfunction getDefaultCachePoint(\n block: unknown\n): NormalizedBedrockCachePoint | undefined {\n if (typeof block !== 'object' || block === null) {\n return undefined;\n }\n if (!('cachePoint' in block)) {\n return undefined;\n }\n const cachePoint = (block as { cachePoint?: unknown }).cachePoint;\n if (typeof cachePoint !== 'object' || cachePoint === null) {\n return undefined;\n }\n if (!('type' in cachePoint)) {\n return undefined;\n }\n if ((cachePoint as { type?: string }).type !== 'default') {\n return undefined;\n }\n const ttl = (cachePoint as { ttl?: unknown }).ttl;\n return ttl === '5m' || ttl === '1h'\n ? { type: 'default', ttl }\n : { type: 'default' };\n}\n\n/**\n * Convert a LangChain content block to a Bedrock Converse content block.\n */\nfunction convertLangChainContentBlockToConverseContentBlock({\n block,\n onUnknown = 'throw',\n}: {\n block: string | MessageContentComplex;\n onUnknown?: 'throw' | 'passthrough';\n}): BedrockContentBlock {\n if (typeof block === 'string') {\n return { text: block };\n }\n\n if (isDataContentBlock(block)) {\n return convertToProviderContentBlock(block, standardContentBlockConverter);\n }\n\n if (block.type === 'text') {\n return { text: (block as { text: string }).text };\n }\n\n if (block.type === 'image_url') {\n const imageUrl =\n typeof (block as { image_url: string | { url: string } }).image_url ===\n 'string'\n ? (block as { image_url: string }).image_url\n : (block as { image_url: { url: string } }).image_url.url;\n return extractImageInfo(imageUrl);\n }\n\n if (block.type === 'image') {\n // Handle standard image block format\n const imageBlock = block as {\n source_type?: string;\n url?: string;\n data?: string;\n mime_type?: string;\n };\n if (\n imageBlock.source_type === 'url' &&\n imageBlock.url != null &&\n imageBlock.url !== ''\n ) {\n const parsedData = parseBase64DataUrl({\n dataUrl: imageBlock.url,\n asTypedArray: true,\n });\n if (parsedData != null) {\n const parsedMimeType = parseMimeType(parsedData.mime_type);\n return {\n image: {\n format: parsedMimeType.subtype as 'gif' | 'jpeg' | 'png' | 'webp',\n source: {\n bytes: parsedData.data as Uint8Array,\n },\n },\n };\n }\n } else if (\n imageBlock.source_type === 'base64' &&\n imageBlock.data != null &&\n imageBlock.data !== ''\n ) {\n let format: 'gif' | 'jpeg' | 'png' | 'webp' | undefined;\n if (imageBlock.mime_type != null && imageBlock.mime_type !== '') {\n const parsedMimeType = parseMimeType(imageBlock.mime_type);\n format = parsedMimeType.subtype as typeof format;\n }\n return {\n image: {\n format,\n source: {\n bytes: Uint8Array.from(atob(imageBlock.data), (c) =>\n c.charCodeAt(0)\n ),\n },\n },\n };\n }\n // If it already has the Bedrock image structure, pass through\n if ((block as { image?: unknown }).image !== undefined) {\n return {\n image: (block as { image: unknown }).image,\n } as BedrockContentBlock;\n }\n }\n\n if (\n block.type === 'video' &&\n (block as { video?: unknown }).video !== undefined\n ) {\n return {\n video: (block as { video: unknown }).video,\n } as BedrockContentBlock;\n }\n\n if (block.type === 'video') {\n return convertMultimodalVideoBlock(block as MediaContentBlock);\n }\n\n if (\n block.type === 'audio' &&\n (block as { audio?: unknown }).audio !== undefined\n ) {\n return {\n audio: (block as { audio: unknown }).audio,\n } as BedrockContentBlock;\n }\n\n if (block.type === 'audio') {\n return convertMultimodalAudioBlock(block as MediaContentBlock);\n }\n\n if (\n block.type === 'document' &&\n (block as { document?: unknown }).document !== undefined\n ) {\n return {\n document: (block as { document: unknown }).document,\n } as BedrockContentBlock;\n }\n\n const cachePoint = getDefaultCachePoint(block);\n if (cachePoint != null) {\n return {\n cachePoint,\n } as BedrockContentBlock;\n }\n\n if (onUnknown === 'throw') {\n throw new Error(`Unsupported content block type: ${block.type}`);\n } else {\n return block as unknown as BedrockContentBlock;\n }\n}\n\n/**\n * Convert a system message to Bedrock system content blocks.\n */\nfunction convertSystemMessageToConverseMessage(\n msg: BaseMessage\n): BedrockSystemContentBlock[] {\n if (typeof msg.content === 'string') {\n return [{ text: msg.content }];\n } else if (Array.isArray(msg.content) && msg.content.length > 0) {\n const contentBlocks: BedrockSystemContentBlock[] = [];\n for (const block of msg.content) {\n if (\n typeof block === 'object' &&\n block.type === 'text' &&\n typeof (block as { text?: string }).text === 'string'\n ) {\n contentBlocks.push({\n text: (block as { text: string }).text,\n });\n } else {\n const cachePoint = getDefaultCachePoint(block);\n if (cachePoint == null) {\n break;\n }\n contentBlocks.push({\n cachePoint,\n } as BedrockSystemContentBlock);\n }\n }\n if (msg.content.length === contentBlocks.length) {\n return contentBlocks;\n }\n }\n throw new Error(\n 'System message content must be either a string, or an array of text blocks, optionally including a cache point.'\n );\n}\n\n/**\n * Convert an AI message to a Bedrock message.\n */\nfunction convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {\n // Check for v1 format from other providers (PR #9766 fix)\n const responseMetadata = msg.response_metadata as\n | { output_version?: string }\n | undefined;\n if (responseMetadata?.output_version === 'v1') {\n return convertFromV1ToChatBedrockConverseMessage(msg);\n }\n\n const assistantMsg: BedrockMessage = {\n role: 'assistant',\n content: [],\n };\n\n if (typeof msg.content === 'string' && msg.content !== '') {\n assistantMsg.content?.push({ text: msg.content });\n } else if (Array.isArray(msg.content)) {\n const concatenatedBlocks = concatenateLangchainReasoningBlocks(\n msg.content as Array<MessageContentComplex | MessageContentReasoningBlock>\n );\n const contentBlocks: BedrockContentBlock[] = [];\n\n concatenatedBlocks.forEach((block) => {\n if (block.type === 'text') {\n const text = (block as { text?: string }).text ?? '';\n appendSerializableBedrockTextBlock(contentBlocks, text);\n } else if (block.type === 'reasoning_content') {\n const reasoningBlock = block as MessageContentReasoningBlock;\n // Bedrock Converse rejects reasoningContent whose reasoningText.text is\n // null/empty (a signature-only block that never merged with its text).\n // Drop it rather than emit an invalid request; the empty-turn\n // placeholder below covers a turn left with no content.\n if (!isSerializableBedrockReasoningBlock(reasoningBlock)) {\n return;\n }\n contentBlocks.push({\n reasoningContent:\n langchainReasoningBlockToBedrockReasoningBlock(reasoningBlock),\n } as BedrockContentBlock);\n } else {\n const cachePoint = getDefaultCachePoint(block);\n if (cachePoint != null) {\n contentBlocks.push({\n cachePoint,\n } as BedrockContentBlock);\n } else if (FOREIGN_REASONING_TYPES.some((t) => t === block.type)) {\n // Reasoning from another provider (Anthropic `thinking`/\n // `redacted_thinking`, Google `reasoning`, LibreChat `think`).\n // Bedrock's native reasoning is `reasoning_content` (handled above); a\n // foreign block carries a signature Bedrock cannot validate, so drop\n // it on a cross-provider handoff (e.g. Anthropic → Bedrock) rather\n // than crash. The Bedrock model produces its own reasoning. Anything\n // else unknown still throws below — real content must be surfaced.\n return;\n } else {\n const blockValues = Object.fromEntries(\n Object.entries(block).filter(([key]) => key !== 'type')\n );\n throw new Error(\n `Unsupported content block type: ${block.type} with content of ${JSON.stringify(blockValues, null, 2)}`\n );\n }\n }\n });\n\n assistantMsg.content = [...(assistantMsg.content ?? []), ...contentBlocks];\n }\n\n // Important: this must be placed after any reasoning content blocks\n if (isAIMessage(msg) && msg.tool_calls != null && msg.tool_calls.length > 0) {\n const toolUseBlocks = msg.tool_calls.map((tc) => ({\n toolUse: {\n toolUseId: tc.id,\n name: tc.name,\n input: tc.args as Record<string, unknown>,\n },\n }));\n assistantMsg.content = [\n ...(assistantMsg.content ?? []),\n ...toolUseBlocks,\n ] as BedrockContentBlock[];\n }\n\n // Bedrock rejects an assistant message with no content blocks; if filtering\n // (e.g. dropping foreign reasoning) left it empty, emit a placeholder.\n if (assistantMsg.content == null || assistantMsg.content.length === 0) {\n assistantMsg.content = [{ text: BEDROCK_EMPTY_TEXT_PLACEHOLDER }];\n }\n\n return assistantMsg;\n}\n\n/**\n * Convert a v1 format message from other providers to Bedrock format.\n * This handles messages with standard content blocks like tool_call and reasoning.\n * (Implements PR #9766 fix for output_version v1 detection)\n */\nfunction convertFromV1ToChatBedrockConverseMessage(\n msg: BaseMessage\n): BedrockMessage {\n const contentBlocks: BedrockContentBlock[] = [];\n const assistantMsg: BedrockMessage = {\n role: 'assistant',\n content: contentBlocks,\n };\n let droppedUnserializableContent = false;\n let unconvertedContentType: string | undefined;\n\n if (Array.isArray(msg.content)) {\n const concatenatedBlocks = concatenateLangchainReasoningBlocks(\n msg.content as Array<MessageContentComplex | MessageContentReasoningBlock>\n );\n for (const block of concatenatedBlocks) {\n if (typeof block === 'string') {\n if (!appendSerializableBedrockTextBlock(contentBlocks, block)) {\n droppedUnserializableContent = true;\n }\n } else if (block.type === 'text') {\n const text = (block as { text?: string }).text ?? '';\n if (!appendSerializableBedrockTextBlock(contentBlocks, text)) {\n droppedUnserializableContent = true;\n }\n } else if (block.type === 'tool_call') {\n const toolCall = block as {\n id: string;\n name: string;\n args: Record<string, unknown>;\n };\n contentBlocks.push({\n toolUse: {\n toolUseId: toolCall.id,\n name: toolCall.name,\n input: toolCall.args as Record<string, unknown>,\n },\n } as BedrockContentBlock);\n } else if (block.type === 'reasoning') {\n const reasoning = block as { reasoning?: string };\n /** Bedrock Converse rejects `reasoningText` with a null/empty `text`\n * (`Member must not be null`), e.g. when the producing model omitted\n * reasoning text (`thinking.display: \"omitted\"`) — drop rather than send. */\n if (reasoning.reasoning == null || reasoning.reasoning === '') {\n droppedUnserializableContent = true;\n continue;\n }\n contentBlocks.push({\n reasoningContent: {\n reasoningText: { text: reasoning.reasoning },\n },\n } as BedrockContentBlock);\n } else if (block.type === 'reasoning_content') {\n const reasoningBlock = block as MessageContentReasoningBlock;\n if (!isSerializableBedrockReasoningBlock(reasoningBlock)) {\n droppedUnserializableContent = true;\n continue;\n }\n contentBlocks.push({\n reasoningContent:\n langchainReasoningBlockToBedrockReasoningBlock(reasoningBlock),\n } as BedrockContentBlock);\n } else {\n unconvertedContentType ??= block.type;\n }\n }\n } else if (typeof msg.content === 'string') {\n if (!appendSerializableBedrockTextBlock(contentBlocks, msg.content)) {\n droppedUnserializableContent = true;\n }\n }\n\n // Also handle tool_calls from the message\n if (isAIMessage(msg) && msg.tool_calls != null && msg.tool_calls.length > 0) {\n // Check if tool calls are already in content\n const existingToolUseIds = new Set(\n contentBlocks\n .filter((c) => 'toolUse' in c)\n .map((c) => (c as { toolUse: { toolUseId: string } }).toolUse.toolUseId)\n );\n\n for (const tc of msg.tool_calls) {\n if (!existingToolUseIds.has(tc.id ?? '')) {\n contentBlocks.push({\n toolUse: {\n toolUseId: tc.id,\n name: tc.name,\n input: tc.args as Record<string, unknown>,\n },\n } as BedrockContentBlock);\n }\n }\n }\n\n const hasNoContent = contentBlocks.length === 0;\n if (hasNoContent && unconvertedContentType != null) {\n throw new Error(\n `Unsupported v1 content block type: ${unconvertedContentType}`\n );\n }\n if (hasNoContent && droppedUnserializableContent) {\n contentBlocks.push({ text: BEDROCK_EMPTY_TEXT_PLACEHOLDER });\n }\n\n return assistantMsg;\n}\n\n/**\n * Convert a human message to a Bedrock message.\n */\nfunction convertHumanMessageToConverseMessage(\n msg: BaseMessage\n): BedrockMessage {\n const userMessage: BedrockMessage = {\n role: 'user',\n content: [],\n };\n\n if (typeof msg.content === 'string') {\n userMessage.content = [{ text: msg.content }];\n } else if (Array.isArray(msg.content)) {\n userMessage.content = msg.content.map((block) =>\n convertLangChainContentBlockToConverseContentBlock({ block })\n );\n }\n\n return userMessage;\n}\n\n/**\n * Convert a tool message to a Bedrock message.\n */\nfunction convertToolMessageToConverseMessage(msg: BaseMessage): BedrockMessage {\n const toolCallId = (msg as { tool_call_id?: string }).tool_call_id;\n\n let content: BedrockContentBlock[];\n if (typeof msg.content === 'string') {\n content = [{ text: msg.content }];\n } else if (Array.isArray(msg.content)) {\n content = msg.content.map((block) =>\n convertLangChainContentBlockToConverseContentBlock({\n block,\n onUnknown: 'passthrough',\n })\n );\n } else {\n content = [{ text: String(msg.content) }];\n }\n\n // A `cachePoint` is a message-level ContentBlock — it is NOT a valid\n // ToolResultContentBlock. A tail prompt-cache breakpoint that anchors on a\n // tool result therefore ends up nested inside `toolResult.content`, which\n // Bedrock silently ignores (no cache write, no cache read). Hoist any\n // cachePoint(s) out of the tool result body so they sit as siblings after\n // it, which is the only position Bedrock honors.\n const toolResultContent: BedrockContentBlock[] = [];\n const trailingCachePoints: BedrockContentBlock[] = [];\n for (const block of content) {\n const cachePoint = getDefaultCachePoint(block);\n if (cachePoint != null) {\n trailingCachePoints.push({\n cachePoint,\n } as BedrockContentBlock);\n } else {\n toolResultContent.push(block);\n }\n }\n\n return {\n role: 'user',\n content: [\n {\n toolResult: {\n toolUseId: toolCallId,\n content: toolResultContent as { text: string }[],\n },\n },\n ...trailingCachePoints,\n ],\n };\n}\n\n/**\n * Convert LangChain messages to Bedrock Converse messages.\n */\nexport function convertToConverseMessages(messages: BaseMessage[]): {\n converseMessages: BedrockMessage[];\n converseSystem: BedrockSystemContentBlock[];\n} {\n const converseSystem = messages\n .filter((msg) => msg._getType() === 'system')\n .flatMap((msg) => convertSystemMessageToConverseMessage(msg));\n\n const converseMessages = messages\n .filter((msg) => msg._getType() !== 'system')\n .map((msg) => {\n if (msg._getType() === 'ai') {\n return convertAIMessageToConverseMessage(msg);\n } else if (msg._getType() === 'human' || msg._getType() === 'generic') {\n return convertHumanMessageToConverseMessage(msg);\n } else if (msg._getType() === 'tool') {\n return convertToolMessageToConverseMessage(msg);\n } else {\n throw new Error(`Unsupported message type: ${msg._getType()}`);\n }\n });\n\n // Combine consecutive user tool result messages into a single message\n const combinedConverseMessages = converseMessages.reduce<BedrockMessage[]>(\n (acc, curr) => {\n if (acc.length === 0) {\n acc.push(curr);\n return acc;\n }\n const lastMessage = acc[acc.length - 1];\n const lastHasToolResult =\n lastMessage.content?.some((c) => 'toolResult' in c) === true;\n const currHasToolResult =\n curr.content?.some((c) => 'toolResult' in c) === true;\n if (\n lastMessage.role === 'user' &&\n lastHasToolResult &&\n curr.role === 'user' &&\n currHasToolResult\n ) {\n lastMessage.content = lastMessage.content?.concat(curr.content ?? []);\n } else {\n acc.push(curr);\n }\n return acc;\n },\n []\n );\n\n return { converseMessages: combinedConverseMessages, converseSystem };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createDefaultLogger } from "./utils.mjs";
|
|
2
|
+
import { processContent } from "./content.mjs";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
//#region src/tools/search/crw-scraper.ts
|
|
5
|
+
/** HTTP headroom over the payload render budget: fastCRW's queue/verification
|
|
6
|
+
* overhead is not counted against `timeout`, so the client must wait longer. */
|
|
7
|
+
const CRW_TIMEOUT_BUFFER = 5e3;
|
|
8
|
+
/**
|
|
9
|
+
* fastCRW scraper. Firecrawl-compatible web scraper; single binary;
|
|
10
|
+
* self-host or cloud. Posts to {base}/v1/scrape.
|
|
11
|
+
*/
|
|
12
|
+
var CrwScraper = class {
|
|
13
|
+
apiKey;
|
|
14
|
+
apiUrl;
|
|
15
|
+
defaultFormats;
|
|
16
|
+
timeout;
|
|
17
|
+
logger;
|
|
18
|
+
onlyMainContent;
|
|
19
|
+
includeTags;
|
|
20
|
+
excludeTags;
|
|
21
|
+
waitFor;
|
|
22
|
+
headers;
|
|
23
|
+
renderJs;
|
|
24
|
+
cssSelector;
|
|
25
|
+
xpath;
|
|
26
|
+
proxy;
|
|
27
|
+
stealth;
|
|
28
|
+
constructor(config = {}) {
|
|
29
|
+
this.apiKey = config.apiKey ?? process.env.CRW_API_KEY ?? "";
|
|
30
|
+
const baseUrl = config.apiUrl ?? process.env.CRW_API_URL ?? "https://api.fastcrw.com";
|
|
31
|
+
this.apiUrl = `${baseUrl.replace(/\/+$/, "")}/v1/scrape`;
|
|
32
|
+
this.defaultFormats = config.formats ?? ["markdown", "html"];
|
|
33
|
+
this.timeout = config.timeout ?? 7500;
|
|
34
|
+
this.logger = config.logger || createDefaultLogger();
|
|
35
|
+
this.onlyMainContent = config.onlyMainContent;
|
|
36
|
+
this.includeTags = config.includeTags;
|
|
37
|
+
this.excludeTags = config.excludeTags;
|
|
38
|
+
this.waitFor = config.waitFor;
|
|
39
|
+
this.headers = config.headers;
|
|
40
|
+
this.renderJs = config.renderJs;
|
|
41
|
+
this.cssSelector = config.cssSelector;
|
|
42
|
+
this.xpath = config.xpath;
|
|
43
|
+
this.proxy = config.proxy;
|
|
44
|
+
this.stealth = config.stealth;
|
|
45
|
+
if (!this.apiKey) this.logger.warn("CRW_API_KEY is not set. Scraping will not work.");
|
|
46
|
+
this.logger.debug(`CRW scraper initialized with API URL: ${this.apiUrl}`);
|
|
47
|
+
}
|
|
48
|
+
async scrapeUrl(url, options = {}) {
|
|
49
|
+
try {
|
|
50
|
+
const payloadTimeout = options.timeout ?? this.timeout;
|
|
51
|
+
const payload = omitUndefined({
|
|
52
|
+
url,
|
|
53
|
+
formats: options.formats ?? this.defaultFormats,
|
|
54
|
+
onlyMainContent: options.onlyMainContent ?? this.onlyMainContent,
|
|
55
|
+
includeTags: options.includeTags ?? this.includeTags,
|
|
56
|
+
excludeTags: options.excludeTags ?? this.excludeTags,
|
|
57
|
+
waitFor: options.waitFor ?? this.waitFor,
|
|
58
|
+
headers: options.headers ?? this.headers,
|
|
59
|
+
renderJs: options.renderJs ?? this.renderJs,
|
|
60
|
+
cssSelector: options.cssSelector ?? this.cssSelector,
|
|
61
|
+
xpath: options.xpath ?? this.xpath,
|
|
62
|
+
proxy: options.proxy ?? this.proxy,
|
|
63
|
+
stealth: options.stealth ?? this.stealth,
|
|
64
|
+
timeout: payloadTimeout,
|
|
65
|
+
deadlineMs: Math.max(1, Math.min(payloadTimeout, 6e4))
|
|
66
|
+
});
|
|
67
|
+
const headers = { "Content-Type": "application/json" };
|
|
68
|
+
if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
|
|
69
|
+
return [url, normalizeCrwResponse((await axios.post(this.apiUrl, payload, {
|
|
70
|
+
headers,
|
|
71
|
+
timeout: payloadTimeout + CRW_TIMEOUT_BUFFER
|
|
72
|
+
})).data)];
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return [url, {
|
|
75
|
+
success: false,
|
|
76
|
+
error: `fastCRW API request failed: ${error instanceof Error ? error.message : String(error)}`
|
|
77
|
+
}];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extract content from scrape response. Mirrors FirecrawlScraper — reads
|
|
82
|
+
* response.data.*, which normalizeCrwResponse guarantees, preserving the
|
|
83
|
+
* processContent ref-markers used by the reranker. Parameter is typed as the
|
|
84
|
+
* NARROWER t.CrwScrapeResponse, exactly like TavilyScraper/FirecrawlScraper:
|
|
85
|
+
* TS class-method bivariance accepts this against BaseScraper's
|
|
86
|
+
* AnyScraperResponse, AND it lets us read response.data.plainText (a
|
|
87
|
+
* CrwScrapeResponse-only field) with no cast.
|
|
88
|
+
*/
|
|
89
|
+
extractContent(response) {
|
|
90
|
+
if (!response.success || !response.data) return ["", void 0];
|
|
91
|
+
const htmlSource = response.data.html ?? response.data.rawHtml;
|
|
92
|
+
if (response.data.markdown != null && htmlSource != null) try {
|
|
93
|
+
const { markdown, ...rest } = processContent(htmlSource, response.data.markdown);
|
|
94
|
+
return [markdown, rest];
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.logger.error("Error processing content:", error);
|
|
97
|
+
return [response.data.markdown, void 0];
|
|
98
|
+
}
|
|
99
|
+
else if (response.data.markdown != null) return [response.data.markdown, void 0];
|
|
100
|
+
if (response.data.html != null) return [response.data.html, void 0];
|
|
101
|
+
if (response.data.rawHtml != null) return [response.data.rawHtml, void 0];
|
|
102
|
+
if (response.data.plainText != null) return [response.data.plainText, void 0];
|
|
103
|
+
return ["", void 0];
|
|
104
|
+
}
|
|
105
|
+
extractMetadata(response) {
|
|
106
|
+
if (!response.success || !response.data || !response.data.metadata) return {};
|
|
107
|
+
return response.data.metadata;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Create a fastCRW scraper instance
|
|
112
|
+
* @param config Scraper configuration
|
|
113
|
+
* @returns fastCRW scraper instance
|
|
114
|
+
*/
|
|
115
|
+
const createCrwScraper = (config = {}) => new CrwScraper(config);
|
|
116
|
+
/**
|
|
117
|
+
* fastCRW cloud nests scrape fields under `data` ({success, data: {markdown,
|
|
118
|
+
* ...}}, live-verified 2026-07-02), matching Firecrawl. Prefer the nested
|
|
119
|
+
* container and fall back to top-level fields for self-host/legacy responses.
|
|
120
|
+
*/
|
|
121
|
+
function normalizeCrwResponse(raw) {
|
|
122
|
+
if (raw == null) return {
|
|
123
|
+
success: false,
|
|
124
|
+
error: "Empty fastCRW response"
|
|
125
|
+
};
|
|
126
|
+
if (raw.success === false) return {
|
|
127
|
+
success: false,
|
|
128
|
+
error: raw.error_code != null ? `[${raw.error_code}] ${raw.error ?? "Unknown error"}` : raw.error ?? "fastCRW scrape failed",
|
|
129
|
+
error_code: raw.error_code
|
|
130
|
+
};
|
|
131
|
+
const data = raw.data ?? raw;
|
|
132
|
+
return {
|
|
133
|
+
success: true,
|
|
134
|
+
data: {
|
|
135
|
+
markdown: stripBase64DataUris(data.markdown),
|
|
136
|
+
html: stripBase64DataUris(data.html),
|
|
137
|
+
rawHtml: stripBase64DataUris(data.rawHtml),
|
|
138
|
+
plainText: stripBase64DataUris(data.plainText),
|
|
139
|
+
screenshot: data.screenshot,
|
|
140
|
+
links: data.links,
|
|
141
|
+
metadata: data.metadata
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Replace inline base64 data-URI payloads (typically images) in scraped text
|
|
147
|
+
* with a short placeholder. Such payloads can be hundreds of KB; the content
|
|
148
|
+
* processor builds a per-link RegExp from each URL, and one that large overflows
|
|
149
|
+
* the engine's pattern-size limit ("Invalid regular expression"). Firecrawl
|
|
150
|
+
* drops them via removeBase64Images — mirror that so image-heavy pages stay
|
|
151
|
+
* processable (and don't bloat the LLM context with base64 noise).
|
|
152
|
+
*/
|
|
153
|
+
function stripBase64DataUris(text) {
|
|
154
|
+
if (text == null) return text;
|
|
155
|
+
return text.replace(/data:[\w.+-]+\/[\w.+-]+;base64,[A-Za-z0-9+/=]+/g, "data:base64-content-removed");
|
|
156
|
+
}
|
|
157
|
+
function omitUndefined(obj) {
|
|
158
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
export { createCrwScraper };
|
|
162
|
+
|
|
163
|
+
//# sourceMappingURL=crw-scraper.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crw-scraper.mjs","names":[],"sources":["../../../../src/tools/search/crw-scraper.ts"],"sourcesContent":["import axios from 'axios';\nimport type * as t from './types';\nimport { createDefaultLogger } from './utils';\nimport { processContent } from './content';\n\n/** HTTP headroom over the payload render budget: fastCRW's queue/verification\n * overhead is not counted against `timeout`, so the client must wait longer. */\nconst CRW_TIMEOUT_BUFFER = 5000;\n\n/**\n * fastCRW scraper. Firecrawl-compatible web scraper; single binary;\n * self-host or cloud. Posts to {base}/v1/scrape.\n */\nexport class CrwScraper implements t.BaseScraper {\n private apiKey: string;\n private apiUrl: string;\n private defaultFormats: string[];\n private timeout: number;\n private logger: t.Logger;\n private onlyMainContent?: boolean;\n private includeTags?: string[];\n private excludeTags?: string[];\n private waitFor?: number;\n private headers?: Record<string, string>;\n private renderJs?: boolean | null;\n private cssSelector?: string;\n private xpath?: string;\n private proxy?: string;\n private stealth?: boolean;\n\n constructor(config: t.CrwScraperConfig = {}) {\n this.apiKey = config.apiKey ?? process.env.CRW_API_KEY ?? '';\n\n const baseUrl =\n config.apiUrl ?? process.env.CRW_API_URL ?? 'https://api.fastcrw.com';\n this.apiUrl = `${baseUrl.replace(/\\/+$/, '')}/v1/scrape`;\n\n this.defaultFormats = config.formats ?? ['markdown', 'html'];\n this.timeout = config.timeout ?? 7500;\n this.logger = config.logger || createDefaultLogger();\n\n this.onlyMainContent = config.onlyMainContent;\n this.includeTags = config.includeTags;\n this.excludeTags = config.excludeTags;\n this.waitFor = config.waitFor;\n this.headers = config.headers;\n this.renderJs = config.renderJs;\n this.cssSelector = config.cssSelector;\n this.xpath = config.xpath;\n this.proxy = config.proxy;\n this.stealth = config.stealth;\n\n // Self-host fastCRW may run without auth, so a missing key is only a\n // warning — unlike Firecrawl/Tavily, scrapeUrl does NOT early-return on it.\n if (!this.apiKey) {\n this.logger.warn('CRW_API_KEY is not set. Scraping will not work.');\n }\n this.logger.debug(`CRW scraper initialized with API URL: ${this.apiUrl}`);\n }\n\n async scrapeUrl(\n url: string,\n options: t.CrwScrapeOptions = {}\n ): Promise<[string, t.CrwScrapeResponse]> {\n try {\n const payloadTimeout = options.timeout ?? this.timeout;\n const payload = omitUndefined({\n url,\n formats: options.formats ?? this.defaultFormats,\n onlyMainContent: options.onlyMainContent ?? this.onlyMainContent,\n includeTags: options.includeTags ?? this.includeTags,\n excludeTags: options.excludeTags ?? this.excludeTags,\n waitFor: options.waitFor ?? this.waitFor,\n headers: options.headers ?? this.headers,\n renderJs: options.renderJs ?? this.renderJs,\n cssSelector: options.cssSelector ?? this.cssSelector,\n xpath: options.xpath ?? this.xpath,\n proxy: options.proxy ?? this.proxy,\n stealth: options.stealth ?? this.stealth,\n // Cloud honors `timeout` (live-verified); the published OpenAPI\n // documents `deadlineMs` (1..60000) instead. Send both.\n timeout: payloadTimeout,\n deadlineMs: Math.max(1, Math.min(payloadTimeout, 60000)),\n });\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (this.apiKey) {\n headers.Authorization = `Bearer ${this.apiKey}`;\n }\n\n const response = await axios.post<t.CrwRawScrapeResponse>(\n this.apiUrl,\n payload,\n {\n headers,\n timeout: payloadTimeout + CRW_TIMEOUT_BUFFER,\n }\n );\n\n return [url, normalizeCrwResponse(response.data)];\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n return [\n url,\n {\n success: false,\n error: `fastCRW API request failed: ${errorMessage}`,\n },\n ];\n }\n }\n\n /**\n * Extract content from scrape response. Mirrors FirecrawlScraper — reads\n * response.data.*, which normalizeCrwResponse guarantees, preserving the\n * processContent ref-markers used by the reranker. Parameter is typed as the\n * NARROWER t.CrwScrapeResponse, exactly like TavilyScraper/FirecrawlScraper:\n * TS class-method bivariance accepts this against BaseScraper's\n * AnyScraperResponse, AND it lets us read response.data.plainText (a\n * CrwScrapeResponse-only field) with no cast.\n */\n extractContent(\n response: t.CrwScrapeResponse\n ): [string, undefined | t.References] {\n if (!response.success || !response.data) {\n return ['', undefined];\n }\n\n const htmlSource = response.data.html ?? response.data.rawHtml;\n if (response.data.markdown != null && htmlSource != null) {\n try {\n const { markdown, ...rest } = processContent(\n htmlSource,\n response.data.markdown\n );\n return [markdown, rest];\n } catch (error) {\n this.logger.error('Error processing content:', error);\n return [response.data.markdown, undefined];\n }\n } else if (response.data.markdown != null) {\n return [response.data.markdown, undefined];\n }\n\n // Fall back to HTML content\n if (response.data.html != null) {\n return [response.data.html, undefined];\n }\n\n // Fall back to raw HTML content\n if (response.data.rawHtml != null) {\n return [response.data.rawHtml, undefined];\n }\n\n // CRW-only fallback (no Firecrawl equivalent): plain-text body.\n if (response.data.plainText != null) {\n return [response.data.plainText, undefined];\n }\n\n return ['', undefined];\n }\n\n extractMetadata(response: t.CrwScrapeResponse): t.ScrapeMetadata {\n if (!response.success || !response.data || !response.data.metadata) {\n return {};\n }\n\n return response.data.metadata;\n }\n}\n\n/**\n * Create a fastCRW scraper instance\n * @param config Scraper configuration\n * @returns fastCRW scraper instance\n */\nexport const createCrwScraper = (config: t.CrwScraperConfig = {}): CrwScraper =>\n new CrwScraper(config);\n\n/**\n * fastCRW cloud nests scrape fields under `data` ({success, data: {markdown,\n * ...}}, live-verified 2026-07-02), matching Firecrawl. Prefer the nested\n * container and fall back to top-level fields for self-host/legacy responses.\n */\nfunction normalizeCrwResponse(\n raw: t.CrwRawScrapeResponse | null | undefined\n): t.CrwScrapeResponse {\n if (raw == null) {\n return { success: false, error: 'Empty fastCRW response' };\n }\n if (raw.success === false) {\n return {\n success: false,\n error:\n raw.error_code != null\n ? `[${raw.error_code}] ${raw.error ?? 'Unknown error'}`\n : (raw.error ?? 'fastCRW scrape failed'),\n error_code: raw.error_code,\n };\n }\n const data = raw.data ?? raw;\n return {\n success: true,\n data: {\n // Strip inline base64 image payloads from text content (not `screenshot`,\n // which is base64 by design and never reaches the content processor).\n markdown: stripBase64DataUris(data.markdown),\n html: stripBase64DataUris(data.html),\n rawHtml: stripBase64DataUris(data.rawHtml),\n plainText: stripBase64DataUris(data.plainText),\n screenshot: data.screenshot,\n links: data.links,\n metadata: data.metadata,\n },\n };\n}\n\n/**\n * Replace inline base64 data-URI payloads (typically images) in scraped text\n * with a short placeholder. Such payloads can be hundreds of KB; the content\n * processor builds a per-link RegExp from each URL, and one that large overflows\n * the engine's pattern-size limit (\"Invalid regular expression\"). Firecrawl\n * drops them via removeBase64Images — mirror that so image-heavy pages stay\n * processable (and don't bloat the LLM context with base64 noise).\n */\nfunction stripBase64DataUris(text: string | undefined): string | undefined {\n if (text == null) {\n return text;\n }\n return text.replace(\n /data:[\\w.+-]+\\/[\\w.+-]+;base64,[A-Za-z0-9+/=]+/g,\n 'data:base64-content-removed'\n );\n}\n\n// Helper function to clean up payload for fastCRW\nfunction omitUndefined<T extends object>(obj: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(obj).filter(([, v]) => v !== undefined)\n ) as Partial<T>;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
//#region src/tools/search/crw-search.ts
|
|
3
|
+
const DEFAULT_CRW_TIMEOUT = 15e3;
|
|
4
|
+
const getHostname = (link) => {
|
|
5
|
+
try {
|
|
6
|
+
return new URL(link).hostname;
|
|
7
|
+
} catch {
|
|
8
|
+
return link;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
/** The published OpenAPI (docs.fastcrw.com) documents `data` as a flat
|
|
12
|
+
* SearchResult[] with a `category` field; route rows into source groups. */
|
|
13
|
+
const splitByCategory = (rows) => {
|
|
14
|
+
const groups = {
|
|
15
|
+
web: [],
|
|
16
|
+
images: [],
|
|
17
|
+
news: []
|
|
18
|
+
};
|
|
19
|
+
for (const row of rows) if (row.category === "news") groups.news.push(row);
|
|
20
|
+
else if (row.category === "images") groups.images.push({
|
|
21
|
+
...row,
|
|
22
|
+
imageUrl: row.url
|
|
23
|
+
});
|
|
24
|
+
else groups.web.push(row);
|
|
25
|
+
return groups;
|
|
26
|
+
};
|
|
27
|
+
const createCrwAPI = (apiKey, apiUrl, options) => {
|
|
28
|
+
const base = (apiUrl ?? process.env.CRW_API_URL ?? "https://api.fastcrw.com").replace(/\/+$/, "");
|
|
29
|
+
const config = {
|
|
30
|
+
apiKey: apiKey ?? process.env.CRW_API_KEY,
|
|
31
|
+
apiUrl: `${base}/v1/search`,
|
|
32
|
+
timeout: options?.timeout ?? DEFAULT_CRW_TIMEOUT
|
|
33
|
+
};
|
|
34
|
+
const getSources = async ({ query, date, numResults = 8, type }) => {
|
|
35
|
+
if (!query.trim()) return {
|
|
36
|
+
success: false,
|
|
37
|
+
error: "Query cannot be empty"
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
const limit = Math.min(Math.max(1, options?.maxResults ?? numResults), 20);
|
|
41
|
+
let sources;
|
|
42
|
+
if (type === "images") sources = ["images"];
|
|
43
|
+
else if (type === "news") sources = ["news"];
|
|
44
|
+
else sources = options?.includeImages === true ? ["web", "images"] : ["web"];
|
|
45
|
+
const payload = {
|
|
46
|
+
query,
|
|
47
|
+
limit,
|
|
48
|
+
sources
|
|
49
|
+
};
|
|
50
|
+
if (date != null) payload.tbs = `qdr:${date}`;
|
|
51
|
+
const headers = { "Content-Type": "application/json" };
|
|
52
|
+
if (config.apiKey != null && config.apiKey !== "") headers.Authorization = `Bearer ${config.apiKey}`;
|
|
53
|
+
const body = (await axios.post(config.apiUrl, payload, {
|
|
54
|
+
headers,
|
|
55
|
+
timeout: config.timeout
|
|
56
|
+
})).data;
|
|
57
|
+
if (body.success === false) return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: `fastCRW search failed: ${body.error_code != null ? `[${body.error_code}] ` : ""}${body.error ?? "Unknown error"}`
|
|
60
|
+
};
|
|
61
|
+
const container = body.data ?? {};
|
|
62
|
+
const data = Array.isArray(container) ? splitByCategory(container) : container.results ?? container;
|
|
63
|
+
return {
|
|
64
|
+
success: true,
|
|
65
|
+
data: {
|
|
66
|
+
organic: (data.web ?? []).filter((r) => r.url != null && r.url !== "").map((r) => ({
|
|
67
|
+
title: r.title ?? "",
|
|
68
|
+
link: r.url,
|
|
69
|
+
snippet: r.description ?? r.snippet ?? "",
|
|
70
|
+
position: r.position
|
|
71
|
+
})),
|
|
72
|
+
images: (data.images ?? []).filter((r) => r.imageUrl != null && r.imageUrl !== "").map((r) => ({
|
|
73
|
+
title: r.title,
|
|
74
|
+
imageUrl: r.imageUrl,
|
|
75
|
+
link: r.url,
|
|
76
|
+
position: r.position
|
|
77
|
+
})),
|
|
78
|
+
topStories: [],
|
|
79
|
+
videos: [],
|
|
80
|
+
news: (data.news ?? []).filter((r) => r.url != null && r.url !== "").map((r) => ({
|
|
81
|
+
title: r.title ?? "",
|
|
82
|
+
link: r.url,
|
|
83
|
+
snippet: r.description ?? r.snippet ?? "",
|
|
84
|
+
date: r.publishedDate,
|
|
85
|
+
source: getHostname(r.url),
|
|
86
|
+
position: r.position
|
|
87
|
+
})),
|
|
88
|
+
relatedSearches: []
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
error: `fastCRW search request failed: ${error instanceof Error ? error.message : String(error)}`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return { getSources };
|
|
99
|
+
};
|
|
100
|
+
//#endregion
|
|
101
|
+
export { createCrwAPI };
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=crw-search.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crw-search.mjs","names":[],"sources":["../../../../src/tools/search/crw-search.ts"],"sourcesContent":["import axios from 'axios';\nimport type * as t from './types';\n\nconst DEFAULT_CRW_TIMEOUT = 15000;\n\nconst getHostname = (link: string): string => {\n try {\n return new URL(link).hostname;\n } catch {\n return link;\n }\n};\n\n/** The published OpenAPI (docs.fastcrw.com) documents `data` as a flat\n * SearchResult[] with a `category` field; route rows into source groups. */\nconst splitByCategory = (rows: t.CrwSearchResult[]): t.CrwSearchGroups => {\n const groups: Required<t.CrwSearchGroups> = { web: [], images: [], news: [] };\n for (const row of rows) {\n if (row.category === 'news') {\n groups.news.push(row);\n } else if (row.category === 'images') {\n groups.images.push({ ...row, imageUrl: row.url });\n } else {\n groups.web.push(row);\n }\n }\n return groups;\n};\n\nexport const createCrwAPI = (\n apiKey?: string,\n apiUrl?: string,\n options?: t.CrwSearchOptions\n): {\n getSources: (params: t.GetSourcesParams) => Promise<t.SearchResult>;\n} => {\n // NOTE: fastCRW /v1/search is cloud-only, but we still allow a base-URL\n // override for parity with the scraper. Self-host may have no auth, so —\n // unlike Tavily (createTavilyAPI throws on missing key) — we do NOT throw\n // here; we only attach the Authorization header when a key is present.\n const base = (\n apiUrl ??\n process.env.CRW_API_URL ??\n 'https://api.fastcrw.com'\n ).replace(/\\/+$/, '');\n const config = {\n apiKey: apiKey ?? process.env.CRW_API_KEY,\n apiUrl: `${base}/v1/search`,\n timeout: options?.timeout ?? DEFAULT_CRW_TIMEOUT,\n };\n\n const getSources = async ({\n query,\n date,\n numResults = 8,\n type,\n }: t.GetSourcesParams): Promise<t.SearchResult> => {\n if (!query.trim()) {\n return { success: false, error: 'Query cannot be empty' };\n }\n\n try {\n const limit = Math.min(\n Math.max(1, options?.maxResults ?? numResults),\n 20\n );\n // Mirror Serper's verticals: image/news requests hit only their native\n // fastCRW source; plain web optionally adds images via includeImages.\n let sources: t.CrwSearchSource[];\n if (type === 'images') {\n sources = ['images'];\n } else if (type === 'news') {\n sources = ['news'];\n } else {\n sources = options?.includeImages === true ? ['web', 'images'] : ['web'];\n }\n\n const payload: t.CrwSearchPayload = { query, limit, sources };\n if (date != null) {\n // Serper-style qdr filter; live-verified to constrain results.\n payload.tbs = `qdr:${date}`;\n }\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n if (config.apiKey != null && config.apiKey !== '') {\n headers.Authorization = `Bearer ${config.apiKey}`;\n }\n\n const response = await axios.post<t.CrwSearchResponse>(\n config.apiUrl,\n payload,\n { headers, timeout: config.timeout }\n );\n\n const body = response.data;\n if (body.success === false) {\n return {\n success: false,\n error: `fastCRW search failed: ${\n body.error_code != null ? `[${body.error_code}] ` : ''\n }${body.error ?? 'Unknown error'}`,\n };\n }\n\n // fastCRW cloud keys results by source: {data: {web: [...], images:\n // [...], news: [...]}} (live-verified 2026-07-02); the published\n // OpenAPI documents a flat SearchResult[] instead (self-host), and a\n // {data: {results: {...groups}}} wrapper also exists. Accept all three.\n // OrganicResult.link is a REQUIRED string (types.ts), so defend against\n // null/empty urls to never feed a broken '' link into the scraper.\n const container = body.data ?? {};\n const data: t.CrwSearchGroups = Array.isArray(container)\n ? splitByCategory(container)\n : (container.results ?? container);\n const organicResults: t.OrganicResult[] = (data.web ?? [])\n .filter((r) => r.url != null && r.url !== '')\n .map((r) => ({\n title: r.title ?? '',\n link: r.url as string,\n snippet: r.description ?? r.snippet ?? '',\n position: r.position,\n }));\n\n const imageResults: t.ImageResult[] = (data.images ?? [])\n .filter((r) => r.imageUrl != null && r.imageUrl !== '')\n .map((r) => ({\n title: r.title,\n imageUrl: r.imageUrl,\n link: r.url,\n position: r.position,\n }));\n\n const newsResults: t.NewsResult[] = (data.news ?? [])\n .filter((r) => r.url != null && r.url !== '')\n .map((r) => ({\n title: r.title ?? '',\n link: r.url as string,\n snippet: r.description ?? r.snippet ?? '',\n date: r.publishedDate,\n source: getHostname(r.url as string),\n position: r.position,\n }));\n\n const results: t.SearchResultData = {\n organic: organicResults,\n images: imageResults,\n topStories: [],\n videos: [],\n news: newsResults,\n relatedSearches: [],\n };\n\n return { success: true, data: results };\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n return {\n success: false,\n error: `fastCRW search request failed: ${errorMessage}`,\n };\n }\n };\n\n return { getSources };\n};\n"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createDefaultLogger, getAttribution } from "./utils.mjs";
|
|
2
2
|
import { createKeenableAPI } from "./keenable-search.mjs";
|
|
3
3
|
import { createTavilyAPI } from "./tavily-search.mjs";
|
|
4
|
+
import { createCrwAPI } from "./crw-search.mjs";
|
|
4
5
|
import axios from "axios";
|
|
5
6
|
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
|
|
6
7
|
//#region src/tools/search/search.ts
|
|
@@ -272,12 +273,13 @@ const createSearXNGAPI = (instanceUrl, apiKey) => {
|
|
|
272
273
|
return { getSources };
|
|
273
274
|
};
|
|
274
275
|
const createSearchAPI = (config) => {
|
|
275
|
-
const { searchProvider = "serper", serperApiKey, searxngInstanceUrl, searxngApiKey, tavilyApiKey, tavilySearchUrl, tavilySearchOptions, keenableApiKey, keenableApiUrl, keenableSearchOptions } = config;
|
|
276
|
+
const { searchProvider = "serper", serperApiKey, searxngInstanceUrl, searxngApiKey, tavilyApiKey, tavilySearchUrl, tavilySearchOptions, keenableApiKey, keenableApiUrl, keenableSearchOptions, crwApiKey, crwApiUrl, crwSearchOptions } = config;
|
|
276
277
|
if (searchProvider.toLowerCase() === "serper") return createSerperAPI(serperApiKey);
|
|
277
278
|
else if (searchProvider.toLowerCase() === "searxng") return createSearXNGAPI(searxngInstanceUrl, searxngApiKey);
|
|
278
279
|
else if (searchProvider.toLowerCase() === "tavily") return createTavilyAPI(tavilyApiKey, tavilySearchUrl, tavilySearchOptions);
|
|
279
280
|
else if (searchProvider.toLowerCase() === "keenable") return createKeenableAPI(keenableApiKey, keenableApiUrl, keenableSearchOptions);
|
|
280
|
-
else
|
|
281
|
+
else if (searchProvider.toLowerCase() === "crw") return createCrwAPI(crwApiKey, crwApiUrl, crwSearchOptions);
|
|
282
|
+
else throw new Error(`Invalid search provider: ${searchProvider}. Must be 'serper', 'searxng', 'tavily', 'keenable', or 'crw'`);
|
|
281
283
|
};
|
|
282
284
|
const createSourceProcessor = (config = {}, scraperInstance) => {
|
|
283
285
|
if (!scraperInstance) throw new Error("Scraper instance is required");
|