Trong bài viết này, chúng tôi sẽ hướng dẫn bạn cách tạo một ứng dụng web tiện dụng có thể tóm tắt nội dung của bất kỳ trang web nào. Sử dụng Next.js để có trải nghiệm web mượt mà và nhanh chóng, LangChain để xử lý ngôn ngữ, OpenAI để tạo bản tóm tắt và Supabase để quản lý và lưu trữ dữ liệu vectơ, chúng ta sẽ cùng nhau xây dựng một công cụ mạnh mẽ.
Tất cả chúng ta đều phải đối mặt với tình trạng quá tải thông tin với quá nhiều nội dung trực tuyến. Bằng cách tạo ra một ứng dụng cung cấp các bản tóm tắt nhanh chóng, chúng tôi giúp mọi người tiết kiệm thời gian và luôn cập nhật thông tin. Cho dù bạn là một công nhân bận rộn, một sinh viên hay chỉ là người muốn cập nhật tin tức và bài viết thì ứng dụng này sẽ là một công cụ hữu ích cho bạn.
Ứng dụng của chúng tôi sẽ cho phép người dùng nhập bất kỳ URL trang web nào và nhanh chóng nhận được bản tóm tắt ngắn gọn về trang. Điều này có nghĩa là bạn có thể hiểu những điểm chính của các bài viết dài, bài đăng trên blog hoặc tài liệu nghiên cứu mà không cần đọc chúng đầy đủ.
Ứng dụng tóm tắt này có thể hữu ích theo nhiều cách. Nó có thể giúp các nhà nghiên cứu lướt qua các bài báo học thuật, cập nhật cho những người yêu thích tin tức và hơn thế nữa. Ngoài ra, các nhà phát triển có thể xây dựng trên ứng dụng này để tạo ra nhiều tính năng hữu ích hơn nữa.
Next.js là một khung React mạnh mẽ và linh hoạt do Vercel phát triển, cho phép các nhà phát triển xây dựng các ứng dụng web tĩnh và kết xuất phía máy chủ (SSR) một cách dễ dàng. Nó kết hợp các tính năng tốt nhất của React với các khả năng bổ sung để tạo các ứng dụng web được tối ưu hóa và có thể mở rộng.
Mô-đun OpenAI trong Node.js cung cấp cách tương tác với API của OpenAI, cho phép các nhà phát triển tận dụng các mô hình ngôn ngữ mạnh mẽ như GPT-3 và GPT-4. Mô-đun này cho phép bạn tích hợp các chức năng AI nâng cao vào các ứng dụng Node.js của mình.
LangChain là một framework mạnh mẽ được thiết kế để phát triển các ứng dụng bằng mô hình ngôn ngữ. Ban đầu được phát triển cho Python, sau đó nó đã được điều chỉnh cho các ngôn ngữ khác, bao gồm cả Node.js. Dưới đây là tổng quan về LangChain trong bối cảnh Node.js:
LangChain là một thư viện giúp đơn giản hóa việc tạo ứng dụng bằng mô hình ngôn ngữ lớn (LLM) . Nó cung cấp các công cụ để quản lý và tích hợp LLM vào ứng dụng của bạn, xử lý chuỗi lệnh gọi đến các mô hình này và cho phép thực hiện các quy trình công việc phức tạp một cách dễ dàng.
Các Mô hình ngôn ngữ lớn (LLM) như GPT-3.5 của OpenAI được đào tạo trên lượng lớn dữ liệu văn bản để hiểu và tạo ra văn bản giống con người. Họ có thể tạo phản hồi, dịch ngôn ngữ và thực hiện nhiều tác vụ xử lý ngôn ngữ tự nhiên khác.
Supabase là nền tảng backend-as-a-service (BaaS) nguồn mở được thiết kế để giúp các nhà phát triển nhanh chóng xây dựng và triển khai các ứng dụng có thể mở rộng. Nó cung cấp một bộ công cụ và dịch vụ giúp đơn giản hóa việc quản lý cơ sở dữ liệu, xác thực, lưu trữ và khả năng thời gian thực, tất cả đều được xây dựng dựa trên PostgreSQL
Trước khi chúng tôi bắt đầu, hãy đảm bảo bạn có những điều sau:
Đầu tiên, chúng ta cần thiết lập một dự án Supabase và tạo các bảng cần thiết để lưu trữ dữ liệu của mình.
Vào Supabase và đăng ký tài khoản.
Tạo một dự án mới và ghi lại URL Supabase và khóa API của bạn. Bạn sẽ cần những thứ này sau.
Tạo một truy vấn SQL mới trong bảng điều khiển Supabase của bạn và chạy các tập lệnh sau để tạo các bảng và hàm cần thiết:
Đầu tiên, tạo tiện ích mở rộng nếu nó chưa tồn tại cho cửa hàng vector của chúng tôi:
create extension if not exists vector;
Tiếp theo, tạo một bảng có tên là “tài liệu”. Bảng này sẽ dùng để lưu trữ và nhúng nội dung của trang web dưới dạng vector:
create table if not exists documents ( id bigint primary key generated always as identity, content text, metadata jsonb, embedding vector(1536) );
Bây giờ, chúng ta cần một hàm để truy vấn dữ liệu được nhúng của mình:
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; $$;
Tiếp theo, chúng ta cần thiết lập bảng để lưu trữ thông tin chi tiết của trang web:
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
Cài đặt các phụ thuộc cần thiết:
npm install @langchain/community @langchain/core @langchain/openai @supabase/supabase-js langchain openai axios
Sau đó, chúng tôi sẽ cài đặt Material UI để xây dựng giao diện của mình; vui lòng sử dụng thư viện khác:
npm install @mui/material @emotion/react @emotion/styled
Tiếp theo, chúng ta cần thiết lập ứng dụng khách OpenAI và Supabase. Tạo thư mục libs
trong dự án của bạn và thêm các tệp sau.
src/libs/openAI.ts
Tệp này sẽ cấu hình ứng dụng khách 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
: Phiên bản mô hình ngôn ngữ sẽ tạo ra các bản tóm tắt của chúng tôi.
embeddings
: Điều này sẽ tạo các phần nhúng cho tài liệu của chúng tôi, giúp tìm kiếm nội dung tương tự.src/libs/supabaseClient.ts
Tập tin này sẽ cấu hình máy khách 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
: Phiên bản máy khách Supabase để tương tác với cơ sở dữ liệu Supabase của chúng tôi. Tạo một thư mục services
và thêm các tệp sau để xử lý việc tìm nạp nội dung và quản lý tệp.
src/services/content.ts
Dịch vụ này sẽ tìm nạp nội dung trang web và làm sạch nó bằng cách xóa các thẻ, tập lệnh và kiểu 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, " "); }
Hàm này tìm nạp nội dung HTML của một URL nhất định và dọn sạch nó bằng cách xóa kiểu, tập lệnh và thẻ HTML.
src/services/file.ts
Dịch vụ này sẽ lưu nội dung trang web vào Supabase và truy xuất các bản tóm tắt.
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
: Lưu tệp và nội dung của nó vào Supabase, chia nội dung thành các phần có thể quản lý được và lưu trữ chúng trong kho lưu trữ vectơ.
getSummarization
: Truy xuất các tài liệu có liên quan từ kho lưu trữ vectơ và tạo bản tóm tắt bằng OpenAI.Bây giờ, hãy tạo một trình xử lý API để xử lý nội dung và tạo bản tóm tắt.
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 }); } }
Trình xử lý API này nhận được một URL, tìm nạp nội dung, lưu nó vào Supabase và tạo một bản tóm tắt. Nó xử lý cả hàm saveFile
và getSummarization
từ các dịch vụ của chúng tôi.
Cuối cùng, hãy tạo giao diện người dùng trong src/pages/index.tsx
để cho phép người dùng nhập URL và hiển thị phần tóm tắt.
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> ); }
Thành phần React này cho phép người dùng nhập URL, gửi nó và hiển thị bản tóm tắt đã tạo. Nó xử lý trạng thái tải và thông báo lỗi để cung cấp trải nghiệm người dùng tốt hơn.
Tạo tệp .env trong thư mục gốc của dự án để lưu trữ các biến môi trường của bạn:
SUPABASE_URL=your-supabase-url SUPABASE_ANON_KEY=your-supabase-anon-key OPENAI_API_KEY=your-openai-api-key
Cuối cùng, khởi động ứng dụng Next.js của bạn:
npm run dev
Bây giờ, bạn phải có một ứng dụng đang chạy nơi bạn có thể nhập URL của trang web và nhận phản hồi tóm tắt của trang đó.
Chúc mừng! Bạn đã xây dựng một ứng dụng tóm tắt trang web đầy đủ chức năng bằng Next.js, OpenAI, LangChain và Supabase. Người dùng có thể nhập URL, tìm nạp nội dung, lưu trữ trong Supabase và tạo bản tóm tắt bằng khả năng của OpenAI. Thiết lập này cung cấp nền tảng vững chắc để cải tiến và tùy chỉnh hơn nữa dựa trên nhu cầu của bạn.
Vui lòng mở rộng dự án này bằng cách thêm nhiều tính năng hơn, cải thiện giao diện người dùng hoặc tích hợp các API bổ sung.
https://github.com/firstpersoncode/summarize-page
Chúc mừng mã hóa!