diff --git a/__pycache__/data_filter.cpython-312.pyc b/__pycache__/data_filter.cpython-312.pyc index 5232154..b43c857 100644 Binary files a/__pycache__/data_filter.cpython-312.pyc and b/__pycache__/data_filter.cpython-312.pyc differ diff --git a/data_filter.py b/data_filter.py index 556efd9..9d7cb51 100644 --- a/data_filter.py +++ b/data_filter.py @@ -7,184 +7,148 @@ import shutil from typing import Dict, List, Any, Optional, Tuple, Union, Set -def is_odoobot_message(message: Dict[str, Any]) -> bool: +def is_odoobot_author(message: Dict[str, Any]) -> bool: """ - Détecte si un message provient d'OdooBot ou d'un bot système. + Vérifie si l'auteur du message est OdooBot ou un autre système. Args: - message: Dictionnaire du message à analyser + message: Le message à vérifier Returns: - True si le message est d'OdooBot, False sinon + True si le message provient d'OdooBot, False sinon """ - if not message: - return False + # Vérifier le nom de l'auteur + if 'author_id' in message and isinstance(message['author_id'], list) and len(message['author_id']) > 1: + author_name = message['author_id'][1].lower() + if 'odoobot' in author_name or 'bot' in author_name or 'système' in author_name: + return True - # Vérifier par le nom de l'auteur - author_name = "" - if message.get('author_id') and isinstance(message.get('author_id'), list) and len(message.get('author_id')) > 1: - author_name = message.get('author_id')[1].lower() - elif message.get('author_details', {}).get('name'): - author_name = message.get('author_details', {}).get('name', '').lower() - - if 'odoobot' in author_name or 'bot' in author_name or 'système' in author_name or 'system' in author_name: - return True - - # Vérifier par le contenu du message (messages système typiques) - body = message.get('body', '').lower() - if body and isinstance(body, str): - system_patterns = [ - r'assigné à', - r'assigned to', - r'étape changée', - r'stage changed', - r'créé automatiquement', - r'automatically created', - r'a modifié la date limite', - r'changed the deadline', - r'a ajouté une pièce jointe', - r'added an attachment' - ] - - for pattern in system_patterns: - if re.search(pattern, body, re.IGNORECASE): - return True - - # Vérifier par le type de message/sous-type + # Vérifier le type de message (souvent les notifications système) if message.get('message_type') == 'notification': return True - subtype_name = "" - if message.get('subtype_id') and isinstance(message.get('subtype_id'), list) and len(message.get('subtype_id')) > 1: - subtype_name = message.get('subtype_id')[1].lower() - elif message.get('subtype_details') and isinstance(message.get('subtype_details'), list) and len(message.get('subtype_details')) > 0: - subtype_name = message.get('subtype_details')[0].get('name', '').lower() + # Vérifier le sous-type du message + if 'subtype_id' in message and isinstance(message['subtype_id'], list) and len(message['subtype_id']) > 1: + subtype = message['subtype_id'][1].lower() + if 'notification' in subtype or 'system' in subtype: + return True - if subtype_name and ('notification' in subtype_name or 'system' in subtype_name): - return True + # Vérifier le contenu du message + if 'body' in message and isinstance(message['body'], str): + body = message['body'].lower() + system_patterns = [ + 'assigné à', 'étape changée', 'créé automatiquement', + 'assigned to', 'stage changed', 'automatically created', + 'updated', 'mis à jour', 'a modifié', 'changed' + ] + + for pattern in system_patterns: + if pattern in body: + return True return False -def is_important_image(img_tag: Any, message_text: str) -> bool: +def is_important_image(tag, message_text: str) -> bool: """ - Détermine si une image est importante ou s'il s'agit d'une image inutile (logo, signature, etc.). + Détermine si une image est importante ou s'il s'agit d'un logo/signature. Args: - img_tag: Balise d'image BeautifulSoup - message_text: Texte du message complet pour contexte + tag: La balise d'image à analyser + message_text: Le texte complet du message pour contexte Returns: True si l'image semble importante, False sinon """ # Vérifier les attributs de l'image - img_src = img_tag.get('src', '') - img_alt = img_tag.get('alt', '') - img_class = img_tag.get('class', '') - img_style = img_tag.get('style', '') + src = tag.get('src', '') + alt = tag.get('alt', '') + title = tag.get('title', '') + css_class = tag.get('class', '') - # Mots-clés indiquant des images inutiles - useless_patterns = [ - 'logo', 'signature', 'footer', 'header', 'separator', 'separateur', - 'outlook', 'mail_signature', 'icon', 'emoticon', 'emoji', 'cid:', - 'pixel', 'spacer', 'vignette', 'footer', 'banner', 'banniere' + # Patterns pour les images inutiles + useless_img_patterns = [ + 'logo', 'signature', 'outlook', 'footer', 'header', 'icon', + 'emoticon', 'emoji', 'cid:', 'pixel', 'spacer', 'vignette', + 'banner', 'separator', 'decoration', 'mail_signature' ] - # Vérifier le src/alt/class pour les motifs inutiles - for pattern in useless_patterns: - if (pattern in img_src.lower() or - pattern in img_alt.lower() or - (isinstance(img_class, list) and any(pattern in c.lower() for c in img_class)) or - (isinstance(img_class, str) and pattern in img_class.lower()) or - pattern in img_style.lower()): + # Vérifier si c'est une image inutile + for pattern in useless_img_patterns: + if (pattern in src.lower() or + pattern in alt.lower() or + pattern in title.lower() or + (css_class and any(pattern in c.lower() for c in css_class if isinstance(c, str)))): return False - # Vérifier les dimensions (logos et icônes sont souvent petits) - width = img_tag.get('width', '') - height = img_tag.get('height', '') - - # Convertir en entiers si possible + # Vérifier la taille (les petites images sont souvent des icônes/logos) + width = tag.get('width', '') + height = tag.get('height', '') try: - width = int(width) if width and width.isdigit() else None - height = int(height) if height and height.isdigit() else None - except (ValueError, TypeError): - # Extraire les dimensions des attributs style si disponibles - if img_style: - width_match = re.search(r'width:[ ]*(\d+)', img_style) - height_match = re.search(r'height:[ ]*(\d+)', img_style) - - width = int(width_match.group(1)) if width_match else None - height = int(height_match.group(1)) if height_match else None - - # Images très petites sont souvent des éléments décoratifs - if width is not None and height is not None: - if width <= 50 and height <= 50: # Taille arbitraire pour les petites images + width = int(width) if width and str(width).isdigit() else None + height = int(height) if height and str(height).isdigit() else None + if width and height and width <= 50 and height <= 50: return False + except (ValueError, TypeError): + pass - # Rechercher des termes qui indiquent l'importance de l'image dans le texte du message - importance_indicators = [ + # Vérifier si l'image est mentionnée dans le texte + image_indicators = [ 'capture', 'screenshot', 'image', 'photo', 'illustration', - 'pièce jointe', 'attachment', 'voir', 'regarder', 'ci-joint', - 'écran', 'erreur', 'problème', 'bug', 'issue' + 'voir', 'regarder', 'ci-joint', 'écran', 'erreur', 'problème', + 'bug', 'pièce jointe', 'attachment', 'veuillez trouver' ] - for indicator in importance_indicators: + for indicator in image_indicators: if indicator in message_text.lower(): return True - # Par défaut, considérer l'image comme importante si aucun des filtres ci-dessus ne s'applique + # Par défaut, considérer les images qui ne sont pas clairement inutiles comme potentiellement importantes return True -def find_relevant_attachments(message_text: str, attachments_info: List[Dict[str, Any]]) -> List[int]: +def find_attachment_references(message_text: str, attachments_info: List[Dict[str, Any]]) -> List[int]: """ - Trouve les pièces jointes pertinentes mentionnées dans le message. + Identifie les pièces jointes mentionnées dans le message. Args: message_text: Texte du message - attachments_info: Liste des informations sur les pièces jointes + attachments_info: Informations sur les pièces jointes disponibles Returns: Liste des IDs des pièces jointes pertinentes """ - relevant_ids = [] - if not message_text or not attachments_info: - return relevant_ids + return [] - # Rechercher les mentions de pièces jointes dans le texte + # Patterns indiquant des pièces jointes attachment_indicators = [ - r'pi(è|e)ce(s)? jointe(s)?', r'attachment(s)?', r'fichier(s)?', r'file(s)?', - r'voir (le|la|les) document(s)?', r'voir (le|la|les) fichier(s)?', - r'voir (le|la|les) image(s)?', r'voir (le|la|les) screenshot(s)?', - r'voir (le|la|les) capture(s)?', r'voir (le|la|les) photo(s)?', - r'voir ci-joint', r'voir ci-dessous', r'voir ci-après', - r'veuillez trouver', r'please find', r'in attachment', - r'joint(e)?(s)?', r'attached', r'screenshot(s)?', r'capture(s)? d(\'|e) (é|e)cran', - r'image(s)?', r'photo(s)?' + r'pi[èe]ce[s]? jointe[s]?', r'attachment[s]?', r'fichier[s]?', r'file[s]?', + r'veuillez trouver', r'please find', r'voir ci-joint', r'voir ci-dessous', + r'ci-joint', r'joint[e]?[s]?', r'attached', r'screenshot[s]?', + r'capture[s]? d[\'e] ?[ée]cran', r'image[s]?', r'photo[s]?' ] - has_attachment_mention = False - for indicator in attachment_indicators: - if re.search(indicator, message_text, re.IGNORECASE): - has_attachment_mention = True + relevant_ids = [] + + # Vérifier si le message mentionne des pièces jointes + mention_found = False + for pattern in attachment_indicators: + if re.search(pattern, message_text, re.IGNORECASE): + mention_found = True break - # Si le message mentionne des pièces jointes - if has_attachment_mention: + if mention_found: + # Identifier les pièces jointes pertinentes (non logos/images d'interface) for attachment in attachments_info: - # Exclure les pièces jointes qui semblent être des signatures ou des logos - name = attachment.get('name', '').lower() - useless_patterns = ['logo', 'signature', 'outlook', 'footer', 'header', 'icon', 'emoticon', 'emoji'] + name = attachment.get('name', '').lower() if attachment.get('name') else '' - is_useless = False - for pattern in useless_patterns: - if pattern in name: - is_useless = True - break + # Exclure les pièces jointes qui semblent être des logos ou images d'interface + useless_patterns = ['logo', 'signature', 'outlook', 'icon', 'emoticon', 'emoji'] + is_useless = any(pattern in name for pattern in useless_patterns) - if not is_useless: - relevant_ids.append(attachment.get('id')) + if not is_useless and 'id' in attachment: + relevant_ids.append(attachment['id']) return relevant_ids @@ -192,7 +156,7 @@ def find_relevant_attachments(message_text: str, attachments_info: List[Dict[str def clean_html(html_content: str) -> str: """ Nettoie le contenu HTML en supprimant toutes les balises mais en préservant le texte. - Améliore le traitement des images, supprime les signatures et les éléments inutiles. + Traite spécifiquement les images pour garder uniquement celles pertinentes. Args: html_content: Contenu HTML à nettoyer @@ -206,41 +170,38 @@ def clean_html(html_content: str) -> str: # Utiliser BeautifulSoup pour manipuler le HTML soup = BeautifulSoup(html_content, 'html.parser') - # Supprimer les signatures et pieds de courriels typiques - signature_selectors = [ + # Supprimer les éléments de signature + signature_elements = [ 'div.signature', '.gmail_signature', '.signature', - 'hr + div', 'hr + p', '.footer', '.mail-signature', - '.ms-signature', '[data-smartmail="gmail_signature"]' + 'hr + div', 'hr + p', '.footer', '.mail-signature' ] - for selector in signature_selectors: + for selector in signature_elements: for element in soup.select(selector): element.decompose() - # Supprimer les lignes horizontales qui séparent souvent les signatures + # Supprimer les lignes horizontales (souvent utilisées pour séparer les signatures) for hr in soup.find_all('hr'): hr.decompose() + # Récupérer le texte complet pour analyse + full_text = soup.get_text(' ', strip=True) + # Traiter les images - message_text = soup.get_text() for img in soup.find_all('img'): - if is_important_image(img, message_text): + if is_important_image(img, full_text): # Remplacer les images importantes par une description alt_text = img.get('alt', '') or img.get('title', '') or '[Image importante]' img.replace_with(f" [Image: {alt_text}] ") else: - # Supprimer les images inutiles + # Supprimer les images non pertinentes img.decompose() - # Traiter les références aux pièces jointes - attachment_refs = soup.find_all('a', href=re.compile(r'attachment|piece|fichier|file', re.IGNORECASE)) - for ref in attachment_refs: - ref.replace_with(f" [Pièce jointe: {ref.get_text()}] ") - - # Filtrer les éléments vides ou non significatifs - for tag in soup.find_all(['span', 'div', 'p']): - if not tag.get_text(strip=True): - tag.decompose() + # Traiter les liens vers des pièces jointes + for a in soup.find_all('a', href=True): + href = a.get('href', '').lower() + if 'attachment' in href or 'download' in href or 'file' in href: + a.replace_with(f" [Pièce jointe: {a.get_text()}] ") # Récupérer le texte sans balises HTML text = soup.get_text(separator=' ', strip=True) @@ -254,7 +215,7 @@ def clean_html(html_content: str) -> str: # Nettoyer les lignes vides multiples text = re.sub(r'\n\s*\n', '\n\n', text) - # Supprimer les footers typiques des emails + # Supprimer les disclaimers et signatures standards footer_patterns = [ r'Sent from my .*', r'Envoyé depuis mon .*', @@ -267,12 +228,7 @@ def clean_html(html_content: str) -> str: r'This message and any attachments.*', r'Ce message et ses pièces jointes.*', r'AVIS DE CONFIDENTIALITÉ.*', - r'PRIVACY NOTICE.*', - r'Droit à la déconnexion.*', - r'L\'objectif du Support Technique.*', - r'\\*\\*\\*\\*\\*\\* ATTENTION \\*\\*\\*\\*\\*\\*.*', - r'Please consider the environment.*', - r'Pensez à l\'environnement.*' + r'PRIVACY NOTICE.*' ] for pattern in footer_patterns: @@ -288,14 +244,14 @@ def process_message_file(message_file_path: str, output_dir: str, attachments_in Args: message_file_path: Chemin du fichier de message à traiter output_dir: Répertoire de sortie pour le fichier traité - attachments_info: Informations sur les pièces jointes (optionnel) + attachments_info: Informations sur les pièces jointes disponibles """ try: with open(message_file_path, 'r', encoding='utf-8') as f: message_data = json.load(f) # Ignorer les messages d'OdooBot - if is_odoobot_message(message_data): + if is_odoobot_author(message_data): print(f"Message ignoré (OdooBot): {os.path.basename(message_file_path)}") return @@ -304,9 +260,9 @@ def process_message_file(message_file_path: str, output_dir: str, attachments_in # Remplacer le contenu HTML par le texte filtré message_data['body'] = clean_html(message_data['body']) - # Identifier les pièces jointes pertinentes si disponibles + # Identifier les pièces jointes pertinentes mentionnées dans le message if attachments_info and message_data['body']: - relevant_attachments = find_relevant_attachments(message_data['body'], attachments_info) + relevant_attachments = find_attachment_references(message_data['body'], attachments_info) if relevant_attachments: message_data['relevant_attachment_ids'] = relevant_attachments @@ -328,48 +284,44 @@ def process_messages_threads(threads_file_path: str, output_dir: str, attachment Args: threads_file_path: Chemin du fichier de threads de messages output_dir: Répertoire de sortie pour le fichier traité - attachments_info: Informations sur les pièces jointes (optionnel) + attachments_info: Informations sur les pièces jointes disponibles """ try: with open(threads_file_path, 'r', encoding='utf-8') as f: threads_data = json.load(f) - # Stocker les IDs des threads à supprimer (qui ne contiennent que des messages d'OdooBot) + # Liste des threads à supprimer (ceux qui ne contiennent que des messages d'OdooBot) threads_to_remove = [] # Parcourir tous les threads for thread_id, thread in threads_data.items(): - - # Vérifier si le message principal existe et n'est pas d'OdooBot + # Traiter le message principal main_message_is_bot = False if thread.get('main_message'): - if is_odoobot_message(thread['main_message']): + if is_odoobot_author(thread['main_message']): main_message_is_bot = True - # Si c'est un message d'OdooBot, on le supprime thread['main_message'] = None elif 'body' in thread['main_message']: - # Sinon, on nettoie le corps du message thread['main_message']['body'] = clean_html(thread['main_message']['body']) # Identifier les pièces jointes pertinentes if attachments_info and thread['main_message']['body']: - relevant_attachments = find_relevant_attachments( + relevant_attachments = find_attachment_references( thread['main_message']['body'], attachments_info ) if relevant_attachments: thread['main_message']['relevant_attachment_ids'] = relevant_attachments - # Filtrer les réponses pour supprimer celles d'OdooBot + # Traiter les réponses (filtrer les messages d'OdooBot) filtered_replies = [] for reply in thread.get('replies', []): - if not is_odoobot_message(reply): - # Nettoyer le corps du message + if not is_odoobot_author(reply): if 'body' in reply: reply['body'] = clean_html(reply['body']) # Identifier les pièces jointes pertinentes if attachments_info and reply['body']: - relevant_attachments = find_relevant_attachments(reply['body'], attachments_info) + relevant_attachments = find_attachment_references(reply['body'], attachments_info) if relevant_attachments: reply['relevant_attachment_ids'] = relevant_attachments @@ -378,8 +330,7 @@ def process_messages_threads(threads_file_path: str, output_dir: str, attachment # Mettre à jour les réponses thread['replies'] = filtered_replies - # Si le thread ne contient plus de messages (tous étaient des messages d'OdooBot), - # marquer pour suppression + # Si le thread ne contient que des messages de bot, le marquer pour suppression if main_message_is_bot and not filtered_replies: threads_to_remove.append(thread_id) @@ -394,7 +345,7 @@ def process_messages_threads(threads_file_path: str, output_dir: str, attachment print(f"Fichier de threads traité: {os.path.basename(threads_file_path)}") if threads_to_remove: - print(f" {len(threads_to_remove)} threads supprimés (messages d'OdooBot uniquement)") + print(f" {len(threads_to_remove)} threads supprimés (OdooBot uniquement)") except Exception as e: print(f"Erreur lors du traitement du fichier {threads_file_path}: {e}") @@ -407,7 +358,7 @@ def process_messages_collection(messages_file_path: str, output_dir: str, attach Args: messages_file_path: Chemin du fichier de collection de messages output_dir: Répertoire de sortie pour le fichier traité - attachments_info: Informations sur les pièces jointes (optionnel) + attachments_info: Informations sur les pièces jointes disponibles """ try: with open(messages_file_path, 'r', encoding='utf-8') as f: @@ -416,14 +367,14 @@ def process_messages_collection(messages_file_path: str, output_dir: str, attach # Filtrer les messages pour supprimer ceux d'OdooBot filtered_messages = [] for message in messages_data: - if not is_odoobot_message(message): - # Nettoyer le corps du message + if not is_odoobot_author(message): + # Nettoyer le contenu HTML if 'body' in message: message['body'] = clean_html(message['body']) # Identifier les pièces jointes pertinentes if attachments_info and message['body']: - relevant_attachments = find_relevant_attachments(message['body'], attachments_info) + relevant_attachments = find_attachment_references(message['body'], attachments_info) if relevant_attachments: message['relevant_attachment_ids'] = relevant_attachments @@ -464,7 +415,7 @@ def process_ticket_folder(ticket_folder: str, output_base_dir: str) -> None: shutil.copy2(src_file, dst_file) print(f"Fichier copié: {file_name}") - # Charger les informations sur les pièces jointes si disponibles + # Charger les informations sur les pièces jointes attachments_info = [] attachments_info_file = os.path.join(ticket_folder, 'attachments_info.json') if os.path.exists(attachments_info_file): @@ -502,7 +453,7 @@ def process_ticket_folder(ticket_folder: str, output_base_dir: str) -> None: if os.path.exists(message_threads_file): process_messages_threads(message_threads_file, output_ticket_dir, attachments_info) - # Copier le répertoire des pièces jointes (on conserve toutes les pièces jointes) + # Copier le répertoire des pièces jointes (on garde toutes les pièces jointes) src_attachments_dir = os.path.join(ticket_folder, 'attachments') if os.path.exists(src_attachments_dir): dst_attachments_dir = os.path.join(output_ticket_dir, 'attachments') @@ -556,9 +507,9 @@ def run_filter_wizard() -> None: print("\n==== FILTRAGE DES MESSAGES DES TICKETS ====") print("Cette fonction va:") print("1. Supprimer les messages provenant d'OdooBot") - print("2. Supprimer les logos, signatures et images non pertinentes") - print("3. Conserver uniquement le texte utile des messages") - print("4. Identifier les pièces jointes mentionnées dans les messages\n") + print("2. Filtrer les images inutiles (logos, signatures, images Outlook)") + print("3. Conserver les images pertinentes pour la demande") + print("4. Identifier les pièces jointes importantes mentionnées dans les messages\n") # Demander le répertoire source default_source = 'exported_tickets'