From aaae0596aca743d8ae6adc1aec62777dc8fb105c Mon Sep 17 00:00:00 2001 From: Zhijie He Date: Tue, 19 Nov 2024 10:46:07 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20support=20InternL?= =?UTF-8?q?M=20(=E4=B9=A6=E7=94=9F=E6=B5=A6=E8=AF=AD)=20provider=20(#4711)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: add support InternLM (书生浦语) provider * 🐛 fix: fix tools calling, disable streaming, not support * 💄 style: disable Client Fetch mode, not support * 🔨 chore: cleanup code --- Dockerfile | 2 + Dockerfile.database | 2 + .../settings/llm/ProviderList/providers.tsx | 2 + src/config/llm.ts | 6 + src/config/modelProviders/index.ts | 4 + src/config/modelProviders/internlm.ts | 42 +++ src/libs/agent-runtime/AgentRuntime.ts | 7 + src/libs/agent-runtime/internlm/index.test.ts | 255 ++++++++++++++++++ src/libs/agent-runtime/internlm/index.ts | 18 ++ src/libs/agent-runtime/types/type.ts | 1 + src/server/modules/AgentRuntime/index.ts | 7 + src/types/user/settings/keyVaults.ts | 1 + 12 files changed, 347 insertions(+) create mode 100644 src/config/modelProviders/internlm.ts create mode 100644 src/libs/agent-runtime/internlm/index.test.ts create mode 100644 src/libs/agent-runtime/internlm/index.ts diff --git a/Dockerfile b/Dockerfile index d3528c81e582..7efd79cc23a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -168,6 +168,8 @@ ENV \ HUGGINGFACE_API_KEY="" HUGGINGFACE_MODEL_LIST="" HUGGINGFACE_PROXY_URL="" \ # Hunyuan HUNYUAN_API_KEY="" HUNYUAN_MODEL_LIST="" \ + # InternLM + INTERNLM_API_KEY="" INTERNLM_MODEL_LIST="" \ # Minimax MINIMAX_API_KEY="" MINIMAX_MODEL_LIST="" \ # Mistral diff --git a/Dockerfile.database b/Dockerfile.database index cca4cc35ee78..d5e0797e3530 100644 --- a/Dockerfile.database +++ b/Dockerfile.database @@ -203,6 +203,8 @@ ENV \ HUGGINGFACE_API_KEY="" HUGGINGFACE_MODEL_LIST="" HUGGINGFACE_PROXY_URL="" \ # Hunyuan HUNYUAN_API_KEY="" HUNYUAN_MODEL_LIST="" \ + # InternLM + INTERNLM_API_KEY="" INTERNLM_MODEL_LIST="" \ # Minimax MINIMAX_API_KEY="" MINIMAX_MODEL_LIST="" \ # Mistral diff --git a/src/app/(main)/settings/llm/ProviderList/providers.tsx b/src/app/(main)/settings/llm/ProviderList/providers.tsx index d67e56fa1242..f49cdd22cea1 100644 --- a/src/app/(main)/settings/llm/ProviderList/providers.tsx +++ b/src/app/(main)/settings/llm/ProviderList/providers.tsx @@ -10,6 +10,7 @@ import { GoogleProviderCard, GroqProviderCard, HunyuanProviderCard, + InternLMProviderCard, MinimaxProviderCard, MistralProviderCard, MoonshotProviderCard, @@ -85,6 +86,7 @@ export const useProviderList = (): ProviderItem[] => { MinimaxProviderCard, Ai360ProviderCard, TaichuProviderCard, + InternLMProviderCard, SiliconCloudProviderCard, ], [ diff --git a/src/config/llm.ts b/src/config/llm.ts index 0d34d4f8da1d..4c9a831d4843 100644 --- a/src/config/llm.ts +++ b/src/config/llm.ts @@ -124,6 +124,9 @@ export const getLLMConfig = () => { ENABLED_XAI: z.boolean(), XAI_API_KEY: z.string().optional(), + + ENABLED_INTERNLM: z.boolean(), + INTERNLM_API_KEY: z.string().optional(), }, runtimeEnv: { API_KEY_SELECT_MODE: process.env.API_KEY_SELECT_MODE, @@ -246,6 +249,9 @@ export const getLLMConfig = () => { ENABLED_XAI: !!process.env.XAI_API_KEY, XAI_API_KEY: process.env.XAI_API_KEY, + + ENABLED_INTERNLM: !!process.env.INTERNLM_API_KEY, + INTERNLM_API_KEY: process.env.INTERNLM_API_KEY, }, }); }; diff --git a/src/config/modelProviders/index.ts b/src/config/modelProviders/index.ts index 3fa1e590c1e5..c79ee701497f 100644 --- a/src/config/modelProviders/index.ts +++ b/src/config/modelProviders/index.ts @@ -14,6 +14,7 @@ import GoogleProvider from './google'; import GroqProvider from './groq'; import HuggingFaceProvider from './huggingface'; import HunyuanProvider from './hunyuan'; +import InternLMProvider from './internlm'; import MinimaxProvider from './minimax'; import MistralProvider from './mistral'; import MoonshotProvider from './moonshot'; @@ -69,6 +70,7 @@ export const LOBE_DEFAULT_MODEL_LIST: ChatModelCard[] = [ HunyuanProvider.chatModels, WenxinProvider.chatModels, SenseNovaProvider.chatModels, + InternLMProvider.chatModels, ].flat(); export const DEFAULT_MODEL_PROVIDER_LIST = [ @@ -105,6 +107,7 @@ export const DEFAULT_MODEL_PROVIDER_LIST = [ MinimaxProvider, Ai360Provider, TaichuProvider, + InternLMProvider, SiliconCloudProvider, ]; @@ -131,6 +134,7 @@ export { default as GoogleProviderCard } from './google'; export { default as GroqProviderCard } from './groq'; export { default as HuggingFaceProviderCard } from './huggingface'; export { default as HunyuanProviderCard } from './hunyuan'; +export { default as InternLMProviderCard } from './internlm'; export { default as MinimaxProviderCard } from './minimax'; export { default as MistralProviderCard } from './mistral'; export { default as MoonshotProviderCard } from './moonshot'; diff --git a/src/config/modelProviders/internlm.ts b/src/config/modelProviders/internlm.ts new file mode 100644 index 000000000000..07e07117ea47 --- /dev/null +++ b/src/config/modelProviders/internlm.ts @@ -0,0 +1,42 @@ +import { ModelProviderCard } from '@/types/llm'; + +const InternLM: ModelProviderCard = { + chatModels: [ + { + description: '我们最新的模型系列,有着卓越的推理性能,支持 1M 的上下文长度以及更强的指令跟随和工具调用能力。', + displayName: 'InternLM2.5', + enabled: true, + functionCall: true, + id: 'internlm2.5-latest', + maxOutput: 4096, + pricing: { + input: 0, + output: 0, + }, + tokens: 32_768, + }, + { + description: '我们仍在维护的老版本模型,有 7B、20B 多种模型参数量可选。', + displayName: 'InternLM2 Pro Chat', + functionCall: true, + id: 'internlm2-pro-chat', + maxOutput: 4096, + pricing: { + input: 0, + output: 0, + }, + tokens: 32_768, + }, + ], + checkModel: 'internlm2.5-latest', + description: + '致力于大模型研究与开发工具链的开源组织。为所有 AI 开发者提供高效、易用的开源平台,让最前沿的大模型与算法技术触手可及', + disableBrowserRequest: true, + id: 'internlm', + modelList: { showModelFetcher: true }, + modelsUrl: 'https://internlm.intern-ai.org.cn/doc/docs/Models#%E8%8E%B7%E5%8F%96%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8', + name: 'InternLM', + url: 'https://internlm.intern-ai.org.cn', +}; + +export default InternLM; diff --git a/src/libs/agent-runtime/AgentRuntime.ts b/src/libs/agent-runtime/AgentRuntime.ts index 7f54ec1abd8e..8ee4a61b2d62 100644 --- a/src/libs/agent-runtime/AgentRuntime.ts +++ b/src/libs/agent-runtime/AgentRuntime.ts @@ -17,6 +17,7 @@ import { LobeGoogleAI } from './google'; import { LobeGroq } from './groq'; import { LobeHuggingFaceAI } from './huggingface'; import { LobeHunyuanAI } from './hunyuan'; +import { LobeInternLMAI } from './internlm'; import { LobeMinimaxAI } from './minimax'; import { LobeMistralAI } from './mistral'; import { LobeMoonshotAI } from './moonshot'; @@ -141,6 +142,7 @@ class AgentRuntime { groq: Partial; huggingface: { apiKey?: string; baseURL?: string }; hunyuan: Partial; + internlm: Partial; minimax: Partial; mistral: Partial; moonshot: Partial; @@ -335,6 +337,11 @@ class AgentRuntime { runtimeModel = new LobeCloudflareAI(params.cloudflare ?? {}); break; } + + case ModelProvider.InternLM: { + runtimeModel = new LobeInternLMAI(params.internlm); + break; + } } return new AgentRuntime(runtimeModel); } diff --git a/src/libs/agent-runtime/internlm/index.test.ts b/src/libs/agent-runtime/internlm/index.test.ts new file mode 100644 index 000000000000..73925e4ed1f8 --- /dev/null +++ b/src/libs/agent-runtime/internlm/index.test.ts @@ -0,0 +1,255 @@ +// @vitest-environment node +import OpenAI from 'openai'; +import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + ChatStreamCallbacks, + LobeOpenAICompatibleRuntime, + ModelProvider, +} from '@/libs/agent-runtime'; + +import * as debugStreamModule from '../utils/debugStream'; +import { LobeInternLMAI } from './index'; + +const provider = ModelProvider.InternLM; +const defaultBaseURL = 'https://internlm-chat.intern-ai.org.cn/puyu/api/v1'; + +const bizErrorType = 'ProviderBizError'; +const invalidErrorType = 'InvalidProviderAPIKey'; + +// Mock the console.error to avoid polluting test output +vi.spyOn(console, 'error').mockImplementation(() => {}); + +let instance: LobeOpenAICompatibleRuntime; + +beforeEach(() => { + instance = new LobeInternLMAI({ apiKey: 'test' }); + + // 使用 vi.spyOn 来模拟 chat.completions.create 方法 + vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue( + new ReadableStream() as any, + ); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('LobeInternLMAI', () => { + describe('init', () => { + it('should correctly initialize with an API key', async () => { + const instance = new LobeInternLMAI({ apiKey: 'test_api_key' }); + expect(instance).toBeInstanceOf(LobeInternLMAI); + expect(instance.baseURL).toEqual(defaultBaseURL); + }); + }); + + describe('chat', () => { + describe('Error', () => { + it('should return OpenAIBizError with an openai error response when OpenAI.APIError is thrown', async () => { + // Arrange + const apiError = new OpenAI.APIError( + 400, + { + status: 400, + error: { + message: 'Bad Request', + }, + }, + 'Error message', + {}, + ); + + vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError); + + // Act + try { + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'internlm2.5-latest', + temperature: 0, + }); + } catch (e) { + expect(e).toEqual({ + endpoint: defaultBaseURL, + error: { + error: { message: 'Bad Request' }, + status: 400, + }, + errorType: bizErrorType, + provider, + }); + } + }); + + it('should throw AgentRuntimeError with NoOpenAIAPIKey if no apiKey is provided', async () => { + try { + new LobeInternLMAI({}); + } catch (e) { + expect(e).toEqual({ errorType: invalidErrorType }); + } + }); + + it('should return OpenAIBizError with the cause when OpenAI.APIError is thrown with cause', async () => { + // Arrange + const errorInfo = { + stack: 'abc', + cause: { + message: 'api is undefined', + }, + }; + const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {}); + + vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError); + + // Act + try { + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'internlm2.5-latest', + temperature: 0, + }); + } catch (e) { + expect(e).toEqual({ + endpoint: defaultBaseURL, + error: { + cause: { message: 'api is undefined' }, + stack: 'abc', + }, + errorType: bizErrorType, + provider, + }); + } + }); + + it('should return OpenAIBizError with an cause response with desensitize Url', async () => { + // Arrange + const errorInfo = { + stack: 'abc', + cause: { message: 'api is undefined' }, + }; + const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {}); + + instance = new LobeInternLMAI({ + apiKey: 'test', + + baseURL: 'https://api.abc.com/v1', + }); + + vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError); + + // Act + try { + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'internlm2.5-latest', + temperature: 0, + }); + } catch (e) { + expect(e).toEqual({ + endpoint: 'https://api.***.com/v1', + error: { + cause: { message: 'api is undefined' }, + stack: 'abc', + }, + errorType: bizErrorType, + provider, + }); + } + }); + + it('should throw an InvalidInternLMAIAPIKey error type on 401 status code', async () => { + // Mock the API call to simulate a 401 error + const error = new Error('Unauthorized') as any; + error.status = 401; + vi.mocked(instance['client'].chat.completions.create).mockRejectedValue(error); + + try { + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'internlm2.5-latest', + temperature: 0, + }); + } catch (e) { + // Expect the chat method to throw an error with InvalidInternLMAIAPIKey + expect(e).toEqual({ + endpoint: defaultBaseURL, + error: new Error('Unauthorized'), + errorType: invalidErrorType, + provider, + }); + } + }); + + it('should return AgentRuntimeError for non-OpenAI errors', async () => { + // Arrange + const genericError = new Error('Generic Error'); + + vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(genericError); + + // Act + try { + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'internlm2.5-latest', + temperature: 0, + }); + } catch (e) { + expect(e).toEqual({ + endpoint: defaultBaseURL, + errorType: 'AgentRuntimeError', + provider, + error: { + name: genericError.name, + cause: genericError.cause, + message: genericError.message, + stack: genericError.stack, + }, + }); + } + }); + }); + + describe('DEBUG', () => { + it('should call debugStream and return StreamingTextResponse when DEBUG_INTERNLM_CHAT_COMPLETION is 1', async () => { + // Arrange + const mockProdStream = new ReadableStream() as any; // 模拟的 prod 流 + const mockDebugStream = new ReadableStream({ + start(controller) { + controller.enqueue('Debug stream content'); + controller.close(); + }, + }) as any; + mockDebugStream.toReadableStream = () => mockDebugStream; // 添加 toReadableStream 方法 + + // 模拟 chat.completions.create 返回值,包括模拟的 tee 方法 + (instance['client'].chat.completions.create as Mock).mockResolvedValue({ + tee: () => [mockProdStream, { toReadableStream: () => mockDebugStream }], + }); + + // 保存原始环境变量值 + const originalDebugValue = process.env.DEBUG_INTERNLM_CHAT_COMPLETION; + + // 模拟环境变量 + process.env.DEBUG_INTERNLM_CHAT_COMPLETION = '1'; + vi.spyOn(debugStreamModule, 'debugStream').mockImplementation(() => Promise.resolve()); + + // 执行测试 + // 运行你的测试函数,确保它会在条件满足时调用 debugStream + // 假设的测试函数调用,你可能需要根据实际情况调整 + await instance.chat({ + messages: [{ content: 'Hello', role: 'user' }], + model: 'internlm2.5-latest', + stream: true, + temperature: 0, + }); + + // 验证 debugStream 被调用 + expect(debugStreamModule.debugStream).toHaveBeenCalled(); + + // 恢复原始环境变量值 + process.env.DEBUG_INTERNLM_CHAT_COMPLETION = originalDebugValue; + }); + }); + }); +}); diff --git a/src/libs/agent-runtime/internlm/index.ts b/src/libs/agent-runtime/internlm/index.ts new file mode 100644 index 000000000000..3dfaf0edda41 --- /dev/null +++ b/src/libs/agent-runtime/internlm/index.ts @@ -0,0 +1,18 @@ +import { ModelProvider } from '../types'; +import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory'; + +export const LobeInternLMAI = LobeOpenAICompatibleFactory({ + baseURL: 'https://internlm-chat.intern-ai.org.cn/puyu/api/v1', + chatCompletion: { + handlePayload: (payload) => { + return { + ...payload, + stream: !payload.tools, + } as any; + }, + }, + debug: { + chatCompletion: () => process.env.DEBUG_INTERNLM_CHAT_COMPLETION === '1', + }, + provider: ModelProvider.InternLM, +}); diff --git a/src/libs/agent-runtime/types/type.ts b/src/libs/agent-runtime/types/type.ts index 5a00cbc3dd9c..6629f2bf9920 100644 --- a/src/libs/agent-runtime/types/type.ts +++ b/src/libs/agent-runtime/types/type.ts @@ -36,6 +36,7 @@ export enum ModelProvider { Groq = 'groq', HuggingFace = 'huggingface', Hunyuan = 'hunyuan', + InternLM = 'internlm', Minimax = 'minimax', Mistral = 'mistral', Moonshot = 'moonshot', diff --git a/src/server/modules/AgentRuntime/index.ts b/src/server/modules/AgentRuntime/index.ts index 6d2ead94a01a..8134bed5c7a2 100644 --- a/src/server/modules/AgentRuntime/index.ts +++ b/src/server/modules/AgentRuntime/index.ts @@ -293,6 +293,13 @@ const getLlmOptionsFromPayload = (provider: string, payload: JWTPayload) => { const apiKey = apiKeyManager.pick(payload?.apiKey || XAI_API_KEY); + return { apiKey }; + } + case ModelProvider.InternLM: { + const { INTERNLM_API_KEY } = getLLMConfig(); + + const apiKey = apiKeyManager.pick(payload?.apiKey || INTERNLM_API_KEY); + return { apiKey }; } } diff --git a/src/types/user/settings/keyVaults.ts b/src/types/user/settings/keyVaults.ts index dec9a9c1ab7c..492cf7449834 100644 --- a/src/types/user/settings/keyVaults.ts +++ b/src/types/user/settings/keyVaults.ts @@ -46,6 +46,7 @@ export interface UserKeyVaults { groq?: OpenAICompatibleKeyVault; huggingface?: OpenAICompatibleKeyVault; hunyuan?: OpenAICompatibleKeyVault; + internlm?: OpenAICompatibleKeyVault; lobehub?: any; minimax?: OpenAICompatibleKeyVault; mistral?: OpenAICompatibleKeyVault; From c154cb584a46bd4a88bf3b12b279c492afbca71b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 19 Nov 2024 02:54:04 +0000 Subject: [PATCH 2/7] :bookmark: chore(release): v1.32.0 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [Version 1.32.0](https://github.com/lobehub/lobe-chat/compare/v1.31.11...v1.32.0) Released on **2024-11-19** #### ✨ Features - **misc**: Add support InternLM (书生浦语) provider.
Improvements and Fixes #### What's improved * **misc**: Add support InternLM (书生浦语) provider, closes [#4711](https://github.com/lobehub/lobe-chat/issues/4711) ([aaae059](https://github.com/lobehub/lobe-chat/commit/aaae059))
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
--- CHANGELOG.md | 25 +++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a229a321703c..3ba72a478506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ # Changelog +## [Version 1.32.0](https://github.com/lobehub/lobe-chat/compare/v1.31.11...v1.32.0) + +Released on **2024-11-19** + +#### ✨ Features + +- **misc**: Add support InternLM (书生浦语) provider. + +
+ +
+Improvements and Fixes + +#### What's improved + +- **misc**: Add support InternLM (书生浦语) provider, closes [#4711](https://github.com/lobehub/lobe-chat/issues/4711) ([aaae059](https://github.com/lobehub/lobe-chat/commit/aaae059)) + +
+ +
+ +[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) + +
+ ### [Version 1.31.11](https://github.com/lobehub/lobe-chat/compare/v1.31.10...v1.31.11) Released on **2024-11-18** diff --git a/package.json b/package.json index 0ec223125084..acd8539f5303 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lobehub/chat", - "version": "1.31.11", + "version": "1.32.0", "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.", "keywords": [ "framework", From 415d7721ec677122ae4f0fa602c7f5540ee0a1c5 Mon Sep 17 00:00:00 2001 From: Yao Cai <67412196+cy948@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:32:36 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=90=9B=20fix:=20keyword=20search=20fo?= =?UTF-8?q?r=20chat=20history=20&=20sessions=20(#4725)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :bug: fix: sql for keyword search * :bug: fix: display search topics in time mode * :bug: fix: keyword search on sessions * :test_tube: test: adjust tests to new keyword search --- .../server/models/__tests__/session.test.ts | 42 +++++++++++++-- src/database/server/models/session.ts | 53 +++++++++++++++---- src/database/server/models/topic.ts | 7 ++- src/store/chat/slices/topic/selectors.ts | 2 +- 4 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/database/server/models/__tests__/session.test.ts b/src/database/server/models/__tests__/session.test.ts index 4d128e5be37e..0a91adb6f7f8 100644 --- a/src/database/server/models/__tests__/session.test.ts +++ b/src/database/server/models/__tests__/session.test.ts @@ -231,8 +231,18 @@ describe('SessionModel', () => { it('should return sessions with matching title', async () => { await serverDB.insert(sessions).values([ - { id: '1', userId, title: 'Hello World', description: 'Some description' }, - { id: '2', userId, title: 'Another Session', description: 'Another description' }, + { id: '1', userId }, + { id: '2', userId }, + ]); + + await serverDB.insert(agents).values([ + { id: 'agent-1', userId, model: 'gpt-3.5-turbo', title: 'Hello, Agent 1' }, + { id: 'agent-2', userId, model: 'gpt-4', title: 'Agent 2' }, + ]); + + await serverDB.insert(agentsToSessions).values([ + { agentId: 'agent-1', sessionId: '1' }, + { agentId: 'agent-2', sessionId: '2' }, ]); const result = await sessionModel.queryByKeyword('hello'); @@ -241,9 +251,21 @@ describe('SessionModel', () => { }); it('should return sessions with matching description', async () => { + // The sessions has no title and desc, + // see: https://github.com/lobehub/lobe-chat/pull/4725 await serverDB.insert(sessions).values([ - { id: '1', userId, title: 'Session 1', description: 'Description with keyword' }, - { id: '2', userId, title: 'Session 2', description: 'Another description' }, + { id: '1', userId }, + { id: '2', userId }, + ]); + + await serverDB.insert(agents).values([ + { id: 'agent-1', userId, model: 'gpt-3.5-turbo', title: 'Agent 1', description: 'Description with Keyword' }, + { id: 'agent-2', userId, model: 'gpt-4', title: 'Agent 2' }, + ]); + + await serverDB.insert(agentsToSessions).values([ + { agentId: 'agent-1', sessionId: '1' }, + { agentId: 'agent-2', sessionId: '2' }, ]); const result = await sessionModel.queryByKeyword('keyword'); @@ -253,11 +275,23 @@ describe('SessionModel', () => { it('should return sessions with matching title or description', async () => { await serverDB.insert(sessions).values([ + { id: '1', userId }, + { id: '2', userId }, + { id: '3', userId }, + ]); + + await serverDB.insert(agents).values([ { id: '1', userId, title: 'Title with keyword', description: 'Some description' }, { id: '2', userId, title: 'Another Session', description: 'Description with keyword' }, { id: '3', userId, title: 'Third Session', description: 'Third description' }, ]); + await serverDB.insert(agentsToSessions).values([ + { agentId: '1', sessionId: '1' }, + { agentId: '2', sessionId: '2' }, + { agentId: '3', sessionId: '3' }, + ]); + const result = await sessionModel.queryByKeyword('keyword'); expect(result).toHaveLength(2); expect(result.map((s) => s.id)).toEqual(['1', '2']); diff --git a/src/database/server/models/session.ts b/src/database/server/models/session.ts index 136104da2ead..01d4bb287d86 100644 --- a/src/database/server/models/session.ts +++ b/src/database/server/models/session.ts @@ -61,7 +61,7 @@ export class SessionModel { const keywordLowerCase = keyword.toLowerCase(); - const data = await this.findSessions({ keyword: keywordLowerCase }); + const data = await this.findSessionsByKeywords({ keyword: keywordLowerCase }); return data.map((item) => this.mapSessionItem(item as any)); } @@ -281,15 +281,15 @@ export class SessionModel { pinned !== undefined ? eq(sessions.pinned, pinned) : eq(sessions.userId, this.userId), keyword ? or( - like( - sql`lower(${sessions.title})` as unknown as Column, - `%${keyword.toLowerCase()}%`, - ), - like( - sql`lower(${sessions.description})` as unknown as Column, - `%${keyword.toLowerCase()}%`, - ), - ) + like( + sql`lower(${sessions.title})` as unknown as Column, + `%${keyword.toLowerCase()}%`, + ), + like( + sql`lower(${sessions.description})` as unknown as Column, + `%${keyword.toLowerCase()}%`, + ), + ) : eq(sessions.userId, this.userId), group ? eq(sessions.groupId, group) : isNull(sessions.groupId), ), @@ -297,4 +297,37 @@ export class SessionModel { with: { agentsToSessions: { columns: {}, with: { agent: true } }, group: true }, }); } + + async findSessionsByKeywords(params: { + current?: number; + keyword: string; + pageSize?: number; + }) { + const { keyword, pageSize = 9999, current = 0 } = params; + const offset = current * pageSize; + const results = await serverDB.query.agents.findMany({ + limit: pageSize, + offset, + orderBy: [desc(agents.updatedAt)], + where: and( + eq(agents.userId, this.userId), + or( + like( + sql`lower(${agents.title})` as unknown as Column, + `%${keyword.toLowerCase()}%`, + ), + like( + sql`lower(${agents.description})` as unknown as Column, + `%${keyword.toLowerCase()}%`, + ), + ) + ), + with: { agentsToSessions: { columns: {}, with: { session: true } } }, + }); + try { + // @ts-expect-error + return results.map((item) => item.agentsToSessions[0].session); + } catch {} + return [] + } } diff --git a/src/database/server/models/topic.ts b/src/database/server/models/topic.ts index 7dac6002baf3..128bd2d801aa 100644 --- a/src/database/server/models/topic.ts +++ b/src/database/server/models/topic.ts @@ -85,7 +85,12 @@ export class TopicModel { serverDB .select() .from(messages) - .where(and(eq(messages.topicId, topics.id), or(matchKeyword(messages.content)))), + .where( + and( + eq(messages.topicId, topics.id), + matchKeyword(messages.content) + ) + ), ), ), ), diff --git a/src/store/chat/slices/topic/selectors.ts b/src/store/chat/slices/topic/selectors.ts index a05d4deba710..07d54dcf535d 100644 --- a/src/store/chat/slices/topic/selectors.ts +++ b/src/store/chat/slices/topic/selectors.ts @@ -42,7 +42,7 @@ const currentActiveTopicSummary = (s: ChatStoreState): ChatTopicSummary | undefi const isCreatingTopic = (s: ChatStoreState) => s.creatingTopic; const groupedTopicsSelector = (s: ChatStoreState): GroupedTopic[] => { - const topics = currentTopics(s); + const topics = displayTopics(s); if (!topics) return []; const favTopics = currentFavTopics(s); From 7e9e71acccd6957dcac2f9aed9d92009d1c7bd83 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Tue, 19 Nov 2024 11:32:49 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=92=84=20style:=20support=20o1=20mode?= =?UTF-8?q?ls=20using=20streaming=20(#4732)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/libs/agent-runtime/github/index.ts | 2 +- src/libs/agent-runtime/openai/index.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/agent-runtime/github/index.ts b/src/libs/agent-runtime/github/index.ts index fd7fa6f280bc..2612bc6fe697 100644 --- a/src/libs/agent-runtime/github/index.ts +++ b/src/libs/agent-runtime/github/index.ts @@ -10,7 +10,7 @@ export const LobeGithubAI = LobeOpenAICompatibleFactory({ const { model } = payload; if (o1Models.has(model)) { - return pruneO1Payload(payload) as any; + return { ...pruneO1Payload(payload), stream: false } as any; } return { ...payload, stream: payload.stream ?? true }; diff --git a/src/libs/agent-runtime/openai/index.ts b/src/libs/agent-runtime/openai/index.ts index d4d718df4b49..9c42965cce07 100644 --- a/src/libs/agent-runtime/openai/index.ts +++ b/src/libs/agent-runtime/openai/index.ts @@ -17,7 +17,6 @@ export const pruneO1Payload = (payload: ChatStreamPayload) => ({ role: message.role === 'system' ? 'user' : message.role, })), presence_penalty: 0, - stream: false, temperature: 1, top_p: 1, }); From 41d92d8382551fbe2bb027717036a5aa883a9e0a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 19 Nov 2024 03:40:37 +0000 Subject: [PATCH 5/7] :bookmark: chore(release): v1.32.1 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### [Version 1.32.1](https://github.com/lobehub/lobe-chat/compare/v1.32.0...v1.32.1) Released on **2024-11-19** #### 🐛 Bug Fixes - **misc**: Keyword search for chat history & sessions. #### 💄 Styles - **misc**: Support o1 models using streaming.
Improvements and Fixes #### What's fixed * **misc**: Keyword search for chat history & sessions, closes [#4725](https://github.com/lobehub/lobe-chat/issues/4725) ([415d772](https://github.com/lobehub/lobe-chat/commit/415d772)) #### Styles * **misc**: Support o1 models using streaming, closes [#4732](https://github.com/lobehub/lobe-chat/issues/4732) ([7e9e71a](https://github.com/lobehub/lobe-chat/commit/7e9e71a))
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
--- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ba72a478506..0e0cb3e3b60c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ # Changelog +### [Version 1.32.1](https://github.com/lobehub/lobe-chat/compare/v1.32.0...v1.32.1) + +Released on **2024-11-19** + +#### 🐛 Bug Fixes + +- **misc**: Keyword search for chat history & sessions. + +#### 💄 Styles + +- **misc**: Support o1 models using streaming. + +
+ +
+Improvements and Fixes + +#### What's fixed + +- **misc**: Keyword search for chat history & sessions, closes [#4725](https://github.com/lobehub/lobe-chat/issues/4725) ([415d772](https://github.com/lobehub/lobe-chat/commit/415d772)) + +#### Styles + +- **misc**: Support o1 models using streaming, closes [#4732](https://github.com/lobehub/lobe-chat/issues/4732) ([7e9e71a](https://github.com/lobehub/lobe-chat/commit/7e9e71a)) + +
+ +
+ +[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) + +
+ ## [Version 1.32.0](https://github.com/lobehub/lobe-chat/compare/v1.31.11...v1.32.0) Released on **2024-11-19** diff --git a/package.json b/package.json index acd8539f5303..93d6ddae715b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lobehub/chat", - "version": "1.32.0", + "version": "1.32.1", "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.", "keywords": [ "framework", From f711cefca3ca87f81eee825dfce4e8c4fb3bb2f8 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Tue, 19 Nov 2024 21:52:44 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20perf:=20fix=20slow=20d?= =?UTF-8?q?elete=20file=20sql=20(#4738)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/database/server/models/file.ts | 37 ++++++++++++++++++++++++++++++ src/server/routers/lambda/file.ts | 5 ---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/database/server/models/file.ts b/src/database/server/models/file.ts index f26b7e23071c..189065490174 100644 --- a/src/database/server/models/file.ts +++ b/src/database/server/models/file.ts @@ -1,5 +1,6 @@ import { asc, count, eq, ilike, inArray, notExists, or, sum } from 'drizzle-orm'; import { and, desc, like } from 'drizzle-orm/expressions'; +import type { PgTransaction } from 'drizzle-orm/pg-core'; import { serverDBEnv } from '@/config/db'; import { serverDB } from '@/database/server/core/db'; @@ -9,6 +10,9 @@ import { FileItem, NewFile, NewGlobalFile, + chunks, + embeddings, + fileChunks, files, globalFiles, knowledgeBaseFiles, @@ -68,6 +72,10 @@ export class FileModel { const fileHash = file.fileHash!; return await serverDB.transaction(async (trx) => { + // 1. 删除相关的 chunks + await this.deleteFileChunks(trx as any, [id]); + + // 2. 删除文件记录 await trx.delete(files).where(and(eq(files.id, id), eq(files.userId, this.userId))); const result = await trx @@ -107,6 +115,9 @@ export class FileModel { const hashList = fileList.map((file) => file.fileHash!); return await serverDB.transaction(async (trx) => { + // 1. 删除相关的 chunks + await this.deleteFileChunks(trx as any, ids); + // delete the files await trx.delete(files).where(and(inArray(files.id, ids), eq(files.userId, this.userId))); @@ -289,4 +300,30 @@ export class FileModel { ), }); } + + // 抽象出通用的删除 chunks 方法 + private async deleteFileChunks(trx: PgTransaction, fileIds: string[]) { + const BATCH_SIZE = 1000; // 每批处理的数量 + + // 1. 获取所有关联的 chunk IDs + const relatedChunks = await trx + .select({ chunkId: fileChunks.chunkId }) + .from(fileChunks) + .where(inArray(fileChunks.fileId, fileIds)); + + const chunkIds = relatedChunks.map((c) => c.chunkId).filter(Boolean) as string[]; + + if (chunkIds.length === 0) return; + + // 2. 分批处理删除 + for (let i = 0; i < chunkIds.length; i += BATCH_SIZE) { + const batchChunkIds = chunkIds.slice(i, i + BATCH_SIZE); + + await trx.delete(embeddings).where(inArray(embeddings.chunkId, batchChunkIds)); + + await trx.delete(chunks).where(inArray(chunks.id, batchChunkIds)); + } + + return chunkIds; + } } diff --git a/src/server/routers/lambda/file.ts b/src/server/routers/lambda/file.ts index 10b84ef09a57..8b73cc715ca7 100644 --- a/src/server/routers/lambda/file.ts +++ b/src/server/routers/lambda/file.ts @@ -154,8 +154,6 @@ export const fileRouter = router({ removeFile: fileProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => { const file = await ctx.fileModel.delete(input.id); - // delete the orphan chunks - await ctx.chunkModel.deleteOrphanChunks(); if (!file) return; // delele the file from remove from S3 if it is not used by other files @@ -187,9 +185,6 @@ export const fileRouter = router({ .mutation(async ({ input, ctx }) => { const needToRemoveFileList = await ctx.fileModel.deleteMany(input.ids); - // delete the orphan chunks - await ctx.chunkModel.deleteOrphanChunks(); - if (!needToRemoveFileList || needToRemoveFileList.length === 0) return; // remove from S3 From 75a9b008984c31615525f94da6bcfd69e586d38a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 19 Nov 2024 14:00:44 +0000 Subject: [PATCH 7/7] :bookmark: chore(release): v1.32.2 [skip ci] ### [Version 1.32.2](https://github.com/lobehub/lobe-chat/compare/v1.32.1...v1.32.2) Released on **2024-11-19**
Improvements and Fixes
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
--- CHANGELOG.md | 17 +++++++++++++++++ package.json | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e0cb3e3b60c..6d1e30ebe7c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ # Changelog +### [Version 1.32.2](https://github.com/lobehub/lobe-chat/compare/v1.32.1...v1.32.2) + +Released on **2024-11-19** + +
+ +
+Improvements and Fixes + +
+ +
+ +[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top) + +
+ ### [Version 1.32.1](https://github.com/lobehub/lobe-chat/compare/v1.32.0...v1.32.1) Released on **2024-11-19** diff --git a/package.json b/package.json index 93d6ddae715b..231f8623cb26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lobehub/chat", - "version": "1.32.1", + "version": "1.32.2", "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.", "keywords": [ "framework",