wangqifan 2 vuotta sitten
vanhempi
commit
d277b5d018
3 muutettua tiedostoa jossa 205 lisäystä ja 0 poistoa
  1. 14 0
      src/chat-pdf/chat-pdf.controller.ts
  2. 38 0
      src/chat-pdf/chat-pdf.service.ts
  3. 153 0
      views/customerChatPdf.hbs

+ 14 - 0
src/chat-pdf/chat-pdf.controller.ts

@@ -20,6 +20,12 @@ export class ChatPdfController {
         return await this.chatPdfService.ask(q, name)
     }
 
+    @Public()
+    @Post('customerServiceAsk')
+    public async customerAsk(@Body() { q, name }) {
+        return await this.chatPdfService.customerAsk(q, name)
+    }
+
     @Public()
     @Public()
     @Get('ui')
@@ -27,4 +33,12 @@ export class ChatPdfController {
     public async answer() {
         return {}
     }
+
+    @Public()
+    @Public()
+    @Get('customerUi')
+    @Render('customerChatPdf')
+    public async customerAnswer() {
+        return {}
+    }
 }

+ 38 - 0
src/chat-pdf/chat-pdf.service.ts

@@ -248,4 +248,42 @@ export class ChatPdfService {
             throw new InternalServerErrorException(error.message)
         }
     }
+
+    async customerAsk(q: string, name: string) {
+        const keywords = await this.getKeywords(q)
+        const { embedding: keywordEmbedding } = await this.getEmbedding(keywords)
+        const context = this.cutContext((await this.searchEmbedding(name, keywordEmbedding)).map((item) => item.text))
+        if (!context || !context.length) {
+            return {
+                answer: '客服无法回答这个问题,换个试试吧'
+            }
+        }
+        const content = dedent`You are a helpful AI article assistant. '
+        The following are the relevant article content fragments found from the article. 
+        The relevance is sorted from high to low. '
+        You can only answer according to the following content:
+        \`\`\`
+        ${context.join('\n')}
+        \`\`\`
+        You need to carefully consider your answer to ensure that it is based on the context. 
+        If the context does not mention the content or it is uncertain whether it is correct, 
+        please answer "Current context cannot provide effective information."
+        You must use Chinese to respond.`
+        try {
+            const response = await this.openai.createChatCompletion({
+                model: 'gpt35',
+                messages: [
+                    { role: 'user', content },
+                    { role: 'user', content: q }
+                ]
+            })
+            return { answer: response.data.choices[0].message.content }
+        } catch (error) {
+            Logger.error(error.message)
+            if (error.response) {
+                Logger.error(error.response.data)
+            }
+            throw new InternalServerErrorException(error.message)
+        }
+    }
 }

+ 153 - 0
views/customerChatPdf.hbs

@@ -0,0 +1,153 @@
+<!DOCTYPE html>
+<html lang="en" class="h-full">
+    <head>
+        <meta charset="UTF-8" />
+        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
+        <title>ChatPDF(客服)</title>
+        <script type="text/javascript" src="https://unpkg.com/jquery@3.3.1/dist/jquery.min.js"></script>
+        <script src="https://unpkg.com/eruda@3.0.0/eruda.js"></script>
+        <script src="https://cdn.tailwindcss.com"></script>
+        <script>
+            tailwind.config = {
+                corePlugins: {
+                    preflight: false
+                },
+                theme: {
+                    extend: {
+                        colors: {
+                            clifford: "#da373d"
+                        }
+                    }
+                }
+            };
+        </script>
+        <!-- Import style -->
+        <link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" />
+        <!-- Import Vue 3 -->
+        <script src="//unpkg.com/vue@3"></script>
+        <!-- Import component library -->
+        <script src="//unpkg.com/element-plus"></script>
+        <script src="//unpkg.com/@element-plus/icons-vue"></script>
+        <style>
+            html,
+            body {
+                padding: 0;
+                margin: 0;
+            }
+            ::-webkit-scrollbar {
+                width: 0;
+                height: 0;
+                display: none;
+            }
+        </style>
+    </head>
+    <body class="h-full">
+        <div id="app" class="h-full">
+            <el-container class="h-full">
+                <el-main class="flex flex-col">
+                    <el-alert v-if="name" :title="fileName" type="success" effect="dark" @close="clear"></el-alert>
+                    <el-upload
+                        v-else
+                        class="upload-demo"
+                        drag
+                        v-model:file-list="fileList"
+                        action="/api/chat-pdf/upload"
+                        accept="application/pdf"
+                        :on-success="onSuccess"
+                    >
+                        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+                        <div class="el-upload__text">请上传客服回答文档</div>
+                    </el-upload>
+                    <div id="list" class="flex flex-col flex-1 overflow-auto">
+                        <div
+                            v-for="(item,i) in conversations"
+                            :key="i"
+                            class="mt-4 p-3 rounded-t-xl"
+                            :class="item.value.role==='system' ? 'mr-10 bg-slate-300	 rounded-r-xl' : 'ml-10 bg-neutral-200 rounded-l-xl'"
+                        >
+                            <el-icon v-if="item.value.loading" class="is-loading">
+                                <Loading />
+                            </el-icon>
+                            <span v-else>\{{item.value.content}}</span>
+                        </div>
+                    </div>
+                </el-main>
+                <el-footer class="py-4 flex items-end" @keyup.enter.stop.prevent="ask">
+                    <el-input v-model="q" :disabled="!name" placeholder="提问"></el-input>
+                    <el-button class="ml-3" type="primary" :disabled="!name" @click="ask">发送</el-button>
+                </el-footer>
+            </el-container>
+        </div>
+    </body>
+    <script>
+        var app = Vue.createApp({
+            data() {
+                return {
+                    name: "",
+                    fileName: "",
+                    fileList: [],
+                    q: "",
+                    conversations: []
+                };
+            },
+
+            methods: {
+                onSuccess(res, file) {
+                    console.log(res, file);
+                    this.name = res.name;
+                    this.fileName = file.name;
+                },
+                clear() {
+                    this.fileList = [];
+                    this.name = "";
+                    this.fileName = "";
+                    this.q = "";
+                    this.conversations = [];
+                },
+                ask(e) {
+                    console.log(this.q);
+                    if (this.q) {
+                        this.conversations.push(
+                            Vue.ref({
+                                role: "user",
+                                content: this.q
+                            })
+                        );
+                        var msg = Vue.ref({
+                            role: "system",
+                            loading: true,
+                            content: ""
+                        });
+                        this.conversations.push(msg);
+
+                        setTimeout(function () {
+                            $("#list").scrollTop($("#list")[0].scrollHeight);
+                        }, 50);
+                        $.post(
+                            "/api/chat-pdf/customerServiceAsk",
+                            {
+                                name: this.name,
+                                q: this.q
+                            },
+                            function (res) {
+                                console.log(res);
+                                msg.value.loading = false;
+                                msg.value.content = res.answer;
+                                setTimeout(function () {
+                                    $("#list").scrollTop($("#list")[0].scrollHeight);
+                                }, 50);
+                            }
+                        );
+                        this.q = "";
+                    }
+                }
+            }
+        });
+        app.use(ElementPlus);
+        for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+            app.component(key, component);
+        }
+        app.mount("#app");
+    </script>
+</html>