Skip to content
Open
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
23 changes: 23 additions & 0 deletions packages/components/credentials/BaiduQianfanApiKey.credential.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { INodeParams, INodeCredential } from '../src/Interface'

class BaiduQianfanApiKey implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]

constructor() {
this.label = 'Baidu Qianfan API Key'
this.name = 'baiduQianfanApiKey'
this.version = 1.0
this.inputs = [
{
label: 'Qianfan API Key',
name: 'qianfanApiKey',
type: 'password'
}
]
}
}

module.exports = { credClass: BaiduQianfanApiKey }
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Document } from '@langchain/core/documents'
import { BaiduQianfanRerank } from './BaiduQianfanRerank'

const originalFetch = global.fetch
const mockedFetch = jest.fn()

describe('BaiduQianfanRerank', () => {
beforeEach(() => {
jest.clearAllMocks()
global.fetch = mockedFetch as unknown as typeof fetch
})

afterAll(() => {
global.fetch = originalFetch
})

it('calls Qianfan rerank API and preserves metadata from ranked indexes', async () => {
mockedFetch.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({
results: [
{ index: 1, document: 'second', relevance_score: 0.92 },
{ index: 0, document: 'first', relevance_score: 0.41 }
]
})
})

const compressor = new BaiduQianfanRerank('api-key', 'bce-reranker-base', 2)
const documents = [
new Document({ pageContent: 'first', metadata: { source: 'a' } }),
new Document({ pageContent: 'second', metadata: { source: 'b' } })
]

const result = await compressor.compressDocuments(documents, 'weather in Shanghai')

expect(mockedFetch).toHaveBeenCalledWith('https://qianfan.baidubce.com/v2/rerank', {
method: 'POST',
headers: {
Authorization: 'Bearer api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'bce-reranker-base',
query: 'weather in Shanghai',
documents: ['first', 'second'],
top_n: 2
})
})
expect(result.map((doc) => doc.pageContent)).toEqual(['second', 'first'])
expect(result[0].metadata).toEqual({ source: 'b', relevance_score: 0.92 })
expect(result[1].metadata).toEqual({ source: 'a', relevance_score: 0.41 })
})

it('returns an empty array without calling Qianfan when no documents are provided', async () => {
const compressor = new BaiduQianfanRerank('api-key', 'bce-reranker-base', 4)

await expect(compressor.compressDocuments([], 'query')).resolves.toEqual([])
expect(mockedFetch).not.toHaveBeenCalled()
})

it('falls back to the original documents when Qianfan returns an invalid index', async () => {
mockedFetch.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({
results: [{ index: 99, document: 'missing', relevance_score: 0.9 }]
})
})

const compressor = new BaiduQianfanRerank('api-key', 'bce-reranker-base', 4)
const documents = [new Document({ pageContent: 'first', metadata: { source: 'a' } })]

await expect(compressor.compressDocuments(documents, 'query')).resolves.toBe(documents)
})

it('falls back to the original documents when Qianfan returns an API error', async () => {
mockedFetch.mockResolvedValue({
ok: false,
status: 404,
text: jest.fn().mockResolvedValue('model not found')
})

const compressor = new BaiduQianfanRerank('api-key', 'missing-model', 4)
const documents = [new Document({ pageContent: 'first', metadata: { source: 'a' } })]

await expect(compressor.compressDocuments(documents, 'query')).resolves.toBe(documents)
})

it('falls back to the original documents when the Qianfan call fails', async () => {
mockedFetch.mockRejectedValue(new Error('network failed'))

const compressor = new BaiduQianfanRerank('api-key', 'bce-reranker-base', 4)
const documents = [new Document({ pageContent: 'first', metadata: { source: 'a' } })]

await expect(compressor.compressDocuments(documents, 'query')).resolves.toBe(documents)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Callbacks } from '@langchain/core/callbacks/manager'
import { Document } from '@langchain/core/documents'
import { BaseDocumentCompressor } from '@langchain/classic/retrievers/document_compressors'

const QIANFAN_RERANK_API_URL = 'https://qianfan.baidubce.com/v2/rerank'

type QianfanRerankResult = {
index: number
document: string
relevance_score: number
}

type QianfanRerankResponse = {
results?: QianfanRerankResult[]
}

export class BaiduQianfanRerank extends BaseDocumentCompressor {
private readonly qianfanApiKey: string
private readonly model: string
private readonly topN: number

constructor(qianfanApiKey: string, model: string, topN: number) {
super()
this.qianfanApiKey = qianfanApiKey
this.model = model
this.topN = topN
}

async compressDocuments(
documents: Document<Record<string, any>>[],
query: string,
_?: Callbacks | undefined
): Promise<Document<Record<string, any>>[]> {
if (documents.length === 0) return []

try {
const response = await fetch(QIANFAN_RERANK_API_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${this.qianfanApiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: this.model,
query,
documents: documents.map((doc) => doc.pageContent),
top_n: this.topN
})
})

if (!response.ok) throw new Error(`Baidu Qianfan Rerank API call failed with status ${response.status}`)

const rerankResponse = (await response.json()) as QianfanRerankResponse

if (!Array.isArray(rerankResponse.results)) return documents

const rerankedDocuments: Document<Record<string, any>>[] = []
for (const result of rerankResponse.results) {
const doc = documents[result.index]
if (!doc) return documents
rerankedDocuments.push(
new Document({
pageContent: doc.pageContent,
metadata: {
...doc.metadata,
relevance_score: result.relevance_score
}
})
)
}

return rerankedDocuments
} catch (error) {
return documents
}
Comment on lines +73 to +75
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Swallowing all errors silently makes debugging extremely difficult if the API key is invalid, rate limits are hit, or the model name is incorrect. Consider logging the error to console.error before returning the fallback documents so that users and administrators can troubleshoot issues.

        } catch (error) {
            console.error('Baidu Qianfan Rerank Error:', error)
            return documents
        }

}
}
Loading
Loading