在本文中,我们将向您展示如何创建一个可以汇总任何网页内容的便捷 Web 应用程序。使用Next.js实现流畅快速的 Web 体验、使用LangChain处理语言、使用OpenAI生成摘要以及使用Supabase管理和存储矢量数据,我们将共同构建一个强大的工具。
我们都面临着信息过载的问题,网上的内容太多了。通过制作一款提供快速摘要的应用,我们可以帮助人们节省时间并随时了解最新信息。无论您是忙碌的上班族、学生,还是只想了解新闻和文章的人,这款应用都会成为您的得力助手。
我们的应用可让用户输入任何网站网址并快速获得页面的简要摘要。这意味着您无需完整阅读长篇文章、博客文章或研究论文即可了解其要点。
这款摘要应用用途广泛。它可以帮助研究人员浏览学术论文、让新闻爱好者了解最新动态等等。此外,开发人员可以在此应用的基础上创建更多实用功能。
Next.js 是由 Vercel 开发的功能强大且灵活的 React 框架,可帮助开发人员轻松构建服务器端渲染 (SSR) 和静态 Web 应用程序。它结合了 React 的最佳功能和其他功能,可创建优化且可扩展的 Web 应用程序。
Node.js 中的 OpenAI 模块提供了一种与 OpenAI API 交互的方法,使开发人员能够利用 GPT-3 和 GPT-4 等强大的语言模型。此模块可让您将高级 AI 功能集成到 Node.js 应用程序中。
LangChain 是一个功能强大的框架,旨在使用语言模型开发应用程序。它最初是为 Python 开发的,后来被改编为其他语言,包括 Node.js。以下是在 Node.js 环境中对 LangChain 的概述:
LangChain 是一个简化使用大型语言模型 (LLM)创建应用程序的库。它提供了管理 LLM 并将其集成到应用程序中的工具,处理对这些模型的调用链接,并轻松实现复杂的工作流程。
大型语言模型 (LLM)(例如OpenAI 的 GPT-3.5 )经过大量文本数据训练,可以理解和生成类似人类的文本。它们可以生成响应、翻译语言并执行许多其他自然语言处理任务。
Supabase 是一个开源的后端即服务 (BaaS) 平台,旨在帮助开发人员快速构建和部署可扩展的应用程序。它提供了一套工具和服务,可简化数据库管理、身份验证、存储和实时功能,所有这些都建立在PostgreSQL之上
在开始之前,请确保您已准备好以下内容:
首先,我们需要建立一个 Supabase 项目并创建必要的表来存储我们的数据。
前往Supabase并注册一个账户。
创建一个新项目,并记下您的 Supabase URL 和 API 密钥。您稍后会需要这些信息。
在 Supabase 仪表板中创建一个新的 SQL 查询,并运行以下脚本来创建所需的表和函数:
首先,如果我们的矢量存储尚不存在扩展,则创建一个扩展:
create extension if not exists vector;
接下来,创建一个名为“documents”的表。该表将用于以矢量格式存储和嵌入网页内容:
create table if not exists documents ( id bigint primary key generated always as identity, content text, metadata jsonb, embedding vector(1536) );
现在,我们需要一个函数来查询嵌入的数据:
create or replace function match_documents ( query_embedding vector(1536), match_count int default null, filter jsonb default '{}' ) returns table ( id bigint, content text, metadata jsonb, similarity float ) language plpgsql as $$ begin return query select id, content, metadata, 1 - (documents.embedding <=> query_embedding) as similarity from documents where metadata @> filter order by documents.embedding <=> query_embedding limit match_count; end; $$;
接下来,我们需要建立表来存储网页的详细信息:
create table if not exists files ( id bigint primary key generated always as identity, url text not null, created_at timestamp with time zone default timezone('utc'::text, now()) not null );
$ npx create-next-app summarize-page $ cd ./summarize-page
安装所需的依赖项:
npm install @langchain/community @langchain/core @langchain/openai @supabase/supabase-js langchain openai axios
然后,我们将安装 Material UI 来构建我们的界面;请随意使用其他库:
npm install @mui/material @emotion/react @emotion/styled
接下来,我们需要设置 OpenAI 和 Supabase 客户端。在项目中创建一个libs
目录,并添加以下文件。
src/libs/openAI.ts
该文件将配置 OpenAI 客户端。
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai"; const openAIApiKey = process.env.OPENAI_API_KEY; if (!openAIApiKey) throw new Error('OpenAI API Key not found.') export const llm = new ChatOpenAI({ openAIApiKey, modelName: "gpt-3.5-turbo", temperature: 0.9, }); export const embeddings = new OpenAIEmbeddings( { openAIApiKey, }, { maxRetries: 0 } );
llm
:语言模型实例,将生成我们的摘要。
embeddings
:这将为我们的文档创建嵌入,有助于查找类似的内容。src/libs/supabaseClient.ts
该文件将配置 Supabase 客户端。
import { createClient } from "@supabase/supabase-js"; const supabaseUrl = process.env.SUPABASE_URL || ""; const supabaseAnonKey = process.env.SUPABASE_ANON_KEY || ""; if (!supabaseUrl) throw new Error("Supabase URL not found."); if (!supabaseAnonKey) throw new Error("Supabase Anon key not found."); export const supabaseClient = createClient(supabaseUrl, supabaseAnonKey);
supabaseClient
:与我们的 Supabase 数据库交互的 Supabase 客户端实例。创建一个services
目录,并添加以下文件来处理获取内容和管理文件。
src/services/content.ts
该服务将获取网页内容并通过删除 HTML 标签、脚本和样式来清理它。
import axios from "axios"; export async function getContent(url: string): Promise<string> { let htmlContent: string = ""; const response = await axios.get(url as string); htmlContent = response.data; if (!htmlContent) return ""; // Remove unwanted elements and tags return htmlContent .replace(/style="[^"]*"/gi, "") .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "") .replace(/\s*on\w+="[^"]*"/gi, "") .replace( /<script(?![^>]*application\/ld\+json)[^>]*>[\s\S]*?<\/script>/gi, "" ) .replace(/<[^>]*>/g, "") .replace(/\s+/g, " "); }
此函数获取给定 URL 的 HTML 内容并通过删除样式、脚本和 HTML 标签来清理它。
src/services/file.ts
该服务将网页内容保存到Supabase并检索摘要。
import { embeddings, llm } from "@/libs/openAI"; import { supabaseClient } from "@/libs/supabaseClient"; import { SupabaseVectorStore } from "@langchain/community/vectorstores/supabase"; import { StringOutputParser } from "@langchain/core/output_parsers"; import { ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate, } from "@langchain/core/prompts"; import { RunnablePassthrough, RunnableSequence, } from "@langchain/core/runnables"; import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; import { formatDocumentsAsString } from "langchain/util/document"; export interface IFile { id?: number | undefined; url: string; created_at?: Date | undefined; } export async function saveFile(url: string, content: string): Promise<IFile> { const doc = await supabaseClient .from("files") .select() .eq("url", url) .single<IFile>(); if (!doc.error && doc.data?.id) return doc.data; const { data, error } = await supabaseClient .from("files") .insert({ url }) .select() .single<IFile>(); if (error) throw error; const splitter = new RecursiveCharacterTextSplitter({ separators: ["\n\n", "\n", " ", ""], }); const output = await splitter.createDocuments([content]); const docs = output.map((d) => ({ ...d, metadata: { ...d.metadata, file_id: data.id }, })); await SupabaseVectorStore.fromDocuments(docs, embeddings, { client: supabaseClient, tableName: "documents", queryName: "match_documents", }); return data; } export async function getSummarization(fileId: number): Promise<string> { const vectorStore = await SupabaseVectorStore.fromExistingIndex(embeddings, { client: supabaseClient, tableName: "documents", queryName: "match_documents", }); const retriever = vectorStore.asRetriever({ filter: (rpc) => rpc.filter("metadata->>file_id", "eq", fileId), k: 2, }); const SYSTEM_TEMPLATE = `Use the following pieces of context, explain what is it about and summarize it. If you can't explain it, just say that you don't know, don't try to make up some explanation. ---------------- {context}`; const messages = [ SystemMessagePromptTemplate.fromTemplate(SYSTEM_TEMPLATE), HumanMessagePromptTemplate.fromTemplate("{format_answer}"), ]; const prompt = ChatPromptTemplate.fromMessages(messages); const chain = RunnableSequence.from([ { context: retriever.pipe(formatDocumentsAsString), format_answer: new RunnablePassthrough(), }, prompt, llm, new StringOutputParser(), ]); const format_summarization = ` Give it title, subject, description, and the conclusion of the context in this format, replace the brackets with the actual content: [Write the title here] By: [Name of the author or owner or user or publisher or writer or reporter if possible, otherwise leave it "Not Specified"] [Write the subject, it could be a long text, at least minimum of 300 characters] ---------------- [Write the description in here, it could be a long text, at least minimum of 1000 characters] Conclusion: [Write the conclusion in here, it could be a long text, at least minimum of 500 characters] `; const summarization = await chain.invoke(format_summarization); return summarization; }
saveFile
:将文件及其内容保存到 Supabase,将内容分成可管理的块,并将它们存储在向量存储中。
getSummarization
:从向量存储中检索相关文档并使用 OpenAI 生成摘要。现在,让我们创建一个 API 处理程序来处理内容并生成摘要。
pages/api/content.ts
import { getContent } from "@/services/content"; import { getSummarization, saveFile } from "@/services/file"; import { NextApiRequest, NextApiResponse } from "next"; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== "POST") return res.status(404).json({ message: "Not found" }); const { body } = req; try { const content = await getContent(body.url); const file = await saveFile(body.url, content); const result = await getSummarization(file.id as number); res.status(200).json({ result }); } catch (err) { res.status( 500).json({ error: err }); } }
此 API 处理程序接收 URL、获取内容、将其保存到 Supabase 并生成摘要。它处理我们服务中的saveFile
和getSummarization
函数。
最后,让我们在src/pages/index.tsx
中创建前端,以允许用户输入 URL 并显示摘要。
src/pages/index.tsx
import axios from "axios"; import { useState } from "react"; import { Alert, Box, Button, Container, LinearProgress, Stack, TextField, Typography, } from "@mui/material"; export default function Home() { const [loading, setLoading] = useState(false); const [url, setUrl] = useState(""); const [result, setResult] = useState(""); const [error, setError] = useState<any>(null); const onSubmit = async () => { try { setError(null); setLoading(true); const res = await axios.post("/api/content", { url }); setResult(res.data.result); } catch (err) { console.error("Failed to fetch content", err); setError(err as any); } finally { setLoading(false); } }; return ( <Box sx={{ height: "100vh", overflowY: "auto" }}> <Container sx={{ backgroundColor: (theme) => theme.palette.background.default, position: "sticky", top: 0, zIndex: 2, py: 2, }} > <Typography sx={{ mb: 2, fontSize: "24px" }}> Summarize the content of any page </Typography> <TextField fullWidth label="Input page's URL" value={url} onChange={(e) => { if (result) setResult(""); setUrl(e.target.value); }} sx={{ mb: 2 }} /> <Button disabled={loading} variant="contained" onClick={onSubmit} > Summarize </Button> </Container> <Container maxWidth="lg" sx={{ py: 2 }}> {loading ? ( <LinearProgress /> ) : ( <Stack sx={{ gap: 2 }}> {result && ( <Alert> <Typography sx={{ whiteSpace: "pre-line", wordBreak: "break-word", }} > {result} </Typography> </Alert> )} {error && <Alert severity="error">{error.message || error}</Alert>} </Stack> )} </Container> </Box> ); }
此 React 组件允许用户输入 URL、提交并显示生成的摘要。它处理加载状态和错误消息以提供更好的用户体验。
在项目根目录中创建一个 .env 文件来存储环境变量:
SUPABASE_URL=your-supabase-url SUPABASE_ANON_KEY=your-supabase-anon-key OPENAI_API_KEY=your-openai-api-key
最后,启动你的 Next.js 应用程序:
npm run dev
现在,您应该有一个正在运行的应用程序,您可以在其中输入网页的 URL,并接收该页面的汇总响应。
恭喜!您已经使用 Next.js、OpenAI、LangChain 和 Supabase 构建了一个功能齐全的网页摘要应用程序。用户可以输入 URL、获取内容、将其存储在 Supabase 中,并使用 OpenAI 的功能生成摘要。此设置为您的进一步增强和根据您的需求进行定制提供了坚实的基础。
请随意扩展此项目,添加更多功能、改进 UI 或集成其他 API。
https://github.com/firstpersoncode/summarize-page
祝你编码愉快!