簡介

ChatGPT 訓練至 2022 年。

但是,如果您希望它專門為您提供有關您網站的資訊怎麼辦?最有可能的是,這是不可能的,但不再是了!

OpenAI 推出了他們的新功能 - 助手

現在您可以輕鬆地為您的網站建立索引,然後向 ChatGPT 詢問有關該網站的問題。在本教程中,我們將建立一個系統來索引您的網站並讓您查詢它。我們將:

  • 抓取文件網站地圖。

  • 從網站上的所有頁面中提取資訊。

  • 使用新資訊建立新助理。

  • 建立一個簡單的ChatGPT前端介面並查詢助手。

助手


你的後台工作平台🔌

Trigger.dev 是一個開源程式庫,可讓您使用 NextJS、Remix、Astro 等為您的應用程式建立和監控長時間執行的作業!

 

GiveUsStars

請幫我們一顆星🥹。

這將幫助我們建立更多這樣的文章💖


讓我們開始吧🔥

讓我們建立一個新的 NextJS 專案。

 npx create-next-app@latest

💡 我們使用 NextJS 新的應用程式路由器。安裝專案之前請確保您的節點版本為 18+

讓我們建立一個新的資料庫來保存助手和抓取的頁面。

對於我們的範例,我們將使用 Prisma 和 SQLite。

安裝非常簡單,只需執行:

npm install prisma @prisma/client --save

然後加入架構和資料庫

npx prisma init --datasource-provider sqlite

轉到“prisma/schema.prisma”並將其替換為以下架構:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Docs {
  id         Int      @id @default(autoincrement())
  content    String
  url        String @unique
  identifier String
  @@index([identifier])
}

model Assistant {
  id         Int      @id @default(autoincrement())
  aId        String
  url        String   @unique
}

然後執行

npx prisma db push

這將建立一個新的 SQLite 資料庫(本機檔案),其中包含兩個主表:“Docs”和“Assistant”

  • 「Docs」包含所有抓取的頁面

  • Assistant 包含文件的 URL 和內部 ChatGPT 助理 ID。

讓我們新增 Prisma 客戶端。

建立一個名為「helper」的新資料夾,並新增一個名為「prisma.ts」的新文件,並在其中新增以下程式碼:

import {PrismaClient} from '@prisma/client';

export const prisma = new PrismaClient();

我們稍後可以使用“prisma”變數來查詢我們的資料庫。


ScrapeAndIndex

刮擦和索引

建立 Trigger.dev 帳戶

抓取頁面並為其建立索引是一項長期執行的任務。 我們需要:

  • 抓取網站地圖的主網站元 URL。

  • 擷取網站地圖內的所有頁面。

  • 前往每個頁面並提取內容。

  • 將所有內容儲存到 ChatGPT 助手中。

為此,我們使用 Trigger.dev!

註冊 Trigger.dev 帳號

註冊後,建立一個組織並為您的工作選擇一個專案名稱。

pic1

選擇 Next.js 作為您的框架,並按照將 Trigger.dev 新增至現有 Next.js 專案的流程進行操作。

pic2

否則,請點選專案儀表板側邊欄選單上的「環境和 API 金鑰」。

pic3

複製您的 DEV 伺服器 API 金鑰並執行下面的程式碼片段來安裝 Trigger.dev。

仔細按照說明進行操作。

npx @trigger.dev/cli@latest init

在另一個終端中執行以下程式碼片段,在 Trigger.dev 和您的 Next.js 專案之間建立隧道。

npx @trigger.dev/cli@latest dev

安裝 ChatGPT (OpenAI)

我們將使用OpenAI助手,因此我們必須將其安裝到我們的專案中。

建立新的 OpenAI 帳戶 並產生 API 金鑰。

pic4

點擊下拉清單中的「檢視 API 金鑰」以建立 API 金鑰。

pic5

接下來,透過執行下面的程式碼片段來安裝 OpenAI 套件。

npm install @trigger.dev/openai

將您的 OpenAI API 金鑰新增至「.env.local」檔案。

OPENAI_API_KEY=<your_api_key>

建立一個新目錄“helper”並新增一個新檔案“open.ai.tsx”,其中包含以下內容:

import {OpenAI} from "@trigger.dev/openai";

export const openai = new OpenAI({
    id: "openai",
    apiKey: process.env.OPENAI_API_KEY!,
});

這是我們透過 Trigger.dev 整合封裝的 OpenAI 用戶端。

建立後台作業

讓我們繼續建立一個新的後台作業!

前往“jobs”並建立一個名為“process.documentation.ts”的新檔案。 新增以下程式碼:

import { eventTrigger } from "@trigger.dev/sdk";
import { client } from "@openai-assistant/trigger";
import {object, string} from "zod";
import {JSDOM} from "jsdom";
import {openai} from "@openai-assistant/helper/open.ai";

client.defineJob({
  // This is the unique identifier for your Job; it must be unique across all Jobs in your project.
  id: "process-documentation",
  name: "Process Documentation",
  version: "0.0.1",
  // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
  trigger: eventTrigger({
    name: "process.documentation.event",
    schema: object({
      url: string(),
    })
  }),
  integrations: {
    openai
  },
  run: async (payload, io, ctx) => {
  }
});

