first commit

This commit is contained in:
Ladebeze66 2025-04-02 09:01:55 +02:00
commit e2e2b25ab4
2530 changed files with 379470 additions and 0 deletions

View File

@ -0,0 +1,65 @@
# SpecStory Artifacts Directory
This directory is automatically created and maintained by the SpecStory extension to preserve your Cursor composer and chat history.
## What's Here?
- `.specstory/history`: Contains markdown files of your AI coding sessions
- Each file represents a separate chat or composer session
- Files are automatically updated as you work
- `.specstory/cursor_rules_backups`: Contains backups of the `.cursor/rules/derived-cursor-rules.mdc` file
- Backups are automatically created each time the `.cursor/rules/derived-cursor-rules.mdc` file is updated
- You can enable/disable the Cursor Rules feature in the SpecStory settings, it is disabled by default
## Valuable Uses
- Capture: Keep your context window up-to-date when starting new Chat/Composer sessions via @ references
- Search: For previous prompts and code snippets
- Learn: Meta-analyze your patterns and learn from your past experiences
- Derive: Keep Cursor on course with your past decisions by automatically deriving Cursor rules from your AI interactions
## Version Control
We recommend keeping this directory under version control to maintain a history of your AI interactions. However, if you prefer not to version these files, you can exclude them by adding this to your `.gitignore`:
```
.specstory
```
We recommend not keeping the `.specstory/cursor_rules_backups` directory under version control if you are already using git to version the `.cursor/rules` directory, and committing regularly. You can exclude it by adding this to your `.gitignore`:
```
.specstory/cursor_rules_backups
```
## Searching Your Codebase
When searching your codebase in Cursor, search results may include your previous AI coding interactions. To focus solely on your actual code files, you can exclude the AI interaction history from search results.
To exclude AI interaction history:
1. Open the "Find in Files" search in Cursor (Cmd/Ctrl + Shift + F)
2. Navigate to the "files to exclude" section
3. Add the following pattern:
```
.specstory/*
```
This will ensure your searches only return results from your working codebase files.
## Notes
- Auto-save only works when Cursor/sqlite flushes data to disk. This results in a small delay after the AI response is complete before SpecStory can save the history.
- Auto-save does not yet work on remote WSL workspaces.
## Settings
You can control auto-saving behavior in Cursor:
1. Open Cursor → Settings → VS Code Settings (Cmd/Ctrl + ,)
2. Search for "SpecStory"
3. Find "Auto Save" setting to enable/disable
Auto-save occurs when changes are detected in Cursor's sqlite database, or every 2 minutes as a safety net.

View File

@ -0,0 +1,154 @@
<!-- Generated by SpecStory -->
# Problème d'exécution du programme (2025-04-01 15:55:52)
_**User**_
problème lors de l'execution du programme
---
_**Assistant**_
Pouvez-vous me donner plus de détails sur le problème que vous rencontrez lors de l'exécution du programme ? Quel est le message d'erreur ou le comportement inattendu que vous observez ?
Je vois que vous avez un fichier `utils/__init__.py` qui contient juste la ligne `#!/usr/bin/env python3`, mais j'aurais besoin de plus d'informations sur votre programme et l'erreur spécifique pour pouvoir vous aider efficacement.
---
_**User**_
oui tu as dans le terminal line 107-115
---
_**Assistant**_
Je vois l'erreur dans le terminal. C'est une erreur d'importation relative qui dépasse le package de niveau supérieur.
Le problème se trouve dans le fichier `utils/ticket_analyzer.py` à la ligne 13 où vous essayez d'importer depuis `..agents`. Les importations relatives avec `..` ne fonctionnent que si votre module fait partie d'un package et est exécuté en tant que tel.
Pour résoudre ce problème, vous avez deux options :
1. Remplacer l'importation relative par une importation absolue :
---
Read file: utils/ticket_analyzer.py
---
Je vais maintenant corriger le problème d'importation dans le fichier ticket_analyzer.py :
---
```diff
from typing import Dict, List, Any, Optional
- from ..agents import AgentFiltreImages, AgentAnalyseImage, AgentQuestionReponse
+ from agents import AgentFiltreImages, AgentAnalyseImage, AgentQuestionReponse
class TicketAnalyzer:
```
---
Le problème a été résolu. L'erreur provenait de l'utilisation d'une importation relative (`..agents`) alors que le module n'était pas exécuté comme faisant partie d'un package.
J'ai remplacé l'importation relative par une importation absolue :
```python
from agents import AgentFiltreImages, AgentAnalyseImage, AgentQuestionReponse
```
Cette modification devrait permettre à votre programme de s'exécuter correctement maintenant. Les importations relatives avec `..` ne fonctionnent que si le module est exécuté comme partie d'un package, mais votre erreur indiquait que ce n'était pas le cas lors de l'exécution de `main.py`.
---
_**User**_
toujours des soucis
---
_**Assistant**_
Je vois qu'il y a un autre problème d'importation relative. Cette fois-ci dans le fichier `agents/agent_filtre_images.py` qui essaie d'importer `from ..llm import Pixtral`.
Corrigeons ce problème :
---
Read file: agents/agent_filtre_images.py
---
Voyons ensuite le contenu de `agents/__init__.py` pour comprendre la structure du package :
---
Read file: agents/__init__.py
---
Maintenant, examinons le problème et modifions le fichier `agent_filtre_images.py` pour corriger l'importation relative :
---
```diff
from .agent_base import Agent
- from ..llm import Pixtral
+ from llm import Pixtral
class AgentFiltreImages(Agent):
```
---
Il est possible que nous devions faire la même correction pour d'autres agents. Vérifions le fichier agent_analyse_image.py :
---
Read file: agents/agent_analyse_image.py
---
Corrigeons aussi l'importation dans ce fichier :
---
```diff
from .agent_base import Agent
- from ..llm import Pixtral
+ from llm import Pixtral
class AgentAnalyseImage(Agent):
```
---
Enfin, vérifions le dernier agent (`agent_question_reponse.py`) pour une cohérence complète :
---
Read file: agents/agent_question_reponse.py
---
Corrigeons également l'importation dans ce fichier :
---
Cancelled
---
---
---

107
README.md Normal file
View File

