diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 19960269a..dce1da52c 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -862,8 +862,34 @@ CONFIG_METADATA_2 = { "api_base": "https://openspeech.bytedance.com/api/v1/tts", "timeout": 20, }, + "OpenAI Embedding": { + "id": "openai_embedding", + "type": "openai_embedding", + "provider_type": "embedding", + "enable": True, + "embedding_api_key": "", + "embedding_api_base": "", + "embedding_model": "", + "embedding_dimensions": 1536, + "timeout": 20, + }, }, "items": { + "embedding_dimensions": { + "description": "嵌入维度", + "type": "int", + "hint": "嵌入向量的维度。根据模型不同,可能需要调整,请参考具体模型的文档。此配置项请务必填写正确,否则将导致向量数据库无法正常工作。", + }, + "embedding_model": { + "description": "嵌入模型", + "type": "string", + "hint": "嵌入模型名称。", + }, + "embedding_api_key": { + "description": "API Key", + "type": "string", + "hint": "API Key", + }, "volcengine_cluster": { "type": "string", "description": "火山引擎集群", diff --git a/astrbot/core/provider/entities.py b/astrbot/core/provider/entities.py index 6ad67da55..e01e46cf9 100644 --- a/astrbot/core/provider/entities.py +++ b/astrbot/core/provider/entities.py @@ -19,6 +19,7 @@ class ProviderType(enum.Enum): CHAT_COMPLETION = "chat_completion" SPEECH_TO_TEXT = "speech_to_text" TEXT_TO_SPEECH = "text_to_speech" + EMBEDDING = "embedding" @dataclass @@ -155,7 +156,9 @@ class ProviderRequest: if self.image_urls: user_content = { "role": "user", - "content": [{"type": "text", "text": self.prompt if self.prompt else "[图片]"}], + "content": [ + {"type": "text", "text": self.prompt if self.prompt else "[图片]"} + ], } for image_url in self.image_urls: if image_url.startswith("http"): diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 78337ce95..edfd9f581 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -98,6 +98,8 @@ class ProviderManager: """加载的 Speech To Text Provider 的实例""" self.tts_provider_insts: List[TTSProvider] = [] """加载的 Text To Speech Provider 的实例""" + self.embedding_provider_insts: List[Provider] = [] + """加载的 Embedding Provider 的实例""" self.inst_map = {} """Provider 实例映射. key: provider_id, value: Provider 实例""" self.llm_tools = llm_tools @@ -211,6 +213,10 @@ class ProviderManager: from .sources.volcengine_tts import ( ProviderVolcengineTTS as ProviderVolcengineTTS, ) + case "openai_embedding": + from .sources.openai_embedding_source import ( + OpenAIEmbeddingProvider as OpenAIEmbeddingProvider, + ) except (ImportError, ModuleNotFoundError) as e: logger.critical( f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。" @@ -290,6 +296,14 @@ class ProviderManager: if not self.curr_provider_inst: self.curr_provider_inst = inst + elif provider_metadata.provider_type == ProviderType.EMBEDDING: + inst = provider_metadata.cls_type( + provider_config, self.provider_settings + ) + if getattr(inst, "initialize", None): + await inst.initialize() + self.embedding_provider_insts.append(inst) + self.inst_map[provider_config["id"]] = inst except Exception as e: logger.error(traceback.format_exc()) diff --git a/astrbot/core/provider/provider.py b/astrbot/core/provider/provider.py index 7019113c7..c285ebd42 100644 --- a/astrbot/core/provider/provider.py +++ b/astrbot/core/provider/provider.py @@ -192,6 +192,11 @@ class EmbeddingProvider(AbstractProvider): """获取文本的向量""" ... + @abc.abstractmethod + async def get_embeddings(self, text: list[str]) -> list[list[float]]: + """批量获取文本的向量""" + ... + @abc.abstractmethod def get_dim(self) -> int: """获取向量的维度""" diff --git a/astrbot/core/provider/sources/openai_embedding_source.py b/astrbot/core/provider/sources/openai_embedding_source.py new file mode 100644 index 000000000..2d339e57e --- /dev/null +++ b/astrbot/core/provider/sources/openai_embedding_source.py @@ -0,0 +1,42 @@ +from openai import AsyncOpenAI +from ..provider import EmbeddingProvider +from ..register import register_provider_adapter +from ..entities import ProviderType + + +@register_provider_adapter( + "openai_embedding", + "OpenAI API Embedding 提供商适配器", + provider_type=ProviderType.EMBEDDING, +) +class OpenAIEmbeddingProvider(EmbeddingProvider): + def __init__(self, provider_config: dict, provider_settings: dict) -> None: + super().__init__(provider_config, provider_settings) + self.provider_config = provider_config + self.provider_settings = provider_settings + self.client = AsyncOpenAI( + api_key=provider_config.get("embedding_api_key"), + base_url=provider_config.get( + "embedding_api_base", "https://api.openai.com/v1" + ), + ) + self.model = provider_config.get("embedding_model", "text-embedding-3-small") + self.dimension = provider_config.get("embedding_dimensions", 1536) + + async def get_embedding(self, text: str) -> list[float]: + """ + 获取文本的嵌入 + """ + embedding = await self.client.embeddings.create(input=text, model=self.model) + return embedding.data[0].embedding + + async def get_embeddings(self, texts: list[str]) -> list[list[float]]: + """ + 批量获取文本的嵌入 + """ + embeddings = await self.client.embeddings.create(input=texts, model=self.model) + return [item.embedding for item in embeddings.data] + + def get_dim(self) -> int: + """获取向量的维度""" + return self.dimension diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index 996b8ae5e..880b0c72c 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -125,11 +125,8 @@ class Context: self.provider_manager.provider_insts.append(provider) def get_provider_by_id(self, provider_id: str) -> Provider: - """通过 ID 获取用于文本生成任务的 LLM Provider(Chat_Completion 类型)。""" - for provider in self.provider_manager.provider_insts: - if provider.meta().id == provider_id: - return provider - return None + """通过 ID 获取对应的 LLM Provider(Chat_Completion 类型)。""" + return self.provider_manager.inst_map.get(provider_id) def get_all_providers(self) -> List[Provider]: """获取所有用于文本生成任务的 LLM Provider(Chat_Completion 类型)。""" diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index b2677de10..2d214b77c 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -164,6 +164,7 @@ class ConfigRoute(Route): "/config/provider/update": ("POST", self.post_update_provider), "/config/provider/delete": ("POST", self.post_delete_provider), "/config/llmtools": ("GET", self.get_llm_tools), + "/config/provider/list": ("GET", self.get_provider_config_list), } self.register_routes() @@ -175,6 +176,17 @@ class ConfigRoute(Route): return Response().ok(await self._get_astrbot_config()).__dict__ return Response().ok(await self._get_plugin_config(plugin_name)).__dict__ + async def get_provider_config_list(self): + provider_type = request.args.get("provider_type", None) + if not provider_type: + return Response().error("缺少参数 provider_type").__dict__ + provider_list = [] + astrbot_config = self.core_lifecycle.astrbot_config + for provider in astrbot_config["provider"]: + if provider.get("provider_type", None) == provider_type: + provider_list.append(provider) + return Response().ok(provider_list).__dict__ + async def post_astrbot_configs(self): post_configs = await request.json try: diff --git a/dashboard/src/views/ProviderPage.vue b/dashboard/src/views/ProviderPage.vue index 8bad17e4d..53e7c8bc7 100644 --- a/dashboard/src/views/ProviderPage.vue +++ b/dashboard/src/views/ProviderPage.vue @@ -27,13 +27,39 @@ + + + + + mdi-filter-variant + 全部 + + + mdi-message-text + 基本对话 + + + mdi-microphone-message + 语音转文字 + + + mdi-volume-high + 文字转语音 + + + mdi-code-json + Embedding + + + + mdi-volume-high 文字转语音 + + mdi-code-json + Embedding + - @@ -225,6 +255,21 @@ export default { // 新增提供商对话框相关 showAddProviderDialog: false, activeProviderTab: 'chat_completion', + + // 添加提供商类型分类 + activeProviderTypeTab: 'all', + } + }, + + computed: { + // 根据选择的标签过滤提供商列表 + filteredProviders() { + if (!this.config_data.provider || this.activeProviderTypeTab === 'all') { + return this.config_data.provider || []; + } + + return this.config_data.provider.filter(provider => + provider.provider_type === this.activeProviderTypeTab); } }, @@ -243,6 +288,15 @@ export default { }); }, + // 获取空列表文本 + getEmptyText() { + if (this.activeProviderTypeTab === 'all') { + return "暂无服务提供商,点击 新增服务提供商 添加"; + } else { + return `暂无${this.getTabTypeName(this.activeProviderTypeTab)}类型的服务提供商,点击 新增服务提供商 添加`; + } + }, + // 按提供商类型获取模板列表 getTemplatesByType(type) { const templates = this.metadata['provider_group']?.metadata?.provider?.config_template || {}; @@ -294,7 +348,8 @@ export default { const names = { 'chat_completion': '基本对话', 'speech_to_text': '语音转文本', - 'text_to_speech': '文本转语音' + 'text_to_speech': '文本转语音', + 'embedding': 'Embedding' }; return names[tabType] || tabType; }, diff --git a/dashboard/src/views/alkaid/KnowledgeBase.vue b/dashboard/src/views/alkaid/KnowledgeBase.vue index 5b678fac1..c3f0b4244 100644 --- a/dashboard/src/views/alkaid/KnowledgeBase.vue +++ b/dashboard/src/views/alkaid/KnowledgeBase.vue @@ -72,6 +72,10 @@ + + + @@ -256,7 +260,8 @@ export default { newKB: { name: '', emoji: '🙂', - description: '' + description: '', + embedding_provider_id: '' }, snackbar: { show: false, @@ -306,13 +311,21 @@ export default { deleteTarget: { collection_name: '' }, - deleting: false + deleting: false, + embeddingProviderConfigs: [] } }, mounted() { this.checkPlugin(); + this.getEmbeddingProviderList(); }, methods: { + embeddingModelProps(providerConfig) { + return { + title: providerConfig.embedding_model, + subtitle: `提供商 ID: ${providerConfig.id}`, + } + }, checkPlugin() { axios.get('/api/plugin/get?name=astrbot_plugin_knowledge_base') .then(response => { @@ -365,10 +378,15 @@ export default { }, createCollection(name, emoji, description) { + // 如果 this.newKB.embedding_provider_id 是 Object + if (typeof this.newKB.embedding_provider_id === 'object') { + this.newKB.embedding_provider_id = this.newKB.embedding_provider_id.id || ''; + } axios.post('/api/plug/alkaid/kb/create_collection', { collection_name: name, emoji: emoji, - description: description + description: description, + embedding_provider_id: this.newKB.embedding_provider_id || '' }) .then(response => { if (response.data.status === 'ok') { @@ -394,7 +412,8 @@ export default { this.createCollection( this.newKB.name, this.newKB.emoji || '🙂', - this.newKB.description + this.newKB.description, + this.newKB.embedding_provider_id || '' ); }, @@ -402,7 +421,8 @@ export default { this.newKB = { name: '', emoji: '🙂', - description: '' + description: '', + embedding_provider: '' }; }, @@ -582,6 +602,27 @@ export default { this.deleting = false; }); }, + + getEmbeddingProviderList() { + axios.get('/api/config/provider/list', { + params: { + provider_type: 'embedding' + } + }) + .then(response => { + if (response.data.status === 'ok') { + this.embeddingProviderConfigs = response.data.data || []; + } else { + this.showSnackbar(response.data.message || '获取嵌入模型列表失败', 'error'); + return []; + } + }) + .catch(error => { + console.error('Error fetching embedding providers:', error); + this.showSnackbar('获取嵌入模型列表失败', 'error'); + return []; + }); + } } }