이 글에서는 모든 웹 페이지의 콘텐츠를 요약할 수 있는 편리한 웹 앱을 만드는 방법을 보여 드리겠습니다. 원활하고 빠른 웹 경험을 위한 Next.js , 언어 처리를 위한 LangChain , 요약 생성을 위한 OpenAI , 벡터 데이터 관리 및 저장을 위한 Supabase를 사용하여 강력한 도구를 함께 구축하겠습니다.
우리 모두는 온라인에 너무 많은 콘텐츠로 인한 정보 과부하에 직면해 있습니다. 빠른 요약을 제공하는 앱을 만들어 사람들이 시간을 절약하고 최신 정보를 얻을 수 있도록 돕습니다. 당신이 바쁜 직장인이든, 학생이든, 아니면 단지 뉴스와 기사를 확인하고 싶은 사람이든 관계없이 이 앱은 당신에게 유용한 도구가 될 것입니다.
우리 앱을 사용하면 사용자가 웹사이트 URL을 입력하고 페이지에 대한 간략한 요약을 빠르게 얻을 수 있습니다. 이는 긴 기사, 블로그 게시물, 연구 논문을 완전히 읽지 않고도 주요 내용을 이해할 수 있다는 의미입니다.
이 요약 앱은 여러 면에서 유용할 수 있습니다. 연구자들이 학술 논문을 훑어보고, 뉴스를 좋아하는 사람들에게 최신 소식을 계속 제공하는 등의 작업에 도움이 될 수 있습니다. 또한 개발자는 이 앱을 기반으로 더욱 유용한 기능을 만들 수 있습니다.
Next.js는 개발자가 서버 측 렌더링(SSR) 및 정적 웹 애플리케이션을 쉽게 구축할 수 있도록 지원하는 Vercel에서 개발한 강력하고 유연한 React 프레임워크입니다. React의 최고의 기능과 추가 기능을 결합하여 최적화되고 확장 가능한 웹 애플리케이션을 만듭니다.
Node.js의 OpenAI 모듈은 OpenAI의 API와 상호 작용하는 방법을 제공하므로 개발자는 GPT-3 및 GPT-4와 같은 강력한 언어 모델을 활용할 수 있습니다. 이 모듈을 사용하면 고급 AI 기능을 Node.js 애플리케이션에 통합할 수 있습니다.
LangChain은 언어 모델을 사용하여 애플리케이션을 개발하기 위해 설계된 강력한 프레임워크입니다. 원래 Python용으로 개발되었지만 이후 Node.js를 포함한 다른 언어에도 적용되었습니다. Node.js의 맥락에서 LangChain의 개요는 다음과 같습니다.
LangChain은 LLM(대형 언어 모델)을 사용하여 애플리케이션 생성을 단순화하는 라이브러리입니다. LLM을 관리 및 애플리케이션에 통합하고, 이러한 모델에 대한 호출 연결을 처리하고, 복잡한 워크플로를 쉽게 활성화하는 도구를 제공합니다.
OpenAI의 GPT-3.5 와 같은 LLM(대형 언어 모델)은 인간과 유사한 텍스트를 이해하고 생성하기 위해 방대한 양의 텍스트 데이터를 학습합니다. 응답을 생성하고, 언어를 번역하고, 기타 다양한 자연어 처리 작업을 수행할 수 있습니다.
Supabase는 개발자가 확장 가능한 애플리케이션을 신속하게 구축하고 배포할 수 있도록 설계된 오픈 소스 BaaS(backend-as-a-service) 플랫폼입니다. PostgreSQL을 기반으로 구축된 데이터베이스 관리, 인증, 저장 및 실시간 기능을 단순화하는 도구 및 서비스 제품군을 제공합니다.
시작하기 전에 다음 사항이 있는지 확인하세요.
먼저 Supabase 프로젝트를 설정하고 데이터를 저장하는 데 필요한 테이블을 생성해야 합니다.
Supabase 로 이동하여 계정을 등록하세요.
새 프로젝트를 만들고 Supabase URL과 API 키를 기록해 두세요. 나중에 필요합니다.
Supabase 대시보드에서 새 SQL 쿼리를 생성하고 다음 스크립트를 실행하여 필요한 테이블과 함수를 생성합니다.
먼저, 벡터 저장소에 대한 확장이 아직 존재하지 않는 경우 확장을 만듭니다.
create extension if not exists vector;
다음으로 '문서'라는 테이블을 만듭니다. 이 테이블은 웹 페이지의 콘텐츠를 벡터 형식으로 저장하고 삽입하는 데 사용됩니다.
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
기능을 모두 처리합니다.
마지막으로 사용자가 URL을 입력하고 요약을 표시할 수 있도록 src/pages/index.tsx
에 프런트엔드를 만들어 보겠습니다.
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
즐거운 코딩하세요!