This commit is contained in:
Pierre-Edouard Portier 2024-01-10 15:06:42 +01:00
parent c41ffcd5b9
commit efaa188404
7 changed files with 2056 additions and 2843 deletions

2
.gitignore vendored
View File

@ -3,6 +3,7 @@
cera_hierarchy.html
chromadb/
chromadb_copie_20231230/
chromadb_copie_20240107
docs/
gradio_cached_examples/
index/
@ -12,3 +13,4 @@ index_cera2_distiluse/
__pycache__/
chromadbtest/
rag.log
embedding.log

77
debug2.py Normal file
View File

@ -0,0 +1,77 @@
from bs4 import BeautifulSoup
import base64
import re
from transformers import AutoTokenizer
import logging
import os
html_folder_path = '../scrapcera/htmls/'
txt_folder_path = '../scrapcera/docs/'
for html_filename in ['f6d921ced8.html']: # os.listdir(html_folder_path):
html_file_path = os.path.join(html_folder_path, html_filename)
txt_filename = re.sub(r'\.html', '.txt', html_filename)
txt_file_path = os.path.join(txt_folder_path, txt_filename)
with open(txt_file_path, 'r') as file:
txt_file_contents = file.read()
url = txt_file_contents.split('\n')[0]
if '?' in url: # URLs with a '?' corresponds to call to services and have no useful content
continue
if not url.startswith('https://www.caisse-epargne.fr/rhone-alpes/'):
continue
prefix = 'https://www.caisse-epargne.fr/'
suffix = url.replace(prefix, '')
tags = suffix.split('/')
tags = [tag for tag in tags if tag] # remove empty par
with open(html_file_path, 'r') as file:
html_file_contents = file.read()
soup = BeautifulSoup(html_file_contents, 'html.parser')
page_title_present = soup.find('section').find('h1')
if not page_title_present:
continue
page_title = page_title_present.get_text()
sections = soup.find_all(lambda tag: tag.name in ['section'] and 'key-informations' not in tag.get('class', []))
struct_page = {'title': page_title}
current_section = ''
for section in sections:
# breakpoint()
for wysiwyg_tag in section.find_all(class_="wysiwyg"):
# Check for a title within the wysiwyg container
internal_title = wysiwyg_tag.find(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) or wysiwyg_tag.find('p', class_='title')
# If no internal title, find the nearest title from previous siblings
if not internal_title:
# Find the nearest title from previous siblings
nearest_title = None
for previous in wysiwyg_tag.find_all_previous():
if previous.name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
nearest_title = previous.get_text().strip()
break
if previous.name == 'p' and 'title' in previous.get('class', []):
nearest_title = previous.get_text().strip()
break
if nearest_title:
nearest_title = re.sub(r'\(\d\)', '', nearest_title)
nearest_title = re.sub(r'^\d+\.\s*', '', nearest_title)
current_section = nearest_title
struct_page[current_section] = []
else:
continue
for child in wysiwyg_tag.find_all(['p', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']):
text = child.get_text().strip()
text = re.sub(r'\(\d\)', '', text)
if child.name.startswith('h') or (child.name == 'p' and 'title' in child.get('class', [])):
text = re.sub(r'^\d+\.\s*', '', text)
current_section = text
struct_page[current_section] = []
else: # <p> not of class title, or <li>
if 'is-style-mentions' not in child.get('class', []):
if current_section in struct_page:
struct_page[current_section].append(text)
print(struct_page)

238
embedding2.py Normal file
View File

@ -0,0 +1,238 @@
from transformers import AutoTokenizer
from sentence_transformers import SentenceTransformer
import os
import re
import copy
import chromadb
import logging
from bs4 import BeautifulSoup
logging.basicConfig(filename='embedding.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
class EmbeddingModel:
def __init__(self, model_name, chromadb_path, collection_name):
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = SentenceTransformer(model_name)
self.chroma_client = chromadb.PersistentClient(path=chromadb_path)
self.collection = self.chroma_client.get_or_create_collection(name=collection_name)
def token_length(self, text):
"""
Calculates the token length of a given text
Args:
text (str): The text to be tokenized.
Returns:
int: The number of tokens in the text.
This function takes a string, tokenizes the string, and returns the number of tokens.
"""
return len(self.tokenizer.encode(text, add_special_tokens=False))
def passage_str(self, paragraphs, title):
"""
Constructs a passage string from given paragraphs and a title.
Args:
paragraphs (list of str): A list of paragraphs.
title (str): The title of the passage.
Returns:
str: A passage string that combines the title and paragraphs.
This function takes a list of paragraphs and a title, and constructs a single string
with the title followed by the paragraphs, formatted for embedding.
"""
return f"passage: {title}\n" + '\n'.join(paragraphs)
def embed_folder(self, html_folder_path, txt_folder_path):
"""
Embeds all the .html files within a specified folder into a ChromaDB collection using a specified embedding model.
The txt folder is required to get the URL of the webpage. TODO: change this behavior in a future version.
Args:
html_folder_path (str): Path to the folder containing .html files.
txt_folder_path (str): Path to the folder containing .txt files.
Returns:
None
This function processes each .html file in the given folder, extracts the content, and uses `embed_page`
to embed the content into the specified ChromaDB collection.
"""
for html_filename in os.listdir(html_folder_path):
html_file_path = os.path.join(html_folder_path, html_filename)
txt_filename = re.sub(r'\.html', '.txt', html_filename)
txt_file_path = os.path.join(txt_folder_path, txt_filename)
with open(txt_file_path, 'r') as file:
txt_file_contents = file.read()
url = txt_file_contents.split('\n')[0]
if '?' in url: # URLs with a '?' corresponds to call to services and have no useful content
continue
if not url.startswith('https://www.caisse-epargne.fr/'):
continue
prefix = 'https://www.caisse-epargne.fr/'
suffix = url.replace(prefix, '')
tags = suffix.split('/')
tags = [tag for tag in tags if tag] # remove empty parts
with open(html_file_path, 'r') as file:
html_file_contents = file.read()
soup = BeautifulSoup(html_file_contents, 'html.parser')
first_section = soup.find('section')
if not first_section:
continue
page_title_present = first_section.find('h1')
if not page_title_present:
continue
page_title = page_title_present.get_text()
sections = soup.find_all(lambda tag: tag.name in ['section'] and 'key-informations' not in tag.get('class', []))
struct_page = {'title': page_title}
current_section = ''
for section in sections:
for wysiwyg_tag in section.find_all(class_="wysiwyg"):
# Check for a title within the wysiwyg container
internal_title = wysiwyg_tag.find(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) or wysiwyg_tag.find('p', class_='title')
# If no internal title, find the nearest title from previous tags
if not internal_title:
# Find the nearest title from previous tags
nearest_title = None
for previous in wysiwyg_tag.find_all_previous():
if previous.name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
nearest_title = previous.get_text().strip()
break
if previous.name == 'p' and 'title' in previous.get('class', []):
nearest_title = previous.get_text().strip()
break
if nearest_title:
nearest_title = re.sub(r'\(\d\)', '', nearest_title)
nearest_title = re.sub(r'^\d+\.\s*', '', nearest_title)
current_section = nearest_title
struct_page[current_section] = []
else:
continue
for child in wysiwyg_tag.find_all(['p', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']):
text = child.get_text().strip()
text = re.sub(r'\(\d\)', '', text)
if child.name.startswith('h') or (child.name == 'p' and 'title' in child.get('class', [])):
text = re.sub(r'^\d+\.\s*', '', text)
current_section = text
struct_page[current_section] = []
else: # <p> not of class title, or <li>
if 'is-style-mentions' not in child.get('class', []):
if current_section in struct_page:
struct_page[current_section].append(text)
logging.info(f"{html_filename} : Start")
self.embed_page(html_filename, url, struct_page, tags)
def token_length(self, text):
"""
Calculates the token length of a given text
Args:
text (str): The text to be tokenized.
Returns:
int: The number of tokens in the text.
This function takes a string, tokenizes the string, and returns the number of tokens.
"""
return len(self.tokenizer.encode(text, add_special_tokens=False))
def passage_str(self, paragraphs, title, subtitle):
"""
Constructs a passage string from given paragraphs and a title.
Args:
paragraphs (list of str): A list of paragraphs.
title (str): The title of the webpage.
subtitle (str): The title of the passage.
Returns:
str: A passage string that combines the titles and paragraphs.
This function takes a passage made of a list of paragraphs extracted
from a webpage, the title of the webpage, the subtitle corresponding to
the passage, and constructs a single string with the titles followed by
the paragraphs, formatted for embedding.
"""
return f"passage: {title}\n\n{subtitle}\n\n" + '\n'.join(paragraphs)
def embed_page(self, html_filename, url, struct_page, tags, max_chunk_size=512):
documents = []
title = struct_page['title']
for subtitle, paragraphs in struct_page.items():
if subtitle != 'title':
doc_str = self.passage_str(paragraphs, title, subtitle)
doc_token_length = self.token_length(doc_str)
if doc_token_length > max_chunk_size:
sub_paragraphs = []
sub_paragraphs_token_length = 0
paragraph_index = 0
while True:
while sub_paragraphs_token_length < max_chunk_size and paragraph_index < len(paragraphs):
sub_paragraphs.append(paragraphs[paragraph_index])
sub_paragraphs_str = self.passage_str(sub_paragraphs, title, subtitle)
sub_paragraphs_token_length = self.token_length(sub_paragraphs_str)
paragraph_index += 1
if paragraph_index == len(paragraphs):
if sub_paragraphs_token_length >= max_chunk_size:
sub_paragraphs_str_1 = self.passage_str(sub_paragraphs[:-1], title, subtitle)
sub_paragraphs_str_2 = self.passage_str([sub_paragraphs[-1]], title, subtitle)
documents.append(sub_paragraphs_str_1)
documents.append(sub_paragraphs_str_2)
else:
sub_paragraphs_str = self.passage_str(sub_paragraphs, title, subtitle)
documents.append(sub_paragraphs_str)
break
else:
sub_paragraphs_str = self.passage_str(sub_paragraphs[:-1], title, subtitle)
documents.append(sub_paragraphs_str)
paragraph_index -= 1
sub_paragraphs = []
sub_paragraphs_token_length = 0
else:
documents.append(doc_str)
if len(documents) == 0:
return
embeddings = self.model.encode(documents, normalize_embeddings=True)
embeddings = embeddings.tolist()
# We consider the subpart of an URL as tags describing the webpage
# For example,
# "https://www.caisse-epargne.fr/rhone-alpes/professionnels/financer-projets-optimiser-tresorerie/"
# is associated to the tags:
# tags[0] == 'rhone-alpes'
# tags[1] == 'professionnels'
# tags[2] == 'financer-projets-optimiser-tresorerie'
if len(tags) < 2:
category = ''
else:
if tags[0] == 'rhone-alpes':
category = tags[1]
else: category = tags[0]
metadata = {'category': category, 'url': url}
# All the documents corresponding to a same webpage have the same metadata, i.e. URL and category
metadatas = [copy.deepcopy(metadata) for _ in range(len(documents))]
ids = [html_filename + '-' + str(i+1) for i in range(len(documents))]
self.collection.add(embeddings=embeddings, documents=documents, metadatas=metadatas, ids=ids)

98
rag.py
View File

@ -17,76 +17,64 @@ class RAG:
self.tag_end = '</s>'
self.rag_prompt = """
{tag_system}
Objectif
========
Votre mission :
===============
Vous êtes un assistant IA spécialiste des produits et services de la Caisse d'Epargne Rhône-Alpes, \
une banque régionale française.
Vous aidez un conseiller clientèle de la banque à mieux répondre aux besoins des clients.
Vous fournissez avec soin des réponses précises et factuelles aux questions du conseiller.
Vous êtes un assistant IA qui répond à des questions sur des produits et \
services de la Caisse d'Epargne Rhône-Alpes, une banque régionale française.
Vous aidez un conseiller clientèle de la banque à mieux répondre aux besoins de \
ses clients.
Vous fournissez avec soin des réponses précises et factuelles aux questions du \
conseiller.
Utilisation du contexte
=======================
Instructions pour l'utilisation du contexte :
=============================================
Vous répondez à la question posée par le conseiller en utilisant un contexte \
formé de passages exraits du site web commercial de la banque.
Votre réponse se base exclusivement sur les informations factuelles présentes dans le contexte.
Vous répondez de façon brève et factuelle à la question posée par le conseiller \
en utilisant un contexte formé de passages exraits du site web commercial \
de la banque. Le contexte est délimité entre <<< et >>>.
Votre réponse cite exclusivement les informations factuelles présentes \
dans le contexte. Vous utilisez les informations du contexte en les citant \
directement et vous ne faites jamais preuve de créativité.
Si vous ne pouvez pas répondre à la question sur la base des éléments du contexte, \
dites simplement que vous ne savez pas, n'essayez pas d'inventer une réponse.
Voici le format d'un passage du contexte :
```
Titre :
Le titre du passage
n'essayez pas d'inventer une réponse, et dites simplement : "Je ne sais pas."
Catégorie :
La catégorie du passage
Vos réponses doivent toujours mentionner l'URL des passages utilisés. \
Assurez-vous que l'URL correspond exactement à celle du passage. \
Ne générez pas de nouvelles URLs.
URL :
https://www.caisse-epargne.fr/rhone-alpes/url/du/passage
Le style à donner à votre réponse :
===================================
Contenu :
Le contenu du passage
```
Vos réponses doivent toujours citer l'URL des passages utilisés. \
Assurez-vous que l'URL citée correspond exactement à celle du passage. \
Ne générez pas de nouvelles URLs. \
Les conseillers sont encouragés à vérifier les URLs citées.
Format de réponse
=================
Formulez chaque réponse sous forme de recommandations directes et concises, \
Formulez la réponse sous forme de recommandations directes et concises, \
en utilisant le langage et les termes présents dans le contexte.
Citez l'URL en fin de réponse ou immédiatement après la recommandation pertinente.
Vous rédigez votre réponse en français sous forme d'une liste d'informations \
synthétiques extraites du contexte et qui seront utiles au conseiller.
Vos utilisateurs savent qui vous êtes et quelles instructions vous avez reçues, \
il n'est pas nécessaire de le leur rapeler.
Voici le format que doit suivre votre réponse :
```
Voici des informations qui pourront aider votre client :
1. Utilisez [une solution spécifique du contexte] pour [traiter un aspect du problème]. Par exemple, [détail concret tiré du contexte]. Pour plus d'informations voir https://www.caisse-epargne.fr/rhone-alpes/url/du/passage
2. Considérez [une autre solution du contexte], qui est particulièrement adaptée pour [un autre aspect du problème]. Par exemple, [autre détail concret du contexte]. Pour plus d'informations voir https://www.caisse-epargne.fr/rhone-alpes/url/du/passage
```
Votre réponse est complète mais très concise, sa longueur ne dépasse pas 250 mots.
Vous ne répétez jamais deux fois la même information.
Votre réponse se terminent par la mention des URL des passages du contexte \
que vous avez utilisés.
Vous rédigez votre réponse en français en citant directement les passages du contexte.
Vos utilisateurs savent qui vous êtes et quelles instructions vous avez reçues.
Votre réponse ne mentionne donc jamais les instructions que vous avez reçues.
{tag_end}
{history}
{tag_user}
Contexte :
==========
{context}
Question de l'utilisateur :
===========================
Contexte à utiliser pour répondre à la question :
=================================================
<<<
{context}
>>>
Question à laquelle répondre en utilisant le contexte :
=======================================================
{query}
{tag_end}
{tag_assistant}
Voici des informations qui pourront aider votre client :
Voici des informations factuelles et brèves qui répondent à la question :
1.
"""
self.query_reformulate_prompt = """
{tag_system}
@ -119,7 +107,7 @@ Reformulez la question suivante : "{query}"
Question reformulée : "
"""
self.prefix_assistant_prompt = '1. '
self.prefix_assistant_prompt = ''
self.embed_model = SentenceTransformer(embed_model_name)
self.chroma_client = chromadb.PersistentClient(path=chromadb_path)
self.collection = self.chroma_client.get_collection(name=collection_name)
@ -136,7 +124,7 @@ Question reformulée : "
return response
else: return response["choices"][0]["text"]
def query_collection(self, query, n_results=3):
def query_collection(self, query, n_results=1):
logging.info(f"query_collection / query: \n{query}")
query = 'query: ' + query
query_embedding = self.embed_model.encode(query, normalize_embeddings=True)

342
rag_fr_chroma.ipynb Normal file
View File

@ -0,0 +1,342 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "9659df5b-f71a-4594-8f08-44af62ce0056",
"metadata": {},
"outputs": [],
"source": [
"import chromadb\n",
"import numpy as np"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "aad1a570-a5d6-482b-a683-b77d1ae126cc",
"metadata": {},
"outputs": [],
"source": [
"chromadb_path = './chromadb'\n",
"collection_name = 'cera'\n",
"chroma_client = chromadb.PersistentClient(path=chromadb_path)\n",
"collection = chroma_client.get_collection(name=collection_name)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "77c80131-ad2d-423e-a936-e548a4982d1f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"passage: Devenez sociétaire !\n",
"\n",
"Devenez sociétaire !\n",
"\n",
"Être sociétaire de sa Caisse dEpargne, cest soutenir une banque qui agit en local, à travers de nombreuses actions soutenues sur son territoire et sur le financement de léconomie régionale.\n",
"Chaque année le sociétaire est invité à participer à lassemblée générale de sa société locale dépargne (SLE)…et à sexprimer selon le principe « un homme=une voix ». Il assiste à la présentation du rapport dactivité de sa banque coopérative et participe à lapprobation des résultats financiers de sa SLE.\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Qui peut devenir sociétaire ?\n",
"\n",
"Tous les clients de la Caisse dEpargne peuvent souscrire des parts sociales : particuliers, personnes morales (associations, entreprises), EPCI (Établissements Publics de Coopération Intercommunale) à fiscalité propre. Les collectivités territoriales peuvent également devenir sociétaires.\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Comment devenir sociétaire ?\n",
"\n",
"Vous souscrivez vos parts sociales de la Société Locale dEpargne (SLE) auprès de lagence où est domicilié votre compte principal. Pour tout renseignement, contactez votre conseiller, il saura vous orienter.\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Valeur nominale\n",
"\n",
"Contrairement aux actions, la part sociale nest pas cotée. Elle nest pas soumise aux aléas de la bourse. Sa valeur nominale reste fixe : 20 euros.\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Rémunération\n",
"\n",
"Vous avez droit à un intérêt annuel. Vous ne payez aucuns frais tant à la souscription, à la tenue de compte quau remboursement.\n",
"\n",
"Lintérêt des parts sociales est conditionné à une décision de lassemblée générale de la Caisse dEpargne selon ses résultats.\n",
"\n",
"A noter : Les parts sociales sont rémunérées prorata temporis par mois civil entier de détention, à compter du 1er jour du mois qui suit la date dagrément du sociétaire, jusquà la date de clôture de lexercice.\n",
"\n",
"Si vous revendez vos parts sociales avant la clôture de lexercice, vous ne percevez pas les intérêts pour lexercice en cours. En cas de liquidation de votre SLE, vous navez pas de droit sur lactif net (principe coopératif).\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Pas de frais de gestion\n",
"\n",
"Dans le cas dune détention sur un compte dédié, les parts sociales ne sont soumises à aucuns frais : pas de commission de souscription, pas de frais de rachat ni de droits de garde. Une commission de tenue de compte peut cependant être prélevée dans le cadre dun compte de parts sociales (comptes titres) ou dun PEA. La souscription est conditionnée à lagrément du conseil dadministration.\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Fiscalité\n",
"\n",
"Les intérêts aux parts sociales sont soumis au même régime fiscal que les dividendes dactions françaises. Si vous nêtes pas domicilié fiscalement en France, les intérêts aux parts sociales supportent une retenue à la source dont le taux peut être réduit par la convention fiscale liant votre pays de résidence fiscale et la France. Nous vous conseillons de vous rapprocher, le cas échéant dun conseiller fiscal.\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Rachat\n",
"\n",
"Si vous souhaitez que la SLE rachète vos parts sociales, votre demande, soumise à lautorisation du conseil dadministration de la SLE, doit être formulée avant le 31 mai, date de clôture de lexercice. De ce fait, aucune assurance ne peut être donnée quant à la liquidité des parts sociales.\n",
"\n",
"Par ailleurs, les rachats de parts sociales sont subordonnés au respect du capital minimum en deçà duquel la SLE ne peut descendre. En conséquence, les sociétaires doivent être conscients quils pourraient ne pas être en mesure de céder facilement leurs parts sociales.\n",
"\n",
"Le remboursement effectif, quil soit consécutif à la perte de la qualité de sociétaire ou à une demande de rachat, intervient le 1er jour ouvré du nouvel exercice ou dans un délai maximum de 3 mois suivant la demande pour les cas dérogatoires suivants :\n",
"\n",
" Pour un particulier : changement de foyer fiscal, décès, divorce, invalidité, licenciement, départ à la retraite ou préretraite, transfert du domicile à létranger, déménagement hors du ressort territorial de la Caisse dEpargne daffiliation, redressement judiciaire du sociétaire, de clôture dun livret A lorsque le client ne détient pas dautres produits et tout évènement exceptionnel revêtant une gravité telle quelle contraigne le sociétaire à liquider tout ou partie de ses parts.\n",
"\n",
" Pour une personne morale : redressement judiciaire, liquidation ou dissolution.\n",
"\n",
"Les parts sociales sont remboursées à leur valeur nominale sous réserve du risque investisseur (cf. paragraphe « Responsabilité risque de perte en capital »)\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Responsabilité risque de perte en capital\n",
"\n",
"Votre responsabilité dinvestisseur est limitée au niveau de votre investissement. Le risque investisseur (risque de perte en capital) porte sur le Groupe BPCE et non sur la SLE ou la Caisse dEpargne (du fait du mécanisme de solidarité interne au Groupe BPCE).\n",
"\n",
"Les parts sociales demeurent des instruments risqués. Votre responsabilité, limitée au montant de lapport, est engagée jusquà 5 ans après le retrait. Il existe un risque de perte en capital en cas de défaut ou de faillite de la Caisse dEpargne ou de mise en œuvre de mesures de résolution au sein du Groupe BPCE. Les parts sociales ne sont pas éligibles au mécanisme de garantie des investisseurs ou de garantie des déposants et leur rémunération nest pas garantie.\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Rang de subordination\n",
"\n",
"En cas de liquidation, les liquidateurs seront chargés de réaliser lactif, deffectuer le paiement des dettes sociales et, en dernier lieu, de rembourser éventuellement le capital social aux sociétaires si celui-ci est suffisant après paiement des dettes de la SLE.\n",
"Pour tout renseignement, contactez votre conseiller, il saura vous orienter. \n",
"\n",
"Préalablement à toute souscription, conformément à larticle 212-38-13 du règlement général de lAMF, il est recommandé de lire attentivement le prospectus visé par lAMF établi pour loffre au public de parts sociales et plus particulièrement la rubrique « facteurs de risques ». Ce prospectus est disponible sur simple demande sans frais en agence et sur le site de lAMF www.amf-france.org et sur le site www.caisse-epargne.fr.\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Prenez part à la vie de votre Caisse d'Epargne\n",
"\n",
"En tant que sociétaire, vous êtes impliqué dans la vie de votre Caisse dEpargne et prenez part aux décisions en y exerçant un droit de vote. Vous élisez vos représentants parmi les sociétaires, les administrateurs, qui élisent à leur tour leur président qui les représente à lassemblée générale de votre Caisse dEpargne. \n",
"\n",
"Vous participez ainsi aux grandes orientations de votre Caisse dEpargne. Chaque année, à lAssemblée Générale de votre SLE, vous rencontrez les dirigeants de votre Caisse dEpargne et bénéficiez dune information spécifique. \n",
"\n",
"Tout au long de lannée, vous pouvez également être invité à participer à des événements organisés par votre Caisse dEpargne.\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Contribuez au développement économique et social de vos territoires\n",
"\n",
"Chez Caisse dEpargne Rhône Alpes, votre épargne est réinvestie dans des projets de territoire. Votre Caisse dEpargne est un acteur majeur et le premier financeur privé de léconomie sociale et solidaire.\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Soutenez des actions de solidarité locale\n",
"\n",
"Au cœur des métiers de Caisse dEpargne Rhône Alpes, cette solidarité sexprime concrètement par les nombreuses actions de mécénat soutenues localement. \n",
"\n",
"Vos représentants participent à la remontée de projets locaux associatifs et à la sélection de ceux qui seront soutenus par Caisse dEpargne Rhône Alpes.\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Le site sociétaires\n",
"\n",
"Sur www.societaires.caisse-epargne.fr, vous disposez dun site dinformation et davantages sélectionnés pour vous. Vous y découvrirez les réalisations et engagements de votre Caisse dEpargne sur votre territoire : actualité, partenariats, soutien aux actions sociétales…\n",
"Cest aussi une source incontournable dinformations sur lorganisation et les valeurs coopératives, les assemblées générales, la vie du sociétariat et des sociétés locales dépargne.\n",
"\n",
"----------\n",
"\n",
"passage: Devenez sociétaire !\n",
"\n",
"Le Club\n",
"\n",
"\n",
"\n",
"----------\n",
"\n"
]
}
],
"source": [
"nb_passage = 1\n",
"while True:\n",
" id = \"f6d921ced8.html-\" + str(nb_passage)\n",
" societaires = collection.get(ids=[id])\n",
" if len(societaires['documents']) > 0:\n",
" print(societaires['documents'][0])\n",
" print(\"\\n----------\\n\")\n",
" nb_passage += 1\n",
" else:\n",
" break"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a15e2f52-f3f6-4b6d-9ac9-b39528580d9e",
"metadata": {},
"outputs": [],
"source": [
"collection.get(ids=['f6d921ced8.html-10'])"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "089de7c4-7c17-464d-a0dd-04db86299cda",
"metadata": {},
"outputs": [],
"source": [
"col = collection.get()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "eb193ac5-41a2-4fdd-81f5-0d3c56f72829",
"metadata": {},
"outputs": [],
"source": [
"col['ids'][0]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "97b08f88-9837-45f3-8622-49a95a2b312c",
"metadata": {},
"outputs": [],
"source": [
"lengths = np.array([len(doc) for doc in col['documents']])"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "10d4a676-648a-4412-a168-8dcbf754af1e",
"metadata": {},
"outputs": [],
"source": [
"# Calculating statistics\n",
"min_length = np.min(lengths)\n",
"max_length = np.max(lengths)\n",
"mean_length = np.mean(lengths)\n",
"median_length = np.median(lengths)\n",
"std_dev_length = np.std(lengths)\n",
"\n",
"# Printing the statistics\n",
"print(f\"Minimum Length: {min_length}\")\n",
"print(f\"Maximum Length: {max_length}\")\n",
"print(f\"Mean Length: {mean_length:.2f}\")\n",
"print(f\"Median Length: {median_length}\")\n",
"print(f\"Standard Deviation: {std_dev_length:.2f}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e6399d75-1823-4723-8f6b-955a30e226aa",
"metadata": {},
"outputs": [],
"source": [
"docs = list(zip(col['documents'], col['ids']))\n",
"sorted_docs_ids = sorted(docs, key=lambda x: len(x[0]))\n",
"sorted_docs, sorted_ids = zip(*sorted_docs_ids)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "61900d54-5af3-4c3d-8dcc-2e80a8b735d7",
"metadata": {},
"outputs": [],
"source": [
"for i, doc in enumerate(sorted_docs[200:210]):\n",
" print(sorted_ids[i])\n",
" print(doc)\n",
" print(\"\\n---\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "14778379-01fc-415d-841d-b02909863383",
"metadata": {},
"outputs": [],
"source": [
"docs = col['documents']\n",
"sorted_docs = sorted(docs, key=len)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "195a89b2-a269-4baf-acf2-f60ddaa78082",
"metadata": {},
"outputs": [],
"source": [
"for doc in sorted_docs[:10]:\n",
" print(doc)\n",
" print(\"\\n---\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "19f3d7bf-0c22-498b-988e-45f8e65474f6",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "RAG_ENV",
"language": "python",
"name": "rag_env"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.18"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

File diff suppressed because it is too large Load Diff

252
rag_fr_embedding_test.ipynb Normal file
View File

@ -0,0 +1,252 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "d8acc709-ebb2-4fa6-982b-3d13fe8d2beb",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/peportier/miniforge3/envs/RAG_ENV/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
" from .autonotebook import tqdm as notebook_tqdm\n",
"/Users/peportier/miniforge3/envs/RAG_ENV/lib/python3.9/site-packages/transformers/utils/generic.py:441: UserWarning: torch.utils._pytree._register_pytree_node is deprecated. Please use torch.utils._pytree.register_pytree_node instead.\n",
" _torch_pytree._register_pytree_node(\n"
]
}
],
"source": [
"from bs4 import BeautifulSoup\n",
"import base64\n",
"import re\n",
"from transformers import AutoTokenizer\n",
"import logging\n",
"import os\n",
"from IPython.display import Markdown, display"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "54f5ab50-2ee3-45ad-9208-c1e2dc362152",
"metadata": {},
"outputs": [],
"source": [
"from transformers import AutoTokenizer\n",
"model_name = 'intfloat/multilingual-e5-large'\n",
"tokenizer = AutoTokenizer.from_pretrained(model_name)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "8c9e6f4e-609d-488d-a738-41934a62e92a",
"metadata": {},
"outputs": [],
"source": [
"def token_length(text):\n",
" return len(tokenizer.encode(text, add_special_tokens=False))"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "8deef599-04b0-4d9f-9b3e-ac9ae5a472a0",
"metadata": {},
"outputs": [],
"source": [
"def passage_str(paragraphs, title, subtitle):\n",
" return f\"passage: {title}\\n\\n{subtitle}\\n\\n\" + '\\n'.join(paragraphs)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "1ef97436-37c2-45b4-8e00-7737d87c261e",
"metadata": {},
"outputs": [],
"source": [
"html_folder_path = '../scrapcera/htmls/'\n",
"txt_folder_path = '../scrapcera/docs/'\n",
"html_filename = '97e88fd1d6.html'"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "560280af-ad79-43e8-b4df-c4f69aa40dcf",
"metadata": {},
"outputs": [],
"source": [
"for html_filename in ['f6d921ced8.html']: # os.listdir(html_folder_path):\n",
" \n",
" html_file_path = os.path.join(html_folder_path, html_filename)\n",
" txt_filename = re.sub(r'\\.html', '.txt', html_filename)\n",
" txt_file_path = os.path.join(txt_folder_path, txt_filename)\n",
" with open(txt_file_path, 'r') as file:\n",
" txt_file_contents = file.read()\n",
" \n",
" url = txt_file_contents.split('\\n')[0]\n",
" if '?' in url: # URLs with a '?' corresponds to call to services and have no useful content\n",
" continue\n",
" if not url.startswith('https://www.caisse-epargne.fr/rhone-alpes/'):\n",
" continue\n",
" \n",
" prefix = 'https://www.caisse-epargne.fr/'\n",
" suffix = url.replace(prefix, '')\n",
" tags = suffix.split('/')\n",
" tags = [tag for tag in tags if tag] # remove empty par\n",
" with open(html_file_path, 'r') as file:\n",
" html_file_contents = file.read()\n",
" soup = BeautifulSoup(html_file_contents, 'html.parser')\n",
" page_title_present = soup.find('section').find('h1')\n",
" if not page_title_present:\n",
" continue\n",
" page_title = page_title_present.get_text()\n",
" \n",
" sections = soup.find_all(lambda tag: tag.name in ['section'] and 'key-informations' not in tag.get('class', []))\n",
" \n",
" struct_page = {'title': page_title}\n",
" current_section = ''\n",
" for section in sections:\n",
" for wysiwyg_tag in section.find_all(class_=\"wysiwyg\"):\n",
" # Check for a title within the wysiwyg container\n",
" internal_title = wysiwyg_tag.find(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) or wysiwyg_tag.find('p', class_='title')\n",
" \n",
" # If no internal title, find the nearest title from previous siblings\n",
" if not internal_title:\n",
" # Find the nearest title from previous siblings\n",
" nearest_title = None\n",
" for sibling in wysiwyg_tag.find_previous_siblings():\n",
" if sibling.name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:\n",
" nearest_title = sibling.get_text().strip()\n",
" break\n",
" if sibling.name == 'p' and 'title' in sibling.get('class', []):\n",
" nearest_title = sibling.get_text().strip()\n",
" break\n",
" if nearest_title:\n",
" nearest_title = re.sub(r'\\(\\d\\)', '', nearest_title)\n",
" nearest_title = re.sub(r'^\\d+\\.\\s*', '', nearest_title)\n",
" current_section = nearest_title\n",
" struct_page[current_section] = []\n",
" else:\n",
" continue\n",
" for child in wysiwyg_tag.find_all(['p', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']):\n",
" text = child.get_text().strip()\n",
" text = re.sub(r'\\(\\d\\)', '', text)\n",
" if child.name.startswith('h') or (child.name == 'p' and 'title' in child.get('class', [])):\n",
" text = re.sub(r'^\\d+\\.\\s*', '', text)\n",
" current_section = text\n",
" struct_page[current_section] = []\n",
" else: # <p> not of class title, or <li>\n",
" if 'is-style-mentions' not in child.get('class', []):\n",
" if current_section in struct_page:\n",
" struct_page[current_section].append(text)\n",
"\n",
" # detect_big_chunks(struct_page, html_filename)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "e6da54d7-6c70-44eb-b08b-392c742d0525",
"metadata": {},
"outputs": [],
"source": [
"chunks_length = []\n",
"def detect_big_chunks(struct_page, filename):\n",
" global big_chunks_length\n",
" max_chunk_size=512\n",
" title = struct_page['title']\n",
" for subtitle, paragraphs in struct_page.items():\n",
" if subtitle != 'title':\n",
" doc_str = passage_str(paragraphs, title, subtitle)\n",
" doc_token_length = token_length(doc_str)\n",
" if doc_token_length > max_chunk_size:\n",
" sub_paragraphs = []\n",
" sub_paragraphs_token_length = 0\n",
" paragraph_index = 0\n",
" while True:\n",
" while sub_paragraphs_token_length < max_chunk_size and paragraph_index < len(paragraphs):\n",
" sub_paragraphs.append(paragraphs[paragraph_index])\n",
" sub_paragraphs_str = passage_str(sub_paragraphs, title, subtitle)\n",
" sub_paragraphs_token_length = token_length(sub_paragraphs_str)\n",
" paragraph_index += 1\n",
" if paragraph_index == len(paragraphs):\n",
" if sub_paragraphs_token_length >= max_chunk_size:\n",
" sub_paragraphs_str_1 = passage_str(sub_paragraphs[:-1], title, subtitle)\n",
" sub_paragraphs_str_2 = passage_str([sub_paragraphs[-1]], title, subtitle)\n",
" chunks_length.append(len(sub_paragraphs_str_1))\n",
" chunks_length.append(len(sub_paragraphs_str_2))\n",
" else:\n",
" sub_paragraphs_str = passage_str(sub_paragraphs, title, subtitle)\n",
" chunks_length.append(len(sub_paragraphs_str))\n",
" break\n",
" else:\n",
" sub_paragraphs_str = passage_str(sub_paragraphs[:-1], title, subtitle)\n",
" chunks_length.append(len(sub_paragraphs_str))\n",
" paragraph_index -= 1\n",
" sub_paragraphs = []\n",
" sub_paragraphs_token_length = 0\n",
" \n",
" chunks_length.append(len(doc_str))"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "8a534ec5-a85a-41bf-b229-c896612cec42",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'title': 'Devenez sociétaire !',\n",
" 'Qui peut devenir sociétaire ?': ['Tous les clients de la Caisse dEpargne peuvent souscrire des parts sociales : particuliers, personnes morales (associations, entreprises), EPCI (Établissements Publics de Coopération Intercommunale) à fiscalité propre. Les collectivités territoriales peuvent également devenir sociétaires.'],\n",
" 'Comment devenir sociétaire\\xa0?': ['Vous souscrivez vos parts sociales de la Société Locale dEpargne (SLE) auprès de lagence où est domicilié votre compte principal. Pour tout renseignement, contactez votre conseiller, il saura vous orienter.'],\n",
" 'Le site sociétaires': ['Sur www.societaires.caisse-epargne.fr, vous disposez dun site dinformation et davantages sélectionnés pour vous. Vous y découvrirez les réalisations et engagements de votre Caisse dEpargne sur votre territoire : actualité, partenariats, soutien aux actions sociétales…',\n",
" 'Cest aussi une source incontournable dinformations sur lorganisation et les valeurs coopératives, les assemblées générales, la vie du sociétariat et des sociétés locales dépargne.']}"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"struct_page"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fdd70455-c279-4b08-87e9-d42c5c093bc6",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "RAG_ENV",
"language": "python",
"name": "rag_env"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.18"
}
},
"nbformat": 4,
"nbformat_minor": 5
}