@ -0,0 +1,107 @@
# LLM-Ticket3
Système d'analyse de tickets de support avec LLM pour l'extraction d'informations pertinentes.
## Description
Ce projet fournit une solution pour analyser des tickets de support en utilisant des modèles de langage (LLM). Les fonctionnalités principales sont :
- Extraction de tickets depuis Odoo
- Filtrage des images pertinentes
- Analyse détaillée des images en contexte
- Extraction des questions et réponses des échanges
- Génération d'un rapport unifié au format JSON/Markdown
## Architecture
Le projet suit une architecture modulaire avec :
- **agents/** : Modules pour les différents agents spécialisés
- `agent_base.py` : Classe abstraite pour tous les agents
- `agent_filtre_images.py` : Agent pour filtrer les images pertinentes
- `agent_analyse_image.py` : Agent pour analyser les images en détail
- `agent_question_reponse.py` : Agent pour extraire questions/réponses
- **llm/** : Modules pour les interfaces avec les LLM
- `llm_base.py` : Classe abstraite pour les LLM
- `mistral.py` : Implémentation pour l'API Mistral
- `pixtral.py` : Implémentation pour l'API Pixtral (avec capacités vision)
- **utils/** : Utilitaires pour le traitement des tickets
- `ticket_manager.py` : Gestion de l'extraction des tickets depuis Odoo
- `ticket_analyzer.py` : Coordination de l'analyse par les agents
- **main.py** : Script principal
## Installation
1. Clonez le dépôt :
```bash
git clone https://github.com/votre-user/llm-ticket3.git
cd llm-ticket3
```
2. Installez les dépendances :
```bash
pip install -r requirements.txt
```
3. Copiez et configurez le fichier de configuration :
```bash
cp config.json.example config.json
# Éditez config.json avec vos informations
```
## Utilisation
### Analyser un ticket
```bash
python main.py T12345 --config config.json --output ./resultats
```
Où :
- `T12345` est le code du ticket à analyser
- `--config` (optionnel) spécifie le chemin du fichier de configuration
- `--output` (optionnel) spécifie le répertoire de sortie
### Analyser un ticket déjà extrait
Si vous avez déjà extrait les données d'un ticket et souhaitez uniquement refaire l'analyse :
```bash
python main.py T12345 --skip-extraction
```
## Sortie
L'analyse génère :
1. `ticket_T12345/rapport/ticket_analysis.json` - Données d'analyse complètes en JSON
2. `ticket_T12345/rapport/ticket_analysis.md` - Rapport formaté en Markdown
3. `ticket_T12345/questions_reponses.md` - Tableau des questions et réponses
## Dépendances
- Python 3.8+
- requests
- mistralai (API client)
## Configuration
Le fichier `config.json` contient :
```json
{
"odoo": {
"url": "https://example.odoo.com",
"db": "database_name",
"username": "user@example.com",
"api_key": "your_odoo_api_key_or_password"
},
"llm": {
"api_key": "your_mistral_api_key"
},
"output_dir": "output"
}
```

Binary file not shown.

14
agents/__init__.py Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from .agent_base import Agent
from .agent_filtre_images import AgentFiltreImages
from .agent_analyse_image import AgentAnalyseImage
from .agent_question_reponse import AgentQuestionReponse
__all__ = [
'Agent',
'AgentFiltreImages',
'AgentAnalyseImage',
'AgentQuestionReponse'
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,91 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Agent pour analyser en détail les images pertinentes dans un contexte de support technique.
"""
import os
from typing import Dict, Any, Optional
from .agent_base import Agent
from llm import Pixtral
class AgentAnalyseImage(Agent):
"""
Agent qui analyse en détail le contenu d'une image technique.
"""
def __init__(self, api_key: Optional[str] = None):
"""
Initialise l'agent d'analyse d'images.
Args:
api_key: Clé API pour le LLM
"""
super().__init__("AgentAnalyseImage")
self.llm = Pixtral(api_key=api_key)
self.llm.system_prompt = """
Vous êtes un expert en analyse d'images techniques.
Votre rôle est d'examiner des captures d'écran ou des photos liées à des problèmes techniques et de:
1. Décrire précisément le contenu de l'image
2. Identifier les éléments techniques visibles (erreurs, interfaces, configurations)
3. Extraire tout texte visible (messages d'erreur, logs, indicateurs)
Soyez précis et factuel dans votre analyse.
"""
def executer(self, image_path: str, contexte: Optional[str] = None) -> Dict[str, Any]:
"""
Analyse en détail une image technique.
Args:
image_path: Chemin vers l'image à analyser
contexte: Contexte optionnel du ticket pour une analyse plus pertinente
Returns:
Dictionnaire contenant l'analyse de l'image
"""
# Vérifier que l'image existe
if not os.path.exists(image_path):
erreur = f"L'image {image_path} n'existe pas"
self.ajouter_historique("analyse_image_erreur", image_path, erreur)
return {"error": erreur}
# Construire le prompt pour l'analyse
if contexte:
prompt = f"""
Analysez cette image dans le contexte suivant:
{contexte}
Décrivez précisément:
1. Ce que vous voyez dans l'image
2. Les éléments techniques visibles
3. Tout texte visible (messages d'erreur, logs, etc.)
4. Les indices sur le problème potentiel
"""
else:
prompt = """
Analysez cette image en détail et décrivez ce que vous voyez.
Identifiez tout problème technique visible, messages d'erreur, et éléments importants.
"""
# Enregistrer l'action dans l'historique
self.ajouter_historique("analyse_image", image_path, "Analyse en cours...")
# Appel au LLM
try:
resultat = self.llm.analyze_image(image_path, prompt)
# Vérifier si l'appel a réussi
if "error" in resultat:
self.ajouter_historique("analyse_image_erreur", image_path, resultat["error"])
return resultat
self.ajouter_historique("analyse_image_resultat", "Analyse terminée",
resultat.get("content", "")[:200])
return resultat
except Exception as e:
erreur = f"Erreur lors de l'analyse: {str(e)}"
self.ajouter_historique("analyse_image_erreur", image_path, erreur)
return {"error": erreur}

174
agents/agent_base.py Normal file
View File

@ -0,0 +1,174 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Module définissant la classe abstraite Agent qui servira de base pour tous les agents
spécialisés du système d'analyse de tickets.
"""
import os
import json
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Dict, List, Any, Optional
class Agent(ABC):
"""
Classe abstraite définissant l'interface commune de tous les agents.
"""
def __init__(self, nom: str):
"""
Initialise un agent avec son nom et son historique d'actions.
Args:
nom: Nom de l'agent
"""
self.nom: str = nom
self.historique: List[Dict[str, Any]] = []
self.llm_params: Dict[str, Any] = {}
self.llm = None
def ajouter_historique(self, action: str, input_data: Any, output_data: Any) -> None:
"""
Ajoute une entrée dans l'historique de l'agent.
Args:
action: Type d'action réalisée
input_data: Données d'entrée (limitées pour économiser l'espace)
output_data: Données de sortie (limitées pour économiser l'espace)
"""
self.historique.append({
"timestamp": datetime.now().isoformat(),
"action": action,
"input": str(input_data)[:500], # Limitation pour éviter des historiques trop volumineux
"output": str(output_data)[:500] # Limitation pour éviter des historiques trop volumineux
})
def obtenir_historique(self) -> List[Dict[str, Any]]:
"""
Retourne l'historique complet de l'agent.
Returns:
Liste d'actions avec leurs timestamp et données
"""
return self.historique
def sauvegarder_historique(self, chemin_fichier: str) -> bool:
"""
Sauvegarde l'historique de l'agent dans un fichier JSON.
Args:
chemin_fichier: Chemin sauvegarder le fichier JSON
Returns:
True si la sauvegarde a réussi, False sinon
"""
try:
# Créer le répertoire parent si nécessaire
os.makedirs(os.path.dirname(chemin_fichier), exist_ok=True)
with open(chemin_fichier, 'w', encoding='utf-8') as f:
json.dump(self.historique, f, ensure_ascii=False, indent=2)
return True
except Exception as e:
print(f"Erreur lors de la sauvegarde de l'historique: {e}")
return False
def configurer_llm(self, **parametres: Any) -> None:
"""
Configure les paramètres du LLM associé à l'agent.
Args:
**parametres: Paramètres à configurer (température, modèle, etc.)
"""
if self.llm is None:
raise ValueError("Aucun LLM associé à cet agent")
# Enregistrer les paramètres modifiés
self.llm_params.update(parametres)
# Appliquer les paramètres au LLM
for param, valeur in parametres.items():
if hasattr(self.llm, param):
setattr(self.llm, param, valeur)
else:
print(f"Avertissement: Le paramètre '{param}' n'existe pas dans le LLM")
# Ajouter à l'historique
self.ajouter_historique("configuration_llm",
f"Paramètres: {parametres}",
f"Paramètres actuels: {self.obtenir_parametres_llm()}")
def obtenir_parametres_llm(self) -> Dict[str, Any]:
"""
Obtient les paramètres actuels du LLM associé à l'agent.
Returns:
Dictionnaire des paramètres actuels du LLM
"""
if self.llm is None:
return {}
# Paramètres à récupérer (étendre selon vos besoins)
params_keys = [
"model", "temperature", "max_tokens", "top_p",
"frequency_penalty", "presence_penalty", "system_prompt"
]
# Extraire les valeurs des paramètres
params = {}
for key in params_keys:
if hasattr(self.llm, key):
params[key] = getattr(self.llm, key)
return params
def generer_rapport_parametres(self) -> Dict[str, Any]:
"""
Génère un rapport des paramètres utilisés par l'agent.
Returns:
Dictionnaire contenant les informations sur les paramètres
"""
return {
"agent": self.nom,
"llm_type": self.llm.__class__.__name__ if self.llm else "Aucun",
"parametres": self.obtenir_parametres_llm(),
"parametres_modifies": self.llm_params
}
def appliquer_parametres_globaux(self, params_globaux: Dict[str, Any]) -> None:
"""
Applique des paramètres globaux au LLM de l'agent.
Les paramètres spécifiques déjà définis ont priorité sur les globaux.
Args:
params_globaux: Dictionnaire de paramètres globaux à appliquer
"""
if self.llm is None:
return
# Filtrer les paramètres applicables
params_a_appliquer = {}
for param, valeur in params_globaux.items():
# Ne pas écraser les paramètres déjà définis spécifiquement pour cet agent
if param not in self.llm_params and hasattr(self.llm, param):
params_a_appliquer[param] = valeur
# Appliquer les paramètres filtrés
if params_a_appliquer:
self.configurer_llm(**params_a_appliquer)
self.ajouter_historique("application_params_globaux",
f"Paramètres globaux appliqués: {params_a_appliquer}",
f"Paramètres actuels: {self.obtenir_parametres_llm()}")
@abstractmethod
def executer(self, *args, **kwargs) -> Dict[str, Any]:
"""
Méthode abstraite que chaque agent concret doit implémenter.
Cette méthode exécute la fonction principale de l'agent.
Returns:
Dictionnaire contenant les résultats de l'exécution
"""
pass

View File

@ -0,0 +1,169 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Agent pour filtrer les images non pertinentes dans un contexte de support technique.
"""
import os
import json
import re
from typing import Dict, Any, Optional
from .agent_base import Agent
from llm import Pixtral
class AgentFiltreImages(Agent):
"""
Agent qui détermine si une image est pertinente dans un contexte de support technique.
Filtre les logos, signatures, icônes, etc.
"""
def __init__(self, api_key: Optional[str] = None):
"""
Initialise l'agent de filtrage d'images.
Args:
api_key: Clé API pour le LLM
"""
super().__init__("AgentFiltreImages")
self.llm = Pixtral(api_key=api_key)
# Configuration par défaut du LLM
default_system_prompt = """
Vous êtes un expert en analyse d'images techniques. Votre mission est de déterminer
si une image est pertinente dans un contexte de support technique ou non.
Images PERTINENTES:
- Captures d'écran montrant des problèmes, erreurs, bugs
- Photos d'équipements avec problèmes visibles
- Schémas techniques ou diagrammes
- Graphiques de données techniques
Images NON PERTINENTES:
- Logos d'entreprise
- Signatures ou avatars
- Icônes ou boutons isolés
- Bannières décoratives, séparateurs
- Images génériques sans information technique
"""
self.configurer_llm(
system_prompt=default_system_prompt,
temperature=0.2, # Basse température pour des réponses précises
max_tokens=500 # Réponses courtes pour le filtrage
)
def executer(self, image_path: str) -> Dict[str, Any]:
"""
Détermine si une image est pertinente pour l'analyse technique.
Args:
image_path: Chemin vers l'image à analyser
Returns:
Dictionnaire contenant le résultat du filtrage avec au minimum
{'pertinente': True/False}
"""
# Vérifier que l'image existe
if not os.path.exists(image_path):
erreur = f"L'image {image_path} n'existe pas"
self.ajouter_historique("filtre_image_erreur", image_path, erreur)
return {"pertinente": False, "error": erreur}
# Construire le prompt pour la classification
prompt = """
Analysez cette image et déterminez si elle est PERTINENTE ou NON PERTINENTE dans un contexte de support technique.
Répondez au format JSON suivant:
{
"pertinente": true/false,
"type_image": "capture_ecran|photo_equipement|schema|graphique|logo|icone|decorative|autre",
"description": "Brève description du contenu",
"confiance": "valeur de 0 à 100",
"justification": "Explication de votre décision"
}
"""
# Enregistrer l'action dans l'historique
self.ajouter_historique("filtre_image", image_path, "Filtrage en cours...")
# Appel au LLM
try:
# Pour les cas complexes, augmenter légèrement la température
if "complexe" in image_path or os.path.getsize(image_path) > 500000:
self.configurer_llm(temperature=0.4)
self.ajouter_historique("ajustement_temperature", "Augmentation pour image complexe", "temperature=0.4")
resultat_brut = self.llm.analyze_image(image_path, prompt)
# Essayer d'extraire le JSON de la réponse
try:
content = resultat_brut.get("content", "")
# Si le contenu est déjà au format JSON correctement formaté
if content.strip().startswith("{") and content.strip().endswith("}"):
try:
resultat_json = json.loads(content)
except json.JSONDecodeError:
# Si le contenu a la structure JSON mais ne peut pas être décodé
# nettoyer et réessayer
content_cleaned = content.replace("```json", "").replace("```", "").strip()
resultat_json = json.loads(content_cleaned)
else:
# Chercher un bloc JSON dans la réponse
json_match = re.search(r'```json\s*(.*?)\s*```', content, re.DOTALL)
if json_match:
json_str = json_match.group(1).strip()
else:
# Essayer d'extraire un objet JSON sans les blocs de code
json_match = re.search(r'(\{.*?\})', content, re.DOTALL)
if json_match:
json_str = json_match.group(1).strip()
else:
# Sinon, prendre tout le contenu comme JSON potentiel
json_str = content.strip()
# Parser le JSON
resultat_json = json.loads(json_str)
# S'assurer que le champ 'pertinente' existe
if "pertinente" not in resultat_json:
resultat_json["pertinente"] = False
resultat_json["error"] = "Format de réponse incorrect"
# Ajouter les paramètres LLM utilisés
resultat_json["parametres_llm"] = self.generer_rapport_parametres()
self.ajouter_historique("filtre_image_resultat", "Filtrage terminé",
f"Pertinente: {resultat_json.get('pertinente', False)}")
return resultat_json
except Exception as e:
# Pour les modules en mode simulation, utiliser directement la réponse
if "pertinente" in resultat_brut.get("content", ""):
try:
resultat_json = json.loads(resultat_brut.get("content", ""))
resultat_json["parametres_llm"] = self.generer_rapport_parametres()
self.ajouter_historique("filtre_image_resultat", "Filtrage terminé",
f"Pertinente: {resultat_json.get('pertinente', False)}")
return resultat_json
except:
pass
# En cas d'erreur de parsing, retourner un résultat par défaut
resultat = {
"pertinente": False,
"error": f"Erreur de parsing JSON: {str(e)}",
"response_raw": resultat_brut.get("content", "")[:200],
"parametres_llm": self.generer_rapport_parametres()
}
self.ajouter_historique("filtre_image_parsing_erreur", "Erreur de parsing", str(e))
return resultat
except Exception as e:
erreur = f"Erreur lors du filtrage: {str(e)}"
self.ajouter_historique("filtre_image_erreur", image_path, erreur)
return {
"pertinente": False,
"error": erreur,
"parametres_llm": self.generer_rapport_parametres()
}

View File

@ -0,0 +1,474 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Agent pour extraire et structurer les questions et réponses des tickets de support.
"""
import os
import re
from typing import Dict, List, Any, Optional
from .agent_base import Agent
from llm import Mistral
from post_process import normaliser_accents
class AgentQuestionReponse(Agent):
"""
Agent qui extrait les questions et réponses des messages de support.
"""
def __init__(self, api_key: Optional[str] = None):
"""
Initialise l'agent d'extraction de questions et réponses.
Args:
api_key: Clé API pour le LLM
"""
super().__init__("AgentQuestionReponse")
self.llm = Mistral(api_key=api_key)
# Configuration par défaut du LLM
default_system_prompt = """
Vous êtes un expert en analyse de conversations de support technique.
Votre mission est d'identifier avec précision:
1. Le rôle de chaque intervenant (client ou support technique)
2. La nature de chaque message (question, réponse, information additionnelle)
3. Le contenu essentiel de chaque message en éliminant les formules de politesse,
signatures, mentions légales et autres éléments non pertinents
Pour l'identification client/support:
- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email
comme @cbao.fr, @odoo.com, mentions "support technique", etc.
- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions
Pour la classification en question/réponse:
- Questions: Demandes explicites (avec "?"), demandes implicites de résolution
de problèmes, descriptions de bugs ou dysfonctionnements
- Réponses: Explications techniques, solutions proposées, instructions fournies
par le support
Concentrez-vous uniquement sur le contenu technique utile en ignorant tous les
éléments superflus qui n'apportent pas d'information sur le problème ou sa solution.
"""
self.configurer_llm(
system_prompt=default_system_prompt,
temperature=0.3, # Basse température pour une extraction précise
max_tokens=2000
)
def _nettoyer_contenu(self, texte: str) -> str:
"""
Nettoie le contenu en supprimant signatures, mentions légales, etc.
Args:
texte: Texte brut à nettoyer
Returns:
Texte nettoyé des éléments non pertinents
"""
# Si l'entrée n'est pas une chaîne, convertir en chaîne ou retourner vide
if not isinstance(texte, str):
if texte is None:
return ""
try:
texte = str(texte)
except:
return ""
# Détection de contenu HTML
contient_html = bool(re.search(r'<[a-z]+[^>]*>', texte, re.IGNORECASE))
# Supprimer les balises HTML - approche plus robuste
try:
# Première passe - balises standard
texte_nettoye = re.sub(r'</?[a-z]+[^>]*>', ' ', texte, flags=re.IGNORECASE)
# Deuxième passe - balises restantes, y compris les mal formées
texte_nettoye = re.sub(r'<[^>]*>', ' ', texte_nettoye)
# Troisième passe pour les balises qui pourraient avoir échappé
texte_nettoye = re.sub(r'<[^>]*$', ' ', texte_nettoye) # Balises incomplètes à la fin
except Exception as e:
self.ajouter_historique("erreur_nettoyage_html", "Échec", str(e))
texte_nettoye = texte
# Remplacer les références aux images
texte_nettoye = re.sub(r'\[Image:[^\]]+\]', '[Image]', texte_nettoye)
texte_nettoye = re.sub(r'<img[^>]+>', '[Image]', texte_nettoye, flags=re.IGNORECASE)
# Supprimer les éléments courants non pertinents
patterns_a_supprimer = [
r'Cordialement,[\s\S]*?$',
r'Bien cordialement,[\s\S]*?$',
r'Bonne réception[\s\S]*?$',
r'À votre disposition[\s\S]*?$',
r'Support technique[\s\S]*?$',
r'L\'objectif du Support Technique[\s\S]*?$',
r'Notre service est ouvert[\s\S]*?$',
r'Dès réception[\s\S]*?$',
r'Confidentialité[\s\S]*?$',
r'Ce message électronique[\s\S]*?$',
r'Droit à la déconnexion[\s\S]*?$',
r'Afin d\'assurer une meilleure traçabilité[\s\S]*?$',
r'tél\s*:\s*[\d\s\+]+',
r'mobile\s*:\s*[\d\s\+]+',
r'www\.[^\s]+\.[a-z]{2,3}',
r'\*{10,}.*?\*{10,}', # Lignes de séparation avec astérisques
r'----.*?----', # Lignes de séparation avec tirets
]
for pattern in patterns_a_supprimer:
texte_nettoye = re.sub(pattern, '', texte_nettoye, flags=re.IGNORECASE)
# Supprimer les lignes multiples vides
texte_nettoye = re.sub(r'\n\s*\n', '\n', texte_nettoye)
# Supprimer les espaces multiples
texte_nettoye = re.sub(r'\s+', ' ', texte_nettoye)
# Convertir les entités HTML
html_entities = {
'&nbsp;': ' ', '&lt;': '<', '&gt;': '>', '&amp;': '&',
'&quot;': '"', '&apos;': "'", '&euro;': '', '&copy;': '©',
'&reg;': '®', '&eacute;': 'é', '&egrave;': 'è', '&agrave;': 'à',
'&ccedil;': 'ç', '&ecirc;': 'ê', '&acirc;': 'â', '&icirc;': 'î',
'&ocirc;': 'ô', '&ucirc;': 'û'
}
for entity, char in html_entities.items():
texte_nettoye = texte_nettoye.replace(entity, char)
# Normaliser les caractères accentués
try:
texte_nettoye = normaliser_accents(texte_nettoye)
except Exception as e:
self.ajouter_historique("erreur_normalisation_accents", "Échec", str(e))
return texte_nettoye.strip()
def _detecter_role(self, message: Dict[str, Any]) -> str:
"""
Détecte si un message provient du client ou du support.
Args:
message: Dictionnaire contenant les informations du message
Returns:
"Client" ou "Support"
"""
# Vérifier le champ 'role' s'il existe déjà
if "role" in message and message["role"] in ["Client", "Support"]:
return message["role"]
# Indices de support dans l'email
domaines_support = ["@cbao.fr", "@odoo.com", "support@", "ticket.support"]
indices_nom_support = ["support", "cbao", "technique", "odoo"]
email = message.get("email_from", "").lower()
# Nettoyer le format "Nom <email@domaine.com>"
if "<" in email and ">" in email:
match = re.search(r'<([^>]+)>', email)
if match:
email = match.group(1).lower()
# Vérifier le domaine email
if any(domaine in email for domaine in domaines_support):
return "Support"
# Vérifier le nom d'auteur
auteur = ""
if "author_id" in message and isinstance(message["author_id"], list) and len(message["author_id"]) > 1:
auteur = str(message["author_id"][1]).lower()
elif "auteur" in message:
auteur = str(message["auteur"]).lower()
if any(indice in auteur for indice in indices_nom_support):
return "Support"
# Par défaut, considérer comme client
return "Client"
def _analyser_messages_llm(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Utilise le LLM pour analyser les messages et en extraire les questions/réponses.
Args:
messages: Liste des messages à analyser
Returns:
Analyse des messages et paires de questions/réponses
"""
self.ajouter_historique("analyse_messages_llm", f"{len(messages)} messages", "Analyse en cours...")
# Vérifier s'il y a des messages à analyser
if len(messages) == 0:
self.ajouter_historique("analyse_messages_llm_erreur", "Aucun message", "La liste des messages est vide")
return {
"success": False,
"error": "Aucun message à analyser",
"messages_analyses": [],
"paires_qr": []
}
# Vérifier si nous n'avons qu'un seul message (probablement le message du système)
if len(messages) == 1:
message_unique = messages[0]
role = message_unique.get("role", "")
# Si c'est un message système, nous n'avons pas de vraie conversation
if role == "system":
self.ajouter_historique("analyse_messages_llm_erreur", "Un seul message système",
"Pas de conversation à analyser")
return {
"success": True,
"messages_analyses": [],
"paires_qr": []
}
try:
# Préparation des messages pour le LLM
messages_for_llm = []
for i, msg in enumerate(messages):
# Inclure uniquement les messages de type Client ou Support
role = msg.get("role", "")
if role not in ["Client", "Support"]:
continue
# Formater le message pour le LLM
messages_for_llm.append({
"numero": i + 1,
"role": role,
"date": msg.get("date", ""),
"contenu": msg.get("body", "")
})
# S'il n'y a aucun message Client ou Support
if not messages_for_llm:
self.ajouter_historique("analyse_messages_llm_erreur", "Aucun message pertinent",
"Pas de message Client ou Support à analyser")
return {
"success": True,
"messages_analyses": [],
"paires_qr": []
}
# Utiliser la nouvelle méthode analyze_messages_json de Mistral
resultat = self.llm.analyze_messages_json(messages_for_llm)
if "error" in resultat:
self.ajouter_historique("analyse_messages_llm_erreur", "Erreur API", resultat["error"])
return {
"success": False,
"error": resultat["error"],
"messages_analyses": [],
"paires_qr": []
}
contenu = resultat.get("content", "")
self.ajouter_historique("analyse_messages_llm_resultat", "Analyse complétée", contenu[:200] + "...")
# Traiter la réponse pour extraire les messages analysés
messages_analyses = []
pattern_messages = r"MESSAGE (\d+):\s*- Rôle: (Client|Support)\s*- Type: (Question|Réponse|Information)\s*- Contenu essentiel: (.*?)(?=MESSAGE \d+:|PAIRE \d+:|$)"
for match in re.finditer(pattern_messages, contenu, re.DOTALL):
num = int(match.group(1))
role = match.group(2)
type_msg = match.group(3)
contenu_essentiel = match.group(4).strip()
# Trouver le message correspondant
msg_idx = num - 1
msg_id = ""
msg_date = ""
if 0 <= msg_idx < len(messages_for_llm):
original_idx = messages_for_llm[msg_idx]["numero"] - 1
if 0 <= original_idx < len(messages):
msg_id = messages[original_idx].get("id", "") or messages[original_idx].get("ID", "")
msg_date = messages[original_idx].get("date", "")
messages_analyses.append({
"id": msg_id,
"date": msg_date,
"role": role,
"type": type_msg,
"contenu": contenu_essentiel
})
# Extraire les paires QR
paires_qr = []
pattern_paires = r"PAIRE (\d+):\s*- Question \((Client|Support)\): (.*?)(?:\s*- Réponse \((Client|Support)\): (.*?))?(?=PAIRE \d+:|$)"
for match in re.finditer(pattern_paires, contenu, re.DOTALL):
num = match.group(1)
q_role = match.group(2)
question = match.group(3).strip()
r_role = match.group(4) if match.group(4) else ""
reponse = match.group(5).strip() if match.group(5) else ""
paires_qr.append({
"numero": num,
"question": {
"role": q_role,
"contenu": question
},
"reponse": {
"role": r_role,
"contenu": reponse
} if reponse else None
})
# Ne pas générer de paires artificielles si le LLM n'en a pas détecté
if not paires_qr:
self.ajouter_historique("analyse_paires_qr", "Aucune paire", "Le LLM n'a détecté aucune paire question/réponse")
return {
"success": True,
"messages_analyses": messages_analyses,
"paires_qr": paires_qr
}
except Exception as e:
self.ajouter_historique("analyse_messages_llm_erreur", f"{len(messages)} messages", str(e))
return {
"success": False,
"error": str(e),
"messages_analyses": [],
"paires_qr": []
}
def _generer_tableau_markdown(self, paires_qr: List[Dict[str, Any]]) -> str:
"""
Génère un tableau Markdown avec les questions et réponses.
Args:
paires_qr: Liste de paires question/réponse
Returns:
Tableau Markdown formaté
"""
# Créer le tableau
markdown = ["# Analyse des Questions et Réponses\n"]
markdown.append("| Question | Réponse |")
markdown.append("|---------|---------|")
for paire in paires_qr:
question = paire.get("question", {})
reponse = paire.get("reponse", {})
q_role = question.get("role", "Client")
q_contenu = question.get("contenu", "")
# Normaliser le contenu des questions pour corriger les accents
q_contenu = normaliser_accents(q_contenu)
if reponse:
r_role = reponse.get("role", "Support")
r_contenu = reponse.get("contenu", "")
# Normaliser le contenu des réponses pour corriger les accents
r_contenu = normaliser_accents(r_contenu)
markdown.append(f"| **{q_role}**: {q_contenu} | **{r_role}**: {r_contenu} |")
else:
markdown.append(f"| **{q_role}**: {q_contenu} | *Pas de réponse* |")
# Ajouter les informations sur les paramètres LLM utilisés
markdown.append("\n## Paramètres LLM utilisés\n")
params = self.generer_rapport_parametres()
markdown.append(f"- **Type de LLM**: {params['llm_type']}")
markdown.append(f"- **Modèle**: {params['parametres'].get('model', 'Non spécifié')}")
markdown.append(f"- **Température**: {params['parametres'].get('temperature', 'Non spécifiée')}")
markdown.append(f"- **Tokens max**: {params['parametres'].get('max_tokens', 'Non spécifié')}")
if params['parametres_modifies']:
markdown.append("\n**Paramètres modifiés durant l'analyse:**")
for param, valeur in params['parametres_modifies'].items():
if param != 'system_prompt': # Exclure le system_prompt car trop long
markdown.append(f"- **{param}**: {valeur}")
# Normaliser tout le contenu markdown final pour s'assurer que tous les accents sont corrects
return normaliser_accents("\n".join(markdown))
def executer(self, messages_data: List[Dict[str, Any]], output_path: Optional[str] = None) -> Dict[str, Any]:
"""
Analyse les messages pour extraire les questions et réponses.
Args:
messages_data: Liste des messages du ticket
output_path: Chemin sauvegarder le tableau Markdown (optionnel)
Returns:
Résultats de l'analyse avec le tableau Markdown
"""
self.ajouter_historique("debut_execution", f"{len(messages_data)} messages", "Début de l'analyse")
try:
# Préparation des messages
messages_prepares = []
for msg in messages_data:
# Nettoyer le contenu
contenu = msg.get("body", "") or msg.get("contenu", "")
contenu_nettoye = self._nettoyer_contenu(contenu)
# Détecter le rôle
role = self._detecter_role(msg)
# Ajouter le message préparé si non vide après nettoyage
if contenu_nettoye.strip():
messages_prepares.append({
"id": msg.get("id", "") or msg.get("ID", ""),
"date": msg.get("date", ""),
"author_id": msg.get("author_id", []),
"email_from": msg.get("email_from", ""),
"role": role,
"body": contenu_nettoye
})
# Trier par date si disponible
messages_prepares.sort(key=lambda x: x.get("date", ""))
# Analyser avec le LLM
resultats_analyse = self._analyser_messages_llm(messages_prepares)
# Générer le tableau Markdown avec normalisation des accents
tableau_md = self._generer_tableau_markdown(resultats_analyse.get("paires_qr", []))
# Dernière vérification pour s'assurer que les accents sont normalisés
tableau_md = normaliser_accents(tableau_md)
# Sauvegarder le tableau si un chemin est fourni
if output_path:
try:
# Créer le dossier parent si nécessaire
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(tableau_md)
self.ajouter_historique("sauvegarde_tableau", output_path, "Tableau sauvegardé")
except Exception as e:
self.ajouter_historique("erreur_sauvegarde", output_path, str(e))
# Préparer le résultat
resultat = {
"success": resultats_analyse.get("success", False),
"messages_analyses": resultats_analyse.get("messages_analyses", []),
"paires_qr": resultats_analyse.get("paires_qr", []),
"nb_questions": len(resultats_analyse.get("paires_qr", [])),
"nb_reponses": sum(1 for p in resultats_analyse.get("paires_qr", []) if p.get("reponse")),
"tableau_md": tableau_md,
"parametres_llm": self.generer_rapport_parametres()
}
self.ajouter_historique("fin_execution", "Analyse terminée",
f"{resultat['nb_questions']} questions, {resultat['nb_reponses']} réponses")
return resultat
except Exception as e:
erreur = f"Erreur lors de l'analyse: {str(e)}"
self.ajouter_historique("erreur_execution", "Exception", erreur)
return {
"success": False,
"error": erreur
}

12
config.json Normal file
View File

@ -0,0 +1,12 @@
{
"odoo": {
"url": "https://odoo.cbao.fr",
"db": "production_cbao",
"username": "fernand@cbao.fr",
"api_key": "Lestat66!"
},
"llm": {
"api_key": "your_mistral_api_key"
},
"output_dir": "output"
}

12
config.json.example Normal file
View File

@ -0,0 +1,12 @@
{
"odoo": {
"url": "https://example.odoo.com",
"db": "database_name",
"username": "user@example.com",
"api_key": "your_odoo_api_key_or_password"
},
"llm": {
"api_key": "your_mistral_api_key"
},
"output_dir": "output"
}

8
llm/__init__.py Normal file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from .llm_base import LLM
from .mistral import Mistral
from .pixtral import Pixtral
__all__ = ['LLM', 'Mistral', 'Pixtral']

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

52
llm/llm_base.py Normal file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Module définissant la classe abstraite de base pour les modèles LLM.
"""
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional
class LLM(ABC):
"""
Classe abstraite définissant l'interface commune pour tous les modèles LLM.
"""
def __init__(self, api_key: Optional[str] = None):
"""
Initialise l'instance LLM avec une clé API.
Args:
api_key: Clé API pour accéder au service LLM
"""
self.api_key = api_key
self.system_prompt = ""
@abstractmethod
def generate_response(self, prompt: str, **kwargs) -> Dict[str, Any]:
"""
Génère une réponse à partir d'un prompt.
Args:
prompt: Texte d'entrée pour la génération
**kwargs: Options supplémentaires
Returns:
Dictionnaire contenant la réponse et les métadonnées
"""
pass
@abstractmethod
def analyze_image(self, image_path: str, prompt: str, **kwargs) -> Dict[str, Any]:
"""
Analyse une image selon un prompt.
Args:
image_path: Chemin vers l'image à analyser
prompt: Instructions pour l'analyse
**kwargs: Options supplémentaires
Returns:
Dictionnaire contenant l'analyse et les métadonnées
"""
pass

243
llm/mistral.py Normal file
View File

@ -0,0 +1,243 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Module implémentant la classe Mistral pour interagir avec l'API Mistral AI.
"""
import os
import requests
import json
from typing import Dict, List, Any, Optional
from .llm_base import LLM
class Mistral(LLM):
"""
Implémentation de l'interface LLM pour Mistral AI.
"""
API_URL = "https://api.mistral.ai/v1/chat/completions"
DEFAULT_MODEL = "mistral-medium"
def __init__(self, api_key: Optional[str] = None):
"""
Initialise le client Mistral.
Args:
api_key: Clé API Mistral (si None, utilise la variable d'environnement MISTRAL_API_KEY)
"""
api_key = api_key or os.environ.get("MISTRAL_API_KEY")
super().__init__(api_key)
# Configuration par défaut
self.model = self.DEFAULT_MODEL
self.temperature = 0.7
self.max_tokens = 2000
self.top_p = 1.0
# Prompt système par défaut
self.system_prompt = (
"Vous êtes un expert en support technique pour les logiciels spécialisés. "
"Analysez précisément les problèmes techniques décrits."
)
# État d'initialisation
self.initialized = True
self.headers = None
def initialize(self) -> None:
"""
Initialise la connexion à l'API.
"""
if not self.api_key:
print("Mode simulation: Aucune clé API nécessaire.")
self.headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
self.initialized = True
def validate_and_parse_json(self, messages_data: Any) -> List[Dict[str, Any]]:
"""
Valide et analyse les données JSON.
Args:
messages_data: Données JSON en string ou déjà décodées
Returns:
Liste d'objets messages validés
"""
if isinstance(messages_data, str):
try:
messages = json.loads(messages_data)
except json.JSONDecodeError:
return [{"error": "Format JSON invalide", "content": messages_data}]
else:
messages = messages_data
if not isinstance(messages, list):
return [{"error": "Le format attendu est une liste de messages", "content": str(messages)}]
return messages
def generate_response(self, prompt: str, **kwargs) -> Dict[str, Any]:
"""
Génère une réponse textuelle à partir d'un prompt.
Args:
prompt: Texte d'entrée pour la génération
**kwargs: Options supplémentaires
Returns:
Dictionnaire contenant la réponse et les métadonnées
"""
print("Mode simulation: Génération de réponse textuelle")
# Simulation d'une réponse
response = {
"content": f"Je suis un modèle simulé. Voici ma réponse à votre prompt: {prompt[:100]}...",
"model": self.model,
"usage": {
"prompt_tokens": len(prompt) // 4,
"completion_tokens": 200,
"total_tokens": len(prompt) // 4 + 200
}
}
return response
def analyze_messages_json(self, messages_json: Any, **kwargs) -> Dict[str, Any]:
"""
Analyse les messages fournis au format JSON pour extraire les questions et réponses.
Args:
messages_json: Messages au format JSON (liste d'objets ou chaîne JSON)
**kwargs: Options supplémentaires
Returns:
Analyse des messages avec identification des questions et réponses
"""
print("Mode simulation: Analyse de messages JSON")
# Valider et analyser le JSON
messages = self.validate_and_parse_json(messages_json)
if any(msg.get("error") for msg in messages):
error_msg = next((msg.get("error") for msg in messages if msg.get("error")), "Erreur de format JSON")
return {"error": error_msg, "content": ""}
# Extraire les informations du ticket et de contexte
ticket_info = next((msg for msg in messages if msg.get("id") == "ticket_info"), {})
ticket_code = ticket_info.get("code", "Inconnu")
ticket_name = ticket_info.get("name", "Ticket sans titre")
ticket_desc = ticket_info.get("description", "")
# Séparer les messages par rôle
context_msgs = [msg for msg in messages if msg.get("role") == "system" or msg.get("type") == "contexte"]
client_msgs = [msg for msg in messages if msg.get("role") == "Client"]
support_msgs = [msg for msg in messages if msg.get("role") == "Support"]
other_msgs = [msg for msg in messages if msg.get("role") not in ["system", "Client", "Support"] and msg.get("type") != "contexte"]
# Organisation des messages par ordre chronologique pour analyse
all_content_msgs = client_msgs + support_msgs + other_msgs
# Trier par date si possible
sorted_msgs = sorted(all_content_msgs, key=lambda x: x.get("date", "0"), reverse=False)
# Préparer l'analyse des messages
message_analyses = []
for i, msg in enumerate(sorted_msgs):
role = msg.get("role", "Inconnu")
msg_type = msg.get("type", "Information" if role == "Support" else "Question")
body = msg.get("body", "").strip()
if body:
message_analyses.append({
"numero": i + 1,
"role": role,
"type": msg_type,
"contenu": body[:500] # Limiter la longueur du contenu
})
# Extraire les paires question-réponse
pairs_qr = []
current_question = None
for msg in sorted_msgs:
role = msg.get("role", "Inconnu")
body = msg.get("body", "").strip()
if not body:
continue
if role == "Client" or (role not in ["Support", "system"] and not current_question):
# Nouveau client message = nouvelle question potentielle
current_question = {
"role": role,
"contenu": body
}
elif role == "Support" and current_question:
# Message de support après une question = réponse potentielle
pairs_qr.append({
"numero": len(pairs_qr) + 1,
"question": current_question,
"reponse": {
"role": role,
"contenu": body
}
})
current_question = None
# Ajouter les questions sans réponse
if current_question:
pairs_qr.append({
"numero": len(pairs_qr) + 1,
"question": current_question,
"reponse": None
})
# Générer le résultat formaté
result = f"ANALYSE DU TICKET {ticket_code}: {ticket_name}\n\n"
# Ajouter les analyses de messages
for i, msg in enumerate(message_analyses):
result += f"MESSAGE {msg['numero']}:\n"
result += f"- Rôle: {msg['role']}\n"
result += f"- Type: {msg['type']}\n"
result += f"- Contenu essentiel: {msg['contenu']}\n\n"
# Ajouter les paires question-réponse
for pair in pairs_qr:
result += f"PAIRE {pair['numero']}:\n"
result += f"- Question ({pair['question']['role']}): {pair['question']['contenu']}\n"
if pair['reponse']:
result += f"- Réponse ({pair['reponse']['role']}): {pair['reponse']['contenu']}\n\n"
else:
result += "- Réponse: Aucune réponse trouvée\n\n"
return {
"content": result,
"model": self.model,
"usage": {
"prompt_tokens": sum(len(msg.get("body", "")) // 4 for msg in messages),
"completion_tokens": len(result) // 2,
"total_tokens": sum(len(msg.get("body", "")) // 4 for msg in messages) + len(result) // 2
}
}
def analyze_image(self, image_path: str, prompt: str, **kwargs) -> Dict[str, Any]:
"""
Analyse une image (non supporté par Mistral standard).
Args:
image_path: Chemin vers l'image à analyser
prompt: Instructions pour l'analyse
**kwargs: Options supplémentaires
Returns:
Dictionnaire d'erreur (fonctionnalité non supportée)
"""
return {
"error": "L'analyse d'images n'est pas supportée par Mistral. Utilisez Pixtral."
}

245
llm/pixtral.py Normal file
View File

@ -0,0 +1,245 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Module implémentant la classe Pixtral pour l'analyse d'images via l'API Mistral AI.
"""
import os
import base64
import requests
import json
from typing import Dict, Any, Optional, List
from .llm_base import LLM
class Pixtral(LLM):
"""
Implémentation de l'interface LLM pour Pixtral (Mistral AI avec vision).
"""
API_URL = "https://api.mistral.ai/v1/chat/completions"
DEFAULT_MODEL = "pixtral-12b-2409"
def __init__(self, api_key: Optional[str] = None):
"""
Initialise le client Pixtral.
Args:
api_key: Clé API Mistral (si None, utilise la variable d'environnement MISTRAL_API_KEY)
"""
api_key = api_key or os.environ.get("MISTRAL_API_KEY")
super().__init__(api_key)
# Configuration par défaut
self.model = self.DEFAULT_MODEL
self.temperature = 0.3
self.max_tokens = 1024
self.top_p = 1.0
# Prompt système par défaut
self.system_prompt = (
"Vous êtes un expert en analyse d'images techniques. "
"Décrivez précisément ce que vous voyez et identifiez les problèmes visibles."
)
# État d'initialisation
self.initialized = True
self.headers = None
def initialize(self) -> None:
"""
Initialise la connexion à l'API.
"""
if not self.api_key:
print("Mode simulation: Aucune clé API nécessaire.")
self.headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
self.initialized = True
def _encode_image(self, image_path: str) -> str:
"""
Encode une image en base64 pour l'API.
Args:
image_path: Chemin vers l'image
Returns:
Image encodée en base64 avec le bon format URI
"""
if not os.path.isfile(image_path):
raise FileNotFoundError(f"Image non trouvée: {image_path}")
try:
with open(image_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
# Déterminer le type MIME
file_extension = os.path.splitext(image_path)[1].lower()
if file_extension in ['.jpg', '.jpeg']:
mime_type = 'image/jpeg'
elif file_extension == '.png':
mime_type = 'image/png'
elif file_extension == '.gif':
mime_type = 'image/gif'
elif file_extension == '.webp':
mime_type = 'image/webp'
else:
mime_type = 'image/jpeg' # Par défaut
return f"data:{mime_type};base64,{encoded_string}"
except Exception as e:
raise IOError(f"Erreur lors de l'encodage de l'image {image_path}: {str(e)}")
def generate_response(self, prompt: str, **kwargs) -> Dict[str, Any]:
"""
Génère une réponse textuelle à partir d'un prompt.
Args:
prompt: Texte d'entrée pour la génération
**kwargs: Options supplémentaires
Returns:
Dictionnaire contenant la réponse et les métadonnées
"""
print("Mode simulation: Génération de réponse textuelle")
# Simulation d'une réponse
response = {
"content": f"Je suis un modèle simulé. Voici ma réponse à votre prompt: {prompt[:50]}...",
"model": self.model,
"usage": {
"prompt_tokens": len(prompt) // 4,
"completion_tokens": 100,
"total_tokens": len(prompt) // 4 + 100
}
}
return response
def analyze_image(self, image_path: str, prompt: str, contexte: str = "", **kwargs) -> Dict[str, Any]:
"""
Analyse une image selon un prompt et un contexte optionnel.
Args:
image_path: Chemin vers l'image à analyser
prompt: Instructions pour l'analyse
contexte: Contexte du ticket pour contextualiser l'analyse (optionnel)
**kwargs: Options supplémentaires
Returns:
Dictionnaire contenant l'analyse et les métadonnées
"""
if not os.path.exists(image_path):
return {
"error": f"Image introuvable: {image_path}",
"content": "",
"model": self.model
}
print(f"Mode simulation: Analyse d'image {os.path.basename(image_path)}")
# Vérifier le type de fichier
try:
file_extension = os.path.splitext(image_path)[1].lower()
file_size = os.path.getsize(image_path)
if file_extension not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
return {
"error": f"Format d'image non supporté: {file_extension}",
"content": "",
"model": self.model
}
if file_size > 10 * 1024 * 1024: # 10 MB
return {
"error": f"Image trop volumineuse ({file_size/1024/1024:.2f} MB), max 10 MB",
"content": "",
"model": self.model
}
except Exception as e:
return {
"error": f"Erreur lors de la vérification de l'image: {str(e)}",
"content": "",
"model": self.model
}
# Analyser le nom du fichier et le contexte pour la simulation
filename = os.path.basename(image_path).lower()
is_logo = any(substr in filename for substr in ["logo", "signature", "icon", "avatar", "image003"])
is_screenshot = any(substr in filename for substr in ["screen", "capture", "scr", "interface", "error", "bug", "image004", "image"])
is_diagram = any(substr in filename for substr in ["diagram", "flow", "schema", "archi"])
# Si le contexte spécifie des problèmes techniques, favoriser la pertinence des captures d'écran
ticket_has_technical_issue = False
if contexte:
technical_keywords = ["problème", "erreur", "bug", "dysfonctionnement", "ne fonctionne pas", "modification"]
ticket_has_technical_issue = any(kw in contexte.lower() for kw in technical_keywords)
# Simuler une réponse d'analyse d'image
if is_logo or (not is_screenshot and not is_diagram):
# Simuler une image non pertinente
content = json.dumps({
"pertinente": False,
"type_image": "logo" if is_logo else "autre",
"description": "Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique.",
"confiance": 90,
"justification": "L'image ne contient pas d'éléments techniques utiles pour résoudre un problème."
}, indent=2)
else:
# Simuler une image pertinente (capture d'écran ou diagramme)
image_type = "capture_ecran" if is_screenshot else "schema" if is_diagram else "autre"
description = ""
if is_screenshot:
description = "Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage."
if ticket_has_technical_issue and "image004" in filename:
description = "Capture d'écran montrant l'interface de gestion des centrales d'enrobage. On peut voir un formulaire avec le champ de nom et une flèche à droite qui permet de modifier certaines propriétés."
elif is_diagram:
description = "Schéma technique montrant l'architecture ou le flux de données du système."
else:
description = "Image technique liée au contexte du ticket."
content = json.dumps({
"pertinente": True,
"type_image": image_type,
"description": description,
"confiance": 85,
"justification": "L'image montre clairement une interface utilisateur avec des fonctionnalités techniques liées au problème décrit dans le ticket."
}, indent=2)
return {
"content": content,
"model": self.model,
"usage": {
"prompt_tokens": len(prompt) // 4 + (len(contexte) // 8 if contexte else 0),
"completion_tokens": 150,
"total_tokens": len(prompt) // 4 + (len(contexte) // 8 if contexte else 0) + 150
},
"image_analyzed": os.path.basename(image_path)
}
def analyze_images_batch(self, image_paths: List[str], prompt: str, contexte: str = "", **kwargs) -> List[Dict[str, Any]]:
"""
Analyse un lot d'images en une seule fois.
Args:
image_paths: Liste des chemins vers les images à analyser
prompt: Instructions pour l'analyse
contexte: Contexte du ticket (optionnel)
**kwargs: Options supplémentaires
Returns:
Liste de dictionnaires contenant les analyses
"""
results = []
for image_path in image_paths:
result = self.analyze_image(image_path, prompt, contexte, **kwargs)
results.append({
"image_path": image_path,
"result": result
})
return results

400
main.py Normal file
View File

@ -0,0 +1,400 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script principal pour l'analyse de tickets de support.
Ce script coordonne l'extraction de données depuis Odoo et l'analyse avec les agents LLM.
"""
import os
import json
import argparse
import subprocess
import shutil
import re
from typing import Dict, List, Any, Optional
from utils import TicketAnalyzer, TicketManager
from post_process import transformer_messages, corriger_json_accents, corriger_markdown_accents, reparer_ticket
def charger_config(config_path: str) -> Dict[str, Any]:
"""
Charge la configuration depuis un fichier JSON.
Args:
config_path: Chemin vers le fichier de configuration
Returns:
Dictionnaire de configuration
"""
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
return config
except Exception as e:
print(f"Erreur lors du chargement de la configuration: {e}")
# Configuration par défaut minimale
return {
"odoo": {
"url": "https://example.odoo.com",
"db": "db_name",
"username": "user@example.com",
"api_key": "your_api_key"
},
"llm": {
"api_key": "your_mistral_api_key"
},
"output_dir": "output"
}
def extraire_ticket(config: Dict[str, Any], ticket_code: str, output_dir: str) -> Dict[str, Any]:
"""
Extrait les données d'un ticket depuis Odoo.
Args:
config: Configuration avec les paramètres de connexion
ticket_code: Code du ticket à extraire
output_dir: Répertoire sauvegarder les données
Returns:
Données du ticket extraites
"""
# Créer le gestionnaire de tickets
manager = TicketManager(
url=config["odoo"]["url"],
db=config["odoo"]["db"],
username=config["odoo"]["username"],
api_key=config["odoo"]["api_key"]
)
# Récupérer le ticket par son code
ticket = manager.get_ticket_by_code(ticket_code)
if not ticket:
print(f"Ticket {ticket_code} non trouvé")
return {}
# Extraire toutes les données du ticket
ticket_dir = os.path.join(output_dir, f"ticket_{ticket_code}")
ticket_data = manager.extract_ticket_data(ticket["id"], ticket_dir)
# Post-traiter immédiatement les messages
post_traiter_messages(ticket_dir)
return ticket_data
def post_traiter_messages(ticket_dir: str) -> None:
"""
Post-traite les messages du ticket pour une meilleure analyse.
Args:
ticket_dir: Répertoire contenant les données du ticket
"""
messages_file = os.path.join(ticket_dir, "messages.json")
# Vérifier que le fichier existe
if not os.path.exists(messages_file):
print(f"AVERTISSEMENT: Fichier messages.json introuvable dans {ticket_dir}")
return
print(f"Post-traitement des messages du ticket...")
# Créer une sauvegarde avant transformation
backup_file = os.path.join(ticket_dir, "messages.json.backup")
if not os.path.exists(backup_file):
shutil.copy2(messages_file, backup_file)
print(f"Sauvegarde créée: {backup_file}")
# Transformer les messages pour un format optimal
transformer_messages(messages_file)
# Vérifier que la transformation a réussi
try:
with open(messages_file, 'r', encoding='utf-8') as f:
messages = json.load(f)
print(f"Post-traitement terminé, {len(messages)} messages formatés.")
except Exception as e:
print(f"ERREUR: Échec du post-traitement: {e}")
# Restaurer la sauvegarde si nécessaire
if os.path.exists(backup_file):
shutil.copy2(backup_file, messages_file)
print("Restauration de la sauvegarde des messages.")
def preparer_donnees_ticket(ticket_dir: str) -> Dict[str, Any]:
"""
Prépare les données du ticket pour l'analyse à partir des fichiers stockés.
Args:
ticket_dir: Répertoire contenant les données du ticket
Returns:
Dictionnaire des données du ticket prêtes pour l'analyse
"""
# Chemins des fichiers sources
ticket_file = os.path.join(ticket_dir, "ticket_info.json")
messages_file = os.path.join(ticket_dir, "messages.json")
attachments_file = os.path.join(ticket_dir, "attachments_info.json")
attachments_dir = os.path.join(ticket_dir, "attachments")
# Vérifier que les fichiers nécessaires existent
if not all(os.path.exists(f) for f in [ticket_file, messages_file, attachments_file]):
missing = [f for f in [ticket_file, messages_file, attachments_file] if not os.path.exists(f)]
raise FileNotFoundError(f"Fichiers manquants: {', '.join(missing)}")
# Charger les données
try:
with open(ticket_file, 'r', encoding='utf-8') as f:
ticket_info = json.load(f)
with open(messages_file, 'r', encoding='utf-8') as f:
messages = json.load(f)
with open(attachments_file, 'r', encoding='utf-8') as f:
attachments = json.load(f)
# Vérifier et corriger les chemins des pièces jointes
for attachment in attachments:
if "file_path" in attachment:
# S'assurer que le chemin est absolu
if not os.path.isabs(attachment["file_path"]):
attachment["file_path"] = os.path.join(attachments_dir, os.path.basename(attachment["file_path"]))
# Vérifier que le fichier existe
if not os.path.exists(attachment["file_path"]):
print(f"AVERTISSEMENT: Pièce jointe introuvable: {attachment['file_path']}")
return {
"ticket": ticket_info,
"messages": messages,
"attachments": attachments,
"files": {
"ticket_info": ticket_file,
"messages": messages_file,
"attachments_info": attachments_file,
"attachments_dir": attachments_dir
}
}
except Exception as e:
raise ValueError(f"Erreur lors du chargement des données du ticket: {e}")
def analyser_ticket(ticket_data: Dict[str, Any], config: Dict[str, Any], output_dir: str, llm_params: Optional[Dict[str, Any]] = None) -> Dict[str, str]:
"""
Analyse un ticket avec les agents LLM.
Args:
ticket_data: Données du ticket extraites
config: Configuration avec les clés API
output_dir: Répertoire sauvegarder les résultats
llm_params: Paramètres LLM globaux à appliquer
Returns:
Chemins des fichiers générés
"""
# Créer l'analyseur de tickets
analyzer = TicketAnalyzer(api_key=config["llm"]["api_key"], llm_params=llm_params)
# Préparer le contexte pour l'analyse des images
ticket_info = ticket_data.get("ticket", {})
contexte = f"""
TICKET: {ticket_info.get('code', 'Inconnu')} - {ticket_info.get('name', 'Sans titre')}
DESCRIPTION:
{ticket_info.get('description', 'Aucune description')}
"""
# Récupérer les chemins des pièces jointes (images)
attachments = ticket_data.get("attachments", [])
image_paths = []
# Vérification des doublons par nom de fichier
image_noms = set()
for attachment in attachments:
chemin = attachment.get("file_path")
nom_fichier = os.path.basename(chemin) if chemin else ""
if not chemin or not os.path.exists(chemin):
continue
mimetype = attachment.get("mimetype", "")
# Vérifier que c'est une image et qu'on ne l'a pas déjà incluse
if mimetype.startswith("image/") and nom_fichier not in image_noms:
image_paths.append(chemin)
image_noms.add(nom_fichier)
print(f"Image ajoutée pour analyse: {nom_fichier}")
# Filtrer les images pertinentes
print(f"Filtrage de {len(image_paths)} images...")
images_pertinentes = analyzer.filtrer_images(image_paths)
print(f"Images pertinentes: {len(images_pertinentes)}/{len(image_paths)}")
# Imprimer les détails pour le débogage
for i, img in enumerate(image_paths):
est_pertinente = img in images_pertinentes
print(f" Image {i+1}: {os.path.basename(img)} - {'Pertinente' if est_pertinente else 'Non pertinente'}")
# Analyser les images pertinentes
print("Analyse des images pertinentes...")
resultats_images = analyzer.analyser_images(images_pertinentes, contexte)
print(f"Analyses d'images terminées: {len(resultats_images)}")
# Extraire les questions et réponses
print("Extraction des questions et réponses...")
messages = ticket_data.get("messages", [])
# Vérifier que les messages sont traités et ne contiennent pas de balises HTML
for msg in messages:
body = msg.get("body", "")
if isinstance(body, str) and re.search(r'<[a-z]+[^>]*>', body, re.IGNORECASE):
print(f"AVERTISSEMENT: Message {msg.get('id', 'inconnu')} contient du HTML non traité")
qr_path = os.path.join(output_dir, "questions_reponses.md")
resultats_qr = analyzer.extraire_questions_reponses(messages, qr_path)
print(f"Questions extraites: {resultats_qr.get('nb_questions', 0)}")
print(f"Réponses extraites: {resultats_qr.get('nb_reponses', 0)}")
# Générer le rapport final
print("Génération du rapport final...")
rapport_dir = os.path.join(output_dir, "rapport")
fichiers = analyzer.generer_rapport(rapport_dir)
print(f"Rapport généré: {rapport_dir}")
# Corriger les problèmes d'accents dans les fichiers JSON
corriger_accents_fichiers(rapport_dir)
return fichiers
def corriger_accents_fichiers(dir_path: str) -> None:
"""
Corrige les problèmes d'accents dans tous les fichiers JSON et Markdown d'un répertoire.
Args:
dir_path: Chemin du répertoire contenant les fichiers à corriger
"""
print("Correction des problèmes d'accents dans les fichiers...")
if not os.path.exists(dir_path):
print(f"Répertoire non trouvé: {dir_path}")
return
for root, _, files in os.walk(dir_path):
for file in files:
# Corriger les fichiers JSON
if file.endswith(".json"):
json_file = os.path.join(root, file)
corriger_json_accents(json_file)
# Corriger les fichiers Markdown
elif file.endswith(".md"):
md_file = os.path.join(root, file)
corriger_markdown_accents(md_file)
# Corriger également les fichiers Markdown du répertoire de ticket
ticket_dir = os.path.dirname(dir_path)
for file in os.listdir(ticket_dir):
if file.endswith(".md"):
md_file = os.path.join(ticket_dir, file)
corriger_markdown_accents(md_file)
print("Correction des accents terminée.")
def main():
"""
Fonction principale du script.
"""
# Parser les arguments de la ligne de commande
parser = argparse.ArgumentParser(description="Analyse de tickets de support")
parser.add_argument("ticket_code", help="Code du ticket à analyser")
parser.add_argument("--config", "-c", default="config.json", help="Chemin vers le fichier de configuration")
parser.add_argument("--output", "-o", default="output", help="Répertoire de sortie")
parser.add_argument("--skip-extraction", "-s", action="store_true", help="Ignorer l'extraction du ticket (utiliser les données existantes)")
parser.add_argument("--fix-accents", "-f", action="store_true", help="Corriger les problèmes d'accents dans les fichiers existants")
parser.add_argument("--llm-params", "-p", type=str, help="Paramètres LLM au format JSON (ex: '{\"temperature\": 0.5}')")
parser.add_argument("--reprocess", "-r", action="store_true", help="Forcer le retraitement des messages même si l'extraction est ignorée")
parser.add_argument("--repair", action="store_true", help="Réparer un ticket corrompu avant analyse")
args = parser.parse_args()
# Charger la configuration
config = charger_config(args.config)
# Charger les paramètres LLM supplémentaires si spécifiés
llm_params = {}
if args.llm_params:
try:
llm_params = json.loads(args.llm_params)
print(f"Paramètres LLM personnalisés: {llm_params}")
except json.JSONDecodeError as e:
print(f"Erreur lors du parsing des paramètres LLM: {e}")
# Créer le répertoire de sortie
os.makedirs(args.output, exist_ok=True)
# Construire le chemin du répertoire du ticket
ticket_dir = os.path.join(args.output, f"ticket_{args.ticket_code}")
# Réparer le ticket si demandé
if args.repair:
if os.path.exists(ticket_dir):
print(f"Réparation du ticket {args.ticket_code}...")
success = reparer_ticket(ticket_dir)
if not success:
print("ERREUR: La réparation du ticket a échoué. Impossible de continuer.")
return
print(f"Réparation terminée, poursuite de l'analyse...")
else:
print(f"Impossible de réparer: répertoire du ticket {args.ticket_code} introuvable.")
if not args.skip_extraction:
print("Le ticket sera extrait depuis Odoo.")
else:
print("ERREUR: Impossible de continuer sans extraction.")
return
# Si l'option de correction des accents est activée uniquement
if args.fix_accents and not args.skip_extraction and not args.repair:
rapport_dir = os.path.join(ticket_dir, "rapport")
if os.path.exists(rapport_dir):
corriger_accents_fichiers(rapport_dir)
return
# Extraction ou chargement des données du ticket
try:
if not args.skip_extraction:
# Extraire les données du ticket
print(f"Extraction du ticket {args.ticket_code}...")
ticket_data = extraire_ticket(config, args.ticket_code, args.output)
if not ticket_data:
print("Impossible de continuer sans données de ticket.")
return
else:
# Si on ignore l'extraction mais qu'on veut retraiter
if args.reprocess:
print(f"Retraitement forcé des messages du ticket {args.ticket_code}...")
post_traiter_messages(ticket_dir)
# Charger les données existantes
print(f"Chargement des données du ticket {args.ticket_code}...")
ticket_data = preparer_donnees_ticket(ticket_dir)
print("Données chargées avec succès.")
# Analyser le ticket
print(f"Analyse du ticket {args.ticket_code}...")
fichiers = analyser_ticket(ticket_data, config, ticket_dir, llm_params)
print("\nAnalyse terminée!")
print(f"Rapport JSON: {fichiers['json']}")
print(f"Rapport Markdown: {fichiers['markdown']}")
except FileNotFoundError as e:
print(f"ERREUR: {e}")
print("Utilisez l'extraction ou assurez-vous que tous les fichiers nécessaires existent.")
print("Ou bien utilisez l'option --repair pour réparer le ticket.")
except ValueError as e:
print(f"ERREUR: {e}")
print("Vous pouvez essayer l'option --repair pour réparer le ticket.")
except Exception as e:
print(f"ERREUR inattendue: {e}")
print("Vous pouvez essayer l'option --repair pour réparer le ticket.")
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@ -0,0 +1,9 @@
[
{
"id": 31975,
"name": "ecran - 2020-04-07T120505.164.png",
"mimetype": "image/png",
"create_date": "2020-04-07 13:16:52",
"file_path": "output/ticket_T0140/attachments/31975_ecran - 2020-04-07T120505.164.png"
}
]

View File

@ -0,0 +1,25 @@
[
{
"id": "ticket_info",
"name": "Problème sur numérotation de lots de béton qui s'effectue par unité !",
"code": "T0140",
"description": "<p>Problème sur numérotation de lots qui s'effectue par unité !</p><p>Serait-il possible de remédier à cela ?</p><p>Tél : 06.20.90.27.57</p><p>Plage horaire : 9h / 17h</p><p>Réf. GestCom : D006435</p><p>Origine : Logiciel</p>",
"date_create": "2020-04-07 13:15:23",
"role": "system",
"type": "contexte",
"body": "TICKET T0140: Problème sur numérotation de lots de béton qui s'effectue par unité !.\n\nDESCRIPTION: <p>Problème sur numérotation de lots qui s'effectue par unité !</p><p>Serait-il possible de remédier à cela ?</p><p>Tél : 06.20.90.27.57</p><p>Plage horaire : 9h / 17h</p><p>Réf. GestCom : D006435</p><p>Origine : Logiciel</p>"
},
{
"id": "11136",
"author_id": [
10288,
"CBAO S.A.R.L., Youness BENDEQ"
],
"role": "Support",
"type": "Réponse",
"date": "2020-04-10 15:15:54",
"email_from": "\"Youness BENDEQ\" <youness.bendeq@cbao.fr>",
"subject": "Re: [T0140] Problème sur numérotation de lots de béton qui s'effectue par unité !",
"body": "Bonjour, Oui en effet l'ordre de tri d'affichage par défaut du tableau est par ordre alphabétique car certains de nos clients préfèrent utiliser une numérotation par lettre (A, B, C, D,..) pour identifier les numéros de lot. Si vous souhaitez identifier vos lots par numéro il vous suffit alors de mettre deux ou trois 0 devant chaque numéro de lot (ex: 0001, 0002, 0003,0004,...) Je reste à votre disposition pour toute explication ou demande supplémentaire. L'objectif du"
}
]

View File

@ -0,0 +1,59 @@
[
{
"id": 11137,
"body": "",
"date": "2020-04-10 15:15:58",
"author_id": [
10288,
"CBAO S.A.R.L., Youness BENDEQ"
],
"email_from": "\"Youness BENDEQ\" <youness.bendeq@cbao.fr>",
"subject": false,
"parent_id": [
11072,
"[T0140] Problème sur numérotation de lots de béton qui s'effectue par unité !"
]
},
{
"id": 11136,
"body": "<p>Bonjour,<br><br>Oui en effet l'ordre de tri d'affichage par défaut du tableau est par ordre alphabétique car certains de nos clients préfèrent utiliser une numérotation par lettre (A, B, C, D,..) pour identifier les numéros de lot. Si vous souhaitez identifier vos lots par numéro il vous suffit alors de mettre deux ou trois 0 devant chaque numéro de lot (ex: 0001, 0002, 0003,0004,...)<br>Je reste à votre disposition pour toute explication ou demande supplémentaire.<br>L'objectif du Support Technique est de vous aider : n'hésitez jamais à nous contacter si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes.<br>Cordialement.</p>",
"date": "2020-04-10 15:15:54",
"author_id": [
10288,
"CBAO S.A.R.L., Youness BENDEQ"
],
"email_from": "\"Youness BENDEQ\" <youness.bendeq@cbao.fr>",
"subject": "Re: [T0140] Problème sur numérotation de lots de béton qui s'effectue par unité !",
"parent_id": [
11072,
"[T0140] Problème sur numérotation de lots de béton qui s'effectue par unité !"
]
},
{
"id": 11088,
"body": "",
"date": "2020-04-09 09:49:57",
"author_id": [
10288,
"CBAO S.A.R.L., Youness BENDEQ"
],
"email_from": "\"Youness BENDEQ\" <youness.bendeq@cbao.fr>",
"subject": false,
"parent_id": [
11072,
"[T0140] Problème sur numérotation de lots de béton qui s'effectue par unité !"
]
},
{
"id": 11072,
"body": "",
"date": "2020-04-07 13:15:23",
"author_id": [
10288,
"CBAO S.A.R.L., Youness BENDEQ"
],
"email_from": "\"Youness BENDEQ\" <youness.bendeq@cbao.fr>",
"subject": false,
"parent_id": false
}
]

View File

@ -0,0 +1,15 @@
# Analyse des Questions et Réponses
| Question | Réponse |
|---------|---------|
## Paramètres LLM utilisés
- **Type de LLM**: Mistral
- **Modèle**: mistral-medium
- **Température**: 0.3
- **Tokens max**: 2000
**Paramètres modifiés durant l'analyse:**
- **temperature**: 0.3
- **max_tokens**: 2000

View File

@ -0,0 +1,90 @@
{
"entries": [
{
"timestamp": "2025-04-01 17:31:17",
"action": "filter_image",
"agent": "AgentFiltreImages",
"llm": {
"model": "pixtral-12b-2409"
},
"parametres_llm": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"image_path": "output/ticket_T0140/attachments/31975_ecran - 2020-04-07T120505.164.png",
"response": {
"pertinente": false,
"type_image": "autre",
"description": "Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique.",
"confiance": 90,
"justification": "L'image ne contient pas d'éléments techniques utiles pour résoudre un problème.",
"parametres_llm": {
"agent": "AgentFiltreImages",
"llm_type": "Pixtral",
"parametres": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"parametres_modifies": {
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n",
"temperature": 0.2,
"max_tokens": 500
}
}
}
},
{
"timestamp": "2025-04-01 17:31:17",
"action": "extract_questions_reponses",
"agent": "AgentQuestionReponse",
"llm": {
"model": "mistral-medium"
},
"parametres_llm": {
"model": "mistral-medium",
"temperature": 0.3,
"max_tokens": 2000,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n"
},
"response": {
"success": true,
"messages_analyses": [
{
"id": 11136,
"date": "2020-04-10 15:15:54",
"role": "Support",
"type": "Information",
"contenu": "Bonjour, Oui en effet l'ordre de tri d'affichage par défaut du tableau est par ordre alphabétique car certains de nos clients préfèrent utiliser une numérotation par lettre (A, B, C, D,..) pour identifier les numéros de lot. Si vous souhaitez identifier vos lots par numéro il vous suffit alors de mettre deux ou trois 0 devant chaque numéro de lot (ex: 0001, 0002, 0003,0004,...) Je reste à votre disposition pour toute explication ou demande supplémentaire. L'objectif du"
}
],
"paires_qr": [],
"nb_questions": 0,
"nb_reponses": 0,
"tableau_md": "# Analyse des Questions et Réponses\n\n| Question | Réponse |\n|---------|---------|\n\n## Paramètres LLM utilisés\n\n- **Type de LLM**: Mistral\n- **Modèle**: mistral-medium\n- **Température**: 0.3\n- **Tokens max**: 2000\n\n**Paramètres modifiés durant l'analyse:**\n- **temperature**: 0.3\n- **max_tokens**: 2000",
"parametres_llm": {
"agent": "AgentQuestionReponse",
"llm_type": "Mistral",
"parametres": {
"model": "mistral-medium",
"temperature": 0.3,
"max_tokens": 2000,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n"
},
"parametres_modifies": {
"system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n",
"temperature": 0.3,
"max_tokens": 2000
}
}
}
}
]
}

View File

@ -0,0 +1,76 @@
# Analyse de ticket de support
## Statistiques
- Images analysées: 1 (0 pertinentes)
- Questions identifiées: 0
- Réponses identifiées: 0
## Paramètres LLM par agent
### Agent de filtrage d'images
- **Type de LLM**: Pixtral
- **Modèle**: pixtral-12b-2409
- **Température**: 0.2
- **Tokens max**: 500
### Agent d'analyse d'images
- **Type de LLM**: Pixtral
- **Modèle**: pixtral-12b-2409
- **Température**: 0.3
- **Tokens max**: 1024
### Agent d'extraction questions-réponses
- **Type de LLM**: Mistral
- **Modèle**: mistral-medium
- **Température**: 0.3
- **Tokens max**: 2000
## Journal d'actions
### 2025-04-01 17:31:17 - AgentFiltreImages (pixtral-12b-2409)
**Action**: filter_image
**Image**: 31975_ecran - 2020-04-07T120505.164.png
**Résultat**: Non pertinente
**Type**: autre
**Description**: Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique.
**Paramètres LLM utilisés:**
- Température: 0.2
---
### 2025-04-01 17:31:17 - AgentQuestionReponse (mistral-medium)
**Action**: extract_questions_reponses
**Questions**: 0
**Réponses**: 0
# Analyse des Questions et Réponses
| Question | Réponse |
|---------|---------|
## Paramètres LLM utilisés
- **Type de LLM**: Mistral
- **Modèle**: mistral-medium
- **Température**: 0.3
- **Tokens max**: 2000
**Paramètres modifiés durant l'analyse:**
- **temperature**: 0.3
- **max_tokens**: 2000
**Paramètres LLM utilisés:**
- **model**: mistral-medium
- **temperature**: 0.3
- **max_tokens**: 2000
- **top_p**: 1.0
---

View File

@ -0,0 +1,140 @@
{
"id": 152,
"active": true,
"name": "Problème sur numérotation de lots de béton qui s'effectue par unité !",
"description": "<p>Problème sur numérotation de lots qui s'effectue par unité !</p><p>Serait-il possible de remédier à cela ?</p><p>Tél : 06.20.90.27.57</p><p>Plage horaire : 9h / 17h</p><p>Réf. GestCom : D006435</p><p>Origine : Logiciel</p>",
"sequence": 11,
"stage_id": [
8,
"Clôturé"
],
"kanban_state": "normal",
"create_date": "2020-04-07 13:15:23",
"write_date": "2024-10-03 13:10:50",
"date_start": "2020-04-07 09:24:00",
"date_end": false,
"date_assign": "2020-04-07 13:15:23",
"date_deadline": false,
"date_last_stage_update": "2020-04-10 15:15:57",
"project_id": [
3,
"Demandes"
],
"notes": false,
"planned_hours": 0.0,
"user_id": [
9,
"Youness BENDEQ"
],
"partner_id": [
7732,
"CONSEIL DEPARTEMENTAL DE HAUTE SAVOIE (74), Patrick LAVY"
],
"company_id": [
1,
"CBAO S.A.R.L."
],
"color": 0,
"displayed_image_id": false,
"parent_id": false,
"child_ids": [],
"email_from": "patrick.lavy@hautesavoie.fr",
"email_cc": false,
"working_hours_open": 0.0,
"working_hours_close": 0.0,
"working_days_open": 0.0,
"working_days_close": 0.0,
"website_message_ids": [
11136
],
"remaining_hours": 0.0,
"effective_hours": 0.0,
"total_hours_spent": 0.0,
"progress": 0.0,
"subtask_effective_hours": 0.0,
"timesheet_ids": [],
"priority": "0",
"code": "T0140",
"milestone_id": false,
"sale_line_id": false,
"sale_order_id": false,
"billable_type": "no",
"activity_ids": [],
"message_follower_ids": [
10888
],
"message_ids": [
11137,
11136,
11088,
11072
],
"message_main_attachment_id": [
31975,
"ecran - 2020-04-07T120505.164.png"
],
"failed_message_ids": [],
"rating_ids": [],
"rating_last_value": 0.0,
"access_token": "e2a9af6a-dc90-4892-82b7-33fa5d062d75",
"create_uid": [
9,
"Youness BENDEQ"
],
"write_uid": [
1,
"OdooBot"
],
"x_CBAO_windows_maj_ID": false,
"x_CBAO_version_signalement": false,
"x_CBAO_version_correction": false,
"x_CBAO_DateCorrection": false,
"x_CBAO_Scoring_Facilite": 0,
"x_CBAO_Scoring_Importance": 0,
"x_CBAO_Scoring_Urgence": 0,
"x_CBAO_Scoring_Incidence": 0,
"x_CBAO_Scoring_Resultat": 0,
"x_CBAO_InformationsSup": false,
"kanban_state_label": "En cours",
"subtask_planned_hours": 0.0,
"manager_id": [
22,
"Fabien LAFAY"
],
"user_email": "youness@cbao.fr",
"attachment_ids": [
31975
],
"legend_blocked": "Bloqué",
"legend_done": "Prêt pour la prochaine étape",
"legend_normal": "En cours",
"subtask_project_id": [
3,
"Demandes"
],
"subtask_count": 0,
"analytic_account_active": true,
"allow_timesheets": true,
"use_milestones": false,
"show_time_control": "start",
"is_project_map_empty": true,
"activity_state": false,
"activity_user_id": false,
"activity_type_id": false,
"activity_date_deadline": false,
"activity_summary": false,
"message_is_follower": false,
"message_unread": false,
"message_unread_counter": 0,
"message_needaction": false,
"message_needaction_counter": 0,
"message_has_error": false,
"message_has_error_counter": 0,
"message_attachment_count": 1,
"rating_last_feedback": false,
"rating_count": 0,
"access_url": "/my/task/152",
"access_warning": "",
"display_name": "[T0140] Problème sur numérotation de lots de béton qui s'effectue par unité !",
"__last_update": "2024-10-03 13:10:50"
}

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1,168 @@
[
{
"id": "ticket_info",
"name": "Problème d'impression des PV de compression des éprouvettes 11H22",
"code": "T0150",
"description": "<p>La résistance affichée sur le PV correspond à la résistance brute au lieu de la résistance corrigée.</p>",
"date_create": "2020-04-14 12:41:38",
"role": "system",
"type": "contexte",
"body": "TICKET T0150: Problème d'impression des PV de compression des éprouvettes 11H22.\n\nDESCRIPTION: <p>La résistance affichée sur le PV correspond à la résistance brute au lieu de la résistance corrigée.</p>"
},
{
"id": "ticket_info",
"author_id": [
0,
""
],
"role": "Client",
"type": "Question",
"date": "",
"email_from": "",
"subject": "",
"body": "TICKET T0150: Problème d'impression des PV de compression des éprouvettes 11H22. DESCRIPTION: La résistance affichée sur le PV correspond à la résistance brute au lieu de la résistance corrigée."
},
{
"id": "11183",
"author_id": [
0,
""
],
"role": "Client",
"type": "Question",
"date": "2020-04-14 12:39:49",
"email_from": "Monsieur Yoan Cazard <noreply@bureau24.fr>",
"subject": "Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2.",
"body": "Notification d'appel Version imprimable Notification d'appel Nous avons reçu un appel pour"
},
{
"id": "11291",
"author_id": [
10288,
"CBAO S.A.R.L., Youness BENDEQ"
],
"role": "Support",
"type": "Réponse",
"date": "2020-04-20 16:40:40",
"email_from": "\"Youness BENDEQ\" <youness.bendeq@cbao.fr>",
"subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22",
"body": "Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste"
},
{
"id": "11296",
"author_id": [
9043,
"CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD"
],
"role": "Client",
"type": "Question",
"date": "2020-04-21 08:29:06",
"email_from": "CAZARD Yoan <yoan.cazard@cd66.fr>",
"subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22",
"body": "Bonjour, Vous pouvez me rappeler? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste"
},
{
"id": "11304",
"author_id": [
10288,
"CBAO S.A.R.L., Youness BENDEQ"
],
"role": "Support",
"type": "Réponse",
"date": "2020-04-21 14:57:48",
"email_from": "\"Youness BENDEQ\" <youness.bendeq@cbao.fr>",
"subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22",
"body": "Bonjour, Etes vous disponible demain dans la matinée ? Je reste"
},
{
"id": "11305",
"author_id": [
9043,
"CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD"
],
"role": "Client",
"type": "Question",
"date": "2020-04-21 15:03:22",
"email_from": "CAZARD Yoan <yoan.cazard@cd66.fr>",
"subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22",
"body": "Oui pas de souci merci. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Mardi 21 Avril 2020 16:57:48 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Etes vous disponible demain dans la matinée ? Je reste"
},
{
"id": "11309",
"author_id": [
10288,
"CBAO S.A.R.L., Youness BENDEQ"
],
"role": "Support",
"type": "Réponse",
"date": "2020-04-22 08:22:21",
"email_from": "\"Youness BENDEQ\" <youness.bendeq@cbao.fr>",
"subject": "False",
"body": "Salut Laurent, > Si cela pose un problème, on peut mettre une option dans le logiciel pour imprimer le rapport soit avec la résistance brute, soit avec la résistance corrigée à toi de voir. Cela pose un gros problème cette histoire de résistance brute dans le rapport car M CAZARD obtient une non-conformité du coup. Pouvoir donner la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée semble être la meilleure solution. En effet, en ce moment il travail sur des ROANE (remblais auto-nivelant non essorable). Il s'agit de béton auto-plaçant et auto-nivelant pour les tranchées de la fibre optique. Cela permet d'intervenir plus facilement en cas de coupure du câble. C'est la raison pour laquelle les valeurs en MPa sont aussi faibles. Ils cherchent des valeurs comprises entre 2 et 3 Mpa. Or avec la résistance brute, il obtient une non conformité. Apparemment ils vont reprendre la travail début mai. Je lui ai dit qu'on allé voir ça pour cette date. C'est faisable ?"
},
{
"id": "11320",
"author_id": [
9043,
"CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD"
],
"role": "Client",
"type": "Question",
"date": "2020-04-23 09:46:11",
"email_from": "CAZARD Yoan <yoan.cazard@cd66.fr>",
"subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22",
"body": "Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. C'est un peu plus clair. On devrait pouvoir continuer à afficher les deux valeurs de résistances sur les PV et rapports d'essais. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste"
},
{
"id": "11321",
"author_id": [
10288,
"CBAO S.A.R.L., Youness BENDEQ"
],
"role": "Support",
"type": "Réponse",
"date": "2020-04-23 10:33:06",
"email_from": "\"Youness BENDEQ\" <youness.bendeq@cbao.fr>",
"subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22",
"body": "Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. Je ne vois pas l'échange complet, pouvez-vous me le renvoyer svp ? Je reste"
},
{
"id": "11322",
"author_id": [
9043,
"CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD"
],
"role": "Client",
"type": "Question",
"date": "2020-04-23 12:33:46",
"email_from": "CAZARD Yoan <yoan.cazard@cd66.fr>",
"subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22",
"body": "Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre détudes et dexpertise sur les risques, lenvironnement, la mobilité et laménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"PAYA Pierre\" À: \"CAZARD Yoan\" Cc: \"JAN Didier\" , \"ddumas\" , \"Gérald LACROIX\" , \"s labourasse\" , \"Stephane roques\" , \"claude trelcat\" , \"benoit gaumet\" , \"jm dao\" , \"a ferrer\" , \"pierre david\" , \"philippe delorme\" , \"Morales Frederic\" , \"CELLI Sylvain\" , \"BOISSONNADE-CORP Pierre\" , \"Richard BOREL\" , \"alain guiraud\" , \"raymond cayzac\" , \"georges pouget\" , \"noel sanchez\" , \"joseph pau\" , \"nicolas teisseire\" , mmohammedi@ardeche.fr , \"jerome gombault\" , \"alain nargeot\" , \"bhilaire\" , \"TEKATLIAN Annick\" , \"CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB\" , \"GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB\" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : \"Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac.\" Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : \" Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment).\" Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien"
},
{
"id": "11323",
"author_id": [
0,
""
],
"role": "Support",
"type": "Réponse",
"date": "2020-04-23 12:59:43",
"email_from": "Support Technique CBAO <support@cbao.fr>",
"subject": "échanges de mail entre M. Pierre PAYA du Cerema d'Aix et M.Dierkens du cerema de LYON",
"body": "-------- Message transféré -------- Sujet : Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Date : Thu, 23 Apr 2020 14:33:46 +0200 (CEST) De : CAZARD Yoan Pour : support Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre détudes et dexpertise sur les risques, lenvironnement, la mobilité et laménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"PAYA Pierre\" À: \"CAZARD Yoan\" Cc: \"JAN Didier\" , \"ddumas\" , \"Gérald LACROIX\" , \"s labourasse\" , \"Stephane roques\" , \"claude trelcat\" , \"benoit gaumet\" , \"jm dao\" , \"a ferrer\" , \"pierre david\" , \"philippe delorme\" , \"Morales Frederic\" , \"CELLI Sylvain\" , \"BOISSONNADE-CORP Pierre\" , \"Richard BOREL\" , \"alain guiraud\" , \"raymond cayzac\" , \"georges pouget\" , \"noel sanchez\" , \"joseph pau\" , \"nicolas teisseire\" , mmohammedi@ardeche.fr , \"jerome gombault\" , \"alain nargeot\" , \"bhilaire\" , \"TEKATLIAN Annick\" , \"CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB\" , \"GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB\" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : \"Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac.\" Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : \" Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment).\" Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien"
},
{
"id": "11462",
"author_id": [
10288,
"CBAO S.A.R.L., Youness BENDEQ"
],
"role": "Support",
"type": "Réponse",
"date": "2020-05-06 11:36:50",
"email_from": "\"Youness BENDEQ\" <youness.bendeq@cbao.fr>",
"subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22",
"body": "Appeler fait par Youness BENDEQ Pour savoir si la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée a été ajouté. Commentaire Possibilité ajoutée"
}
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,164 @@
{
"id": 162,
"active": true,
"name": "Problème d'impression des PV de compression des éprouvettes 11H22",
"description": "<p>La résistance affichée sur le PV correspond à la résistance brute au lieu de la résistance corrigée.</p>",
"sequence": 17,
"stage_id": [
8,
"Clôturé"
],
"kanban_state": "normal",
"create_date": "2020-04-14 12:41:38",
"write_date": "2024-10-03 13:10:50",
"date_start": "2020-04-14 12:41:38",
"date_end": false,
"date_assign": "2020-04-14 12:53:27",
"date_deadline": false,
"date_last_stage_update": "2020-05-06 11:37:10",
"project_id": [
3,
"Demandes"
],
"notes": false,
"planned_hours": 0.0,
"user_id": [
9,
"Youness BENDEQ"
],
"partner_id": [
9043,
"CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD"
],
"company_id": [
1,
"CBAO S.A.R.L."
],
"color": 0,
"displayed_image_id": false,
"parent_id": false,
"child_ids": [],
"email_from": "yoan.cazard@cd66.fr",
"email_cc": "",
"working_hours_open": 0.0,
"working_hours_close": 0.0,
"working_days_open": 0.0,
"working_days_close": 0.0,
"website_message_ids": [
11323,
11322,
11321,
11320,
11309,
11305,
11304,
11296,
11291,
11183
],
"remaining_hours": -0.25,
"effective_hours": 0.25,
"total_hours_spent": 0.25,
"progress": 0.0,
"subtask_effective_hours": 0.0,
"timesheet_ids": [
50
],
"priority": "0",
"code": "T0150",
"milestone_id": false,
"sale_line_id": false,
"sale_order_id": false,
"billable_type": "no",
"activity_ids": [],
"message_follower_ids": [
10906,
10962
],
"message_ids": [
11463,
11462,
11323,
11322,
11321,
11320,
11310,
11309,
11305,
11304,
11296,
11291,
11279,
11244,
11190,
11189,
11188,
11185,
11184,
11183,
11182
],
"message_main_attachment_id": false,
"failed_message_ids": [],
"rating_ids": [],
"rating_last_value": 0.0,
"access_token": "fc567ae1-ede0-4cd0-b08b-4e3310759d4d",
"create_uid": [
1,
"OdooBot"
],
"write_uid": [
1,
"OdooBot"
],
"x_CBAO_windows_maj_ID": false,
"x_CBAO_version_signalement": false,
"x_CBAO_version_correction": false,
"x_CBAO_DateCorrection": false,
"x_CBAO_Scoring_Facilite": 0,
"x_CBAO_Scoring_Importance": 0,
"x_CBAO_Scoring_Urgence": 0,
"x_CBAO_Scoring_Incidence": 0,
"x_CBAO_Scoring_Resultat": 0,
"x_CBAO_InformationsSup": false,
"kanban_state_label": "En cours",
"subtask_planned_hours": 0.0,
"manager_id": [
22,
"Fabien LAFAY"
],
"user_email": "youness@cbao.fr",
"attachment_ids": [],
"legend_blocked": "Bloqué",
"legend_done": "Prêt pour la prochaine étape",
"legend_normal": "En cours",
"subtask_project_id": [
3,
"Demandes"
],
"subtask_count": 0,
"analytic_account_active": true,
"allow_timesheets": true,
"use_milestones": false,
"show_time_control": "start",
"is_project_map_empty": true,
"activity_state": false,
"activity_user_id": false,
"activity_type_id": false,
"activity_date_deadline": false,
"activity_summary": false,
"message_is_follower": false,
"message_unread": false,
"message_unread_counter": 0,
"message_needaction": false,
"message_needaction_counter": 0,
"message_has_error": false,
"message_has_error_counter": 0,
"message_attachment_count": 0,
"rating_last_feedback": false,
"rating_count": 0,
"access_url": "/my/task/162",
"access_warning": "",
"display_name": "[T0150] Problème d'impression des PV de compression des éprouvettes 11H22",
"__last_update": "2024-10-03 13:10:50"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

View File

@ -0,0 +1,16 @@
[
{
"id": 32382,
"name": "Problème partie.png",
"mimetype": "image/png",
"create_date": "2020-04-27 06:21:36",
"file_path": "output/ticket_T0167/attachments/32382_Problème partie.png"
},
{
"id": 32380,
"name": "image001.png",
"mimetype": "image/png",
"create_date": "2020-04-27 06:21:36",
"file_path": "output/ticket_T0167/attachments/32380_image001.png"
}
]

View File

@ -0,0 +1,25 @@
[
{
"id": "ticket_info",
"name": "Pb d'affaire/chantier/partie dans un programme d'essai",
"code": "T0167",
"description": "<p>Je viens vers toi car Mr NOVO ma fait remonter un léger beug sur le numéro déchantillon B2020-0001 (Voir PJ).</p><p>En effet, il narrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro daffaire -> 20017 SETR -> LIAISON RD403 RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE GIBERVILLE.</p><p>Jai essayé de modifié la partie mais je ny arrive pas.</p>",
"date_create": "2020-04-27 06:21:36",
"role": "system",
"type": "contexte",
"body": "TICKET T0167: Pb d'affaire/chantier/partie dans un programme d'essai.\n\nDESCRIPTION: <p>Je viens vers toi car Mr NOVO ma fait remonter un léger beug sur le numéro déchantillon B2020-0001 (Voir PJ).</p><p>En effet, il narrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro daffaire -> 20017 SETR -> LIAISON RD403 RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE GIBERVILLE.</p><p>Jai essayé de modifié la partie mais je ny arrive pas.</p>"
},
{
"id": "11333",
"author_id": [
10288,
"CBAO S.A.R.L., Youness BENDEQ"
],
"role": "Support",
"type": "Réponse",
"date": "2020-04-27 06:20:22",
"email_from": "Youness BENDEQ <youness.bendeq@cbao.fr>",
"subject": "Pb d'affaire/chantier/partie dans un programme d'essai",
"body": "-------- Message transféré -------- Sujet : De retour ! Date : Mon, 20 Apr 2020 14:52:05 +0000 De : LENEVEU Guillaume <Guillaume.LENEVEU@calvados.fr> Pour : Youness BENDEQ <youness.bendeq@cbao.fr> Bonjour Youness, Jespère que tu vas bien ainsi que toute léquipe BRG-LAB. Je viens vers toi car Mr NOVO ma fait remonter un léger beug sur le numéro déchantillon B2020-0001 (Voir PJ). En effet, il narrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro daffaire -> 20017 SETR -> LIAISON RD403 RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE GIBERVILLE. Jai essayé de modifié la partie mais je ny arrive pas. Merci de ta réponse. Bonne fin de journée."
}
]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
# Analyse des Questions et Réponses
| Question | Réponse |
|---------|---------|
## Paramètres LLM utilisés
- **Type de LLM**: Mistral
- **Modèle**: mistral-medium
- **Température**: 0.3
- **Tokens max**: 2000
**Paramètres modifiés durant l'analyse:**
- **temperature**: 0.3
- **max_tokens**: 2000

View File

@ -0,0 +1,151 @@
{
"entries": [
{
"timestamp": "2025-04-01 17:34:01",
"action": "filter_image",
"agent": "AgentFiltreImages",
"llm": {
"model": "pixtral-12b-2409"
},
"parametres_llm": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"image_path": "output/ticket_T0167/attachments/32382_Problème partie.png",
"response": {
"pertinente": false,
"type_image": "autre",
"description": "Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique.",
"confiance": 90,
"justification": "L'image ne contient pas d'éléments techniques utiles pour résoudre un problème.",
"parametres_llm": {
"agent": "AgentFiltreImages",
"llm_type": "Pixtral",
"parametres": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"parametres_modifies": {
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n",
"temperature": 0.2,
"max_tokens": 500
}
}
}
},
{
"timestamp": "2025-04-01 17:34:01",
"action": "filter_image",
"agent": "AgentFiltreImages",
"llm": {
"model": "pixtral-12b-2409"
},
"parametres_llm": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"image_path": "output/ticket_T0167/attachments/32380_image001.png",
"response": {
"pertinente": true,
"type_image": "capture_ecran",
"description": "Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.",
"confiance": 85,
"justification": "L'image montre clairement une interface utilisateur avec des fonctionnalités techniques liées au problème décrit dans le ticket.",
"parametres_llm": {
"agent": "AgentFiltreImages",
"llm_type": "Pixtral",
"parametres": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"parametres_modifies": {
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n",
"temperature": 0.2,
"max_tokens": 500
}
}
}
},
{
"timestamp": "2025-04-01 17:34:01",
"action": "analyze_image",
"agent": "AgentAnalyseImage",
"llm": {
"model": "pixtral-12b-2409"
},
"parametres_llm": {
"model": "pixtral-12b-2409",
"temperature": 0.3,
"max_tokens": 1024,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques.\nVotre rôle est d'examiner des captures d'écran ou des photos liées à des problèmes techniques et de:\n1. Décrire précisément le contenu de l'image\n2. Identifier les éléments techniques visibles (erreurs, interfaces, configurations)\n3. Extraire tout texte visible (messages d'erreur, logs, indicateurs)\n\nSoyez précis et factuel dans votre analyse.\n"
},
"image_path": "output/ticket_T0167/attachments/32380_image001.png",
"response": "{\n \"pertinente\": true,\n \"type_image\": \"capture_ecran\",\n \"description\": \"Capture d'\\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.\",\n \"confiance\": 85,\n \"justification\": \"L'image montre clairement une interface utilisateur avec des fonctionnalit\\u00e9s techniques li\\u00e9es au probl\\u00e8me d\\u00e9crit dans le ticket.\"\n}",
"tokens": {
"prompt_tokens": 208,
"completion_tokens": 150,
"total_tokens": 358
}
},
{
"timestamp": "2025-04-01 17:34:01",
"action": "extract_questions_reponses",
"agent": "AgentQuestionReponse",
"llm": {
"model": "mistral-medium"
},
"parametres_llm": {
"model": "mistral-medium",
"temperature": 0.3,
"max_tokens": 2000,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n"
},
"response": {
"success": true,
"messages_analyses": [
{
"id": 11333,
"date": "2020-04-27 06:20:22",
"role": "Support",
"type": "Information",
"contenu": "-------- Message transféré -------- Sujet : De retour ! Date : Mon, 20 Apr 2020 14:52:05 +0000 De : LENEVEU Guillaume <Guillaume.LENEVEU@calvados.fr> Pour : Youness BENDEQ <youness.bendeq@cbao.fr> Bonjour Youness, Jespère que tu vas bien ainsi que toute léquipe BRG-LAB. Je viens vers toi car Mr NOVO ma fait remonter un léger beug sur le numéro déchantillon B2020-0001 (Voir PJ). En effet, il narrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrai"
}
],
"paires_qr": [],
"nb_questions": 0,
"nb_reponses": 0,
"tableau_md": "# Analyse des Questions et Réponses\n\n| Question | Réponse |\n|---------|---------|\n\n## Paramètres LLM utilisés\n\n- **Type de LLM**: Mistral\n- **Modèle**: mistral-medium\n- **Température**: 0.3\n- **Tokens max**: 2000\n\n**Paramètres modifiés durant l'analyse:**\n- **temperature**: 0.3\n- **max_tokens**: 2000",
"parametres_llm": {
"agent": "AgentQuestionReponse",
"llm_type": "Mistral",
"parametres": {
"model": "mistral-medium",
"temperature": 0.3,
"max_tokens": 2000,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n"
},
"parametres_modifies": {
"system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n",
"temperature": 0.3,
"max_tokens": 2000
}
}
}
}
]
}

View File

@ -0,0 +1,116 @@
# Analyse de ticket de support
## Statistiques
- Images analysées: 2 (1 pertinentes)
- Questions identifiées: 0
- Réponses identifiées: 0
## Paramètres LLM par agent
### Agent de filtrage d'images
- **Type de LLM**: Pixtral
- **Modèle**: pixtral-12b-2409
- **Température**: 0.2
- **Tokens max**: 500
### Agent d'analyse d'images
- **Type de LLM**: Pixtral
- **Modèle**: pixtral-12b-2409
- **Température**: 0.3
- **Tokens max**: 1024
### Agent d'extraction questions-réponses
- **Type de LLM**: Mistral
- **Modèle**: mistral-medium
- **Température**: 0.3
- **Tokens max**: 2000
## Journal d'actions
### 2025-04-01 17:34:01 - AgentFiltreImages (pixtral-12b-2409)
**Action**: filter_image
**Image**: 32382_Problème partie.png
**Résultat**: Non pertinente
**Type**: autre
**Description**: Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique.
**Paramètres LLM utilisés:**
- Température: 0.2
---
### 2025-04-01 17:34:01 - AgentFiltreImages (pixtral-12b-2409)
**Action**: filter_image
**Image**: 32380_image001.png
**Résultat**: Pertinente
**Type**: capture_ecran
**Description**: Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.
**Paramètres LLM utilisés:**
- Température: 0.2
---
### 2025-04-01 17:34:01 - AgentAnalyseImage (pixtral-12b-2409)
**Action**: analyze_image
**Image analysée**: 32380_image001.png
**Analyse**:
```
{
"pertinente": true,
"type_image": "capture_ecran",
"description": "Capture d'\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.",
"confiance": 85,
"justification": "L'image montre clairement une interface utilisateur avec des fonctionnalit\u00e9s techniques li\u00e9es au probl\u00e8me d\u00e9crit dans le ticket."
}
```
**Paramètres LLM utilisés:**
- **model**: pixtral-12b-2409
- **temperature**: 0.3
- **max_tokens**: 1024
- **top_p**: 1.0
**Tokens utilisés**:
- Prompt: 208
- Completion: 150
- Total: 358
---
### 2025-04-01 17:34:01 - AgentQuestionReponse (mistral-medium)
**Action**: extract_questions_reponses
**Questions**: 0
**Réponses**: 0
# Analyse des Questions et Réponses
| Question | Réponse |
|---------|---------|
## Paramètres LLM utilisés
- **Type de LLM**: Mistral
- **Modèle**: mistral-medium
- **Température**: 0.3
- **Tokens max**: 2000
**Paramètres modifiés durant l'analyse:**
- **temperature**: 0.3
- **max_tokens**: 2000
**Paramètres LLM utilisés:**
- **model**: mistral-medium
- **temperature**: 0.3
- **max_tokens**: 2000
- **top_p**: 1.0
---

View File

@ -0,0 +1,145 @@
{
"id": 179,
"active": true,
"name": "Pb d'affaire/chantier/partie dans un programme d'essai",
"description": "<p>Je viens vers toi car Mr NOVO ma fait remonter un léger beug sur le numéro déchantillon B2020-0001 (Voir PJ).</p><p>En effet, il narrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro daffaire -&gt; 20017 SETR -&gt; LIAISON RD403 RD402 DESSERTE PORTUAIRE VIADUC -&gt; JOINT DE CHAUSSEE COTE GIBERVILLE.</p><p>Jai essayé de modifié la partie mais je ny arrive pas.</p>",
"sequence": 22,
"stage_id": [
8,
"Clôturé"
],
"kanban_state": "normal",
"create_date": "2020-04-27 06:21:36",
"write_date": "2024-10-03 13:10:50",
"date_start": "2020-04-20 14:52:00",
"date_end": false,
"date_assign": "2020-04-27 07:15:48",
"date_deadline": false,
"date_last_stage_update": "2020-04-27 07:24:40",
"project_id": [
3,
"Demandes"
],
"notes": false,
"planned_hours": 0.0,
"user_id": [
9,
"Youness BENDEQ"
],
"partner_id": [
8504,
"CONSEIL DEPARTEMENTAL DU CALVADOS (14), Guillaume LENEVEU"
],
"company_id": [
1,
"CBAO S.A.R.L."
],
"color": 0,
"displayed_image_id": false,
"parent_id": false,
"child_ids": [],
"email_from": "guillaume.leneveu@calvados.fr",
"email_cc": "",
"working_hours_open": 0.0,
"working_hours_close": 0.0,
"working_days_open": 0.0,
"working_days_close": 0.0,
"website_message_ids": [
11333
],
"remaining_hours": -0.5,
"effective_hours": 0.5,
"total_hours_spent": 0.5,
"progress": 0.0,
"subtask_effective_hours": 0.0,
"timesheet_ids": [
51
],
"priority": "0",
"code": "T0167",
"milestone_id": false,
"sale_line_id": false,
"sale_order_id": false,
"billable_type": "no",
"activity_ids": [],
"message_follower_ids": [
10972
],
"message_ids": [
11346,
11345,
11344,
11343,
11342,
11335,
11334,
11333,
11332
],
"message_main_attachment_id": [
32380,
"image001.png"
],
"failed_message_ids": [],
"rating_ids": [],
"rating_last_value": 0.0,
"access_token": "cd4fbf5c-27d3-48ed-8c9b-c07f20c3e2d4",
"create_uid": [
1,
"OdooBot"
],
"write_uid": [
1,
"OdooBot"
],
"x_CBAO_windows_maj_ID": false,
"x_CBAO_version_signalement": false,
"x_CBAO_version_correction": false,
"x_CBAO_DateCorrection": false,
"x_CBAO_Scoring_Facilite": 0,
"x_CBAO_Scoring_Importance": 0,
"x_CBAO_Scoring_Urgence": 0,
"x_CBAO_Scoring_Incidence": 0,
"x_CBAO_Scoring_Resultat": 0,
"x_CBAO_InformationsSup": false,
"kanban_state_label": "En cours",
"subtask_planned_hours": 0.0,
"manager_id": [
22,
"Fabien LAFAY"
],
"user_email": "youness@cbao.fr",
"attachment_ids": [],
"legend_blocked": "Bloqué",
"legend_done": "Prêt pour la prochaine étape",
"legend_normal": "En cours",
"subtask_project_id": [
3,
"Demandes"
],
"subtask_count": 0,
"analytic_account_active": true,
"allow_timesheets": true,
"use_milestones": false,
"show_time_control": "start",
"is_project_map_empty": true,
"activity_state": false,
"activity_user_id": false,
"activity_type_id": false,
"activity_date_deadline": false,
"activity_summary": false,
"message_is_follower": false,
"message_unread": false,
"message_unread_counter": 0,
"message_needaction": false,
"message_needaction_counter": 0,
"message_has_error": false,
"message_has_error_counter": 0,
"message_attachment_count": 2,
"rating_last_feedback": false,
"rating_count": 0,
"access_url": "/my/task/179",
"access_warning": "",
"display_name": "[T0167] Pb d'affaire/chantier/partie dans un programme d'essai",
"__last_update": "2024-10-03 13:10:50"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1,23 @@
[
{
"id": 144796,
"name": "image.png",
"mimetype": "image/png",
"create_date": "2025-03-18 14:18:51",
"file_path": "output/ticket_T11067/attachments/144796_image.png"
},
{
"id": 144794,
"name": "image004.jpg",
"mimetype": "image/jpeg",
"create_date": "2025-03-18 13:22:27",
"file_path": "output/ticket_T11067/attachments/144794_image004.jpg"
},
{
"id": 144792,
"name": "image003.png",
"mimetype": "image/png",
"create_date": "2025-03-18 13:22:27",
"file_path": "output/ticket_T11067/attachments/144792_image003.png"
}
]

View File

@ -0,0 +1,38 @@
[
{
"id": "ticket_info",
"name": "changement nom centrale d'enrobage",
"code": "T11067",
"description": "<p><br></p>",
"date_create": "2025-03-18 13:22:27",
"role": "system",
"type": "contexte",
"body": "TICKET T11067: changement nom centrale d'enrobage.\n\nDESCRIPTION: <p><br></p>"
},
{
"id": 227725,
"author_id": [
5144,
"CONSEIL DEPARTEMENTAL DU MORBIHAN (56), Dominique CARVAL"
],
"role": "Client",
"type": "Question",
"date": "2025-03-18 13:18:31",
"email_from": "CARVAL Dominique <dominique.carval@morbihan.fr>",
"subject": "changement nom centrale d'enrobage",
"body": "Bonjour, 3 centrales denrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites)"
},
{
"id": 227731,
"author_id": [
32165,
"Romuald GRUSON"
],
"role": "Support",
"type": "Réponse",
"date": "2025-03-18 14:18:51",
"email_from": "support@cbao.fr",
"subject": "Re: [T11067] - changement nom centrale d'enrobage",
"body": "Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre deffectuer cette modification. Pour cela, il faut éditer le nom du poste de production denrobée, lenregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire."
}
]

View File

@ -0,0 +1,35 @@
[
{
"id": "ticket_info",
"name": "changement nom centrale d'enrobage",
"code": "T11067",
"description": "<p><br></p>",
"date_create": "2025-03-18 13:22:27"
},
{
"id": 227725,
"author_id": [
5144,
"CONSEIL DEPARTEMENTAL DU MORBIHAN (56), Dominique CARVAL"
],
"role": "Client",
"type": "Question",
"date": "2025-03-18 13:18:31",
"email_from": "CARVAL Dominique <dominique.carval@morbihan.fr>",
"subject": "changement nom centrale d'enrobage",
"body": "Bonjour, 3 centrales denrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites)"
},
{
"id": 227731,
"author_id": [
32165,
"Romuald GRUSON"
],
"role": "Support",
"type": "Réponse",
"date": "2025-03-18 14:18:51",
"email_from": "support@cbao.fr",
"subject": "Re: [T11067] - changement nom centrale d'enrobage",
"body": "Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre deffectuer cette modification. Pour cela, il faut éditer le nom du poste de production denrobée, lenregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire."
}
]

View File

@ -0,0 +1,16 @@
# Analyse des Questions et Réponses
| Question | Réponse |
|---------|---------|
| **Client**: Bonjour, 3 centrales denrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites) | **Support**: Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre deffectuer cette modification. Pour cela, il faut éditer le nom du poste de production denrobée, lenregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire. |
## Paramètres LLM utilisés
- **Type de LLM**: Mistral
- **Modèle**: mistral-medium
- **Température**: 0.3
- **Tokens max**: 2000
**Paramètres modifiés durant l'analyse:**
- **temperature**: 0.3
- **max_tokens**: 2000

View File

@ -0,0 +1,238 @@
{
"entries": [
{
"timestamp": "2025-04-01 17:08:06",
"action": "filter_image",
"agent": "AgentFiltreImages",
"llm": {
"model": "pixtral-12b-2409"
},
"parametres_llm": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"image_path": "output/ticket_T11067/attachments/144796_image.png",
"response": {
"pertinente": true,
"type_image": "capture_ecran",
"description": "Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.",
"confiance": 85,
"justification": "L'image montre clairement une interface utilisateur avec des fonctionnalités techniques liées au problème décrit dans le ticket.",
"parametres_llm": {
"agent": "AgentFiltreImages",
"llm_type": "Pixtral",
"parametres": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"parametres_modifies": {
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n",
"temperature": 0.2,
"max_tokens": 500
}
}
}
},
{
"timestamp": "2025-04-01 17:08:06",
"action": "filter_image",
"agent": "AgentFiltreImages",
"llm": {
"model": "pixtral-12b-2409"
},
"parametres_llm": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"image_path": "output/ticket_T11067/attachments/144794_image004.jpg",
"response": {
"pertinente": true,
"type_image": "capture_ecran",
"description": "Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.",
"confiance": 85,
"justification": "L'image montre clairement une interface utilisateur avec des fonctionnalités techniques liées au problème décrit dans le ticket.",
"parametres_llm": {
"agent": "AgentFiltreImages",
"llm_type": "Pixtral",
"parametres": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"parametres_modifies": {
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n",
"temperature": 0.2,
"max_tokens": 500
}
}
}
},
{
"timestamp": "2025-04-01 17:08:06",
"action": "filter_image",
"agent": "AgentFiltreImages",
"llm": {
"model": "pixtral-12b-2409"
},
"parametres_llm": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"image_path": "output/ticket_T11067/attachments/144792_image003.png",
"response": {
"pertinente": false,
"type_image": "logo",
"description": "Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique.",
"confiance": 90,
"justification": "L'image ne contient pas d'éléments techniques utiles pour résoudre un problème.",
"parametres_llm": {
"agent": "AgentFiltreImages",
"llm_type": "Pixtral",
"parametres": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"parametres_modifies": {
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n",
"temperature": 0.2,
"max_tokens": 500
}
}
}
},
{
"timestamp": "2025-04-01 17:08:06",
"action": "analyze_image",
"agent": "AgentAnalyseImage",
"llm": {
"model": "pixtral-12b-2409"
},
"parametres_llm": {
"model": "pixtral-12b-2409",
"temperature": 0.3,
"max_tokens": 1024,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques.\nVotre rôle est d'examiner des captures d'écran ou des photos liées à des problèmes techniques et de:\n1. Décrire précisément le contenu de l'image\n2. Identifier les éléments techniques visibles (erreurs, interfaces, configurations)\n3. Extraire tout texte visible (messages d'erreur, logs, indicateurs)\n\nSoyez précis et factuel dans votre analyse.\n"
},
"image_path": "output/ticket_T11067/attachments/144796_image.png",
"response": "{\n \"pertinente\": true,\n \"type_image\": \"capture_ecran\",\n \"description\": \"Capture d'\\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.\",\n \"confiance\": 85,\n \"justification\": \"L'image montre clairement une interface utilisateur avec des fonctionnalit\\u00e9s techniques li\\u00e9es au probl\\u00e8me d\\u00e9crit dans le ticket.\"\n}",
"tokens": {
"prompt_tokens": 84,
"completion_tokens": 150,
"total_tokens": 234
}
},
{
"timestamp": "2025-04-01 17:08:06",
"action": "analyze_image",
"agent": "AgentAnalyseImage",
"llm": {
"model": "pixtral-12b-2409"
},
"parametres_llm": {
"model": "pixtral-12b-2409",
"temperature": 0.3,
"max_tokens": 1024,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques.\nVotre rôle est d'examiner des captures d'écran ou des photos liées à des problèmes techniques et de:\n1. Décrire précisément le contenu de l'image\n2. Identifier les éléments techniques visibles (erreurs, interfaces, configurations)\n3. Extraire tout texte visible (messages d'erreur, logs, indicateurs)\n\nSoyez précis et factuel dans votre analyse.\n"
},
"image_path": "output/ticket_T11067/attachments/144794_image004.jpg",
"response": "{\n \"pertinente\": true,\n \"type_image\": \"capture_ecran\",\n \"description\": \"Capture d'\\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.\",\n \"confiance\": 85,\n \"justification\": \"L'image montre clairement une interface utilisateur avec des fonctionnalit\\u00e9s techniques li\\u00e9es au probl\\u00e8me d\\u00e9crit dans le ticket.\"\n}",
"tokens": {
"prompt_tokens": 84,
"completion_tokens": 150,
"total_tokens": 234
}
},
{
"timestamp": "2025-04-01 17:08:06",
"action": "extract_questions_reponses",
"agent": "AgentQuestionReponse",
"llm": {
"model": "mistral-medium"
},
"parametres_llm": {
"model": "mistral-medium",
"temperature": 0.3,
"max_tokens": 2000,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n"
},
"response": {
"success": true,
"messages_analyses": [
{
"id": "ticket_info",
"date": "",
"role": "Client",
"type": "Question",
"contenu": "TICKET T11067: changement nom centrale d'enrobage. DESCRIPTION:"
},
{
"id": 227725,
"date": "2025-03-18 13:18:31",
"role": "Client",
"type": "Question",
"contenu": "Bonjour, 3 centrales denrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites)"
},
{
"id": 227731,
"date": "2025-03-18 14:18:51",
"role": "Support",
"type": "Information",
"contenu": "Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre deffectuer cette modification. Pour cela, il faut éditer le nom du poste de production denrobée, lenregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire."
}
],
"paires_qr": [
{
"numero": "1",
"question": {
"role": "Client",
"contenu": "Bonjour, 3 centrales denrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites)"
},
"reponse": {
"role": "Support",
"contenu": "Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre deffectuer cette modification. Pour cela, il faut éditer le nom du poste de production denrobée, lenregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire."
}
}
],
"nb_questions": 1,
"nb_reponses": 1,
"tableau_md": "# Analyse des Questions et Réponses\n\n| Question | Réponse |\n|---------|---------|\n| **Client**: Bonjour, 3 centrales denrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites) | **Support**: Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre deffectuer cette modification. Pour cela, il faut éditer le nom du poste de production denrobée, lenregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire. |\n\n## Paramètres LLM utilisés\n\n- **Type de LLM**: Mistral\n- **Modèle**: mistral-medium\n- **Température**: 0.3\n- **Tokens max**: 2000\n\n**Paramètres modifiés durant l'analyse:**\n- **temperature**: 0.3\n- **max_tokens**: 2000",
"parametres_llm": {
"agent": "AgentQuestionReponse",
"llm_type": "Mistral",
"parametres": {
"model": "mistral-medium",
"temperature": 0.3,
"max_tokens": 2000,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n"
},
"parametres_modifies": {
"system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n",
"temperature": 0.3,
"max_tokens": 2000
}
}
}
}
]
}

View File

@ -0,0 +1,157 @@
# Analyse de ticket de support
## Statistiques
- Images analysées: 3 (2 pertinentes)
- Questions identifiées: 1
- Réponses identifiées: 1
## Paramètres LLM par agent
### Agent de filtrage d'images
- **Type de LLM**: Pixtral
- **Modèle**: pixtral-12b-2409
- **Température**: 0.2
- **Tokens max**: 500
### Agent d'analyse d'images
- **Type de LLM**: Pixtral
- **Modèle**: pixtral-12b-2409
- **Température**: 0.3
- **Tokens max**: 1024
### Agent d'extraction questions-réponses
- **Type de LLM**: Mistral
- **Modèle**: mistral-medium
- **Température**: 0.3
- **Tokens max**: 2000
## Journal d'actions
### 2025-04-01 17:08:06 - AgentFiltreImages (pixtral-12b-2409)
**Action**: filter_image
**Image**: 144796_image.png
**Résultat**: Pertinente
**Type**: capture_ecran
**Description**: Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.
**Paramètres LLM utilisés:**
- Température: 0.2
---
### 2025-04-01 17:08:06 - AgentFiltreImages (pixtral-12b-2409)
**Action**: filter_image
**Image**: 144794_image004.jpg
**Résultat**: Pertinente
**Type**: capture_ecran
**Description**: Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.
**Paramètres LLM utilisés:**
- Température: 0.2
---
### 2025-04-01 17:08:06 - AgentFiltreImages (pixtral-12b-2409)
**Action**: filter_image
**Image**: 144792_image003.png
**Résultat**: Non pertinente
**Type**: logo
**Description**: Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique.
**Paramètres LLM utilisés:**
- Température: 0.2
---
### 2025-04-01 17:08:06 - AgentAnalyseImage (pixtral-12b-2409)
**Action**: analyze_image
**Image analysée**: 144796_image.png
**Analyse**:
```
{
"pertinente": true,
"type_image": "capture_ecran",
"description": "Capture d'\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.",
"confiance": 85,
"justification": "L'image montre clairement une interface utilisateur avec des fonctionnalit\u00e9s techniques li\u00e9es au probl\u00e8me d\u00e9crit dans le ticket."
}
```
**Paramètres LLM utilisés:**
- **model**: pixtral-12b-2409
- **temperature**: 0.3
- **max_tokens**: 1024
- **top_p**: 1.0
**Tokens utilisés**:
- Prompt: 84
- Completion: 150
- Total: 234
---
### 2025-04-01 17:08:06 - AgentAnalyseImage (pixtral-12b-2409)
**Action**: analyze_image
**Image analysée**: 144794_image004.jpg
**Analyse**:
```
{
"pertinente": true,
"type_image": "capture_ecran",
"description": "Capture d'\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.",
"confiance": 85,
"justification": "L'image montre clairement une interface utilisateur avec des fonctionnalit\u00e9s techniques li\u00e9es au probl\u00e8me d\u00e9crit dans le ticket."
}
```
**Paramètres LLM utilisés:**
- **model**: pixtral-12b-2409
- **temperature**: 0.3
- **max_tokens**: 1024
- **top_p**: 1.0
**Tokens utilisés**:
- Prompt: 84
- Completion: 150
- Total: 234
---
### 2025-04-01 17:08:06 - AgentQuestionReponse (mistral-medium)
**Action**: extract_questions_reponses
**Questions**: 1
**Réponses**: 1
# Analyse des Questions et Réponses
| Question | Réponse |
|---------|---------|
| **Client**: Bonjour, 3 centrales denrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites) | **Support**: Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre deffectuer cette modification. Pour cela, il faut éditer le nom du poste de production denrobée, lenregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire. |
## Paramètres LLM utilisés
- **Type de LLM**: Mistral
- **Modèle**: mistral-medium
- **Température**: 0.3
- **Tokens max**: 2000
**Paramètres modifiés durant l'analyse:**
- **temperature**: 0.3
- **max_tokens**: 2000
**Paramètres LLM utilisés:**
- **model**: mistral-medium
- **temperature**: 0.3
- **max_tokens**: 2000
- **top_p**: 1.0
---

View File

@ -0,0 +1,145 @@
{
"id": 11046,
"active": true,
"name": "changement nom centrale d'enrobage",
"description": "<p><br></p>",
"sequence": 10,
"stage_id": [
32,
"En attente d'infos / retours"
],
"kanban_state": "normal",
"create_date": "2025-03-18 13:22:27",
"write_date": "2025-03-18 14:19:28",
"date_start": "2025-03-18 13:22:28",
"date_end": false,
"date_assign": "2025-03-18 13:42:04",
"date_deadline": "2025-04-02",
"date_last_stage_update": "2025-03-18 14:19:28",
"project_id": [
3,
"Demandes"
],
"notes": false,
"planned_hours": 0.0,
"user_id": [
32,
"Romuald GRUSON"
],
"partner_id": [
5144,
"CONSEIL DEPARTEMENTAL DU MORBIHAN (56), Dominique CARVAL"
],
"company_id": [
1,
"CBAO S.A.R.L."
],
"color": 0,
"displayed_image_id": false,
"parent_id": false,
"child_ids": [],
"email_from": "CARVAL Dominique <dominique.carval@morbihan.fr>",
"email_cc": "",
"working_hours_open": 0.0,
"working_hours_close": 0.0,
"working_days_open": 0.0,
"working_days_close": 0.0,
"website_message_ids": [
227731,
227725
],
"remaining_hours": 0.0,
"effective_hours": 0.0,
"total_hours_spent": 0.0,
"progress": 0.0,
"subtask_effective_hours": 0.0,
"timesheet_ids": [],
"priority": "0",
"code": "T11067",
"milestone_id": false,
"sale_line_id": false,
"sale_order_id": false,
"billable_type": "no",
"activity_ids": [],
"message_follower_ids": [
89590,
89592,
89593
],
"message_ids": [
227733,
227732,
227731,
227730,
227728,
227726,
227725,
227724
],
"message_main_attachment_id": [
144792,
"image003.png"
],
"failed_message_ids": [],
"rating_ids": [],
"rating_last_value": 0.0,
"access_token": "3295983b-a3aa-4d8c-817d-2332829ca264",
"create_uid": [
1,
"OdooBot"
],
"write_uid": [
32,
"Romuald GRUSON"
],
"x_CBAO_windows_maj_ID": false,
"x_CBAO_version_signalement": false,
"x_CBAO_version_correction": false,
"x_CBAO_DateCorrection": false,
"x_CBAO_Scoring_Facilite": 0,
"x_CBAO_Scoring_Importance": 0,
"x_CBAO_Scoring_Urgence": 0,
"x_CBAO_Scoring_Incidence": 0,
"x_CBAO_Scoring_Resultat": 0,
"x_CBAO_InformationsSup": false,
"kanban_state_label": "En cours",
"subtask_planned_hours": 0.0,
"manager_id": [
22,
"Fabien LAFAY"
],
"user_email": "romuald@mail.cbao.fr",
"attachment_ids": [],
"legend_blocked": "Bloqué",
"legend_done": "Prêt pour la prochaine étape",
"legend_normal": "En cours",
"subtask_project_id": [
3,
"Demandes"
],
"subtask_count": 0,
"analytic_account_active": true,
"allow_timesheets": true,
"use_milestones": false,
"show_time_control": "start",
"is_project_map_empty": true,
"activity_state": false,
"activity_user_id": false,
"activity_type_id": false,
"activity_date_deadline": false,
"activity_summary": false,
"message_is_follower": false,
"message_unread": false,
"message_unread_counter": 0,
"message_needaction": false,
"message_needaction_counter": 0,
"message_has_error": false,
"message_has_error_counter": 0,
"message_attachment_count": 3,
"rating_last_feedback": false,
"rating_count": 0,
"access_url": "/my/task/11046",
"access_warning": "",
"display_name": "[T11067] changement nom centrale d'enrobage",
"__last_update": "2025-03-18 14:19:28"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@ -0,0 +1,9 @@
[
{
"id": 144918,
"name": "image.png",
"mimetype": "image/png",
"create_date": "2025-03-21 14:50:36",
"file_path": "output/ticket_T11094/attachments/144918_image.png"
}
]

View File

@ -0,0 +1,25 @@
[
{
"id": "ticket_info",
"name": "Calcul de la masse volumique",
"code": "T11094",
"description": "<h1>Point particulier :</h1><ul><li>Essais :E2025-0002</li><li>Une norme :NF EN 12697-6</li><li>Multi laboratoire :Rouen labo sol</li><li><b>Le cas est bloquant</b></li></ul><h1>Description du problème :</h1><p>Bonjour,\r\nEst-il possible de vérifier pourquoi le calcul de ma masse volumique ne fonctionne pas.\r\n\r\nCordialement</p>",
"date_create": "2025-03-21 07:54:57",
"role": "system",
"type": "contexte",
"body": "TICKET T11094: Calcul de la masse volumique.\n\nDESCRIPTION: <h1>Point particulier :</h1><ul><li>Essais :E2025-0002</li><li>Une norme :NF EN 12697-6</li><li>Multi laboratoire :Rouen labo sol</li><li><b>Le cas est bloquant</b></li></ul><h1>Description du problème :</h1><p>Bonjour,\r\nEst-il possible de vérifier pourquoi le calcul de ma masse volumique ne fonctionne pas.\r\n\r\nCordialement</p>"
},
{
"id": "228106",
"author_id": [
32165,
"Romuald GRUSON"
],
"role": "Support",
"type": "Réponse",
"date": "2025-03-21 14:50:37",
"email_from": "support@cbao.fr",
"subject": "Re: [T11094] - Calcul de la masse volumique",
"body": "Bonjour , Pour obtenir une valeur, il est nécessaire de renseigner la \"masse volumique du produit d'étanchéité\". Vous trouverez une capture d'écran ci-dessous pour vous guider dans cette démarche. Je reste à votre entière disposition pour toute information complémentaire."
}
]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
# Analyse des Questions et Réponses
| Question | Réponse |
|---------|---------|
## Paramètres LLM utilisés
- **Type de LLM**: Mistral
- **Modèle**: mistral-medium
- **Température**: 0.3
- **Tokens max**: 2000
**Paramètres modifiés durant l'analyse:**
- **temperature**: 0.3
- **max_tokens**: 2000

View File

@ -0,0 +1,112 @@
{
"entries": [
{
"timestamp": "2025-04-01 17:18:01",
"action": "filter_image",
"agent": "AgentFiltreImages",
"llm": {
"model": "pixtral-12b-2409"
},
"parametres_llm": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"image_path": "output/ticket_T11094/attachments/144918_image.png",
"response": {
"pertinente": true,
"type_image": "capture_ecran",
"description": "Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.",
"confiance": 85,
"justification": "L'image montre clairement une interface utilisateur avec des fonctionnalités techniques liées au problème décrit dans le ticket.",
"parametres_llm": {
"agent": "AgentFiltreImages",
"llm_type": "Pixtral",
"parametres": {
"model": "pixtral-12b-2409",
"temperature": 0.2,
"max_tokens": 500,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n"
},
"parametres_modifies": {
"system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n",
"temperature": 0.2,
"max_tokens": 500
}
}
}
},
{
"timestamp": "2025-04-01 17:18:01",
"action": "analyze_image",
"agent": "AgentAnalyseImage",
"llm": {
"model": "pixtral-12b-2409"
},
"parametres_llm": {
"model": "pixtral-12b-2409",
"temperature": 0.3,
"max_tokens": 1024,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse d'images techniques.\nVotre rôle est d'examiner des captures d'écran ou des photos liées à des problèmes techniques et de:\n1. Décrire précisément le contenu de l'image\n2. Identifier les éléments techniques visibles (erreurs, interfaces, configurations)\n3. Extraire tout texte visible (messages d'erreur, logs, indicateurs)\n\nSoyez précis et factuel dans votre analyse.\n"
},
"image_path": "output/ticket_T11094/attachments/144918_image.png",
"response": "{\n \"pertinente\": true,\n \"type_image\": \"capture_ecran\",\n \"description\": \"Capture d'\\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.\",\n \"confiance\": 85,\n \"justification\": \"L'image montre clairement une interface utilisateur avec des fonctionnalit\\u00e9s techniques li\\u00e9es au probl\\u00e8me d\\u00e9crit dans le ticket.\"\n}",
"tokens": {
"prompt_tokens": 162,
"completion_tokens": 150,
"total_tokens": 312
}
},
{
"timestamp": "2025-04-01 17:18:01",
"action": "extract_questions_reponses",
"agent": "AgentQuestionReponse",
"llm": {
"model": "mistral-medium"
},
"parametres_llm": {
"model": "mistral-medium",
"temperature": 0.3,
"max_tokens": 2000,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n"
},
"response": {
"success": true,
"messages_analyses": [
{
"id": 228106,
"date": "2025-03-21 14:50:37",
"role": "Support",
"type": "Information",
"contenu": "Bonjour , Pour obtenir une valeur, il est nécessaire de renseigner la \"masse volumique du produit d'étanchéité\". Vous trouverez une capture d'écran ci-dessous pour vous guider dans cette démarche. Je reste à votre entière disposition pour toute information complémentaire."
}
],
"paires_qr": [],
"nb_questions": 0,
"nb_reponses": 0,
"tableau_md": "# Analyse des Questions et Réponses\n\n| Question | Réponse |\n|---------|---------|\n\n## Paramètres LLM utilisés\n\n- **Type de LLM**: Mistral\n- **Modèle**: mistral-medium\n- **Température**: 0.3\n- **Tokens max**: 2000\n\n**Paramètres modifiés durant l'analyse:**\n- **temperature**: 0.3\n- **max_tokens**: 2000",
"parametres_llm": {
"agent": "AgentQuestionReponse",
"llm_type": "Mistral",
"parametres": {
"model": "mistral-medium",
"temperature": 0.3,
"max_tokens": 2000,
"top_p": 1.0,
"system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n"
},
"parametres_modifies": {
"system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n",
"temperature": 0.3,
"max_tokens": 2000
}
}
}
}
]
}

View File

@ -0,0 +1,104 @@
# Analyse de ticket de support
## Statistiques
- Images analysées: 1 (1 pertinentes)
- Questions identifiées: 0
- Réponses identifiées: 0
## Paramètres LLM par agent
### Agent de filtrage d'images
- **Type de LLM**: Pixtral
- **Modèle**: pixtral-12b-2409
- **Température**: 0.2
- **Tokens max**: 500
### Agent d'analyse d'images
- **Type de LLM**: Pixtral
- **Modèle**: pixtral-12b-2409
- **Température**: 0.3
- **Tokens max**: 1024
### Agent d'extraction questions-réponses
- **Type de LLM**: Mistral
- **Modèle**: mistral-medium
- **Température**: 0.3
- **Tokens max**: 2000
## Journal d'actions
### 2025-04-01 17:18:01 - AgentFiltreImages (pixtral-12b-2409)
**Action**: filter_image
**Image**: 144918_image.png
**Résultat**: Pertinente
**Type**: capture_ecran
**Description**: Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.
**Paramètres LLM utilisés:**
- Température: 0.2
---
### 2025-04-01 17:18:01 - AgentAnalyseImage (pixtral-12b-2409)
**Action**: analyze_image
**Image analysée**: 144918_image.png
**Analyse**:
```
{
"pertinente": true,
"type_image": "capture_ecran",
"description": "Capture d'\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.",
"confiance": 85,
"justification": "L'image montre clairement une interface utilisateur avec des fonctionnalit\u00e9s techniques li\u00e9es au probl\u00e8me d\u00e9crit dans le ticket."
}
```
**Paramètres LLM utilisés:**
- **model**: pixtral-12b-2409
- **temperature**: 0.3
- **max_tokens**: 1024
- **top_p**: 1.0
**Tokens utilisés**:
- Prompt: 162
- Completion: 150
- Total: 312
---
### 2025-04-01 17:18:01 - AgentQuestionReponse (mistral-medium)
**Action**: extract_questions_reponses
**Questions**: 0
**Réponses**: 0
# Analyse des Questions et Réponses
| Question | Réponse |
|---------|---------|
## Paramètres LLM utilisés
- **Type de LLM**: Mistral
- **Modèle**: mistral-medium
- **Température**: 0.3
- **Tokens max**: 2000
**Paramètres modifiés durant l'analyse:**
- **temperature**: 0.3
- **max_tokens**: 2000
**Paramètres LLM utilisés:**
- **model**: mistral-medium
- **temperature**: 0.3
- **max_tokens**: 2000
- **top_p**: 1.0
---

View File

@ -0,0 +1,143 @@
{
"id": 11073,
"active": true,
"name": "Calcul de la masse volumique",
"description": "<h1>Point particulier :</h1><ul><li>Essais :E2025-0002</li><li>Une norme :NF EN 12697-6</li><li>Multi laboratoire :Rouen labo sol</li><li><b>Le cas est bloquant</b></li></ul><h1>Description du problème :</h1><p>Bonjour,\r\nEst-il possible de vérifier pourquoi le calcul de ma masse volumique ne fonctionne pas.\r\n\r\nCordialement</p>",
"sequence": 10,
"stage_id": [
32,
"En attente d'infos / retours"
],
"kanban_state": "normal",
"create_date": "2025-03-21 07:54:57",
"write_date": "2025-03-21 14:50:41",
"date_start": "2025-03-21 07:54:57",
"date_end": false,
"date_assign": "2025-03-21 10:57:58",
"date_deadline": "2025-04-05",
"date_last_stage_update": "2025-03-21 14:50:41",
"project_id": [
3,
"Demandes"
],
"notes": false,
"planned_hours": 0.0,
"user_id": [
32,
"Romuald GRUSON"
],
"partner_id": [
29954,
"INFRANEO - PANTIN, Mickaël RESSE"
],
"company_id": [
1,
"CBAO S.A.R.L."
],
"color": 0,
"displayed_image_id": false,
"parent_id": false,
"child_ids": [],
"email_from": "m.resse@infraneo.com",
"email_cc": "",
"working_hours_open": 1.9661111111111111,
"working_hours_close": 0.0,
"working_days_open": 0.0819212962962963,
"working_days_close": 0.0,
"website_message_ids": [
228106
],
"remaining_hours": 0.0,
"effective_hours": 0.0,
"total_hours_spent": 0.0,
"progress": 0.0,
"subtask_effective_hours": 0.0,
"timesheet_ids": [],
"priority": "3",
"code": "T11094",
"milestone_id": false,
"sale_line_id": false,
"sale_order_id": false,
"billable_type": "no",
"activity_ids": [],
"message_follower_ids": [
89735,
89736,
89740,
89747
],
"message_ids": [
228108,
228107,
228106,
228091,
228086,
228083
],
"message_main_attachment_id": [
144918,
"image.png"
],
"failed_message_ids": [],
"rating_ids": [],
"rating_last_value": 0.0,
"access_token": "bdbe88e0-f951-416f-bec8-9bfda0441405",
"create_uid": [
28,
"Support Robot"
],
"write_uid": [
32,
"Romuald GRUSON"
],
"x_CBAO_windows_maj_ID": "",
"x_CBAO_version_signalement": "20250318",
"x_CBAO_version_correction": "",
"x_CBAO_DateCorrection": false,
"x_CBAO_Scoring_Facilite": 0,
"x_CBAO_Scoring_Importance": 0,
"x_CBAO_Scoring_Urgence": 0,
"x_CBAO_Scoring_Incidence": 0,
"x_CBAO_Scoring_Resultat": 0,
"x_CBAO_InformationsSup": false,
"kanban_state_label": "En cours",
"subtask_planned_hours": 0.0,
"manager_id": [
22,
"Fabien LAFAY"
],
"user_email": "romuald@mail.cbao.fr",
"attachment_ids": [],
"legend_blocked": "Bloqué",
"legend_done": "Prêt pour la prochaine étape",
"legend_normal": "En cours",
"subtask_project_id": [
3,
"Demandes"
],
"subtask_count": 0,
"analytic_account_active": true,
"allow_timesheets": true,
"use_milestones": false,
"show_time_control": "start",
"is_project_map_empty": true,
"activity_state": false,
"activity_user_id": false,
"activity_type_id": false,
"activity_date_deadline": false,
"activity_summary": false,
"message_is_follower": false,
"message_unread": false,
"message_unread_counter": 0,
"message_needaction": false,
"message_needaction_counter": 0,
"message_has_error": false,
"message_has_error_counter": 0,
"message_attachment_count": 1,
"rating_last_feedback": false,
"rating_count": 0,
"access_url": "/my/task/11073",
"access_warning": "",
"display_name": "[T11094] Calcul de la masse volumique",
"__last_update": "2025-03-21 14:50:41"
}

982
post_process.py Normal file
View File

@ -0,0 +1,982 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script de post-traitement pour améliorer les fichiers JSON avant analyse.
"""
import os
import sys
import json
import re
import unicodedata
import shutil
from typing import Dict, List, Any, Optional
def nettoyer_html(texte: str, debug: bool = False) -> str:
"""
Nettoie le contenu HTML en supprimant les balises et le formatage.
Args:
texte: Texte HTML à nettoyer
debug: Afficher des informations de débogage pendant le nettoyage
Returns:
Texte nettoyé
"""
# Vérifier et convertir l'entrée
if not texte:
return ""
if not isinstance(texte, str):
try:
texte = str(texte)
except Exception as e:
print(f"AVERTISSEMENT: Impossible de convertir en texte: {e}")
return ""
if debug:
print(f"Texte original ({len(texte)} caractères): {texte[:100]}...")
# Détection de HTML
contient_html = bool(re.search(r'<[a-z]+[^>]*>', texte, re.IGNORECASE))
if debug and contient_html:
print(f"Le texte contient du HTML, nettoyage nécessaire")
# Supprimer les balises HTML - regex plus agressive pour capturer tous types de balises
try:
# Première passe - balises standard
texte_nettoye = re.sub(r'</?[a-z]+[^>]*>', ' ', texte, flags=re.IGNORECASE)
# Deuxième passe - balises restantes, y compris les mal formées
texte_nettoye = re.sub(r'<[^>]*>', ' ', texte_nettoye)
if debug and contient_html:
print(f"Après suppression des balises HTML: {texte_nettoye[:100]}...")
except Exception as e:
print(f"AVERTISSEMENT: Erreur lors du nettoyage HTML: {e}")
texte_nettoye = texte
# Remplacer les références aux images
try:
texte_nettoye = re.sub(r'\[Image:[^\]]+\]', '[Image]', texte_nettoye)
texte_nettoye = re.sub(r'<img[^>]+>', '[Image]', texte_nettoye, flags=re.IGNORECASE)
except Exception as e:
if debug:
print(f"AVERTISSEMENT: Erreur lors du traitement des images: {e}")
# Supprimer les éléments courants non pertinents
patterns_a_supprimer = [
r'Cordialement,[\s\S]*?$',
r'Bien cordialement,[\s\S]*?$',
r'Bonne réception[\s\S]*?$',
r'À votre disposition[\s\S]*?$',
r'Support technique[\s\S]*?$',
r'L\'objectif du Support Technique[\s\S]*?$',
r'Notre service est ouvert[\s\S]*?$',
r'Dès réception[\s\S]*?$',
r'Confidentialité[\s\S]*?$',
r'Ce message électronique[\s\S]*?$',
r'Droit à la déconnexion[\s\S]*?$',
r'Afin d\'assurer une meilleure traçabilité[\s\S]*?$',
r'tél\s*:\s*[\d\s\+]+',
r'mobile\s*:\s*[\d\s\+]+',
r'www\.[^\s]+\.[a-z]{2,3}',
]
try:
for pattern in patterns_a_supprimer:
texte_avant = texte_nettoye
texte_nettoye = re.sub(pattern, '', texte_nettoye, flags=re.IGNORECASE)
if debug and texte_avant != texte_nettoye:
print(f"Suppression de pattern '{pattern[:20]}...'")
except Exception as e:
# En cas d'échec des expressions régulières, conserver le texte tel quel
if debug:
print(f"AVERTISSEMENT: Erreur lors de la suppression des patterns: {e}")
try:
# Supprimer les lignes multiples vides
texte_nettoye = re.sub(r'\n\s*\n', '\n', texte_nettoye)
# Supprimer les espaces multiples
texte_nettoye = re.sub(r'\s+', ' ', texte_nettoye)
except Exception as e:
if debug:
print(f"AVERTISSEMENT: Erreur lors du nettoyage des espaces: {e}")
# Normaliser les caractères accentués
try:
texte_nettoye = normaliser_accents(texte_nettoye)
except Exception as e:
print(f"AVERTISSEMENT: Erreur lors de la normalisation des accents: {e}")
if debug:
print(f"Texte final ({len(texte_nettoye)} caractères): {texte_nettoye[:100]}...")
return texte_nettoye.strip()
def normaliser_accents(texte: str) -> str:
"""
Normalise les caractères accentués pour éviter les problèmes d'encodage.
Args:
texte: Texte à normaliser
Returns:
Texte avec caractères accentués normalisés
"""
# Si l'entrée n'est pas une chaîne, convertir en chaîne ou retourner vide
if not isinstance(texte, str):
if texte is None:
return ""
try:
texte = str(texte)
except:
return ""
# Convertir les caractères spéciaux HTML (comme &eacute;)
special_chars = {
'&aacute;': 'á', '&eacute;': 'é', '&iacute;': 'í', '&oacute;': 'ó', '&uacute;': 'ú',
'&Aacute;': 'Á', '&Eacute;': 'É', '&Iacute;': 'Í', '&Oacute;': 'Ó', '&Uacute;': 'Ú',
'&agrave;': 'à', '&egrave;': 'è', '&igrave;': 'ì', '&ograve;': 'ò', '&ugrave;': 'ù',
'&Agrave;': 'À', '&Egrave;': 'È', '&Igrave;': 'Ì', '&Ograve;': 'Ò', '&Ugrave;': 'Ù',
'&acirc;': 'â', '&ecirc;': 'ê', '&icirc;': 'î', '&ocirc;': 'ô', '&ucirc;': 'û',
'&Acirc;': 'Â', '&Ecirc;': 'Ê', '&Icirc;': 'Î', '&Ocirc;': 'Ô', '&Ucirc;': 'Û',
'&atilde;': 'ã', '&etilde;': '', '&itilde;': 'ĩ', '&otilde;': 'õ', '&utilde;': 'ũ',
'&Atilde;': 'Ã', '&Etilde;': '', '&Itilde;': 'Ĩ', '&Otilde;': 'Õ', '&Utilde;': 'Ũ',
'&auml;': 'ä', '&euml;': 'ë', '&iuml;': 'ï', '&ouml;': 'ö', '&uuml;': 'ü',
'&Auml;': 'Ä', '&Euml;': 'Ë', '&Iuml;': 'Ï', '&Ouml;': 'Ö', '&Uuml;': 'Ü',
'&ccedil;': 'ç', '&Ccedil;': 'Ç', '&ntilde;': 'ñ', '&Ntilde;': 'Ñ',
'&nbsp;': ' ', '&lt;': '<', '&gt;': '>', '&amp;': '&', '&quot;': '"', '&apos;': "'",
'&euro;': '', '&copy;': '©', '&reg;': '®', '&trade;': ''
}
for html, char in special_chars.items():
texte = texte.replace(html, char)
# Normaliser les caractères composés (par exemple, e plus accent)
# Exemple: 'é' qui pourrait être stocké comme 'e' + accent combinant
return unicodedata.normalize('NFC', texte)
def detecter_role(message: Dict[str, Any]) -> str:
"""
Détecte si un message provient du client ou du support.
Args:
message: Dictionnaire contenant les informations du message
Returns:
"Client" ou "Support"
"""
# Vérifier le champ 'role' s'il existe déjà
if "role" in message and message["role"] in ["Client", "Support"]:
return message["role"]
# Indices de support dans l'email
domaines_support = ["@cbao.fr", "@odoo.com", "support@", "ticket.support"]
indices_nom_support = ["support", "cbao", "technique", "odoo"]
email = message.get("email_from", "").lower()
# Nettoyer le format "Nom <email@domaine.com>"
if "<" in email and ">" in email:
match = re.search(r'<([^>]+)>', email)
if match:
email = match.group(1).lower()
# Vérifier le domaine email
if any(domaine in email for domaine in domaines_support):
return "Support"
# Vérifier le nom d'auteur
auteur = ""
if "author_id" in message and isinstance(message["author_id"], list) and len(message["author_id"]) > 1:
auteur = str(message["author_id"][1]).lower()
if any(indice in auteur for indice in indices_nom_support):
return "Support"
# Par défaut, considérer comme client
return "Client"
def transformer_messages(input_file: str, output_file: Optional[str] = None, debug: bool = False) -> None:
"""
Transforme le fichier messages.json en un format amélioré pour l'analyse LLM.
Args:
input_file: Chemin du fichier messages.json original
output_file: Chemin du fichier de sortie (par défaut, écrase le fichier d'entrée)
debug: Activer le mode débogage pour afficher plus d'informations
"""
if output_file is None:
output_file = input_file
if debug:
print(f"Transformation du fichier {input_file} vers {output_file} (mode débogage activé)")
try:
# Lire le fichier messages.json original
with open(input_file, 'r', encoding='utf-8') as f:
messages = json.load(f)
# Trouver le répertoire du ticket et charger les informations
ticket_dir = os.path.dirname(input_file)
ticket_info_path = os.path.join(ticket_dir, "ticket_info.json")
ticket_info = {}
# Extraire le code du ticket du nom du répertoire
try:
dir_name = os.path.basename(ticket_dir)
ticket_code = dir_name.replace("ticket_", "") if dir_name.startswith("ticket_") else dir_name
except Exception as e:
print(f"AVERTISSEMENT: Impossible d'extraire le code du ticket du répertoire: {e}")
ticket_code = "UNKNOWN"
# Charger les informations du ticket
if os.path.exists(ticket_info_path):
try:
with open(ticket_info_path, 'r', encoding='utf-8') as f:
ticket_info = json.load(f)
except Exception as e:
print(f"AVERTISSEMENT: Impossible de charger ticket_info.json: {e}")
# Créer une version améliorée des messages
processed_messages = []
# Extraire et formater les informations du ticket de manière sécurisée
ticket_name = ""
ticket_description = ""
ticket_date = ""
try:
if ticket_info:
ticket_name = str(ticket_info.get("name", "")) if ticket_info.get("name") is not None else ""
ticket_description = str(ticket_info.get("description", "")) if ticket_info.get("description") is not None else ""
ticket_date = str(ticket_info.get("create_date", "")) if ticket_info.get("create_date") is not None else ""
ticket_code = str(ticket_info.get("code", ticket_code)) if ticket_info.get("code") is not None else ticket_code
except Exception as e:
print(f"AVERTISSEMENT: Problème lors de l'extraction des données du ticket: {e}")
# Nettoyer la description pour éliminer les balises HTML
ticket_description_nettoyee = nettoyer_html(ticket_description, debug=debug)
if debug:
print(f"Description originale: {ticket_description[:100]}...")
print(f"Description nettoyée: {ticket_description_nettoyee[:100]}...")
# Ajouter les informations du ticket comme premier "message"
formatted_ticket_info = {
"id": "ticket_info",
"name": normaliser_accents(ticket_name) or f"Ticket {ticket_code}",
"code": ticket_code,
"description": ticket_description_nettoyee,
"date_create": ticket_date,
"role": "system",
"type": "contexte",
"body": f"TICKET {ticket_code}: {normaliser_accents(ticket_name)}.\n\nDESCRIPTION: {ticket_description_nettoyee or 'Aucune description disponible.'}"
}
processed_messages.append(formatted_ticket_info)
# Vérifier que messages est bien une liste
if not isinstance(messages, list):
print(f"AVERTISSEMENT: Le fichier messages.json ne contient pas une liste valide. Type: {type(messages)}")
messages = []
# Transformer chaque message
valid_messages = 0
for msg in messages:
# Vérifier que msg est un dictionnaire
if not isinstance(msg, dict):
print(f"AVERTISSEMENT: Message ignoré car ce n'est pas un dictionnaire. Type: {type(msg)}")
continue
# Ignorer les messages vides
body = msg.get("body", "")
if not body or not isinstance(body, str):
continue
if debug:
contient_html = bool(re.search(r'<[a-z]+[^>]*>', body, re.IGNORECASE))
if contient_html:
print(f"Message {msg.get('id', 'unknown')} contient du HTML")
# Déterminer le type (question/réponse) basé sur le rôle
role = detecter_role(msg)
message_type = "Question" if role == "Client" else "Réponse"
# Nettoyer le contenu de manière sécurisée
contenu_nettoye = nettoyer_html(body, debug=debug)
if not contenu_nettoye:
if debug:
print(f"Message {msg.get('id', 'unknown')} ignoré - contenu vide après nettoyage")
continue
# Normaliser les champs textuels
email_from = normaliser_accents(msg.get("email_from", ""))
subject = normaliser_accents(msg.get("subject", ""))
# Créer le message transformé avec des valeurs sécurisées
msg_id = msg.get("id", "")
if not msg_id:
msg_id = f"msg_{valid_messages+1}"
# Convertir msg_id en string si ce n'est pas le cas
if not isinstance(msg_id, str):
try:
msg_id = str(msg_id)
except:
msg_id = f"msg_{valid_messages+1}"
# Récupérer author_id de manière sécurisée
author_id = msg.get("author_id", [0, ""])
if not isinstance(author_id, list):
author_id = [0, ""]
# Récupérer la date de manière sécurisée
date = msg.get("date", "")
if not isinstance(date, str):
try:
date = str(date)
except:
date = ""
processed_message = {
"id": msg_id,
"author_id": author_id,
"role": role,
"type": message_type,
"date": date,
"email_from": email_from,
"subject": subject,
"body": contenu_nettoye
}
processed_messages.append(processed_message)
valid_messages += 1
# Trier par date (sauf le premier message qui est le contexte)
try:
processed_messages[1:] = sorted(processed_messages[1:], key=lambda x: x.get("date", ""))
except Exception as e:
print(f"AVERTISSEMENT: Impossible de trier les messages par date: {e}")
# Vérifier qu'il y a au moins un message valide en plus du contexte
if valid_messages == 0:
print("AVERTISSEMENT: Aucun message valide trouvé après nettoyage.")
# Ajouter un message factice pour éviter les erreurs
processed_messages.append({
"id": "msg_default",
"role": "Client",
"type": "Question",
"date": formatted_ticket_info.get("date_create", ""),
"body": f"Problème concernant {formatted_ticket_info.get('name', 'ce ticket')}."
})
# Écrire le fichier transformé
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(processed_messages, f, indent=2, ensure_ascii=False)
print(f"Transformation réussie: {len(processed_messages)} messages traités ({valid_messages} messages réels)")
except Exception as e:
print(f"Erreur lors de la transformation des messages: {str(e)}")
import traceback
print(f"Détails: {traceback.format_exc()}")
raise
def corriger_json_accents(input_file: str, output_file: Optional[str] = None) -> None:
"""
Corrige les problèmes d'accents dans un fichier JSON.
Args:
input_file: Chemin du fichier JSON à corriger
output_file: Chemin du fichier de sortie (par défaut, écrase le fichier d'entrée)
"""
if output_file is None:
output_file = input_file
try:
# Lire le fichier JSON
with open(input_file, 'r', encoding='utf-8') as f:
content = json.load(f)
# Fonction récursive pour normaliser tous les textes dans le JSON
def normaliser_json(obj):
if isinstance(obj, str):
return normaliser_accents(obj)
elif isinstance(obj, list):
return [normaliser_json(item) for item in obj]
elif isinstance(obj, dict):
return {k: normaliser_json(v) for k, v in obj.items()}
else:
return obj
# Normaliser tout le contenu JSON
content_normalise = normaliser_json(content)
# Écrire le fichier normalisé
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(content_normalise, f, indent=2, ensure_ascii=False)
print(f"Correction des accents réussie pour {os.path.basename(input_file)}")
except Exception as e:
print(f"Erreur lors de la correction des accents: {str(e)}")
raise
def corriger_markdown_accents(input_file: str, output_file: Optional[str] = None) -> None:
"""
Corrige les problèmes d'accents dans un fichier Markdown.
Args:
input_file: Chemin du fichier Markdown à corriger
output_file: Chemin du fichier de sortie (par défaut, écrase le fichier d'entrée)
"""
if output_file is None:
output_file = input_file
try:
# Lire le fichier Markdown
with open(input_file, 'r', encoding='utf-8') as f:
content = f.read()
# Normaliser les accents
content_normalise = normaliser_accents(content)
# Vérifier si des changements ont été effectués
if content != content_normalise:
# Écrire le fichier normalisé
with open(output_file, 'w', encoding='utf-8') as f:
f.write(content_normalise)
print(f"Correction des accents réussie pour {os.path.basename(input_file)}")
else:
print(f"Aucune correction nécessaire pour {os.path.basename(input_file)}")
except Exception as e:
print(f"Erreur lors de la correction des accents dans le markdown: {str(e)}")
raise
def reparer_ticket(ticket_dir: str) -> bool:
"""
Répare et réinitialise le traitement d'un ticket dont les données sont corrompues.
Args:
ticket_dir: Chemin du répertoire du ticket
Returns:
True si la réparation a réussi, False sinon
"""
try:
print(f"Tentative de réparation du ticket dans {ticket_dir}...")
# Vérifier que le répertoire existe
if not os.path.isdir(ticket_dir):
print(f"ERREUR: Répertoire de ticket introuvable: {ticket_dir}")
return False
# Chemins des fichiers critiques
ticket_info_path = os.path.join(ticket_dir, "ticket_info.json")
messages_path = os.path.join(ticket_dir, "messages.json")
attachments_path = os.path.join(ticket_dir, "attachments_info.json")
# Vérifier et réparer ticket_info.json
if os.path.exists(ticket_info_path):
try:
with open(ticket_info_path, 'r', encoding='utf-8') as f:
ticket_info = json.load(f)
# Vérifier la structure minimale
if not isinstance(ticket_info, dict):
raise ValueError("ticket_info.json n'est pas un dictionnaire valide")
# Réparer les champs manquants ou invalides
code = os.path.basename(ticket_dir).replace("ticket_", "")
if "code" not in ticket_info or not isinstance(ticket_info["code"], str):
ticket_info["code"] = code
if "name" not in ticket_info or not isinstance(ticket_info["name"], str):
ticket_info["name"] = f"Ticket {code}"
if "description" not in ticket_info or not isinstance(ticket_info["description"], str):
ticket_info["description"] = ""
# Réécrire le fichier nettoyé
with open(ticket_info_path, 'w', encoding='utf-8') as f:
json.dump(ticket_info, f, indent=2, ensure_ascii=False)
print(f"✓ ticket_info.json réparé")
except Exception as e:
print(f"! Erreur lors de la réparation de ticket_info.json: {str(e)}")
# Créer un ticket_info minimal
ticket_info = {
"code": os.path.basename(ticket_dir).replace("ticket_", ""),
"name": f"Ticket {os.path.basename(ticket_dir).replace('ticket_', '')}",
"description": "",
"create_date": ""
}
# Sauvegarder la version minimale
with open(ticket_info_path, 'w', encoding='utf-8') as f:
json.dump(ticket_info, f, indent=2, ensure_ascii=False)
print(f"✓ ticket_info.json recréé avec structure minimale")
else:
# Créer un ticket_info minimal
ticket_info = {
"code": os.path.basename(ticket_dir).replace("ticket_", ""),
"name": f"Ticket {os.path.basename(ticket_dir).replace('ticket_', '')}",
"description": "",
"create_date": ""
}
# Sauvegarder la version minimale
with open(ticket_info_path, 'w', encoding='utf-8') as f:
json.dump(ticket_info, f, indent=2, ensure_ascii=False)
print(f"✓ ticket_info.json créé avec structure minimale")
# Vérifier et réparer messages.json
messages_valides = False
if os.path.exists(messages_path):
try:
# Sauvegarder l'original s'il n'y a pas encore de backup
backup_file = os.path.join(ticket_dir, "messages.json.original")
if not os.path.exists(backup_file):
shutil.copy2(messages_path, backup_file)
print(f"✓ Sauvegarde originale créée: {backup_file}")
# Essayer de charger le fichier
with open(messages_path, 'r', encoding='utf-8') as f:
messages = json.load(f)
# Vérifier que c'est une liste
if not isinstance(messages, list):
raise ValueError("messages.json n'est pas une liste valide")
messages_valides = True
print(f"✓ messages.json valide ({len(messages)} messages)")
except Exception as e:
print(f"! Erreur dans messages.json: {str(e)}")
print(" Tentative de récupération...")
# Essayer de récupérer depuis la sauvegarde
backup_file = os.path.join(ticket_dir, "messages.json.original")
if os.path.exists(backup_file):
try:
with open(backup_file, 'r', encoding='utf-8') as f:
messages = json.load(f)
if isinstance(messages, list):
# Sauvegarder la version récupérée
with open(messages_path, 'w', encoding='utf-8') as f:
json.dump(messages, f, indent=2, ensure_ascii=False)
messages_valides = True
print(f"✓ messages.json récupéré depuis la sauvegarde")
except Exception:
print(" Échec de la récupération depuis la sauvegarde")
# Si les messages sont toujours invalides, créer un fichier minimal
if not messages_valides:
# Créer un fichier messages minimal
messages = [{
"id": 1,
"body": f"Message par défaut pour le ticket {os.path.basename(ticket_dir)}",
"date": "",
"email_from": "client@example.com"
}]
# Sauvegarder la version minimale
with open(messages_path, 'w', encoding='utf-8') as f:
json.dump(messages, f, indent=2, ensure_ascii=False)
print(f"✓ messages.json recréé avec message par défaut")
# Transformer messages.json pour le format attendu
print("Transformation des messages pour le bon format...")
transformer_messages(messages_path)
print("✓ Transformation des messages terminée")
# Vérifier et réparer attachments_info.json
if os.path.exists(attachments_path):
try:
with open(attachments_path, 'r', encoding='utf-8') as f:
attachments = json.load(f)
# Vérifier que c'est une liste
if not isinstance(attachments, list):
attachments = []
with open(attachments_path, 'w', encoding='utf-8') as f:
json.dump(attachments, f, indent=2, ensure_ascii=False)
print(f"✓ attachments_info.json réparé (liste vide)")
else:
print(f"✓ attachments_info.json valide ({len(attachments)} pièces jointes)")
except Exception as e:
print(f"! Erreur dans attachments_info.json: {str(e)}")
# Créer une liste vide
with open(attachments_path, 'w', encoding='utf-8') as f:
json.dump([], f, indent=2, ensure_ascii=False)
print(f"✓ attachments_info.json recréé (liste vide)")
else:
# Créer une liste vide
with open(attachments_path, 'w', encoding='utf-8') as f:
json.dump([], f, indent=2, ensure_ascii=False)
print(f"✓ attachments_info.json créé (liste vide)")
print(f"Réparation du ticket terminée avec succès!")
return True
except Exception as e:
print(f"ERREUR lors de la réparation du ticket: {str(e)}")
import traceback
print(f"Détails: {traceback.format_exc()}")
return False
def diagnostiquer_ticket(ticket_dir: str) -> Dict[str, Any]:
"""
Diagnostique les problèmes dans un ticket et propose des solutions.
Args:
ticket_dir: Chemin du répertoire du ticket
Returns:
Rapport de diagnostic avec les problèmes identifiés et solutions proposées
"""
diagnostic = {
"problemes": [],
"suggestions": [],
"etat_fichiers": {}
}
print(f"Diagnostic du ticket dans {ticket_dir}...")
# Vérifier que le répertoire existe
if not os.path.isdir(ticket_dir):
diagnostic["problemes"].append(f"Répertoire de ticket introuvable: {ticket_dir}")
diagnostic["suggestions"].append("Créer le répertoire du ticket")
return diagnostic
# Chemins des fichiers critiques
ticket_info_path = os.path.join(ticket_dir, "ticket_info.json")
messages_path = os.path.join(ticket_dir, "messages.json")
messages_backup_path = os.path.join(ticket_dir, "messages.json.backup")
attachments_path = os.path.join(ticket_dir, "attachments_info.json")
attachments_dir = os.path.join(ticket_dir, "attachments")
questions_reponses_path = os.path.join(ticket_dir, "questions_reponses.md")
rapport_dir = os.path.join(ticket_dir, "rapport")
# Vérifier ticket_info.json
if os.path.exists(ticket_info_path):
try:
with open(ticket_info_path, 'r', encoding='utf-8') as f:
ticket_info = json.load(f)
diagnostic["etat_fichiers"]["ticket_info.json"] = "valide"
# Vérifier la structure minimale
if not isinstance(ticket_info, dict):
diagnostic["problemes"].append("ticket_info.json n'est pas un dictionnaire valide")
diagnostic["suggestions"].append("Réparer ticket_info.json avec --repair")
diagnostic["etat_fichiers"]["ticket_info.json"] = "invalide"
# Vérifier les champs HTML
description = ticket_info.get("description", "")
if isinstance(description, str) and re.search(r'<[a-z]+[^>]*>', description, re.IGNORECASE):
diagnostic["problemes"].append("La description contient du HTML non traité")
diagnostic["suggestions"].append("Traiter les balises HTML dans la description")
except Exception as e:
diagnostic["problemes"].append(f"Erreur dans ticket_info.json: {str(e)}")
diagnostic["suggestions"].append("Réparer ticket_info.json avec --repair")
diagnostic["etat_fichiers"]["ticket_info.json"] = "corrompu"
else:
diagnostic["problemes"].append(f"Fichier manquant: ticket_info.json")
diagnostic["suggestions"].append("Créer ticket_info.json avec --repair")
diagnostic["etat_fichiers"]["ticket_info.json"] = "manquant"
# Vérifier messages.json
if os.path.exists(messages_path):
try:
with open(messages_path, 'r', encoding='utf-8') as f:
messages = json.load(f)
diagnostic["etat_fichiers"]["messages.json"] = "valide"
# Vérifier que c'est une liste
if not isinstance(messages, list):
diagnostic["problemes"].append("messages.json n'est pas une liste valide")
diagnostic["suggestions"].append("Réparer messages.json avec --repair")
diagnostic["etat_fichiers"]["messages.json"] = "invalide"
# Vérifier le contenu HTML dans les messages
html_count = 0
for msg in messages:
if not isinstance(msg, dict):
continue
body = msg.get("body", "")
if isinstance(body, str) and re.search(r'<[a-z]+[^>]*>', body, re.IGNORECASE):
html_count += 1
if html_count > 0:
diagnostic["problemes"].append(f"{html_count} message(s) contiennent du HTML non traité")
diagnostic["suggestions"].append("Retraiter messages.json avec --debug pour voir les problèmes")
# Vérifier les accents dans les messages
accents_count = 0
for msg in messages:
if not isinstance(msg, dict):
continue
body = msg.get("body", "")
if isinstance(body, str):
# Vérifier les entités HTML pour les accents
if re.search(r'&[aeiounc][a-z]{3,5};', body, re.IGNORECASE):
accents_count += 1
if accents_count > 0:
diagnostic["problemes"].append(f"{accents_count} message(s) contiennent des entités HTML d'accent non converties")
diagnostic["suggestions"].append("Corriger les accents avec --fix-all")
except Exception as e:
diagnostic["problemes"].append(f"Erreur dans messages.json: {str(e)}")
diagnostic["suggestions"].append("Réparer messages.json avec --repair")
diagnostic["etat_fichiers"]["messages.json"] = "corrompu"
else:
diagnostic["problemes"].append(f"Fichier manquant: messages.json")
diagnostic["suggestions"].append("Créer messages.json avec --repair")
diagnostic["etat_fichiers"]["messages.json"] = "manquant"
# Vérifier si une sauvegarde des messages existe
if os.path.exists(messages_backup_path):
diagnostic["etat_fichiers"]["messages.json.backup"] = "présent"
else:
diagnostic["etat_fichiers"]["messages.json.backup"] = "manquant"
# Vérifier le fichier des questions et réponses
if os.path.exists(questions_reponses_path):
try:
with open(questions_reponses_path, 'r', encoding='utf-8') as f:
content = f.read()
diagnostic["etat_fichiers"]["questions_reponses.md"] = "présent"
# Vérifier si des questions/réponses sont présentes
if "| Question | Réponse |" in content and not re.search(r'\| \*\*[^|]+\*\*: ', content):
diagnostic["problemes"].append("Le fichier questions_reponses.md ne contient pas de questions/réponses")
diagnostic["suggestions"].append("Retraiter le ticket pour extraire les questions/réponses")
except Exception as e:
diagnostic["problemes"].append(f"Erreur dans questions_reponses.md: {str(e)}")
diagnostic["etat_fichiers"]["questions_reponses.md"] = "invalide"
else:
diagnostic["etat_fichiers"]["questions_reponses.md"] = "manquant"
# Vérifier les pièces jointes
if os.path.exists(attachments_path):
try:
with open(attachments_path, 'r', encoding='utf-8') as f:
attachments = json.load(f)
diagnostic["etat_fichiers"]["attachments_info.json"] = "valide"
# Vérifier que c'est une liste
if not isinstance(attachments, list):
diagnostic["problemes"].append("attachments_info.json n'est pas une liste valide")
diagnostic["suggestions"].append("Réparer attachments_info.json avec --repair")
diagnostic["etat_fichiers"]["attachments_info.json"] = "invalide"
# Vérifier que les fichiers attachés existent
if os.path.exists(attachments_dir):
diagnostic["etat_fichiers"]["attachments/"] = "présent"
for attachment in attachments:
if not isinstance(attachment, dict):
continue
file_path = attachment.get("file_path", "")
if not file_path:
continue
# Normaliser le chemin
if not os.path.isabs(file_path):
file_path = os.path.join(attachments_dir, os.path.basename(file_path))
if not os.path.exists(file_path):
file_name = os.path.basename(file_path)
diagnostic["problemes"].append(f"Fichier attaché manquant: {file_name}")
else:
diagnostic["etat_fichiers"]["attachments/"] = "manquant"
diagnostic["problemes"].append("Répertoire attachments/ manquant")
except Exception as e:
diagnostic["problemes"].append(f"Erreur dans attachments_info.json: {str(e)}")
diagnostic["suggestions"].append("Réparer attachments_info.json avec --repair")
diagnostic["etat_fichiers"]["attachments_info.json"] = "corrompu"
else:
diagnostic["etat_fichiers"]["attachments_info.json"] = "manquant"
diagnostic["problemes"].append("Fichier attachments_info.json manquant")
diagnostic["suggestions"].append("Créer attachments_info.json avec --repair")
# Vérifier le répertoire rapport
if os.path.exists(rapport_dir):
diagnostic["etat_fichiers"]["rapport/"] = "présent"
rapport_json = os.path.join(rapport_dir, "ticket_analysis.json")
rapport_md = os.path.join(rapport_dir, "ticket_analysis.md")
if os.path.exists(rapport_json):
diagnostic["etat_fichiers"]["rapport/ticket_analysis.json"] = "présent"
else:
diagnostic["etat_fichiers"]["rapport/ticket_analysis.json"] = "manquant"
diagnostic["problemes"].append("Rapport JSON manquant")
if os.path.exists(rapport_md):
diagnostic["etat_fichiers"]["rapport/ticket_analysis.md"] = "présent"
else:
diagnostic["etat_fichiers"]["rapport/ticket_analysis.md"] = "manquant"
diagnostic["problemes"].append("Rapport Markdown manquant")
else:
diagnostic["etat_fichiers"]["rapport/"] = "manquant"
# Ajouter des suggestions globales si nécessaires
if len(diagnostic["problemes"]) > 3:
diagnostic["suggestions"].insert(0, "Utiliser l'option --repair pour essayer de corriger tous les problèmes automatiquement")
# Afficher le résumé du diagnostic
print(f"\nRésumé du diagnostic pour {os.path.basename(ticket_dir)}:")
print(f"- Problèmes identifiés: {len(diagnostic['problemes'])}")
for i, probleme in enumerate(diagnostic["problemes"]):
print(f" {i+1}. {probleme}")
print("\nSuggestions:")
for suggestion in diagnostic["suggestions"]:
print(f"- {suggestion}")
return diagnostic
def main():
"""
Point d'entrée principal du script.
"""
# Analyser les arguments
if len(sys.argv) < 2:
print("Usage: python post_process.py <ticket_dir> [options]")
print("Options:")
print(" --fix-all Corriger les accents dans tous les fichiers JSON et Markdown")
print(" --fix-md Corriger uniquement les fichiers Markdown")
print(" --repair Réparer un ticket corrompu")
print(" --debug Activer le mode débogage")
print(" --diagnose Diagnostiquer les problèmes du ticket")
print(" --help Afficher cette aide")
sys.exit(1)
# Afficher l'aide
if "--help" in sys.argv:
print("Usage: python post_process.py <ticket_dir> [options]")
print("Options:")
print(" --fix-all Corriger les accents dans tous les fichiers JSON et Markdown")
print(" --fix-md Corriger uniquement les fichiers Markdown")
print(" --repair Réparer un ticket corrompu")
print(" --debug Activer le mode débogage")
print(" --diagnose Diagnostiquer les problèmes du ticket")
print(" --help Afficher cette aide")
sys.exit(0)
ticket_dir = sys.argv[1]
fix_all = "--fix-all" in sys.argv
fix_md = "--fix-md" in sys.argv
repair = "--repair" in sys.argv
debug = "--debug" in sys.argv
diagnose = "--diagnose" in sys.argv
# Vérifier que le répertoire existe
if not os.path.isdir(ticket_dir):
print(f"ERREUR: Répertoire non trouvé: {ticket_dir}")
sys.exit(1)
# Option de diagnostic du ticket
if diagnose:
diagnostiquer_ticket(ticket_dir)
sys.exit(0)
# Option de réparation du ticket
if repair:
success = reparer_ticket(ticket_dir)
if not success:
print("La réparation du ticket a échoué.")
sys.exit(1)
print("Ticket réparé avec succès!")
sys.exit(0)
# Option de correction des accents dans les fichiers Markdown uniquement
if fix_md:
rapport_dir = os.path.join(ticket_dir, "rapport")
corrected = False
# Corriger les fichiers Markdown du répertoire rapport
if os.path.exists(rapport_dir):
for root, _, files in os.walk(rapport_dir):
for file in files:
if file.endswith(".md"):
md_file = os.path.join(root, file)
corriger_markdown_accents(md_file)
corrected = True
# Corriger les fichiers Markdown à la racine du ticket
for file in os.listdir(ticket_dir):
if file.endswith(".md"):
md_file = os.path.join(ticket_dir, file)
corriger_markdown_accents(md_file)
corrected = True
if corrected:
print("Correction des accents terminée dans les fichiers Markdown.")
else:
print("Aucun fichier Markdown trouvé.")
sys.exit(0)
# Transformation standard des messages
messages_file = os.path.join(ticket_dir, "messages.json")
if not os.path.exists(messages_file):
print(f"Fichier non trouvé: {messages_file}")
sys.exit(1)
try:
# Transformer les messages
transformer_messages(messages_file, debug=debug)
print(f"Post-traitement terminé pour {messages_file}")
# Corriger les accents dans tous les fichiers si demandé
if fix_all:
rapport_dir = os.path.join(ticket_dir, "rapport")
if os.path.exists(rapport_dir):
# Corriger les fichiers JSON
for root, _, files in os.walk(rapport_dir):
for file in files:
if file.endswith(".json"):
json_file = os.path.join(root, file)
corriger_json_accents(json_file)
# Corriger les fichiers Markdown
for root, _, files in os.walk(rapport_dir):
for file in files:
if file.endswith(".md"):
md_file = os.path.join(root, file)
corriger_markdown_accents(md_file)
# Corriger les fichiers Markdown à la racine du ticket
for file in os.listdir(ticket_dir):
if file.endswith(".md"):
md_file = os.path.join(ticket_dir, file)
corriger_markdown_accents(md_file)
print("Correction des accents terminée dans tous les fichiers.")
except Exception as e:
print(f"ERREUR lors du post-traitement: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
requests>=2.28.0
mistralai>=0.0.7

444
scripts/analyze_ticket.py Normal file
View File

@ -0,0 +1,444 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script d'analyse de ticket pour extraire les informations essentielles
et générer un rapport d'analyse complet.
"""
import os
import sys
import json
import argparse
import logging
from typing import Dict, List, Any, Optional
# Configuration du logger
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("analyze_ticket.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("analyze_ticket")
try:
from llm import Mistral
except ImportError:
logger.error("Module LLM non trouvé. Veuillez vous assurer que le répertoire parent est dans PYTHONPATH.")
sys.exit(1)
class TicketAnalyzer:
"""
Agent d'analyse de ticket qui extrait les informations pertinentes.
"""
def __init__(self, api_key: Optional[str] = None):
"""
Initialise l'agent d'analyse de ticket.
Args:
api_key: Clé API pour le LLM
"""
self.llm = Mistral(api_key=api_key)
self.llm.set_model("mistral-medium")
self.llm.set_temperature(0.3)
self.llm.set_max_tokens(1000)
# Définir le prompt système par défaut
self.system_prompt = """
Tu es un expert en analyse de tickets de support technique.
Ton objectif est d'analyser un ticket de support pour:
1. Identifier le problème principal
2. Résumer la solution (si présente)
3. Extraire les informations clés
4. Catégoriser le problème et sa gravité
5. Évaluer la qualité de la résolution
Utilise un ton professionnel et factuel.
Concentre-toi uniquement sur les informations pertinentes.
Ne spécule pas au-delà de ce qui est présent dans les données.
Si une information n'est pas disponible, indique-le clairement.
"""
self.historique = []
def ajouter_historique(self, action: str, entree: str, resultat: str) -> None:
"""
Ajoute une entrée à l'historique des actions.
Args:
action: Type d'action effectuée
entree: Entrée de l'action
resultat: Résultat de l'action
"""
self.historique.append({
"action": action,
"entree": entree,
"resultat": resultat
})
def analyser_ticket(self, messages: List[Dict[str, Any]], infos_images: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Analyse un ticket à partir de ses messages et informations d'images.
Args:
messages: Liste des messages du ticket
infos_images: Informations sur les images analysées (optionnel)
Returns:
Résultats de l'analyse du ticket
"""
if not messages:
logger.warning("Aucun message à analyser")
return {
"success": False,
"erreur": "Aucun message à analyser"
}
logger.info(f"Analyse de ticket avec {len(messages)} messages")
# Extraire les informations du ticket depuis le premier message (contexte)
ticket_info = {}
if messages and messages[0].get("role") == "system" and messages[0].get("type") == "contexte":
ticket_info = {
"id": messages[0].get("id", ""),
"code": messages[0].get("code", ""),
"name": messages[0].get("name", ""),
"description": messages[0].get("description", ""),
"date_create": messages[0].get("date_create", "")
}
# Retirer le message de contexte pour l'analyse
actual_messages = messages[1:]
else:
actual_messages = messages
# Préparer le prompt pour l'analyse
prompt = f"""
Analyse ce ticket de support:
TICKET: {ticket_info.get('code', 'N/A')} - {ticket_info.get('name', 'Sans titre')}
DATE: {ticket_info.get('date_create', 'Inconnue')}
DESCRIPTION:
{ticket_info.get('description', 'Aucune description')}
MESSAGES:
"""
# Ajouter les messages
for i, msg in enumerate(actual_messages):
role = msg.get("role", "Inconnu")
date = msg.get("date", "")
body = msg.get("body", "")
prompt += f"\n--- MESSAGE {i+1} ({role}, {date}) ---\n{body}\n"
# Ajouter les informations sur les images si disponibles
if infos_images:
prompt += "\n\nIMAGES ANALYSÉES:\n"
for image_path, analyse in infos_images.get("analyses", {}).items():
if analyse.get("pertinente", False):
prompt += f"- {image_path}: {analyse.get('description', 'Pas de description')} ({analyse.get('type_image', 'type inconnu')})\n"
# Demander une analyse structurée
prompt += """
Fais une analyse complète et structurée du ticket avec les sections suivantes:
1. PROBLÈME: Résume clairement le problème principal en 1-2 phrases
2. CATÉGORIE: Catégorise le problème (bug, demande de fonctionnalité, question, etc.)
3. GRAVITÉ: Évalue la gravité (Critique, Élevée, Moyenne, Faible)
4. SOLUTION: Résume la solution fournie ou indique qu'aucune solution n'a été fournie
5. EFFICACITÉ: Évalue si la solution a résolu le problème (Résolue, Partiellement résolue, Non résolue, Inconnue)
6. RÉSUMÉ: Fournis un résumé complet de l'incident en 3-5 phrases
7. POINTS CLÉS: Liste les 3-5 points les plus importants à retenir de ce ticket
Réponds en format Markdown bien structuré.
"""
try:
# Effectuer l'analyse avec le LLM
resultat = self.llm.generate_text(prompt, system_prompt=self.system_prompt)
self.ajouter_historique("analyze_ticket", f"{len(messages)} messages", "Analyse effectuée")
# Extraire le contenu
analyse_texte = resultat.get("content", "")
# Extraire les différentes sections
sections = {}
current_section = None
current_content = []
for line in analyse_texte.split("\n"):
# Détecter les en-têtes de section
if line.startswith("# "):
if current_section:
sections[current_section] = "\n".join(current_content).strip()
current_section = line[2:].strip().lower()
current_content = []
elif line.startswith("## "):
if current_section:
sections[current_section] = "\n".join(current_content).strip()
current_section = line[3:].strip().lower()
current_content = []
elif ":" in line and not "://" in line and not current_section:
# Cas des lignes "SECTION: contenu" sans formatage Markdown
parts = line.split(":", 1)
if len(parts) == 2 and parts[0].strip().upper() == parts[0].strip():
current_section = parts[0].strip().lower()
current_content = [parts[1].strip()]
else:
if current_section:
current_content.append(line)
else:
if current_section:
current_content.append(line)
# Ajouter la dernière section
if current_section:
sections[current_section] = "\n".join(current_content).strip()
# Si on n'a pas pu extraire les sections, utiliser tout le texte
if not sections:
sections = {
"analyse_complete": analyse_texte
}
# Créer le résultat final
resultat_analyse = {
"success": True,
"ticket_info": ticket_info,
"sections": sections,
"analyse_brute": analyse_texte,
"nb_messages": len(actual_messages)
}
logger.info("Analyse de ticket terminée avec succès")
return resultat_analyse
except Exception as e:
erreur = f"Erreur lors de l'analyse du ticket: {str(e)}"
logger.error(erreur)
return {
"success": False,
"erreur": erreur
}
def generer_rapport_markdown(self, analyse: Dict[str, Any]) -> str:
"""
Génère un rapport Markdown à partir de l'analyse du ticket.
Args:
analyse: Résultat de l'analyse du ticket
Returns:
Rapport au format Markdown
"""
if not analyse.get("success", False):
return f"# Échec de l'analyse\n\nErreur: {analyse.get('erreur', 'Inconnue')}"
ticket_info = analyse.get("ticket_info", {})
sections = analyse.get("sections", {})
# En-tête du rapport
rapport = f"# Rapport d'analyse de ticket\n\n"
rapport += f"**Ticket**: {ticket_info.get('code', 'N/A')} - {ticket_info.get('name', 'Sans titre')}\n"
rapport += f"**Date**: {ticket_info.get('date_create', 'Inconnue')}\n"
rapport += f"**Messages analysés**: {analyse.get('nb_messages', 0)}\n\n"
# Récupérer les sections principales
problem = sections.get("problème", sections.get("probleme", ""))
category = sections.get("catégorie", sections.get("categorie", ""))
severity = sections.get("gravité", sections.get("gravite", ""))
solution = sections.get("solution", "")
efficacy = sections.get("efficacité", sections.get("efficacite", ""))
summary = sections.get("résumé", sections.get("resume", ""))
key_points = sections.get("points clés", sections.get("points cles", ""))
# Ajouter les sections au rapport
if problem:
rapport += f"## Problème\n\n{problem}\n\n"
if category or severity:
rapport += "## Classification\n\n"
if category:
rapport += f"**Catégorie**: {category}\n\n"
if severity:
rapport += f"**Gravité**: {severity}\n\n"
if solution:
rapport += f"## Solution\n\n{solution}\n\n"
if efficacy:
rapport += f"**Efficacité**: {efficacy}\n\n"
if summary:
rapport += f"## Résumé\n\n{summary}\n\n"
if key_points:
rapport += f"## Points clés\n\n{key_points}\n\n"
# Ajouter les autres sections qui n'auraient pas été traitées
for name, content in sections.items():
if name not in ["problème", "probleme", "catégorie", "categorie",
"gravité", "gravite", "solution", "efficacité",
"efficacite", "résumé", "resume", "points clés",
"points cles", "analyse_complete"]:
rapport += f"## {name.title()}\n\n{content}\n\n"
# Ajouter le rapport complet si on n'a pas pu extraire les sections
if "analyse_complete" in sections and len(sections) == 1:
rapport += f"## Analyse complète\n\n{sections['analyse_complete']}\n\n"
# Ajouter les paramètres de l'analyse
rapport += "## Paramètres de l'analyse\n\n"
rapport += f"- **Modèle**: {self.llm.get_model()}\n"
rapport += f"- **Température**: {self.llm.get_temperature()}\n"
return rapport
def charger_config():
"""
Charge la configuration depuis config.json.
Returns:
Configuration chargée
"""
config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json")
if not os.path.exists(config_path):
logger.warning(f"Fichier de configuration non trouvé: {config_path}")
return {"llm": {"api_key": None}}
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
return config
except Exception as e:
logger.error(f"Erreur lors du chargement de la configuration: {str(e)}")
return {"llm": {"api_key": None}}
def main():
"""
Point d'entrée du script.
"""
parser = argparse.ArgumentParser(description="Analyse un ticket de support.")
parser.add_argument("--messages", "-m", required=True, help="Chemin vers le fichier messages.json")
parser.add_argument("--images-rapport", "-i", help="Chemin vers le rapport d'analyse d'images (filter_report.json)")
parser.add_argument("--output", "-o", help="Répertoire de sortie pour les rapports")
parser.add_argument("--format", "-f", choices=["json", "md", "both"], default="both",
help="Format de sortie (json, md, both)")
parser.add_argument("--verbose", "-v", action="store_true", help="Afficher plus d'informations")
args = parser.parse_args()
# Configurer le niveau de log
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
# Vérifier que le fichier messages existe
if not os.path.exists(args.messages):
logger.error(f"Fichier de messages non trouvé: {args.messages}")
sys.exit(1)
# Charger les messages
try:
with open(args.messages, 'r', encoding='utf-8') as f:
messages = json.load(f)
if not isinstance(messages, list):
logger.error(f"Format de fichier messages.json invalide. Une liste est attendue.")
sys.exit(1)
except Exception as e:
logger.error(f"Erreur lors du chargement des messages: {str(e)}")
sys.exit(1)
# Charger les informations sur les images si disponibles
images_info = None
if args.images_rapport and os.path.exists(args.images_rapport):
try:
with open(args.images_rapport, 'r', encoding='utf-8') as f:
images_info = json.load(f)
logger.info(f"Informations sur les images chargées: {args.images_rapport}")
except Exception as e:
logger.warning(f"Impossible de charger les informations sur les images: {str(e)}")
# Déterminer le répertoire de sortie
output_dir = args.output
if not output_dir:
# Par défaut, utiliser le même répertoire que le fichier messages
output_dir = os.path.dirname(args.messages)
if not output_dir:
output_dir = "."
# Créer le répertoire de sortie s'il n'existe pas
rapport_dir = os.path.join(output_dir, "rapport")
os.makedirs(rapport_dir, exist_ok=True)
# Charger la configuration
config = charger_config()
api_key = config.get("llm", {}).get("api_key")
# Initialiser l'analyseur de ticket
analyzer = TicketAnalyzer(api_key=api_key)
try:
# Analyser le ticket
resultat = analyzer.analyser_ticket(messages, images_info)
if not resultat.get("success", False):
logger.error(f"Échec de l'analyse: {resultat.get('erreur', 'Erreur inconnue')}")
sys.exit(1)
# Générer le rapport Markdown
rapport_md = analyzer.generer_rapport_markdown(resultat)
# Sauvegarder les résultats selon le format demandé
if args.format in ["json", "both"]:
json_path = os.path.join(rapport_dir, "ticket_analysis.json")
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(resultat, f, indent=2, ensure_ascii=False)
logger.info(f"Rapport JSON sauvegardé: {json_path}")
if args.format in ["md", "both"]:
md_path = os.path.join(rapport_dir, "ticket_analysis.md")
with open(md_path, 'w', encoding='utf-8') as f:
f.write(rapport_md)
logger.info(f"Rapport Markdown sauvegardé: {md_path}")
# Afficher un résumé
print("\nRésumé de l'analyse:")
print(f"Ticket: {resultat.get('ticket_info', {}).get('code', 'N/A')} - {resultat.get('ticket_info', {}).get('name', 'Sans titre')}")
print(f"Messages analysés: {resultat.get('nb_messages', 0)}")
print(f"Sections extraites: {len(resultat.get('sections', {}))}")
# Afficher un extrait du problème et de la solution
sections = resultat.get("sections", {})
probleme = sections.get("problème", sections.get("probleme", ""))
solution = sections.get("solution", "")
if probleme:
probleme_court = probleme[:150] + "..." if len(probleme) > 150 else probleme
print(f"\nProblème: {probleme_court}")
if solution:
solution_court = solution[:150] + "..." if len(solution) > 150 else solution
print(f"\nSolution: {solution_court}")
print(f"\nRappport complet sauvegardé dans: {rapport_dir}")
except Exception as e:
logger.error(f"Erreur lors de l'analyse: {str(e)}")
import traceback
logger.debug(f"Détails: {traceback.format_exc()}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,534 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script d'extraction des questions et réponses d'un ticket.
Génère un tableau Markdown avec les questions et réponses identifiées.
"""
import os
import sys
import json
import argparse
import logging
import re
from typing import Dict, List, Any, Optional
# Configuration du logger
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("extract_qr.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("extract_qr")
try:
from llm import Mistral
except ImportError:
logger.error("Module LLM non trouvé. Veuillez vous assurer que le répertoire parent est dans PYTHONPATH.")
sys.exit(1)
class QuestionReponseExtractor:
"""
Agent d'extraction des questions et réponses d'un ticket.
"""
def __init__(self, api_key: Optional[str] = None):
"""
Initialise l'agent d'extraction de questions-réponses.
Args:
api_key: Clé API pour le LLM
"""
self.llm = Mistral(api_key=api_key)
# Configurer le LLM
try:
self.llm.model = "mistral-medium"
self.llm.temperature = 0.3
self.llm.max_tokens = 2000
except Exception as e:
logger.warning(f"Impossible de configurer le modèle: {e}")
# Définir le prompt système par défaut
self.system_prompt = """
Tu es un expert en analyse de conversations de support technique.
Votre mission est d'identifier avec précision:
1. Le rôle de chaque intervenant (client ou support technique)
2. La nature de chaque message (question, réponse, information additionnelle)
3. Le contenu essentiel de chaque message en éliminant les formules de politesse,
signatures, mentions légales et autres éléments non pertinents
Pour l'identification client/support:
- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email
comme @cbao.fr, @odoo.com, mentions "support technique", etc.
- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions
Pour la classification en question/réponse:
- Questions: Demandes explicites (avec "?"), demandes implicites de résolution
de problèmes, descriptions de bugs ou dysfonctionnements
- Réponses: Explications techniques, solutions proposées, instructions fournies
par le support
Concentre-toi uniquement sur le contenu technique utile en ignorant tous les
éléments superflus qui n'apportent pas d'information sur le problème ou sa solution.
"""
self.historique = []
def ajouter_historique(self, action: str, entree: str, resultat: str) -> None:
"""
Ajoute une entrée à l'historique des actions.
Args:
action: Type d'action effectuée
entree: Entrée de l'action
resultat: Résultat de l'action
"""
self.historique.append({
"action": action,
"entree": entree,
"resultat": resultat
})
def nettoyer_contenu(self, texte: str) -> str:
"""
Nettoie le contenu en supprimant signatures, mentions légales, etc.
Args:
texte: Texte brut à nettoyer
Returns:
Texte nettoyé des éléments non pertinents
"""
# Si l'entrée n'est pas une chaîne, convertir en chaîne ou retourner vide
if not isinstance(texte, str):
if texte is None:
return ""
try:
texte = str(texte)
except:
return ""
# Détecter et supprimer les balises HTML avec regex robuste
try:
# Première passe - balises standard
texte_nettoye = re.sub(r'</?[a-z]+[^>]*>', ' ', texte, flags=re.IGNORECASE)
# Deuxième passe - balises restantes, y compris les mal formées
texte_nettoye = re.sub(r'<[^>]*>', ' ', texte_nettoye)
# Troisième passe pour les balises qui pourraient avoir échappé
texte_nettoye = re.sub(r'<[^>]*$', ' ', texte_nettoye) # Balises incomplètes à la fin
except Exception as e:
logger.warning(f"Erreur lors du nettoyage HTML: {e}")
texte_nettoye = texte
# Remplacer les références aux images
texte_nettoye = re.sub(r'\[Image:[^\]]+\]', '[Image]', texte_nettoye)
texte_nettoye = re.sub(r'<img[^>]+>', '[Image]', texte_nettoye, flags=re.IGNORECASE)
# Supprimer les éléments courants non pertinents
patterns_a_supprimer = [
r'Cordialement,[\s\S]*?$',
r'Bien cordialement,[\s\S]*?$',
r'Bonne réception[\s\S]*?$',
r'À votre disposition[\s\S]*?$',
r'Support technique[\s\S]*?$',
r'L\'objectif du Support Technique[\s\S]*?$',
r'Notre service est ouvert[\s\S]*?$',
r'Dès réception[\s\S]*?$',
r'Confidentialité[\s\S]*?$',
r'Ce message électronique[\s\S]*?$',
r'Droit à la déconnexion[\s\S]*?$',
r'Afin d\'assurer une meilleure traçabilité[\s\S]*?$',
r'tél\s*:\s*[\d\s\+]+',
r'mobile\s*:\s*[\d\s\+]+',
r'www\.[^\s]+\.[a-z]{2,3}',
r'\*{10,}.*?\*{10,}', # Lignes de séparation avec astérisques
r'----.*?----', # Lignes de séparation avec tirets
]
for pattern in patterns_a_supprimer:
texte_nettoye = re.sub(pattern, '', texte_nettoye, flags=re.IGNORECASE)
# Supprimer les lignes multiples vides et espaces multiples
texte_nettoye = re.sub(r'\n\s*\n', '\n', texte_nettoye)
texte_nettoye = re.sub(r'\s+', ' ', texte_nettoye)
# Convertir les entités HTML
html_entities = {
'&nbsp;': ' ', '&lt;': '<', '&gt;': '>', '&amp;': '&',
'&quot;': '"', '&apos;': "'", '&euro;': '', '&copy;': '©',
'&reg;': '®', '&eacute;': 'é', '&egrave;': 'è', '&agrave;': 'à',
'&ccedil;': 'ç', '&ecirc;': 'ê', '&acirc;': 'â', '&icirc;': 'î',
'&ocirc;': 'ô', '&ucirc;': 'û'
}
for entity, char in html_entities.items():
texte_nettoye = texte_nettoye.replace(entity, char)
return texte_nettoye.strip()
def detecter_role(self, message: Dict[str, Any]) -> str:
"""
Détecte si un message provient du client ou du support.
Args:
message: Dictionnaire contenant les informations du message
Returns:
"Client" ou "Support"
"""
# Vérifier le champ 'role' s'il existe déjà
if "role" in message and message["role"] in ["Client", "Support"]:
return message["role"]
# Indices de support dans l'email
domaines_support = ["@cbao.fr", "@odoo.com", "support@", "ticket.support"]
indices_nom_support = ["support", "cbao", "technique", "odoo"]
email = message.get("email_from", "").lower()
# Nettoyer le format "Nom <email@domaine.com>"
if "<" in email and ">" in email:
match = re.search(r'<([^>]+)>', email)
if match:
email = match.group(1).lower()
# Vérifier le domaine email
if any(domaine in email for domaine in domaines_support):
return "Support"
# Vérifier le nom d'auteur
auteur = ""
if "author_id" in message and isinstance(message["author_id"], list) and len(message["author_id"]) > 1:
auteur = str(message["author_id"][1]).lower()
elif "auteur" in message:
auteur = str(message["auteur"]).lower()
if any(indice in auteur for indice in indices_nom_support):
return "Support"
# Par défaut, considérer comme client
return "Client"
def extraire_questions_reponses(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Extrait les questions et réponses d'une liste de messages.
Args:
messages: Liste des messages du ticket
Returns:
Dictionnaire avec les questions et réponses extraites
"""
if not messages:
logger.warning("Aucun message à analyser")
return {
"success": False,
"erreur": "Aucun message à analyser",
"paires_qr": []
}
logger.info(f"Extraction des questions et réponses de {len(messages)} messages")
# Préparation des messages
messages_prepares = []
for msg in messages:
# Nettoyer le contenu
contenu = msg.get("body", "") or msg.get("contenu", "")
contenu_nettoye = self.nettoyer_contenu(contenu)
# Détecter le rôle
role = self.detecter_role(msg)
# Ajouter le message préparé si non vide après nettoyage
if contenu_nettoye.strip():
messages_prepares.append({
"id": msg.get("id", "") or msg.get("ID", ""),
"date": msg.get("date", ""),
"role": role,
"body": contenu_nettoye
})
# S'il n'y a pas assez de messages pour une conversation
if len(messages_prepares) < 2:
logger.warning("Pas assez de messages pour une conversation")
return {
"success": True,
"paires_qr": [],
"nb_questions": 0,
"nb_reponses": 0
}
# Trier par date si disponible
messages_prepares.sort(key=lambda x: x.get("date", ""))
# Préparer l'entrée pour le LLM
messages_for_llm = []
for i, msg in enumerate(messages_prepares):
messages_for_llm.append({
"numero": i + 1,
"role": msg.get("role", "Inconnu"),
"date": msg.get("date", ""),
"contenu": msg.get("body", "")
})
# Préparer le prompt pour extraire les paires Q/R
prompt = """
Analyse la conversation suivante et identifie toutes les paires de questions et réponses.
Pour chaque message:
1. Identifie s'il s'agit d'une question, d'une réponse ou d'une information.
2. Extrais le contenu essentiel en ignorant les formules de politesse et les signatures.
Ensuite, forme des paires de questions-réponses en associant chaque question avec sa réponse correspondante.
Réponds en utilisant la structure suivante:
```
MESSAGE 1:
- Rôle: [Client/Support]
- Type: [Question/Réponse/Information]
- Contenu essentiel: [Contenu essentiel du message]
MESSAGE 2:
...
PAIRE 1:
- Question (Client): [Question posée]
- Réponse (Support): [Réponse donnée]
PAIRE 2:
...
```
Si une question n'a pas de réponse, indique-le.
"""
try:
# Appeler le LLM pour l'analyse
from json import dumps
resultat = self.llm.chat_completion([
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": prompt + "\n\nConversation:\n" + dumps(messages_for_llm, indent=2)}
])
contenu = resultat.get("choices", [{}])[0].get("message", {}).get("content", "")
self.ajouter_historique("analyze_messages", f"{len(messages)} messages", "Analyse effectuée")
# Traiter la réponse pour extraire les messages analysés
messages_analyses = []
pattern_messages = r"MESSAGE (\d+):\s*- Rôle: (Client|Support)\s*- Type: (Question|Réponse|Information)\s*- Contenu essentiel: (.*?)(?=MESSAGE \d+:|PAIRE \d+:|$)"
for match in re.finditer(pattern_messages, contenu, re.DOTALL):
num = int(match.group(1))
role = match.group(2)
type_msg = match.group(3)
contenu_essentiel = match.group(4).strip()
# Trouver le message correspondant
msg_idx = num - 1
msg_id = ""
msg_date = ""
if 0 <= msg_idx < len(messages_for_llm):
original_idx = messages_for_llm[msg_idx]["numero"] - 1
if 0 <= original_idx < len(messages_prepares):
msg_id = messages_prepares[original_idx].get("id", "")
msg_date = messages_prepares[original_idx].get("date", "")
messages_analyses.append({
"id": msg_id,
"date": msg_date,
"role": role,
"type": type_msg,
"contenu": contenu_essentiel
})
# Extraire les paires QR
paires_qr = []
pattern_paires = r"PAIRE (\d+):\s*- Question \((Client|Support)\): (.*?)(?:\s*- Réponse \((Client|Support)\): (.*?))?(?=PAIRE \d+:|$)"
for match in re.finditer(pattern_paires, contenu, re.DOTALL):
num = match.group(1)
q_role = match.group(2)
question = match.group(3).strip()
r_role = match.group(4) if match.group(4) else ""
reponse = match.group(5).strip() if match.group(5) else ""
paires_qr.append({
"numero": num,
"question": {
"role": q_role,
"contenu": question
},
"reponse": {
"role": r_role,
"contenu": reponse
} if reponse else None
})
return {
"success": True,
"messages_analyses": messages_analyses,
"paires_qr": paires_qr,
"nb_questions": len(paires_qr),
"nb_reponses": sum(1 for p in paires_qr if p.get("reponse"))
}
except Exception as e:
erreur = f"Erreur lors de l'extraction des questions et réponses: {str(e)}"
logger.error(erreur)
return {
"success": False,
"erreur": erreur,
"paires_qr": []
}
def generer_tableau_markdown(self, paires_qr: List[Dict[str, Any]]) -> str:
"""
Génère un tableau Markdown avec les questions et réponses.
Args:
paires_qr: Liste de paires question/réponse
Returns:
Tableau Markdown formaté
"""
# Créer le tableau
markdown = ["# Analyse des Questions et Réponses\n"]
markdown.append("| Question | Réponse |")
markdown.append("|---------|---------|")
if not paires_qr:
# Si aucune paire n'a été trouvée, laisser le tableau vide
pass
else:
for paire in paires_qr:
question = paire.get("question", {})
reponse = paire.get("reponse", {})
q_role = question.get("role", "Client")
q_contenu = question.get("contenu", "")
if reponse:
r_role = reponse.get("role", "Support")
r_contenu = reponse.get("contenu", "")
markdown.append(f"| **{q_role}**: {q_contenu} | **{r_role}**: {r_contenu} |")
else:
markdown.append(f"| **{q_role}**: {q_contenu} | *Pas de réponse* |")
# Ajouter les informations sur les paramètres LLM utilisés
markdown.append("\n## Paramètres LLM utilisés\n")
markdown.append(f"- **Type de LLM**: Mistral")
markdown.append(f"- **Modèle**: {getattr(self.llm, 'model', 'mistral-medium')}")
markdown.append(f"- **Température**: {getattr(self.llm, 'temperature', 0.3)}")
markdown.append(f"- **Tokens max**: {getattr(self.llm, 'max_tokens', 2000)}")
return "\n".join(markdown)
def charger_config():
"""
Charge la configuration depuis config.json.
Returns:
Configuration chargée
"""
config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json")
if not os.path.exists(config_path):
logger.warning(f"Fichier de configuration non trouvé: {config_path}")
return {"llm": {"api_key": None}}
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
return config
except Exception as e:
logger.error(f"Erreur lors du chargement de la configuration: {str(e)}")
return {"llm": {"api_key": None}}
def main():
"""
Point d'entrée du script.
"""
parser = argparse.ArgumentParser(description="Extrait les questions et réponses d'un ticket de support.")
parser.add_argument("--messages", "-m", required=True, help="Chemin vers le fichier messages.json")
parser.add_argument("--output", "-o", help="Chemin du fichier de sortie pour le tableau Markdown (par défaut: <dossier_messages>/questions_reponses.md)")
parser.add_argument("--verbose", "-v", action="store_true", help="Afficher plus d'informations")
args = parser.parse_args()
# Configurer le niveau de log
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
# Vérifier que le fichier messages existe
if not os.path.exists(args.messages):
logger.error(f"Fichier de messages non trouvé: {args.messages}")
sys.exit(1)
# Charger les messages
try:
with open(args.messages, 'r', encoding='utf-8') as f:
messages = json.load(f)
if not isinstance(messages, list):
logger.error(f"Format de fichier messages.json invalide. Une liste est attendue.")
sys.exit(1)
except Exception as e:
logger.error(f"Erreur lors du chargement des messages: {str(e)}")
sys.exit(1)
# Déterminer le chemin de sortie
output_path = args.output
if not output_path:
# Par défaut, utiliser le même répertoire que le fichier messages
output_dir = os.path.dirname(args.messages)
if not output_dir:
output_dir = "."
output_path = os.path.join(output_dir, "questions_reponses.md")
# Charger la configuration
config = charger_config()
api_key = config.get("llm", {}).get("api_key")
# Initialiser l'extracteur de questions-réponses
extractor = QuestionReponseExtractor(api_key=api_key)
try:
# Extraire les questions et réponses
resultats = extractor.extraire_questions_reponses(messages)
if not resultats.get("success", False):
logger.error(f"Échec de l'extraction: {resultats.get('erreur', 'Erreur inconnue')}")
sys.exit(1)
# Générer le tableau Markdown
tableau_md = extractor.generer_tableau_markdown(resultats.get("paires_qr", []))
# Sauvegarder le tableau
with open(output_path, 'w', encoding='utf-8') as f:
f.write(tableau_md)
logger.info(f"Tableau Markdown sauvegardé: {output_path}")
# Afficher un résumé
print("\nRésumé de l'extraction:")
print(f"Messages analysés: {len(messages)}")
print(f"Questions extraites: {resultats.get('nb_questions', 0)}")
print(f"Réponses extraites: {resultats.get('nb_reponses', 0)}")
print(f"Tableau Markdown sauvegardé: {output_path}")
except Exception as e:
logger.error(f"Erreur lors de l'extraction: {str(e)}")
import traceback
logger.debug(f"Détails: {traceback.format_exc()}")
sys.exit(1)
if __name__ == "__main__":
main()

504
scripts/extract_ticket.py Normal file
View File

@ -0,0 +1,504 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script d'extraction et de prétraitement des tickets.
Nettoie les données et sépare les pièces jointes des messages.
"""
import os
import sys
import json
import re
import shutil
import argparse
import unicodedata
from typing import Dict, List, Any, Optional
from bs4 import BeautifulSoup
import logging
# Configuration du logger
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("extract_ticket.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("extract_ticket")
def nettoyer_html(texte: str) -> str:
"""
Nettoie le contenu HTML en utilisant BeautifulSoup.
Args:
texte: Texte HTML à nettoyer
Returns:
Texte nettoyé
"""
if not texte:
return ""
if not isinstance(texte, str):
try:
texte = str(texte)
except Exception as e:
logger.warning(f"Impossible de convertir en texte: {e}")
return ""
# Utiliser BeautifulSoup pour le nettoyage
try:
soup = BeautifulSoup(texte, 'html.parser')
texte_nettoye = soup.get_text(separator=' ')
except Exception as e:
logger.warning(f"Erreur lors du nettoyage HTML avec BeautifulSoup: {e}")
# Fallback à regex si BeautifulSoup échoue
try:
texte_nettoye = re.sub(r'<[^>]+>', ' ', texte)
except Exception as e:
logger.warning(f"Erreur lors du nettoyage HTML avec regex: {e}")
texte_nettoye = texte
# Remplacer les références aux images
texte_nettoye = re.sub(r'\[Image:[^\]]+\]', '[Image]', texte_nettoye)
# Supprimer les éléments courants non pertinents
patterns_a_supprimer = [
r'Cordialement,[\s\S]*?$',
r'Bien cordialement,[\s\S]*?$',
r'Bonne réception[\s\S]*?$',
r'À votre disposition[\s\S]*?$',
r'Support technique[\s\S]*?$',
r'L\'objectif du Support Technique[\s\S]*?$',
r'Notre service est ouvert[\s\S]*?$',
r'Dès réception[\s\S]*?$',
r'Confidentialité[\s\S]*?$',
r'Ce message électronique[\s\S]*?$',
r'Droit à la déconnexion[\s\S]*?$',
r'Afin d\'assurer une meilleure traçabilité[\s\S]*?$',
r'tél\s*:\s*[\d\s\+]+',
r'mobile\s*:\s*[\d\s\+]+',
r'www\.[^\s]+\.[a-z]{2,3}',
]
for pattern in patterns_a_supprimer:
texte_nettoye = re.sub(pattern, '', texte_nettoye, flags=re.IGNORECASE)
# Supprimer les lignes multiples vides
texte_nettoye = re.sub(r'\n\s*\n', '\n', texte_nettoye)
# Supprimer les espaces multiples
texte_nettoye = re.sub(r'\s+', ' ', texte_nettoye)
# Normaliser les caractères accentués
texte_nettoye = normaliser_accents(texte_nettoye)
return texte_nettoye.strip()
def normaliser_accents(texte: str) -> str:
"""
Normalise les caractères accentués pour éviter les problèmes d'encodage.
Args:
texte: Texte à normaliser
Returns:
Texte avec caractères accentués normalisés
"""
if not isinstance(texte, str):
if texte is None:
return ""
try:
texte = str(texte)
except:
return ""
# Convertir les caractères spéciaux HTML
special_chars = {
'&aacute;': 'á', '&eacute;': 'é', '&iacute;': 'í', '&oacute;': 'ó', '&uacute;': 'ú',
'&Aacute;': 'Á', '&Eacute;': 'É', '&Iacute;': 'Í', '&Oacute;': 'Ó', '&Uacute;': 'Ú',
'&agrave;': 'à', '&egrave;': 'è', '&igrave;': 'ì', '&ograve;': 'ò', '&ugrave;': 'ù',
'&Agrave;': 'À', '&Egrave;': 'È', '&Igrave;': 'Ì', '&Ograve;': 'Ò', '&Ugrave;': 'Ù',
'&acirc;': 'â', '&ecirc;': 'ê', '&icirc;': 'î', '&ocirc;': 'ô', '&ucirc;': 'û',
'&Acirc;': 'Â', '&Ecirc;': 'Ê', '&Icirc;': 'Î', '&Ocirc;': 'Ô', '&Ucirc;': 'Û',
'&atilde;': 'ã', '&etilde;': '', '&itilde;': 'ĩ', '&otilde;': 'õ', '&utilde;': 'ũ',
'&Atilde;': 'Ã', '&Etilde;': '', '&Itilde;': 'Ĩ', '&Otilde;': 'Õ', '&Utilde;': 'Ũ',
'&auml;': 'ä', '&euml;': 'ë', '&iuml;': 'ï', '&ouml;': 'ö', '&uuml;': 'ü',
'&Auml;': 'Ä', '&Euml;': 'Ë', '&Iuml;': 'Ï', '&Ouml;': 'Ö', '&Uuml;': 'Ü',
'&ccedil;': 'ç', '&Ccedil;': 'Ç', '&ntilde;': 'ñ', '&Ntilde;': 'Ñ',
'&nbsp;': ' ', '&lt;': '<', '&gt;': '>', '&amp;': '&', '&quot;': '"', '&apos;': "'",
'&euro;': '', '&copy;': '©', '&reg;': '®', '&trade;': ''
}
for html, char in special_chars.items():
texte = texte.replace(html, char)
# Normaliser les caractères composés
return unicodedata.normalize('NFC', texte)
def detecter_role(message: Dict[str, Any]) -> str:
"""
Détecte si un message provient du client ou du support.
Args:
message: Dictionnaire contenant les informations du message
Returns:
"Client" ou "Support"
"""
# Vérifier le champ 'role' s'il existe déjà
if "role" in message and message["role"] in ["Client", "Support"]:
return message["role"]
# Indices de support dans l'email
domaines_support = ["@cbao.fr", "@odoo.com", "support@", "ticket.support"]
indices_nom_support = ["support", "cbao", "technique", "odoo"]
email = message.get("email_from", "").lower()
# Nettoyer le format "Nom <email@domaine.com>"
if "<" in email and ">" in email:
match = re.search(r'<([^>]+)>', email)
if match:
email = match.group(1).lower()
# Vérifier le domaine email
if any(domaine in email for domaine in domaines_support):
return "Support"
# Vérifier le nom d'auteur
auteur = ""
if "author_id" in message and isinstance(message["author_id"], list) and len(message["author_id"]) > 1:
auteur = str(message["author_id"][1]).lower()
elif "auteur" in message:
auteur = str(message["auteur"]).lower()
if any(indice in auteur for indice in indices_nom_support):
return "Support"
# Par défaut, considérer comme client
return "Client"
def pretraiter_ticket(input_dir: str, output_dir: str) -> Dict[str, Any]:
"""
Prétraite les données d'un ticket et les sépare en fichiers distincts.
Args:
input_dir: Répertoire contenant les données brutes du ticket
output_dir: Répertoire sauvegarder les données prétraitées
Returns:
Rapport de prétraitement avec les fichiers générés
"""
logger.info(f"Prétraitement du ticket: {input_dir} -> {output_dir}")
# Créer le répertoire de sortie s'il n'existe pas
os.makedirs(output_dir, exist_ok=True)
# Créer les sous-répertoires
attachments_dir = os.path.join(output_dir, "attachments")
os.makedirs(attachments_dir, exist_ok=True)
# Chemins des fichiers d'entrée
ticket_info_path = os.path.join(input_dir, "ticket_info.json")
messages_path = os.path.join(input_dir, "messages.json")
messages_backup_path = os.path.join(input_dir, "messages.json.backup")
# Rapport de prétraitement
rapport = {
"ticket_id": os.path.basename(input_dir),
"fichiers_generes": [],
"erreurs": []
}
# Prétraiter ticket_info.json
if os.path.exists(ticket_info_path):
try:
with open(ticket_info_path, 'r', encoding='utf-8') as f:
ticket_info = json.load(f)
# Nettoyer la description
if isinstance(ticket_info, dict) and "description" in ticket_info:
ticket_info["description"] = nettoyer_html(ticket_info["description"])
# Sauvegarder dans le répertoire de sortie
output_ticket_info_path = os.path.join(output_dir, "ticket_info.json")
with open(output_ticket_info_path, 'w', encoding='utf-8') as f:
json.dump(ticket_info, f, indent=2, ensure_ascii=False)
rapport["fichiers_generes"].append("ticket_info.json")
logger.info(f"Ticket info prétraité et sauvegardé: {output_ticket_info_path}")
except Exception as e:
erreur = f"Erreur lors du prétraitement de ticket_info.json: {str(e)}"
rapport["erreurs"].append(erreur)
logger.error(erreur)
else:
erreur = f"Fichier ticket_info.json non trouvé dans {input_dir}"
rapport["erreurs"].append(erreur)
logger.warning(erreur)
# Prétraiter messages.json
messages_content = None
# D'abord essayer messages.json
if os.path.exists(messages_path):
try:
with open(messages_path, 'r', encoding='utf-8') as f:
messages_content = f.read()
except Exception as e:
logger.warning(f"Impossible de lire messages.json: {str(e)}")
# Si messages.json est vide ou corrompu, essayer la sauvegarde
if not messages_content and os.path.exists(messages_backup_path):
try:
with open(messages_backup_path, 'r', encoding='utf-8') as f:
messages_content = f.read()
logger.info("Utilisation de messages.json.backup comme source")
except Exception as e:
erreur = f"Impossible de lire messages.json.backup: {str(e)}"
rapport["erreurs"].append(erreur)
logger.error(erreur)
# Traiter les messages si nous avons un contenu valide
if messages_content:
try:
messages = json.loads(messages_content)
# Créer une version améliorée des messages
processed_messages = []
# Déterminer le code du ticket à partir du nom du répertoire
ticket_code = os.path.basename(input_dir)
if ticket_code.startswith("ticket_"):
ticket_code = ticket_code[7:] # Extraire le code sans "ticket_"
# Extraire les informations du ticket si disponibles
ticket_info_dict = {}
if os.path.exists(ticket_info_path):
try:
with open(ticket_info_path, 'r', encoding='utf-8') as f:
ticket_info_dict = json.load(f)
except Exception:
pass
# Créer le message de contexte avec les informations du ticket
ticket_name = ticket_info_dict.get("name", f"Ticket {ticket_code}")
ticket_description = ticket_info_dict.get("description", "")
ticket_date = ticket_info_dict.get("create_date", "")
# Nettoyer les informations du ticket
ticket_name = normaliser_accents(ticket_name)
ticket_description = nettoyer_html(ticket_description)
ticket_context = {
"id": "ticket_info",
"name": ticket_name,
"code": ticket_code,
"description": ticket_description,
"date_create": ticket_date,
"role": "system",
"type": "contexte",
"body": f"TICKET {ticket_code}: {ticket_name}.\n\nDESCRIPTION: {ticket_description or 'Aucune description disponible.'}"
}
processed_messages.append(ticket_context)
# Prétraiter chaque message
attachments_info = []
valid_messages = 0
for msg in messages:
if not isinstance(msg, dict):
continue
# Ignorer les messages vides
body = msg.get("body", "")
if not body or not isinstance(body, str):
continue
# Détecter le rôle
role = detecter_role(msg)
message_type = "Question" if role == "Client" else "Réponse"
# Nettoyer le contenu
contenu_nettoye = nettoyer_html(body)
if not contenu_nettoye:
continue
# Normaliser les champs textuels
email_from = normaliser_accents(msg.get("email_from", ""))
subject = normaliser_accents(msg.get("subject", ""))
# Gérer l'identifiant du message
msg_id = msg.get("id", f"msg_{valid_messages+1}")
if not isinstance(msg_id, str):
try:
msg_id = str(msg_id)
except:
msg_id = f"msg_{valid_messages+1}"
# Récupérer les autres champs de manière sécurisée
author_id = msg.get("author_id", [0, ""])
if not isinstance(author_id, list):
author_id = [0, ""]
date = msg.get("date", "")
if not isinstance(date, str):
try:
date = str(date)
except:
date = ""
# Traiter les pièces jointes si présentes
if "attachments" in msg and isinstance(msg["attachments"], list):
for attachment in msg["attachments"]:
if not isinstance(attachment, dict):
continue
attachment_data = attachment.get("datas")
attachment_name = attachment.get("name", "")
attachment_type = attachment.get("mimetype", "")
if attachment_data and attachment_name:
# Générer un nom de fichier unique
attachment_id = attachment.get("id", len(attachments_info) + 1)
safe_name = f"{attachment_id}_{attachment_name}"
file_path = os.path.join(attachments_dir, safe_name)
# Traiter différemment selon le type de pièce jointe
if attachment_type.startswith("image/"):
try:
# Sauvegarder l'image
import base64
with open(file_path, 'wb') as f:
f.write(base64.b64decode(attachment_data))
# Ajouter l'information à la liste des pièces jointes
attachments_info.append({
"id": attachment_id,
"name": attachment_name,
"mimetype": attachment_type,
"message_id": msg_id,
"date": date,
"file_path": file_path
})
logger.info(f"Pièce jointe sauvegardée: {file_path}")
except Exception as e:
logger.warning(f"Erreur lors de la sauvegarde de la pièce jointe {attachment_name}: {str(e)}")
# Créer le message transformé
processed_message = {
"id": msg_id,
"author_id": author_id,
"role": role,
"type": message_type,
"date": date,
"email_from": email_from,
"subject": subject,
"body": contenu_nettoye
}
processed_messages.append(processed_message)
valid_messages += 1
# Trier par date (sauf le premier message qui est le contexte)
try:
processed_messages[1:] = sorted(processed_messages[1:], key=lambda x: x.get("date", ""))
except Exception as e:
logger.warning(f"Impossible de trier les messages par date: {e}")
# Sauvegarder les messages prétraités
output_messages_path = os.path.join(output_dir, "messages.json")
with open(output_messages_path, 'w', encoding='utf-8') as f:
json.dump(processed_messages, f, indent=2, ensure_ascii=False)
rapport["fichiers_generes"].append("messages.json")
logger.info(f"Messages prétraités et sauvegardés: {output_messages_path} ({valid_messages} messages)")
# Sauvegarder les informations sur les pièces jointes
if attachments_info:
output_attachments_info_path = os.path.join(output_dir, "attachments_info.json")
with open(output_attachments_info_path, 'w', encoding='utf-8') as f:
json.dump(attachments_info, f, indent=2, ensure_ascii=False)
rapport["fichiers_generes"].append("attachments_info.json")
rapport["nb_attachments"] = len(attachments_info)
logger.info(f"Informations sur les pièces jointes sauvegardées: {output_attachments_info_path} ({len(attachments_info)} pièces jointes)")
except Exception as e:
erreur = f"Erreur lors du prétraitement des messages: {str(e)}"
rapport["erreurs"].append(erreur)
logger.error(erreur)
else:
erreur = "Aucun fichier messages.json ou messages.json.backup trouvé ou lisible"
rapport["erreurs"].append(erreur)
logger.error(erreur)
# Sauvegarder le rapport de prétraitement
rapport_path = os.path.join(output_dir, "pretraitement_rapport.json")
with open(rapport_path, 'w', encoding='utf-8') as f:
json.dump(rapport, f, indent=2, ensure_ascii=False)
logger.info(f"Rapport de prétraitement sauvegardé: {rapport_path}")
return rapport
def main():
"""
Point d'entrée du script.
"""
parser = argparse.ArgumentParser(description="Prétraite les données d'un ticket.")
parser.add_argument("input_dir", help="Répertoire contenant les données brutes du ticket")
parser.add_argument("--output-dir", help="Répertoire où sauvegarder les données prétraitées (par défaut: <input_dir>_processed)")
parser.add_argument("--verbose", "-v", action="store_true", help="Afficher plus d'informations")
args = parser.parse_args()
# Configurer le niveau de log
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
# Déterminer le répertoire de sortie
input_dir = args.input_dir
if not os.path.isdir(input_dir):
logger.error(f"Le répertoire d'entrée n'existe pas: {input_dir}")
sys.exit(1)
output_dir = args.output_dir
if not output_dir:
# Par défaut, ajouter "_processed" au nom du répertoire d'entrée
if input_dir.endswith("/"):
input_dir = input_dir[:-1]
output_dir = input_dir + "_processed"
# Prétraiter le ticket
try:
rapport = pretraiter_ticket(input_dir, output_dir)
# Afficher un résumé
print("\nRésumé du prétraitement:")
print(f"Ticket: {rapport['ticket_id']}")
print(f"Fichiers générés: {len(rapport['fichiers_generes'])}")
for fichier in rapport['fichiers_generes']:
print(f" - {fichier}")
if "nb_attachments" in rapport:
print(f"Pièces jointes: {rapport['nb_attachments']}")
if rapport['erreurs']:
print(f"Erreurs: {len(rapport['erreurs'])}")
for erreur in rapport['erreurs']:
print(f" - {erreur}")
else:
print("Aucune erreur")
print(f"\nPrétraitement terminé. Données sauvegardées dans: {output_dir}")
except Exception as e:
logger.error(f"Erreur lors du prétraitement: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()

380
scripts/filter_images.py Normal file
View File

@ -0,0 +1,380 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script de filtrage des images pertinentes dans un ticket.
Identifie et sépare les images utiles des images non pertinentes.
"""
import os
import sys
import json
import argparse
import logging
import datetime
import re
from typing import Dict, List, Any, Optional
# Configuration du logger
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("filter_images.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("filter_images")
try:
from llm import Pixtral # Importer le modèle d'analyse d'images
except ImportError:
logger.error("Module LLM non trouvé. Veuillez vous assurer que le répertoire parent est dans PYTHONPATH.")
sys.exit(1)
class ImageFilterAgent:
"""
Agent responsable du filtrage des images pertinentes.
"""
def __init__(self, api_key: Optional[str] = None):
"""
Initialise l'agent de filtrage d'images.
Args:
api_key: Clé API pour le modèle de vision
"""
self.llm = Pixtral(api_key=api_key)
# Configurer le modèle de vision
self.llm.set_model("pixtral-12b-2409")
self.llm.set_temperature(0.2)
self.llm.set_max_tokens(500)
self.historique = []
def ajouter_historique(self, action: str, entree: str, resultat: str) -> None:
"""
Ajoute une entrée à l'historique des actions.
Args:
action: Type d'action effectuée
entree: Entrée de l'action
resultat: Résultat de l'action
"""
self.historique.append({
"action": action,
"entree": entree,
"resultat": resultat,
"timestamp": datetime.datetime.now().isoformat()
})
def est_image_pertinente(self, image_path: str, contexte: Optional[str] = None) -> Dict[str, Any]:
"""
Détermine si une image est pertinente pour l'analyse du ticket.
Args:
image_path: Chemin vers l'image à analyser
contexte: Contexte du ticket pour aider à l'analyse
Returns:
Résultat de l'analyse avec la pertinence et le type d'image
"""
if not os.path.exists(image_path):
logger.warning(f"Image introuvable: {image_path}")
return {
"pertinente": False,
"type_image": "inconnue",
"description": "Image introuvable",
"erreur": "Fichier non trouvé"
}
try:
# Préparer le prompt pour l'analyse
prompt_base = """
Tu es un expert en analyse d'images techniques pour le support informatique.
Analyse cette image et détermine si elle est pertinente pour comprendre le problème technique décrit.
Une image pertinente est:
- Une capture d'écran montrant une interface, une erreur, ou une fonctionnalité logicielle
- Un schéma technique ou une illustration du problème
- Une photo d'un équipement ou d'un matériel en lien avec le ticket
Une image non pertinente est:
- Un logo, une signature, ou une image décorative
- Une photo de personne sans lien avec le problème technique
- Une image générique non liée au contexte du ticket
Réponds au format JSON avec les champs suivants:
- pertinente: boolean (true/false)
- type_image: string (capture_ecran, schéma, photo_équipement, logo, autre)
- description: string (description courte de ce que l'image montre)
- confiance: integer (niveau de confiance de 0 à 100)
- justification: string (pourquoi cette image est pertinente ou non)
"""
# Ajouter le contexte si disponible
if contexte:
prompt_base += f"\n\nContexte du ticket:\n{contexte}"
# Analyser l'image avec le modèle de vision
try:
resultat = self.llm.analyze_image(image_path, prompt_base)
self.ajouter_historique("analyze_image", os.path.basename(image_path), "Analyse effectuée")
except Exception as e:
logger.error(f"Erreur lors de l'appel au modèle de vision: {str(e)}")
return {
"pertinente": False,
"type_image": "inconnue",
"description": "Erreur d'analyse",
"erreur": str(e)
}
# Extraire le JSON de la réponse
json_match = re.search(r'```json\s*(.*?)\s*```', resultat.get("content", ""), re.DOTALL)
if json_match:
try:
analyse = json.loads(json_match.group(1))
return analyse
except Exception as e:
logger.error(f"Erreur lors du parsing JSON: {str(e)}")
else:
# Essayer de trouver un JSON sans les backticks
try:
# Nettoyer la réponse pour essayer d'extraire le JSON
content = resultat.get("content", "")
# Trouver les accolades ouvrantes et fermantes
start_idx = content.find('{')
end_idx = content.rfind('}')
if start_idx != -1 and end_idx != -1:
json_str = content[start_idx:end_idx+1]
analyse = json.loads(json_str)
return analyse
except Exception as e:
logger.error(f"Impossible d'extraire le JSON de la réponse: {str(e)}")
# Si on n'a pas pu extraire le JSON, analyser manuellement la réponse
content = resultat.get("content", "").lower()
est_pertinente = "pertinente" in content and not "non pertinente" in content
return {
"pertinente": est_pertinente,
"type_image": "inconnue" if not est_pertinente else "autre",
"description": "Analyse non structurée disponible",
"confiance": 50,
"reponse_brute": resultat.get("content", "")
}
except Exception as e:
logger.error(f"Erreur lors de l'analyse de l'image {image_path}: {str(e)}")
return {
"pertinente": False,
"type_image": "inconnue",
"description": "Erreur lors de l'analyse",
"erreur": str(e)
}
def filtrer_images(self, images_paths: List[str], contexte: Optional[str] = None) -> Dict[str, Any]:
"""
Analyse et filtre une liste d'images pour identifier celles qui sont pertinentes.
Args:
images_paths: Liste des chemins vers les images à analyser
contexte: Contexte du ticket pour aider à l'analyse
Returns:
Rapport de filtrage avec les images pertinentes et non pertinentes
"""
logger.info(f"Filtrage de {len(images_paths)} images...")
resultats = {
"images_pertinentes": [],
"images_non_pertinentes": [],
"erreurs": [],
"analyses": {}
}
for image_path in images_paths:
logger.info(f"Analyse de l'image: {os.path.basename(image_path)}")
# Vérifier que le fichier existe et est une image
if not os.path.exists(image_path):
logger.warning(f"Image introuvable: {image_path}")
resultats["erreurs"].append(f"Image introuvable: {image_path}")
continue
# Vérifier l'extension pour s'assurer que c'est une image
_, extension = os.path.splitext(image_path)
if extension.lower() not in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']:
logger.warning(f"Format de fichier non supporté: {image_path}")
resultats["erreurs"].append(f"Format de fichier non supporté: {image_path}")
continue
# Analyser l'image
analyse = self.est_image_pertinente(image_path, contexte)
# Ajouter à la catégorie appropriée
if "erreur" in analyse:
resultats["erreurs"].append(f"Erreur d'analyse pour {os.path.basename(image_path)}: {analyse['erreur']}")
resultats["analyses"][os.path.basename(image_path)] = analyse
elif analyse.get("pertinente", False):
resultats["images_pertinentes"].append(image_path)
resultats["analyses"][os.path.basename(image_path)] = analyse
logger.info(f"Image pertinente: {os.path.basename(image_path)} - {analyse.get('type_image', 'type inconnu')}")
else:
resultats["images_non_pertinentes"].append(image_path)
resultats["analyses"][os.path.basename(image_path)] = analyse
logger.info(f"Image non pertinente: {os.path.basename(image_path)}")
logger.info(f"Filtrage terminé. {len(resultats['images_pertinentes'])} images pertinentes, {len(resultats['images_non_pertinentes'])} non pertinentes, {len(resultats['erreurs'])} erreurs.")
return resultats
def charger_config():
"""
Charge la configuration depuis config.json.
Returns:
Configuration chargée
"""
config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json")
if not os.path.exists(config_path):
logger.warning(f"Fichier de configuration non trouvé: {config_path}")
return {"llm": {"api_key": None}}
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
return config
except Exception as e:
logger.error(f"Erreur lors du chargement de la configuration: {str(e)}")
return {"llm": {"api_key": None}}
def main():
"""
Point d'entrée du script.
"""
parser = argparse.ArgumentParser(description="Filtre les images pertinentes dans un ticket.")
parser.add_argument("--images", "-i", nargs="+", help="Liste des chemins vers les images à filtrer")
parser.add_argument("--contexte", "-c", help="Fichier JSON contenant le contexte du ticket")
parser.add_argument("--dossier-ticket", "-d", help="Dossier du ticket contenant attachments/ et ticket_info.json")
parser.add_argument("--output", "-o", help="Chemin du fichier de sortie pour le rapport JSON (par défaut: filter_report.json)")
parser.add_argument("--verbose", "-v", action="store_true", help="Afficher plus d'informations")
args = parser.parse_args()
# Configurer le niveau de log
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
# Charger la configuration
config = charger_config()
api_key = config.get("llm", {}).get("api_key")
# Initialiser l'agent de filtrage
agent = ImageFilterAgent(api_key=api_key)
# Déterminer les images à filtrer
images_paths = []
contexte = None
if args.images:
images_paths = args.images
elif args.dossier_ticket:
# Chercher dans le dossier attachments/ du ticket
attachments_dir = os.path.join(args.dossier_ticket, "attachments")
if os.path.isdir(attachments_dir):
# Récupérer toutes les images du dossier
for filename in os.listdir(attachments_dir):
file_path = os.path.join(attachments_dir, filename)
_, extension = os.path.splitext(filename)
if os.path.isfile(file_path) and extension.lower() in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']:
images_paths.append(file_path)
# Charger le contexte du ticket
ticket_info_path = os.path.join(args.dossier_ticket, "ticket_info.json")
if os.path.exists(ticket_info_path):
try:
with open(ticket_info_path, 'r', encoding='utf-8') as f:
ticket_info = json.load(f)
# Créer un contexte à partir des informations du ticket
contexte = f"""
TICKET: {ticket_info.get('code', 'Inconnu')} - {ticket_info.get('name', 'Sans titre')}
DESCRIPTION:
{ticket_info.get('description', 'Aucune description')}
"""
except Exception as e:
logger.warning(f"Impossible de charger le contexte depuis ticket_info.json: {str(e)}")
# Charger le contexte explicite si fourni
if args.contexte:
try:
with open(args.contexte, 'r', encoding='utf-8') as f:
if args.contexte.endswith('.json'):
contexte_data = json.load(f)
if isinstance(contexte_data, dict):
contexte = f"""
TICKET: {contexte_data.get('code', 'Inconnu')} - {contexte_data.get('name', 'Sans titre')}
DESCRIPTION:
{contexte_data.get('description', 'Aucune description')}
"""
else:
contexte = str(contexte_data)
else:
contexte = f.read()
except Exception as e:
logger.warning(f"Impossible de charger le contexte depuis {args.contexte}: {str(e)}")
# Vérifier que nous avons des images à traiter
if not images_paths:
logger.error("Aucune image à filtrer. Utilisez --images ou --dossier-ticket pour spécifier les images.")
sys.exit(1)
# Filtrer les images
try:
resultats = agent.filtrer_images(images_paths, contexte)
# Déterminer le chemin de sortie
output_path = args.output
if not output_path:
if args.dossier_ticket:
output_path = os.path.join(args.dossier_ticket, "filter_report.json")
else:
output_path = "filter_report.json"
# Sauvegarder le rapport
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(resultats, f, indent=2, ensure_ascii=False)
logger.info(f"Rapport de filtrage sauvegardé: {output_path}")
# Afficher un résumé
print("\nRésumé du filtrage:")
print(f"Images analysées: {len(images_paths)}")
print(f"Images pertinentes: {len(resultats['images_pertinentes'])}")
print(f"Images non pertinentes: {len(resultats['images_non_pertinentes'])}")
print(f"Erreurs: {len(resultats['erreurs'])}")
if resultats['images_pertinentes']:
print("\nImages pertinentes:")
for img in resultats['images_pertinentes']:
img_name = os.path.basename(img)
img_type = resultats['analyses'].get(img_name, {}).get('type_image', 'type inconnu')
print(f" - {img_name} ({img_type})")
if resultats['erreurs']:
print("\nErreurs:")
for err in resultats['erreurs']:
print(f" - {err}")
print(f"\nRapport complet sauvegardé dans: {output_path}")
except Exception as e:
logger.error(f"Erreur lors du filtrage des images: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()

7
utils/__init__.py Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from .ticket_analyzer import TicketAnalyzer
from .ticket_manager import TicketManager
__all__ = ['TicketAnalyzer', 'TicketManager']

Binary file not shown.

Binary file not shown.

Binary file not shown.

358
utils/ticket_analyzer.py Normal file
View File

@ -0,0 +1,358 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Utilitaire pour l'analyse de tickets de support utilisant les agents spécialisés.
"""
import os
import json
from datetime import datetime
from typing import Dict, List, Any, Optional
from agents import AgentFiltreImages, AgentAnalyseImage, AgentQuestionReponse
from post_process import normaliser_accents, corriger_markdown_accents
class TicketAnalyzer:
"""
Classe utilitaire pour analyser des tickets de support.
Coordonne le travail des différents agents spécialisés.
"""
def __init__(self, api_key: Optional[str] = None, llm_params: Optional[Dict[str, Any]] = None):
"""
Initialise l'analyseur de tickets.
Args:
api_key: Clé API pour les modèles LLM
llm_params: Paramètres globaux pour les LLM
"""
self.api_key = api_key
self.llm_params = llm_params or {}
# Initialisation des agents
self.agent_filtre = AgentFiltreImages(api_key=api_key)
self.agent_analyse = AgentAnalyseImage(api_key=api_key)
self.agent_qr = AgentQuestionReponse(api_key=api_key)
# Appliquer les paramètres globaux
self._appliquer_parametres_globaux()
# Journal d'analyse
self.entries = []
def _appliquer_parametres_globaux(self) -> None:
"""
Applique les paramètres globaux à tous les agents.
"""
if not self.llm_params:
return
print(f"Application des paramètres globaux LLM: {self.llm_params}")
self.agent_filtre.appliquer_parametres_globaux(self.llm_params)
self.agent_analyse.appliquer_parametres_globaux(self.llm_params)
self.agent_qr.appliquer_parametres_globaux(self.llm_params)
def filtrer_images(self, images_paths: List[str]) -> List[str]:
"""
Filtre les images pour ne conserver que celles pertinentes.
Args:
images_paths: Liste des chemins d'images à filtrer
Returns:
Liste des chemins d'images pertinentes
"""
images_pertinentes = []
for image_path in images_paths:
# Enregistrer le début de l'action
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
entry = {
"timestamp": timestamp,
"action": "filter_image",
"agent": self.agent_filtre.nom,
"llm": {"model": self.agent_filtre.llm.model},
"parametres_llm": self.agent_filtre.obtenir_parametres_llm(),
"image_path": image_path
}
# Exécuter le filtrage d'image
resultat = self.agent_filtre.executer(image_path)
# Ajouter le résultat à l'entrée
entry["response"] = resultat
# Ajouter au journal
self.entries.append(entry)
# Conserver l'image si pertinente
if resultat.get("pertinente", False):
images_pertinentes.append(image_path)
return images_pertinentes
def analyser_images(self, images_paths: List[str], contexte_ticket: Optional[str] = None) -> List[Dict[str, Any]]:
"""
Analyse les images pertinentes en détail.
Args:
images_paths: Liste des chemins d'images à analyser
contexte_ticket: Contexte du ticket pour une analyse plus pertinente
Returns:
Liste des résultats d'analyse
"""
resultats_analyses = []
for image_path in images_paths:
# Enregistrer le début de l'action
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
entry = {
"timestamp": timestamp,
"action": "analyze_image",
"agent": self.agent_analyse.nom,
"llm": {"model": self.agent_analyse.llm.model},
"parametres_llm": self.agent_analyse.obtenir_parametres_llm(),
"image_path": image_path
}
# Exécuter l'analyse d'image
resultat = self.agent_analyse.executer(image_path, contexte_ticket)
# Ajouter le résultat à l'entrée
entry["response"] = resultat.get("content", "")
if "usage" in resultat:
entry["tokens"] = resultat["usage"]
# Ajouter au journal
self.entries.append(entry)
# Ajouter aux résultats
if "error" not in resultat:
resultats_analyses.append({
"image_path": image_path,
"analyse": resultat.get("content", ""),
"usage": resultat.get("usage", {}),
"parametres_llm": self.agent_analyse.generer_rapport_parametres()
})
return resultats_analyses
def extraire_questions_reponses(self, messages: List[Dict[str, Any]], output_path: Optional[str] = None) -> Dict[str, Any]:
"""
Extrait les questions et réponses des messages du ticket.
Args:
messages: Liste des messages du ticket
output_path: Chemin sauvegarder le tableau Markdown (optionnel)
Returns:
Résultat de l'extraction avec tableau Markdown
"""
# Enregistrer le début de l'action
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
entry = {
"timestamp": timestamp,
"action": "extract_questions_reponses",
"agent": self.agent_qr.nom,
"llm": {"model": self.agent_qr.llm.model},
"parametres_llm": self.agent_qr.obtenir_parametres_llm()
}
# Exécuter l'extraction des questions/réponses
resultat = self.agent_qr.executer(messages, output_path)
# Ajouter le résultat à l'entrée
entry["response"] = resultat
# Ajouter au journal
self.entries.append(entry)
return resultat
def generer_rapport(self, output_path: str) -> Dict[str, str]:
"""
Génère un rapport complet d'analyse au format JSON et Markdown.
Args:
output_path: Chemin sauvegarder le rapport
Returns:
Dictionnaire avec les chemins des fichiers générés
"""
# Créer le répertoire de sortie si nécessaire
os.makedirs(output_path, exist_ok=True)
# Chemins des fichiers de sortie
json_path = os.path.join(output_path, "ticket_analysis.json")
md_path = os.path.join(output_path, "ticket_analysis.md")
# Sauvegarder au format JSON
with open(json_path, "w", encoding="utf-8") as f:
json.dump({"entries": self.entries}, f, indent=2, ensure_ascii=False)
# Générer le contenu Markdown
md_content = self._generer_markdown()
# Normaliser les accents dans le contenu Markdown avant de l'écrire
md_content = normaliser_accents(md_content)
# Sauvegarder au format Markdown
with open(md_path, "w", encoding="utf-8") as f:
f.write(md_content)
return {
"json": json_path,
"markdown": md_path
}
def _generer_markdown(self) -> str:
"""
Génère le contenu Markdown à partir des entrées du journal.
Returns:
Contenu Markdown formaté
"""
contenu = ["# Analyse de ticket de support\n"]
# Statistiques
stats = self._calculer_statistiques()
contenu.append("## Statistiques\n")
contenu.append(f"- Images analysées: {stats['images_total']} ({stats['images_pertinentes']} pertinentes)")
contenu.append(f"- Questions identifiées: {stats['questions']}")
contenu.append(f"- Réponses identifiées: {stats['reponses']}")
contenu.append("\n")
# Paramètres LLM globaux
if self.llm_params:
contenu.append("## Paramètres LLM globaux\n")
for param, valeur in self.llm_params.items():
contenu.append(f"- **{param}**: {valeur}")
contenu.append("\n")
# Paramètres LLM utilisés
contenu.append("## Paramètres LLM par agent\n")
# Filtre d'images
agent_filtre_params = self.agent_filtre.obtenir_parametres_llm()
contenu.append("### Agent de filtrage d'images\n")
contenu.append(f"- **Type de LLM**: {self.agent_filtre.llm.__class__.__name__}")
contenu.append(f"- **Modèle**: {agent_filtre_params.get('model', 'Non spécifié')}")
contenu.append(f"- **Température**: {agent_filtre_params.get('temperature', 'Non spécifiée')}")
contenu.append(f"- **Tokens max**: {agent_filtre_params.get('max_tokens', 'Non spécifié')}")
# Analyse d'images
agent_analyse_params = self.agent_analyse.obtenir_parametres_llm()
contenu.append("\n### Agent d'analyse d'images\n")
contenu.append(f"- **Type de LLM**: {self.agent_analyse.llm.__class__.__name__}")
contenu.append(f"- **Modèle**: {agent_analyse_params.get('model', 'Non spécifié')}")
contenu.append(f"- **Température**: {agent_analyse_params.get('temperature', 'Non spécifiée')}")
contenu.append(f"- **Tokens max**: {agent_analyse_params.get('max_tokens', 'Non spécifié')}")
# Questions-réponses
agent_qr_params = self.agent_qr.obtenir_parametres_llm()
contenu.append("\n### Agent d'extraction questions-réponses\n")
contenu.append(f"- **Type de LLM**: {self.agent_qr.llm.__class__.__name__}")
contenu.append(f"- **Modèle**: {agent_qr_params.get('model', 'Non spécifié')}")
contenu.append(f"- **Température**: {agent_qr_params.get('temperature', 'Non spécifiée')}")
contenu.append(f"- **Tokens max**: {agent_qr_params.get('max_tokens', 'Non spécifié')}")
contenu.append("\n")
# Actions chronologiques
contenu.append("## Journal d'actions\n")
for entry in self.entries:
timestamp = entry.get("timestamp", "")
action = entry.get("action", "")
agent = entry.get("agent", "")
model = entry.get("llm", {}).get("model", "")
# En-tête de l'action
contenu.append(f"### {timestamp} - {agent} ({model})")
contenu.append(f"**Action**: {action}")
# Détails spécifiques selon le type d'action
if action == "filter_image":
image_path = entry.get("image_path", "")
response = entry.get("response", {})
pertinente = response.get("pertinente", False)
type_image = response.get("type_image", "inconnue")
description = response.get("description", "")
contenu.append(f"**Image**: {os.path.basename(image_path)}")
contenu.append(f"**Résultat**: {'Pertinente' if pertinente else 'Non pertinente'}")
contenu.append(f"**Type**: {type_image}")
contenu.append(f"**Description**: {description}")
# Paramètres LLM utilisés
params_llm = response.get("parametres_llm", {})
if params_llm:
contenu.append("\n**Paramètres LLM utilisés:**")
temp = params_llm.get("parametres", {}).get("temperature", "N/A")
contenu.append(f"- Température: {temp}")
elif action == "analyze_image":
image_path = entry.get("image_path", "")
response = entry.get("response", "")
contenu.append(f"**Image analysée**: {os.path.basename(image_path)}")
contenu.append("\n**Analyse**:")
contenu.append(f"```\n{response}\n```")
elif action == "extract_questions_reponses":
response = entry.get("response", {})
tableau_md = response.get("tableau_md", "")
contenu.append(f"**Questions**: {response.get('nb_questions', 0)}")
contenu.append(f"**Réponses**: {response.get('nb_reponses', 0)}")
contenu.append("\n")
contenu.append(tableau_md)
# Paramètres LLM spécifiques à cette action
params_llm = entry.get("parametres_llm", {})
if params_llm and action != "filter_image": # Pour éviter la duplication avec le filtre d'image
contenu.append("\n**Paramètres LLM utilisés:**")
for key, value in params_llm.items():
if key != "system_prompt": # Éviter d'afficher le prompt système ici
contenu.append(f"- **{key}**: {value}")
# Tokens utilisés
if "tokens" in entry:
tokens = entry["tokens"]
contenu.append("\n**Tokens utilisés**:")
contenu.append(f"- Prompt: {tokens.get('prompt_tokens', 0)}")
contenu.append(f"- Completion: {tokens.get('completion_tokens', 0)}")
contenu.append(f"- Total: {tokens.get('total_tokens', 0)}")
contenu.append("\n---\n")
return "\n".join(contenu)
def _calculer_statistiques(self) -> Dict[str, int]:
"""
Calcule les statistiques à partir des entrées du journal.
Returns:
Dictionnaire de statistiques
"""
stats = {
"images_total": 0,
"images_pertinentes": 0,
"questions": 0,
"reponses": 0
}
for entry in self.entries:
action = entry.get("action", "")
if action == "filter_image":
stats["images_total"] += 1
if entry.get("response", {}).get("pertinente", False):
stats["images_pertinentes"] += 1
elif action == "extract_questions_reponses":
stats["questions"] = entry.get("response", {}).get("nb_questions", 0)
stats["reponses"] = entry.get("response", {}).get("nb_reponses", 0)
return stats

408
utils/ticket_manager.py Normal file
View File

@ -0,0 +1,408 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Module pour gérer l'extraction de tickets depuis Odoo.
Cette version est simplifiée et indépendante de odoo_toolkit.
"""
import os
import json
import base64
from typing import Dict, List, Any, Optional
import requests
from agents import AgentFiltreImages, AgentAnalyseImage, AgentQuestionReponse
class TicketManager:
"""
Gestionnaire de tickets pour extraire des données depuis Odoo.
"""
def __init__(self, url: str, db: str, username: str, api_key: str):
"""
Initialise le gestionnaire de tickets avec les paramètres de connexion.
Args:
url: URL du serveur Odoo
db: Nom de la base de données
username: Nom d'utilisateur
api_key: Clé API ou mot de passe
"""
self.url = url
self.db = db
self.username = username
self.api_key = api_key
self.uid = None
self.session_id = None
self.model_name = "project.task" # Modèle par défaut pour les tickets
def login(self) -> bool:
"""
Établit la connexion au serveur Odoo.
Returns:
True si la connexion réussit, False sinon
"""
try:
# Point d'entrée pour le login
login_url = f"{self.url}/web/session/authenticate"
# Données pour la requête de login
login_data = {
"jsonrpc": "2.0",
"params": {
"db": self.db,
"login": self.username,
"password": self.api_key
}
}
# Effectuer la requête
response = requests.post(login_url, json=login_data)
response.raise_for_status()
# Extraire les résultats
result = response.json()
if result.get("error"):
print(f"Erreur de connexion: {result['error']['message']}")
return False
# Récupérer l'ID utilisateur et la session
self.uid = result.get("result", {}).get("uid")
self.session_id = response.cookies.get("session_id")
if not self.uid:
print("Erreur: Impossible de récupérer l'ID utilisateur")
return False
print(f"Connecté avec succès à {self.url} (User ID: {self.uid})")
return True
except Exception as e:
print(f"Erreur de connexion: {str(e)}")
return False
def _ensure_connection(self) -> bool:
"""
Vérifie que la connexion est établie, tente de se reconnecter si nécessaire.
Returns:
True si la connexion est disponible, False sinon
"""
if not self.uid or not self.session_id:
return self.login()
return True
def _rpc_call(self, endpoint: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""
Effectue un appel RPC vers le serveur Odoo.
Args:
endpoint: Point d'entrée de l'API (/web/dataset/call_kw, etc.)
params: Paramètres de la requête
Returns:
Résultat de la requête
"""
if not self._ensure_connection():
return {"error": "Non connecté"}
try:
# Préparer la requête
full_url = f"{self.url}{endpoint}"
headers = {"Content-Type": "application/json"}
# Données de la requête
data = {
"jsonrpc": "2.0",
"method": "call",
"params": params
}
# Effectuer la requête
response = requests.post(
full_url,
json=data,
headers=headers,
cookies={"session_id": self.session_id} if self.session_id else None
)
response.raise_for_status()
# Traiter la réponse
result = response.json()
if result.get("error"):
return {"error": result["error"]["message"]}
return result.get("result", {})
except Exception as e:
return {"error": str(e)}
def search_read(self, model: str, domain: List, fields: List[str], limit: int = 0) -> List[Dict[str, Any]]:
"""
Recherche et lit des enregistrements selon un domaine.
Args:
model: Nom du modèle
domain: Domaine de recherche
fields: Champs à récupérer
limit: Nombre max de résultats (0 pour illimité)
Returns:
Liste des enregistrements trouvés
"""
params = {
"model": model,
"method": "search_read",
"args": [domain, fields],
"kwargs": {"limit": limit}
}
result = self._rpc_call("/web/dataset/call_kw", params)
if isinstance(result, dict) and "error" in result:
print(f"Erreur lors de la recherche: {result['error']}")
return []
return result if isinstance(result, list) else []
def read(self, model: str, ids: List[int], fields: List[str]) -> List[Dict[str, Any]]:
"""
Lit des enregistrements par leurs IDs.
Args:
model: Nom du modèle
ids: Liste des IDs à lire
fields: Champs à récupérer
Returns:
Liste des enregistrements lus
"""
params = {
"model": model,
"method": "read",
"args": [ids, fields],
"kwargs": {}
}
result = self._rpc_call("/web/dataset/call_kw", params)
if isinstance(result, dict) and "error" in result:
print(f"Erreur lors de la lecture: {result['error']}")
return []
return result if isinstance(result, list) else []
def get_ticket_by_code(self, ticket_code: str) -> Dict[str, Any]:
"""
Récupère un ticket par son code.
Args:
ticket_code: Code du ticket à récupérer
Returns:
Données du ticket ou dictionnaire vide si non trouvé
"""
# Rechercher l'ID du ticket par son code
tickets = self.search_read(
model=self.model_name,
domain=[("code", "=", ticket_code)],
fields=["id"],
limit=1
)
if not tickets:
print(f"Aucun ticket trouvé avec le code {ticket_code}")
return {}
# Récupérer toutes les données du ticket
ticket_id = tickets[0]["id"]
return self.get_ticket_by_id(ticket_id)
def get_ticket_by_id(self, ticket_id: int) -> Dict[str, Any]:
"""
Récupère un ticket par son ID.
Args:
ticket_id: ID du ticket à récupérer
Returns:
Données du ticket ou dictionnaire vide si non trouvé
"""
# Récupérer les champs disponibles pour le modèle
fields_info = self._get_model_fields(self.model_name)
# Lire les données du ticket
tickets = self.read(
model=self.model_name,
ids=[ticket_id],
fields=fields_info
)
if not tickets:
print(f"Aucun ticket trouvé avec l'ID {ticket_id}")
return {}
return tickets[0]
def _get_model_fields(self, model_name: str) -> List[str]:
"""
Récupère la liste des champs disponibles pour un modèle.
Args:
model_name: Nom du modèle
Returns:
Liste des noms de champs
"""
params = {
"model": model_name,
"method": "fields_get",
"args": [],
"kwargs": {"attributes": ["name", "type"]}
}
result = self._rpc_call("/web/dataset/call_kw", params)
if "error" in result:
print(f"Erreur lors de la récupération des champs: {result['error']}")
return []
# Filtrer les types de champs problématiques
invalid_types = ["many2many", "binary"]
valid_fields = [
field for field, info in result.items()
if info.get("type") not in invalid_types
]
return valid_fields
def get_ticket_messages(self, ticket_id: int) -> List[Dict[str, Any]]:
"""
Récupère les messages d'un ticket.
Args:
ticket_id: ID du ticket
Returns:
Liste des messages du ticket
"""
# D'abord récupérer les IDs des messages
ticket = self.read(
model=self.model_name,
ids=[ticket_id],
fields=["message_ids"]
)
if not ticket or "message_ids" not in ticket[0]:
print(f"Impossible de récupérer les messages pour le ticket {ticket_id}")
return []
message_ids = ticket[0]["message_ids"]
# Récupérer les détails des messages
messages = self.read(
model="mail.message",
ids=message_ids,
fields=["id", "body", "date", "author_id", "email_from", "subject", "parent_id"]
)
return messages
def get_ticket_attachments(self, ticket_id: int, download_path: Optional[str] = None) -> List[Dict[str, Any]]:
"""
Récupère les pièces jointes d'un ticket, avec option de téléchargement.
Args:
ticket_id: ID du ticket
download_path: Chemin télécharger les pièces jointes (optionnel)
Returns:
Liste des informations sur les pièces jointes
"""
# Rechercher les pièces jointes liées au ticket
attachments = self.search_read(
model="ir.attachment",
domain=[("res_model", "=", self.model_name), ("res_id", "=", ticket_id)],
fields=["id", "name", "mimetype", "create_date", "datas"]
)
if not attachments:
print(f"Aucune pièce jointe trouvée pour le ticket {ticket_id}")
return []
if download_path:
# Créer le répertoire si nécessaire
os.makedirs(download_path, exist_ok=True)
# Télécharger chaque pièce jointe
for attachment in attachments:
if "datas" in attachment and attachment["datas"]:
# Déchiffrer les données base64
binary_data = base64.b64decode(attachment["datas"])
# Nettoyer le nom de fichier
safe_name = attachment["name"].replace("/", "_").replace("\\", "_")
file_path = os.path.join(download_path, f"{attachment['id']}_{safe_name}")
# Sauvegarder le fichier
with open(file_path, "wb") as f:
f.write(binary_data)
# Remplacer les données binaires par le chemin du fichier
attachment["file_path"] = file_path
del attachment["datas"]
return attachments
def extract_ticket_data(self, ticket_id: int, output_dir: str) -> Dict[str, Any]:
"""
Extrait toutes les données d'un ticket, y compris messages et pièces jointes.
Args:
ticket_id: ID du ticket
output_dir: Répertoire de sortie
Returns:
Dictionnaire avec toutes les données du ticket
"""
# Créer le répertoire de sortie
os.makedirs(output_dir, exist_ok=True)
# Récupérer les données du ticket
ticket = self.get_ticket_by_id(ticket_id)
if not ticket:
return {"error": f"Ticket {ticket_id} non trouvé"}
# Sauvegarder les données du ticket
ticket_path = os.path.join(output_dir, "ticket_info.json")
with open(ticket_path, "w", encoding="utf-8") as f:
json.dump(ticket, f, indent=2, ensure_ascii=False)
# Récupérer et sauvegarder les messages
messages = self.get_ticket_messages(ticket_id)
messages_path = os.path.join(output_dir, "messages.json")
with open(messages_path, "w", encoding="utf-8") as f:
json.dump(messages, f, indent=2, ensure_ascii=False)
# Récupérer et sauvegarder les pièces jointes
attachments_dir = os.path.join(output_dir, "attachments")
attachments = self.get_ticket_attachments(ticket_id, attachments_dir)
attachments_path = os.path.join(output_dir, "attachments_info.json")
with open(attachments_path, "w", encoding="utf-8") as f:
json.dump(attachments, f, indent=2, ensure_ascii=False)
# Compiler toutes les informations
result = {
"ticket": ticket,
"messages": messages,
"attachments": [
{k: v for k, v in a.items() if k != "datas"}
for a in attachments
],
"files": {
"ticket_info": ticket_path,
"messages": messages_path,
"attachments_info": attachments_path,
"attachments_dir": attachments_dir
}
}
return result

247
venv/bin/Activate.ps1 Normal file
View File

@ -0,0 +1,247 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"

70
venv/bin/activate Normal file
View File

@ -0,0 +1,70 @@
# This file must be used with "source bin/activate" *from bash*
# You cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
# transform D:\path\to\venv to /d/path/to/venv on MSYS
# and to /cygdrive/d/path/to/venv on Cygwin
export VIRTUAL_ENV=$(cygpath /home/fgras-ca/llm-ticket3/venv)
else
# use the path as-is
export VIRTUAL_ENV=/home/fgras-ca/llm-ticket3/venv
fi
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"bin":$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1='(venv) '"${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT='(venv) '
export VIRTUAL_ENV_PROMPT
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null

27
venv/bin/activate.csh Normal file
View File

@ -0,0 +1,27 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /home/fgras-ca/llm-ticket3/venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = '(venv) '"$prompt"
setenv VIRTUAL_ENV_PROMPT '(venv) '
endif
alias pydoc python -m pydoc
rehash

69
venv/bin/activate.fish Normal file
View File

@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /home/fgras-ca/llm-ticket3/venv
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT '(venv) '
end

8
venv/bin/httpx Executable file
View File

@ -0,0 +1,8 @@
#!/home/fgras-ca/llm-ticket3/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from httpx import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/normalizer Executable file
View File

@ -0,0 +1,8 @@
#!/home/fgras-ca/llm-ticket3/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from charset_normalizer import cli
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli.cli_detect())

8
venv/bin/pip Executable file
View File

@ -0,0 +1,8 @@
#!/home/fgras-ca/llm-ticket3/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/pip3 Executable file
View File

@ -0,0 +1,8 @@
#!/home/fgras-ca/llm-ticket3/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
venv/bin/pip3.12 Executable file
View File

@ -0,0 +1,8 @@
#!/home/fgras-ca/llm-ticket3/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

1
venv/bin/python Symbolic link
View File

@ -0,0 +1 @@
python3

1
venv/bin/python3 Symbolic link
View File

@ -0,0 +1 @@
/usr/bin/python3

1
venv/bin/python3.12 Symbolic link
View File

@ -0,0 +1 @@
python3

View File

@ -0,0 +1,3 @@
Original Author
---------------
Sébastien Alix <sebastien.alix@osiell.com>, <seb@usr-src.org>

View File

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@ -0,0 +1,163 @@
Metadata-Version: 2.1
Name: OdooRPC
Version: 0.10.1
Summary: OdooRPC is a Python package providing an easy way to pilot your Odoo servers through RPC.
Home-page: https://github.com/OCA/odoorpc
Author: Sebastien Alix
Author-email: seb@usr-src.org
License: LGPL v3
Keywords: openerp odoo server rpc client xml-rpc xmlrpc jsonrpc json-rpc odoorpc oerplib communication lib library python service web webservice
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Framework :: Odoo
Description-Content-Type: text/x-rst
License-File: LICENSE
License-File: AUTHORS
=======
OdooRPC
=======
.. image:: https://img.shields.io/pypi/v/OdooRPC.svg
:target: https://pypi.python.org/pypi/OdooRPC/
:alt: Latest Version
.. image:: https://travis-ci.org/OCA/odoorpc.svg?branch=master
:target: https://travis-ci.org/OCA/odoorpc
:alt: Build Status
.. image:: https://img.shields.io/pypi/pyversions/OdooRPC.svg
:target: https://pypi.python.org/pypi/OdooRPC/
:alt: Supported Python versions
.. image:: https://img.shields.io/pypi/l/OdooRPC.svg
:target: https://pypi.python.org/pypi/OdooRPC/
:alt: License
**OdooRPC** is a Python package providing an easy way to
pilot your **Odoo** servers through `RPC`.
Features supported:
- access to all data model methods (even ``browse``) with an API similar
to the server-side API,
- use named parameters with model methods,
- user context automatically sent providing support for
internationalization,
- browse records,
- execute workflows,
- manage databases,
- reports downloading,
- JSON-RPC protocol (SSL supported),
How does it work? See below:
.. code-block:: python
import odoorpc
# Prepare the connection to the server
odoo = odoorpc.ODOO('localhost', port=8069)
# Check available databases
print(odoo.db.list())
# Login
odoo.login('db_name', 'user', 'passwd')
# Current user
user = odoo.env.user
print(user.name) # name of the user connected
print(user.company_id.name) # the name of its company
# Simple 'raw' query
user_data = odoo.execute('res.users', 'read', [user.id])
print(user_data)
# Use all methods of a model
if 'sale.order' in odoo.env:
Order = odoo.env['sale.order']
order_ids = Order.search([])
for order in Order.browse(order_ids):
print(order.name)
products = [line.product_id.name for line in order.order_line]
print(products)
# Update data through a record
user.name = "Brian Jones"
See the documentation for more details and features.
Supported Odoo server versions
==============================
`OdooRPC` is tested on all major releases of `Odoo` (starting from 8.0).
Supported Python versions
=========================
`OdooRPC` support Python 2.7, 3.7+.
License
=======
This software is made available under the `LGPL v3` license.
Generate the documentation
==========================
To generate the documentation, you have to install `Sphinx` documentation
generator::
pip install sphinx
Then, you can use the ``build_doc`` option of the ``setup.py``::
python setup.py build_doc
The generated documentation will be in the ``./doc/build/html`` directory.
Changes in this version
=======================
Consult the ``CHANGELOG`` file.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/odoorpc/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.
Credits
=======
Contributors
------------
* Sébastien Alix <sebastien.alix@osiell.com>
Do not contact contributors directly about support or help with technical issues.
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This package is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

View File

@ -0,0 +1,34 @@
OdooRPC-0.10.1.dist-info/AUTHORS,sha256=Kjdl6zj2iQulcwF4iADsfzyuusIPWLKsRK9rM2Bh4TY,95
OdooRPC-0.10.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
OdooRPC-0.10.1.dist-info/LICENSE,sha256=2n6rt7r999OuXp8iOqW9we7ORaxWncIbOwN1ILRGR2g,7651
OdooRPC-0.10.1.dist-info/METADATA,sha256=UuFVcRgJiOT8MOZ9sREZ4ebCik2JUuM8yckCO1HP9so,4803
OdooRPC-0.10.1.dist-info/RECORD,,
OdooRPC-0.10.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
OdooRPC-0.10.1.dist-info/WHEEL,sha256=m9WAupmBd2JGDsXWQGJgMGXIWbQY3F5c2xBJbBhq0nY,110
OdooRPC-0.10.1.dist-info/top_level.txt,sha256=qdAy2XwLvCFM_VdG79vIgP3UV43zLZmvNqbdk4L1b6E,8
odoorpc/__init__.py,sha256=Zk5CzvWtqvlGWNupLWb8OJQh69KdB9Mv8wfnmfTHVf8,2495
odoorpc/__pycache__/__init__.cpython-312.pyc,,
odoorpc/__pycache__/db.cpython-312.pyc,,
odoorpc/__pycache__/env.cpython-312.pyc,,
odoorpc/__pycache__/error.cpython-312.pyc,,
odoorpc/__pycache__/fields.cpython-312.pyc,,
odoorpc/__pycache__/models.cpython-312.pyc,,
odoorpc/__pycache__/odoo.cpython-312.pyc,,
odoorpc/__pycache__/report.cpython-312.pyc,,
odoorpc/__pycache__/session.cpython-312.pyc,,
odoorpc/__pycache__/tools.cpython-312.pyc,,
odoorpc/db.py,sha256=cBZzZvnNc5lBC-InKFfRGTBH4psG5mZJ8UOl0GDXt9k,10178
odoorpc/env.py,sha256=ncP9TnvCwtrD4aHcsv4rSeMXaXTUNajUgYAwQeAWXwQ,10119
odoorpc/error.py,sha256=QkGjqv5Y0aHxvtuV7oRiFbNhAXz8AK1srmMRLIc0gfU,3284
odoorpc/fields.py,sha256=Kf5af_m0TDz0k4lKFJLv75YUsu8ClwUOcsKWbTv8EHU,27004
odoorpc/models.py,sha256=4gsHOcqp8vhN4N9U66B5cnleSbf5gO93gqn7jEZN7Lc,15034
odoorpc/odoo.py,sha256=UQWQCJppn05XDOgpAdMRKXZEHH6Dv-LkFd6heJaAZ1w,22740
odoorpc/report.py,sha256=zF_XJDNyDmRDiMVjjQZtgnTBg4iFZZakrw6nUvE8U5k,7396
odoorpc/rpc/__init__.py,sha256=DFNJYDtwlCHo1d6xBAKV4bXziVoBJLJ8b-Bu85xIgvs,9465
odoorpc/rpc/__pycache__/__init__.cpython-312.pyc,,
odoorpc/rpc/__pycache__/error.cpython-312.pyc,,
odoorpc/rpc/__pycache__/jsonrpclib.cpython-312.pyc,,
odoorpc/rpc/error.py,sha256=LOb2kvZmXNGy5ZWw6W6UKWvF75YqmcVvL017budrnts,349
odoorpc/rpc/jsonrpclib.py,sha256=oY0eChMXUinC5YFjUcUO5ZWqt4ar9Dq2X0TJiFnpGb0,5342
odoorpc/session.py,sha256=YXGVVTKCZMzGCwxoGGeo_XDO04JK2rojrji7o9TuWC8,5567
odoorpc/tools.py,sha256=yYvMIreEDgZKSoQhZYD6W4xZpY2XppbTnttqHMR1i2w,3539

View File

@ -0,0 +1,6 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.41.1)
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any

View File

@ -0,0 +1 @@
odoorpc

Some files were not shown because too many files have changed in this diff Show More