构建一个全栈Chatgpt应用程序
#javascript #react #openai #convex

在这篇文章中,我们将浏览一个完整的聊天应用程序,并添加一些功能。

我们将使用React和Vite,尽管任何React框架都起作用。我们将使用Convex作为后端,我们的服务器端功能将运行,以及我们将存储应用程序数据的位置。

要看到此操作,代码为here,并且运行版本是here(目前)。您可以克隆它并在读书中使用一些配置自己运行,但请继续阅读以查看有关构建自己的逐步指南。此外,如果您想查看添加auth。

要自己运行,您需要做一个OpenAI accountget an API key

如果您熟悉凸,可以跳到步骤2。

0. bootstrap vite react应用程序

如果您已经有一个应用程序,我们可以制作一个:

npm create vite@latest

我选择了convex-chatgpt作为项目名称,React作为框架,而Javascript作为变体。

此时,如果我们运行:

npm install
npm run dev

我们有一个本地运行的WebApp。

让我们更改src/App.jsx列出消息并具有提交消息的表格:

app.jsx

import {useState} from "react";
import "./App.css";

function App() {
  const messages = [
    {author: "user", body: "Hello, world"},
  ];
  const sendMessage = body =>
    console.log("Trying to send: " + body);
  const [newMessageText, setNewMessageText] =
    useState("");

  return (
    <div className="App">
      {messages.map((message, i) => (
        <p key={i}>
          <span>{message.author}: </span>
          <span style={{ whiteSpace: "pre-wrap" }}>
            {message.body ?? "..."}
          </span>
        </p>
      ))}
      <form onSubmit={(e) => {
        e.preventDefault();
        setNewMessageText("");
        sendMessage(newMessageText);
      }}>
        <input
          value={newMessageText}
          onChange={e => setNewMessageText(e.target.value)}
          placeholder="Write a message…"
        />
        <input type="submit" value="Send" disabled={!newMessageText} />
      </form>
    </div>
  );
}

export default App;

在这一点上,我们有一个应用程序,该应用在尝试发送消息时向控制台显示静态列表和日志。

1.添加凸

这与凸quickstart相似。在新的终端(留下另一个运行npm run dev):

npm install convex
npx convex init

如果您以前没有使用过凸,请提示您登录,创建一个帐户等。我选择了convex-chatgpt作为项目名称。

让我们添加发送消息和列表消息的功能。我们将添加在凸(在服务器上)运行的函数,这些函数将使用querymutation读取并写入数据库。

添加convex/messages.ts

import { query, mutation } from "./_generated/server";

export const list = query(async ({ db }) => {
  return await db.query("messages").collect();
});

export const send = mutation(async ({ db }, { body }) => {
  await db.insert("messages", {
    body,
    author: "user",
  });
  const botMessageId = await db.insert("messages", {
    author: "assistant",
  });
});

在与运行npm run dev的一个单独终端中,运行npx convex dev。这将这些功能将这些功能部署到您的新凸后端,并且在编辑功能时,它将自动重新部署。这将要求您将部署URL保存到.env.env.local文件中:这些点在您的生产和开发部署的凸后端。我们将与开发部署合作。

要从内部React访问凸,您需要在应用程序的顶层添加<ConvexProvider>上下文。编辑main.jsx

...
import { ConvexProvider, ConvexReactClient } from "convex/react";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <ConvexProvider client={convex}>
      <App />
    </ConvexProvider>
  </React.StrictMode>,
);

然后我们可以从App.jsx中使用这些功能:

function App() {
  const messages = useQuery("messages:list") || [];
  const sendMessage = useMutation("messages:send");
    ...

太好了!现在,我们从数据库中写了消息。试试看!那些新的凸面的人会惊讶地发现,添加新消息将自动导致useQuery("messages:list")挂钩返回新消息。这是凸的魔力的一部分。了解有关它的更多信息here

您可以在仪表板:npx convex dashboard中查看数据。

您可能会注意到查询和突变由filename:function命名。查看有关此here的更多信息。

您会注意到每次发送消息时,都会从助手那里发出消息。接下来,让我们从chatgpt更新该消息。

2.将消息发送到chatgpt API

到目前为止,我们一直在使用querymutation,它们是与数据库相互作用的凸函数。为了与外部服务互动,我们需要在action中进行此操作 - 这是一个凸功能,它不在确定性环境中,也不是数据库事务的一部分。这使我们释放了副作用的事情,例如向OpenAI的API提出请求。我们将通过几个步骤进行操作。

获取消息发送到chatgpt

在我们的send突变中,我们可以使用数据库查询获取最新的10条消息,以发送到chatgpt:

export const send = mutation(async ({ db }, { body }) => {
  //... insert messages to the table
  const messages = await db
    .query("messages")
    .order("desc")
    .filter((q) => q.neq(q.field("body"), undefined))
    .take(21);
  messages.reverse();
  return { messages, botMessageId };
});

此订购消息按降序订购(除非使用其他index),除非创建时间排序),从而滤除没有身体的消息(例如,新的占位符系统消息),并采用第一个21最近的消息)。然后,它颠倒了列表,因此按上升时间顺序排序。我选择了21个,以便我们有10对用户/机器人消息,然后是最新提示。它返回将用作聊天完成API的输入的消息,我们将下一步添加。

