feat(knowledge): support Voyage AI (#3810)

* feat(knowledge): support Voyage AI

* chore
This commit is contained in:
Chen Tao
2025-03-23 19:22:39 +08:00
committed by kangfenmao
parent 229b14f3fd
commit 72a01b8ba6
22 changed files with 765 additions and 52 deletions
+24
View File
@@ -0,0 +1,24 @@
import type { BaseEmbeddings } from '@llm-tools/embedjs-interfaces'
import { KnowledgeBaseParams } from '@types'
import EmbeddingsFactory from './EmbeddingsFactory'
export default class Embeddings {
private sdk: BaseEmbeddings
constructor({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams) {
this.sdk = EmbeddingsFactory.create({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams)
}
public async init(): Promise<void> {
return this.sdk.init()
}
public async getDimensions(): Promise<number> {
return this.sdk.getDimensions()
}
public async embedDocuments(texts: string[]): Promise<number[][]> {
return this.sdk.embedDocuments(texts)
}
public async embedQuery(text: string): Promise<number[]> {
return this.sdk.embedQuery(text)
}
}
+38
View File
@@ -0,0 +1,38 @@
import type { BaseEmbeddings } from '@llm-tools/embedjs-interfaces'
import { OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
import { AzureOpenAiEmbeddings } from '@llm-tools/embedjs-openai/src/azure-openai-embeddings'
import { getInstanceName } from '@main/utils'
import { KnowledgeBaseParams } from '@types'
import VoyageEmbeddings from './VoyageEmbeddings'
export default class EmbeddingsFactory {
static create({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams): BaseEmbeddings {
const batchSize = 10
if (model.includes('voyage')) {
return new VoyageEmbeddings({
modelName: model,
apiKey,
outputDimension: dimensions,
batchSize: 8
})
}
if (apiVersion !== undefined) {
return new AzureOpenAiEmbeddings({
azureOpenAIApiKey: apiKey,
azureOpenAIApiVersion: apiVersion,
azureOpenAIApiDeploymentName: model,
azureOpenAIApiInstanceName: getInstanceName(baseURL),
dimensions,
batchSize
})
}
return new OpenAiEmbeddings({
model,
apiKey,
dimensions,
batchSize,
configuration: { baseURL }
})
}
}
+30
View File
@@ -0,0 +1,30 @@
import { VoyageEmbeddings as _VoyageEmbeddings } from '@langchain/community/embeddings/voyage'
import { BaseEmbeddings } from '@llm-tools/embedjs-interfaces'
export default class VoyageEmbeddings extends BaseEmbeddings {
private model: _VoyageEmbeddings
constructor(private readonly configuration?: ConstructorParameters<typeof _VoyageEmbeddings>[0]) {
super()
if (!this.configuration) this.configuration = {}
if (!this.configuration.modelName) this.configuration.modelName = 'voyage-3'
if (!this.configuration.outputDimension) {
throw new Error('You need to pass in the optional dimensions parameter for this model')
}
this.model = new _VoyageEmbeddings(this.configuration)
}
override async getDimensions(): Promise<number> {
if (!this.configuration?.outputDimension) {
throw new Error('You need to pass in the optional dimensions parameter for this model')
}
return this.configuration?.outputDimension
}
override async embedDocuments(texts: string[]): Promise<number[][]> {
return this.model.embedDocuments(texts)
}
override async embedQuery(text: string): Promise<number[]> {
return this.model.embedQuery(text)
}
}
+3
View File
@@ -4,6 +4,7 @@ import BaseReranker from './BaseReranker'
import DefaultReranker from './DefaultReranker'
import JinaReranker from './JinaReranker'
import SiliconFlowReranker from './SiliconFlowReranker'
import VoyageReranker from './VoyageReranker'
export default class RerankerFactory {
static create(base: KnowledgeBaseParams): BaseReranker {
@@ -11,6 +12,8 @@ export default class RerankerFactory {
return new SiliconFlowReranker(base)
} else if (base.rerankModelProvider === 'jina') {
return new JinaReranker(base)
} else if (base.rerankModelProvider === 'voyageai') {
return new VoyageReranker(base)
}
return new DefaultReranker(base)
}
+60
View File
@@ -0,0 +1,60 @@
import { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import { KnowledgeBaseParams } from '@types'
import axios from 'axios'
import BaseReranker from './BaseReranker'
export default class VoyageReranker extends BaseReranker {
constructor(base: KnowledgeBaseParams) {
super(base)
}
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
let baseURL = this.base?.rerankBaseURL?.endsWith('/')
? this.base.rerankBaseURL.slice(0, -1)
: this.base.rerankBaseURL
if (baseURL && !baseURL.endsWith('/v1')) {
baseURL = `${baseURL}/v1`
}
const url = `${baseURL}/rerank`
const requestBody = {
model: this.base.rerankModel,
query,
documents: searchResults.map((doc) => doc.pageContent),
top_k: this.base.topN,
return_documents: false,
truncation: true
}
try {
const { data } = await axios.post(url, requestBody, {
headers: {
...this.defaultHeaders()
}
})
const rerankResults = data.data
const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0]))
return searchResults
.map((doc: ExtractChunkData, index: number) => {
const score = resultMap.get(index)
if (score === undefined) return undefined
return {
...doc,
score
}
})
.filter((doc): doc is ExtractChunkData => doc !== undefined)
.sort((a, b) => b.score - a.score)
} catch (error: any) {
console.error('Voyage Reranker API 错误:', error.message || error)
throw new Error(`${error} - BaseUrl: ${baseURL}`)
}
}
}
+15 -25
View File
@@ -21,11 +21,10 @@ import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import { LibSqlDb } from '@llm-tools/embedjs-libsql'
import { SitemapLoader } from '@llm-tools/embedjs-loader-sitemap'
import { WebLoader } from '@llm-tools/embedjs-loader-web'
import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
import Embeddings from '@main/embeddings/Embeddings'
import { addFileLoader } from '@main/loader'
import Reranker from '@main/reranker/Reranker'
import { windowService } from '@main/services/WindowService'
import { getInstanceName } from '@main/utils'
import { getAllFiles } from '@main/utils/file'
import type { LoaderReturn } from '@shared/config/types'
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
@@ -114,29 +113,20 @@ class KnowledgeService {
baseURL,
dimensions
}: KnowledgeBaseParams): Promise<RAGApplication> => {
const batchSize = 10
return new RAGApplicationBuilder()
.setModel('NO_MODEL')
.setEmbeddingModel(
apiVersion
? new AzureOpenAiEmbeddings({
azureOpenAIApiKey: apiKey,
azureOpenAIApiVersion: apiVersion,
azureOpenAIApiDeploymentName: model,
azureOpenAIApiInstanceName: getInstanceName(baseURL),
dimensions,
batchSize
})
: new OpenAiEmbeddings({
model,
apiKey,
dimensions,
batchSize,
configuration: { baseURL }
})
)
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
.build()
let ragApplication: RAGApplication
const embeddings = new Embeddings({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams)
try {
ragApplication = await new RAGApplicationBuilder()
.setModel('NO_MODEL')
.setEmbeddingModel(embeddings)
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
.build()
} catch (e) {
Logger.error(e)
throw new Error(`Failed to create RAGApplication: ${e}`)
}
return ragApplication
}
public create = async (_: Electron.IpcMainInvokeEvent, base: KnowledgeBaseParams): Promise<void> => {
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+52
View File
@@ -242,6 +242,58 @@ export const EMBEDDING_MODELS = [
{
id: 'mistral-embed',
max_context: 8000
},
{
id: 'voyage-3-large',
max_context: 1024
},
{
id: 'voyage-3-large',
max_context: 256
},
{
id: 'voyage-3-large',
max_context: 512
},
{
id: 'voyage-3-large',
max_context: 2048
},
{
id: 'voyage-3',
max_context: 1024
},
{
id: 'voyage-3-lite',
max_context: 512
},
{
id: 'voyage-code-3',
max_context: 1024
},
{
id: 'voyage-code-3',
max_context: 256
},
{
id: 'voyage-code-3',
max_context: 512
},
{
id: 'voyage-code-3',
max_context: 2048
},
{
id: 'voyage-finance-2',
max_context: 1024
},
{
id: 'voyage-law-2',
max_context: 1024
},
{
id: 'voyage-code-2',
max_context: 1536
}
]
+62 -3
View File
@@ -122,6 +122,7 @@ import UpstageModelLogo from '@renderer/assets/images/models/upstage.png'
import UpstageModelLogoDark from '@renderer/assets/images/models/upstage_dark.png'
import ViduModelLogo from '@renderer/assets/images/models/vidu.png'
import ViduModelLogoDark from '@renderer/assets/images/models/vidu_dark.png'
import VoyageModelLogo from '@renderer/assets/images/models/voyageai.png'
import WenxinModelLogo from '@renderer/assets/images/models/wenxin.png'
import WenxinModelLogoDark from '@renderer/assets/images/models/wenxin_dark.png'
import XirangModelLogo from '@renderer/assets/images/models/xirang.png'
@@ -175,7 +176,8 @@ export const REASONING_REGEX =
/^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*)$/i
// Embedding models
export const EMBEDDING_REGEX = /(?:^text-|embed|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings)/i
export const EMBEDDING_REGEX =
/(?:^text-|embed|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings|voyage-)/i
// Rerank models
export const RERANKING_REGEX = /(?:rerank|re-rank|re-ranker|re-ranking|retrieval|retriever)/i
@@ -327,7 +329,8 @@ export function getModelLogo(modelId: string) {
embedding: isLight ? EmbeddingModelLogo : EmbeddingModelLogoDark,
perplexity: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
sonar: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
'bge-': BgeModelLogo
'bge-': BgeModelLogo,
'voyage-': VoyageModelLogo
}
for (const key in logoMap) {
@@ -1801,7 +1804,63 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'DeepSeek'
}
],
gpustack: []
gpustack: [],
voyageai: [
{
id: 'voyage-3-large',
provider: 'voyageai',
name: 'voyage-3-large',
group: 'Voyage Embeddings V3'
},
{
id: 'voyage-3',
provider: 'voyageai',
name: 'voyage-3',
group: 'Voyage Embeddings V3'
},
{
id: 'voyage-3-lite',
provider: 'voyageai',
name: 'voyage-3-lite',
group: 'Voyage Embeddings V3'
},
{
id: 'voyage-code-3',
provider: 'voyageai',
name: 'voyage-code-3',
group: 'Voyage Embeddings V3'
},
{
id: 'voyage-finance-3',
provider: 'voyageai',
name: 'voyage-finance-3',
group: 'Voyage Embeddings V2'
},
{
id: 'voyage-law-2',
provider: 'voyageai',
name: 'voyage-law-2',
group: 'Voyage Embeddings V2'
},
{
id: 'voyage-code-2',
provider: 'voyageai',
name: 'voyage-code-2',
group: 'Voyage Embeddings V2'
},
{
id: 'rerank-2',
provider: 'voyageai',
name: 'rerank-2',
group: 'Voyage Rerank V2'
},
{
id: 'rerank-2-lite',
provider: 'voyageai',
name: 'rerank-2-lite',
group: 'Voyage Rerank V2'
}
]
}
export const TEXT_TO_IMAGES_MODELS = [
+15 -2
View File
@@ -38,6 +38,7 @@ import StepProviderLogo from '@renderer/assets/images/providers/step.png'
import TencentCloudProviderLogo from '@renderer/assets/images/providers/tencent-cloud-ti.png'
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
import BytedanceProviderLogo from '@renderer/assets/images/providers/volcengine.png'
import VoyageAIProviderLogo from '@renderer/assets/images/providers/voyageai.png'
import XirangProviderLogo from '@renderer/assets/images/providers/xirang.png'
import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
@@ -86,14 +87,15 @@ const PROVIDER_LOGO_MAP = {
o3: O3ProviderLogo,
'tencent-cloud-ti': TencentCloudProviderLogo,
gpustack: GPUStackProviderLogo,
alayanew: AlayaNewProviderLogo
alayanew: AlayaNewProviderLogo,
voyageai: VoyageAIProviderLogo
} as const
export function getProviderLogo(providerId: string) {
return PROVIDER_LOGO_MAP[providerId as keyof typeof PROVIDER_LOGO_MAP]
}
export const SUPPORTED_REANK_PROVIDERS = ['silicon', 'jina']
export const SUPPORTED_REANK_PROVIDERS = ['silicon', 'jina', 'voyageai']
export const PROVIDER_CONFIG = {
openai: {
@@ -560,5 +562,16 @@ export const PROVIDER_CONFIG = {
docs: 'https://docs.gpustack.ai/latest/',
models: 'https://docs.gpustack.ai/latest/overview/#supported-models'
}
},
voyageai: {
api: {
url: 'https://api.voyageai.com'
},
websites: {
official: 'https://www.voyageai.com/',
apiKey: 'https://dashboard.voyageai.com/organization/api-keys',
docs: 'https://docs.voyageai.com/docs',
models: 'https://docs.voyageai.com/docs'
}
}
}
+2 -1
View File
@@ -678,7 +678,8 @@
"xirang": "State Cloud Xirang",
"yi": "Yi",
"zhinao": "360AI",
"zhipu": "ZHIPU AI"
"zhipu": "ZHIPU AI",
"voyageai": "Voyage AI"
},
"restore": {
"confirm": "Are you sure you want to restore data?",
+2 -1
View File
@@ -678,7 +678,8 @@
"xirang": "天翼クラウド 息壤",
"yi": "零一万物",
"zhinao": "360智脳",
"zhipu": "智譜AI"
"zhipu": "智譜AI",
"voyageai": "Voyage AI"
},
"restore": {
"confirm": "データを復元しますか?",
+2 -1
View File
@@ -678,7 +678,8 @@
"xirang": "State Cloud Xirang",
"yi": "Yi",
"zhinao": "360AI",
"zhipu": "ZHIPU AI"
"zhipu": "ZHIPU AI",
"voyageai": "Voyage AI"
},
"restore": {
"confirm": "Вы уверены, что хотите восстановить данные?",
+2 -1
View File
@@ -678,7 +678,8 @@
"xirang": "天翼云息壤",
"yi": "零一万物",
"zhinao": "360智脑",
"zhipu": "智谱AI"
"zhipu": "智谱AI",
"voyageai":"Voyage AI"
},
"restore": {
"confirm": "确定要恢复数据吗?",
+2 -1
View File
@@ -678,7 +678,8 @@
"xirang": "天翼雲息壤",
"yi": "零一萬物",
"zhinao": "360 智腦",
"zhipu": "智譜 AI"
"zhipu": "智譜 AI",
"voyageai": "Voyage AI"
},
"restore": {
"confirm": "確定要復原資料嗎?",
@@ -257,6 +257,7 @@ const ModelList: React.FC<ModelListProps> = ({ provider: _provider, modelStatuse
{sortedModelGroups[group].map((model) => {
const modelStatus = modelStatuses.find((status) => status.model.id === model.id)
const isChecking = modelStatus?.checking === true
console.log('model', model.id, getModelLogo(model.id))
return (
<ModelListItem key={model.id}>
+1 -1
View File
@@ -40,7 +40,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 83,
version: 84,
blacklist: ['runtime', 'messages'],
migrate
},
+10
View File
@@ -456,6 +456,16 @@ export const INITIAL_PROVIDERS: Provider[] = [
models: SYSTEM_MODELS.gpustack,
isSystem: true,
enabled: false
},
{
id: 'voyageai',
name: 'VoyageAI',
type: 'openai',
apiKey: '',
apiHost: 'https://api.voyageai.com',
models: SYSTEM_MODELS.voyageai,
isSystem: true,
enabled: false
}
]
+4
View File
@@ -795,6 +795,10 @@ const migrateConfig = {
state.settings.launchToTray = false
state.settings.trayOnClose = true
return state
},
'84': (state: RootState) => {
addProvider(state, 'voyageai')
return state
}
}