ChatGPT 訓練至 2022 年。
但是,如果您希望它專門為您提供有關您網站的資訊怎麼辦?最有可能的是,這是不可能的,但不再是了!
OpenAI 推出了他們的新功能 - 助手。
現在您可以輕鬆地為您的網站建立索引,然後向 ChatGPT 詢問有關該網站的問題。在本教程中,我們將建立一個系統來索引您的網站並讓您查詢它。我們將:
抓取文件網站地圖。
從網站上的所有頁面中提取資訊。
使用新資訊建立新助理。
建立一個簡單的ChatGPT前端介面並查詢助手。
Trigger.dev 是一個開源程式庫,可讓您使用 NextJS、Remix、Astro 等為您的應用程式建立和監控長時間執行的作業!
請幫我們一顆星🥹。
這將幫助我們建立更多這樣的文章💖
讓我們建立一個新的 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”變數來查詢我們的資料庫。
抓取頁面並為其建立索引是一項長期執行的任務。 我們需要:
抓取網站地圖的主網站元 URL。
擷取網站地圖內的所有頁面。
前往每個頁面並提取內容。
將所有內容儲存到 ChatGPT 助手中。
為此,我們使用 Trigger.dev!
註冊 Trigger.dev 帳號。
註冊後,建立一個組織並為您的工作選擇一個專案名稱。
選擇 Next.js 作為您的框架,並按照將 Trigger.dev 新增至現有 Next.js 專案的流程進行操作。
否則,請點選專案儀表板側邊欄選單上的「環境和 API 金鑰」。
複製您的 DEV 伺服器 API 金鑰並執行下面的程式碼片段來安裝 Trigger.dev。
仔細按照說明進行操作。
npx @trigger.dev/cli@latest init
在另一個終端中執行以下程式碼片段,在 Trigger.dev 和您的 Next.js 專案之間建立隧道。
npx @trigger.dev/cli@latest dev
我們將使用OpenAI助手,因此我們必須將其安裝到我們的專案中。
建立新的 OpenAI 帳戶 並產生 API 金鑰。
點擊下拉清單中的「檢視 API 金鑰」以建立 API 金鑰。
接下來,透過執行下面的程式碼片段來安裝 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
解析它
我們刪除頁面上存在的所有可能的“