创建动作

我们将使用openai NPM软件包。您可以安装它:

npm install openai

制作一个新文件:convex/openai.js

"use node";
import { Configuration, OpenAIApi } from "openai";
import { action } from "../_generated/server";

export const chat = action(async ({ runMutation }, { body }) => {
  const { messages, botMessageId } = await runMutation("messages:send", { body });
  const fail = async (reason) => throw new Error(reason);
  // Grab the API key from environment variables
  // Specify this in your dashboard: `npx convex dashboard`
  const apiKey = process.env.OPENAI_API_KEY;
  if (!apiKey) {
    await fail("Add your OPENAI_API_KEY as an env variable");
  }
  const configuration = new Configuration({ apiKey });
  const openai = new OpenAIApi(configuration);

  const openaiResponse = await openai.createChatCompletion({
    model: "gpt-3.5-turbo",
    messages: [
      {
        role: "system",
        content: instructions,
      },
      ...messages.map(({ body, author }) => ({
        role: author,
        content: body,
      })),
    ],
  });
  if (openaiResponse.status !== 200) {
    await fail("OpenAI error: " + openaiResponse.statusText);
  }
  const body = openaiResponse.data.choices[0].message.content;
    console.log("Response: " + body);
});

这将首先使用我们修改的发送突变发送消息,获取消息ID列表和消息ID列表,以使用Bot的消息更新。然后,它将向gpt-3.5-turbo型号提出请求,并传递一个带有指令的系统消息(目前为硬编码),然后是每条消息。我们将身体和作者领域变成了角色和内容。有关API的更多详细信息,请参见他们的文档here

将您的API键添加到仪表板

要对OpenAI的请求进行身份验证,您需要将API key添加到凸仪表板中,因此您的操作(在凸面服务器上运行)可以访问它。使用npx convex dashboard进入仪表板,然后在左图中进入设置,然后添加键的键。当您在那里时,您可以通过切换左图中的下拉列表将其添加为Env变量。

Dashboard side panel

触发UI的动作

我们可以将对useQuery("messages:send")的呼叫更改为

const sendMessage = useAction("openai:chat");

请注意,该操作是由文件(openai)和功能(chat)的路径解决的。 API是相同的(以一个参数为body),所以它是一个替换!

此时,如果运行它,它将向Chatgpt and Console发送请求。凸面打印凸功能将登录到浏览器的控制台,尽管您还可以在dashboard中看到它们,以向您自己证明它在云中运行。现在让我们在UI中显示它们。

通过回复更新消息

为了将响应获取到聊天消息中,我们希望更新我们添加的空占位符消息。将每个呼吁称为突变作为交易。一旦我们调用了第一个突变,这些消息就会发布到数据库,UI可以在对OpenAI的慢速通话之前显示新消息。一旦获得响应,我们将更新Bot的消息,UI将通过messages:list查询自动更新。我们将在convex/messages.js文件中添加一个新突变:

// An `internalMuation` can only be called from other server functions.
export const update = internalMutation(async ({ db }, { messageId, patch }) => {
  await db.patch(messageId, patch);
});

这修补了指定的消息。例如,您可以通过{body: "hi"}将消息的身体更改为hi,例如。自第一个突变返回botMessageId以来,我们可以从convex/openai.js中的动作中使用它:

export const chat = action(async ({ runMutation }, { body }) => {
  const { messages, botMessageId } = await runMutation("messages:send", { body });

  // Call OpenAI

  await runMutation("messages:update", {
    messageId: botMessageId, 
    patch: {
      body: openaiResponse.data.choices[0].message.content,
      // Track how many tokens we're using for various messages
      usage: openaiResponse.data.usage,
      updatedAt: Date.now(),
      // How long it took OpenAI
      ms: Number(openaiResponse.headers["openai-processing-ms"]),
    }
  });
});

当我们在这里时,我们可以将大量有趣的数据与身体一起存储到消息中。凸的数据库足够灵活,可以存储strings, numbers, javascript objects, koude32s, etc.,同时还可以让您钉住schema,以获取打字稿类型,自动完成等。

ð现在您已经回复了您的消息!

概括

在这篇文章中,我们制作了一个全栈网络应用程序,可以与Openai的Chatgpt API聊天。让我们知道in Discord您的想法,如果您想看到一些扩展: