RAG + Langchain Python Project: Easy AI/Chat For Your Docs
Summary
TLDREste video ofrece una guía paso a paso para construir una aplicación de generación aumentada por recuperación (RAG) utilizando Langchain y OpenAI. La aplicación es ideal para interactuar con grandes volúmenes de datos textuales, como colecciones de libros, documentos o conferencias. La demostración incluye la preparación de los datos, la creación de una base de datos vectorial con ChromaDB y el uso de técnicas de búsqueda para encontrar información relevante. El resultado es una respuesta coherente y personalizada basada en la fuente de datos proporcionada. Además, se explora la generación de consultas y la respuesta a preguntas utilizando un modelo de lenguaje grande (LLM). El video finaliza con un ejemplo práctico de cómo usar la documentación de AWS Lambda como fuente de datos para la aplicación.
Takeaways
- 🚀 **Construimos una aplicación de generación aumentada por recuperación** usando Langchain y OpenAI para interactuar con documentos o fuentes de datos personales.
- 📚 **La aplicación es ideal para manejar grandes volúmenes de datos textuales**, como colecciones de libros, documentos o conferencias.
- 🤖 **Ejemplo de uso**: crear un chatbot de soporte al cliente que siga un conjunto de instrucciones o hacer preguntas sobre los datos.
- 💡 **Técnica utilizada**: RAG (Retrieval Augmented Generation), que permite al agente utilizar la documentación para responder y citar la fuente de la información.
- 📁 **Preparación de datos**: Se necesita una fuente de datos como archivos PDF, texto o markdown, que se divide en diferentes bloques de texto.
- 📚 **Uso de Langchain**: El módulo de carga de directorios de Langchain se utiliza para convertir archivos markdown en documentos que contienen contenido y metadatos.
- 🔍 **Creación de una base de datos vectorial**: Los bloques de texto se transforman en una base de datos usando ChromaDB, que utiliza vectores de incrustación como clave.
- 📊 **Vectores de incrustación**: Son representaciones vectoriales del texto que capturan su significado y se calculan a través de funciones de OpenAI.
- 🔗 **Búsqueda de datos relevantes**: Se utiliza una función de incrustación para convertir una consulta en un vector y buscar en la base de datos los bloques más cercanos en términos de distancia de incrustación.
- 📝 **Generación de respuestas**: Se utiliza la información relevante encontrada para crear una respuesta personalizada usando un modelo de LLM como OpenAI.
- 📜 **Inclusión de referencias**: La respuesta final incluye referencias a los materiales fuente utilizados, proporcionando trazabilidad y confianza en la información.
- 🔧 **Disponibilidad del código**: El código de la aplicación se comparte en GitHub para que los espectadores puedan probarlo con sus propios conjuntos de datos.
Q & A
¿Qué es una aplicación de generación aumentada por recuperación y cómo se utiliza?
-Una aplicación de generación aumentada por recuperación es una herramienta que utiliza la inteligencia artificial para interactuar con grandes cantidades de datos de texto, como colecciones de libros, documentos o conferencias. Se puede utilizar para hacer preguntas sobre los datos o para construir un chatbot de soporte al cliente que siga un conjunto de instrucciones.
¿Qué técnica se utiliza para construir la aplicación mostrada en el video?
-Se utiliza la técnica llamada RAG, que significa 'generación aumentada por recuperación'. Esta técnica permite que el agente utilice una documentación para proporcionar una respuesta y citar la fuente de la información original.
¿Cómo se carga y se divide el contenido de los documentos en la aplicación?
-Se utiliza el módulo de carga de directorios de Langchain para cargar datos de markdown desde una carpeta en Python. Cada archivo de markdown se convierte en un 'documento' que contiene todo el contenido de la página y se puede dividir en 'chunks' más pequeños para una búsqueda más eficiente.
¿Qué es un 'chunk' y cómo se determina su tamaño?
-Un 'chunk' es una porción de texto que puede ser un párrafo, una oración o incluso varias páginas. Se determina su tamaño en número de caracteres y se establece una superposición entre cada 'chunk' para mantener la coherencia del texto.
¿Cómo se convierte el contenido en una base de datos consultable?
-Se utiliza ChromaDB, una base de datos que utiliza vectores de incrustación como clave. Se generan vectores de incrustación para cada 'chunk' utilizando la función de incrustación de OpenAI y se almacenan en la base de datos para su posterior consulta.
¿Qué son los vectores de incrustación y cómo se relacionan con el significado del texto?
-Los vectores de incrustación son representaciones vectoriales del texto que capturan su significado. Son listas de números que actúan como coordenadas en un espacio multidimensional. Si dos piezas de texto están relacionadas en significado, sus vectores también estarán cerca en el espacio.
¿Cómo se genera un vector a partir de una palabra o un texto?
-Para generar un vector se requiere un modelo de lenguaje grande (LLM) como OpenAI. Se puede utilizar una API o una función para convertir una palabra o un texto en un vector de incrustación, que es en realidad una lista de números.
¿Cómo se utiliza la base de datos para responder a una consulta?
-Se toma una consulta, se convierte en un vector utilizando la misma función que se utilizó para crear la base de datos y luego se busca en la base de datos para encontrar los 'chunks' de información más cercanos en distancia de vector a la consulta. Estos 'chunks' se utilizan para crear una respuesta personalizada.
¿Cómo se crea una respuesta de calidad utilizando los datos relevantes?
-Se utiliza un modelo de lenguaje grande (LLM) como OpenAI con un prompt que incluye el contexto relevante extraído de la base de datos y la consulta original. OpenAI utiliza estos datos para crear una respuesta que utiliza la información de la fuente.
¿Cómo se puede proporcionar una referencia de volta a la fuente material en la respuesta final?
-Se puede extraer la información de la metadata de cada 'chunk' de documento utilizado para responder la consulta. Esta información se puede incluir en la respuesta final para proporcionar una referencia de volta a la fuente material.
¿Dónde puedo encontrar el código fuente de este proyecto?
-El código fuente del proyecto se publicará en GitHub y se incluirá un enlace en la descripción del video. Puedes clonar el repositorio y probar el proyecto con tu propio conjunto de datos.
¿Cómo puedo saber si este tipo de aplicación es adecuada para mi caso de uso?
-Si tienes una gran cantidad de datos de texto y deseas interactuar con ellos de manera eficiente, como hacer preguntas o construir un chatbot de soporte al cliente, una aplicación de generación aumentada por recuperación puede ser una excelente opción.
Outlines
🚀 Introducción a la construcción de una aplicación de generación mejorada por recuperación
El primer párrafo presenta el objetivo del video, que es mostrar cómo construir una aplicación de generación mejorada por recuperación utilizando Langchain y OpenAI. Destacan la utilidad de este tipo de aplicación para manejar grandes volúmenes de datos textuales, como colecciones de libros, documentos o conferencias. La aplicación puede ser útil para hacer preguntas sobre los datos o para crear un chatbot de soporte al cliente que siga un conjunto de instrucciones. El vídeo guía a los espectadores a través de cada paso del proyecto, desde la preparación de los datos hasta la creación de una base de datos vectorial y la búsqueda de datos relevantes para formar una respuesta coherente.
📚 Preparación y división de los datos fuente
Se describe el proceso de preparación de los datos fuente, que puede incluir archivos PDF, textos o archivos de markdown. Se sugiere encontrar archivos markdown para usar en el proyecto y se menciona el uso de la biblioteca Langchain para cargar estos datos en Python. Cada archivo se convierte en un 'documento' que contiene todo el contenido y metadatos como el nombre del archivo fuente. Además, se destaca la necesidad de dividir documentos largos en 'chunks' más pequeños para mejorar la relevancia en búsquedas, utilizando un 'recursive character text splitter' con un tamaño de chunk y una superposición específicos.
🗃️ Creación de una base de datos vectorial con ChromaDB
Para consultar los 'chunks' de texto, se necesita una base de datos. Se utiliza ChromaDB, que utiliza vectores de incrustación como claves. Se explica cómo crear una base de datos de Chroma a partir de los 'chunks' utilizando el método de incrustación de OpenAI para generar vectores. Se discute la importancia de las incrustaciones vectoriales, que son representaciones vectoriales del texto que capturan su significado, y cómo se pueden calcular las distancias entre vectores usando similitud coseno o distancia euclidiana. Se proporciona un ejemplo de cómo generar vectores y comparar la distancia entre ellos utilizando una función de evaluador de Langchain.
🔍 Búsqueda y generación de respuestas con OpenAI
Se describe cómo buscar en la base de datos los 'chunks' más relevantes en relación con una consulta y cómo usar esta información para crear una respuesta personalizada utilizando OpenAI. Se menciona la necesidad de un modelo de LLM (Modelo de Lenguaje Grande) y cómo se puede usar para responder a una pregunta basada en el contexto proporcionado. Se muestra cómo crear una plantilla de prompt para OpenAI con los datos relevantes y la consulta, y cómo enviar esta prompt a OpenAI para obtener una respuesta. Además, se explica cómo extraer y proporcionar referencias de los materiales fuente utilizados en la respuesta.
🌟 Ejemplos y recursos adicionales
Se presentan ejemplos de cómo la aplicación puede utilizarse con diferentes fuentes de datos, como la documentación de AWS Lambda. Se muestra cómo la aplicación puede encontrar información relevante y publicar una respuesta resumida. Se alentará a los espectadores a probar el proyecto con sus propios conjuntos de datos y a dejar comentarios sobre los temas de tutoriales que les gustaría ver a futuro. Se ofrece un enlace a GitHub con el código del video en la descripción.
Mindmap
Keywords
💡Retrieval Augmented Generation (RAG)
💡Langchain
💡OpenAI
💡Vector Embeddings
💡ChromaDB
💡Metadata
💡Querying
💡Document Chunking
💡Embedding Distance
💡Prompt Template
💡Source Material
Highlights
Se muestra cómo construir una aplicación de generación aumentada por recuperación usando Langchain y OpenAI.
La aplicación es útil para interactuar con grandes cantidades de datos de texto, como colecciones de libros, documentos o conferencias.
Se utiliza una técnica llamada RAG (retrieval augmented generation) para responder a preguntas basadas en la documentación proporcionada.
El agente podrá citar la fuente de la información utilizada, evitando respuestas inventadas.
Se detalla cómo preparar los datos, convertirlos en una base de datos vectorial y consultarla para obtener respuestas coherentes.
Se necesita una fuente de datos como archivos PDF o una colección de archivos de texto o markdown.
Se utiliza el módulo directory loader de Langchain para cargar datos de markdown en Python.
Los documentos cargados se dividen en trozos más pequeños para una búsqueda más eficiente.
Se utiliza un separador de texto por caracteres recursivo para dividir los documentos en trozos con un tamaño y una superposición definidos.
ChromaDB se utiliza para crear una base de datos vectorial de los trozos de texto.
Se requiere una cuenta de OpenAI para generar las incrustaciones vectoriales de cada trozo usando la función de incrustación de OpenAI.
Las incrustaciones vectoriales son representaciones vectoriales del texto que capturan su significado.
La distancia entre vectores se calcula usando similitud coseno o distancia euclidiana.
Langchain proporciona una función utilitaria para comparar la distancia de incrustación directamente usando OpenAI.
Se busca en la base de datos los trozos más relevantes para la consulta del usuario.
Se utiliza una plantilla de prompt para crear una pregunta y se alimenta a OpenAI para obtener una respuesta de calidad.
Se puede proporcionar referencias de volta a los materiales fuente en los metadatos de cada trozo de documento.
Se muestra un ejemplo de cómo se utiliza la documentación de AWS Lambda para responder a consultas específicas.
El resultado final es una respuesta basada en la información de la fuente y una lista de referencias a los archivos fuente utilizados.
Transcripts
Hey everyone, welcome to this video where I'm going
to show you how to build a retrieval augmented
generation app using Langchain and OpenAI.
You can then use this app to interact with your
own documents or your own data source.
This type of application is great for when
you have a lot of text data to work with.
For example, a collection of books, documents or lectures. And
you want to be able to interact with that data using AI.
For example, you might want to be able to ask
questions about that data or perhaps build
something like a customer support chatbot that
you want to follow a set of instructions.
Today, we're going to learn how to build this using
OpenAI and the Langchain library in Python.
We're going to be using a technique called RAG, which
stands for retrieval augmented generation.
In this example, the data source I've given it is the AWS documentation for Lambda.
And here I'm asking it a question based on that documentation.
The agent will be able to use that documentation
to give me a response as well as quote the source
where it got that information from originally.
This way, you always know that it's using data from the sources
you provided it with rather than hallucinating the response.
If this project sounds complex or difficult to you, then
don't worry because it's a lot easier than you think.
I'll walk you through every step of the project, starting
with how to prepare the data that you want to use
and then how to turn that into a vector database.
Then we'll also look at how to query that database for relevant pieces of data.
Finally, you can then put all those pieces together to form a coherent response.
If that sounds good, then let's get started.
To begin, we'll first need a data source like a
PDF or a collection of text or markdown files.
This can be anything.
For example, it could be documentation files for your software.
It could be a customer support handbook, or it
could even be transcripts from a podcast.
First, find some markdown files you want to use as data for this project.
But if you want some ideas, then here I've got the Alice
in Wonderland book as a markdown file, or I also have
the AWS documentation as a bunch of markdown files.
And I have each of them in their own separate
folder under this data folder in my project.
So make sure you have a setup like this first before you start.
Once you have that source material, we're going to need to load
it up and then split it into different chunks of text.
To load some markdown data from your folder into Python,
you can use this directory loader module from Langchain.
Just update this data path variable with wherever you've decided to put your data.
Here I'm using data/books.
If you only have one markdown file in that folder, it's okay.
Or if you have multiple markdown files, then
this will load everything and turn each of those
files into something called a document.
If I use this piece of code on my AWS Lambda
documents folder instead, then each of these
markdown files will become a document.
And a document is going to contain all of the content on this page.
So basically all of the text you see here.
And it's also going to contain a bunch of metadata.
For example, the name of the source file where the text originally came from.
And after you've created your document, you can also choose
to add any other metadata you want to that document.
Now the next problem we encounter is that a
single document can be really, really long.
So it's not enough that we load each markdown file into one document.
We have to also split each document if they're too long on their own.
With something as long as this, we're going to want
to split this big document into smaller chunks.
And a chunk could be a paragraph, it could be a
sentence, or it could be even several pages.
It depends on what we want.
By doing this, the outcome that we're looking
for is that when we search through all of this
data, each chunk is going to be more focused
and more relevant to what we're looking for.
To achieve this, we can use a recursive character text splitter.
And here we can set the chunk size in number of characters
and then the overlap between each chunk.
So in this example, we're going to make the chunk
size about 1000 characters, and each chunk
is going to have an overlap of 500 characters.
So I've just ran the script to split up my text into several chunks.
And here I've printed out the number of original documents
and the number of chunks it was split into.
Since I used this on the Alice in Wonderland
text, it split one document into 282 chunks.
And down here, I've just picked a random chunk as
a document and I printed out the page content and
the metadata so you could see what it looks like.
So the page content is just literally a part of the text taken out of that chunk.
So here you can see that it's about one or two paragraphs of the story.
And the metadata right now, it only has the source, which is
the path of the file it got this from, and the start index.
So where in that source does this particular chunk begin?
And if you try the same code with the AWS Lambda docs
instead, you'll see that the source also points to
the file that the information of each chunk is from.
So this is also useful if you have a lot of different files,
rather than just one big file splitting into smaller chunks.
To be able to query each chunk, we're going to need to turn this into a database.
We'll be using ChromaDB for this, which is a special kind
of database that uses vector embeddings as the key.
This is the code that you can use to create a Chroma database from our chunks.
For this, you're going to need an OpenAI account because
we're going to use the OpenAI embeddings function
to generate the vector embeddings for each chunk.
I'm also going to create a Chroma path and set that
as the persistent directory so that when we create
this database, I have a bunch of folders on my
disk that I can use to load the data later on.
This is useful because normally I might want to
put this database into a Lambda function or I
might want to put it in the cloud somewhere.
So I want to be able to save it to disk so
that I can copy it or deploy it as a file.
Now before I create the database or before I save
it to disk, I can also use this code snippet
to remove it first if it already exists.
This is useful if I want to clear all of my previous versions
of the database before I run the script to create a new one.
Now the database should save automatically after we create it,
but you can also force it to save using this persist method.
So once you've put all of that together and then
you've run your script to generate your database,
you should see this line where it's saved
all of your chunks to the Chroma database.
And you can see here on your disk that the data should be there as well.
And here it's going to be saved as a SQLite3 file.
So now at this point we have our vector database
created and we're ready to start using it.
But first you're probably going to want to know what a vector embedding is.
If you already know what embedding vectors are,
then feel free to skip the section entirely.
Otherwise, I'll give you a really quick explanation just to bring you up to speed.
Embeddings are vector representations of text that capture their meaning.
In Python, this is literally a list of numbers.
You can think of them as sort of coordinates in multi-dimensional
space and if two pieces of text
are closely related to each other in meaning, then
those coordinates will also be close together.
The distance between these vectors can then be calculated pretty
easily using cosine similarity or Euclidean distance.
We don't need to do that ourselves though, because there's a
lot of existing functions that can do that for us already.
And this will give us a single number that tells
us how far these two vectors are apart.
To actually generate a vector from a word, we'll need an LLM, like OpenAI.
And this is usually just an API or a function we can call.
For example, you can use this code to turn
the word "apple" into a vector embedding.
And this is the result I get from using that function.
So you could see that the vector here is literally a really long list of numbers.
And the first number is 0.007 something-something, but
I truncated the rest because the list is quite long.
In fact, if you print the length of the vector, you
could see that the list has 1536 characters.
So this is basically a list of one and a half thousand numbers.
The numbers themselves aren't interesting though.
What's really interesting is the distance between two vectors themselves.
And this is quite hard to calculate from scratch, but
Langchain actually gives us a utility function to compare
the embedding distance directly using OpenAI.
So it's called an evaluator and this is how you can create one.
And here's the code to run an evaluation.
So here I'm comparing the distance of the word "apple" to the word "orange".
And running this, the result is a score of 0.13.
So we don't actually know whether that's good or not
by comparing an apple to an orange, because we don't
know where 0.13 sits on the scale of other words.
So let's try a couple of other words just to see what's a better
match with apple than orange, and what's a worse match.
Here, if I compare "apple" to the word "beach", it's actually 0.2.
So "beach" is further away from "apple" than "orange"
is, I suppose because one is a fruit.
So that naturally makes it more similar.
Now if I compare the word "apple" to itself, this should
technically be 0 because it's literally the same word.
But in this case, it's close enough.
It's 2.5 x 10^-6.
Now what about if we compare the word "apple" to "iPhone"?
In this case, the score is even better than when we compared it with "orange".
The score is 0.09.
And this is really interesting as well, because in our
first example with apples and oranges, they were
both fruits, so they were similar in that respect.
But here, we're sort of interpreting the word "apple" from a different perspective.
We're seeing it as the name of the company "apple" instead.
So when you compare it with the word "iPhone",
the association is actually much stronger.
So now that you understand what embeddings are,
let's see how we can use it to fetch data.
To query for relevant data, our objective is to find
the chunks in our database that will most likely contain
the answer to the question that we want to ask.
So to do that, we'll need the database that we created
earlier, and we'll need the same embedding
function that we used to create that database.
Our goal now is to take a query, like the one
on the left here, and then turn that into
an embedding using the same function, and
then scan through our database and find
maybe five chunks of information that are
closest in embedding distance from our query.
So here, in this example, I might ask the question
like, "How does Alice meet the Mad Hatter in Alice
in Wonderland?" And when we scan our database,
we might get maybe four or five snippets of
text that we think is similar to this question.
And from that, we can put that together, have
the AI read all of that information, and decide
what is the response to give to the user.
So although we're not just simply returning the
chunks of information verbatim, we're actually
using it to craft a more custom response
that is still based on our source information.
To load the Chroma database that we created, we're
first going to need the path, which we have from
earlier, and we're going to need an embedding
function, which should be the same one we used
to create the database with in the first place.
So here, I'm just going to use the OpenAI embeddings function again.
This should load your database from that path.
If it doesn't, then just check that the path exists,
or just go back to the previous chapter and
run the script to create the database again.
Once the database is loaded, we can then search for the
chunk that best matches our query by using this method.
We need to pass in our query text as an argument and
specify the number of results we want to retrieve.
So in this example, we want to retrieve three best matches for our query.
The results of the search will be a list of tuples where
each tuple contains a document and its relevance score.
Before actually processing the results though, we can also add some checks.
For example, if there are no matches or if the
relevant score of the first result is below
a certain threshold, we can return early.
This will help us to make sure that we actually
find good, relevant information first before
moving to the next step of the process.
So now let's go to our code editor and put all that together and see what we get.
So here I've got the main function.
I just made a quick argument parser so I can
input the query text in the command line.
I've got my embeddings function and I'm going to
search the database that I've loaded and I'm
just going to print the content for each page.
So I'm going to find the top three results for my query.
So that's my script. Let's give it a go.
So here I'm running my script with the query,
which is how does Alice meet the mad hatter?
Here it's returned the three most relevant chunks
in the text that it thought best match our query.
So we have this piece of information, this piece
of information, and then this one here.
Now here the chunk size is quite small, so it doesn't
have the full context of each part of the text.
So if you want to edit that you can play with
that chunk size variable and make it either
bigger or smaller, depending on what
you think will give you the best results.
But for now, let's move on to the next step
and see if we can get the AI to use this
information and give us a direct response.
Now that we have found relevant data chunks for our
query, we can feed this into OpenAI to create a high
quality response using that data as our source.
First, we'll need a prompt template to create a prompt with.
You can use something like this.
Notice that there's placeholders for this template.
The first is the context that we're going to pass in.
So that's going to be the pieces of information that we got from the database.
And then the second is the actual query itself.
Next, here's the code to actually use that data to create the
actual prompt by formatting the template with our keys.
So after running this, you should have a single piece of string.
It's going to be quite a long string, but it's going
to be the entire prompt with all the chunks of information
and the query that you asked at the beginning.
After running that piece of code, you should
get a prompt that looks something like this.
So you're going to have this initial prompt, which is to
answer the question based on the following context.
And then we're going to have our three pieces of information.
And this can be as big or as little as we want
it to be, but here this is what we've chosen.
And then the question that we originally asked.
So here's our query.
How does Alice meet the Mad Hatter?
So this is the overall prompt that we're about to send to OpenAI.
This is actually the easy part.
So simply just call the LLM model of your choice with that prompt.
So here I'm using chatOpenAI, and then you'll have your response.
Finally, if you want to provide references back to
your source material, you can also find that in
the metadata of each of those document chunks.
So here's the code on how you can extract that out and print it out as well.
And going back to our code editor, this is what my script
looks like with all of those pieces put together.
So I've got my prompt template here.
I've got my main argument here, which takes the query, searches
the database for the relevant chunks, creates the
prompt, and then uses the LLM to answer the question.
And then here I'm collecting all the sources that were used
to answer the prompt and print out the entire response.
Let's go ahead and run that.
And here's the result of running that script.
So again, we see the entire prompt here, and this is the final response.
The response is Alice meets the Mad Hatter by walking in
the direction where the March Hare was set to live.
And obviously it took this from the first piece of the context.
And here we also have a list of the source references that it got it from.
This is pretty much pointing to the same file because I only
made it print out the actual file itself and not the index.
But this is pretty good already because you can
see how it's using our query to search for
pieces of information from our source material
and then answer based on that information.
Now let me switch up my data source and show you
a different example just so you can see what
else you can do with something like this.
I switched my database to one I prepared earlier, which
uses the AWS Lambda documentation as a source.
And here the query I'm going to ask it is what
languages or runtimes does AWS Lambda support?
So after I ran this, you can see that the chunks
I use here are much bigger than in the previous
example, but it still managed to find three relevant
chunks of my information and it's published
a response that summarizes that information.
So here it says AWS Lambda supports Java, C#, Python, and etc.
You can read the rest here.
But this is more interesting because unlike in the first
example, the sources were actually from different files.
So you can see here that each of the source is its own file.
So this is useful as well.
If you have data source that spread out across a lot of different
files and you want to see how to reference the source.
So we just covered how you can use the line chain and OpenAI
to create a retrieval augmented generation app.
I'll post a link to the GitHub code in the video
description and I encourage you to try this
out for yourself and with your own data set.
If you want to see more tutorials like this, then please let
me know what type of topics you'd be interested to see next.
Otherwise, I hope you found this useful and thank you for watching.
Ver Más Videos Relacionados
GPT ACTIONS // Cómo editar el SCHEMA [Tutorial completo]
30. Rutas dinámicas con vue-router y useRoute | AbiDev
¿Como realizar una Base de Datos en Google Sheets? Base de datos en la nube Gratis
ESP32 FIREBASE 4 (APP INVENTOR)
Cómo CONECTAR un FORMULARIO con una BASE de DATOS en NOTION
CÓMO CONSUMIR UN API con JAVASCRIPT desde la web
5.0 / 5 (0 votes)