En este artículo, le mostraremos cómo crear una práctica aplicación web que pueda resumir el contenido de cualquier página web. Usando Next.js para una experiencia web rápida y fluida, LangChain para procesar lenguaje, OpenAI para generar resúmenes y Supabase para administrar y almacenar datos vectoriales, crearemos juntos una poderosa herramienta.
Todos nos enfrentamos a una sobrecarga de información con tanto contenido en línea. Al crear una aplicación que ofrece resúmenes rápidos, ayudamos a las personas a ahorrar tiempo y mantenerse informadas. Si usted es un trabajador ocupado, un estudiante o simplemente alguien que quiere mantenerse al día con las noticias y artículos, esta aplicación será una herramienta útil para usted.
Nuestra aplicación permitirá a los usuarios ingresar la URL de cualquier sitio web y obtener rápidamente un breve resumen de la página. Esto significa que puede comprender los puntos principales de artículos extensos, publicaciones de blogs o trabajos de investigación sin leerlos en su totalidad.
Esta aplicación de resumen puede resultar útil de muchas maneras. Puede ayudar a los investigadores a hojear artículos académicos, mantener actualizados a los amantes de las noticias y más. Además, los desarrolladores pueden aprovechar esta aplicación para crear funciones aún más útiles.
Next.js es un marco React potente y flexible desarrollado por Vercel que permite a los desarrolladores crear renderizado del lado del servidor (SSR) y aplicaciones web estáticas con facilidad. Combina las mejores características de React con capacidades adicionales para crear aplicaciones web optimizadas y escalables.
El módulo OpenAI en Node.js proporciona una forma de interactuar con la API de OpenAI, lo que permite a los desarrolladores aprovechar potentes modelos de lenguaje como GPT-3 y GPT-4. Este módulo le permite integrar funcionalidades avanzadas de IA en sus aplicaciones Node.js.
LangChain es un potente marco diseñado para desarrollar aplicaciones con modelos de lenguaje. Originalmente desarrollado para Python, desde entonces se ha adaptado a otros lenguajes, incluido Node.js. Aquí hay una descripción general de LangChain en el contexto de Node.js:
LangChain es una biblioteca que simplifica la creación de aplicaciones utilizando modelos de lenguaje grandes (LLM) . Proporciona herramientas para administrar e integrar LLM en sus aplicaciones, manejar el encadenamiento de llamadas a estos modelos y permitir flujos de trabajo complejos con facilidad.
Los modelos de lenguaje grande (LLM), como GPT-3.5 de OpenAI, se entrenan con grandes cantidades de datos de texto para comprender y generar texto similar a un humano. Pueden generar respuestas, traducir idiomas y realizar muchas otras tareas de procesamiento del lenguaje natural.
Supabase es una plataforma backend como servicio (BaaS) de código abierto diseñada para ayudar a los desarrolladores a crear e implementar rápidamente aplicaciones escalables. Ofrece un conjunto de herramientas y servicios que simplifican la gestión de bases de datos, la autenticación, el almacenamiento y las capacidades en tiempo real, todo ello construido sobre PostgreSQL.
Antes de comenzar, asegúrese de tener lo siguiente:
Primero, necesitamos configurar un proyecto Supabase y crear las tablas necesarias para almacenar nuestros datos.
Vaya a Supabase y regístrese para obtener una cuenta.
Cree un nuevo proyecto y tome nota de su URL de Supabase y clave API. Los necesitarás más tarde.
Cree una nueva consulta SQL en su panel de Supabase y ejecute los siguientes scripts para crear las tablas y funciones necesarias:
Primero, crea una extensión si aún no existe para nuestra tienda de vectores:
create extension if not exists vector;
A continuación, cree una tabla llamada "documentos". Esta tabla se utilizará para almacenar e incrustar el contenido de la página web en formato vectorial:
create table if not exists documents ( id bigint primary key generated always as identity, content text, metadata jsonb, embedding vector(1536) );
Ahora, necesitamos una función para consultar nuestros datos incrustados:
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; $$;
A continuación, debemos configurar nuestra tabla para almacenar los detalles de la página 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
Instale las dependencias requeridas:
npm install @langchain/community @langchain/core @langchain/openai @supabase/supabase-js langchain openai axios
Luego, instalaremos Material UI para construir nuestra interfaz; no dudes en utilizar otra biblioteca:
npm install @mui/material @emotion/react @emotion/styled
A continuación, necesitamos configurar los clientes OpenAI y Supabase. Cree un directorio libs
en su proyecto y agregue los siguientes archivos.
src/libs/openAI.ts
Este archivo configurará el cliente 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
: la instancia del modelo de lenguaje, que generará nuestros resúmenes.
embeddings
: Esto creará incrustaciones para nuestros documentos, que ayudarán a encontrar contenido similar.src/libs/supabaseClient.ts
Este archivo configurará el cliente 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
: la instancia del cliente Supabase para interactuar con nuestra base de datos Supabase. Cree un directorio services
y agregue los siguientes archivos para manejar la búsqueda de contenido y la administración de archivos.
src/services/content.ts
Este servicio buscará el contenido de la página web y lo limpiará eliminando etiquetas, scripts y estilos 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, " "); }
Esta función recupera el contenido HTML de una URL determinada y lo limpia eliminando estilos, scripts y etiquetas HTML.
src/services/file.ts
Este servicio guardará el contenido de la página web en Supabase y recuperará resúmenes.
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
: guarda el archivo y su contenido en Supabase, divide el contenido en fragmentos manejables y los almacena en el almacén de vectores.
getSummarization
: recupera documentos relevantes del almacén de vectores y genera un resumen utilizando OpenAI.Ahora, creemos un controlador API para procesar el contenido y generar un resumen.
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 }); } }
Este controlador de API recibe una URL, recupera el contenido, lo guarda en Supabase y genera un resumen. Maneja las funciones saveFile
y getSummarization
de nuestros servicios.
Finalmente, creemos la interfaz en src/pages/index.tsx
para permitir a los usuarios ingresar URL y mostrar los resúmenes.
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> ); }
Este componente de React permite a los usuarios ingresar una URL, enviarla y mostrar el resumen generado. Maneja estados de carga y mensajes de error para brindar una mejor experiencia de usuario.
Cree un archivo .env en la raíz de su proyecto para almacenar sus variables de entorno:
SUPABASE_URL=your-supabase-url SUPABASE_ANON_KEY=your-supabase-anon-key OPENAI_API_KEY=your-openai-api-key
Finalmente, inicie su aplicación Next.js:
npm run dev
Ahora, debería tener una aplicación en ejecución donde pueda ingresar la URL de la página web y recibir las respuestas resumidas de la página.
¡Felicidades! Ha creado una aplicación de resumen de páginas web completamente funcional utilizando Next.js, OpenAI, LangChain y Supabase. Los usuarios pueden ingresar una URL, buscar el contenido, almacenarlo en Supabase y generar un resumen utilizando las capacidades de OpenAI. Esta configuración proporciona una base sólida para futuras mejoras y personalización según sus necesidades.
No dude en ampliar este proyecto agregando más funciones, mejorando la interfaz de usuario o integrando API adicionales.
https://github.com/firstpersoncode/summarize-page
¡Feliz codificación!