diff --git a/package.json b/package.json index 24997e36c161ab7d6a77847f88289fbc91ba1f42..54ade2fab9c7aaec22838b74c9fd1bc7f0d62610 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "preview": "vite preview --port 4173" }, "dependencies": { + "fetch-event-source": "^1.0.0-alpha.2", "guess": "^1.0.2", "vue": "^3.2.37" }, diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..33ad091d26d8a9dc95ebdf616e217d985ec215b8 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src/App.vue b/src/App.vue index 633a5dfe4e547c48bfa93740a290ba5ba370930a..8617273c511d2c2d03711c66dec86d28b006557b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,47 +1,374 @@ - - + + + + + \ No newline at end of file diff --git a/src/js/config.js b/src/js/config.js new file mode 100644 index 0000000000000000000000000000000000000000..cdd5a36b00a209ad5c5d46d842034a39d9699e59 --- /dev/null +++ b/src/js/config.js @@ -0,0 +1,16 @@ +export default { + getData () { + return { + "code": 200, + "data": { + "id": 128897, + "name": "小羊驼", + "create_time": 1684920670901, + "app": "llm_rep", + "resource_type": "app", + "ext": "{\"mode\": \"chat\", \"model\": \"chatglm2-6b\", \"api_url\": \"https://gpu-pod647d498393e106496a046e94-8000.node.inscode.run/v1\", \"api_type\": \"openai\", \"robot_img\": null, \"api_max_token\": \"2048\", \"default_prompt\": \"请翻译成英文:你是谁?\", \"max_request_len\": \"10000\", \"prompt_template\": \"\", \"api_prompt_prefix\": \"\", \"show_profile_setting\": false}" + }, + "message": "success" + } + } +} \ No newline at end of file diff --git a/src/js/openai.js b/src/js/openai.js new file mode 100644 index 0000000000000000000000000000000000000000..733b496da6f8448f1ed38d3769d4e5f14b75acff --- /dev/null +++ b/src/js/openai.js @@ -0,0 +1,127 @@ +import { fetchEventSource } from '@microsoft/fetch-event-source'; +import Prompt from './prompt.js' + +class OpenAI { + + constructor(config) { + this.config = config + this.abortController = null + this.callback = null + this.temperature = parseFloat(config?.temperature??0.7) + } + + createCompletion (prompt, history, context, callback) { + const config = this.config + + const abortController = new AbortController(); + const signal = abortController.signal; + this.abortController = abortController + + this.callback = callback + + const mode = config?.mode??'chat' + const token = config?.token??'empty' + const url = config.api_url + (mode === 'chat' ? '/chat/completions' : '/completions') + const stop = config?.stop??'[DONE]' + const max_tokens = config?.api_max_token??512 + const model = config?.model??'vicuna-13b-all-v1.1' + const temperature = this.temperature + const top_p = config?.top_p??1.0 + let stop_key = config?.stop_key??null + if (stop_key !== null && stop_key !== '') { + stop_key = stop_key.split(';;') + } + const data = { + model: model, + max_tokens: parseInt(max_tokens), + temperature: parseFloat(temperature), + top_p: parseFloat(top_p), + stream: true, + stop: stop_key + // prefix: prefix + } + if (mode === 'chat') { + data.messages = Prompt.getPromptByChatMode(config, context, history) + } else { + data.prompt = Prompt.getPromptByTemplate(config, context, prompt) + } + // const prefix = config?.prompt_prefix??'' + const fetcher = fetchEventSource(url, { + method: 'POST', + signal: signal, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + onmessage(msg) { + // if the server emits an error message, throw an exception + // so it gets handled by the onerror callback below: + if (msg && msg?.data) { + if (msg?.data === stop) { + if (callback?.onclose) { + callback?.onclose() + } + abortController.abort(); + return + + } else { + console.info(msg.data) + const jsonData = JSON.parse(msg.data) + // 和上面重复触发,只留一个 + // if (jsonData.choices[0].finish_reason === 'stop') { + // if (callback?.onclose) { + // callback?.onclose() + // } + // return + // } + let message = null + if (mode === 'chat') { + message = jsonData?.choices[0]?.message?.content + if (typeof message === 'undefined') { + message = jsonData?.choices[0]?.delta?.content + } + if (typeof message === 'undefined') { + message = '' + } + } else { + message = jsonData?.choices[0]?.text + } + + callback?.onmessage(message, true) + } + + } + + + // if (msg.event === 'FatalError') { + // throw new FatalError(msg.data); + // } + }, + onclose() { + if (callback?.onclose) { + callback?.onclose() + } + // if the server closes the connection unexpectedly, retry: + }, + onerror(err) { + if (callback?.onerror) { + callback?.onerror(err) + } + } + }); + + } + + close () { + if (this.abortController) { + this.abortController.abort() + + if (this.callback && this.callback?.onclose) { + this.callback.onclose() + } + } + } + +} + +export default OpenAI \ No newline at end of file diff --git a/src/js/prompt.js b/src/js/prompt.js new file mode 100644 index 0000000000000000000000000000000000000000..ef9538714c7aa7cdc5578c3068febdbd8dea8453 --- /dev/null +++ b/src/js/prompt.js @@ -0,0 +1,59 @@ +const default_max_token = 1024 + +const getContextContent = (context, max_token=default_max_token) => { + if (context && context.length > 0) { + let id = 0 + let len = context.length + let contextContent = '' + while(contextContent.length < max_token && id < len) { + if (context[id].page_content.length + contextContent.length < max_token) { + contextContent = contextContent + '\n' + context[id].page_content + } + id++ + } + return contextContent + } else { + return '' + } +} + +export default { + + getPromptByTemplate: (config, context, prompt, history) => { + + if (config.api_prompt_prefix) { + prompt = config?.api_prompt_prefix + ' ' + prompt + } + if (config?.prompt_template) { + + const contextContent = getContextContent(context, config?.max_request_len??1024) + return config?.prompt_template.replace(/\{question\}/ig, prompt).replace(/\{context\}/ig, contextContent) + } else { + return prompt + } + }, + getPromptByChatMode (config, context, history) { + + const history_length = Math.min(Math.max(parseInt(config?.history_length??4), 4), 10) + let message = [] + if (history && history.length >= 2) { + const end = history.length - 2 // 结束位置 + const start = Math.max(history.length - 2 - history_length + 1, 0) // 开始位置 + for(let id = start; id <= end; id++) { + const item = history[id] + message.push({ + "role": item.user === 'AI' ? "system" : "user", + "content": item.message + }) + } + } + if (config?.prompt_template) { + const contextContent = getContextContent(context, config?.max_request_len??1024) + message.unshift({ + "role": "user", + "content": config.prompt_template.replace(/\{question\}/ig, '').replace(/\{context\}/ig, contextContent).replace(/\{user_call_name\}/ig, config.user_call_name) + }) + } + return message + } +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index 90e6400b4d8ad8aba0c1caa53874eb4b81380648..c84568c0f0bbfe2eaa2d49104ec9b0594e04aacf 100644 --- a/src/main.js +++ b/src/main.js @@ -2,5 +2,6 @@ import { createApp } from 'vue' import App from './App.vue' import './assets/main.css' +import './style.css' createApp(App).mount('#app') diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000000000000000000000000000000000000..bd6213e1dfe6b0a79ce7d8b37d0d2dc70f0250bb --- /dev/null +++ b/src/style.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000000000000000000000000000000000000..55d1fb970d81b2818576dcc725e6c52fc84af066 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} +