File size: 5,711 Bytes
efb5688
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5327a62
 
efb5688
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5327a62
 
efb5688
 
 
 
 
 
5327a62
efb5688
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import os
from typing import List

from langchain_community.document_loaders import UnstructuredPDFLoader
from langchain_community.embeddings import HuggingFaceInferenceAPIEmbeddings
from langchain_community.llms.huggingface_endpoint import HuggingFaceEndpoint
from langchain.prompts import ChatPromptTemplate
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import RunnablePassthrough
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores.chroma import Chroma
from langchain_core.runnables.base import RunnableSequence
from langchain_core.vectorstores import VectorStoreRetriever

from dotenv import load_dotenv


load_dotenv()
HF_API_KEY = os.environ["HF_API_KEY"]


class MistralOutputParser(StrOutputParser):
    """OutputParser that parser llm result from Mistral API"""

    def parse(self, text: str) -> str:
        """
        Returns the input text with no changes.

        Args:
            text (str): text to parse

        Returns:
            str: parsed text
        """
        return text.split("[/INST]")[-1].strip()


def load_pdf(
    document_path: str,
    mode: str = "single",
    strategy: str = "fast",
    chunk_size: int = 500,
    chunk_overlap: int = 0,
) -> List[str]:
    """
    Load a pdf document and split it into chunks of text.

    Args:
        document_path (Path): path to the pdf document
        mode (str, optional): mode of the loader. Defaults to "single".
        strategy (str, optional): strategy of the loader. Defaults to "fast".
        chunk_size (int, optional): size of the chunks. Defaults to 500.
        chunk_overlap (int, optional): overlap of the chunks. Defaults to 0.

    Returns:
        List[str]: list of chunks of text
    """

    # Load the document
    loader = UnstructuredPDFLoader(
        document_path,
        mode=mode,
        strategy=strategy,
    )

    docs = loader.load()

    # Split the document into chunks of text
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size, chunk_overlap=chunk_overlap
    )
    all_splits = text_splitter.split_documents(docs)

    return all_splits


def store_vector(all_splits: List[str]) -> VectorStoreRetriever:
    """
    Store vector of each chunk of text.

    Args:
        all_splits (List[str]): list of chunks of text

    Returns:
        VectorStoreRetriever: retriever that can be used to retrieve the vector of a chunk of text
    """

    # Use the HuggingFace distilbert-base-uncased model to embed the text
    embeddings_model_url = (
        # "https://api-inference.huggingface.co/models/distilbert-base-uncased"
        "https://api-inference.huggingface.co/models/Salesforce/SFR-Embedding-Mistral"
    )

    embeddings = HuggingFaceInferenceAPIEmbeddings(
        endpoint_url=embeddings_model_url,
        api_key=HF_API_KEY,
    )

    # Store the embeddings of each chunk of text into ChromaDB
    vector_store = Chroma.from_documents(all_splits, embeddings)
    retriever = vector_store.as_retriever()

    return retriever


def generate_mistral_rag_prompt() -> ChatPromptTemplate:
    """
    Generate a prompt for Mistral API wiht RAG.

    Returns:
        ChatPromptTemplate: prompt for Mistral API
    """
    template = "<s>[INST] {context} {prompt} [/INST]"
    prompt_template = ChatPromptTemplate.from_template(template)
    return prompt_template


def generate_mistral_simple_prompt() -> ChatPromptTemplate:
    """
    Generate a simple prompt for Mistral without RAG.

    Returns:
        ChatPromptTemplate: prompt for Mistral API
    """
    template = "[INST] {prompt} [/INST]"
    prompt_template = ChatPromptTemplate.from_template(template)
    return prompt_template


def generate_rag_chain(retriever: VectorStoreRetriever = None) -> RunnableSequence:
    """
    Generate a RAG chain with Mistral API and ChromaDB.

    Args:
        Retriever (VectorStoreRetriever): retriever that can be used to retrieve the vector of a chunk of text

    Returns:
        RunnableSequence: RAG chain
    """
    # Use the Mistral Free prototype API
    mistral_url = (
        # "https://api-inference.huggingface.co/models/mistralai/Mistral-7B-Instruct-v0.2"
        "https://api-inference.huggingface.co/models/mistralai/Mixtral-8x22B-Instruct-v0.1"
    )

    model_endpoint = HuggingFaceEndpoint(
        endpoint_url=mistral_url,
        huggingfacehub_api_token=HF_API_KEY,
        task="text2text-generation",
        max_new_tokens=1024
    )

    # Use a custom output parser
    output_parser = MistralOutputParser()

    # If no retriever is provided, use a simple prompt
    if retriever is None:
        entry = {"prompt": RunnablePassthrough()}
        return entry | generate_mistral_simple_prompt() | model_endpoint | output_parser

    # If a retriever is provided, use a RAG prompt
    retrieval = {"context": retriever, "prompt": RunnablePassthrough()}

    return retrieval | generate_mistral_rag_prompt() | model_endpoint | output_parser


def load_multiple_pdf(document_paths: List[str]) -> List[str]:
    """
    Load multiple pdf documents and split them into chunks of text.

    Args:
        document_paths (List[str]): list of paths to the pdf documents

    Returns:
        List[str]: list of chunks of text
    """
    docs = []
    for document_path in document_paths:
        loader = UnstructuredPDFLoader(
            document_path,
            mode="single",
            strategy="fast",
        )
        docs.extend(loader.load())

    text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=25)
    all_splits = text_splitter.split_documents(docs)
    return all_splits