feat(knowledge): support Voyage AI (#3810)
* feat(knowledge): support Voyage AI * chore
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -678,7 +678,8 @@
|
||||
"xirang": "天翼クラウド 息壤",
|
||||
"yi": "零一万物",
|
||||
"zhinao": "360智脳",
|
||||
"zhipu": "智譜AI"
|
||||
"zhipu": "智譜AI",
|
||||
"voyageai": "Voyage AI"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "データを復元しますか?",
|
||||
|
||||
@@ -678,7 +678,8 @@
|
||||
"xirang": "State Cloud Xirang",
|
||||
"yi": "Yi",
|
||||
"zhinao": "360AI",
|
||||
"zhipu": "ZHIPU AI"
|
||||
"zhipu": "ZHIPU AI",
|
||||
"voyageai": "Voyage AI"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "Вы уверены, что хотите восстановить данные?",
|
||||
|
||||
@@ -678,7 +678,8 @@
|
||||
"xirang": "天翼云息壤",
|
||||
"yi": "零一万物",
|
||||
"zhinao": "360智脑",
|
||||
"zhipu": "智谱AI"
|
||||
"zhipu": "智谱AI",
|
||||
"voyageai":"Voyage AI"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "确定要恢复数据吗?",
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -40,7 +40,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 83,
|
||||
version: 84,
|
||||
blacklist: ['runtime', 'messages'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -795,6 +795,10 @@ const migrateConfig = {
|
||||
state.settings.launchToTray = false
|
||||
state.settings.trayOnClose = true
|
||||
return state
|
||||
},
|
||||
'84': (state: RootState) => {
|
||||
addProvider(state, 'voyageai')
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user