paint-brush
Next.js、OpenAI、LangChain、Supabase を使って Web ページ要約アプリを構築する方法@nassermaronie
新しい歴史

Next.js、OpenAI、LangChain、Supabase を使って Web ページ要約アプリを構築する方法

Nasser Maronie13m2024/06/27
Read on Terminal Reader

長すぎる; 読むには

この記事では、あらゆるWebページの内容を要約できる便利なWebアプリの作成方法を紹介します。スムーズで高速なWebエクスペリエンスを実現する[Next.js]、処理言語に[LangChain]、要約の生成に[OpenAI](https://openai.com/)、ベクターデータの管理と保存に[Supabase]を使用し、強力なツールを一緒に構築します。
featured image - Next.js、OpenAI、LangChain、Supabase を使って Web ページ要約アプリを構築する方法
Nasser Maronie HackerNoon profile picture

あらゆる Web ページのコンテキストを理解できるアプリ。

この記事では、あらゆる Web ページのコンテンツを要約できる便利な Web アプリの作成方法を紹介します。スムーズで高速な Web エクスペリエンスを実現するNext.js 、処理言語にLangChain 、要約の生成にOpenAI 、ベクター データの管理と保存にSupabase を使用し、強力なツールを一緒に構築します。



なぜこれを構築するのか

ネット上には膨大なコンテンツがあり、私たちはみな情報過多に直面しています。簡単な要約を提供するアプリを作成することで、私たちは人々が時間を節約し、情報を入手できるように支援します。忙しい労働者、学生、または単にニュースや記事をチェックしたい人にとって、このアプリは役立つツールとなるでしょう。

どうなるか

当社のアプリでは、ユーザーが任意のウェブサイトの URL を入力すると、そのページの簡単な概要をすぐに表示できます。つまり、長い記事、ブログ投稿、研究論文などを完全に読まなくても、要点を理解できるのです。

潜在力と影響

この要約アプリは、さまざまな用途で役立ちます。研究者が学術論文にざっと目を通したり、ニュース好きの人に最新情報を伝えたりするのに役立ちます。さらに、開発者はこのアプリを基に、さらに便利な機能を作成できます。


技術スタック

Next.js は、Vercel が開発した強力で柔軟な React フレームワークで、開発者がサーバー側レンダリング (SSR) と静的 Web アプリケーションを簡単に構築できるようにします。React の優れた機能と追加機能を組み合わせて、最適化されたスケーラブルな Web アプリケーションを作成します。

オープンAI

Node.js の OpenAI モジュールは、OpenAI の API と対話する方法を提供し、開発者が GPT-3 や GPT-4 などの強力な言語モデルを活用できるようにします。このモジュールを使用すると、高度な AI 機能を Node.js アプリケーションに統合できます。

言語チェーン.js

LangChain は、言語モデルを使用してアプリケーションを開発するために設計された強力なフレームワークです。もともと Python 用に開発されましたが、その後 Node.js を含む他の言語にも適応されました。Node.js のコンテキストにおける LangChain の概要は次のとおりです。

LangChainとは何ですか?

LangChain は、大規模言語モデル (LLM)を使用したアプリケーションの作成を簡素化するライブラリです。LLM を管理してアプリケーションに統合し、これらのモデルへの呼び出しの連鎖を処理し、複雑なワークフローを簡単に実現するためのツールを提供します。

大規模言語モデル (LLM) はどのように機能しますか?

OpenAI の GPT-3.5のような大規模言語モデル (LLM) は、膨大な量のテキスト データでトレーニングされ、人間のようなテキストを理解して生成します。応答を生成したり、言語を翻訳したり、その他多くの自然言語処理タスクを実行したりできます。

スパベース

Supabase は、開発者がスケーラブルなアプリケーションを迅速に構築および展開できるように設計されたオープンソースのバックエンド・アズ・ア・サービス (BaaS) プラットフォームです。データベース管理、認証、ストレージ、リアルタイム機能を簡素化するツールとサービス スイートを提供し、すべてPostgreSQL上に構築されています。


前提条件

始める前に、以下のものを用意してください。

  • Node.jsとnpmがインストールされている
  • Supabaseアカウント
  • OpenAIアカウント

ステップ1: Supabaseのセットアップ

まず、Supabase プロジェクトを設定し、データを保存するために必要なテーブルを作成する必要があります。

Supabaseプロジェクトを作成する

  1. Supabaseにアクセスし、アカウントを登録します。


  2. 新しいプロジェクトを作成し、Supabase URL と API キーをメモします。これらは後で必要になります。

Supabase 用 SQL スクリプト

Supabase ダッシュボードで新しい SQL クエリを作成し、次のスクリプトを実行して必要なテーブルと関数を作成します。

まず、ベクター ストアに拡張機能がまだ存在しない場合は作成します。

 create extension if not exists vector;


次に、「documents」という名前のテーブルを作成します。このテーブルは、Web ページのコンテンツをベクター形式で保存および埋め込むために使用されます。

 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; $$;


次に、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 );

ステップ2: OpenAIの設定

OpenAIプロジェクトを作成する


  • API に移動する: ログイン後、 API セクションに移動して新しい API キーを作成します。これは通常、ダッシュボードからアクセスできます。

ステップ3: Next.jsの設定

Next.js アプリを作成する

$ 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

ステップ4: OpenAIおよびSupabaseクライアント

次に、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 クライアント インスタンス。

ステップ5: コンテンツとファイル用のサービスを作成する

servicesディレクトリを作成し、コンテンツの取得とファイルの管理を処理するために次のファイルを追加します。

src/services/content.ts

このサービスは、Web ページのコンテンツを取得し、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

このサービスは、Web ページのコンテンツを 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 を使用してサマリーを生成します。

ステップ6: APIハンドラーの作成

ここで、コンテンツを処理してサマリーを生成する 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関数の両方を処理します。


ステップ7: フロントエンドの構築

最後に、ユーザーが 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 を入力して送信し、生成された概要を表示できます。読み込み状態とエラー メッセージを処理して、より優れたユーザー エクスペリエンスを提供します。


ステップ8: アプリケーションの実行

環境変数を保存するために、プロジェクトのルートに .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


これで、Web ページの URL を入力して、ページの要約された応答を受け取ることができる実行中のアプリケーションが作成されます。


結論

おめでとうございます! Next.js、OpenAI、LangChain、Supabase を使用して、完全に機能する Web ページ要約アプリケーションを構築しました。ユーザーは URL を入力し、コンテンツを取得して Supabase に保存し、OpenAI の機能を使用して要約を生成できます。このセットアップにより、ニーズに応じてさらに機能強化やカスタマイズを行うための堅牢な基盤が提供されます。


機能の追加、UI の改善、追加の API の統合などにより、このプロジェクトを自由に拡張できます。

このリポジトリのソースコードを確認してください:

https://github.com/firstpersoncode/summarize-page


楽しいコーディングを!