From 0d6897e1801899365991397f1832c0cf93b64771 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Thu, 4 May 2023 10:53:55 +0800 Subject: [PATCH] feat: lafClaude --- package.json | 1 - pnpm-lock.yaml | 7 -- public/docs/intro.md | 32 +++++---- src/api/chat.ts | 1 + src/api/fetch.ts | 102 +++++++++++++------------- src/constants/chat.ts | 3 +- src/constants/model.ts | 22 +++++- src/pages/api/chat/chat.ts | 22 +++--- src/pages/api/chat/saveChat.ts | 4 +- src/pages/api/openapi/chat/chat.ts | 1 - src/pages/api/openapi/chat/lafGpt.ts | 1 - src/pages/chat/index.tsx | 4 +- src/service/events/generateQA.ts | 24 +++---- src/service/events/generateVector.ts | 11 ++- src/service/plugins/searchKb.ts | 60 +++++++--------- src/service/utils/auth.ts | 51 ++++++------- src/service/utils/chat/claude.ts | 103 +++++++++++++++++++++++++++ src/service/utils/chat/index.ts | 29 ++++---- src/service/utils/chat/openai.ts | 12 ++-- src/utils/chat/claude.ts | 3 + src/utils/chat/index.ts | 46 ++++-------- src/utils/chat/openai.ts | 19 ++++- 22 files changed, 327 insertions(+), 231 deletions(-) create mode 100644 src/service/utils/chat/claude.ts create mode 100644 src/utils/chat/claude.ts diff --git a/package.json b/package.json index 2a2cd62b6..e010c98bc 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "axios": "^1.3.3", "crypto": "^1.0.1", "dayjs": "^1.11.7", - "delay": "^5.0.0", "eventsource-parser": "^0.1.0", "formidable": "^2.1.1", "framer-motion": "^9.0.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c72b383b3..348796876 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,6 @@ specifiers: axios: ^1.3.3 crypto: ^1.0.1 dayjs: ^1.11.7 - delay: ^5.0.0 eslint: 8.34.0 eslint-config-next: 13.1.6 eventsource-parser: ^0.1.0 @@ -83,7 +82,6 @@ dependencies: axios: registry.npmmirror.com/axios/1.3.3 crypto: registry.npmmirror.com/crypto/1.0.1 dayjs: registry.npmmirror.com/dayjs/1.11.7 - delay: 5.0.0 eventsource-parser: registry.npmmirror.com/eventsource-parser/0.1.0 formidable: registry.npmmirror.com/formidable/2.1.1 framer-motion: registry.npmmirror.com/framer-motion/9.0.6_biqbaboplfbrettd7655fr4n2y @@ -288,11 +286,6 @@ packages: dev: false optional: true - /delay/5.0.0: - resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} - engines: {node: '>=10'} - dev: false - /fsevents/2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} diff --git a/public/docs/intro.md b/public/docs/intro.md index c496d95f6..fbfd6ff1e 100644 --- a/public/docs/intro.md +++ b/public/docs/intro.md @@ -3,39 +3,41 @@ [Git 仓库](https://github.com/c121914yu/FastGPT) ### 交流群/问题反馈 + 扫码满了,加个小号,定时拉 -wx号: fastgpt123 +wx 号: fastgpt123 ![](/imgs/wx300.jpg) - ### 快速开始 -1. 使用手机号注册账号。 -2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。 + +1. 使用手机号注册账号。 +2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。 3. 如果填写了自己的 openai 账号,使用时会直接用你的账号。如果没有填写,需要付费使用平台的账号。 -4. 进入模型页,创建一个模型,建议直接用 ChatGPT。 -5. 在模型列表点击【对话】,即可使用 API 进行聊天。 +4. 进入模型页,创建一个模型,建议直接用 ChatGPT。 +5. 在模型列表点击【对话】,即可使用 API 进行聊天。 ### 价格表 + 如果使用了自己的 Api Key,不会计费。可以在账号页,看到详细账单。单纯使用 chatGPT 模型进行对话,只有一个计费项目。使用知识库时,包含**对话**和**索引**生成两个计费项。 | 计费项 | 价格: 元/ 1K tokens(包含上下文)| -| --- | --- | +| --- | --- | +| claude - 对话 | 免费 | | chatgpt - 对话 | 0.03 | | 知识库 - 对话 | 0.03 | | 知识库 - 索引 | 0.004 | | 文件拆分 | 0.03 | - ### 定制 prompt -1. 进入模型编辑页 -2. 调整温度和提示词 +1. 进入模型编辑页 +2. 调整温度和提示词 3. 使用该模型对话。每次对话时,提示词和温度都会自动注入,方便管理个人的模型。建议把自己日常经常需要使用的 5~10 个方向预设好。 ### 知识库 -1. 创建模型时选择【知识库】 -2. 进入模型编辑页 -3. 导入数据,可以选择手动导入,或者选择文件导入。文件导入会自动调用 chatGPT 理解文件内容,并生成知识库。 -4. 使用该模型对话。 +1. 创建模型时选择【知识库】 +2. 进入模型编辑页 +3. 导入数据,可以选择手动导入,或者选择文件导入。文件导入会自动调用 chatGPT 理解文件内容,并生成知识库。 +4. 使用该模型对话。 -注意:使用知识库模型对话时,tokens 消耗会加快。 +注意:使用知识库模型对话时,tokens 消耗会加快。 diff --git a/src/api/chat.ts b/src/api/chat.ts index c246a8536..04daf14a0 100644 --- a/src/api/chat.ts +++ b/src/api/chat.ts @@ -24,6 +24,7 @@ export const delChatHistoryById = (id: string) => GET(`/chat/removeHistory?id=${ */ export const postSaveChat = (data: { modelId: string; + newChatId: '' | string; chatId: '' | string; prompts: ChatItemType[]; }) => POST('/chat/saveChat', data); diff --git a/src/api/fetch.ts b/src/api/fetch.ts index ab07e15d5..f3280a014 100644 --- a/src/api/fetch.ts +++ b/src/api/fetch.ts @@ -1,5 +1,5 @@ import { getToken } from '../utils/user'; -import { SYSTEM_PROMPT_PREFIX } from '@/constants/chat'; +import { SYSTEM_PROMPT_HEADER, NEW_CHATID_HEADER } from '@/constants/chat'; interface StreamFetchProps { url: string; @@ -8,60 +8,56 @@ interface StreamFetchProps { abortSignal: AbortController; } export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) => - new Promise<{ responseText: string; systemPrompt: string }>(async (resolve, reject) => { - try { - const res = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: getToken() || '' - }, - body: JSON.stringify(data), - signal: abortSignal.signal - }); - const reader = res.body?.getReader(); - if (!reader) return; + new Promise<{ responseText: string; systemPrompt: string; newChatId: string }>( + async (resolve, reject) => { + try { + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: getToken() || '' + }, + body: JSON.stringify(data), + signal: abortSignal.signal + }); + const reader = res.body?.getReader(); + if (!reader) return; - const decoder = new TextDecoder(); - let responseText = ''; - let systemPrompt = ''; + const decoder = new TextDecoder(); - const read = async () => { - try { - const { done, value } = await reader?.read(); - if (done) { - if (res.status === 200) { - resolve({ responseText, systemPrompt }); - } else { - const parseError = JSON.parse(responseText); - reject(parseError?.message || '请求异常'); + const systemPrompt = decodeURIComponent(res.headers.get(SYSTEM_PROMPT_HEADER) || ''); + const newChatId = decodeURIComponent(res.headers.get(NEW_CHATID_HEADER) || ''); + + let responseText = ''; + + const read = async () => { + try { + const { done, value } = await reader?.read(); + if (done) { + if (res.status === 200) { + resolve({ responseText, systemPrompt, newChatId }); + } else { + const parseError = JSON.parse(responseText); + reject(parseError?.message || '请求异常'); + } + + return; } - - return; + const text = decoder.decode(value).replace(//g, '\n'); + responseText += text; + onMessage(text); + read(); + } catch (err: any) { + if (err?.message === 'The user aborted a request.') { + return resolve({ responseText, systemPrompt, newChatId }); + } + reject(typeof err === 'string' ? err : err?.message || '请求异常'); } - let text = decoder.decode(value).replace(//g, '\n'); - // check system prompt - if (text.includes(SYSTEM_PROMPT_PREFIX)) { - const arr = text.split(SYSTEM_PROMPT_PREFIX); - systemPrompt = arr.pop() || ''; - - text = arr.join(''); - } - responseText += text; - onMessage(text); - - read(); - } catch (err: any) { - if (err?.message === 'The user aborted a request.') { - return resolve({ responseText, systemPrompt }); - } - reject(typeof err === 'string' ? err : err?.message || '请求异常'); - } - }; - - read(); - } catch (err: any) { - console.log(err, '===='); - reject(typeof err === 'string' ? err : err?.message || '请求异常'); + }; + read(); + } catch (err: any) { + console.log(err, '===='); + reject(typeof err === 'string' ? err : err?.message || '请求异常'); + } } - }); + ); diff --git a/src/constants/chat.ts b/src/constants/chat.ts index b1a6b92dc..7f580ff34 100644 --- a/src/constants/chat.ts +++ b/src/constants/chat.ts @@ -1,4 +1,5 @@ -export const SYSTEM_PROMPT_PREFIX = 'SYSTEM_PROMPT:'; +export const SYSTEM_PROMPT_HEADER = 'System-Prompt-Header'; +export const NEW_CHATID_HEADER = 'Chat-Id-Header'; export enum ChatRoleEnum { System = 'System', diff --git a/src/constants/model.ts b/src/constants/model.ts index 53055e020..e6cb1afa0 100644 --- a/src/constants/model.ts +++ b/src/constants/model.ts @@ -8,13 +8,17 @@ export enum OpenAiChatEnum { 'GPT4' = 'gpt-4', 'GPT432k' = 'gpt-4-32k' } +export enum ClaudeEnum { + 'Claude' = 'Claude' +} -export type ChatModelType = `${OpenAiChatEnum}`; +export type ChatModelType = `${OpenAiChatEnum}` | `${ClaudeEnum}`; export type ChatModelItemType = { chatModel: ChatModelType; name: string; contextMaxToken: number; + systemMaxToken: number; maxTemperature: number; price: number; }; @@ -24,6 +28,7 @@ export const ChatModelMap = { chatModel: OpenAiChatEnum.GPT35, name: 'ChatGpt', contextMaxToken: 4096, + systemMaxToken: 3000, maxTemperature: 1.5, price: 3 }, @@ -31,6 +36,7 @@ export const ChatModelMap = { chatModel: OpenAiChatEnum.GPT4, name: 'Gpt4', contextMaxToken: 8000, + systemMaxToken: 4000, maxTemperature: 1.5, price: 30 }, @@ -38,12 +44,24 @@ export const ChatModelMap = { chatModel: OpenAiChatEnum.GPT432k, name: 'Gpt4-32k', contextMaxToken: 32000, + systemMaxToken: 4000, maxTemperature: 1.5, price: 30 + }, + [ClaudeEnum.Claude]: { + chatModel: ClaudeEnum.Claude, + name: 'Claude(免费体验)', + contextMaxToken: 9000, + systemMaxToken: 2500, + maxTemperature: 1, + price: 0 } }; -export const chatModelList: ChatModelItemType[] = [ChatModelMap[OpenAiChatEnum.GPT35]]; +export const chatModelList: ChatModelItemType[] = [ + ChatModelMap[OpenAiChatEnum.GPT35], + ChatModelMap[ClaudeEnum.Claude] +]; export enum ModelStatusEnum { running = 'running', diff --git a/src/pages/api/chat/chat.ts b/src/pages/api/chat/chat.ts index 64f8d3f99..c44f52e2e 100644 --- a/src/pages/api/chat/chat.ts +++ b/src/pages/api/chat/chat.ts @@ -42,11 +42,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await connectToDatabase(); let startTime = Date.now(); - const { model, showModelDetail, content, userApiKey, systemApiKey, userId } = await authChat({ - modelId, - chatId, - authorization - }); + const { model, showModelDetail, content, userOpenAiKey, systemAuthKey, userId } = + await authChat({ + modelId, + chatId, + authorization + }); const modelConstantsData = ChatModelMap[model.chat.chatModel]; @@ -56,8 +57,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // 使用了知识库搜索 if (model.chat.useKb) { const { code, searchPrompt } = await searchKb({ - userApiKey, - systemApiKey, + userOpenAiKey, prompts, similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity, model, @@ -86,10 +86,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // 发出请求 const { streamResponse } = await modelServiceToolMap[model.chat.chatModel].chatCompletion({ - apiKey: userApiKey || systemApiKey, + apiKey: userOpenAiKey || systemAuthKey, temperature: +temperature, messages: prompts, - stream: true + stream: true, + res, + chatId }); console.log('api response time:', `${(Date.now() - startTime) / 1000}s`); @@ -108,7 +110,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // 只有使用平台的 key 才计费 pushChatBill({ - isPay: !userApiKey, + isPay: !userOpenAiKey, chatModel: model.chat.chatModel, userId, chatId, diff --git a/src/pages/api/chat/saveChat.ts b/src/pages/api/chat/saveChat.ts index 0494fea0e..bd754953c 100644 --- a/src/pages/api/chat/saveChat.ts +++ b/src/pages/api/chat/saveChat.ts @@ -9,7 +9,8 @@ import mongoose from 'mongoose'; /* 聊天内容存存储 */ export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - const { chatId, modelId, prompts } = req.body as { + const { chatId, modelId, prompts, newChatId } = req.body as { + newChatId: '' | string; chatId: '' | string; modelId: string; prompts: ChatItemType[]; @@ -35,6 +36,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // 没有 chatId, 创建一个对话 if (!chatId) { const { _id } = await Chat.create({ + _id: newChatId ? new mongoose.Types.ObjectId(newChatId) : undefined, userId, modelId, content, diff --git a/src/pages/api/openapi/chat/chat.ts b/src/pages/api/openapi/chat/chat.ts index 5bbadf836..58ac64956 100644 --- a/src/pages/api/openapi/chat/chat.ts +++ b/src/pages/api/openapi/chat/chat.ts @@ -65,7 +65,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const similarity = ModelVectorSearchModeMap[model.chat.searchMode]?.similarity || 0.22; const { code, searchPrompt } = await searchKb({ - systemApiKey: apiKey, prompts, similarity, model, diff --git a/src/pages/api/openapi/chat/lafGpt.ts b/src/pages/api/openapi/chat/lafGpt.ts index a58b4518a..c0e3542ea 100644 --- a/src/pages/api/openapi/chat/lafGpt.ts +++ b/src/pages/api/openapi/chat/lafGpt.ts @@ -116,7 +116,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // 获取向量匹配到的提示词 const { searchPrompt } = await searchKb({ - systemApiKey: apiKey, similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity, prompts, model, diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index 5d73e2e32..7698ff0e1 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -206,7 +206,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { }; // 流请求,获取数据 - const { responseText, systemPrompt } = await streamFetch({ + let { responseText, systemPrompt, newChatId } = await streamFetch({ url: '/api/chat/chat', data: { prompt, @@ -234,10 +234,10 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { return; } - let newChatId = ''; // save chat record try { newChatId = await postSaveChat({ + newChatId, // 如果有newChatId,会自动以这个Id创建对话框 modelId, chatId, prompts: [ diff --git a/src/service/events/generateQA.ts b/src/service/events/generateQA.ts index eb6a5fc87..7a69593ef 100644 --- a/src/service/events/generateQA.ts +++ b/src/service/events/generateQA.ts @@ -45,12 +45,12 @@ export async function generateQA(next = false): Promise { const textList: string[] = dataItem.textList.slice(-5); // 获取 openapi Key - let userApiKey = '', - systemApiKey = ''; + let userOpenAiKey = '', + systemAuthKey = ''; try { const key = await getApiKey({ model: OpenAiChatEnum.GPT35, userId: dataItem.userId }); - userApiKey = key.userApiKey; - systemApiKey = key.systemApiKey; + userOpenAiKey = key.userOpenAiKey; + systemAuthKey = key.systemAuthKey; } catch (error: any) { if (error?.code === 501) { // 余额不够了, 清空该记录 @@ -73,18 +73,18 @@ export async function generateQA(next = false): Promise { textList.map((text) => modelServiceToolMap[OpenAiChatEnum.GPT35] .chatCompletion({ - apiKey: userApiKey || systemApiKey, + apiKey: userOpenAiKey || systemAuthKey, temperature: 0.8, messages: [ { obj: ChatRoleEnum.System, value: `你是出题人 - ${dataItem.prompt || '下面是"一段长文本"'} - 从中选出5至20个题目和答案.答案详细.按格式返回: Q1: - A1: - Q2: - A2: - ...` +${dataItem.prompt || '下面是"一段长文本"'} +从中选出5至20个题目和答案.答案详细.按格式返回: Q1: +A1: +Q2: +A2: +...` }, { obj: 'Human', @@ -98,7 +98,7 @@ export async function generateQA(next = false): Promise { console.log(`split result length: `, result.length); // 计费 pushSplitDataBill({ - isPay: !userApiKey && result.length > 0, + isPay: !userOpenAiKey && result.length > 0, userId: dataItem.userId, type: 'QA', textLen: responseMessages.map((item) => item.value).join('').length, diff --git a/src/service/events/generateVector.ts b/src/service/events/generateVector.ts index 1c661c6b6..42c118f04 100644 --- a/src/service/events/generateVector.ts +++ b/src/service/events/generateVector.ts @@ -2,7 +2,6 @@ import { openaiCreateEmbedding } from '../utils/chat/openai'; import { getApiKey } from '../utils/auth'; import { openaiError2 } from '../errorCode'; import { PgClient } from '@/service/pg'; -import { embeddingModel } from '@/constants/model'; export async function generateVector(next = false): Promise { if (process.env.queueTask !== '1') { @@ -42,11 +41,10 @@ export async function generateVector(next = false): Promise { dataId = dataItem.id; // 获取 openapi Key - let userApiKey, systemApiKey; + let userOpenAiKey; try { - const res = await getApiKey({ model: embeddingModel, userId: dataItem.userId }); - userApiKey = res.userApiKey; - systemApiKey = res.systemApiKey; + const res = await getApiKey({ model: 'gpt-3.5-turbo', userId: dataItem.userId }); + userOpenAiKey = res.userOpenAiKey; } catch (error: any) { if (error?.code === 501) { await PgClient.delete('modelData', { @@ -63,8 +61,7 @@ export async function generateVector(next = false): Promise { const { vectors } = await openaiCreateEmbedding({ textArr: [dataItem.q], userId: dataItem.userId, - userApiKey, - systemApiKey + userOpenAiKey }); // 更新 pg 向量和状态数据 diff --git a/src/service/plugins/searchKb.ts b/src/service/plugins/searchKb.ts index 48974ccaf..e07bed86a 100644 --- a/src/service/plugins/searchKb.ts +++ b/src/service/plugins/searchKb.ts @@ -3,22 +3,20 @@ import { ModelDataStatusEnum, ModelVectorSearchModeEnum, ChatModelMap } from '@/ import { ModelSchema } from '@/types/mongoSchema'; import { openaiCreateEmbedding } from '../utils/chat/openai'; import { ChatRoleEnum } from '@/constants/chat'; -import { sliceTextByToken } from '@/utils/chat'; +import { modelToolMap } from '@/utils/chat'; import { ChatItemSimpleType } from '@/types/chat'; /** * use openai embedding search kb */ export const searchKb = async ({ - userApiKey, - systemApiKey, + userOpenAiKey, prompts, similarity = 0.2, model, userId }: { - userApiKey?: string; - systemApiKey: string; + userOpenAiKey?: string; prompts: ChatItemSimpleType[]; model: ModelSchema; userId: string; @@ -33,8 +31,7 @@ export const searchKb = async ({ async function search(textArr: string[] = []) { // 获取提示词的向量 const { vectors: promptVectors } = await openaiCreateEmbedding({ - userApiKey, - systemApiKey, + userOpenAiKey, userId, textArr }); @@ -81,11 +78,24 @@ export const searchKb = async ({ ].filter((item) => item); const systemPrompts = await search(searchArr); - // filter system prompt - if ( - systemPrompts.length === 0 && - model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity - ) { + // filter system prompts. + const filterRateMap: Record = { + 1: [1], + 2: [0.7, 0.3] + }; + const filterRate = filterRateMap[systemPrompts.length] || filterRateMap[0]; + + const filterSystemPrompt = filterRate + .map((rate, i) => + modelToolMap[model.chat.chatModel].sliceText({ + text: systemPrompts[i], + length: Math.floor(modelConstantsData.systemMaxToken * rate) + }) + ) + .join('\n'); + + /* 高相似度+不回复 */ + if (!filterSystemPrompt && model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity) { return { code: 201, searchPrompt: { @@ -95,7 +105,7 @@ export const searchKb = async ({ }; } /* 高相似度+无上下文,不添加额外知识,仅用系统提示词 */ - if (systemPrompts.length === 0 && model.chat.searchMode === ModelVectorSearchModeEnum.noContext) { + if (!filterSystemPrompt && model.chat.searchMode === ModelVectorSearchModeEnum.noContext) { return { code: 200, searchPrompt: model.chat.systemPrompt @@ -107,25 +117,7 @@ export const searchKb = async ({ }; } - /* 有匹配情况下,system 添加知识库内容。 */ - - // filter system prompts. max 70% tokens - const filterRateMap: Record = { - 1: [0.7], - 2: [0.5, 0.2] - }; - const filterRate = filterRateMap[systemPrompts.length] || filterRateMap[0]; - - const filterSystemPrompt = filterRate - .map((rate, i) => - sliceTextByToken({ - model: model.chat.chatModel, - text: systemPrompts[i], - length: Math.floor(modelConstantsData.contextMaxToken * rate) - }) - ) - .join('\n'); - + /* 有匹配 */ return { code: 200, searchPrompt: { @@ -133,9 +125,9 @@ export const searchKb = async ({ value: ` ${model.chat.systemPrompt} ${ - model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity ? `不回答知识库外的内容.` : '' + model.chat.searchMode === ModelVectorSearchModeEnum.hightSimilarity ? '不回答知识库外的内容.' : '' } -知识库内容为: ${filterSystemPrompt}' +知识库内容为: '${filterSystemPrompt}' ` } }; diff --git a/src/service/utils/auth.ts b/src/service/utils/auth.ts index 61d4dd431..f2b426134 100644 --- a/src/service/utils/auth.ts +++ b/src/service/utils/auth.ts @@ -4,15 +4,10 @@ import { Chat, Model, OpenApi, User } from '../mongo'; import type { ModelSchema } from '@/types/mongoSchema'; import type { ChatItemSimpleType } from '@/types/chat'; import mongoose from 'mongoose'; -import { defaultModel } from '@/constants/model'; +import { ClaudeEnum, defaultModel } from '@/constants/model'; import { formatPrice } from '@/utils/user'; import { ERROR_ENUM } from '../errorCode'; -import { - ChatModelType, - OpenAiChatEnum, - embeddingModel, - EmbeddingModelType -} from '@/constants/model'; +import { ChatModelType, OpenAiChatEnum } from '@/constants/model'; /* 校验 token */ export const authToken = (token?: string): Promise => { @@ -34,13 +29,7 @@ export const authToken = (token?: string): Promise => { }; /* 获取 api 请求的 key */ -export const getApiKey = async ({ - model, - userId -}: { - model: ChatModelType | EmbeddingModelType; - userId: string; -}) => { +export const getApiKey = async ({ model, userId }: { model: ChatModelType; userId: string }) => { const user = await User.findById(userId); if (!user) { return Promise.reject({ @@ -51,29 +40,29 @@ export const getApiKey = async ({ const keyMap = { [OpenAiChatEnum.GPT35]: { - userApiKey: user.openaiKey || '', - systemApiKey: process.env.OPENAIKEY as string + userOpenAiKey: user.openaiKey || '', + systemAuthKey: process.env.OPENAIKEY as string }, [OpenAiChatEnum.GPT4]: { - userApiKey: user.openaiKey || '', - systemApiKey: process.env.OPENAIKEY as string + userOpenAiKey: user.openaiKey || '', + systemAuthKey: process.env.OPENAIKEY as string }, [OpenAiChatEnum.GPT432k]: { - userApiKey: user.openaiKey || '', - systemApiKey: process.env.OPENAIKEY as string + userOpenAiKey: user.openaiKey || '', + systemAuthKey: process.env.OPENAIKEY as string }, - [embeddingModel]: { - userApiKey: user.openaiKey || '', - systemApiKey: process.env.OPENAIKEY as string + [ClaudeEnum.Claude]: { + userOpenAiKey: '', + systemAuthKey: process.env.LAFKEY as string } }; // 有自己的key - if (keyMap[model].userApiKey) { + if (keyMap[model].userOpenAiKey) { return { user, - userApiKey: keyMap[model].userApiKey, - systemApiKey: '' + userOpenAiKey: keyMap[model].userOpenAiKey, + systemAuthKey: '' }; } @@ -87,8 +76,8 @@ export const getApiKey = async ({ return { user, - userApiKey: '', - systemApiKey: keyMap[model].systemApiKey + userOpenAiKey: '', + systemAuthKey: keyMap[model].systemAuthKey }; }; @@ -176,11 +165,11 @@ export const authChat = async ({ ]); } // 获取 user 的 apiKey - const { userApiKey, systemApiKey } = await getApiKey({ model: model.chat.chatModel, userId }); + const { userOpenAiKey, systemAuthKey } = await getApiKey({ model: model.chat.chatModel, userId }); return { - userApiKey, - systemApiKey, + userOpenAiKey, + systemAuthKey, content, userId, model, diff --git a/src/service/utils/chat/claude.ts b/src/service/utils/chat/claude.ts new file mode 100644 index 000000000..59f589bff --- /dev/null +++ b/src/service/utils/chat/claude.ts @@ -0,0 +1,103 @@ +import { modelToolMap } from '@/utils/chat'; +import { ChatCompletionType, StreamResponseType } from './index'; +import { ChatRoleEnum } from '@/constants/chat'; +import axios from 'axios'; +import mongoose from 'mongoose'; +import { NEW_CHATID_HEADER } from '@/constants/chat'; +import { ClaudeEnum } from '@/constants/model'; + +/* 模型对话 */ +export const lafClaudChat = async ({ + apiKey, + messages, + stream, + chatId, + res +}: ChatCompletionType) => { + const conversationId = chatId || String(new mongoose.Types.ObjectId()); + // create a new chat + !chatId && + messages.filter((item) => item.obj === 'Human').length === 1 && + res?.setHeader(NEW_CHATID_HEADER, conversationId); + + // get system prompt + const systemPrompt = messages + .filter((item) => item.obj === 'System') + .map((item) => item.value) + .join('\n'); + const systemPromptText = systemPrompt ? `这是我的知识:'${systemPrompt}'\n` : ''; + + const prompt = systemPromptText + messages[messages.length - 1].value; + + const lafResponse = await axios.post( + 'https://hnvacz.laf.run/claude-gpt', + { + prompt, + stream, + conversationId + }, + { + headers: { + Authorization: apiKey + }, + timeout: stream ? 40000 : 240000, + responseType: stream ? 'stream' : 'json' + } + ); + + let responseText = ''; + let totalTokens = 0; + + if (!stream) { + responseText = lafResponse.data?.text || ''; + } + + return { + streamResponse: lafResponse, + responseMessages: messages.concat({ obj: ChatRoleEnum.AI, value: responseText }), + responseText, + totalTokens + }; +}; + +/* openai stream response */ +export const lafClaudStreamResponse = async ({ + stream, + chatResponse, + prompts +}: StreamResponseType) => { + try { + let responseContent = ''; + + try { + const decoder = new TextDecoder(); + for await (const chunk of chatResponse.data as any) { + if (stream.destroyed) { + // 流被中断了,直接忽略后面的内容 + break; + } + const content = decoder.decode(chunk); + responseContent += content; + content && stream.push(content.replace(/\n/g, '
')); + } + } catch (error) { + console.log('pipe error', error); + } + // count tokens + const finishMessages = prompts.concat({ + obj: ChatRoleEnum.AI, + value: responseContent + }); + const totalTokens = modelToolMap[ClaudeEnum.Claude].countTokens({ + messages: finishMessages + }); + + return { + responseContent, + totalTokens, + finishMessages + }; + } catch (error) { + return Promise.reject(error); + } +}; diff --git a/src/service/utils/chat/index.ts b/src/service/utils/chat/index.ts index 5da3b6497..56eea2a45 100644 --- a/src/service/utils/chat/index.ts +++ b/src/service/utils/chat/index.ts @@ -1,19 +1,19 @@ import { ChatItemSimpleType } from '@/types/chat'; import { modelToolMap } from '@/utils/chat'; import type { ChatModelType } from '@/constants/model'; -import { ChatRoleEnum, SYSTEM_PROMPT_PREFIX } from '@/constants/chat'; -import { OpenAiChatEnum } from '@/constants/model'; +import { ChatRoleEnum, SYSTEM_PROMPT_HEADER } from '@/constants/chat'; +import { OpenAiChatEnum, ClaudeEnum } from '@/constants/model'; import { chatResponse, openAiStreamResponse } from './openai'; +import { lafClaudChat, lafClaudStreamResponse } from './claude'; import type { NextApiResponse } from 'next'; import type { PassThrough } from 'stream'; -import delay from 'delay'; export type ChatCompletionType = { apiKey: string; temperature: number; messages: ChatItemSimpleType[]; stream: boolean; - params?: any; + [key: string]: any; }; export type ChatCompletionResponseType = { streamResponse: any; @@ -25,6 +25,9 @@ export type StreamResponseType = { stream: PassThrough; chatResponse: any; prompts: ChatItemSimpleType[]; + res: NextApiResponse; + systemPrompt?: string; + [key: string]: any; }; export type StreamResponseReturnType = { responseContent: string; @@ -65,6 +68,10 @@ export const modelServiceToolMap: Record< model: OpenAiChatEnum.GPT432k, ...data }) + }, + [ClaudeEnum.Claude]: { + chatCompletion: lafClaudChat, + streamResponse: lafClaudStreamResponse } }; @@ -143,14 +150,13 @@ export const resStreamResponse = async ({ prompts }: StreamResponseType & { model: ChatModelType; - res: NextApiResponse; - systemPrompt?: string; }) => { // 创建响应流 res.setHeader('Content-Type', 'text/event-stream;charset-utf-8'); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('X-Accel-Buffering', 'no'); res.setHeader('Cache-Control', 'no-cache, no-transform'); + systemPrompt && res.setHeader(SYSTEM_PROMPT_HEADER, encodeURIComponent(systemPrompt)); stream.pipe(res); const { responseContent, totalTokens, finishMessages } = await modelServiceToolMap[ @@ -158,16 +164,11 @@ export const resStreamResponse = async ({ ].streamResponse({ chatResponse, stream, - prompts + prompts, + res, + systemPrompt }); - await delay(100); - - // push system prompt - !stream.destroyed && - systemPrompt && - stream.push(`${SYSTEM_PROMPT_PREFIX}${systemPrompt.replace(/\n/g, '
')}`); - // close stream !stream.destroyed && stream.push(null); stream.destroy(); diff --git a/src/service/utils/chat/openai.ts b/src/service/utils/chat/openai.ts index 4ae804505..6e2a4e240 100644 --- a/src/service/utils/chat/openai.ts +++ b/src/service/utils/chat/openai.ts @@ -19,18 +19,18 @@ export const getOpenAIApi = (apiKey: string) => { /* 获取向量 */ export const openaiCreateEmbedding = async ({ - userApiKey, - systemApiKey, + userOpenAiKey, userId, textArr }: { - userApiKey?: string; - systemApiKey: string; + userOpenAiKey?: string; userId: string; textArr: string[]; }) => { + const systemAuthKey = process.env.OPENAIKEY as string; + // 获取 chatAPI - const chatAPI = getOpenAIApi(userApiKey || systemApiKey); + const chatAPI = getOpenAIApi(userOpenAiKey || systemAuthKey); // 把输入的内容转成向量 const res = await chatAPI @@ -50,7 +50,7 @@ export const openaiCreateEmbedding = async ({ })); pushGenerateVectorBill({ - isPay: !userApiKey, + isPay: !userOpenAiKey, userId, text: textArr.join(''), tokenLen: res.tokenLen diff --git a/src/utils/chat/claude.ts b/src/utils/chat/claude.ts new file mode 100644 index 000000000..75058d93a --- /dev/null +++ b/src/utils/chat/claude.ts @@ -0,0 +1,3 @@ +export const ClaudeSliceTextByToken = ({ text, length }: { text: string; length: number }) => { + return text.slice(0, length); +}; diff --git a/src/utils/chat/index.ts b/src/utils/chat/index.ts index 868d0b563..07fda67c0 100644 --- a/src/utils/chat/index.ts +++ b/src/utils/chat/index.ts @@ -1,46 +1,30 @@ -import { OpenAiChatEnum } from '@/constants/model'; +import { ClaudeEnum, OpenAiChatEnum } from '@/constants/model'; import type { ChatModelType } from '@/constants/model'; import type { ChatItemSimpleType } from '@/types/chat'; -import { countOpenAIToken, getOpenAiEncMap, adaptChatItem_openAI } from './openai'; -import { ChatCompletionRequestMessage } from 'openai'; - -export type CountTokenType = { messages: ChatItemSimpleType[] }; +import { countOpenAIToken, openAiSliceTextByToken } from './openai'; +import { ClaudeSliceTextByToken } from './claude'; export const modelToolMap: Record< ChatModelType, { - countTokens: (data: CountTokenType) => number; - adaptChatMessages: (data: CountTokenType) => ChatCompletionRequestMessage[]; + countTokens: (data: { messages: ChatItemSimpleType[] }) => number; + sliceText: (data: { text: string; length: number }) => string; } > = { [OpenAiChatEnum.GPT35]: { - countTokens: ({ messages }: CountTokenType) => - countOpenAIToken({ model: OpenAiChatEnum.GPT35, messages }), - adaptChatMessages: adaptChatItem_openAI + countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT35, messages }), + sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data }) }, [OpenAiChatEnum.GPT4]: { - countTokens: ({ messages }: CountTokenType) => - countOpenAIToken({ model: OpenAiChatEnum.GPT4, messages }), - adaptChatMessages: adaptChatItem_openAI + countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT4, messages }), + sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data }) }, [OpenAiChatEnum.GPT432k]: { - countTokens: ({ messages }: CountTokenType) => - countOpenAIToken({ model: OpenAiChatEnum.GPT432k, messages }), - adaptChatMessages: adaptChatItem_openAI + countTokens: ({ messages }) => countOpenAIToken({ model: OpenAiChatEnum.GPT432k, messages }), + sliceText: (data) => openAiSliceTextByToken({ model: OpenAiChatEnum.GPT35, ...data }) + }, + [ClaudeEnum.Claude]: { + countTokens: () => 0, + sliceText: ClaudeSliceTextByToken } }; - -export const sliceTextByToken = ({ - model = 'gpt-3.5-turbo', - text, - length -}: { - model: ChatModelType; - text: string; - length: number; -}) => { - const enc = getOpenAiEncMap()[model]; - const encodeText = enc.encode(text); - const decoder = new TextDecoder(); - return decoder.decode(enc.decode(encodeText.slice(0, length))); -}; diff --git a/src/utils/chat/openai.ts b/src/utils/chat/openai.ts index 51d64378c..b39f876da 100644 --- a/src/utils/chat/openai.ts +++ b/src/utils/chat/openai.ts @@ -2,7 +2,7 @@ import { encoding_for_model, type Tiktoken } from '@dqbd/tiktoken'; import type { ChatItemSimpleType } from '@/types/chat'; import { ChatRoleEnum } from '@/constants/chat'; import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai'; - +import { OpenAiChatEnum } from '@/constants/model'; import Graphemer from 'graphemer'; const textDecoder = new TextDecoder(); @@ -52,7 +52,7 @@ export function countOpenAIToken({ model }: { messages: ChatItemSimpleType[]; - model: 'gpt-3.5-turbo' | 'gpt-4' | 'gpt-4-32k'; + model: `${OpenAiChatEnum}`; }) { function getChatGPTEncodingText( messages: { role: 'system' | 'user' | 'assistant'; content: string; name?: string }[], @@ -104,3 +104,18 @@ export function countOpenAIToken({ return text2TokensLen(getOpenAiEncMap()[model], getChatGPTEncodingText(adaptMessages, model)); } + +export const openAiSliceTextByToken = ({ + model = 'gpt-3.5-turbo', + text, + length +}: { + model: `${OpenAiChatEnum}`; + text: string; + length: number; +}) => { + const enc = getOpenAiEncMap()[model]; + const encodeText = enc.encode(text); + const decoder = new TextDecoder(); + return decoder.decode(enc.decode(encodeText.slice(0, length))); +};