File size: 7,404 Bytes
cf3dd65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import click
import fitz  # PyMuPDF
import numpy as np
import itertools as it
import operator as op
import requests
import tiktoken 
from typing import List, Tuple
from dotenv import load_dotenv
from sentence_transformers import SentenceTransformer
from langchain_community.chat_models import ChatOllama
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

load_dotenv()
protocol_buffers_python_implementation = os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]
CACHE_FOLDER = os.environ["CACHE_FOLDER"]
CACHE_FOLDER = "./cache"  # Chemin du dossier cache

# Fonctions existantes pour convertir les PDFs et diviser les pages en chunks
def convert_pdf_to_text(pdf_data: str) -> List[str]:
    document = fitz.open(pdf_data)
    accumulator: List[str] = []
    for page_num in range(len(document)):
        page = document[page_num]
        text = page.get_text()
        accumulator.append(text)
    document.close()
    return accumulator

def split_pages_into_chunks(pages: List[str], chunk_size: int, tokenizer) -> List[str]:
    page_tokens: List[List[int]] = [tokenizer.encode(page) for page in pages]
    document_tokens = list(it.chain(*page_tokens))

    nb_tokens = len(document_tokens)
    nb_partitions = round(nb_tokens / chunk_size)

    accumulator: List[str] = []

    for chunck_tokens in np.array_split(document_tokens, nb_partitions):
        paragraph = tokenizer.decode(chunck_tokens)
        accumulator.append(paragraph)

    return accumulator

def vectorize(chunks: List[str], transformer: SentenceTransformer, device: str = 'cpu') -> List[Tuple[str, np.ndarray]]:
    embeddings = transformer.encode(
        sentences=chunks,
        batch_size=32,
        device=device
        ,show_progress_bar=True,
    )

    return list(zip(chunks, embeddings))

def find_candidates(query_embedding: np.ndarray, chunks: List[str], corpus_embeddings: np.ndarray, top_k: int = 5) -> List[str]:
    dot_product = query_embedding @ corpus_embeddings.T
    norms = np.linalg.norm(query_embedding) * np.linalg.norm(corpus_embeddings, axis=1)

    weighted_scores = dot_product / norms  # cosine similarity

    zipped_chunks_scores = list(zip(chunks, weighted_scores))
    sorted_chunks_scores = sorted(zipped_chunks_scores, key=op.itemgetter(1), reverse=True)
    selected_candidates = sorted_chunks_scores[:top_k]

    return list(map(op.itemgetter(0), selected_candidates))


def generate_prompt(question, chunks, corpus_embeddings, embedding_model, llm_model, top_k, document_type: str = "ce pdf"):
    query_embedding = embedding_model.encode([question])[0]
    candidates = find_candidates(query_embedding=query_embedding, 
                                 chunks=chunks, 
                                 corpus_embeddings=corpus_embeddings, 
                                 top_k=top_k)
    context = "\n".join(candidates)
    prompt = f"""
     ROLE: Tu es un assistant QA. Tu as été crée par Papa Séga de Orange INNVO. 
           Ton rôle est d'aider les utilisateurs à trouver la bonne réponse.
     FONCTIONNEMENT: 
            Voici un ensemble de documents dans {document_type}: {context}.
            Tu dois analyser ces documents pour répondre à la question de l'utilisateur. 
            Tu dois répondre aux utilisateurs uniquement en français.
            Tu ne dois pas répondre en anglais.
            Vérifie bien que la question est en rapport avec ces documents :
                -SI OUI ALORS :
                    - construit une réponse.
                    - tu peux reformuler la réponse
                - SI NON ALORS:
                    - Si la question est en rapport avec ton fonctionnement alors:
                         - rappelle-lui que tu es juste un modèle de QA et ne réponds pas à la question.
                    Si Non Alors: 
                            - il ne faut jamais répondre à cette question qui a été posée par l'utilisateur ! 
        RAPPEL :
            Ton objectif est d'aider l'utilisateur à trouver les réponses pertinentes sur ces questions !

    Question de l'utilisateur : {question}
    SI la question posée est hors contexte,
      - ALORS informe l'utilisateur que la question est hors contexte et que tu ne pourras lui donner une réponse.

    Attention 
      - Tu dois être courtois avec les utilisateurs 
      - Tu n'as pas le droit de faire des hallucinations.
    
    Réponds à la question en te basant UNIQUEMENT sur le contexte suivant :\n{context}\nQuestion : {question}

    """
    
    chain_prompt = ChatPromptTemplate.from_template(prompt)
    chain = chain_prompt | llm_model | StrOutputParser()

    response = chain.invoke({"topic": question})

    return response

@click.command()
@click.option('--pdf_url', required=True, help='URL du fichier PDF à télécharger')
@click.option('--pdf_path', default='document.pdf', help='Chemin local pour enregistrer le fichier PDF téléchargé')
@click.option('--embedding_model', required=True, help="Modèle d'embedding à utiliser")
@click.option('--llm_model', required=True, help='Modèle LLM à utiliser')
@click.option('--top_k', type=int, default=5, help='Nombre de candidats pour la recherche de similarité')
def main(pdf_url, pdf_path, embedding_model, llm_model, top_k):
    os.makedirs(CACHE_FOLDER, exist_ok=True)
    
    response = requests.get(pdf_url)
    with open(pdf_path, 'wb') as f:
        f.write(response.content)

    pages = convert_pdf_to_text(pdf_path)
    print(
    """
    Je suis Llama3, de l'équipe DREAMS TEAM, votre assistant QA pour répondre à vos questions liés aux documents 🙂 !
    Déjà pour info, le nombre de pages de vote document est: """, len(pages)
        )
    tokenizer = tiktoken.get_encoding("cl100k_base")
    chunks = split_pages_into_chunks(pages, 128, tokenizer)
    
    embedding_model = SentenceTransformer(embedding_model)

    knowledge_base = vectorize(chunks, embedding_model)
    chunks, embeddings = list(zip(*knowledge_base))
    corpus_embeddings = np.vstack(embeddings)

    llm_model = ChatOllama(model=llm_model)
    print('📑 Voici le contenu de la première page du document 😎:\n', pages[0])

    keep_looping = True
    while keep_looping:
        try:    
            question = input("Entrez votre question ✍️  |   (ou tapez 'exit' pour quitter) ✨: ")
            if question.lower() == 'exit':
                break

            response = generate_prompt(question, chunks, corpus_embeddings, embedding_model, llm_model, top_k)
            print(response)

        except KeyboardInterrupt:
           print("\nFin de la session de chat 👋.")
           keep_looping = False

if __name__ == "__main__":
    main()


"""
export CACHE_FOLDER="./cache"  #"/home/pswia/veileAI/volume_models_cache"

python chatpdf.py --pdf_url "https://diomayepresident.org/wp-content/uploads/2024/03/Programme-Diomaye-President.pdf" --embedding_model "Sahajtomar/french_semantic" --llm_model "llama3"

python chatpdf.py --pdf_url "https://arxiv.org/pdf/2302.09928" --embedding_model "Sahajtomar/french_semantic" --llm_model "llama3"




python chatpdf.py --pdf_url "https://hellofuture.orange.com/app/uploads/2024/05/2024-Orange-white-paper-on-Mobile-Network-Technology-Evolutions-Beyond-2030.pdf" --embedding_model "Sahajtomar/french_semantic" --llm_model "llama3" --top_k 5


"""