我們定義了一個名為「process.documentation.event」的新作業,並新增了一個名為 URL 的必要參數 - 這是我們稍後要傳送的文件 URL。

正如您所看到的,該作業是空的,所以讓我們向其中加入第一個任務。

我們需要獲取網站網站地圖並將其返回。

抓取網站將返回我們需要解析的 HTML。

為此,我們需要安裝 JSDOM。

npm install jsdom --save

並將其導入到我們文件的頂部:

import {JSDOM} from "jsdom";

現在,我們可以新增第一個任務。

用「runTask」包裝我們的程式碼很重要,這可以讓 Trigger.dev 將其與其他任務分開。觸發特殊架構將任務拆分為不同的進程,因此 Vercel 無伺服器逾時不會影響它們。 這是第一個任務的程式碼:

const getSiteMap = await io.runTask("grab-sitemap", async () => {
  const data = await (await fetch(payload.url)).text();
  const dom = new JSDOM(data);
  const sitemap = dom.window.document.querySelector('[rel="sitemap"]')?.getAttribute('href');
  return new URL(sitemap!, payload.url).toString();
});
  • 我們透過 HTTP 請求從 URL 取得整個 HTML。

  • 我們將其轉換為 JS 物件。

  • 我們找到網站地圖 URL。

  • 我們解析它並返回它。

接下來,我們需要抓取網站地圖,提取所有 URL 並返回它們。

讓我們安裝“Lodash”——陣列結構的特殊函數。

npm install lodash @types/lodash --save

這是任務的程式碼:

export const makeId = (length: number) => {
    let text = '';
    const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

    for (let i = 0; i < length; i += 1) {
        text += possible.charAt(Math.floor(Math.random() * possible.length));
    }
    return text;
};

const {identifier, list} = await io.runTask("load-and-parse-sitemap", async () => {
    const urls = /(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/g;
    const identifier = makeId(5);
    const data = await (await fetch(getSiteMap)).text();
    // @ts-ignore
    return {identifier, list: chunk(([...new Set(data.match(urls))] as string[]).filter(f => f.includes(payload.url)).map(p => ({identifier, url: p})), 25)};
});
  • 我們建立一個名為 makeId 的新函數來為所有頁面產生隨機辨識碼。

  • 我們建立一個新任務並加入正規表示式來提取每個可能的 URL

  • 我們發送一個 HTTP 請求來載入網站地圖並提取其所有 URL。

  • 我們將 URL「分塊」為 25 個元素的陣列(如果有 100 個元素,則會有四個 25 個元素的陣列)

接下來,讓我們建立一個新作業來處理每個 URL。

這是完整的程式碼:

function getElementsBetween(startElement: Element, endElement: Element) {
    let currentElement = startElement;
    const elements = [];

    // Traverse the DOM until the endElement is reached
    while (currentElement && currentElement !== endElement) {
        currentElement = currentElement.nextElementSibling!;

        // If there's no next sibling, go up a level and continue
        if (!currentElement) {
            // @ts-ignore
            currentElement = startElement.parentNode!;
            startElement = currentElement;
            if (currentElement === endElement) break;
            continue;
        }

        // Add the current element to the list
        if (currentElement && currentElement !== endElement) {
            elements.push(currentElement);
        }
    }

    return elements;
}

const processContent = client.defineJob({
  // This is the unique identifier for your Job; it must be unique across all Jobs in your project.
  id: "process-content",
  name: "Process Content",
  version: "0.0.1",
  // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction
  trigger: eventTrigger({
    name: "process.content.event",
    schema: object({
      url: string(),
      identifier: string(),
    })
  }),
  run: async (payload, io, ctx) => {
    return io.runTask('grab-content', async () => {
        // We first grab a raw html of the content from the website
        const data = await (await fetch(payload.url)).text();

        // We load it with JSDOM so we can manipulate it
        const dom = new JSDOM(data);

        // We remove all the scripts and styles from the page
        dom.window.document.querySelectorAll('script, style').forEach((el) => el.remove());

        // We grab all the titles from the page
        const content = Array.from(dom.window.document.querySelectorAll('h1, h2, h3, h4, h5, h6'));

        // We grab the last element so we can get the content between the last element and the next element
        const lastElement = content[content.length - 1]?.parentElement?.nextElementSibling!;
        const elements = [];

        // We loop through all the elements and grab the content between each title
        for (let i = 0; i < content.length; i++) {
            const element = content[i];
            const nextElement = content?.[i + 1] || lastElement;
            const elementsBetween = getElementsBetween(element, nextElement);
            elements.push({
                title: element.textContent, content: elementsBetween.map((el) => el.textContent).join('\n')
            });
        }

        // We create a raw text format of all the content
        const page = `
        ----------------------------------
        url: ${payload.url}\n
        ${elements.map((el) => `${el.title}\n${el.content}`).join('\n')}

        ----------------------------------
        `;

        // We save it to our database
        await prisma.docs.upsert({
            where: {
                url: payload.url
            }, update: {
                content: page, identifier: payload.identifier
            }, create: {
                url: payload.url, content: page, identifier: payload.identifier
            }
        });
    });
  },
});
  • 我們從 URL 中獲取內容(之前從網站地圖中提取)

  • 我們用JSDOM解析它

  • 我們刪除頁面上存在的所有可能的“