From 60b0dee980ef2f75ed1f518f3326d2c68cacc4b5 Mon Sep 17 00:00:00 2001 From: Thibault Genaitay Date: Mon, 7 Oct 2024 17:13:28 +0200 Subject: [PATCH 01/13] feat(genapi): rag tutorial basics --- .../index.mdx | 360 ++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 tutorials/how-to-implement-rag-generativeapis/index.mdx diff --git a/tutorials/how-to-implement-rag-generativeapis/index.mdx b/tutorials/how-to-implement-rag-generativeapis/index.mdx new file mode 100644 index 0000000000..f85ed079f7 --- /dev/null +++ b/tutorials/how-to-implement-rag-generativeapis/index.mdx @@ -0,0 +1,360 @@ +--- +meta: + title: Implementing Retrieval-Augmented Generation (RAG) with LangChain and Scaleway Generative APIs + description: Step by step Retrieval-Augmented Generation (RAG) with LangChain and Scaleway Generative APIs +content: + h1: Implementing Retrieval-Augmented Generation (RAG) with LangChain and Scaleway Generative APIs +tags: inference API postgresql pgvector object storage RAG langchain machine learning AI language models +categories: + - inference +--- + +Retrieval-Augmented Generation (RAG) enhances language models by incorporating relevant information from your own datasets. This hybrid approach improves both the accuracy and contextual relevance of the model's outputs, making it ideal for advanced AI applications. + +In this tutorial, you will learn how to implement RAG using LangChain, a leading framework for developing powerful language model applications. We will integrate LangChain with **Scaleway’s Generative APIs**, **Scaleway’s PostgreSQL Managed Database** (utilizing pgvector for vector storage), and **Scaleway’s Object Storage** to ensure seamless data management and efficient integration. + +## What you will learn +- How to embed text using ***Scaleway Generative APIs*** +- How to store and query embeddings using ***Scaleway’s Managed PostgreSQL Database*** with pgvector +- How to manage large datasets efficiently with ***Scaleway Object Storage*** + + + +- A Scaleway account logged into the [console](https://console.scaleway.com) +- [Owner](/identity-and-access-management/iam/concepts/#owner) status or [IAM permissions](/identity-and-access-management/iam/concepts/#permission) allowing you to perform actions in the intended Organization +- A valid [API key](/identity-and-access-management/iam/how-to/create-api-keys/) +- Access to the [Generative APIs service](ai-data/generative-apis/quickstart/) +- An [Object Storage Bucket](/storage/object/how-to/create-a-bucket/) to store all the data you want to inject into your LLM model. +- A [Managed Database](/managed-databases/postgresql-and-mysql/how-to/create-a-database/) to securely store all your embeddings. + +## Configure your development environment + +### Install required packages + +Run the following command to install the required packages: + + ```sh + pip install langchain psycopg2 python-dotenv + ``` +### Create a .env file + +Create a .env file and add the following variables. These will store your API keys, database connection details, and other configuration values. + + ```sh + # .env file + + # Scaleway API credentials + SCW_ACCESS_KEY=your_scaleway_access_key + SCW_API_KEY=your_scaleway_secret_key + + # Scaleway managed database (PostgreSQL) credentials + SCW_DB_NAME=your_scaleway_managed_db_name + SCW_DB_USER=your_scaleway_managed_db_username + SCW_DB_PASSWORD=your_scaleway_managed_db_password + SCW_DB_HOST=your_scaleway_managed_db_host # The IP address of your database instance + SCW_DB_PORT=your_scaleway_managed_db_port # The port number for your database instance + + # Scaleway S3 bucket configuration + SCW_BUCKET_NAME=your_scaleway_bucket_name + SCW_BUCKET_ENDPOINT="https://{{SCW_BUCKET_NAME}}.s3.{{SCW_REGION}}.scw.cloud" # S3 endpoint, e.g., https://s3.fr-par.scw.cloud + + # Scaleway Generative APIs endpoint + SCW_GENERATIVE_APIs_ENDPOINT="https://api.scaleway.ai/v1" + ``` + +## Setting Up Managed Databases + +### Connect to your PostgreSQL database + +To perform these actions, you will need to connect to your PostgreSQL database. You can use any PostgreSQL client, such as [psql](https://www.postgresql.org/docs/current/app-psql.html). The following steps will guide you through setting up your database to handle vector storage and document tracking. + +### Install the pgvector extension + +[pgvector](https://github.com/pgvector/pgvector) is essential for storing and indexing high-dimensional vectors, which are critical for retrieval-augmented generation (RAG) systems. Ensure that it is installed by executing the following SQL command: + +```sql + CREATE EXTENSION IF NOT EXISTS vector; +``` +### Create a table to track processed documents + +To prevent reprocessing documents that have already been loaded and vectorized, you should create a table to keep track of them. This will ensure that new documents added to your object storage bucket are only processed once, avoiding duplicate downloads and redundant vectorization: + +```sql + CREATE TABLE IF NOT EXISTS object_loaded (id SERIAL PRIMARY KEY, object_key TEXT); +``` + +### Connect to PostgreSQL programmatically + +Connect to your PostgreSQL instance and perform tasks programmatically. + + ```python + # rag.py file + +from dotenv import load_dotenv +import psycopg2 +import os + +# Load environment variables +load_dotenv() + +# Establish connection to PostgreSQL database using environment variables +conn = psycopg2.connect( + database=os.getenv("SCW_DB_NAME"), + user=os.getenv("SCW_DB_USER"), + password=os.getenv("SCW_DB_PASSWORD"), + host=os.getenv("SCW_DB_HOST"), + port=os.getenv("SCW_DB_PORT") + ) + +# Create a cursor to execute SQL commands +cur = conn.cursor() + ``` + +## Embeddings and vector store setup + +### Import required modules + +```python +# rag.py + +from langchain_openai import OpenAIEmbeddings +from langchain_postgres import PGVector +``` + +### Configure OpenAI Embeddings + +We will use the [OpenAIEmbeddings](https://api.python.langchain.com/en/latest/embeddings/langchain_openai.embeddings.base.OpenAIEmbeddings.html) class from LangChain and store the embeddings in PostgreSQL using the PGVector integration. + +```python +# rag.py + +embeddings = OpenAIEmbeddings( + openai_api_key=os.getenv("SCW_API_KEY"), + openai_api_base=os.getenv("SCW_GENERATIVE_APIs_ENDPOINT"), + model="sentence-transformers/sentence-t5-xxl", + tiktoken_enabled=False, + ) +``` + +#### Key parameters: +- `openai_api_key`: This is your API key for accessing the OpenAI-powered embeddings service, in this case, hosted by Scaleway’s Generative APIs. +- `openai_api_base`: This is the base URL that points Scaleway Generative APIs where the embedding model is hosted. This URL serves as the entry point to make API calls for generating embeddings. +- `model="sentence-transformers/sentence-t5-xxl"`: This defines the specific model being used for text embeddings. sentence-transformers/sentence-t5-xxl is a powerful model optimized for generating high-quality sentence embeddings, making it ideal for tasks like document retrieval in RAG systems. +- `tiktoken_enabled=False`: This is parameter disables the use of TikToken for tokenization within the embeddings process. + +#### What is tiktoken_enabled? + +[`tiktoken`](https://github.com/openai/tiktoken) is a tokenization library developed by OpenAI, which is optimized for working with GPT-based models (like GPT-3.5 or GPT-4). It transforms text into smaller token units that the model can process. + +#### Why set tiktoken_enabled=False? + +In the context of using Scaleway’s Generative APIs and the `sentence-t5-xxl` model of this example, TikToken tokenization is not necessary because the model you are using (sentence-transformers) works with raw text and handles its own tokenization internally. +Moreover, leaving `tiktoken_enabled` as `True` causes issues when sending data to Scaleway’s API because it results in tokenized vectors being sent instead of raw text. Since Scaleway's endpoint expects text and not pre-tokenized data, this mismatch can lead to errors or incorrect behavior. +By setting `tiktoken_enabled=False`, you ensure that raw text is sent to Scaleway's Generative APIs endpoint, which is what the sentence-transformers model expects to process. This guarantees that the embedding generation process works smoothly with Scaleway's infrastructure. + +### Create a PGVector store + +Configure the connection string for your PostgreSQL instance and create a PGVector store to store these embeddings. + +```python +# rag.py + +connection_string = f"postgresql+psycopg2://{conn.info.user}:{conn.info.password}@{conn.info.host}:{conn.info.port}/{conn.info.dbname}" +vector_store = PGVector(connection=connection_string, embeddings=embeddings) +``` + +PGVector: This creates the vector store in your PostgreSQL database to store the embeddings. + +## Load and process documents + +Use the [`S3FileLoader`](https://api.python.langchain.com/en/latest/document_loaders/langchain_community.document_loaders.s3_file.S3FileLoader.html) to load documents and split them into chunks. Then, embed and store them in your PostgreSQL database. + +### Import required modules + +```python +#rag.py + +import boto3 +from langchain_community.document_loaders import S3FileLoader +from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain_openai import OpenAIEmbeddings + +``` + +### Load metadata for improved efficiency + +Load metadata for improved efficiency: By loading the metadata for all objects in your bucket, you can speed up the process significantly. This allows you to quickly check if a document has already been embedded without the need to load the entire document. + +```python +# rag.py + +endpoint_s3 = f"https://s3.{os.getenv('SCW_DEFAULT_REGION', '')}.scw.cloud" +session = boto3.session.Session() +client_s3 = session.client(service_name='s3', endpoint_url=endpoint_s3, + aws_access_key_id=os.getenv("SCW_ACCESS_KEY", ""), + aws_secret_access_key=os.getenv("SCW_SECRET_KEY", "")) +paginator = client_s3.get_paginator('list_objects_v2') +page_iterator = paginator.paginate(Bucket=BUCKET_NAME) +``` + +In this code sample we: +- Set up a Boto3 session: We initialize a Boto3 session, which is the AWS SDK for Python, fully compatible with Scaleway Object Storage. This session manages configuration, including credentials and settings, that Boto3 uses for API requests. +- Create an S3 client: We establish an S3 client to interact with the Scaleway Object Storage service. +- Set up pagination for listing objects: We prepare pagination to handle potentially large lists of objects efficiently. +- Iterate through the bucket: This initiates the pagination process, allowing us to list all objects within the specified Scaleway Object bucket seamlessly. + +### Iterate through metadata + +Iterate through metadata: Next, we will iterate through the metadata to determine if each object has already been embedded. If an object hasn’t been processed yet, we will embed it and load it into the database. + +```python +# rag.py + +text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=0, add_start_index=True, length_function=len, is_separator_regex=False) +for page in page_iterator: + for obj in page.get('Contents', []): + cur.execute("SELECT object_key FROM object_loaded WHERE object_key = %s", (obj['Key'],)) + response = cur.fetchone() + if response is None: + file_loader = S3FileLoader( + bucket=BUCKET_NAME, + key=obj['Key'], + endpoint_url=endpoint_s3, + aws_access_key_id=os.getenv("SCW_ACCESS_KEY", ""), + aws_secret_access_key=os.getenv("SCW_SECRET_KEY", "") + ) + file_to_load = file_loader.load() + cur.execute("INSERT INTO object_loaded (object_key) VALUES (%s)", (obj['Key'],)) + chunks = text_splitter.split_text(file_to_load[0].page_content) + try: + embeddings_list = [embeddings.embed_query(chunk) for chunk in chunks] + vector_store.add_embeddings(chunks, embeddings_list) + cur.execute("INSERT INTO object_loaded (object_key) VALUES (%s)", (obj['Key'],)) + except Exception as e: + logger.error(f"An error occurred: {e}") + +conn.commit() +``` + +- S3FileLoader: The S3FileLoader loads each file individually from your ***Scaleway Object Storage bucket*** using the file's object_key (extracted from the file's metadata). It ensures that only the specific file is loaded from the bucket, minimizing the amount of data being retrieved at any given time. +- RecursiveCharacterTextSplitter: The RecursiveCharacterTextSplitter breaks each document into smaller chunks of text. This is crucial because embeddings models, like those used in Retrieval-Augmented Generation (RAG), typically have a limited context window (the number of tokens they can process at once). +- Embedding the chunks: For each document, the text is split into smaller chunks using the text splitter, and an embedding is generated for each chunk using the embeddings.embed_query(chunk) function. This function transforms each chunk into a vector representation that can later be used for similarity search. +- Embedding storage: After generating the embeddings for each chunk, they are stored in a vector database (e.g., PostgreSQL with pgvector) using the vector_store.add_embeddings(embedding, chunk) method. Each embedding is stored alongside its corresponding text chunk, enabling retrieval during a query. +- Avoiding redundant processing: The script checks the object_loaded table in PostgreSQL to see if a document has already been processed (i.e., the object_key exists in the table). If it has, the file is skipped, avoiding redundant downloads, vectorization, and database inserts. This ensures that only new or modified documents are processed, reducing the system's computational load and saving both time and resources. + +#### Why 500 characters? + +The chunk size of 500 characters is chosen to fit comfortably within the context size limits of typical embeddings models, which often range between 512 and 1024 tokens. Since most models tokenize text into smaller units (tokens) based on words, punctuation, and subwords, the exact number of tokens for 480 characters will vary depending on the language and the content. By keeping chunks small, we avoid exceeding the model’s context window, which could lead to truncated embeddings or poor performance during inference. + +This approach ensures that only new or modified documents are loaded into memory and embedded, saving significant computational resources and reducing redundant work. + +#### Why store both chunk and embedding? + +Storing both the chunk and its corresponding embedding allows for efficient document retrieval later. +When a query is made, the RAG system will retrieve the most relevant embeddings, and the corresponding text chunks will be used to generate the final response. + +### Query the RAG System with a pre-defined prompt template + +### Import required modules + +```python +#rag.py + +from langchain import hub +from langchain_core.output_parsers import StrOutputParser +from langchain_core.runnables import RunnablePassthrough + +``` + +### Setup LLM for querying + +Now, set up the RAG system to handle queries + +```python +#rag.py + +llm = ChatOpenAI( + base_url=os.getenv("SCW_GENERATIVE_APIs_ENDPOINT"), + api_key=os.getenv("SCW_SECRET_KEY"), + model="llama-3.1-8b-instruct", + ) + +prompt = hub.pull("rlm/rag-prompt") +retriever = vector_store.as_retriever() + + +rag_chain = ( + {"context": retriever, "question": RunnablePassthrough()} + | prompt + | llm + | StrOutputParser() + ) + +for r in rag_chain.stream("Your question"): + print(r, end="", flush=True) + time.sleep(0.1) +``` +- LLM initialization: We initialize the ChatOpenAI instance using the endpoint and API key from the environment variables, along with the specified model name. + +- Prompt setup: The prompt is pulled from the hub using a pre-defined template, ensuring consistent query formatting. + +- Retriever configuration: We set up the retriever to access the vector store, allowing the RAG system to retrieve relevant information based on the query. + +- RAG chain construction: We create the RAG chain, which connects the retriever, prompt, LLM, and output parser in a streamlined workflow. + +- Query execution: Finally, we stream the output of the RAG chain for a specified question, printing each response with a slight delay for better readability. + +### Query the RAG system with your own prompt template + +Personalizing your prompt template allows you to tailor the responses from your RAG (Retrieval-Augmented Generation) system to better fit your specific needs. This can significantly improve the relevance and tone of the answers you receive. Below is a detailed guide on how to create a custom prompt for querying the system. + +```python +#rag.py + +from langchain.chains.combine_documents import create_stuff_documents_chain +from langchain_core.prompts import PromptTemplate +from langchain_openai import ChatOpenAI + +llm = ChatOpenAI( + base_url=os.getenv("SCW_GENERATIVE_APIs_ENDPOINT"), + api_key=os.getenv("SCW_SECRET_KEY"), + model="llama-3.1-8b-instruct", + ) +prompt = """Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. Always finish your answer with "Thank you for asking". {context} Question: {question} Helpful Answer:""" +custom_rag_prompt = PromptTemplate.from_template(prompt) +retriever = vector_store.as_retriever() +custom_rag_chain = create_stuff_documents_chain(llm, custom_rag_prompt) + + +context = retriever.invoke("your question") +for r in custom_rag_chain.stream({"question":"your question", "context": context}): + print(r, end="", flush=True) + time.sleep(0.1) +``` + +- Prompt template: The prompt template is meticulously crafted to direct the model's responses. It clearly instructs the model on how to leverage the provided context and emphasizes the importance of honesty in cases where it lacks information. +To make the responses more engaging, consider adding a light-hearted conclusion or a personalized touch. For example, you might modify the closing line to say, "Thank you for asking! I'm here to help with anything else you need!" +Retrieving context: +- The retriever.invoke(new_message) method fetches relevant information from your vector store based on the user’s query. It's essential that this step retrieves high-quality context to ensure that the model's responses are accurate and helpful. +You can enhance the quality of the context by fine-tuning your embeddings and ensuring that the documents in your vector store are relevant and well-structured. +Creating the RAG chain: +- The create_stuff_documents_chain function connects the language model with your custom prompt. This integration allows the model to process the retrieved context effectively and formulate a coherent and context-aware response. +Consider experimenting with different chain configurations to see how they affect the output. For instance, using a different chain type may yield varied responses. +Streaming responses: +- The loop that streams responses from the custom_rag_chain provides a dynamic user experience. Instead of waiting for the entire output, users can see responses as they are generated, enhancing interactivity. +You can customize the streaming behavior further, such as implementing progress indicators or more sophisticated UI elements for applications. + +#### Example use cases +- Customer support: Use a custom prompt to answer customer queries effectively, making the interactions feel more personalized and engaging. +- Research assistance: Tailor prompts to provide concise summaries or detailed explanations on specific topics, enhancing your research capabilities. +- Content generation: Personalize prompts for creative writing, generating responses that align with specific themes or tones. + +## Conclusion + +In this tutorial, we explored essential techniques for efficiently processing and storing large document datasets within a Retrieval-Augmented Generation (RAG) system. By leveraging metadata, we ensured that our system avoids redundant data handling, allowing for smooth and efficient operations. The use of chunking optimizes document processing, maximizing the performance of the language model. Storing embeddings in PostgreSQL via pgvector enables rapid and scalable retrieval, ensuring quick responses to user queries. + +Furthermore, you can continually enhance your RAG system by implementing mechanisms to retain chat history. Keeping track of past interactions allows for more contextually aware responses, fostering a more engaging user experience. This historical data can be used to refine your prompts, adapt to user preferences, and improve the overall accuracy of responses. + +By integrating Scaleway Object Storage, Managed Database for PostgreSQL with pgvector, and LangChain’s embedding tools, you have the foundation to build a powerful RAG system that scales with your data while offering robust information retrieval capabilities. This approach equips you with the tools necessary to handle complex queries and deliver accurate, relevant results efficiently. + +With ongoing refinement and adaptation, your RAG system can evolve to meet the changing needs of your users, ensuring that it remains a valuable asset in your AI toolkit. \ No newline at end of file From 165c8a6380427527feabd27f90850ce02417c7a8 Mon Sep 17 00:00:00 2001 From: Thibault Genaitay Date: Tue, 8 Oct 2024 12:26:45 +0200 Subject: [PATCH 02/13] feat(genapi): improved instructions --- .../index.mdx | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/tutorials/how-to-implement-rag-generativeapis/index.mdx b/tutorials/how-to-implement-rag-generativeapis/index.mdx index f85ed079f7..38c27d9bac 100644 --- a/tutorials/how-to-implement-rag-generativeapis/index.mdx +++ b/tutorials/how-to-implement-rag-generativeapis/index.mdx @@ -4,7 +4,7 @@ meta: description: Step by step Retrieval-Augmented Generation (RAG) with LangChain and Scaleway Generative APIs content: h1: Implementing Retrieval-Augmented Generation (RAG) with LangChain and Scaleway Generative APIs -tags: inference API postgresql pgvector object storage RAG langchain machine learning AI language models +tags: inference API postgresql pgvector object storage RAG langchain AI LLMs embeddings categories: - inference --- @@ -43,30 +43,31 @@ Create a .env file and add the following variables. These will store your API ke ```sh # .env file - # Scaleway API credentials - SCW_ACCESS_KEY=your_scaleway_access_key + # Scaleway API credentials https://console.scaleway.com/iam/api-keys + SCW_ACCESS_KEY=your_scaleway_access_key_id SCW_API_KEY=your_scaleway_secret_key - # Scaleway managed database (PostgreSQL) credentials - SCW_DB_NAME=your_scaleway_managed_db_name + # Scaleway Managed Database (PostgreSQL) credentials SCW_DB_USER=your_scaleway_managed_db_username SCW_DB_PASSWORD=your_scaleway_managed_db_password + SCW_DB_NAME="rdb" SCW_DB_HOST=your_scaleway_managed_db_host # The IP address of your database instance SCW_DB_PORT=your_scaleway_managed_db_port # The port number for your database instance # Scaleway S3 bucket configuration SCW_BUCKET_NAME=your_scaleway_bucket_name - SCW_BUCKET_ENDPOINT="https://{{SCW_BUCKET_NAME}}.s3.{{SCW_REGION}}.scw.cloud" # S3 endpoint, e.g., https://s3.fr-par.scw.cloud + SCW_REGION=fr-par + SCW_BUCKET_ENDPOINT="https://{{SCW_BUCKET_NAME}}.s3.{{SCW_REGION}}.scw.cloud" # S3 endpoint, e.g., https://name.s3.fr-par.scw.cloud # Scaleway Generative APIs endpoint SCW_GENERATIVE_APIs_ENDPOINT="https://api.scaleway.ai/v1" ``` -## Setting Up Managed Databases +## Setting Up Managed Database ### Connect to your PostgreSQL database -To perform these actions, you will need to connect to your PostgreSQL database. You can use any PostgreSQL client, such as [psql](https://www.postgresql.org/docs/current/app-psql.html). The following steps will guide you through setting up your database to handle vector storage and document tracking. +You can use any PostgreSQL client, such as [psql](https://www.postgresql.org/docs/current/app-psql.html). The following steps will guide you through setting up your database to handle vector storage and document tracking. ### Install the pgvector extension @@ -188,13 +189,12 @@ Load metadata for improved efficiency: By loading the metadata for all objects i ```python # rag.py -endpoint_s3 = f"https://s3.{os.getenv('SCW_DEFAULT_REGION', '')}.scw.cloud" session = boto3.session.Session() -client_s3 = session.client(service_name='s3', endpoint_url=endpoint_s3, +client_s3 = session.client(service_name='s3', endpoint_url=os.getenv("SCW_BUCKET_ENDPOINT", ""), aws_access_key_id=os.getenv("SCW_ACCESS_KEY", ""), - aws_secret_access_key=os.getenv("SCW_SECRET_KEY", "")) + aws_secret_access_key=os.getenv("SCW_API_KEY", "")) paginator = client_s3.get_paginator('list_objects_v2') -page_iterator = paginator.paginate(Bucket=BUCKET_NAME) +page_iterator = paginator.paginate(Bucket=os.getenv("SCW_BUCKET_NAME", "")) ``` In this code sample we: @@ -215,23 +215,23 @@ for page in page_iterator: for obj in page.get('Contents', []): cur.execute("SELECT object_key FROM object_loaded WHERE object_key = %s", (obj['Key'],)) response = cur.fetchone() - if response is None: - file_loader = S3FileLoader( - bucket=BUCKET_NAME, - key=obj['Key'], - endpoint_url=endpoint_s3, - aws_access_key_id=os.getenv("SCW_ACCESS_KEY", ""), - aws_secret_access_key=os.getenv("SCW_SECRET_KEY", "") - ) - file_to_load = file_loader.load() + if response is None: + file_loader = S3FileLoader( + bucket=os.getenv("SCW_BUCKET_NAME", ""), + key=obj['Key'], + endpoint_url=os.getenv("SCW_BUCKET_ENDPOINT", ""), + aws_access_key_id=os.getenv("SCW_ACCESS_KEY", ""), + aws_secret_access_key=os.getenv("SCW_API_KEY", "") + ) + file_to_load = file_loader.load() + cur.execute("INSERT INTO object_loaded (object_key) VALUES (%s)", (obj['Key'],)) + chunks = text_splitter.split_text(file_to_load[0].page_content) + try: + embeddings_list = [embeddings.embed_query(chunk) for chunk in chunks] + vector_store.add_embeddings(chunks, embeddings_list) cur.execute("INSERT INTO object_loaded (object_key) VALUES (%s)", (obj['Key'],)) - chunks = text_splitter.split_text(file_to_load[0].page_content) - try: - embeddings_list = [embeddings.embed_query(chunk) for chunk in chunks] - vector_store.add_embeddings(chunks, embeddings_list) - cur.execute("INSERT INTO object_loaded (object_key) VALUES (%s)", (obj['Key'],)) - except Exception as e: - logger.error(f"An error occurred: {e}") + except Exception as e: + logger.error(f"An error occurred: {e}") conn.commit() ``` From 50fdebffffc0230af845d01c00feaab05f4b2ae1 Mon Sep 17 00:00:00 2001 From: Thibault Genaitay Date: Tue, 8 Oct 2024 12:30:16 +0200 Subject: [PATCH 03/13] feat(genapi): simplified --- .../index.mdx | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tutorials/how-to-implement-rag-generativeapis/index.mdx b/tutorials/how-to-implement-rag-generativeapis/index.mdx index 38c27d9bac..5294a91a8b 100644 --- a/tutorials/how-to-implement-rag-generativeapis/index.mdx +++ b/tutorials/how-to-implement-rag-generativeapis/index.mdx @@ -44,10 +44,12 @@ Create a .env file and add the following variables. These will store your API ke # .env file # Scaleway API credentials https://console.scaleway.com/iam/api-keys + ## Will be used to authenticate to Scaleway Object Storage and Scaleway Generative APIs SCW_ACCESS_KEY=your_scaleway_access_key_id SCW_API_KEY=your_scaleway_secret_key # Scaleway Managed Database (PostgreSQL) credentials + ## Will be used to store embeddings of your proprietary data SCW_DB_USER=your_scaleway_managed_db_username SCW_DB_PASSWORD=your_scaleway_managed_db_password SCW_DB_NAME="rdb" @@ -55,15 +57,17 @@ Create a .env file and add the following variables. These will store your API ke SCW_DB_PORT=your_scaleway_managed_db_port # The port number for your database instance # Scaleway S3 bucket configuration + ## Will be used to store your proprietary data (PDF, CSV etc) SCW_BUCKET_NAME=your_scaleway_bucket_name SCW_REGION=fr-par SCW_BUCKET_ENDPOINT="https://{{SCW_BUCKET_NAME}}.s3.{{SCW_REGION}}.scw.cloud" # S3 endpoint, e.g., https://name.s3.fr-par.scw.cloud # Scaleway Generative APIs endpoint + ## LLM and Embedding model are served through this base URL SCW_GENERATIVE_APIs_ENDPOINT="https://api.scaleway.ai/v1" ``` -## Setting Up Managed Database +## Setting Up Scaleway Managed Database ### Connect to your PostgreSQL database @@ -141,17 +145,7 @@ embeddings = OpenAIEmbeddings( - `openai_api_key`: This is your API key for accessing the OpenAI-powered embeddings service, in this case, hosted by Scaleway’s Generative APIs. - `openai_api_base`: This is the base URL that points Scaleway Generative APIs where the embedding model is hosted. This URL serves as the entry point to make API calls for generating embeddings. - `model="sentence-transformers/sentence-t5-xxl"`: This defines the specific model being used for text embeddings. sentence-transformers/sentence-t5-xxl is a powerful model optimized for generating high-quality sentence embeddings, making it ideal for tasks like document retrieval in RAG systems. -- `tiktoken_enabled=False`: This is parameter disables the use of TikToken for tokenization within the embeddings process. - -#### What is tiktoken_enabled? - -[`tiktoken`](https://github.com/openai/tiktoken) is a tokenization library developed by OpenAI, which is optimized for working with GPT-based models (like GPT-3.5 or GPT-4). It transforms text into smaller token units that the model can process. - -#### Why set tiktoken_enabled=False? - -In the context of using Scaleway’s Generative APIs and the `sentence-t5-xxl` model of this example, TikToken tokenization is not necessary because the model you are using (sentence-transformers) works with raw text and handles its own tokenization internally. -Moreover, leaving `tiktoken_enabled` as `True` causes issues when sending data to Scaleway’s API because it results in tokenized vectors being sent instead of raw text. Since Scaleway's endpoint expects text and not pre-tokenized data, this mismatch can lead to errors or incorrect behavior. -By setting `tiktoken_enabled=False`, you ensure that raw text is sent to Scaleway's Generative APIs endpoint, which is what the sentence-transformers model expects to process. This guarantees that the embedding generation process works smoothly with Scaleway's infrastructure. +- `tiktoken_enabled=False`: This parameter disables the use of TikToken for tokenization within the embeddings process. ### Create a PGVector store From 15ebd78ed0c3c5c7b5b0ed85a1850b7fa8de15e4 Mon Sep 17 00:00:00 2001 From: Thibault Genaitay Date: Wed, 9 Oct 2024 22:39:49 +0200 Subject: [PATCH 04/13] feat(genapi): fixed errors and created pixtral tutorial --- .../index.mdx | 28 +- .../index.mdx | 257 ++++++++++++++++++ 2 files changed, 271 insertions(+), 14 deletions(-) create mode 100644 tutorials/processing-images-structured-outputs-pixtral/index.mdx diff --git a/tutorials/how-to-implement-rag-generativeapis/index.mdx b/tutorials/how-to-implement-rag-generativeapis/index.mdx index 5294a91a8b..d66c576808 100644 --- a/tutorials/how-to-implement-rag-generativeapis/index.mdx +++ b/tutorials/how-to-implement-rag-generativeapis/index.mdx @@ -34,7 +34,7 @@ In this tutorial, you will learn how to implement RAG using LangChain, a leading Run the following command to install the required packages: ```sh - pip install langchain psycopg2 python-dotenv + pip install langchain psycopg2 python-dotenv langchainhub ``` ### Create a .env file @@ -60,7 +60,7 @@ Create a .env file and add the following variables. These will store your API ke ## Will be used to store your proprietary data (PDF, CSV etc) SCW_BUCKET_NAME=your_scaleway_bucket_name SCW_REGION=fr-par - SCW_BUCKET_ENDPOINT="https://{{SCW_BUCKET_NAME}}.s3.{{SCW_REGION}}.scw.cloud" # S3 endpoint, e.g., https://name.s3.fr-par.scw.cloud + SCW_BUCKET_ENDPOINT="https://s3.{{SCW_REGION}}.scw.cloud" # S3 main endpoint, e.g., https://s3.fr-par.scw.cloud # Scaleway Generative APIs endpoint ## LLM and Embedding model are served through this base URL @@ -136,7 +136,7 @@ We will use the [OpenAIEmbeddings](https://api.python.langchain.com/en/latest/em embeddings = OpenAIEmbeddings( openai_api_key=os.getenv("SCW_API_KEY"), openai_api_base=os.getenv("SCW_GENERATIVE_APIs_ENDPOINT"), - model="sentence-transformers/sentence-t5-xxl", + model="sentence-t5-xxl", tiktoken_enabled=False, ) ``` @@ -144,7 +144,7 @@ embeddings = OpenAIEmbeddings( #### Key parameters: - `openai_api_key`: This is your API key for accessing the OpenAI-powered embeddings service, in this case, hosted by Scaleway’s Generative APIs. - `openai_api_base`: This is the base URL that points Scaleway Generative APIs where the embedding model is hosted. This URL serves as the entry point to make API calls for generating embeddings. -- `model="sentence-transformers/sentence-t5-xxl"`: This defines the specific model being used for text embeddings. sentence-transformers/sentence-t5-xxl is a powerful model optimized for generating high-quality sentence embeddings, making it ideal for tasks like document retrieval in RAG systems. +- `model="sentence-t5-xxl"`: This defines the specific model being used for text embeddings. sentence-transformers/sentence-t5-xxl is a powerful model optimized for generating high-quality sentence embeddings, making it ideal for tasks like document retrieval in RAG systems. - `tiktoken_enabled=False`: This parameter disables the use of TikToken for tokenization within the embeddings process. ### Create a PGVector store @@ -158,11 +158,12 @@ connection_string = f"postgresql+psycopg2://{conn.info.user}:{conn.info.password vector_store = PGVector(connection=connection_string, embeddings=embeddings) ``` -PGVector: This creates the vector store in your PostgreSQL database to store the embeddings. - ## Load and process documents -Use the [`S3FileLoader`](https://api.python.langchain.com/en/latest/document_loaders/langchain_community.document_loaders.s3_file.S3FileLoader.html) to load documents and split them into chunks. Then, embed and store them in your PostgreSQL database. +At this stage, you need to have proprietary data (e.g PDF, CSV) stored in your Scaleway Object storage bucket. + +Below we will use Langchain's [`S3FileLoader`](https://api.python.langchain.com/en/latest/document_loaders/langchain_community.document_loaders.s3_file.S3FileLoader.html) to load documents and split them into chunks. +Then, we will embed and store them in your PostgreSQL database. ### Import required modules @@ -172,13 +173,12 @@ Use the [`S3FileLoader`](https://api.python.langchain.com/en/latest/document_loa import boto3 from langchain_community.document_loaders import S3FileLoader from langchain.text_splitter import RecursiveCharacterTextSplitter -from langchain_openai import OpenAIEmbeddings ``` ### Load metadata for improved efficiency -Load metadata for improved efficiency: By loading the metadata for all objects in your bucket, you can speed up the process significantly. This allows you to quickly check if a document has already been embedded without the need to load the entire document. +By loading the metadata for all objects in your bucket, you can speed up the process significantly. This allows you to quickly check if a document has already been embedded without the need to load the entire document. ```python # rag.py @@ -199,7 +199,7 @@ In this code sample we: ### Iterate through metadata -Iterate through metadata: Next, we will iterate through the metadata to determine if each object has already been embedded. If an object hasn’t been processed yet, we will embed it and load it into the database. +Next, we will iterate through the metadata to determine if each object has already been embedded. If an object hasn’t been processed yet, we will embed it and load it into the database. ```python # rag.py @@ -238,9 +238,7 @@ conn.commit() #### Why 500 characters? -The chunk size of 500 characters is chosen to fit comfortably within the context size limits of typical embeddings models, which often range between 512 and 1024 tokens. Since most models tokenize text into smaller units (tokens) based on words, punctuation, and subwords, the exact number of tokens for 480 characters will vary depending on the language and the content. By keeping chunks small, we avoid exceeding the model’s context window, which could lead to truncated embeddings or poor performance during inference. - -This approach ensures that only new or modified documents are loaded into memory and embedded, saving significant computational resources and reducing redundant work. +The chunk size of 500 characters is chosen to fit comfortably within the context size limit if the embedding model used in this tutorial. By keeping chunks small, we avoid exceeding the model’s context window, which could lead to truncated embeddings or poor performance during inference. #### Why store both chunk and embedding? @@ -257,6 +255,7 @@ When a query is made, the RAG system will retrieve the most relevant embeddings, from langchain import hub from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough +from langchain_openai import ChatOpenAI ``` @@ -269,7 +268,7 @@ Now, set up the RAG system to handle queries llm = ChatOpenAI( base_url=os.getenv("SCW_GENERATIVE_APIs_ENDPOINT"), - api_key=os.getenv("SCW_SECRET_KEY"), + api_key=os.getenv("SCW_API_KEY"), model="llama-3.1-8b-instruct", ) @@ -308,6 +307,7 @@ Personalizing your prompt template allows you to tailor the responses from your from langchain.chains.combine_documents import create_stuff_documents_chain from langchain_core.prompts import PromptTemplate from langchain_openai import ChatOpenAI +import time llm = ChatOpenAI( base_url=os.getenv("SCW_GENERATIVE_APIs_ENDPOINT"), diff --git a/tutorials/processing-images-structured-outputs-pixtral/index.mdx b/tutorials/processing-images-structured-outputs-pixtral/index.mdx new file mode 100644 index 0000000000..cf28b19f3b --- /dev/null +++ b/tutorials/processing-images-structured-outputs-pixtral/index.mdx @@ -0,0 +1,257 @@ +--- +meta: + title: Processing Images and Getting Structured Outputs with Pixtral Vision Model + description: Learn how to use Mistral's Pixtral vision model to automatically generate a product catalog from images. +content: + h1: Processing Images and Getting Structured Outputs with Pixtral Vision Model + paragraph: Discover how to leverage Mistral's Pixtral vision model to analyze images and generate comprehensive structured outputs. +tags: AI vision-model image-processing Pixtral Mistral structured-data +categories: + - artificial-intelligence + - computer-vision +hero: assets/scaleway-pixtral-hero.webp +dates: + validation: 2024-10-09 + posted: 2024-10-09 +--- + +In today's data-driven world, the ability to extract structured information from visual content is becoming increasingly valuable across various industries. From analyzing medical images to interpreting financial charts, from processing historical documents to cataloging diverse product lines, the applications are vast and varied. + +[Pixtral](https://mistral.ai/news/pixtral-12b/), developed by Mistral AI, is a powerful vision model capable of understanding and describing images with remarkable accuracy. By leveraging Pixtral, we can automate the process of extracting structured information from a wide range of visual inputs, including photographs, scanned documents, graphs, and more. + +This tutorial will guide you through the process of using the Pixtral vision model to analyze images and automatically generate structured outputs. We'll use Python to interact with the model and structure our data, making it easy to integrate this solution into your existing workflows. While we'll use a product catalog as an example, the techniques demonstrated here can be adapted to various use cases across different industries. + + + +- A Python environment (version 3.7 or higher) +- An API key from Scaleway's Identity and Access Management (IAM) +- Access to a Scaleway [Managed Inference]() endpoint with Pixtral deployed or to Scaleway [Generative APIs]() service +- The `openai` and `pydantic` Python libraries installed + +## Setting up the environment + +Before we dive into using Pixtral, let's set up our Python environment and install the necessary libraries. + +1. Create a new directory for your project: + ``` + mkdir pixtral-image-processor + cd pixtral-image-processor + ``` + +2. Create a virtual environment and activate it: + ``` + python3 -m venv venv + source venv/bin/activate # On Windows, use `venv\Scripts\activate` + ``` + +3. Install the required libraries: + ``` + pip install openai pydantic + ``` + +## Defining the data model + +We'll start by defining our data model using Pydantic. This will ensure that our structured output has a consistent format and that all required fields are present. + +Create a new file called `models.py` and add the following code: + +```python +from pydantic import BaseModel, Field +from typing import List, Optional +from datetime import date + +class Dimension(BaseModel): + length: float + width: float + height: float + unit: str = Field(..., pattern="(cm|in|mm)") + +class Price(BaseModel): + amount: float + currency: str = Field(..., pattern="[A-Z]{3}") + +class Review(BaseModel): + user_id: str + rating: int = Field(..., ge=1, le=5) + comment: Optional[str] = None + date: date + +class Product(BaseModel): + id: str + name: str + description: str + category: str + subcategory: Optional[str] = None + brand: str + sku: str + price: Price + dimensions: Dimension + weight: float + weight_unit: str = Field(..., pattern="(kg|g|lb|oz)") + in_stock: bool + available_colors: List[str] = Field(..., min_items=1) + features: List[str] = Field(..., min_items=1) + image_urls: List[str] = Field(..., min_items=1) + reviews: List[Review] = Field(default_factory=list) + average_rating: Optional[float] = None + +class ProductCatalog(BaseModel): + products: List[Product] + total_products: int + last_updated: date +``` + +This model defines the structure for our product catalog, which we'll use as an example of structured output from image processing. + +## Setting up the Pixtral client + +Next, we'll set up the client to interact with the Pixtral model. Create a new file called `pixtral_client.py` and add the following code: + +```python +from openai import OpenAI +import os + +MODEL = "pixtral-12b-2409" +API_KEY = os.environ.get("SCALEWAY_API_KEY") +BASE_URL = os.environ.get("SCALEWAY_INFERENCE_ENDPOINT_URL") +# use https://api.scaleway.ai/v1 for Scaleway Generative APIs + +client = OpenAI( + base_url=BASE_URL, + api_key=API_KEY +) + +def get_pixtral_client(): + return client +``` + +Make sure to set the `SCALEWAY_API_KEY` and `SCALEWAY_INFERENCE_ENDPOINT_URL` environment variables with your actual API key from Scaleway's IAM and the appropriate endpoint URL for Scaleway Managed Inference or Generative APIs service. + +## Creating the image processor + +Now, let's create the main script that will use Pixtral to analyze images and generate our structured output. Create a file called `process_images.py` and add the following code: + +```python +import json +from datetime import date +from pixtral_client import get_pixtral_client +from models import ProductCatalog + +def process_images(image_urls): + client = get_pixtral_client() + + prompt = """ + Extract detailed information from the provided images. Create an entry for each image. + Include the following details for each item: + - A descriptive name and detailed description + - Appropriate category and subcategory + - Realistic dimensions, weight, and pricing (if applicable) + - At least 3 key features or characteristics + - Any visible attributes (e.g., colors, materials) + - Generate 2 hypothetical reviews or interpretations + + Ensure all information is consistent with what can be seen or reasonably inferred from the images. + """ + + messages = [ + { + "role": "system", + "content": "You are a helpful assistant. Only reply in JSON.", + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": prompt + }, + *[{"type": "image_url", "image_url": {"url": url}} for url in image_urls] + ], + }, + ] + + try: + response = client.chat.completions.create( + messages=messages, + model=MODEL, + response_format={ + "type": "json_schema", + "json_schema": { + "strict": True, + "name": "Processed Image Data", + "schema": ProductCatalog.model_json_schema() + } + }, + ) + + processed_data = response.choices[0].message.parsed + structured_output = ProductCatalog(**processed_data) + structured_output.last_updated = date.today() + structured_output.total_products = len(structured_output.products) + + return structured_output + + except Exception as e: + print(f"Error processing images: {e}") + return None + +def save_output_to_json(output, filename): + with open(filename, 'w') as f: + json.dump(output.model_dump(), f, indent=2, default=str) + +if __name__ == "__main__": + image_urls = [ + "https://picsum.photos/id/26/800/600", # Sample image 1 + "https://picsum.photos/id/3/800/600" # Sample image 2 + ] + + processed_output = process_images(image_urls) + + if processed_output: + save_output_to_json(processed_output, "processed_image_data.json") + print(f"Image processing complete. Structured data for {processed_output.total_products} items generated.") + else: + print("Failed to process images.") +``` + +This script does the following: +1. Imports the necessary modules and models. +2. Defines a function to process images using the Pixtral model. +3. Creates a prompt that instructs the model on how to analyze the images and what information to extract. +4. Sends the images and prompt to the Pixtral model and receives the generated structured data. +5. Validates the received data against our Pydantic models. +6. Saves the generated structured output to a JSON file. + +## Running the image processor + +To use the image processor: + +1. Set the environment variables for your Scaleway API key and inference endpoint URL: + ``` + export SCALEWAY_API_KEY="your_api_key_here" + export SCALEWAY_INFERENCE_ENDPOINT_URL="your_endpoint_url_here" + ``` + +2. Run the script: + ``` + python process_images.py + ``` + +The script will process the sample images and generate a `processed_image_data.json` file containing the extracted structured information. + +## Customizing the image processor + +You can easily customize the image processor for your specific needs: + +1. Modify the `prompt` in `process_images.py` to extract different or additional information from the images. +2. Update the `models.py` file to change the structure of your output data to fit your specific use case. +3. Add error handling and logging to make the script more robust for production use. + +## Conclusion + +In this tutorial, we've explored how to leverage Mistral's Pixtral vision model to process images and generate structured outputs following a strict and complex JSON schema. This approach can be applied to a wide range of industries and use cases, from cataloging products to analyzing medical images, from interpreting financial charts to processing historical documents. + +By combining the power of AI vision models with structured data validation, we've created a flexible and extensible solution that can be adapted to various image processing needs. + +Remember to always verify the AI-generated information for accuracy before using it in critical applications or decision-making processes. + From 5957f06d6977045cbb8dce0a419c17ee0496061b Mon Sep 17 00:00:00 2001 From: Thibault Genaitay Date: Thu, 10 Oct 2024 08:58:33 +0200 Subject: [PATCH 05/13] feat(genapi): add picture --- .../assets/Pixtral-Structured-Outputs.webp | Bin 0 -> 55634 bytes .../index.mdx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tutorials/processing-images-structured-outputs-pixtral/assets/Pixtral-Structured-Outputs.webp diff --git a/tutorials/processing-images-structured-outputs-pixtral/assets/Pixtral-Structured-Outputs.webp b/tutorials/processing-images-structured-outputs-pixtral/assets/Pixtral-Structured-Outputs.webp new file mode 100644 index 0000000000000000000000000000000000000000..652449df0b8f9da4e4c55b33627b64404e16be1b GIT binary patch literal 55634 zcmV((K;XYpNk&FW*#H1nMM6+kP&il$0000C0002z0{}7t09H^qAXL`?0I&lCodGK7 z12Y0XQ6`W^BcdUsq_jD3kO~P+!Ckk5%AfP|A+h|L4}Q=Gmi()K`_ukMm`7iq1Rm8o z;5V?(j)gEbc=z<)%-L-Eas1!pzc0LE;@`3UxANEgf9+?`9~SoqZa`r}n?_ z&-VZHyz%+}_22kD;669}v-N-WpWT1Sf7kyz|NsC0;9p0-b^hRg!+)E9z5gTq|NiIu zfB*mg{<*zo{^bAQ{f+no`uG29{SWW|&Y%0A+&^>w-+e$ooPUS^vH9uq@Bd5KZ}*?r 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 literal 0 HcmV?d00001 diff --git a/tutorials/processing-images-structured-outputs-pixtral/index.mdx b/tutorials/processing-images-structured-outputs-pixtral/index.mdx index cf28b19f3b..1c1156e49b 100644 --- a/tutorials/processing-images-structured-outputs-pixtral/index.mdx +++ b/tutorials/processing-images-structured-outputs-pixtral/index.mdx @@ -9,7 +9,7 @@ tags: AI vision-model image-processing Pixtral Mistral structured-data categories: - artificial-intelligence - computer-vision -hero: assets/scaleway-pixtral-hero.webp +hero: assets/Pixtral-Structured-Outputs.webp dates: validation: 2024-10-09 posted: 2024-10-09 From a38a1231c314895410e948d2c8c6c281fdb63181 Mon Sep 17 00:00:00 2001 From: Thibault Genaitay Date: Thu, 10 Oct 2024 09:01:09 +0200 Subject: [PATCH 06/13] feat(genapi): added links --- .../processing-images-structured-outputs-pixtral/index.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorials/processing-images-structured-outputs-pixtral/index.mdx b/tutorials/processing-images-structured-outputs-pixtral/index.mdx index 1c1156e49b..9bba140cf9 100644 --- a/tutorials/processing-images-structured-outputs-pixtral/index.mdx +++ b/tutorials/processing-images-structured-outputs-pixtral/index.mdx @@ -24,8 +24,8 @@ This tutorial will guide you through the process of using the Pixtral vision mod - A Python environment (version 3.7 or higher) -- An API key from Scaleway's Identity and Access Management (IAM) -- Access to a Scaleway [Managed Inference]() endpoint with Pixtral deployed or to Scaleway [Generative APIs]() service +- An API key from Scaleway [Identity and Access Management](https://www.scaleway.com/en/docs/identity-and-access-management/iam/) +- Access to a Scaleway [Managed Inference](/ai-data/managed-inference/reference-content/pixtral-12b-2409/) endpoint with Pixtral deployed or to Scaleway [Generative APIs](/ai-data/generative-apis/quickstart/) service - The `openai` and `pydantic` Python libraries installed ## Setting up the environment From 24dac4eda2577c3c3a580acb63e04c8b406327da Mon Sep 17 00:00:00 2001 From: Benedikt Rollik Date: Thu, 10 Oct 2024 10:24:57 +0200 Subject: [PATCH 07/13] Apply suggestions from code review Co-authored-by: SamyOubouaziz --- tutorials/how-to-implement-rag-generativeapis/index.mdx | 5 ++++- .../processing-images-structured-outputs-pixtral/index.mdx | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tutorials/how-to-implement-rag-generativeapis/index.mdx b/tutorials/how-to-implement-rag-generativeapis/index.mdx index d66c576808..cd89292c58 100644 --- a/tutorials/how-to-implement-rag-generativeapis/index.mdx +++ b/tutorials/how-to-implement-rag-generativeapis/index.mdx @@ -5,13 +5,16 @@ meta: content: h1: Implementing Retrieval-Augmented Generation (RAG) with LangChain and Scaleway Generative APIs tags: inference API postgresql pgvector object storage RAG langchain AI LLMs embeddings +dates: + validation: 2024-10-10 + posted: 2018-10-10 categories: - inference --- Retrieval-Augmented Generation (RAG) enhances language models by incorporating relevant information from your own datasets. This hybrid approach improves both the accuracy and contextual relevance of the model's outputs, making it ideal for advanced AI applications. -In this tutorial, you will learn how to implement RAG using LangChain, a leading framework for developing powerful language model applications. We will integrate LangChain with **Scaleway’s Generative APIs**, **Scaleway’s PostgreSQL Managed Database** (utilizing pgvector for vector storage), and **Scaleway’s Object Storage** to ensure seamless data management and efficient integration. +In this tutorial, you will learn how to implement RAG using LangChain, a leading framework for developing powerful language model applications. We will integrate LangChain with **Scaleway’s Generative APIs**, **Scaleway’s PostgreSQL Managed Database** (utilizing `pgvector` for vector storage), and **Scaleway’s Object Storage** to ensure seamless data management and efficient integration. ## What you will learn - How to embed text using ***Scaleway Generative APIs*** diff --git a/tutorials/processing-images-structured-outputs-pixtral/index.mdx b/tutorials/processing-images-structured-outputs-pixtral/index.mdx index 9bba140cf9..fcd2b8d0e1 100644 --- a/tutorials/processing-images-structured-outputs-pixtral/index.mdx +++ b/tutorials/processing-images-structured-outputs-pixtral/index.mdx @@ -23,6 +23,7 @@ This tutorial will guide you through the process of using the Pixtral vision mod +- A Scaleway account logged into the [console](https://console.scaleway.com) - A Python environment (version 3.7 or higher) - An API key from Scaleway [Identity and Access Management](https://www.scaleway.com/en/docs/identity-and-access-management/iam/) - Access to a Scaleway [Managed Inference](/ai-data/managed-inference/reference-content/pixtral-12b-2409/) endpoint with Pixtral deployed or to Scaleway [Generative APIs](/ai-data/generative-apis/quickstart/) service @@ -253,5 +254,7 @@ In this tutorial, we've explored how to leverage Mistral's Pixtral vision model By combining the power of AI vision models with structured data validation, we've created a flexible and extensible solution that can be adapted to various image processing needs. -Remember to always verify the AI-generated information for accuracy before using it in critical applications or decision-making processes. + + Remember to always verify the AI-generated information for accuracy before using it in critical applications or decision-making processes. + From 7822bb94cd2d55370f3455b9f2f10f13749b4b08 Mon Sep 17 00:00:00 2001 From: Benedikt Rollik Date: Thu, 10 Oct 2024 13:48:11 +0200 Subject: [PATCH 08/13] Apply suggestions from code review Co-authored-by: SamyOubouaziz --- tutorials/how-to-implement-rag-generativeapis/index.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tutorials/how-to-implement-rag-generativeapis/index.mdx b/tutorials/how-to-implement-rag-generativeapis/index.mdx index cd89292c58..ce13b4f7b9 100644 --- a/tutorials/how-to-implement-rag-generativeapis/index.mdx +++ b/tutorials/how-to-implement-rag-generativeapis/index.mdx @@ -150,9 +150,9 @@ embeddings = OpenAIEmbeddings( - `model="sentence-t5-xxl"`: This defines the specific model being used for text embeddings. sentence-transformers/sentence-t5-xxl is a powerful model optimized for generating high-quality sentence embeddings, making it ideal for tasks like document retrieval in RAG systems. - `tiktoken_enabled=False`: This parameter disables the use of TikToken for tokenization within the embeddings process. -### Create a PGVector store +### Create a pgvector store -Configure the connection string for your PostgreSQL instance and create a PGVector store to store these embeddings. +Configure the connection string for your PostgreSQL instance and create a pgvector store to store these embeddings. ```python # rag.py @@ -163,9 +163,9 @@ vector_store = PGVector(connection=connection_string, embeddings=embeddings) ## Load and process documents -At this stage, you need to have proprietary data (e.g PDF, CSV) stored in your Scaleway Object storage bucket. +At this stage, you need to have proprietary data (e.g., PDF, CSV) stored in your Scaleway Object storage bucket. -Below we will use Langchain's [`S3FileLoader`](https://api.python.langchain.com/en/latest/document_loaders/langchain_community.document_loaders.s3_file.S3FileLoader.html) to load documents and split them into chunks. +Below we will use LangChain's [`S3FileLoader`](https://api.python.langchain.com/en/latest/document_loaders/langchain_community.document_loaders.s3_file.S3FileLoader.html) to load documents, and split them into chunks. Then, we will embed and store them in your PostgreSQL database. ### Import required modules From 74366266379611235f3c84c1d68d8d54e1e27360 Mon Sep 17 00:00:00 2001 From: Benedikt Rollik Date: Thu, 10 Oct 2024 14:03:20 +0200 Subject: [PATCH 09/13] Apply suggestions from code review Co-authored-by: SamyOubouaziz --- .../index.mdx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tutorials/how-to-implement-rag-generativeapis/index.mdx b/tutorials/how-to-implement-rag-generativeapis/index.mdx index ce13b4f7b9..867b91e386 100644 --- a/tutorials/how-to-implement-rag-generativeapis/index.mdx +++ b/tutorials/how-to-implement-rag-generativeapis/index.mdx @@ -194,11 +194,11 @@ paginator = client_s3.get_paginator('list_objects_v2') page_iterator = paginator.paginate(Bucket=os.getenv("SCW_BUCKET_NAME", "")) ``` -In this code sample we: -- Set up a Boto3 session: We initialize a Boto3 session, which is the AWS SDK for Python, fully compatible with Scaleway Object Storage. This session manages configuration, including credentials and settings, that Boto3 uses for API requests. -- Create an S3 client: We establish an S3 client to interact with the Scaleway Object Storage service. -- Set up pagination for listing objects: We prepare pagination to handle potentially large lists of objects efficiently. -- Iterate through the bucket: This initiates the pagination process, allowing us to list all objects within the specified Scaleway Object bucket seamlessly. +In this code sample, we: +- Set up a Boto3 session: we initialize a Boto3 session, which is the AWS SDK for Python, fully compatible with Scaleway Object Storage. This session manages configuration, including credentials and settings, that Boto3 uses for API requests. +- Create an S3 client: we establish an S3 client to interact with the Scaleway Object Storage service. +- Set up pagination for listing objects: we prepare pagination to handle potentially large lists of objects efficiently. +- Iterate through the bucket: this initiates the pagination process, allowing us to list all objects within the specified Scaleway Object bucket seamlessly. ### Iterate through metadata @@ -233,15 +233,15 @@ for page in page_iterator: conn.commit() ``` -- S3FileLoader: The S3FileLoader loads each file individually from your ***Scaleway Object Storage bucket*** using the file's object_key (extracted from the file's metadata). It ensures that only the specific file is loaded from the bucket, minimizing the amount of data being retrieved at any given time. -- RecursiveCharacterTextSplitter: The RecursiveCharacterTextSplitter breaks each document into smaller chunks of text. This is crucial because embeddings models, like those used in Retrieval-Augmented Generation (RAG), typically have a limited context window (the number of tokens they can process at once). -- Embedding the chunks: For each document, the text is split into smaller chunks using the text splitter, and an embedding is generated for each chunk using the embeddings.embed_query(chunk) function. This function transforms each chunk into a vector representation that can later be used for similarity search. -- Embedding storage: After generating the embeddings for each chunk, they are stored in a vector database (e.g., PostgreSQL with pgvector) using the vector_store.add_embeddings(embedding, chunk) method. Each embedding is stored alongside its corresponding text chunk, enabling retrieval during a query. -- Avoiding redundant processing: The script checks the object_loaded table in PostgreSQL to see if a document has already been processed (i.e., the object_key exists in the table). If it has, the file is skipped, avoiding redundant downloads, vectorization, and database inserts. This ensures that only new or modified documents are processed, reducing the system's computational load and saving both time and resources. +- S3FileLoader: the S3FileLoader loads each file individually from your **Scaleway Object Storage bucket** using the file's `object_key` (extracted from the file's metadata). It ensures that only the specific file is loaded from the bucket, minimizing the amount of data being retrieved at any given time. +- RecursiveCharacterTextSplitter: the RecursiveCharacterTextSplitter breaks each document into smaller chunks of text. This is crucial, because embeddings models, like those used in Retrieval-Augmented Generation (RAG), typically have a limited context window (the number of tokens they can process at once). +- Embedding the chunks: for each document, the text is split into smaller chunks using the text splitter, and an embedding is generated for each chunk using the embeddings.embed_query(chunk) function. This function transforms each chunk into a vector representation that can later be used for similarity search. +- Embedding storage: after generating the embeddings for each chunk, they are stored in a vector database (e.g., PostgreSQL with pgvector) using the vector_store.add_embeddings(embedding, chunk) method. Each embedding is stored alongside its corresponding text chunk, enabling retrieval during a query. +- Avoiding redundant processing: the script checks the `object_loaded` table in PostgreSQL to see if a document has already been processed (i.e., the `object_key` exists in the table). If it has, the file is skipped, avoiding redundant downloads, vectorization, and database inserts. This ensures that only new or modified documents are processed, reducing the system's computational load and saving both time and resources. #### Why 500 characters? -The chunk size of 500 characters is chosen to fit comfortably within the context size limit if the embedding model used in this tutorial. By keeping chunks small, we avoid exceeding the model’s context window, which could lead to truncated embeddings or poor performance during inference. +The chunk size of 500 characters is chosen to fit comfortably within the context size limit of the embedding model used in this tutorial. By keeping chunks small, we avoid exceeding the model’s context window, which could lead to truncated embeddings or poor performance during inference. #### Why store both chunk and embedding? @@ -262,7 +262,7 @@ from langchain_openai import ChatOpenAI ``` -### Setup LLM for querying +### Set up LLM for querying Now, set up the RAG system to handle queries From 4d67584a86889b925128c9feba0cbdde1537c51b Mon Sep 17 00:00:00 2001 From: Benedikt Rollik Date: Thu, 10 Oct 2024 14:05:20 +0200 Subject: [PATCH 10/13] Update tutorials/how-to-implement-rag-generativeapis/index.mdx Co-authored-by: SamyOubouaziz --- .../how-to-implement-rag-generativeapis/index.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tutorials/how-to-implement-rag-generativeapis/index.mdx b/tutorials/how-to-implement-rag-generativeapis/index.mdx index 867b91e386..f7a57e41d4 100644 --- a/tutorials/how-to-implement-rag-generativeapis/index.mdx +++ b/tutorials/how-to-implement-rag-generativeapis/index.mdx @@ -290,15 +290,15 @@ for r in rag_chain.stream("Your question"): print(r, end="", flush=True) time.sleep(0.1) ``` -- LLM initialization: We initialize the ChatOpenAI instance using the endpoint and API key from the environment variables, along with the specified model name. +- LLM initialization: we initialize the ChatOpenAI instance using the endpoint and API key from the environment variables, along with the specified model name. -- Prompt setup: The prompt is pulled from the hub using a pre-defined template, ensuring consistent query formatting. +- Prompt setup: the prompt is pulled from the hub using a predefined template, ensuring consistent query formatting. -- Retriever configuration: We set up the retriever to access the vector store, allowing the RAG system to retrieve relevant information based on the query. +- Retriever configuration: we set up the retriever to access the vector store, allowing the RAG system to retrieve relevant information based on the query. -- RAG chain construction: We create the RAG chain, which connects the retriever, prompt, LLM, and output parser in a streamlined workflow. +- RAG chain construction: we create the RAG chain, which connects the retriever, prompt, LLM, and output parser in a streamlined workflow. -- Query execution: Finally, we stream the output of the RAG chain for a specified question, printing each response with a slight delay for better readability. +- Query execution: finally, we stream the output of the RAG chain for a specified question, printing each response with a slight delay for better readability. ### Query the RAG system with your own prompt template From 2c3d0a96ff8d3f51c963f96a44443c435e15b73b Mon Sep 17 00:00:00 2001 From: Benedikt Rollik Date: Thu, 10 Oct 2024 14:05:27 +0200 Subject: [PATCH 11/13] Update tutorials/how-to-implement-rag-generativeapis/index.mdx Co-authored-by: SamyOubouaziz --- tutorials/how-to-implement-rag-generativeapis/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/how-to-implement-rag-generativeapis/index.mdx b/tutorials/how-to-implement-rag-generativeapis/index.mdx index f7a57e41d4..41eeaed570 100644 --- a/tutorials/how-to-implement-rag-generativeapis/index.mdx +++ b/tutorials/how-to-implement-rag-generativeapis/index.mdx @@ -329,7 +329,7 @@ for r in custom_rag_chain.stream({"question":"your question", "context": context time.sleep(0.1) ``` -- Prompt template: The prompt template is meticulously crafted to direct the model's responses. It clearly instructs the model on how to leverage the provided context and emphasizes the importance of honesty in cases where it lacks information. +- Prompt template: the prompt template is meticulously crafted to direct the model's responses. It clearly instructs the model on how to leverage the provided context and emphasizes the importance of honesty in cases where it lacks information. To make the responses more engaging, consider adding a light-hearted conclusion or a personalized touch. For example, you might modify the closing line to say, "Thank you for asking! I'm here to help with anything else you need!" Retrieving context: - The retriever.invoke(new_message) method fetches relevant information from your vector store based on the user’s query. It's essential that this step retrieves high-quality context to ensure that the model's responses are accurate and helpful. From c2067aa0302d3f66193a5e42145b1dde3630dcb2 Mon Sep 17 00:00:00 2001 From: Benedikt Rollik Date: Thu, 10 Oct 2024 15:03:41 +0200 Subject: [PATCH 12/13] Apply suggestions from code review Co-authored-by: SamyOubouaziz --- .../how-to-implement-rag-generativeapis/index.mdx | 6 +++--- .../index.mdx | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tutorials/how-to-implement-rag-generativeapis/index.mdx b/tutorials/how-to-implement-rag-generativeapis/index.mdx index 41eeaed570..07e3b1d6cb 100644 --- a/tutorials/how-to-implement-rag-generativeapis/index.mdx +++ b/tutorials/how-to-implement-rag-generativeapis/index.mdx @@ -342,9 +342,9 @@ Streaming responses: You can customize the streaming behavior further, such as implementing progress indicators or more sophisticated UI elements for applications. #### Example use cases -- Customer support: Use a custom prompt to answer customer queries effectively, making the interactions feel more personalized and engaging. -- Research assistance: Tailor prompts to provide concise summaries or detailed explanations on specific topics, enhancing your research capabilities. -- Content generation: Personalize prompts for creative writing, generating responses that align with specific themes or tones. +- Customer support: use a custom prompt to answer customer queries effectively, making the interactions feel more personalized and engaging. +- Research assistance: tailor prompts to provide concise summaries or detailed explanations on specific topics, enhancing your research capabilities. +- Content generation: personalize prompts for creative writing, generating responses that align with specific themes or tones. ## Conclusion diff --git a/tutorials/processing-images-structured-outputs-pixtral/index.mdx b/tutorials/processing-images-structured-outputs-pixtral/index.mdx index fcd2b8d0e1..66cbf4306c 100644 --- a/tutorials/processing-images-structured-outputs-pixtral/index.mdx +++ b/tutorials/processing-images-structured-outputs-pixtral/index.mdx @@ -1,9 +1,9 @@ --- meta: - title: Processing Images and Getting Structured Outputs with Pixtral Vision Model + title: Processing images and getting structured outputs with Pixtral vision model description: Learn how to use Mistral's Pixtral vision model to automatically generate a product catalog from images. content: - h1: Processing Images and Getting Structured Outputs with Pixtral Vision Model + h1: Processing images and getting structured outputs with Pixtral vision model paragraph: Discover how to leverage Mistral's Pixtral vision model to analyze images and generate comprehensive structured outputs. tags: AI vision-model image-processing Pixtral Mistral structured-data categories: @@ -52,7 +52,7 @@ Before we dive into using Pixtral, let's set up our Python environment and insta ## Defining the data model -We'll start by defining our data model using Pydantic. This will ensure that our structured output has a consistent format and that all required fields are present. +We'll start by defining our data model using `pydantic`. This will ensure that our structured output has a consistent format and that all required fields are present. Create a new file called `models.py` and add the following code: @@ -126,7 +126,7 @@ def get_pixtral_client(): return client ``` -Make sure to set the `SCALEWAY_API_KEY` and `SCALEWAY_INFERENCE_ENDPOINT_URL` environment variables with your actual API key from Scaleway's IAM and the appropriate endpoint URL for Scaleway Managed Inference or Generative APIs service. +Make sure to set the `SCALEWAY_API_KEY` and `SCALEWAY_INFERENCE_ENDPOINT_URL` environment variables with your actual API key from Scaleway IAM, and the appropriate endpoint URL for Scaleway Managed Inference or Generative APIs service. ## Creating the image processor @@ -220,7 +220,7 @@ This script does the following: 2. Defines a function to process images using the Pixtral model. 3. Creates a prompt that instructs the model on how to analyze the images and what information to extract. 4. Sends the images and prompt to the Pixtral model and receives the generated structured data. -5. Validates the received data against our Pydantic models. +5. Validates the received data against our `pydantic` models. 6. Saves the generated structured output to a JSON file. ## Running the image processor From d3aa47b38f6aea858f79bd3d4361c195415be15f Mon Sep 17 00:00:00 2001 From: Benedikt Rollik Date: Thu, 10 Oct 2024 15:05:54 +0200 Subject: [PATCH 13/13] docs(fix): categories --- tutorials/how-to-implement-rag-generativeapis/index.mdx | 2 +- tutorials/how-to-implement-rag/index.mdx | 2 +- .../processing-images-structured-outputs-pixtral/index.mdx | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tutorials/how-to-implement-rag-generativeapis/index.mdx b/tutorials/how-to-implement-rag-generativeapis/index.mdx index 07e3b1d6cb..bbcd7e9b0d 100644 --- a/tutorials/how-to-implement-rag-generativeapis/index.mdx +++ b/tutorials/how-to-implement-rag-generativeapis/index.mdx @@ -9,7 +9,7 @@ dates: validation: 2024-10-10 posted: 2018-10-10 categories: - - inference + - managed-inference --- Retrieval-Augmented Generation (RAG) enhances language models by incorporating relevant information from your own datasets. This hybrid approach improves both the accuracy and contextual relevance of the model's outputs, making it ideal for advanced AI applications. diff --git a/tutorials/how-to-implement-rag/index.mdx b/tutorials/how-to-implement-rag/index.mdx index 95f1c5fca9..3486331220 100644 --- a/tutorials/how-to-implement-rag/index.mdx +++ b/tutorials/how-to-implement-rag/index.mdx @@ -6,7 +6,7 @@ content: h1: Implementing Retrieval-Augmented Generation (RAG) with LangChain and Scaleway Managed Inference tags: inference managed postgresql pgvector object storage RAG langchain machine learning AI language models categories: - - inference + - managed-inference --- Retrieval-Augmented Generation (RAG) supercharges language models by enabling real-time retrieval of relevant information from external datasets. This hybrid approach boosts both the accuracy and contextual relevance of model outputs, making it essential for advanced AI applications. diff --git a/tutorials/processing-images-structured-outputs-pixtral/index.mdx b/tutorials/processing-images-structured-outputs-pixtral/index.mdx index 66cbf4306c..154498886e 100644 --- a/tutorials/processing-images-structured-outputs-pixtral/index.mdx +++ b/tutorials/processing-images-structured-outputs-pixtral/index.mdx @@ -7,8 +7,7 @@ content: paragraph: Discover how to leverage Mistral's Pixtral vision model to analyze images and generate comprehensive structured outputs. tags: AI vision-model image-processing Pixtral Mistral structured-data categories: - - artificial-intelligence - - computer-vision + - managed-inference hero: assets/Pixtral-Structured-Outputs.webp dates: validation: 2024-10-09