| ## A streamlined .gitignore for modern .NET projects | |||||
| ## including temporary files, build results, and | |||||
| ## files generated by popular .NET tools. If you are | |||||
| ## developing with Visual Studio, the VS .gitignore | |||||
| ## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore | |||||
| ## has more thorough IDE-specific entries. | |||||
| ## | |||||
| ## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore | |||||
| # Build results | |||||
| [Dd]ebug/ | |||||
| [Dd]ebugPublic/ | |||||
| [Rr]elease/ | |||||
| [Rr]eleases/ | |||||
| x64/ | |||||
| x86/ | |||||
| [Ww][Ii][Nn]32/ | |||||
| [Aa][Rr][Mm]/ | |||||
| [Aa][Rr][Mm]64/ | |||||
| bld/ | |||||
| [Bb]in/ | |||||
| [Oo]bj/ | |||||
| [Ll]og/ | |||||
| [Ll]ogs/ | |||||
| # .NET Core | |||||
| project.lock.json | |||||
| project.fragment.lock.json | |||||
| artifacts/ | |||||
| # ASP.NET Scaffolding | |||||
| ScaffoldingReadMe.txt | |||||
| # NuGet Packages | |||||
| *.nupkg | |||||
| # NuGet Symbol Packages | |||||
| *.snupkg | |||||
| # Others | |||||
| ~$* | |||||
| *~ | |||||
| CodeCoverage/ | |||||
| # MSBuild Binary and Structured Log | |||||
| *.binlog | |||||
| # MSTest test Results | |||||
| [Tt]est[Rr]esult*/ | |||||
| [Bb]uild[Ll]og.* | |||||
| # NUnit | |||||
| *.VisualState.xml | |||||
| TestResult.xml | |||||
| nunit-*.xml | |||||
| /.vs | |||||
| *.onnx_data | |||||
| /DocumentsEntreprisesServices/ServicesExternes/ffmpeg/ffmpeg.exe | |||||
| /DocumentsEntreprisesServices/ServicesExternes/ffmpeg/ffplay.exe | |||||
| /DocumentsEntreprisesServices/ServicesExternes/ffmpeg/ffprobe.exe | |||||
| /DocumentsEntreprisesServices/ServicesExternes/ffmpeg/avcodec-62.dll | |||||
| /DocumentsEntreprisesServices/ServicesExternes/ffmpeg/avdevice-62.dll | |||||
| /DocumentsEntreprisesServices/ServicesExternes/ffmpeg/avfilter-11.dll | |||||
| /DocumentsEntreprisesServices/ServicesExternes/ffmpeg/avformat-62.dll | |||||
| /DocumentsEntreprisesServices/ServicesExternes/ffmpeg/avutil-60.dll | |||||
| /DocumentsEntreprisesServices/ServicesExternes/ffmpeg/swresample-6.dll | |||||
| /DocumentsEntreprisesServices/ServicesExternes/ffmpeg/swscale-9.dll | |||||
| /RAGService/onnx/model.onnx | |||||
| /RAGService/onnx/tokenizer.json |
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | |||||
| <TargetFramework>net8.0</TargetFramework> | |||||
| <ImplicitUsings>enable</ImplicitUsings> | |||||
| <Nullable>enable</Nullable> | |||||
| <PlatformTarget>x64</PlatformTarget> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\RAGService\RAGService.csproj" /> | |||||
| <ProjectReference Include="..\ReActAgentService\ReActAgentService.csproj" /> | |||||
| <ProjectReference Include="..\ToolsService\ToolsService.csproj" /> | |||||
| </ItemGroup> | |||||
| </Project> |
| using Services.ReActAgent; | |||||
| using System.Text.RegularExpressions; | |||||
| using System.Xml.Linq; | |||||
| using ToolsServices; | |||||
| namespace Services | |||||
| { | |||||
| public static class GenererCVService | |||||
| { | |||||
| #region Méthodes publiques | |||||
| public static async Task<bool> GenerateOneCV(string fullnameCV, string fullnameTemplateCV, string fullPathOutput, string titreCV, int nbrCompetencesMax, bool isCVAnonyme, bool isUsingIA) | |||||
| { | |||||
| LoggerService.LogInfo("GenererCVService.GenerateOneCV"); | |||||
| try | |||||
| { | |||||
| var reActRagAgent = new Services.ReActAgent.ReActAgent(); | |||||
| #region Logs + vérification de l'existence des fichiers | |||||
| LoggerService.LogInfo($"Génération d'un CV"); | |||||
| LoggerService.LogInfo($"Fichier CV : {fullnameCV}"); | |||||
| if (!File.Exists(fullnameCV)) | |||||
| { | |||||
| LoggerService.LogWarning($"Le fichier CV n'existe pas : {fullnameCV}."); | |||||
| return false; | |||||
| } | |||||
| if (!File.Exists(fullnameTemplateCV)) | |||||
| { | |||||
| LoggerService.LogWarning($"Le fichier Template de CV n'existe pas : {fullnameTemplateCV}."); | |||||
| return false; | |||||
| } | |||||
| if (!Directory.Exists(fullPathOutput)) | |||||
| { | |||||
| LoggerService.LogWarning($"Le dossier des exports des nouveaux CV n'existe pas : {fullPathOutput}."); | |||||
| return false; | |||||
| } | |||||
| #endregion | |||||
| string nameNewFileXML = ""; | |||||
| if (isUsingIA) | |||||
| { | |||||
| #region Vérification si injection de prompt | |||||
| bool isPromptInjectionCV = false; | |||||
| var cvText = reActRagAgent.CleanUserInput(FilesService.ExtractText(fullnameCV), out isPromptInjectionCV); | |||||
| if (isPromptInjectionCV) | |||||
| { | |||||
| LoggerService.LogWarning("Attention, le texte CV contient des éléments suspects"); | |||||
| } | |||||
| #endregion | |||||
| #region Appel au LLM pour extraire les données et les adapter | |||||
| /* | |||||
| var prompt1 = $@" | |||||
| Tu es un assistant RH. | |||||
| Voici un CV brut : | |||||
| <<< | |||||
| {cvText} | |||||
| >>> | |||||
| Voici une offre d’emploi : | |||||
| <<< | |||||
| {ficheMissionText} | |||||
| >>> | |||||
| Ta mission : | |||||
| - Réécris le CV de façon à ce qu’il corresponde au mieux à l’offre | |||||
| - Mets en avant les compétences pertinentes | |||||
| - Réorganise les expériences importantes | |||||
| - Reformule le résumé professionnel avec les bons mots-clés | |||||
| - Supprime les parties non utiles pour ce poste | |||||
| Rends un CV **entièrement réécrit** au format texte brut. Ne commente pas. | |||||
| "; | |||||
| */ | |||||
| var prompt1 = PromptService.GetPrompt(PromptService.ePrompt.GenererCVService_GenerateOneCV_Extract, cvText); | |||||
| LoggerService.LogInfo($"Génération du CV - phase A (extraction des infos) : {fullnameCV}"); | |||||
| var (reponse1, m1) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.CV_To_Xml, true, prompt1, "Génération de 1 CV - phase A"); | |||||
| var reponseCompleteClean1 = reponse1.Replace("\\n", "\n").Replace("\\r", "\r"); | |||||
| #endregion | |||||
| #region Appel au LLM pour mettre au format XML | |||||
| /* | |||||
| var prompt2 = $@" | |||||
| Voici un CV rédigé en texte. Reformate-le en XML structuré selon ce modèle : | |||||
| <CV> | |||||
| <Identite> | |||||
| <Nom></Nom> | |||||
| <Prenom></Prenom> | |||||
| <Email></Email> | |||||
| <Telephone></Telephone> | |||||
| <Adresse></Adresse> | |||||
| <DateDeNaissance></DateDeNaissance> | |||||
| <Nationalite></Nationalite> | |||||
| </Identite> | |||||
| <Profil></Profil> | |||||
| <Competences> | |||||
| <Competence nom="" niveau="" /> | |||||
| ... | |||||
| </Competences> | |||||
| <Langues> | |||||
| <Langue nom="" niveau="" /> | |||||
| ... | |||||
| </Langues> | |||||
| <Experiences> | |||||
| <Experience> | |||||
| <Poste></Poste> | |||||
| <Entreprise></Entreprise> | |||||
| <Lieu></Lieu> | |||||
| <DateDebut></DateDebut> | |||||
| <DateFin></DateFin> | |||||
| <Description></Description> | |||||
| </Experience> | |||||
| ... | |||||
| </Experiences> | |||||
| <Formations> | |||||
| <Formation> | |||||
| <Diplome></Diplome> | |||||
| <Etablissement></Etablissement> | |||||
| <DateDebut></DateDebut> | |||||
| <DateFin></DateFin> | |||||
| </Formation> | |||||
| ... | |||||
| </Formations> | |||||
| <Certifications> | |||||
| <Certification> | |||||
| <Nom></Nom> | |||||
| <Organisme></Organisme> | |||||
| <Date></Date> | |||||
| </Certification> | |||||
| ... | |||||
| </Certifications> | |||||
| </CV> | |||||
| Si une information est absente, laisse le champ vide. Ne commente pas. | |||||
| CV : | |||||
| <<< | |||||
| {reponseCompleteClean1} | |||||
| >>> | |||||
| "; | |||||
| */ | |||||
| var prompt2 = PromptService.GetPrompt(PromptService.ePrompt.GenererCVService_GenerateOneCV_Format, reponseCompleteClean1); | |||||
| LoggerService.LogInfo($"Génération du CV - phase B (formattage en XML) : {fullnameCV}"); | |||||
| var (reponse,m2) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.CV_To_Xml, true, prompt2, "Génération de 1 CV - phase B"); | |||||
| var reponseCompleteClean = reponse.Replace("\\n", "\n").Replace("\\r", "\r"); | |||||
| reponseCompleteClean = reponseCompleteClean.Replace("&", " et "); | |||||
| reponseCompleteClean = reponseCompleteClean.Replace("</start_of_turn>", ""); | |||||
| #endregion | |||||
| #region Enregistrer le XML | |||||
| // Création du XML | |||||
| nameNewFileXML = $"{fullnameCV}_{DateTime.Now.ToString("yyyyMMdd_hhmmss")}.xml"; | |||||
| if (File.Exists(nameNewFileXML)) | |||||
| File.Delete(nameNewFileXML); | |||||
| System.IO.File.WriteAllText(nameNewFileXML, reponseCompleteClean); | |||||
| #endregion | |||||
| } | |||||
| else | |||||
| { | |||||
| LoggerService.LogInfo($"Pas de modèle IA utilisé"); | |||||
| #region Extraction du texte + générer un XML | |||||
| var cv = FilesService.ExtractText(fullnameCV); | |||||
| cv = Regex.Replace(cv, @"\s{2,}", " ").Trim(); | |||||
| var xml = GenerateCvXml(cv); | |||||
| nameNewFileXML = $"{fullnameCV}_{DateTime.Now.ToString("yyyyMMdd_hhmmss")}.xml"; | |||||
| if (File.Exists(nameNewFileXML)) | |||||
| File.Delete(nameNewFileXML); | |||||
| xml.Save(nameNewFileXML); | |||||
| #endregion | |||||
| } | |||||
| #region Génération du CV sur la base du XML et d'un template | |||||
| // Génération du Word | |||||
| var t1 = System.IO.Path.GetFileNameWithoutExtension(fullnameCV); | |||||
| var t2 = System.IO.Path.GetFileNameWithoutExtension(fullnameTemplateCV); | |||||
| var fullnameNewCV = System.IO.Path.Combine(fullPathOutput, $"{t2}_{t1}.docx"); | |||||
| var b = DocxService.Genere_Docx(fullnameTemplateCV, nameNewFileXML, fullnameNewCV, titreCV, nbrCompetencesMax, isCVAnonyme); | |||||
| if (File.Exists(nameNewFileXML)) | |||||
| File.Delete(nameNewFileXML); | |||||
| #endregion | |||||
| return b; | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"Erreur dans la génération du CV (GenererCVService.GenerateOneCV) : {ex.Message}"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes privées | |||||
| private static XElement GenerateCvXml(string text) | |||||
| { | |||||
| LoggerService.LogInfo("GenererCVService.GenerateCvXml"); | |||||
| // === Nom / Prénom === | |||||
| var nameMatch = Regex.Match(text, @"\b([A-Z][a-zéèàêâïî\-']+)\s+([A-ZÉÈÊËÀÂÄÎÏÔÖÙÛÜÇ\-']{2,})\b"); | |||||
| string prenom = nameMatch.Success ? nameMatch.Groups[1].Value : ""; | |||||
| string nom = nameMatch.Success ? nameMatch.Groups[2].Value : ""; | |||||
| // === Email === | |||||
| var emailMatch = Regex.Match(text, @"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,}"); | |||||
| string email = emailMatch.Success ? emailMatch.Value : ""; | |||||
| // === Téléphone === | |||||
| var telMatch = Regex.Match(text, @"0[1-9](?:[\s.-]?\d{2}){4}"); | |||||
| string telephone = telMatch.Success ? telMatch.Value : ""; | |||||
| // === Profil === | |||||
| string profil = ExtractSection(text, "PROFIL", "FORMATIONS"); | |||||
| // === Compétences === | |||||
| var competenceMatches = Regex.Matches(text, @"•\s?(.*?)(?=•|PROJECT MANAGER|EXPERIENCES CLES|PROFIL|$)"); | |||||
| var competences = new XElement("Competences"); | |||||
| foreach (Match match in competenceMatches) | |||||
| { | |||||
| string label = match.Groups[1].Value.Trim(); | |||||
| if (!string.IsNullOrWhiteSpace(label) && label.Contains(":")) | |||||
| { | |||||
| string domaine = label.Split(":")[0].Trim(); | |||||
| competences.Add(new XElement("Competence", | |||||
| new XAttribute("nom", domaine), | |||||
| new XAttribute("niveau", "Non précisé") | |||||
| )); | |||||
| } | |||||
| } | |||||
| // === Langues === | |||||
| var langues = new XElement("Langues"); | |||||
| if (text.Contains("Anglais")) langues.Add(new XElement("Langue", new XAttribute("nom", "Anglais"), new XAttribute("niveau", "Professionnel (Bulats B1)"))); | |||||
| if (text.Contains("Espagnol")) langues.Add(new XElement("Langue", new XAttribute("nom", "Espagnol"), new XAttribute("niveau", "Scolaire"))); | |||||
| // === Expériences === | |||||
| var experiences = new XElement("Experiences"); | |||||
| var expMatches = Regex.Matches(text, @"(\d{4})\s*-\s*(.+?)\s+([A-Z].*?)\s+-\s+(.+?)\s+(.*?)\s(?=\d{4}\s*-|$)", RegexOptions.Singleline); | |||||
| foreach (Match match in expMatches) | |||||
| { | |||||
| experiences.Add(new XElement("Experience", | |||||
| new XElement("Poste", match.Groups[2].Value.Trim()), | |||||
| new XElement("Entreprise", match.Groups[3].Value.Trim()), | |||||
| new XElement("Lieu", match.Groups[4].Value.Trim()), | |||||
| new XElement("DateDebut", match.Groups[1].Value.Trim()), | |||||
| new XElement("DateFin", ""), // à adapter si fin détectable | |||||
| new XElement("Description", match.Groups[5].Value.Trim()) | |||||
| )); | |||||
| } | |||||
| // === Formations === | |||||
| var formations = new XElement("Formations"); | |||||
| var formationMatches = Regex.Matches(text, @"(\d{4})\s*–\s*(\d{4})\s*---\s*(.+?)\((\d{2})\)"); | |||||
| foreach (Match match in formationMatches) | |||||
| { | |||||
| formations.Add(new XElement("Formation", | |||||
| new XElement("Diplome", "Non précisé"), | |||||
| new XElement("Etablissement", match.Groups[3].Value.Trim()), | |||||
| new XElement("DateDebut", match.Groups[1].Value.Trim()), | |||||
| new XElement("DateFin", match.Groups[2].Value.Trim()) | |||||
| )); | |||||
| } | |||||
| // Ajout manuel du Master si non capturé | |||||
| if (!text.Contains("IAE de Montpellier")) | |||||
| { | |||||
| formations.Add(new XElement("Formation", | |||||
| new XElement("Diplome", "Master 2 : Administration des Entreprises"), | |||||
| new XElement("Etablissement", "IAE de Montpellier"), | |||||
| new XElement("DateDebut", "2009"), | |||||
| new XElement("DateFin", "2011") | |||||
| )); | |||||
| } | |||||
| return new XElement("CV", | |||||
| new XElement("Identite", | |||||
| new XElement("Nom", nom), | |||||
| new XElement("Prenom", prenom), | |||||
| new XElement("Email", email), | |||||
| new XElement("Telephone", telephone), | |||||
| new XElement("Adresse", ""), | |||||
| new XElement("DateDeNaissance", ""), | |||||
| new XElement("Nationalite", "") | |||||
| ), | |||||
| new XElement("Profil", profil), | |||||
| competences, | |||||
| langues, | |||||
| experiences, | |||||
| formations, | |||||
| new XElement("Certifications") | |||||
| ); | |||||
| } | |||||
| private static string ExtractSection(string text, string startLabel, string endLabel) | |||||
| { | |||||
| LoggerService.LogInfo("GenererCVService.ExtractSection"); | |||||
| int start = text.IndexOf(startLabel, StringComparison.OrdinalIgnoreCase); | |||||
| int end = text.IndexOf(endLabel, start + startLabel.Length, StringComparison.OrdinalIgnoreCase); | |||||
| if (start >= 0 && end > start) | |||||
| { | |||||
| return text.Substring(start + startLabel.Length, end - start - startLabel.Length).Trim(); | |||||
| } | |||||
| return ""; | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| } |
| using ToolsServices; | |||||
| using Services.ReActAgent; | |||||
| using System.Collections.ObjectModel; | |||||
| using System.Text; | |||||
| using System.Text.Json; | |||||
| using System.Text.Json.Serialization; | |||||
| namespace Services | |||||
| { | |||||
| public static class RechercheCVService | |||||
| { | |||||
| #region Variables | |||||
| private static readonly string NomFichierRechercheCV = FichiersInternesService.ListeRechercheCV;// "listeRechercheCV.json"; | |||||
| private static readonly string NomFichierParametres = FichiersInternesService.ParamsRechercheCV; | |||||
| #endregion | |||||
| #region Méthodes publiques | |||||
| public static (string, string) LoadParametres() | |||||
| { | |||||
| LoggerService.LogInfo("RechercheCVService.LoadParametres"); | |||||
| //ParametresOllamaService SelectedItem = new(); | |||||
| try | |||||
| { | |||||
| string FicheMission=""; | |||||
| string PathCV=""; | |||||
| if (File.Exists(NomFichierParametres)) | |||||
| { | |||||
| string[] lignes = File.ReadAllLines(NomFichierParametres); | |||||
| if (lignes.Length > 0) | |||||
| FicheMission = lignes[0]; | |||||
| if (lignes.Length > 1) | |||||
| PathCV = lignes[1]; | |||||
| } | |||||
| return (FicheMission, PathCV); | |||||
| } | |||||
| catch | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors du chargement des paramètres depuis {NomFichierParametres}"); | |||||
| return ("",""); | |||||
| } | |||||
| } | |||||
| public static bool SaveParametres(string FicheMission, string PathCV) | |||||
| { | |||||
| LoggerService.LogInfo("RechercheCVService.SaveParametres"); | |||||
| try | |||||
| { | |||||
| StringBuilder sb = new(); | |||||
| sb.AppendLine(FicheMission); | |||||
| sb.AppendLine(PathCV); | |||||
| File.WriteAllText(NomFichierParametres, sb.ToString()); | |||||
| return true; | |||||
| } | |||||
| catch | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors de la sauvegarde des paramètres dans {NomFichierParametres}"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| public static async Task<List<ReponseRechercheCV>?> RechercherCV(string ficheMission, string dossierCV, bool isGenererXML) | |||||
| { | |||||
| LoggerService.LogInfo("RechercheCVService.RechercheCV"); | |||||
| var reActRagAgent = new ReActAgent.ReActAgent(); | |||||
| var modeleIA = ReActAgent.ReActAgent.GetModeleIA(ModelsUseCases.TypeUseCase.AnalyseCVMission); | |||||
| LoggerService.LogInfo($"Analyse des CV"); | |||||
| LoggerService.LogInfo($"Fiche mission : {ficheMission}"); | |||||
| LoggerService.LogInfo($"Dossier CV : {dossierCV}"); | |||||
| LoggerService.LogInfo($"Extraire les CV en XML : {isGenererXML}"); | |||||
| LoggerService.LogInfo($"Modèle utilisé : {modeleIA}"); | |||||
| if (!Directory.Exists(dossierCV)) | |||||
| { | |||||
| LoggerService.LogWarning($"Le dossier {dossierCV} n'existe pas."); | |||||
| return null; | |||||
| } | |||||
| if (!File.Exists(ficheMission)) | |||||
| { | |||||
| LoggerService.LogWarning($"Le fichier {ficheMission} n'existe pas."); | |||||
| return null; | |||||
| } | |||||
| var fichiers = Directory | |||||
| .EnumerateFiles(dossierCV, "*.*", SearchOption.AllDirectories) | |||||
| .Where(f => f.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase) | |||||
| || f.EndsWith(".docx", StringComparison.OrdinalIgnoreCase) | |||||
| || f.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase)) | |||||
| .ToArray(); | |||||
| if (fichiers.Length == 0) | |||||
| { | |||||
| LoggerService.LogWarning($"Aucun fichier trouvé dans le dossier spécifié : {dossierCV}"); | |||||
| return null; | |||||
| } | |||||
| string messagePromptInjection1 = ""; | |||||
| bool isPromptInjection = false; | |||||
| var ficheMissionText = reActRagAgent.CleanUserInput(FilesService.ExtractText(ficheMission), out isPromptInjection); | |||||
| if(isPromptInjection) | |||||
| { | |||||
| var msg = "Attention, le texte de la fiche de mission contient des éléments suspects"; | |||||
| messagePromptInjection1 = $"{msg}.\n"; | |||||
| LoggerService.LogWarning(msg); | |||||
| } | |||||
| List<ReponseRechercheCV> retours = new(); | |||||
| foreach (var fichier in fichiers) | |||||
| { | |||||
| var bIngest = await RAGService.IngestDocument(Domain.CV, true, false, FilesService.ExtractText(fichier), fichier); | |||||
| } | |||||
| foreach (var fichier in fichiers) | |||||
| { | |||||
| try | |||||
| { | |||||
| #region Analyse du CV | |||||
| ReponseRechercheCV retour = new(); | |||||
| string messagePromptInjection2 = ""; | |||||
| var texteCV = reActRagAgent.CleanUserInput(FilesService.ExtractText(fichier), out isPromptInjection); | |||||
| if(isPromptInjection) | |||||
| { | |||||
| var msg = "Attention, le texte de la fiche de mission contient des éléments suspects"; | |||||
| messagePromptInjection2 = $"{msg}.\n"; | |||||
| retour.PresenceSuspecte = true; | |||||
| LoggerService.LogWarning($"{msg}: {fichier}"); | |||||
| } | |||||
| /* | |||||
| var prompt = $@" | |||||
| Tu es un expert en recrutement. Ton rôle est d'évaluer la compatibilité entre une offre d'emploi et un CV. | |||||
| Analyse comparative : | |||||
| 1. **Points forts** : liste les éléments du CV qui correspondent bien aux exigences de l'offre (compétences, expériences, formations, etc.). | |||||
| 2. **Points manquants ou faibles** : identifie les éléments de l'offre qui sont absents ou peu développés dans le CV. | |||||
| 3. **Note de compatibilité** : donne une note globale de correspondance entre le CV et l'offre, sur 10, avec une explication. | |||||
| Voici les documents à comparer : | |||||
| --- | |||||
| **Offre d’emploi** : | |||||
| {ficheMissionText} | |||||
| --- | |||||
| **CV du candidat** : | |||||
| {texteCV} | |||||
| --- | |||||
| Réponds en suivant la structure suivante : | |||||
| **Points forts :** | |||||
| - … | |||||
| **Points manquants ou faibles :** | |||||
| - … | |||||
| **Note de compatibilité (sur 10) :** | |||||
| X/10 – Explication : … | |||||
| "; | |||||
| */ | |||||
| var prompt = PromptService.GetPrompt(PromptService.ePrompt.RechercheCVService_RechercheCV_Analyse, ficheMissionText, texteCV); | |||||
| LoggerService.LogInfo($"Analyse du CV : {fichier}"); | |||||
| var (reponse,m1) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.AnalyseCVMission, true, prompt,"Analyse de CV"); | |||||
| var reponseComplete = $"Pour le CV {fichier} :\n{reponse}\n"; | |||||
| //var reponseClean = reponse.Replace("\\n", "\n").Replace("\\r", "\r"); | |||||
| var reponseCompleteClean = reponseComplete.Replace("\\n", "\n").Replace("\\r", "\r"); | |||||
| retour.FichierCV = fichier; | |||||
| retour.Avis = messagePromptInjection1 + messagePromptInjection2 + reponseCompleteClean; | |||||
| // Note : On peut ajouter une logique pour extraire la note de la réponse si nécessaire | |||||
| if (reponse.Contains("Note de compatibilité")) | |||||
| { | |||||
| var noteMatch = System.Text.RegularExpressions.Regex.Match(reponse, @"(\d+)/10"); | |||||
| if (noteMatch.Success && int.TryParse(noteMatch.Groups[1].Value, out int note)) | |||||
| { | |||||
| retour.Note = note; | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Renseigner le modèle IA | |||||
| retour.ModeleIA = modeleIA; | |||||
| #endregion | |||||
| #region Génération XML | |||||
| if (isGenererXML) | |||||
| { | |||||
| /* | |||||
| var promptXML = $@" | |||||
| Tu es un assistant spécialisé dans l'extraction d'informations depuis des CV. | |||||
| Je vais te fournir le contenu brut d’un CV (texte brut sans mise en forme). | |||||
| À partir de ce contenu, génère un document structuré au format XML en respectant la structure suivante : | |||||
| <CV> | |||||
| <Identite> | |||||
| <Nom></Nom> | |||||
| <Prenom></Prenom> | |||||
| <Email></Email> | |||||
| <Telephone></Telephone> | |||||
| <Adresse></Adresse> | |||||
| <DateDeNaissance></DateDeNaissance> | |||||
| <Nationalite></Nationalite> | |||||
| </Identite> | |||||
| <Profil></Profil> | |||||
| <Competences> | |||||
| <Competence nom=""..."" niveau=""..."" /> | |||||
| ... | |||||
| </Competences> | |||||
| <Langues> | |||||
| <Langue nom=""..."" niveau=""..."" /> | |||||
| ... | |||||
| </Langues> | |||||
| <Experiences> | |||||
| <Experience> | |||||
| <Poste></Poste> | |||||
| <Entreprise></Entreprise> | |||||
| <Lieu></Lieu> | |||||
| <DateDebut></DateDebut> | |||||
| <DateFin></DateFin> | |||||
| <Description></Description> | |||||
| </Experience> | |||||
| ... | |||||
| </Experiences> | |||||
| <Formations> | |||||
| <Formation> | |||||
| <Diplome></Diplome> | |||||
| <Etablissement></Etablissement> | |||||
| <DateDebut></DateDebut> | |||||
| <DateFin></DateFin> | |||||
| </Formation> | |||||
| ... | |||||
| </Formations> | |||||
| <Certifications> | |||||
| <Certification> | |||||
| <Nom></Nom> | |||||
| <Organisme></Organisme> | |||||
| <Date></Date> | |||||
| </Certification> | |||||
| ... | |||||
| </Certifications> | |||||
| </CV> | |||||
| Si une information est absente, laisse le champ vide. Ne fais aucun commentaire, retourne uniquement le XML. | |||||
| N'oublie aucune expérience, aucune formation, aucune langue, ni aucune certification. | |||||
| Formate le tout proprement au format XML. N'ajoute pas de texte explicatif. Commence directement par <document>. | |||||
| Voici un contenu PDF extrait : | |||||
| {texteCV} | |||||
| "; | |||||
| */ | |||||
| var promptXML = PromptService.GetPrompt(PromptService.ePrompt.RechercheCVService_RechercheCV_Generate, texteCV); | |||||
| var (reponseXML,m2) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.AnalyseCVMission, true, promptXML,"CV en XML"); | |||||
| var reponseCompleteCleanXML = reponseXML.Replace("\\n", "\n").Replace("\\r", "\r"); | |||||
| retour.VersionXML = reponseCompleteCleanXML; | |||||
| } | |||||
| #endregion | |||||
| #region Vectorisation du CV | |||||
| await RAGService.IngestDocument(Domain.CV,true, false, texteCV, fichier); | |||||
| //reActRagAgent.IngestDocument(texteCV, fichier); | |||||
| #endregion | |||||
| retours.Add(retour); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors de l'extraction du texte de {fichier} : {ex.Message}"); | |||||
| } | |||||
| } | |||||
| LoggerService.LogInfo($"Analyse des CV terminée"); | |||||
| return retours; | |||||
| } | |||||
| public static async Task SauvegarderRechercheAsync(ObservableCollection<ReponseRechercheCV> ListeItems) | |||||
| { | |||||
| LoggerService.LogInfo("RechercheCVService.SauvegarderRechercheAsync"); | |||||
| if (ListeItems == null) | |||||
| return; | |||||
| var options = new JsonSerializerOptions | |||||
| { | |||||
| WriteIndented = true, | |||||
| // Ignore les propriétés non sérialisables comme UniqueId si nécessaire | |||||
| IgnoreReadOnlyProperties = false, | |||||
| DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull | |||||
| }; | |||||
| using FileStream fs = File.Create(NomFichierRechercheCV); | |||||
| await JsonSerializer.SerializeAsync(fs, ListeItems, options); | |||||
| } | |||||
| public static async Task<ObservableCollection<ReponseRechercheCV>> ChargerCVDepuisJsonAsync(ObservableCollection<ReponseRechercheCV> ListeItems) | |||||
| { | |||||
| LoggerService.LogInfo("RechercheCVService.ChargerCVDepuisJsonAsync"); | |||||
| if (!File.Exists(NomFichierRechercheCV)) | |||||
| return new ObservableCollection<ReponseRechercheCV>(); | |||||
| using FileStream fs = File.OpenRead(NomFichierRechercheCV); | |||||
| var items = await JsonSerializer.DeserializeAsync<ObservableCollection<ReponseRechercheCV>>(fs); | |||||
| if (items != null) | |||||
| { | |||||
| ListeItems = items; | |||||
| } | |||||
| return ListeItems; | |||||
| } | |||||
| public static async Task<string> SeekByKeyWord(string RechercheString) | |||||
| { | |||||
| LoggerService.LogInfo("RechercheCVService.SeekByKeyWord"); | |||||
| return await RAGService.Search(Domain.CV, RechercheString); | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes privées | |||||
| #endregion | |||||
| } | |||||
| } |
| using System.Text.Json.Serialization; | |||||
| namespace Services | |||||
| { | |||||
| public class ReponseRechercheCV | |||||
| { | |||||
| public string FichierCV { get; set; } = ""; | |||||
| public string Avis { get; set; } = ""; | |||||
| public int Note { get; set; } = 0; | |||||
| public string ModeleIA { get; set; } = ""; | |||||
| public bool PresenceSuspecte { get; set; } = false; | |||||
| public string VersionXML { get; set; } = ""; | |||||
| [JsonIgnore] | |||||
| public string PresenceSuspecte_STR | |||||
| { | |||||
| get | |||||
| { | |||||
| if (PresenceSuspecte) | |||||
| return "⚠️"; | |||||
| return ""; | |||||
| } | |||||
| } | |||||
| } | |||||
| } |
| namespace Services | |||||
| { | |||||
| public class ChatConversation | |||||
| { | |||||
| public string Id { get; set; } = Guid.NewGuid().ToString(); | |||||
| public string Type { get; set; } = ""; | |||||
| public string Model { get; set; } = ""; | |||||
| public string Title { get; set; } = ""; | |||||
| public string TitleLong { get; set; } = ""; | |||||
| public DateTime LastUse { get; set; }=DateTime.Now; | |||||
| public List<ChatMessage> Messages { get; set; } = new(); | |||||
| } | |||||
| } |
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | |||||
| <TargetFramework>net8.0</TargetFramework> | |||||
| <ImplicitUsings>enable</ImplicitUsings> | |||||
| <Nullable>enable</Nullable> | |||||
| </PropertyGroup> | |||||
| </Project> |
| using System.Text.Json.Serialization; | |||||
| namespace Services | |||||
| { | |||||
| public class ChatMessage | |||||
| { | |||||
| public string Role { get; set; } = "";// "user" ou "assistant" | |||||
| public string Content { get; set; } = ""; | |||||
| public DateTime Timestamp { get; set; } = DateTime.Now; | |||||
| public string Model { get; set; } = ""; | |||||
| } | |||||
| } |
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | |||||
| <TargetFramework>net8.0</TargetFramework> | |||||
| <ImplicitUsings>enable</ImplicitUsings> | |||||
| <Nullable>enable</Nullable> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\ReActAgentService\ReActAgentService.csproj" /> | |||||
| <ProjectReference Include="..\ToolsService\ToolsService.csproj" /> | |||||
| </ItemGroup> | |||||
| </Project> |
| using Services.Fooocus; | |||||
| using Services.Models; | |||||
| using Services.ReActAgent; | |||||
| using System.Text.Json; | |||||
| using ToolsServices; | |||||
| using static Services.ReActAgent.ModelsUseCases; | |||||
| namespace Services | |||||
| { | |||||
| public static class ChatService | |||||
| { | |||||
| #region Variables | |||||
| private static string Folder => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Conversations"); | |||||
| private static Services.ReActAgent.ReActAgent _ReActAagent = new(); | |||||
| #endregion | |||||
| #region Constructeur | |||||
| static ChatService() | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService"); | |||||
| try | |||||
| { | |||||
| if (!Directory.Exists(Folder)) | |||||
| { | |||||
| LoggerService.LogDebug($"ChatService : tentative de création du dossier de conversations : {Folder}"); | |||||
| Directory.CreateDirectory(Folder); | |||||
| LoggerService.LogInfo($"ChatService : dossier de conversations créé : {Folder}"); | |||||
| } | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"ChatService : Erreur : {ex.Message}"); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes publiques | |||||
| public static string GetModeleIA(ModelsUseCases.TypeUseCase useCase) | |||||
| { | |||||
| return ReActAgent.ReActAgent.GetModeleIA(useCase); | |||||
| } | |||||
| public static IEnumerable<string> GetModelesIA(ModelsUseCases.TypeUseCase useCase) | |||||
| { | |||||
| return ReActAgent.ReActAgent.GetModelesIA(useCase); | |||||
| } | |||||
| public static async Task<List<string>> GetFooocusStylesAsync() | |||||
| { | |||||
| return await _ReActAagent.GetFooocusStylesAsync(); | |||||
| } | |||||
| public static (string, string, string) LoadParametresGenerateImg() | |||||
| { | |||||
| return _ReActAagent.LoadParametresGenerateImg(); | |||||
| } | |||||
| public static bool SaveParametresGenerateImg(string url, string endpointV1, string endpointv2) | |||||
| { | |||||
| return _ReActAagent.SaveParametresGenerateImg(url, endpointV1, endpointv2); | |||||
| } | |||||
| public static FooocusRequest_Text_to_Image LoadParametresFooocus() | |||||
| { | |||||
| return _ReActAagent.LoadParametresFooocus(); | |||||
| } | |||||
| public static bool SaveParametresFooocus(FooocusRequest_Text_to_Image param, List<string> ListeSelectedsStylesFooocus) | |||||
| { | |||||
| return _ReActAagent.SaveParametresFooocus(param, ListeSelectedsStylesFooocus); | |||||
| } | |||||
| public static void SaveConversation(ChatConversation conv) | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService.Save"); | |||||
| Directory.CreateDirectory(Folder); | |||||
| var path = Path.Combine(Folder, conv.Id + ".json"); | |||||
| File.WriteAllText(path, JsonSerializer.Serialize(conv, new JsonSerializerOptions { WriteIndented = true })); | |||||
| } | |||||
| public static void Delete(string conversationId) | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService.Delete"); | |||||
| var path = Path.Combine(Folder, conversationId + ".json"); | |||||
| if (File.Exists(path)) File.Delete(path); | |||||
| } | |||||
| public static List<ChatConversation> LoadAll(string type) | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService.LoadAll"); | |||||
| return Directory.GetFiles(Folder, "*.json") | |||||
| .Select(file => JsonSerializer.Deserialize<ChatConversation>(File.ReadAllText(file))) | |||||
| .Where(c => c != null) | |||||
| .Where(c=>c.Type == type) | |||||
| .Cast<ChatConversation>() | |||||
| .ToList(); | |||||
| } | |||||
| public static async Task<(bool, string, string)> SendMessageAsync(ModelsUseCases.TypeUseCase useCase, List<ChatMessage> messages, string model, bool isApiExterne=false) | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService.SendMessageAsync"); | |||||
| var (s,m) = await _ReActAagent.AppelerLLMAsync(useCase, model, messages, isApiExterne); | |||||
| return (true, s, m); | |||||
| } | |||||
| public static async Task<(bool, string, string)> SendMessageAsync(string message, string model, bool isWithAssistant, bool isGenerateParametres) | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService.SendMessageAsync"); | |||||
| var (s,m) = await _ReActAagent.GenerateTextToImage(ModelsUseCases.TypeUseCase.PromptGenerationFooocus, message, model, isWithAssistant, isGenerateParametres); | |||||
| return (true, s, m); | |||||
| } | |||||
| public static async Task<(bool, string, string)> SendMessageAsync(string message, string model, bool isWithAssistant, List<string> documentsFullFilename) | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService.SendMessageAsync"); | |||||
| var (s,m) = await _ReActAagent.GenerateTextToImageWithIP(ModelsUseCases.TypeUseCase.PromptGenerationFooocus, message, model, isWithAssistant, documentsFullFilename); | |||||
| return (true, s, m); | |||||
| } | |||||
| public static async Task<(bool, string, string)> SendMessageAsync(ModelsUseCases.TypeUseCase useCase, string prompt, string model, List<string> documentsFullFilename, bool isApiExterne = false) | |||||
| { | |||||
| if (useCase == TypeUseCase.LLM) | |||||
| { | |||||
| var (b1,s11,s12) = await SendMessageLLMAsync(prompt, model, documentsFullFilename, isApiExterne); | |||||
| List<ChatMessage> messages = new(); | |||||
| prompt = prompt + "\n" + s11; | |||||
| var userMessage = new ChatMessage { Role = "user", Content = prompt, Model = model }; | |||||
| messages.Add(userMessage); | |||||
| var (b2, s21, s22) = await SendMessageAsync(ModelsUseCases.TypeUseCase.LLM, messages, model, isApiExterne); | |||||
| return (b1 && b2, "Résumé\n------: \n" + s11 + "\n------\nFin du résumé\n\n" + s21, "Résumé : " + s12 + "\n" + "Traitement de la demande : " + s22); | |||||
| } | |||||
| else if (useCase == TypeUseCase.LLM_Coder) | |||||
| { | |||||
| return await SendMessageLLMCoderAsync(prompt, model, documentsFullFilename, isApiExterne); | |||||
| } | |||||
| else | |||||
| { | |||||
| return (false, "Erreur : Cas d'usage qui n'est ni LLM, ni LLM Coder", model); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes privées | |||||
| private static async Task<(bool, string, string)> SendMessageLLMAsync(string prompt, string model, List<string> documentsFullFilename, bool isApiExterne = false) | |||||
| { | |||||
| (List<string> lstImages, List<string> lstDocuments) = FilesService.GetListesFichiers(documentsFullFilename); | |||||
| // Images mais pas de documents | |||||
| if (lstImages.Count > 0 && lstDocuments.Count == 0) | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService.SendMessageLLMAsync : {lstImages.Count} image(s) mais pas de documents"); | |||||
| var (s, m) = await _ReActAagent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.InterpretationImages, model, prompt, lstImages, isApiExterne); | |||||
| return (true, s, m); | |||||
| } | |||||
| // Documents mais pas d'image | |||||
| else if (lstImages.Count == 0 && lstDocuments.Count > 0) | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService.SendMessageLLMAsync : {lstDocuments.Count} document(s) mais pas d'image"); | |||||
| var (resume, m) = await GetResume(lstDocuments); | |||||
| return (true, resume.ToString(), m); | |||||
| } | |||||
| // Images + documents | |||||
| else | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService.SendMessageLLMAsync : {lstImages.Count} image(s) + {lstDocuments.Count} document(s)"); | |||||
| var resume = await GetResume(lstDocuments); | |||||
| var (reponse, m) = await _ReActAagent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.InterpretationImages, model, prompt, lstImages, isApiExterne); | |||||
| return (true, resume + "\n\n" + reponse, m); | |||||
| } | |||||
| } | |||||
| private static async Task<(bool, string, string)> SendMessageLLMCoderAsync(string prompt, string model, List<string> documentsFullFilename, bool isApiExterne = false) | |||||
| { | |||||
| (List<string> lstImages, List<string> lstDocuments) = FilesService.GetListesFichiers(documentsFullFilename); | |||||
| // Images non prises en compte dans ce contexte | |||||
| if (lstImages.Count > 0 && lstDocuments.Count == 0) | |||||
| { | |||||
| LoggerService.LogDebug($"ChatService.SendMessageLLMCoderAsync : {lstImages.Count} image(s) mais pas de documents : pas pris en compte"); | |||||
| return (false, "Erreur : les images ne sont prises en compte dans ce contexte", model); | |||||
| } | |||||
| // Documents pris en compte | |||||
| else if (lstDocuments.Count > 0) | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService.SendMessageLLMCoderAsync : {lstDocuments.Count} document(s)"); | |||||
| var codeDocs = ""; | |||||
| foreach (var doc in lstDocuments) | |||||
| { | |||||
| var code = $"{Path.GetFileName(doc)} : \n" + TxtService.ExtractTextFromTxt(doc); | |||||
| codeDocs += "\n" + code; | |||||
| } | |||||
| var (rep, m) = await _ReActAagent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.LLM_Coder, true, prompt + "\n\n" + codeDocs, "Analyse de code", model, isApiExterne); | |||||
| return (true, rep, m); | |||||
| } | |||||
| // cas qui ne devrait pas arriver | |||||
| else | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService.SendMessageLLMCoderAsync : {lstImages.Count} image(s) + {lstDocuments.Count} document(s)"); | |||||
| return (true, "Cas qui ne devrait pas arriver", model); | |||||
| } | |||||
| } | |||||
| private static async Task<(string, string)> GetResume(List<string> lstDocuments) | |||||
| { | |||||
| LoggerService.LogInfo($"ChatService.GetResume"); | |||||
| string resumes = ""; | |||||
| string model = ""; | |||||
| foreach (var doc in lstDocuments) | |||||
| { | |||||
| var filename = System.IO.Path.GetFileName(doc); | |||||
| string content = FilesService.ExtractText(doc); | |||||
| //agent.IngestDocument(content, doc); | |||||
| var (resume, m) = await _ReActAagent.SummarizeIntegralLongDocAsync("ChatRoom", content); | |||||
| resumes += "\n\n" + filename + " : \n" + resume; | |||||
| model = m; | |||||
| } | |||||
| return (resumes, model); | |||||
| } | |||||
| public static async Task<List<OllamaModel>> GetInstalledModelsAsync() | |||||
| { | |||||
| var models = await _ReActAagent.GetInstalledModelsAsync(true); | |||||
| return models; | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| } |
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | |||||
| <TargetFramework>net8.0</TargetFramework> | |||||
| <ImplicitUsings>enable</ImplicitUsings> | |||||
| <Nullable>enable</Nullable> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <None Remove="ServicesExternes\ffmpeg.zip" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\RAGService\RAGService.csproj" /> | |||||
| <ProjectReference Include="..\ReActAgentService\ReActAgentService.csproj" /> | |||||
| <ProjectReference Include="..\ToolsService\ToolsService.csproj" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <None Update="ServicesExternes\ffmpeg\avcodec-62.dll"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="ServicesExternes\ffmpeg\avdevice-62.dll"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="ServicesExternes\ffmpeg\avfilter-11.dll"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="ServicesExternes\ffmpeg\avformat-62.dll"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="ServicesExternes\ffmpeg\avutil-60.dll"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="ServicesExternes\ffmpeg\ffmpeg.exe"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="ServicesExternes\ffmpeg\ffplay.exe"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="ServicesExternes\ffmpeg\ffprobe.exe"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="ServicesExternes\ffmpeg\swresample-6.dll"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="ServicesExternes\ffmpeg\swscale-9.dll"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| </ItemGroup> | |||||
| </Project> |
| using Services.ReActAgent; | |||||
| using System.Text; | |||||
| using System.Text.RegularExpressions; | |||||
| using ToolsServices; | |||||
| namespace Services | |||||
| { | |||||
| public static class FactureService | |||||
| { | |||||
| #region Variables | |||||
| private static readonly string NomFichierParametres = FichiersInternesService.ParamsFactures; | |||||
| #endregion | |||||
| #region Méthodes publiques | |||||
| public static async Task<(bool, string)> Traitement(string inputPath, string outputPath, bool isApiExterne) | |||||
| { | |||||
| StringBuilder sb = new(); | |||||
| LoggerService.LogInfo($"FactureService.Traitement"); | |||||
| if (!Directory.Exists(inputPath)) | |||||
| { | |||||
| LoggerService.LogWarning($"Le dossier {inputPath} n'existe pas."); | |||||
| return (false, $"Le dossier {inputPath} n'existe pas."); | |||||
| } | |||||
| if (!Directory.Exists(outputPath)) | |||||
| { | |||||
| LoggerService.LogWarning($"Le dossier {outputPath} n'existe pas."); | |||||
| return (false, $"Le dossier {outputPath} n'existe pas."); | |||||
| } | |||||
| var fichiers = Directory | |||||
| .EnumerateFiles(inputPath, "*.*", SearchOption.AllDirectories) | |||||
| .Where(f => f.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) | |||||
| .ToArray(); | |||||
| if (fichiers.Length == 0) | |||||
| { | |||||
| LoggerService.LogWarning($"Aucun fichier trouvé dans le dossier spécifié : {inputPath}"); | |||||
| return (false, $"Aucun fichier trouvé dans le dossier spécifié : {inputPath}"); | |||||
| } | |||||
| var reActRagAgent = new ReActAgent.ReActAgent(); | |||||
| var isOk = true; | |||||
| foreach (var fichier in fichiers) | |||||
| { | |||||
| try | |||||
| { | |||||
| #region Analyse de la facture | |||||
| var facturePDF = FilesService.ExtractText(fichier); | |||||
| //facturePDF = CleanPdfText(facturePDF); | |||||
| //facturePDF = SegmentInvoice(facturePDF); | |||||
| var prompt = PromptService.GetPrompt(PromptService.ePrompt.FactureService_AnalyserFacture_Extract, facturePDF); | |||||
| LoggerService.LogInfo($"Traitement de facture PDF to XML : {fichier}"); | |||||
| var (reponse,m) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.Facture_To_Xml, true, prompt, "Traitement de facture PDF to XML", "", isApiExterne); | |||||
| if (!XmlService.IsXML(reponse)) | |||||
| { | |||||
| sb.AppendLine($"Le résultat n'est pas au format XML pour le fichier : {fichier}"); | |||||
| LoggerService.LogWarning($"Le résultat n'est pas au format XML pour le fichier : {fichier}"); | |||||
| string fileNameXML = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(fichier) + ".xml"); | |||||
| TxtService.CreateTextFile(fileNameXML, reponse); | |||||
| } | |||||
| else | |||||
| { | |||||
| #region Sauvegarde du résultat | |||||
| LoggerService.LogDebug($"Le résultat est au format XML pour le fichier : {fichier}"); | |||||
| string fileNameXML = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(fichier) + ".xml"); | |||||
| TxtService.CreateTextFile(fileNameXML, reponse); | |||||
| #endregion | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| isOk = false; | |||||
| LoggerService.LogError($"Erreur lors de l'extraction du texte de {fichier} : {ex.Message}"); | |||||
| } | |||||
| } | |||||
| LoggerService.LogInfo($"Traitement des factures PDF to XML terminée"); | |||||
| return (isOk, sb.ToString()); | |||||
| } | |||||
| public static (string, string) LoadParametres() | |||||
| { | |||||
| LoggerService.LogInfo("FactureService.LoadParametres"); | |||||
| //ParametresOllamaService SelectedItem = new(); | |||||
| try | |||||
| { | |||||
| string FicheMission = ""; | |||||
| string PathCV = ""; | |||||
| if (File.Exists(NomFichierParametres)) | |||||
| { | |||||
| string[] lignes = File.ReadAllLines(NomFichierParametres); | |||||
| if (lignes.Length > 0) | |||||
| FicheMission = lignes[0]; | |||||
| if (lignes.Length > 1) | |||||
| PathCV = lignes[1]; | |||||
| } | |||||
| return (FicheMission, PathCV); | |||||
| } | |||||
| catch | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors du chargement des paramètres depuis {NomFichierParametres}"); | |||||
| return ("", ""); | |||||
| } | |||||
| } | |||||
| public static bool SaveParametres(string inputPath, string outputPath) | |||||
| { | |||||
| LoggerService.LogInfo("FactureService.SaveParametres"); | |||||
| try | |||||
| { | |||||
| StringBuilder sb = new(); | |||||
| sb.AppendLine(inputPath); | |||||
| sb.AppendLine(outputPath); | |||||
| File.WriteAllText(NomFichierParametres, sb.ToString()); | |||||
| return true; | |||||
| } | |||||
| catch | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors de la sauvegarde des paramètres dans {NomFichierParametres}"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes privées | |||||
| private static string CleanPdfText(string rawText) | |||||
| { | |||||
| var text = rawText; | |||||
| // Normaliser les fins de ligne | |||||
| text = text.Replace("\r", "\n"); | |||||
| // Supprimer les caractères de contrôle (sauf \n) | |||||
| text = new string(text.Where(c => !char.IsControl(c) || c == '\n').ToArray()); | |||||
| // Supprimer les en-têtes et pieds de page typiques (ex: "Page 1/3", "Facture ACME", etc.) | |||||
| text = Regex.Replace(text, @"Page\s+\d+(/\d+)?", "", RegexOptions.IgnoreCase); | |||||
| text = Regex.Replace(text, @"Facture\s+n[°º]\s*\d+", "", RegexOptions.IgnoreCase); | |||||
| // Uniformiser les espaces : tabulations → espace, et espaces multiples → 1 seul | |||||
| text = Regex.Replace(text, @"[ \t]+", " "); | |||||
| // Supprimer les lignes vides excessives | |||||
| text = Regex.Replace(text, @"\n{2,}", "\n"); | |||||
| // Supprimer les traits de séparation (souvent des "-----" ou "=====") | |||||
| text = Regex.Replace(text, @"[-=]{3,}", ""); | |||||
| // Corriger les montants : ex "1 234,56 €" → "1234.56 EUR" | |||||
| text = Regex.Replace(text, @"(\d{1,3}(?:[ \u00A0]\d{3})*,\d{2}) ?€", | |||||
| m => m.Groups[1].Value.Replace(" ", "").Replace("\u00A0", "").Replace(",", ".") + " EUR"); | |||||
| // Corriger les dates : ex "01-02-2025" → "2025-02-01" | |||||
| text = Regex.Replace(text, @"\b(\d{2})[-/](\d{2})[-/](\d{4})\b", "$3-$2-$1"); | |||||
| // Fusionner les lignes coupées artificiellement (ex: "TOTAL\n123,45" → "TOTAL 123,45") | |||||
| text = Regex.Replace(text, @"([A-Za-z])\n(\d)", "$1 $2"); | |||||
| // Supprimer les espaces en trop au début et fin | |||||
| text = text.Trim(); | |||||
| return text; | |||||
| } | |||||
| private static string SegmentInvoice(string text) | |||||
| { | |||||
| var sb = new StringBuilder(); | |||||
| // Bloc fournisseur | |||||
| var fournisseurMatch = Regex.Match(text, @"(?i)(?:fournisseur|vendor|seller).{0,50}\n(.+?)(\n|$)"); | |||||
| if (fournisseurMatch.Success) | |||||
| { | |||||
| sb.AppendLine("=== FOURNISSEUR ==="); | |||||
| sb.AppendLine(fournisseurMatch.Groups[1].Value.Trim()); | |||||
| } | |||||
| // Bloc client | |||||
| var clientMatch = Regex.Match(text, @"(?i)(?:client|acheteur|buyer|bill to|destinataire).{0,50}\n(.+?)(\n|$)"); | |||||
| if (clientMatch.Success) | |||||
| { | |||||
| sb.AppendLine("\n=== CLIENT ==="); | |||||
| sb.AppendLine(clientMatch.Groups[1].Value.Trim()); | |||||
| } | |||||
| // Numéro de facture | |||||
| var numeroMatch = Regex.Match(text, @"(?i)facture[^\d]*(\d+)"); | |||||
| if (numeroMatch.Success) | |||||
| { | |||||
| sb.AppendLine("\n=== NUMERO FACTURE ==="); | |||||
| sb.AppendLine(numeroMatch.Groups[1].Value.Trim()); | |||||
| } | |||||
| // Date de facture | |||||
| var dateMatch = Regex.Match(text, @"(?i)(?:date|issued|emission)[^\d]*(\d{4}-\d{2}-\d{2})"); | |||||
| if (dateMatch.Success) | |||||
| { | |||||
| sb.AppendLine("\n=== DATE FACTURE ==="); | |||||
| sb.AppendLine(dateMatch.Groups[1].Value.Trim()); | |||||
| } | |||||
| // Bloc lignes de facture (simplifié : cherche un tableau prix/quantité) | |||||
| var lignesMatch = Regex.Match(text, @"(?is)(description.*?total)", RegexOptions.IgnoreCase); | |||||
| if (lignesMatch.Success) | |||||
| { | |||||
| sb.AppendLine("\n=== LIGNES FACTURE ==="); | |||||
| sb.AppendLine(lignesMatch.Groups[0].Value.Trim()); | |||||
| } | |||||
| // Total général | |||||
| var totalMatch = Regex.Match(text, @"(?i)(total (?:ttc|general|amount)).{0,10}([\d.,]+ ?eur)"); | |||||
| if (totalMatch.Success) | |||||
| { | |||||
| sb.AppendLine("\n=== TOTAL GENERAL ==="); | |||||
| sb.AppendLine(totalMatch.Groups[2].Value.Trim()); | |||||
| } | |||||
| // Retour du texte segmenté | |||||
| return sb.Length > 0 ? sb.ToString() : text; | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| } |
| using Services.ReActAgent; | |||||
| using System.Text; | |||||
| using ToolsServices; | |||||
| namespace Services | |||||
| { | |||||
| public class TF_From_DSFService | |||||
| { | |||||
| private static readonly string NomFichierParametres = FichiersInternesService.ParamsTF_From_DSF; | |||||
| public static async Task<(bool, string)> Traitement(string inputPath, string outputPath, bool isApiExterne) | |||||
| { | |||||
| StringBuilder sb = new(); | |||||
| LoggerService.LogInfo($"TF_From_DSFService.Traitement"); | |||||
| if (!Directory.Exists(inputPath)) | |||||
| { | |||||
| LoggerService.LogWarning($"Le dossier {inputPath} n'existe pas."); | |||||
| return (false, $"Le dossier {inputPath} n'existe pas."); | |||||
| } | |||||
| if (!Directory.Exists(outputPath)) | |||||
| { | |||||
| LoggerService.LogWarning($"Le dossier {outputPath} n'existe pas."); | |||||
| return (false, $"Le dossier {outputPath} n'existe pas."); | |||||
| } | |||||
| var fichiers = Directory | |||||
| .EnumerateFiles(inputPath, "*.*", SearchOption.AllDirectories) | |||||
| .Where(f => f.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".docx", StringComparison.OrdinalIgnoreCase)) | |||||
| .ToArray(); | |||||
| if (fichiers.Length == 0) | |||||
| { | |||||
| LoggerService.LogWarning($"Aucun fichier trouvé dans le dossier spécifié : {inputPath}"); | |||||
| return (false, $"Aucun fichier trouvé dans le dossier spécifié : {inputPath}"); | |||||
| } | |||||
| var reActRagAgent = new ReActAgent.ReActAgent(); | |||||
| var model = ReActAgent.ReActAgent.GetModeleIA(ModelsUseCases.TypeUseCase.TF_From_DSF); | |||||
| foreach (var fichier in fichiers) | |||||
| { | |||||
| try | |||||
| { | |||||
| #region Analyse du DSF | |||||
| var allCsv = new StringBuilder(); | |||||
| var docDSF = FilesService.ExtractText(fichier); | |||||
| var chunks = RAGService.ChunkText(docDSF, 2000);// Chunker.ChunkText(docDSF, 2000); //SplitText(docDSF, 3000); | |||||
| var nbChuncks = chunks.Length; | |||||
| int numChuck = 0; | |||||
| foreach (var chunk in chunks) | |||||
| { | |||||
| numChuck++; | |||||
| var prompt = PromptService.GetPrompt(PromptService.ePrompt.TF_From_DSFService_GenereTF, chunk); | |||||
| LoggerService.LogDebug($"Traitement TF From DSF {numChuck}/{nbChuncks} : {fichier}"); | |||||
| var (reponse,m) = await reActRagAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.TF_From_DSF, true, prompt, "Traitement de TF From DSF", model, isApiExterne); | |||||
| allCsv.AppendLine(reponse); | |||||
| } | |||||
| #region Sauvegarde du résultat | |||||
| /* | |||||
| string horodate = DateTime.Now.ToString("hhmmss"); | |||||
| string fileNameCSV = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(fichier) + $"_{model.Replace(":","-")}_{horodate}.csv"); | |||||
| */ | |||||
| string fileNameCSV = Path.Combine(outputPath, Path.GetFileNameWithoutExtension(fichier) + $".csv"); | |||||
| TxtService.CreateTextFile(fileNameCSV, allCsv.ToString()); | |||||
| #endregion | |||||
| #endregion | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| var msg = $"Erreur lors de l'extraction du texte de {fichier} : {ex.Message}"; | |||||
| LoggerService.LogError(msg); | |||||
| return (false, msg); | |||||
| } | |||||
| } | |||||
| //} | |||||
| LoggerService.LogInfo($"Traitement TF From DSF terminée"); | |||||
| return (true, sb.ToString()); | |||||
| } | |||||
| private static List<string> SplitText(string text, int chunkSize = 3000) | |||||
| { | |||||
| var chunks = new List<string>(); | |||||
| for (int i = 0; i < text.Length; i += chunkSize) | |||||
| { | |||||
| int length = Math.Min(chunkSize, text.Length - i); | |||||
| chunks.Add(text.Substring(i, length)); | |||||
| } | |||||
| return chunks; | |||||
| } | |||||
| public static (string, string) LoadParametres() | |||||
| { | |||||
| LoggerService.LogInfo("TF_From_DSFService.LoadParametres"); | |||||
| //ParametresOllamaService SelectedItem = new(); | |||||
| try | |||||
| { | |||||
| string FicheMission = ""; | |||||
| string PathCV = ""; | |||||
| if (File.Exists(NomFichierParametres)) | |||||
| { | |||||
| string[] lignes = File.ReadAllLines(NomFichierParametres); | |||||
| if (lignes.Length > 0) | |||||
| FicheMission = lignes[0]; | |||||
| if (lignes.Length > 1) | |||||
| PathCV = lignes[1]; | |||||
| } | |||||
| return (FicheMission, PathCV); | |||||
| } | |||||
| catch | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors du chargement des paramètres depuis {NomFichierParametres}"); | |||||
| return ("", ""); | |||||
| } | |||||
| } | |||||
| public static bool SaveParametres(string inputPath, string outputPath) | |||||
| { | |||||
| LoggerService.LogInfo("TF_From_DSFService.SaveParametres"); | |||||
| try | |||||
| { | |||||
| StringBuilder sb = new(); | |||||
| sb.AppendLine(inputPath); | |||||
| sb.AppendLine(outputPath); | |||||
| File.WriteAllText(NomFichierParametres, sb.ToString()); | |||||
| return true; | |||||
| } | |||||
| catch | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors de la sauvegarde des paramètres dans {NomFichierParametres}"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| } | |||||
| } |
| using Services.ReActAgent; | |||||
| using System.Diagnostics; | |||||
| using ToolsServices; | |||||
| namespace Services | |||||
| { | |||||
| public static class TranscriptionAndResumeService | |||||
| { | |||||
| public static async Task<(bool, string)> TranscribeAudio(string fullFileName) | |||||
| { | |||||
| try | |||||
| { | |||||
| LoggerService.LogInfo($"TranscriptionAndResumeService.TranscribeAudio"); | |||||
| var bActif = await ReActAgent.ReActAgent.IsWhisperActif(); | |||||
| if (!bActif) | |||||
| { | |||||
| var msg = "Le service de transcription Whisper n'est pas actif. Veuillez le démarrer et réessayer."; | |||||
| LoggerService.LogWarning(msg); | |||||
| return (false, msg); | |||||
| } | |||||
| var _Agent = new ReActAgent.ReActAgent(); | |||||
| var (b, s) = await _Agent.TranscribeAudio(fullFileName); | |||||
| return (b, s); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| return (false, ex.Message); | |||||
| } | |||||
| } | |||||
| public static async Task<(bool, string)> ResumeTranscribe(string transcribeText, string contexte) | |||||
| { | |||||
| try | |||||
| { | |||||
| LoggerService.LogInfo($"TranscriptionAndResumeService.ResumeTranscribe"); | |||||
| var bActif = await ReActAgent.ReActAgent.IsOllamaActif(true); | |||||
| if (!bActif) | |||||
| { | |||||
| var msg = "Le service de résumé Ollama n'est pas actif. Veuillez le démarrer et réessayer."; | |||||
| LoggerService.LogWarning(msg); | |||||
| return (false, msg); | |||||
| } | |||||
| var _Agent = new ReActAgent.ReActAgent(); | |||||
| /* | |||||
| var prompt = $""" | |||||
| Tu es un expert en résumé de texte. | |||||
| Résume le texte suivant en français de manière claire et concise, en utilisant des phrases complètes. | |||||
| Il s'agit d'une réunion de travail pour faire le bilan du sprint 03 du projet GIEBOX | |||||
| Texte à résumer : | |||||
| {transcribeText} | |||||
| """; | |||||
| */ | |||||
| var prompt = PromptService.GetPrompt(PromptService.ePrompt.TranscriptionAndResumeService_Resume, transcribeText, contexte); | |||||
| var (s, m) = await _Agent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.ResumeDocuments, true, prompt, "Résumé d'une transcription", "", false); | |||||
| return (true, s); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| return (false, ex.Message); | |||||
| } | |||||
| } | |||||
| public static bool ConvertVideoToAudio(string inputFile, string outputFile) | |||||
| { | |||||
| LoggerService.LogInfo($"TranscriptionAndResumeService.ConvertVideoToAudio"); | |||||
| try | |||||
| { | |||||
| if (System.IO.File.Exists(outputFile)) | |||||
| { | |||||
| try | |||||
| { | |||||
| System.IO.File.Delete(outputFile); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"TranscriptionAndResumeService.ConvertVideoToAudio : {ex.Message}"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| // Mono en 16KHz | |||||
| //var ffmpegArgs = $"-i \"{inputFile}\" -ac 1 -ar 16000 \"{outputFile}\""; | |||||
| // Stereo en 44.1KHz | |||||
| var ffmpegArgs = $"-i \"{inputFile}\" -ac 2 -ar 44000 -c:a pcm_s24le \"{outputFile}\""; | |||||
| var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ServicesExternes", "ffmpeg", "ffmpeg.exe"); | |||||
| var process = new Process | |||||
| { | |||||
| StartInfo = new ProcessStartInfo | |||||
| { | |||||
| FileName = path, // ffmpeg.exe n'a pas besoin d'être le PATH (variables environnement) | |||||
| Arguments = ffmpegArgs, | |||||
| RedirectStandardOutput = true, | |||||
| RedirectStandardError = true, | |||||
| UseShellExecute = false, | |||||
| CreateNoWindow = true | |||||
| } | |||||
| }; | |||||
| process.Start(); | |||||
| string output = process.StandardOutput.ReadToEnd(); | |||||
| string error = process.StandardError.ReadToEnd(); | |||||
| process.WaitForExit(); | |||||
| if (process.ExitCode != 0) | |||||
| { | |||||
| LoggerService.LogError($"TranscriptionAndResumeService.ConvertVideoToAudio : {error}"); | |||||
| throw new Exception($"Erreur ffmpeg : {error}"); | |||||
| } | |||||
| return true; | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"TranscriptionAndResumeService.ConvertVideoToAudio : {ex.Message}"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| } | |||||
| } |
| Mettre ici les exe et dll de ffmpeg |
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | |||||
| <TargetFramework>net8.0</TargetFramework> | |||||
| <ImplicitUsings>enable</ImplicitUsings> | |||||
| <Nullable>enable</Nullable> | |||||
| <PlatformTarget>x64</PlatformTarget> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <PackageReference Include="MailKit" Version="4.14.0" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\EmailStructure\EmailStructure.csproj" /> | |||||
| <ProjectReference Include="..\RAGService\RAGService.csproj" /> | |||||
| <ProjectReference Include="..\ReActAgentService\ReActAgentService.csproj" /> | |||||
| <ProjectReference Include="..\ToolsService\ToolsService.csproj" /> | |||||
| </ItemGroup> | |||||
| </Project> |
| using ClosedXML; | |||||
| using Google.Protobuf.WellKnownTypes; | |||||
| using MailKit; | |||||
| using MailKit.Net.Imap; | |||||
| using MailKit.Search; | |||||
| using MailKit.Security; | |||||
| using MimeKit; | |||||
| using Org.BouncyCastle.Security; | |||||
| using Services.ReActAgent; | |||||
| using System.Text.Json; | |||||
| using ToolsServices; | |||||
| namespace Services | |||||
| { | |||||
| #region Classe static annexe JsonDb | |||||
| public static class JsonDb | |||||
| { | |||||
| static string FullFileName = FichiersInternesService.ListeMailsSend; | |||||
| public static ThreadStore Load() | |||||
| { | |||||
| if (!File.Exists(FullFileName)) return new ThreadStore(); | |||||
| var json = File.ReadAllText(FullFileName); | |||||
| return JsonSerializer.Deserialize<ThreadStore>(json) ?? new ThreadStore(); | |||||
| } | |||||
| public static bool Save(ThreadItem thread) | |||||
| { | |||||
| ThreadStore data; | |||||
| // Charger fichier existant | |||||
| if (File.Exists(FullFileName)) | |||||
| { | |||||
| string json = File.ReadAllText(FullFileName); | |||||
| data = JsonSerializer.Deserialize<ThreadStore>(json, new JsonSerializerOptions { WriteIndented = true }) ?? new ThreadStore(); | |||||
| } | |||||
| else | |||||
| { | |||||
| data = new ThreadStore(); | |||||
| } | |||||
| // Chercher si le thread existe déjà | |||||
| var existing = data.Threads.FirstOrDefault(t => t.Id == thread.Id); | |||||
| if (existing != null) | |||||
| { | |||||
| int index = data.Threads.IndexOf(existing); | |||||
| data.Threads[index] = thread; // remplacement | |||||
| } | |||||
| else | |||||
| { | |||||
| data.Threads.Add(thread); // ajout | |||||
| } | |||||
| // Sauvegarder | |||||
| string updatedJson = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }); | |||||
| File.WriteAllText(FullFileName, updatedJson); | |||||
| return true; | |||||
| } | |||||
| public static void Save(ThreadStore store) | |||||
| { | |||||
| Directory.CreateDirectory(Path.GetDirectoryName(FullFileName)!); | |||||
| var json = JsonSerializer.Serialize(store, new JsonSerializerOptions { WriteIndented = true }); | |||||
| var tmp = FullFileName + ".tmp"; | |||||
| File.WriteAllText(tmp, json); | |||||
| File.Move(tmp, FullFileName, true); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Classe static EmailSendService | |||||
| public static class EmailSendService | |||||
| { | |||||
| #region Variables | |||||
| private static ReActAgent.ReActAgent _ReActAgent = new(); | |||||
| private static List<IMailFolder> ListInboxFolders = new List<IMailFolder>(); | |||||
| private static CompteMailUser ParametreUser = new(); | |||||
| #endregion | |||||
| #region Méthodes publiques | |||||
| public static int NbRelancesEnAttente() | |||||
| { | |||||
| var lstReponsesEnAttente = LstRelancesEnAttente(); | |||||
| return lstReponsesEnAttente.Count; | |||||
| } | |||||
| public static ThreadStore ChargerDonnees() | |||||
| { | |||||
| var fileMailsSend = FichiersInternesService.ListeMailsSend; | |||||
| ThreadStore store = new ThreadStore(); | |||||
| if (File.Exists(fileMailsSend)) | |||||
| { | |||||
| var json = System.IO.File.ReadAllText(fileMailsSend); | |||||
| store = JsonSerializer.Deserialize<ThreadStore>(json) ?? new ThreadStore(); | |||||
| } | |||||
| return store; | |||||
| } | |||||
| public static List<ThreadItem> LstRelancesEnAttente() | |||||
| { | |||||
| var store = JsonDb.Load(); | |||||
| var lstReponsesEnAttente = store.Threads | |||||
| .Where(t => !t.SubjectClosed) // Sujet non fermé | |||||
| .Where(t => t.IsOverdue) // En retard | |||||
| .ToList(); | |||||
| return lstReponsesEnAttente; | |||||
| } | |||||
| private class MessageDossier | |||||
| { | |||||
| public UniqueId uid { get; set; } | |||||
| public MimeMessage mimeMessage { get; set; } =new(); | |||||
| public IMailFolder? dossier { get; set; } | |||||
| public string dossierFullname { get; set; } = ""; | |||||
| public string direction { get; set; } = ""; | |||||
| } | |||||
| public static async Task<(bool, string, int)> RunOnce(CompteMailUser parametreUser) | |||||
| { | |||||
| var nbMsgSentSaved = 0; | |||||
| var nbMsgInboxSaved = 0; | |||||
| var nbRelancesCreated = 0; | |||||
| var nbConvReOuvertes = 0; | |||||
| ParametreUser = parametreUser; | |||||
| try | |||||
| { | |||||
| LoggerService.LogInfo("EmailSendService.RunOnce"); | |||||
| var dateFilter = DateTime.UtcNow.AddDays(-1 * ParametreUser.DelaySentRecup); | |||||
| var parametresMail = EmailService.LoadParametres(); | |||||
| if(parametresMail == null) | |||||
| { | |||||
| return (false, "Paramètres email introuvable",0); | |||||
| } | |||||
| var store = JsonDb.Load(); | |||||
| using var client = new ImapClient(); | |||||
| await client.ConnectAsync(parametresMail.ServeurImap, parametresMail.ServeurImapPort, SecureSocketOptions.SslOnConnect); | |||||
| await client.AuthenticateAsync(parametresMail.UserAdresse, parametresMail.UserMotPasse); | |||||
| ListInboxFolders.Clear(); | |||||
| var personal = client.GetFolder(client.PersonalNamespaces[0]); | |||||
| await ProcessFolder(personal); | |||||
| // Récupérer le dossier "Sent" | |||||
| var sentFolder = await personal.GetSubfolderAsync("Sent"); | |||||
| // Si ça ne marche pas (par exemple dossier en français "Envoyés") : | |||||
| if (sentFolder == null || !sentFolder.Exists) | |||||
| { | |||||
| var allFolders = await personal.GetSubfoldersAsync(false); | |||||
| sentFolder = allFolders.FirstOrDefault(f => f.Name.Equals("Sent", StringComparison.OrdinalIgnoreCase) | |||||
| || f.Name.Equals("Envoyés", StringComparison.OrdinalIgnoreCase)); | |||||
| } | |||||
| // Si on a bien trouvé un dossier "Sent" ou équivalent | |||||
| if (sentFolder == null) | |||||
| { | |||||
| return (false, "Dossier mails envoyés introuvables",0); | |||||
| } | |||||
| var sent = sentFolder; | |||||
| await sent.OpenAsync(MailKit.FolderAccess.ReadOnly); | |||||
| //var uidsSent = await sent.SearchAsync(SearchQuery.All); | |||||
| var uidsSent = await sent.SearchAsync(SearchQuery.DeliveredAfter(dateFilter)); | |||||
| List<MessageDossier> lst = new(); | |||||
| foreach (var uid in uidsSent) | |||||
| { | |||||
| var msg = await sent.GetMessageAsync(uid); | |||||
| MessageDossier nouveau = new(); | |||||
| nouveau.uid = uid; | |||||
| nouveau.mimeMessage = msg; | |||||
| nouveau.dossier = sent; | |||||
| nouveau.dossierFullname = sentFolder.FullName; | |||||
| nouveau.direction = "out"; | |||||
| lst.Add(nouveau); | |||||
| } | |||||
| foreach (var oneFolder in ListInboxFolders) | |||||
| { | |||||
| var inbox = oneFolder!;// client.Inbox; | |||||
| await inbox.OpenAsync(MailKit.FolderAccess.ReadWrite); | |||||
| //var uidsInbox = await inbox.SearchAsync(SearchQuery.All); | |||||
| var uidsInbox = await inbox.SearchAsync(SearchQuery.DeliveredAfter(dateFilter)); | |||||
| foreach (var uid in uidsInbox) | |||||
| { | |||||
| var msg = await inbox.GetMessageAsync(uid); | |||||
| MessageDossier nouveau = new(); | |||||
| nouveau.uid = uid; | |||||
| nouveau.mimeMessage = msg; | |||||
| nouveau.dossier = inbox; | |||||
| nouveau.dossierFullname = inbox.FullName; | |||||
| nouveau.direction = "in"; | |||||
| lst.Add(nouveau); | |||||
| } | |||||
| } | |||||
| lst = lst.OrderBy(e => e.mimeMessage.Date).ToList(); | |||||
| foreach (var message in lst) | |||||
| { | |||||
| var bCreateMsg = await AddMessage(message.uid, store, message.mimeMessage, message.direction,message.dossierFullname, parametreUser.OverdueDaysSent); | |||||
| if (bCreateMsg) | |||||
| nbMsgSentSaved++; | |||||
| JsonDb.Save(store); | |||||
| } | |||||
| /* | |||||
| foreach (var uid in uidsSent) | |||||
| { | |||||
| var msg = await sent.GetMessageAsync(uid); | |||||
| var bCreateMsg = await AddMessage(uid, store, msg, "out", sentFolder.FullName, parametreUser.OverdueDaysSent); | |||||
| if (bCreateMsg) | |||||
| nbMsgSentSaved++; | |||||
| JsonDb.Save(store); | |||||
| } | |||||
| // Tous les dossiers Inbox | |||||
| foreach (var oneFolder in ListInboxFolders) | |||||
| { | |||||
| var inbox = oneFolder!;// client.Inbox; | |||||
| await inbox.OpenAsync(MailKit.FolderAccess.ReadWrite); | |||||
| //var uidsInbox = await inbox.SearchAsync(SearchQuery.All); | |||||
| var uidsInbox = await inbox.SearchAsync(SearchQuery.DeliveredAfter(dateFilter)); | |||||
| foreach (var uid in uidsInbox) | |||||
| { | |||||
| var msg = await inbox.GetMessageAsync(uid); | |||||
| var bCreateMsg = await AddMessage(uid, store, msg, "in", oneFolder.FullName, parametreUser.OverdueDaysSent); | |||||
| if (bCreateMsg) | |||||
| nbMsgInboxSaved++; | |||||
| JsonDb.Save(store); | |||||
| } | |||||
| } | |||||
| */ | |||||
| await client.DisconnectAsync(true); | |||||
| // Analyse IA pour déterminer les relances | |||||
| foreach (var thread in store.Threads) | |||||
| { | |||||
| // On prend le dernier mail sortant | |||||
| var lastOut = thread.Messages | |||||
| .FindLast(m => m.Direction == "out"); | |||||
| var theLast = thread.Messages.FindLast(m => m.Direction == "out" || m.Direction == "in"); | |||||
| thread.DateLastMessage = theLast!.Date; | |||||
| if (lastOut != null && lastOut.RequiresResponse) | |||||
| { | |||||
| // On ne régénère pas une relance si on l'a déjà faite pour ce mail | |||||
| bool alreadyFollowedUp = (thread.LastFollowUpForMessageId == lastOut.Id); | |||||
| if (!alreadyFollowedUp) | |||||
| { | |||||
| // Dernier mail entrant | |||||
| var lastIn = thread.Messages | |||||
| .Where(m => m.Direction == "in") | |||||
| .OrderBy(m => m.Date.ToUniversalTime()) | |||||
| .LastOrDefault(); | |||||
| // relance si | |||||
| bool needsFollowUp = ( | |||||
| lastIn == null // pas de réponse | |||||
| || (lastIn.Date.ToUniversalTime() < lastOut.Date.ToUniversalTime()) // pas de réponse APRES le dernier message | |||||
| || (lastIn.Date.ToUniversalTime() >= lastOut.Date.ToUniversalTime() && !lastIn.CoversAllPoints) // réponse APRES le dernier message MAIS tous les points ne sont pas couverts | |||||
| ); | |||||
| if (needsFollowUp) | |||||
| { | |||||
| //if(thread.FollowUpDraft == null || thread.FollowUpDraft == "") | |||||
| thread.FollowUpDraft = await GenerateFollowUp(thread); | |||||
| // Même si cloturée, on ré-ouvre la conversation | |||||
| if(thread.SubjectClosed == true) | |||||
| { | |||||
| nbConvReOuvertes++; | |||||
| thread.SubjectClosed = false; | |||||
| } | |||||
| thread.LastFollowUpForMessageId = lastOut.Id; | |||||
| nbRelancesCreated++; | |||||
| } | |||||
| } | |||||
| } | |||||
| JsonDb.Save(store); | |||||
| } | |||||
| JsonDb.Save(store); | |||||
| LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre messages envoyés sauvegardés : {nbMsgSentSaved}"); | |||||
| LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre messages reçus sauvegardés : {nbMsgInboxSaved}"); | |||||
| LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre relances générées : {nbRelancesCreated}"); | |||||
| LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre coversation réouvertes : {nbConvReOuvertes}"); | |||||
| LoggerService.LogDebug("EmailSendService.RunOnce : terminé"); | |||||
| return (true, "", nbConvReOuvertes); | |||||
| } | |||||
| catch(Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"EmailSendService.RunOnce : {ex.Message}"); | |||||
| LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre messages envoyés sauvegardés : {nbMsgSentSaved}"); | |||||
| LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre messages reçus sauvegardés : {nbMsgInboxSaved}"); | |||||
| LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre relances générées : {nbRelancesCreated}"); | |||||
| LoggerService.LogDebug($"EmailSendService.RunOnce : Nbre coversation réouvertes : {nbConvReOuvertes}"); | |||||
| return (false, $"EmailSendService.RunOnce : {ex.Message}", nbConvReOuvertes); | |||||
| } | |||||
| } | |||||
| public static bool Save(ThreadItem thread) | |||||
| { | |||||
| LoggerService.LogInfo("EmailSendService.Save"); | |||||
| return JsonDb.Save(thread); | |||||
| } | |||||
| public async static Task<bool> ArchiverRestaurerItem(ThreadItemMini thread) | |||||
| { | |||||
| await Task.Delay(1); | |||||
| var store = ChargerDonnees(); | |||||
| var tts = store.Threads; | |||||
| ThreadItem? t = tts!.Where(e => e.Id == thread.Id)!.FirstOrDefault(); | |||||
| if (t == null) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| t.SubjectClosed = !t.SubjectClosed; | |||||
| Save(t); | |||||
| return true; | |||||
| } | |||||
| public async static Task<bool> SaveSendRelanceItem(ThreadItemMini thread) | |||||
| { | |||||
| await Task.Delay(1); | |||||
| var store = ChargerDonnees(); | |||||
| var tts = store.Threads; | |||||
| ThreadItem? t = tts!.Where(e=>e.Id == thread.Id)!.FirstOrDefault(); | |||||
| if (t == null) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| t.FollowUpDraft = thread.FollowUpDraft; | |||||
| Save(t); | |||||
| return true; | |||||
| } | |||||
| public async static Task<bool> SaveAsync(ThreadItem thread) | |||||
| { | |||||
| await Task.Delay(1); | |||||
| LoggerService.LogInfo("EmailSendService.Save"); | |||||
| return JsonDb.Save(thread); | |||||
| } | |||||
| public static void AllConversationsChangeStatus(bool isClosed) | |||||
| { | |||||
| var store = JsonDb.Load(); | |||||
| foreach(var conversation in store.Threads) | |||||
| { | |||||
| conversation.SubjectClosed = isClosed; | |||||
| } | |||||
| JsonDb.Save(store); | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes privées | |||||
| private static async Task ProcessFolder(IMailFolder folder) | |||||
| { | |||||
| var excludedFolders = new[] { "Junk", "spam", "Sent", "Trash", "Drafts" }; | |||||
| if (Array.Exists(excludedFolders, f => string.Equals(f, folder.Name, StringComparison.OrdinalIgnoreCase))) | |||||
| return; | |||||
| if (folder.Attributes.HasFlag(FolderAttributes.NoSelect)) | |||||
| { | |||||
| LoggerService.LogDebug($"📂 {folder.FullName} (conteneur uniquement, pas de mails)"); | |||||
| } | |||||
| else | |||||
| { | |||||
| ListInboxFolders.Add(folder); | |||||
| LoggerService.LogDebug($"📂 {folder.FullName}"); | |||||
| } | |||||
| // Parcours récursif des sous-dossiers | |||||
| foreach (var sub in folder.GetSubfolders()) | |||||
| { | |||||
| await ProcessFolder(sub); | |||||
| } | |||||
| } | |||||
| private static async Task<bool> AddMessage(UniqueId uid, ThreadStore store, MimeMessage msg, string direction, string folder, int overdueDays) | |||||
| { | |||||
| //var folder = (direction == "out" ? "Sent" : "Inbox"); | |||||
| var subject = msg.Subject ?? "(sans objet)"; | |||||
| var body = msg.TextBody ?? msg.HtmlBody ?? ""; | |||||
| var messageId = msg.MessageId ?? Guid.NewGuid().ToString(); | |||||
| var references = msg.References?.ToArray() ?? Array.Empty<string>(); | |||||
| var inReplyTo = msg.InReplyTo; | |||||
| // Trouve un thread existant basé sur Reply-To | |||||
| var thread = (ThreadItem?)null; | |||||
| if (messageId != null) | |||||
| { | |||||
| // On cherche le message par son ID (pour ne pas créer un doublon) | |||||
| thread = store.Threads.Find(t => | |||||
| t.Messages.Exists(m => m.Id == messageId) | |||||
| ); | |||||
| } | |||||
| if (thread == null && inReplyTo != null) | |||||
| { | |||||
| // On cherche si une reponse existe déjà pour mettre ce message dans le même thread | |||||
| thread = store.Threads.Find(t => | |||||
| t.Messages.Exists(m => m.Id == inReplyTo) | |||||
| ); | |||||
| } | |||||
| if (thread == null) | |||||
| { | |||||
| thread = store.Threads.Find(t => | |||||
| t.Messages.Exists(m => m.InReplyTo == messageId) | |||||
| ); | |||||
| } | |||||
| // Vérifie si déjà importé via UID | |||||
| /* | |||||
| if (thread != null && thread.Messages.Exists(m => m.Folder == folder && m.Uid == uid)) | |||||
| return false; | |||||
| */ | |||||
| if (thread != null) | |||||
| { | |||||
| // Si le message existe déjà dans le thread, il a peut être été déplacé de dossier | |||||
| if (thread.Messages.Exists(m => m.Id == msg.MessageId)) | |||||
| { | |||||
| if (thread.Messages.Exists(m => m.Folder == folder && m.Id == msg.MessageId)) | |||||
| { | |||||
| // existe et n'a pas été déplacé : on arrête | |||||
| return false; | |||||
| } | |||||
| else | |||||
| { | |||||
| // a été déplacé --> mettre à jour le folder et uid | |||||
| var msgExist = thread.Messages.Find(m => m.Id == msg.MessageId); | |||||
| if (msgExist != null) | |||||
| { | |||||
| msgExist.Folder = folder; | |||||
| //msgExist.Uid = uid; | |||||
| return true; | |||||
| } | |||||
| else | |||||
| { | |||||
| // cas impossible ou msgExist est null | |||||
| return false; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| if (thread == null) | |||||
| { | |||||
| thread = new ThreadItem(overdueDays) { Subject = subject, DateMessage = msg.Date }; | |||||
| store.Threads.Add(thread); | |||||
| } | |||||
| var analysis = await AnalyzeMessage(body, direction); | |||||
| thread.Messages.Add(new MailMessageItem | |||||
| { | |||||
| Id = msg.MessageId!, | |||||
| InReplyTo = msg.InReplyTo, | |||||
| //UidString = uid.ToString(), | |||||
| Folder = folder, | |||||
| Subject = subject, | |||||
| From = msg.From.Mailboxes.FirstOrDefault()?.Address ?? "", | |||||
| To = string.Join("; ", msg.To.Mailboxes.Select(m => m.Address)), | |||||
| Direction = direction, | |||||
| Date = msg.Date, | |||||
| Body = body, | |||||
| RequiresResponse = analysis.requiresResponse, | |||||
| CoversAllPoints = analysis.coversAllPoints | |||||
| }); | |||||
| return true; | |||||
| } | |||||
| private static async Task<(bool requiresResponse, bool coversAllPoints)> AnalyzeMessage(string body, string direction) | |||||
| { | |||||
| try | |||||
| { | |||||
| var prompt = ""; | |||||
| if (direction == "out") | |||||
| { | |||||
| //prompt = $"Réponds uniquement par oui ou non. Une réponse est-elle nécessaire à ce mail\nDébut du mail:\n{body} \n- fin du mail"; | |||||
| prompt = PromptService.GetPrompt(PromptService.ePrompt.EmailSendService_AnalyserMail_BesoinReponse, body); | |||||
| } | |||||
| else | |||||
| { | |||||
| //prompt = $"Réponds uniquement par oui ou non. Tous les points de ce mail ont-ils été traités\nDébut du mail:\n{body} \n- fin du mail"; | |||||
| prompt = PromptService.GetPrompt(PromptService.ePrompt.EmailSendService_AnalyserMail_AllPointsChecked, body); | |||||
| } | |||||
| var (reponseOllama, m) = await _ReActAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.AnalyseMails, false, prompt, "Analyse mails sortants"); | |||||
| if(reponseOllama == null || reponseOllama.ToString().Trim().Length ==0) | |||||
| { | |||||
| return (true, true); | |||||
| } | |||||
| if (direction == "out") | |||||
| { | |||||
| var rep = (reponseOllama.ToString().ToLower().Contains("oui")); | |||||
| return (rep, false); | |||||
| } | |||||
| else | |||||
| { | |||||
| var rep = (reponseOllama.ToString().ToLower().Contains("oui")); | |||||
| return (false, rep); | |||||
| } | |||||
| } | |||||
| catch | |||||
| { | |||||
| return (true, true); | |||||
| } | |||||
| } | |||||
| private static async Task<string> GenerateFollowUp(ThreadItem thread) | |||||
| { | |||||
| var conversation = string.Join("\n\n", thread.Messages.ConvertAll(m => $"[{m.Direction}] {m.Body}")); | |||||
| //var prompt = $"Génère un brouillon de mail en langue française de relance poli basé sur cette conversation :\n{conversation}"; | |||||
| var prompt = PromptService.GetPrompt(PromptService.ePrompt.EmailSendService_AnalyserMail_Relance, thread.Subject, conversation, ParametreUser.UserNomPrenom, ParametreUser.UserRole, ParametreUser.UserEntreprise); | |||||
| var (reponseOllama, _) = await _ReActAgent.AppelerLLMAsync(ModelsUseCases.TypeUseCase.AnalyseMails, false, prompt, "Génération mail de relance"); | |||||
| return reponseOllama; | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| #endregion | |||||
| } |
| using System.Text; | |||||
| namespace EmailService.Services | |||||
| { | |||||
| public static class TaskService | |||||
| { | |||||
| public static void CreateThunderbirdTask(string subject, DateTime dtStart, int DureeMinutes) | |||||
| { | |||||
| var dtEnd = dtStart.AddMinutes(DureeMinutes); | |||||
| string icsContent = $@" | |||||
| BEGIN:VCALENDAR | |||||
| VERSION:2.0 | |||||
| PRODID:-//MonApplication//Évènement Thunderbird//FR | |||||
| BEGIN:VEVENT | |||||
| SUMMARY:{subject} | |||||
| DTSTART:{dtStart:yyyyMMddTHHmmssZ} | |||||
| DTEND:{dtEnd:yyyyMMddTHHmmssZ} | |||||
| PRIORITY:5 | |||||
| STATUS:CONFIRMED | |||||
| END:VEVENT | |||||
| END:VCALENDAR | |||||
| "; | |||||
| string filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.ics"); | |||||
| File.WriteAllText(filePath, icsContent, Encoding.UTF8); | |||||
| // Ouvre le fichier pour import dans Thunderbird | |||||
| System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo | |||||
| { | |||||
| FileName = filePath, | |||||
| UseShellExecute = true | |||||
| }); | |||||
| } | |||||
| } | |||||
| } |
| namespace Services | |||||
| { | |||||
| public class CompteMailUser | |||||
| { | |||||
| public string UserAdresse { get; set; } = "prenom.nom@gmail.com"; | |||||
| public string UserMotPasse { get; set; } = "motdepasse!!!"; | |||||
| public string ServeurImap { get; set; } = "imap.gmail.com"; | |||||
| public int ServeurImapPort { get; set; } = 993; | |||||
| public string ServeurSmtp { get; set; } = "smtp.gmail.com"; | |||||
| public int ServeurSmtpPort { get; set; } = 587; | |||||
| public bool IsOk { get; set; } = false; | |||||
| public string UserNomPrenom { get; set; } = "Votre nom et votre prénom"; | |||||
| public string UserRole { get; set; } = "Votre rôle"; | |||||
| public string UserEntreprise { get; set; } = "Votre entreprise"; | |||||
| public int DelayRefresh { get; set; } = 5; // en minutes, par défaut 5 minutes | |||||
| public int DelaySentRefresh { get; set; } = 5; // en minutes, par défaut 5 minutes | |||||
| public int DelaySentRecup { get; set; } = 30; // en jours, par défaut 30 jours | |||||
| public int OverdueDaysSent { get; set; } = 7; // en jours, par défaut 7 jours | |||||
| } | |||||
| } |
| using MailKit; | |||||
| using System.Text.Json.Serialization; | |||||
| namespace Services | |||||
| { | |||||
| public class ThreadItemMini | |||||
| { | |||||
| public string Id { get; set; } = ""; | |||||
| public string? FollowUpDraft { get; set; } | |||||
| } | |||||
| public class ThreadStore | |||||
| { | |||||
| public List<ThreadItem> Threads { get; set; } = new(); | |||||
| } | |||||
| public class ThreadItem | |||||
| { | |||||
| #region Constructeurs | |||||
| //Necessaire pour la déserialisation en json | |||||
| public ThreadItem() | |||||
| { | |||||
| OverdueDays = 7; | |||||
| } | |||||
| //Necessaire pour passer le paramètre lors de la création d'objets | |||||
| public ThreadItem(int overdueDays=7) | |||||
| { | |||||
| OverdueDays = overdueDays; | |||||
| } | |||||
| #endregion | |||||
| #region Méthode privée | |||||
| private bool IsOverDueThread() | |||||
| { | |||||
| var t = this; | |||||
| if (t.LastFollowUpForMessageId == null || t.FollowUpDraft == null) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| var m = t.Messages.Where(m => m.Id == LastFollowUpForMessageId).FirstOrDefault(); | |||||
| if (m == null) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| if (m.Date < DateTimeOffset.UtcNow.AddDays(-1 * OverdueDays)) | |||||
| return true; | |||||
| else | |||||
| return false; | |||||
| } | |||||
| #endregion | |||||
| #region Variable | |||||
| [JsonIgnore] | |||||
| public int OverdueDays { get; set; } = 7; | |||||
| #endregion | |||||
| #region Propriétés | |||||
| public string Id { get; set; } = Guid.NewGuid().ToString(); | |||||
| public string Subject { get; set; } = ""; | |||||
| public List<MailMessageItem> Messages { get; set; } = new(); | |||||
| public string? FollowUpDraft { get; set; } | |||||
| public string? LastFollowUpForMessageId { get; set; } | |||||
| public bool SubjectClosed { get; set; } = false; | |||||
| public DateTimeOffset DateMessage { get; set; } | |||||
| public DateTimeOffset DateLastMessage { get; set; } | |||||
| [JsonIgnore] | |||||
| public string DateMessageSTR { get => DateMessage.ToString("dd/MM/yyyy HH:mm"); } | |||||
| [JsonIgnore] | |||||
| public string DateLastMessageSTR { get => DateLastMessage.ToString("dd/MM/yyyy HH:mm"); } | |||||
| [JsonIgnore] | |||||
| public IEnumerable<MailMessageItem> MessagesSorted => Messages.OrderBy(m => m.Date.UtcDateTime); | |||||
| [JsonIgnore] | |||||
| public bool IsOverdue | |||||
| { | |||||
| get | |||||
| { | |||||
| return IsOverDueThread(); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| public class MailMessageItem | |||||
| { | |||||
| public string Id { get; set; } = ""; | |||||
| public string InReplyTo { get; set; } = ""; | |||||
| /*[JsonIgnore] | |||||
| public string UidString | |||||
| { | |||||
| get | |||||
| { | |||||
| return Uid.ToString(); | |||||
| } | |||||
| set | |||||
| { | |||||
| if (value != "0") | |||||
| Uid = UniqueId.Parse(value); | |||||
| } | |||||
| }*/ | |||||
| public string Folder { get; set; } = ""; | |||||
| public string Subject { get; set; } = ""; | |||||
| public string From { get; set; } = ""; | |||||
| public string To { get; set; } = ""; | |||||
| public string Direction { get; set; } = "out"; // "in" ou "out" | |||||
| public DateTimeOffset Date { get; set; } | |||||
| public string Body { get; set; } = ""; | |||||
| public bool RequiresResponse { get; set; } | |||||
| public bool CoversAllPoints { get; set; } | |||||
| //[JsonIgnore] | |||||
| //public UniqueId Uid { get; set; } | |||||
| [JsonIgnore] | |||||
| public string DateSTR { get => $"Le : {Date.DateTime.ToString("dd/MM/yyyy HH:mm")}"; } | |||||
| [JsonIgnore] | |||||
| public string FromSTR { get => $"De : {From}"; } | |||||
| [JsonIgnore] | |||||
| public string ToSTR { get => $"Pour : {To}"; } | |||||
| [JsonIgnore] | |||||
| public string FolderSTR { get => $"Dossier : {Folder}"; } | |||||
| [JsonIgnore] | |||||
| public string SubjectSTR { get => $"Sujet : {Subject}"; } | |||||
| } | |||||
| } |
| using MailKit; | |||||
| using System.Text.Json.Serialization; | |||||
| using System.Xml; | |||||
| namespace Services | |||||
| { | |||||
| public class EmailMessage | |||||
| { | |||||
| [JsonIgnore] | |||||
| public MailKit.UniqueId? Uid { get; set; } // Pour charger le corps plus tard | |||||
| [JsonPropertyName("UidString")] | |||||
| public string? UidString | |||||
| { | |||||
| get => Uid != null ? Uid?.ToString() : null; | |||||
| set | |||||
| { | |||||
| if (!string.IsNullOrWhiteSpace(value)) | |||||
| { | |||||
| if (MailKit.UniqueId.TryParse(value, out var parsed)) | |||||
| Uid = parsed; | |||||
| else | |||||
| Uid = null; // ou log une erreur | |||||
| } | |||||
| else | |||||
| { | |||||
| Uid = null; | |||||
| } | |||||
| } | |||||
| } | |||||
| /* | |||||
| public string UidString | |||||
| { | |||||
| get => Uid.ToString(); | |||||
| set => Uid = UniqueId.Parse(value); | |||||
| } | |||||
| */ | |||||
| public string Id { get; set; } = ""; | |||||
| public string To { get; set; } = ""; | |||||
| public string Cc { get; set; } = ""; | |||||
| public bool IsDestinatairePrincipal { get; set; }=false; | |||||
| public string From { get; set; } = ""; | |||||
| public string FromName { get; set; } = ""; | |||||
| public string Subject { get; set; } = ""; | |||||
| public string InReplyTo { get; set; } = ""; | |||||
| public DateTime Date { get; set; } | |||||
| public string DateSTR { get => Date.ToString("dd/MM/yyyy HH:mm"); } | |||||
| public string Analyse { get; set; } = ""; | |||||
| public string Preview { get; set; } = ""; | |||||
| public string TextBody { get; set; } = ""; | |||||
| public string TextBodyHTML { get; set; } = ""; | |||||
| [JsonIgnore] | |||||
| public string TextBody_PJ | |||||
| { | |||||
| get | |||||
| { | |||||
| if (ContentPJ == null || ContentPJ.Count == 0) | |||||
| return TextBody; | |||||
| var joined = string.Join("\n---\n", ContentPJ); | |||||
| return $"{TextBody}\n\n--- Contenu des pièces jointes ---\n{joined}"; | |||||
| } | |||||
| } | |||||
| [JsonIgnore] | |||||
| public string ContentPJ_STR | |||||
| { | |||||
| get | |||||
| { | |||||
| if (ContentPJ == null || ContentPJ.Count == 0) | |||||
| return ""; | |||||
| var joined = string.Join("\n---\n", ContentPJ); | |||||
| return $"--- Contenu des pièces jointes ---\n{joined}"; | |||||
| } | |||||
| } | |||||
| public string Resume { get; set; } = ""; | |||||
| public string Categorie { get; set; } = ""; | |||||
| public string Strategie { get; set; } = ""; | |||||
| public string Reponse { get; set; } = ""; | |||||
| public bool IsImportant { get; set; } = false; | |||||
| public bool IsUrgent { get; set; } = false; | |||||
| public int ImportanceScore { get; set; } = 0; // de 0 à 5 | |||||
| public int UrgenceScore { get; set; } = 0; // de 0 à 5 | |||||
| public List<string>? ContentPJ { get; set; } = new(); | |||||
| public int PJ_NBR | |||||
| { | |||||
| get | |||||
| { | |||||
| if (ContentPJ == null || ContentPJ.Count == 0) | |||||
| return 0; | |||||
| return ContentPJ.Count; | |||||
| } | |||||
| } | |||||
| [JsonIgnore] | |||||
| public string PJ_NBR_STR | |||||
| { | |||||
| get | |||||
| { | |||||
| if (PJ_NBR > 0) | |||||
| return "📎"; | |||||
| return ""; | |||||
| } | |||||
| } | |||||
| public bool IsPJ { get => ContentPJ != null && ContentPJ.Count > 0; } | |||||
| public string Priorite { get => $"Important : {(IsImportant ? "✔️" : "❌")} - Urgent : {(IsUrgent ? "⚠️" : "❌")}"; } | |||||
| public string ImportanceSTR => $"{ImportanceScore}/5"; | |||||
| public string UrgenceSTR => $"{UrgenceScore}/5"; | |||||
| public string ModeleIA { get; set; } = ""; | |||||
| [JsonIgnore] | |||||
| public bool ToRemove { get; set; } = false; | |||||
| public bool PresenceSuspecte { get; set; } = false; | |||||
| [JsonIgnore] | |||||
| public string PresenceSuspecte_STR | |||||
| { | |||||
| get | |||||
| { | |||||
| if (PresenceSuspecte) | |||||
| return "⚠️"; | |||||
| return ""; | |||||
| } | |||||
| } | |||||
| [JsonIgnore] | |||||
| public string IsDestinatairePrincipal_STR | |||||
| { | |||||
| get | |||||
| { | |||||
| if (IsDestinatairePrincipal) | |||||
| return "📩"; | |||||
| else | |||||
| return "👥"; | |||||
| } | |||||
| } | |||||
| [JsonIgnore] | |||||
| public string IsDestinatairePrincipal_STR_TXT | |||||
| { | |||||
| get | |||||
| { | |||||
| if (IsDestinatairePrincipal) | |||||
| return "Vous êtes destinataire principal de ce mail"; | |||||
| else | |||||
| return "Vous êtes en copie de ce mail"; | |||||
| } | |||||
| } | |||||
| } | |||||
| #region Classe MessageToSend | |||||
| public class MessageToSend | |||||
| { | |||||
| public string Object { get; set; } = ""; | |||||
| public string DestinataireA { get; set; } = ""; | |||||
| public string DestinataireCC { get; set; } = ""; | |||||
| public string Type { get; set; } = "Professionnel"; | |||||
| public string Ton { get; set; } = ""; | |||||
| public string Posture { get; set; } = ""; | |||||
| public string Couleur { get; set; } = ""; | |||||
| public string NiveauDetail{ get; set; } = ""; | |||||
| public bool VOUS_TU { get; set; } = true;// "true : 'Vous'" ou "false : 'Tu'" | |||||
| public string Query { get; set; } = ""; | |||||
| public string Body { get; set; } = "..."; | |||||
| } | |||||
| #endregion | |||||
| } |
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | |||||
| <TargetFramework>net8.0</TargetFramework> | |||||
| <ImplicitUsings>enable</ImplicitUsings> | |||||
| <Nullable>enable</Nullable> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <PackageReference Include="MailKit" Version="4.14.0" /> | |||||
| </ItemGroup> | |||||
| </Project> |
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | |||||
| <TargetFramework>net8.0</TargetFramework> | |||||
| <ImplicitUsings>enable</ImplicitUsings> | |||||
| <Nullable>enable</Nullable> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <PackageReference Include="Emgu.CV.runtime.windows.cuda" Version="4.8.0.5324" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <Folder Include="Models\" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\ToolsService\ToolsService.csproj" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <None Update="Models\face_detection_yunet_2023mar.onnx"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="Models\haarcascade_frontalface_alt.xml"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="Models\haarcascade_frontalface_default.xml"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="Models\model_reconnaissance.yml"> | |||||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
| </None> | |||||
| </ItemGroup> | |||||
| </Project> |
| using Emgu.CV; | |||||
| using Emgu.CV.Dnn; | |||||
| using Emgu.CV.Face; | |||||
| using Emgu.CV.Structure; | |||||
| using System.Drawing; | |||||
| using System.Text; | |||||
| using ToolsServices; | |||||
| using static System.Net.Mime.MediaTypeNames; | |||||
| namespace FaceRecognition | |||||
| { | |||||
| public class FaceRecognitionService | |||||
| { | |||||
| #region Variables privées statiques | |||||
| private static string NomFichierData = FichiersInternesService.ParamsFaceRecognition; | |||||
| #endregion | |||||
| #region Variables privées | |||||
| private readonly string _PathDataTrainSet = @"D:\_TrainingData\FaceRecognition\TrainSet"; | |||||
| private readonly string _PathModel = @"Models"; | |||||
| private readonly string _NomModelDetect = @"face_detection_yunet_2023mar.onnx"; | |||||
| private readonly string _NomModel = @"model_reconnaissance.yml"; | |||||
| private readonly string _FaceHaarXML = @"haarcascade_frontalface_alt.xml"; | |||||
| private readonly int _ReSizeFaceImg = 300; | |||||
| private readonly int _ReSizeImage = 400; | |||||
| private readonly float _ScaleFactor = 0.9f; | |||||
| private readonly System.Drawing.Size _MinSizeDetect = new(30, 30); | |||||
| private readonly int _DistanceMini = 40; | |||||
| private readonly MCvScalar _MCvScalarColorGreen = new(0, 255, 0); | |||||
| private readonly MCvScalar _MCvScalarColorRed = new(0, 0, 255); | |||||
| private readonly int _SizeBorder = 2; | |||||
| private readonly LBPHFaceRecognizer _FaceRecognizer = new(); | |||||
| private readonly FaceDetectorYN _FaceNet; | |||||
| private List<int> _LstLabels = new(); | |||||
| private List<string> _LstLabelsString = new(); | |||||
| private readonly System.Drawing.Size _InputSize = new(300, 300); | |||||
| #endregion | |||||
| #region Constructeur | |||||
| public FaceRecognitionService(bool isCuda = false) | |||||
| { | |||||
| var parametres = LoadParametres(); | |||||
| _PathDataTrainSet = parametres.Path_TrainSet; | |||||
| // Créer un objet CascadeClassifier pour détecter les visages | |||||
| if (isCuda) | |||||
| { | |||||
| _FaceNet = new FaceDetectorYN(TrtFindFileFaceModelDetect(), "", _InputSize, backendId: Emgu.CV.Dnn.Backend.Cuda, targetId: Target.Cuda); | |||||
| } | |||||
| else | |||||
| { | |||||
| _FaceNet = new FaceDetectorYN(TrtFindFileFaceModelDetect(), "", _InputSize, backendId: Emgu.CV.Dnn.Backend.Default, targetId: Target.Cpu); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Gestion des paramètres TrainSet et TestSet | |||||
| public static ParametresFaceRecognitionService LoadParametres() | |||||
| { | |||||
| LoggerService.LogInfo("ParametresFaceRecognitionService.LoadParametres"); | |||||
| ParametresFaceRecognitionService SelectedItem = new(); | |||||
| try | |||||
| { | |||||
| if (File.Exists(NomFichierData)) | |||||
| { | |||||
| string[] lignes = File.ReadAllLines(NomFichierData); | |||||
| if (lignes.Length > 0) | |||||
| SelectedItem.Path_TrainSet = lignes[0]; | |||||
| if (lignes.Length > 1) | |||||
| SelectedItem.Path_TestSet = lignes[1]; | |||||
| } | |||||
| return SelectedItem; | |||||
| } | |||||
| catch | |||||
| { | |||||
| return new(); | |||||
| } | |||||
| } | |||||
| public static bool SaveParametres(ParametresFaceRecognitionService selectedItem) | |||||
| { | |||||
| LoggerService.LogInfo("ParametresFaceRecognitionService.SaveParametres"); | |||||
| try | |||||
| { | |||||
| StringBuilder sb = new(); | |||||
| sb.AppendLine(selectedItem.Path_TrainSet); | |||||
| sb.AppendLine(selectedItem.Path_TestSet); | |||||
| File.WriteAllText(NomFichierData, sb.ToString()); | |||||
| return true; | |||||
| } | |||||
| catch | |||||
| { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes publiques | |||||
| public bool CapturePhoto(string filename) | |||||
| { | |||||
| try | |||||
| { | |||||
| var inputImage = new Mat(filename); | |||||
| var inputClone = inputImage.Clone(); | |||||
| var lstRetour = new List<string>(); | |||||
| if (!File.Exists(filename)) | |||||
| { | |||||
| throw new Exception($"Fichier manquant : {filename}."); | |||||
| } | |||||
| _FaceRecognizer.Read(TrtFindFileModel()); | |||||
| // Détecter les visages dans l'image | |||||
| var w = (int)(_ReSizeImage * 3 * _ScaleFactor); | |||||
| var h = (int)(_ReSizeImage * 3 * inputImage.Height / inputImage.Width * _ScaleFactor); | |||||
| CvInvoke.Resize(inputImage, inputImage, new System.Drawing.Size(w, h)); | |||||
| var faces = DetectMultiFaces(inputImage); | |||||
| if (faces.Length == 0) | |||||
| { | |||||
| inputImage = inputClone.Clone(); | |||||
| w = (int)(_ReSizeImage * 2 * _ScaleFactor); | |||||
| h = (int)(_ReSizeImage * 2 * inputImage.Height / inputImage.Width * _ScaleFactor); | |||||
| CvInvoke.Resize(inputImage, inputImage, new System.Drawing.Size(w, h)); | |||||
| faces = DetectMultiFaces(inputImage); | |||||
| } | |||||
| if (faces.Length == 0) | |||||
| { | |||||
| inputImage = inputClone.Clone(); | |||||
| w = (int)(_ReSizeImage * _ScaleFactor); | |||||
| h = (int)(_ReSizeImage * inputImage.Height / inputImage.Width * _ScaleFactor); | |||||
| CvInvoke.Resize(inputImage, inputImage, new System.Drawing.Size(w, h)); | |||||
| faces = DetectMultiFaces(inputImage); | |||||
| } | |||||
| // Parcourir chaque visage détecté | |||||
| foreach (var face in faces) | |||||
| { | |||||
| // Extraire la région du visage de l'image | |||||
| var faceImage = new Mat(inputImage, face); | |||||
| if (System.IO.File.Exists(filename)) | |||||
| System.IO.File.Delete(filename); | |||||
| CvInvoke.Imwrite(filename, faceImage); | |||||
| } | |||||
| return true; | |||||
| } | |||||
| catch | |||||
| { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| public (bool, string) Training(bool isPhotoVisible) | |||||
| { | |||||
| return TrtTraining(isPhotoVisible); | |||||
| } | |||||
| public List<string> Predict(string filename) | |||||
| { | |||||
| (var lst, _) = TrtPredict(new(filename)); | |||||
| return lst; | |||||
| } | |||||
| public List<string> Predict(Mat inputImage) | |||||
| { | |||||
| (var lst, _) = TrtPredict(inputImage); | |||||
| return lst; | |||||
| } | |||||
| public List<string> PredictLiveNoShow() | |||||
| { | |||||
| List<string> lst = new(); | |||||
| var videoCapture = new Emgu.CV.VideoCapture(0); | |||||
| try | |||||
| { | |||||
| int i = 0; | |||||
| while (true) | |||||
| { | |||||
| try | |||||
| { | |||||
| i++; | |||||
| Mat mat = new(); | |||||
| videoCapture.Read(mat); | |||||
| // Préparation de l'affichage de l'image | |||||
| (lst, _) = TrtPredict(mat, false, true); | |||||
| if (i > 4) | |||||
| break; | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| Console.WriteLine($"Erreur : {ex.Message}"); | |||||
| } | |||||
| } | |||||
| } | |||||
| catch | |||||
| { | |||||
| } | |||||
| return lst; | |||||
| } | |||||
| public void PredictLive() | |||||
| { | |||||
| var videoCapture = new Emgu.CV.VideoCapture(0); | |||||
| try | |||||
| { | |||||
| while (true) | |||||
| { | |||||
| try | |||||
| { | |||||
| Mat mat = new(); | |||||
| Mat matOutput; | |||||
| videoCapture.Read(mat); | |||||
| // Préparation de l'affichage de l'image | |||||
| (_, matOutput) = TrtPredict(mat, false, true); | |||||
| CvInvoke.Imshow("frame", matOutput); | |||||
| // affichage et saisie d'un code clavier (Q ou ECHAP) | |||||
| if (CvInvoke.WaitKey(1) == (int)ConsoleKey.Q || CvInvoke.WaitKey(1) == (int)ConsoleKey.Escape) | |||||
| break; | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| Console.WriteLine($"Erreur : {ex.Message}"); | |||||
| } | |||||
| } | |||||
| } | |||||
| catch | |||||
| { | |||||
| } | |||||
| finally | |||||
| { | |||||
| // Ne pas oublier de fermer le flux et la fenetre | |||||
| CvInvoke.WaitKey(0); | |||||
| CvInvoke.DestroyAllWindows(); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes privées | |||||
| private (bool, string) TrtTraining(bool isPhotoVisible) | |||||
| { | |||||
| bool isReturnOK = true; | |||||
| string msgReturn = ""; | |||||
| try | |||||
| { | |||||
| // Créer des listes pour stocker les images de formation et les étiquettes correspondantes | |||||
| var trainingImages = TrtFindImagesAndLabels(true, isPhotoVisible); | |||||
| // Entraîner le reconnaiseur avec les images de formation et les étiquettes | |||||
| _FaceRecognizer.Train(trainingImages.ToArray(), _LstLabels.ToArray()); | |||||
| for (int i = 0; i < _LstLabels.Count; i++) | |||||
| { | |||||
| _FaceRecognizer.SetLabelInfo(_LstLabels[i], _LstLabelsString[i]); | |||||
| } | |||||
| // Enregistrer le modèle entraîné | |||||
| _FaceRecognizer.Write(Path.Combine(_PathModel, _NomModel)); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| isReturnOK = false; | |||||
| msgReturn = ex.Message; | |||||
| } | |||||
| return (isReturnOK, msgReturn); | |||||
| } | |||||
| private (List<string>, Mat) TrtPredict(Mat inputImage, bool isAffichage = true, bool isOnlyFirstName=true) | |||||
| { | |||||
| var inputClone = inputImage.Clone(); | |||||
| var lstRetour = new List<string>(); | |||||
| string filename = TrtFindFileModel(); | |||||
| if (!File.Exists(filename)) | |||||
| { | |||||
| throw new Exception($"Fichier manquant : {filename}."); | |||||
| } | |||||
| _FaceRecognizer.Read(TrtFindFileModel()); | |||||
| // Convertir l'image en niveaux de gris pour faciliter la détection des visages | |||||
| var grayImage = new Mat(); | |||||
| // Détecter les visages dans l'image | |||||
| var w = (int)(_ReSizeImage * 3 * _ScaleFactor); | |||||
| var h = (int)(_ReSizeImage * 3 * inputImage.Height / inputImage.Width * _ScaleFactor); | |||||
| CvInvoke.Resize(inputImage, inputImage, new System.Drawing.Size(w, h)); | |||||
| var faces = DetectMultiFaces(inputImage); | |||||
| if (faces.Length == 0) | |||||
| { | |||||
| inputImage = inputClone.Clone(); | |||||
| w = (int)(_ReSizeImage * 2 * _ScaleFactor); | |||||
| h = (int)(_ReSizeImage * 2 * inputImage.Height / inputImage.Width * _ScaleFactor); | |||||
| CvInvoke.Resize(inputImage, inputImage, new System.Drawing.Size(w, h)); | |||||
| faces = DetectMultiFaces(inputImage); | |||||
| } | |||||
| if (faces.Length == 0) | |||||
| { | |||||
| inputImage = inputClone.Clone(); | |||||
| w = (int)(_ReSizeImage * _ScaleFactor); | |||||
| h = (int)(_ReSizeImage * inputImage.Height / inputImage.Width * _ScaleFactor); | |||||
| CvInvoke.Resize(inputImage, inputImage, new System.Drawing.Size(w, h)); | |||||
| faces = DetectMultiFaces(inputImage); | |||||
| } | |||||
| CvInvoke.CvtColor(inputImage, grayImage, Emgu.CV.CvEnum.ColorConversion.Bgr2Gray); | |||||
| // Parcourir chaque visage détecté | |||||
| foreach (var face in faces) | |||||
| { | |||||
| // Extraire la région du visage de l'image en niveaux de gris | |||||
| var faceImage = new Mat(grayImage, face); | |||||
| CvInvoke.Resize(faceImage, faceImage, new System.Drawing.Size(_ReSizeFaceImg, _ReSizeFaceImg)); | |||||
| // Reconnaître le visage à partir de l'image redimensionnée | |||||
| var predict = _FaceRecognizer.Predict(faceImage); | |||||
| if (predict.Distance < _DistanceMini) | |||||
| { | |||||
| var nameComplet = _FaceRecognizer.GetLabelInfo(predict.Label); | |||||
| var name = ""; | |||||
| if (isOnlyFirstName) | |||||
| { | |||||
| name = nameComplet.Split('_')[0]; | |||||
| } | |||||
| else | |||||
| { | |||||
| name = nameComplet.Replace("_", " "); | |||||
| } | |||||
| lstRetour.Add(name); | |||||
| CvInvoke.Rectangle(inputImage, face, _MCvScalarColorGreen, _SizeBorder); | |||||
| //CvInvoke.PutText(inputImage, name + " - " + predict.Distance.ToString("0.0"), new System.Drawing.Point(face.X, face.Y), Emgu.CV.CvEnum.FontFace.HersheySimplex, 1.0, _MCvScalarColor, _SizeBorder); | |||||
| CvInvoke.PutText(inputImage, name, new System.Drawing.Point(face.X, face.Y), Emgu.CV.CvEnum.FontFace.HersheySimplex, 1.0, _MCvScalarColorGreen, _SizeBorder); | |||||
| CvInvoke.PutText(inputImage, predict.Distance.ToString("0.0"), new System.Drawing.Point(face.X, face.Y + 30), Emgu.CV.CvEnum.FontFace.HersheySimplex, 0.75, _MCvScalarColorGreen, _SizeBorder); | |||||
| } | |||||
| else | |||||
| { | |||||
| //var name = "???"; | |||||
| //lstRetour.Add(name); | |||||
| CvInvoke.Rectangle(inputImage, face, _MCvScalarColorRed, _SizeBorder); | |||||
| CvInvoke.PutText(inputImage, predict.Distance.ToString("0.0"), new System.Drawing.Point(face.X, face.Y), Emgu.CV.CvEnum.FontFace.HersheySimplex, 1.0, _MCvScalarColorRed, _SizeBorder); | |||||
| } | |||||
| } | |||||
| if (isAffichage) | |||||
| { | |||||
| CvInvoke.Imshow("reconnaissance", inputImage); | |||||
| CvInvoke.WaitKey(0); | |||||
| CvInvoke.DestroyAllWindows(); | |||||
| } | |||||
| return (lstRetour, inputImage); | |||||
| } | |||||
| private List<Mat> TrtFindImagesAndLabels(bool isTraining, bool isPhotoVisible) | |||||
| { | |||||
| // Créer des listes pour stocker les images de formation et les étiquettes correspondantes | |||||
| var lstTrainingImages = new List<Mat>(); | |||||
| if(System.IO.Directory.Exists(_PathDataTrainSet) == false) | |||||
| { | |||||
| throw new Exception($"Le dossier n'existe pas : {_PathDataTrainSet}"); | |||||
| } | |||||
| try | |||||
| { | |||||
| _LstLabels = new(); | |||||
| _LstLabelsString = new(); | |||||
| var dirs = Directory.GetDirectories(_PathDataTrainSet); | |||||
| int numDir = 1; | |||||
| foreach (var dir in dirs) | |||||
| { | |||||
| var files = Directory.GetFiles(dir); | |||||
| foreach (var file in files) | |||||
| { | |||||
| var dernierDir = dir.Split(@"\"); | |||||
| var nomDernierDir = dernierDir[^1]; | |||||
| if (isTraining) | |||||
| { | |||||
| var image = new Mat(file); | |||||
| var grayImage = new Mat(); | |||||
| //var w = (int)(image.Width * _ScaleFactor); | |||||
| //var h = (int)(image.Height * _ScaleFactor); | |||||
| var w = (int)(_ReSizeImage * _ScaleFactor); | |||||
| var h = (int)(_ReSizeImage * image.Height / image.Width * _ScaleFactor); | |||||
| CvInvoke.Resize(image, image, new System.Drawing.Size(w, h)); | |||||
| var faces = DetectMultiFaces(image); | |||||
| CvInvoke.CvtColor(image, grayImage, Emgu.CV.CvEnum.ColorConversion.Bgr2Gray); | |||||
| if (faces.Length > 0) | |||||
| { | |||||
| var face = new Mat(grayImage, faces[0]); | |||||
| CvInvoke.Resize(face, face, new System.Drawing.Size(_ReSizeFaceImg, _ReSizeFaceImg)); | |||||
| lstTrainingImages.Add(face); | |||||
| _LstLabels.Add(numDir); | |||||
| _LstLabelsString.Add(nomDernierDir); | |||||
| if (isPhotoVisible) | |||||
| { | |||||
| CvInvoke.PutText(image, nomDernierDir, new System.Drawing.Point(faces[0].X, faces[0].Y), Emgu.CV.CvEnum.FontFace.HersheySimplex, 1.0, _MCvScalarColorGreen, _SizeBorder); | |||||
| CvInvoke.Rectangle(image, faces[0], _MCvScalarColorGreen, _SizeBorder); | |||||
| } | |||||
| } | |||||
| else | |||||
| { | |||||
| Console.WriteLine("Visage non détecté"); | |||||
| } | |||||
| if (isPhotoVisible) | |||||
| { | |||||
| CvInvoke.Imshow("reconnaissance", image); | |||||
| CvInvoke.WaitKey(0); | |||||
| CvInvoke.DestroyAllWindows(); | |||||
| } | |||||
| } | |||||
| else | |||||
| { | |||||
| _LstLabels.Add(numDir); | |||||
| _LstLabelsString.Add(nomDernierDir); | |||||
| } | |||||
| } | |||||
| numDir++; | |||||
| } | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| throw new Exception(ex.Message); | |||||
| } | |||||
| return lstTrainingImages; | |||||
| } | |||||
| public Rectangle[] DetectMultiFaces(Mat img) | |||||
| { | |||||
| var lstRect = new List<Rectangle>(); | |||||
| _FaceNet.InputSize = new System.Drawing.Size(img.Width, img.Height); | |||||
| var outputFaces = new Mat(); | |||||
| _FaceNet.Detect(img, outputFaces); | |||||
| var detectionArray = outputFaces.GetData(); | |||||
| if (detectionArray is null) | |||||
| { | |||||
| return new Rectangle[0]; | |||||
| } | |||||
| var max = detectionArray.GetLength(0); | |||||
| Parallel.For(0, max, i => | |||||
| { | |||||
| var confidence = (float)((Single)detectionArray.GetValue(i, 14)!); | |||||
| if (confidence > 0.5) | |||||
| { | |||||
| // Coordonnées 2 points qui tracent un rectangle englobe le visage | |||||
| var x1 = (int)((Single)detectionArray.GetValue(i, 0)!); | |||||
| var y1 = (int)((Single)detectionArray.GetValue(i, 1)!); | |||||
| var x2 = (int)((Single)detectionArray.GetValue(i, 2)!); | |||||
| var y2 = (int)((Single)detectionArray.GetValue(i, 3)!); | |||||
| lstRect.Add(new System.Drawing.Rectangle(x1, y1, x2, y2)); | |||||
| } | |||||
| }); | |||||
| return lstRect.ToArray(); | |||||
| } | |||||
| private string TrtFindFileFaceHaarXML() | |||||
| { | |||||
| return Path.Combine(_PathModel, _FaceHaarXML); | |||||
| } | |||||
| private string TrtFindFileFaceModelDetect() | |||||
| { | |||||
| return Path.Combine(_PathModel, _NomModelDetect); | |||||
| } | |||||
| private string TrtFindFileModel() | |||||
| { | |||||
| return Path.Combine(_PathModel, _NomModel); | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| } |
| Fichiers a mettre dans ce répertoire : | |||||
| - face_detection_yunet_2023mar.onnx | |||||
| - haarcascade_frontalface_alt.xml | |||||
| - haarcascade_frontalface_default.xml | |||||
| - model_reconnaissance.yml |
| namespace FaceRecognition | |||||
| { | |||||
| public class ParametresFaceRecognitionService | |||||
| { | |||||
| public string Path_TrainSet { get; set; } = ""; | |||||
| public string Path_TestSet { get; set; } = ""; | |||||
| } | |||||
| } |
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | |||||
| <TargetFramework>net8.0</TargetFramework> | |||||
| <ImplicitUsings>enable</ImplicitUsings> | |||||
| <Nullable>enable</Nullable> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\ToolsService\ToolsService.csproj" /> | |||||
| </ItemGroup> | |||||
| </Project> |
| namespace Services.Fooocus | |||||
| { | |||||
| public class Lora | |||||
| { | |||||
| public bool enabled { get; set; } = false; | |||||
| public string model_name { get; set; } = "sd_xl_offset_example-lora_1.0.safetensors"; | |||||
| public double weight { get; set; } = 0.5; | |||||
| } | |||||
| public class FooocusRequest_Text_to_Image | |||||
| { | |||||
| public string prompt { get; set; } = ""; | |||||
| public string negative_prompt { get; set; } = "blurry, low quality, distorted, watermark, text, extra limbs, cropped"; | |||||
| public string[] style_selections { get; set; } = new string[0]; | |||||
| public string performance_selection { get; set; } = "Speed";//Speed, Quality, Extreme Speed | |||||
| public string aspect_ratios_selection { get; set; } = "1152*896"; | |||||
| public int image_number { get; set; } = 1; | |||||
| public int image_seed { get; set; } = -1; | |||||
| public double sharpness { get; set; } = 2.0; //0-30 | |||||
| public double guidance_scale { get; set; } = 4.0;//1-30 | |||||
| public string base_model_name { get; set; }= "juggernautXL_v8Rundiffusion.safetensors"; | |||||
| public string refiner_model_name { get; set; } = "None"; | |||||
| public double refiner_switch { get; set; } = 0.5; | |||||
| public bool require_base64 { get; set; } = false; | |||||
| public bool async_process { get; set; } = false; | |||||
| public Lora[] loras { get; set; } = new Lora[0]; | |||||
| } | |||||
| public class FooocusRequest | |||||
| { | |||||
| // --- PARAMÈTRES DE BASE --- | |||||
| /// <summary> | |||||
| /// Description textuelle de l’image à générer. | |||||
| /// Exemple : "un chat steampunk sur un toit". | |||||
| /// Obligatoire. | |||||
| /// </summary> | |||||
| public string Prompt { get; set; } = ""; | |||||
| /// <summary> | |||||
| /// Liste de ce que l’on veut éviter (qualité faible, flou...). | |||||
| /// Exemple : "blurry, low quality". | |||||
| /// Recommandé pour améliorer la sortie. | |||||
| /// </summary> | |||||
| public string NegativePrompt { get; set; } = "blurry, low quality, distorted, watermark, text, extra limbs, cropped"; | |||||
| /// <summary> | |||||
| /// Largeur de l’image en pixels. | |||||
| /// Valeurs courantes : 512, 768, 1024. | |||||
| /// Défaut = 768. | |||||
| /// </summary> | |||||
| public int Width { get; set; } = 768; | |||||
| /// <summary> | |||||
| /// Hauteur de l’image en pixels. | |||||
| /// Valeurs courantes : 512, 768, 1024. | |||||
| /// Défaut = 768. | |||||
| /// </summary> | |||||
| public int Height { get; set; } = 768; | |||||
| /// <summary> | |||||
| /// Nombre d’itérations de diffusion (steps). | |||||
| /// Plus c’est haut, plus c’est détaillé mais lent. | |||||
| /// Recommandé : 20–40. | |||||
| /// Défaut = 30. | |||||
| /// </summary> | |||||
| public int Steps { get; set; } = 40; | |||||
| /// <summary> | |||||
| /// Influence du prompt sur le rendu (CFG scale). | |||||
| /// Valeurs typiques : 5–12. | |||||
| /// Recommandé = 7.5. | |||||
| /// </summary> | |||||
| public double GuidanceScale { get; set; } = 9.0; | |||||
| // --- STYLE & PERFORMANCE --- | |||||
| /// <summary> | |||||
| /// Liste des styles appliqués (ex. "Fooocus V2", "Fooocus Enhance"). | |||||
| /// Peut rester vide. | |||||
| /// </summary> | |||||
| public string[] StyleSelections { get; set; } = new string[0]; | |||||
| /// <summary> | |||||
| /// Mode de performance. | |||||
| /// Options : "Speed", "Quality", "Extreme Speed". | |||||
| /// Défaut = "Speed". | |||||
| /// </summary> | |||||
| public string PerformanceSelection { get; set; } = "Speed"; | |||||
| /// <summary> | |||||
| /// À quel moment le modèle "refiner" intervient (0 = jamais, 1 = fin). | |||||
| /// Défaut = 0.5. | |||||
| /// </summary> | |||||
| public double RefinerSwitch { get; set; } = 0.0; | |||||
| /// <summary> | |||||
| /// Netteté appliquée à l’image finale. | |||||
| /// Valeurs : 1.0 (douce) à 3.0 (très nette). | |||||
| /// Défaut = 2.0. | |||||
| /// </summary> | |||||
| public double Sharpness { get; set; } = 2.0; | |||||
| // --- SEED & BATCH --- | |||||
| /// <summary> | |||||
| /// Seed de génération. | |||||
| /// -1 = aléatoire. | |||||
| /// Utiliser une seed fixe pour reproductibilité. | |||||
| /// </summary> | |||||
| public int ImageSeed { get; set; } = -1; | |||||
| /// <summary> | |||||
| /// Nombre d’images à générer en une requête. | |||||
| /// Défaut = 1. | |||||
| /// </summary> | |||||
| public int ImageNumber { get; set; } = 1; | |||||
| /// <summary> | |||||
| /// Ratio prédéfini de l’image (format). | |||||
| /// Exemple : "768*768", "1152*896". | |||||
| /// Défaut = "768*768". | |||||
| /// </summary> | |||||
| public string AspectRatiosSelection { get; set; } = "768*768"; | |||||
| // --- CONTROLNET / IMAGE PROMPTS --- | |||||
| /// <summary> | |||||
| /// Première image de référence (ControlNet). | |||||
| /// Peut être une URL ou un chemin. | |||||
| /// </summary> | |||||
| public string? CnImg1 { get; set; } | |||||
| /// <summary> | |||||
| /// Type de ControlNet pour CnImg1 (ex. "Canny", "Depth", "ImagePrompt"). | |||||
| /// </summary> | |||||
| public string? CnType1 { get; set; } | |||||
| /// <summary> | |||||
| /// Poids de l’influence de CnImg1 (0–1). | |||||
| /// Défaut ≈ 0.8. | |||||
| /// </summary> | |||||
| public double? CnWeight1 { get; set; } | |||||
| /// <summary> | |||||
| /// Étape à laquelle arrêter ControlNet (0–1). | |||||
| /// Défaut ≈ 0.2. | |||||
| /// </summary> | |||||
| public double? CnStop1 { get; set; } | |||||
| // (Idem pour 4 images maximum) | |||||
| public string? CnImg2 { get; set; } | |||||
| public string? CnType2 { get; set; } | |||||
| public double? CnWeight2 { get; set; } | |||||
| public double? CnStop2 { get; set; } | |||||
| public string? CnImg3 { get; set; } | |||||
| public string? CnType3 { get; set; } | |||||
| public double? CnWeight3 { get; set; } | |||||
| public double? CnStop3 { get; set; } | |||||
| public string? CnImg4 { get; set; } | |||||
| public string? CnType4 { get; set; } | |||||
| public double? CnWeight4 { get; set; } | |||||
| public double? CnStop4 { get; set; } | |||||
| // --- LORAS --- | |||||
| /// <summary> | |||||
| /// Utiliser les LoRAs par défaut du modèle. | |||||
| /// Défaut = true. | |||||
| /// </summary> | |||||
| public bool UseDefaultLoras { get; set; } = false; | |||||
| /// <summary> | |||||
| /// URLs de LoRAs personnalisés (séparés par des virgules). | |||||
| /// </summary> | |||||
| public string? LorasCustomUrls { get; set; } | |||||
| /// <summary> | |||||
| /// Poids d’influence des LoRAs. | |||||
| /// Défaut = 1.0. | |||||
| /// </summary> | |||||
| public double? LorasWeight { get; set; } = 0.0; | |||||
| // --- INPAINTING --- | |||||
| /// <summary> | |||||
| /// Image d’entrée pour l’inpainting (corriger une zone). | |||||
| /// </summary> | |||||
| public string? InpaintInputImage { get; set; } | |||||
| /// <summary> | |||||
| /// Masque de l’inpainting (zones à modifier en noir). | |||||
| /// </summary> | |||||
| public string? InpaintInputMask { get; set; } | |||||
| // --- OUTPAINTING --- | |||||
| /// <summary> | |||||
| /// Mode d’outpainting (agrandir une image au-delà des bords). | |||||
| /// Exemple : "left,right,top". | |||||
| /// </summary> | |||||
| public string? OutpaintSelections { get; set; } | |||||
| /// <summary> | |||||
| /// Distance d’extension en haut (en pixels). | |||||
| /// Défaut = 0. | |||||
| /// </summary> | |||||
| public int OutpaintDistanceTop { get; set; } = 0; | |||||
| public int OutpaintDistanceLeft { get; set; } = 0; | |||||
| public int OutpaintDistanceRight { get; set; } = 0; | |||||
| public int OutpaintDistanceBottom { get; set; } = 0; | |||||
| // --- UPSCALE / VARIATIONS --- | |||||
| /// <summary> | |||||
| /// Méthode appliquée : "Upscale", "Variation", "Outpaint". | |||||
| /// </summary> | |||||
| public string? UovMethod { get; set; } | |||||
| /// <summary> | |||||
| /// Image d’entrée pour upscale ou variation. | |||||
| /// </summary> | |||||
| public string? UovInputImage { get; set; } | |||||
| } | |||||
| } |
| using System.Text; | |||||
| using System.Text.Json; | |||||
| using ToolsServices; | |||||
| namespace Services.Fooocus | |||||
| { | |||||
| public static class FooocusService | |||||
| { | |||||
| #region Méthodes publiques | |||||
| #region Gestion des paramètres | |||||
| public static (string, string, string) LoadParametresGenerateImg() | |||||
| { | |||||
| LoggerService.LogInfo("FooocusService.LoadParametresGenerateImg"); | |||||
| string url = ""; | |||||
| string endpointv1 = ""; | |||||
| string endpointv2 = ""; | |||||
| try | |||||
| { | |||||
| if (File.Exists(FichiersInternesService.ParamsGenerateImg)) | |||||
| { | |||||
| string[] lignes = File.ReadAllLines(FichiersInternesService.ParamsGenerateImg); | |||||
| if (lignes.Length > 0) | |||||
| url = lignes[0]; | |||||
| if (lignes.Length > 1) | |||||
| endpointv1 = lignes[1]; | |||||
| if (lignes.Length > 2) | |||||
| endpointv2 = lignes[2]; | |||||
| } | |||||
| return (url, endpointv1, endpointv2); | |||||
| } | |||||
| catch | |||||
| { | |||||
| return ("", "", ""); | |||||
| } | |||||
| } | |||||
| public static bool SaveParametresGenerateImg(string url, string endpointv1, string endpointv2) | |||||
| { | |||||
| LoggerService.LogInfo("FooocusService.SaveParametresGenerateImg"); | |||||
| try | |||||
| { | |||||
| StringBuilder sb = new(); | |||||
| sb.AppendLine(url); | |||||
| sb.AppendLine(endpointv1); | |||||
| sb.AppendLine(endpointv2); | |||||
| File.WriteAllText(FichiersInternesService.ParamsGenerateImg, sb.ToString()); | |||||
| return true; | |||||
| } | |||||
| catch | |||||
| { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| public static FooocusRequest_Text_to_Image LoadParametresFooocus() | |||||
| { | |||||
| LoggerService.LogInfo("FooocusService.LoadParametresFooocus"); | |||||
| FooocusRequest_Text_to_Image param = new(); | |||||
| try | |||||
| { | |||||
| if (File.Exists(FichiersInternesService.ParamsFooocus)) | |||||
| { | |||||
| string[] lignes = File.ReadAllLines(FichiersInternesService.ParamsFooocus); | |||||
| if (lignes.Length > 0) | |||||
| param.negative_prompt = lignes[0]; | |||||
| if (lignes.Length > 1) | |||||
| param.guidance_scale = double.Parse(lignes[1]); | |||||
| if (lignes.Length > 2) | |||||
| param.performance_selection = lignes[2]; | |||||
| if (lignes.Length > 3) | |||||
| param.sharpness = double.Parse(lignes[3]); | |||||
| if (lignes.Length > 4) | |||||
| param.style_selections = lignes[4].Split(',', StringSplitOptions.RemoveEmptyEntries) // coupe sur les virgules | |||||
| .Select(s => s.Trim()) // enlève les espaces inutiles | |||||
| .ToArray(); | |||||
| } | |||||
| return param; | |||||
| } | |||||
| catch | |||||
| { | |||||
| return param; | |||||
| } | |||||
| } | |||||
| public static bool SaveParametresFooocus(FooocusRequest_Text_to_Image param, List<string> ListeSelectedsStylesFooocus) | |||||
| { | |||||
| LoggerService.LogInfo("FooocusService.SaveParametresFooocus"); | |||||
| try | |||||
| { | |||||
| StringBuilder sb = new(); | |||||
| sb.AppendLine(param.negative_prompt); | |||||
| sb.AppendLine(param.guidance_scale.ToString()); | |||||
| sb.AppendLine(param.performance_selection); | |||||
| sb.AppendLine(param.sharpness.ToString()); | |||||
| sb.AppendLine(string.Join(", ", ListeSelectedsStylesFooocus)); | |||||
| File.WriteAllText(FichiersInternesService.ParamsFooocus, sb.ToString()); | |||||
| return true; | |||||
| } | |||||
| catch | |||||
| { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Interactions | |||||
| public static async Task<bool> IsFooocusActif() | |||||
| { | |||||
| LoggerService.LogInfo($"FooocusService.IsFooocusActif"); | |||||
| try | |||||
| { | |||||
| (string baseUrl, _, _) = LoadParametresGenerateImg(); | |||||
| if (baseUrl == "") | |||||
| { | |||||
| var msg = "Erreur de chargement des paramètres Fooocus."; | |||||
| LoggerService.LogWarning(msg); | |||||
| return false; | |||||
| } | |||||
| var url = $"{baseUrl}"; | |||||
| using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(360) }; | |||||
| client.Timeout = TimeSpan.FromSeconds(30); | |||||
| var response = await client.GetAsync(url); | |||||
| LoggerService.LogDebug($"FooocusService.IsFooocusActif : {response.IsSuccessStatusCode}"); | |||||
| return response.IsSuccessStatusCode; | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"FooocusService.IsFooocusActif : False --> {ex.Message}"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| public static async Task<List<string>> GetFooocusStylesAsync() | |||||
| { | |||||
| LoggerService.LogInfo("FooocusService.GetFooocusStylesAsync"); | |||||
| (string baseUrl, _, _) = LoadParametresGenerateImg(); | |||||
| var url = $"{baseUrl}/v1/engines/styles"; | |||||
| if (url == "") | |||||
| { | |||||
| LoggerService.LogWarning("FooocusService.GetFooocusStylesAsync : URL de l'API de génération d'image non configurée."); | |||||
| var lst = new List<string>(); | |||||
| lst.Add("URL de l'API de génération d'image non configurée."); | |||||
| return lst; | |||||
| } | |||||
| using var client = new HttpClient(); | |||||
| var response = await client.GetAsync(url); | |||||
| response.EnsureSuccessStatusCode(); | |||||
| var json = await response.Content.ReadAsStringAsync(); | |||||
| // Le JSON est du type ["Fooocus V2","Random Style",...] | |||||
| var styles = JsonSerializer.Deserialize<List<string>>(json); | |||||
| return styles ?? new List<string>(); | |||||
| } | |||||
| public static async Task<string> GenerateTextToImageWithIP(string promptFinal, List<string> imagesFullFilename) | |||||
| { | |||||
| LoggerService.LogInfo("FooocusService.GenerateTextToImageWithIP"); | |||||
| (string baseUrl, _, string endpoint) = LoadParametresGenerateImg(); | |||||
| //endpoint = "/v2/generation/text-to-image-with-ip"; | |||||
| var images = new List<object>(); | |||||
| double poidsTotal = 1.9; | |||||
| foreach (var img in imagesFullFilename) | |||||
| { | |||||
| poidsTotal = poidsTotal - 0.1; | |||||
| if (poidsTotal < 0.2) | |||||
| poidsTotal = 0.2; | |||||
| var uneImg = new | |||||
| { | |||||
| cn_img = Convert.ToBase64String(File.ReadAllBytes(img)), | |||||
| cn_stop = 0.5, | |||||
| cn_weight = poidsTotal, | |||||
| cn_type = "ImagePrompt" | |||||
| }; | |||||
| images.Add(uneImg); | |||||
| } | |||||
| var requestBody = new | |||||
| { | |||||
| prompt = promptFinal, | |||||
| image_prompts = images.ToArray() | |||||
| }; | |||||
| var url = $"{baseUrl}{endpoint}"; | |||||
| using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(360) }; | |||||
| var json = JsonSerializer.Serialize(requestBody); | |||||
| var content = new StringContent(json, Encoding.UTF8, "application/json"); | |||||
| // Envoi de la requête POST | |||||
| var response = await client.PostAsync($"{url}", content); | |||||
| response.EnsureSuccessStatusCode(); | |||||
| // Lecture de la réponse JSON | |||||
| var responseJson = await response.Content.ReadAsStringAsync(); | |||||
| // Désérialisation | |||||
| using var doc = JsonDocument.Parse(responseJson); | |||||
| var imageUrl = doc.RootElement[0].GetProperty("url").GetString(); | |||||
| if (imageUrl == null) | |||||
| { | |||||
| return ""; | |||||
| } | |||||
| imageUrl = imageUrl.Replace("http://127.0.0.1:8888", baseUrl); | |||||
| return $"{imageUrl}"; | |||||
| } | |||||
| public static async Task<string> GenerateImageWithFooocus(FooocusRequest_Text_to_Image requestBody) | |||||
| { | |||||
| LoggerService.LogInfo("FooocusService.GenerateImageWithFooocus(requestBody)"); | |||||
| try | |||||
| { | |||||
| (string baseUrl, string endpoint, _) = LoadParametresGenerateImg(); | |||||
| var url = $"{baseUrl}{endpoint}"; | |||||
| if (url == "") | |||||
| { | |||||
| LoggerService.LogWarning("FooocusService.GenerateImageWithFooocus : URL de l'API de génération d'image non configurée."); | |||||
| return "URL de l'API de génération d'image non configurée."; | |||||
| } | |||||
| using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(360) }; | |||||
| var json = JsonSerializer.Serialize(requestBody); | |||||
| var content = new StringContent(json, Encoding.UTF8, "application/json"); | |||||
| // Envoi de la requête POST | |||||
| var response = await client.PostAsync($"{url}", content); | |||||
| response.EnsureSuccessStatusCode(); | |||||
| // Lecture de la réponse JSON | |||||
| var responseJson = await response.Content.ReadAsStringAsync(); | |||||
| // Désérialisation | |||||
| using var doc = JsonDocument.Parse(responseJson); | |||||
| var imageUrl = doc.RootElement[0].GetProperty("url").GetString(); | |||||
| if (imageUrl == null) | |||||
| { | |||||
| return ""; | |||||
| } | |||||
| imageUrl = imageUrl.Replace("http://127.0.0.1:8888", baseUrl); | |||||
| return $"{imageUrl}"; | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors de la génération de l'image : {ex.Message}"); | |||||
| return $"Erreur lors de la génération de l'image : {ex.Message}"; | |||||
| } | |||||
| } | |||||
| public static async Task<string> GenerateImageWithFooocus(string prompt) | |||||
| { | |||||
| LoggerService.LogInfo("FooocusService.GenerateImageWithFooocus(prompt)"); | |||||
| try | |||||
| { | |||||
| // Création du JSON à envoyer | |||||
| var requestBody = new FooocusRequest_Text_to_Image(); | |||||
| if (File.Exists(FichiersInternesService.ParamsFooocus)) | |||||
| { | |||||
| requestBody = LoadParametresFooocus(); | |||||
| } | |||||
| requestBody.prompt = prompt; | |||||
| /* | |||||
| Lora lora1 = new Lora() | |||||
| { | |||||
| enabled = true, | |||||
| model_name = "sd_xl_offset_example-lora_1.0.safetensors", | |||||
| weight = 0.5 | |||||
| }; | |||||
| requestBody.loras.Append(lora1); | |||||
| */ | |||||
| /* | |||||
| var requestBody = new | |||||
| { | |||||
| prompt = prompt,//"un chat steampunk sur un toit", | |||||
| negative_prompt = "blurry, low quality, distorted, watermark, text, extra limbs, cropped", | |||||
| guidance_scale = 9.0, | |||||
| StyleSelections = new string[0], // vide => pas d’expansion Fooocus V2 | |||||
| performance_selection = "Quality", | |||||
| sharpness = 2.0, | |||||
| image_seed = -1, | |||||
| image_number = 1 | |||||
| }; | |||||
| */ | |||||
| return await GenerateImageWithFooocus(requestBody); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors de la génération de l'image : {ex.Message}"); | |||||
| return $"Erreur lors de la génération de l'image : {ex.Message}"; | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #endregion | |||||
| } | |||||
| } |
| <?xml version = "1.0" encoding = "UTF-8" ?> | |||||
| <Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | |||||
| xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | |||||
| xmlns:local="clr-namespace:MaUI" | |||||
| x:Class="MaUI.App"> | |||||
| <Application.Resources> | |||||
| <ResourceDictionary> | |||||
| <ResourceDictionary.MergedDictionaries> | |||||
| <ResourceDictionary Source="Resources/Styles/Colors.xaml" /> | |||||
| <ResourceDictionary Source="Resources/Styles/Styles.xaml" /> | |||||
| </ResourceDictionary.MergedDictionaries> | |||||
| </ResourceDictionary> | |||||
| </Application.Resources> | |||||
| </Application> |
| namespace MaUI; | |||||
| public partial class App : Application | |||||
| { | |||||
| public App() | |||||
| { | |||||
| InitializeComponent(); | |||||
| MainPage = new AppShell(); | |||||
| } | |||||
| } |
| <?xml version="1.0" encoding="UTF-8" ?> | |||||
| <Shell | |||||
| x:Class="MaUI.AppShell" | |||||
| xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | |||||
| xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | |||||
| xmlns:local="clr-namespace:MaUI" | |||||
| Shell.FlyoutBehavior="Disabled" | |||||
| Title="MaUI"> | |||||
| <ShellContent | |||||
| Title="Home" | |||||
| ContentTemplate="{DataTemplate local:MainPage}" | |||||
| Route="MainPage" /> | |||||
| </Shell> |
| namespace MaUI; | |||||
| public partial class AppShell : Shell | |||||
| { | |||||
| public AppShell() | |||||
| { | |||||
| InitializeComponent(); | |||||
| } | |||||
| } |
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | |||||
| <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks> | |||||
| <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0-windows10.0.19041.0</TargetFrameworks> | |||||
| <!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET --> | |||||
| <!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> --> | |||||
| <!-- Note for MacCatalyst: | |||||
| The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64. | |||||
| When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>. | |||||
| The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated; | |||||
| either BOTH runtimes must be indicated or ONLY macatalyst-x64. --> | |||||
| <!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> --> | |||||
| <OutputType>Exe</OutputType> | |||||
| <RootNamespace>MaUI</RootNamespace> | |||||
| <UseMaui>true</UseMaui> | |||||
| <SingleProject>true</SingleProject> | |||||
| <ImplicitUsings>enable</ImplicitUsings> | |||||
| <Nullable>enable</Nullable> | |||||
| <!-- Display name --> | |||||
| <ApplicationTitle>MaUI</ApplicationTitle> | |||||
| <!-- App Identifier --> | |||||
| <ApplicationId>com.companyname.maui</ApplicationId> | |||||
| <!-- Versions --> | |||||
| <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> | |||||
| <ApplicationVersion>1</ApplicationVersion> | |||||
| <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion> | |||||
| <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion> | |||||
| <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion> | |||||
| <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion> | |||||
| <TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion> | |||||
| <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <!-- App Icon --> | |||||
| <MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" /> | |||||
| <!-- Splash Screen --> | |||||
| <MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" /> | |||||
| <!-- Images --> | |||||
| <MauiImage Include="Resources\Images\*" /> | |||||
| <MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" /> | |||||
| <!-- Custom Fonts --> | |||||
| <MauiFont Include="Resources\Fonts\*" /> | |||||
| <!-- Raw Assets (also remove the "Resources\Raw" prefix) --> | |||||
| <MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" /> | |||||
| <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" /> | |||||
| <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.1" /> | |||||
| <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.9.0" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <Compile Update="Pages\ChatRoom\ChatRoom_LLM.xaml.cs"> | |||||
| <DependentUpon>ChatRoom_LLM.xaml</DependentUpon> | |||||
| </Compile> | |||||
| <Compile Update="Pages\Mails\ListeEmails.xaml.cs"> | |||||
| <DependentUpon>ListeEmails.xaml</DependentUpon> | |||||
| </Compile> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <MauiXaml Update="Pages\ChatRoom\ChatRoom_LLM.xaml"> | |||||
| <Generator>MSBuild:Compile</Generator> | |||||
| </MauiXaml> | |||||
| <MauiXaml Update="Pages\Mails\ListeEmails.xaml"> | |||||
| <Generator>MSBuild:Compile</Generator> | |||||
| </MauiXaml> | |||||
| </ItemGroup> | |||||
| </Project> |
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||||
| <PropertyGroup> | |||||
| <IsFirstTimeProjectOpen>False</IsFirstTimeProjectOpen> | |||||
| <ActiveDebugFramework>net8.0-windows10.0.19041.0</ActiveDebugFramework> | |||||
| <ActiveDebugProfile>Windows Machine</ActiveDebugProfile> | |||||
| </PropertyGroup> | |||||
| </Project> |
| <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | |||||
| xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | |||||
| x:Class="MaUI.MainPage"> | |||||
| <ScrollView> | |||||
| <StackLayout Padding="10"> | |||||
| <Label Text="MainMenu" FontSize="20"/> | |||||
| <Button Text="Liste Emails" Clicked="OnListeEmails_Clicked" /> | |||||
| <Button Text="ChatRoom" Clicked="OnChatRoom_Clicked" /> | |||||
| <Button Text="Gestion LOGS" Clicked="OnLOGS_Liste_Clicked" /> | |||||
| </StackLayout> | |||||
| </ScrollView> | |||||
| </ContentPage> |
| namespace MaUI | |||||
| { | |||||
| public partial class MainPage : ContentPage | |||||
| { | |||||
| public MainPage() | |||||
| { | |||||
| InitializeComponent(); | |||||
| //VerifConnexion(); | |||||
| } | |||||
| #region Méthodes pour la vérification de connexion | |||||
| private void VerifConnexion() | |||||
| { | |||||
| if (!UserConntected.IsConnected) | |||||
| Navigation.PushAsync(new AuthLogin()); | |||||
| } | |||||
| protected override void OnAppearing() | |||||
| { | |||||
| //VerifConnexion(); | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes du Menu | |||||
| private async void OnListeEmails_Clicked(object sender, EventArgs e) | |||||
| { | |||||
| await Navigation.PushAsync(new ListeEmails()); | |||||
| } | |||||
| private async void OnChatRoom_Clicked(object sender, EventArgs e) | |||||
| { | |||||
| await Navigation.PushAsync(new ChatRoomPage()); | |||||
| } | |||||
| private async void OnLOGS_Liste_Clicked(object sender, EventArgs e) | |||||
| { | |||||
| await Navigation.PushAsync(new LOGS_Liste_Page()); | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| } | |||||
| using Microsoft.Extensions.Logging; | |||||
| namespace MaUI; | |||||
| public static class MauiProgram | |||||
| { | |||||
| public static MauiApp CreateMauiApp() | |||||
| { | |||||
| var builder = MauiApp.CreateBuilder(); | |||||
| builder | |||||
| .UseMauiApp<App>() | |||||
| .ConfigureFonts(fonts => | |||||
| { | |||||
| fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); | |||||
| fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); | |||||
| }); | |||||
| #if DEBUG | |||||
| builder.Logging.AddDebug(); | |||||
| #endif | |||||
| return builder.Build(); | |||||
| } | |||||
| } |
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace MaUI | |||||
| { | |||||
| public partial class LOGS_DTO | |||||
| { | |||||
| public Guid ID { get; set; } | |||||
| public string NIVEAU { get; set; } =""; | |||||
| public DateTime HORODATAGE { get; set; } | |||||
| public string MESSAGE { get; set; } =""; | |||||
| //Les propriétés de navigation | |||||
| } | |||||
| } | |||||
| <?xml version="1.0" encoding="utf-8" ?> | |||||
| <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | |||||
| xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | |||||
| x:Class="MaUI.AuthLogin" | |||||
| Title="Login"> | |||||
| <ScrollView> | |||||
| <StackLayout Padding="10" Spacing="10"> | |||||
| <!-- LOGIN --> | |||||
| <Grid ColumnDefinitions="150, *" Padding="0,5"> | |||||
| <Label Text="Login* :" FontAttributes="Bold" VerticalOptions="Center" Grid.Row="0" Grid.Column="0"/> | |||||
| <Entry Text="{Binding SelectedItem.Username, Mode=TwoWay}" Placeholder="Saisissez votre login" Grid.Row="0" Grid.Column="1"/> | |||||
| </Grid> | |||||
| <!-- PASSWORD --> | |||||
| <Grid ColumnDefinitions="150, *" Padding="0,5"> | |||||
| <Label Text="Password* :" FontAttributes="Bold" VerticalOptions="Center" Grid.Row="1" Grid.Column="0"/> | |||||
| <Entry x:Name="PasswordEntry" Text="{Binding SelectedItem.Password, Mode=TwoWay}" IsPassword="True" Placeholder="Saisissez votre mot de passe" Grid.Row="1" Grid.Column="1"/> | |||||
| </Grid> | |||||
| <Grid ColumnDefinitions="200, 200" Padding="0,5"> | |||||
| <Button Text="Annuler" | |||||
| Command="{Binding AnnulerCommand}" | |||||
| BackgroundColor="Gray" | |||||
| TextColor="White" | |||||
| CornerRadius="10" | |||||
| HeightRequest="50" | |||||
| WidthRequest="120" | |||||
| Grid.Row="6" Grid.Column="0" | |||||
| Margin="0,0,0,0"/> | |||||
| <Button Text="Valider" | |||||
| Command="{Binding ValiderCommand}" | |||||
| BackgroundColor="Green" | |||||
| TextColor="White" | |||||
| CornerRadius="10" | |||||
| HeightRequest="50" | |||||
| WidthRequest="120" | |||||
| Grid.Row="6" Grid.Column="1" | |||||
| Margin="0,0,0,0"/> | |||||
| </Grid> | |||||
| </StackLayout> | |||||
| </ScrollView> | |||||
| </ContentPage> |
| namespace MaUI; | |||||
| public partial class AuthLogin : ContentPage | |||||
| { | |||||
| AuthLogin_VM _VM; | |||||
| public AuthLogin() | |||||
| { | |||||
| InitializeComponent(); | |||||
| _VM = new AuthLogin_VM(); | |||||
| BindingContext = _VM; | |||||
| } | |||||
| } |
| <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | |||||
| xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | |||||
| xmlns:viewmodels="clr-namespace:MaUI" | |||||
| x:Class="MaUI.ChatRoomPage" | |||||
| BackgroundColor="#FFF0F0F0"> | |||||
| <ContentPage.BindingContext> | |||||
| <viewmodels:ChatRoom_VM /> | |||||
| </ContentPage.BindingContext> | |||||
| <Grid RowDefinitions="Auto,*"> | |||||
| <!-- Barre supérieure --> | |||||
| <HorizontalStackLayout Padding="10" Spacing="20"> | |||||
| <Button Text="🗑" WidthRequest="74" Command="{Binding DeleteConversationCommand}" /> | |||||
| <Button Text="Nouvelle conversation" WidthRequest="138" Command="{Binding NewConversationCommand}" /> | |||||
| <Picker ItemsSource="{Binding InstalledModels}" | |||||
| ItemDisplayBinding="{Binding Name}" | |||||
| SelectedItem="{Binding SelectedModel}" WidthRequest="300" /> | |||||
| </HorizontalStackLayout> | |||||
| <!-- Contenu principal --> | |||||
| <Grid Row="1" ColumnDefinitions="200,*" Padding="5"> | |||||
| <!-- Colonne gauche --> | |||||
| <VerticalStackLayout> | |||||
| <CollectionView ItemsSource="{Binding Conversations}" SelectionMode="Single" | |||||
| SelectedItem="{Binding SelectedConversation}"> | |||||
| <CollectionView.ItemTemplate> | |||||
| <DataTemplate> | |||||
| <Label Text="{Binding Title}" /> | |||||
| </DataTemplate> | |||||
| </CollectionView.ItemTemplate> | |||||
| </CollectionView> | |||||
| <Button Text="Ajout de documents" Command="{Binding AddDocumentsCommand}" /> | |||||
| <CollectionView ItemsSource="{Binding SelectedDocumentsListe}" /> | |||||
| <Button Text="🗑" Command="{Binding ClearDocumentsCommand}" /> | |||||
| </VerticalStackLayout> | |||||
| <!-- Colonne droite : chat --> | |||||
| <Grid Grid.Column="1" RowDefinitions="*,Auto"> | |||||
| <!-- Historique --> | |||||
| <ScrollView> | |||||
| <VerticalStackLayout BindableLayout.ItemsSource="{Binding CurrentMessages}"> | |||||
| <BindableLayout.ItemTemplate> | |||||
| <DataTemplate> | |||||
| <Frame Padding="10" CornerRadius="10" Margin="5" | |||||
| BackgroundColor="{Binding Role, Converter={StaticResource RoleToColorConverter}}"> | |||||
| <Label Text="{Binding Content}" LineBreakMode="WordWrap" /> | |||||
| </Frame> | |||||
| </DataTemplate> | |||||
| </BindableLayout.ItemTemplate> | |||||
| </VerticalStackLayout> | |||||
| </ScrollView> | |||||
| <!-- Saisie --> | |||||
| <VerticalStackLayout Grid.Row="1" Padding="5"> | |||||
| <Editor Text="{Binding NewMessage}" AutoSize="TextChanges" HeightRequest="70" /> | |||||
| <HorizontalStackLayout Spacing="10"> | |||||
| <CheckBox IsChecked="{Binding IsApiExterne}" /> | |||||
| <Label Text="API Externe" VerticalOptions="Center" /> | |||||
| </HorizontalStackLayout> | |||||
| <HorizontalStackLayout Spacing="10"> | |||||
| <CheckBox IsChecked="{Binding IsRAG}" /> | |||||
| <Label Text="RAG ?" VerticalOptions="Center" /> | |||||
| </HorizontalStackLayout> | |||||
| <HorizontalStackLayout Spacing="10"> | |||||
| <CheckBox IsChecked="{Binding IsGenerateImg}" /> | |||||
| <Label Text="Générer une image ?" VerticalOptions="Center" /> | |||||
| </HorizontalStackLayout> | |||||
| <HorizontalStackLayout Spacing="10"> | |||||
| <CheckBox IsChecked="{Binding IsWithAssistant}" /> | |||||
| <Label Text="Assistant IA ?" VerticalOptions="Center" /> | |||||
| </HorizontalStackLayout> | |||||
| <Button Text="Envoyer" Command="{Binding SendMessageCommand}" /> | |||||
| </VerticalStackLayout> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </ContentPage> |
| namespace MaUI; | |||||
| public partial class ChatRoomPage : ContentPage | |||||
| { | |||||
| ChatRoom_VM _VM; | |||||
| public ChatRoomPage() | |||||
| { | |||||
| InitializeComponent(); | |||||
| _VM = new ChatRoom_VM(); | |||||
| BindingContext = _VM; | |||||
| } | |||||
| } | |||||
| <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | |||||
| xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | |||||
| x:Class="MaUI.ListeEmails" | |||||
| Title="Liste LOGS"> | |||||
| <ScrollView> | |||||
| <StackLayout Padding="10" Spacing="10"> | |||||
| <!-- Liste des Items --> | |||||
| <ListView | |||||
| ItemsSource="{Binding Liste_Items}" | |||||
| SelectedItem="{Binding SelectedItem}"> | |||||
| <ListView.ItemTemplate> | |||||
| <DataTemplate> | |||||
| <ViewCell> | |||||
| <Grid Padding="10" ColumnDefinitions="*"> | |||||
| <Label Text="{Binding NIVEAU}" FontSize="16" VerticalOptions="Center" HorizontalOptions="Start" /> | |||||
| </Grid> | |||||
| </ViewCell> | |||||
| </DataTemplate> | |||||
| </ListView.ItemTemplate> | |||||
| </ListView> | |||||
| <!-- Boutons pour les opérations --> | |||||
| <StackLayout Orientation="Horizontal" Spacing="10"> | |||||
| <Button Text="Créer" WidthRequest="120" Command="{Binding CreateCommand}" Background="Blue" /> | |||||
| <Button Text="Modifier" WidthRequest="120" Command="{Binding EditCommand}" Background="Green" IsEnabled="{Binding SelectedItem}" /> | |||||
| <Button Text="Supprimer" WidthRequest="120" Command="{Binding DeleteCommand}" Background="Red" IsEnabled="{Binding SelectedItem}" /> | |||||
| </StackLayout> | |||||
| </StackLayout> | |||||
| </ScrollView> | |||||
| </ContentPage> |
| namespace MaUI; | |||||
| public partial class ListeEmails : ContentPage | |||||
| { | |||||
| ListeEmails_VM _VM; | |||||
| public ListeEmails() | |||||
| { | |||||
| InitializeComponent(); | |||||
| _VM = new ListeEmails_VM(); | |||||
| BindingContext = _VM; | |||||
| } | |||||
| } | |||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | |||||
| <application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application> | |||||
| <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | |||||
| <uses-permission android:name="android.permission.INTERNET" /> | |||||
| </manifest> |
| using Android.App; | |||||
| using Android.Content.PM; | |||||
| using Android.OS; | |||||
| namespace MaUI; | |||||
| [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] | |||||
| public class MainActivity : MauiAppCompatActivity | |||||
| { | |||||
| } |
| using Android.App; | |||||
| using Android.Runtime; | |||||
| namespace MaUI; | |||||
| [Application] | |||||
| public class MainApplication : MauiApplication | |||||
| { | |||||
| public MainApplication(IntPtr handle, JniHandleOwnership ownership) | |||||
| : base(handle, ownership) | |||||
| { | |||||
| } | |||||
| protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); | |||||
| } |
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <resources> | |||||
| <color name="colorPrimary">#512BD4</color> | |||||
| <color name="colorPrimaryDark">#2B0B98</color> | |||||
| <color name="colorAccent">#2B0B98</color> | |||||
| </resources> |
| using Foundation; | |||||
| namespace MaUI; | |||||
| [Register("AppDelegate")] | |||||
| public class AppDelegate : MauiUIApplicationDelegate | |||||
| { | |||||
| protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); | |||||
| } |
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |||||
| <plist version="1.0"> | |||||
| <!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.--> | |||||
| <dict> | |||||
| <!-- App Sandbox must be enabled to distribute a MacCatalyst app through the Mac App Store. --> | |||||
| <key>com.apple.security.app-sandbox</key> | |||||
| <true/> | |||||
| <!-- When App Sandbox is enabled, this value is required to open outgoing network connections. --> | |||||
| <key>com.apple.security.network.client</key> | |||||
| <true/> | |||||
| </dict> | |||||
| </plist> | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |||||
| <plist version="1.0"> | |||||
| <dict> | |||||
| <!-- The Mac App Store requires you specify if the app uses encryption. --> | |||||
| <!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption --> | |||||
| <!-- <key>ITSAppUsesNonExemptEncryption</key> --> | |||||
| <!-- Please indicate <true/> or <false/> here. --> | |||||
| <!-- Specify the category for your app here. --> | |||||
| <!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype --> | |||||
| <!-- <key>LSApplicationCategoryType</key> --> | |||||
| <!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> --> | |||||
| <key>UIDeviceFamily</key> | |||||
| <array> | |||||
| <integer>2</integer> | |||||
| </array> | |||||
| <key>UIRequiredDeviceCapabilities</key> | |||||
| <array> | |||||
| <string>arm64</string> | |||||
| </array> | |||||
| <key>UISupportedInterfaceOrientations</key> | |||||
| <array> | |||||
| <string>UIInterfaceOrientationPortrait</string> | |||||
| <string>UIInterfaceOrientationLandscapeLeft</string> | |||||
| <string>UIInterfaceOrientationLandscapeRight</string> | |||||
| </array> | |||||
| <key>UISupportedInterfaceOrientations~ipad</key> | |||||
| <array> | |||||
| <string>UIInterfaceOrientationPortrait</string> | |||||
| <string>UIInterfaceOrientationPortraitUpsideDown</string> | |||||
| <string>UIInterfaceOrientationLandscapeLeft</string> | |||||
| <string>UIInterfaceOrientationLandscapeRight</string> | |||||
| </array> | |||||
| <key>XSAppIconAssets</key> | |||||
| <string>Assets.xcassets/appicon.appiconset</string> | |||||
| </dict> | |||||
| </plist> |
| using ObjCRuntime; | |||||
| using UIKit; | |||||
| namespace MaUI; | |||||
| public class Program | |||||
| { | |||||
| // This is the main entry point of the application. | |||||
| static void Main(string[] args) | |||||
| { | |||||
| // if you want to use a different Application Delegate class from "AppDelegate" | |||||
| // you can specify it here. | |||||
| UIApplication.Main(args, null, typeof(AppDelegate)); | |||||
| } | |||||
| } |
| using System; | |||||
| using Microsoft.Maui; | |||||
| using Microsoft.Maui.Hosting; | |||||
| namespace MaUI; | |||||
| class Program : MauiApplication | |||||
| { | |||||
| protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); | |||||
| static void Main(string[] args) | |||||
| { | |||||
| var app = new Program(); | |||||
| app.Run(args); | |||||
| } | |||||
| } |
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <manifest package="maui-application-id-placeholder" version="0.0.0" api-version="8" xmlns="http://tizen.org/ns/packages"> | |||||
| <profile name="common" /> | |||||
| <ui-application appid="maui-application-id-placeholder" exec="MaUI.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single"> | |||||
| <label>maui-application-title-placeholder</label> | |||||
| <icon>maui-appicon-placeholder</icon> | |||||
| <metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" /> | |||||
| </ui-application> | |||||
| <shortcut-list /> | |||||
| <privileges> | |||||
| <privilege>http://tizen.org/privilege/internet</privilege> | |||||
| </privileges> | |||||
| <dependencies /> | |||||
| <provides-appdefined-privileges /> | |||||
| </manifest> |
| <maui:MauiWinUIApplication | |||||
| x:Class="MaUI.WinUI.App" | |||||
| xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |||||
| xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |||||
| xmlns:maui="using:Microsoft.Maui" | |||||
| xmlns:local="using:MaUI.WinUI"> | |||||
| </maui:MauiWinUIApplication> |
| using Microsoft.UI.Xaml; | |||||
| // To learn more about WinUI, the WinUI project structure, | |||||
| // and more about our project templates, see: http://aka.ms/winui-project-info. | |||||
| namespace MaUI.WinUI; | |||||
| /// <summary> | |||||
| /// Provides application-specific behavior to supplement the default Application class. | |||||
| /// </summary> | |||||
| public partial class App : MauiWinUIApplication | |||||
| { | |||||
| /// <summary> | |||||
| /// Initializes the singleton application object. This is the first line of authored code | |||||
| /// executed, and as such is the logical equivalent of main() or WinMain(). | |||||
| /// </summary> | |||||
| public App() | |||||
| { | |||||
| this.InitializeComponent(); | |||||
| } | |||||
| protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); | |||||
| } | |||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <Package | |||||
| xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" | |||||
| xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" | |||||
| xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" | |||||
| xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" | |||||
| IgnorableNamespaces="uap rescap"> | |||||
| <Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" /> | |||||
| <mp:PhoneIdentity PhoneProductId="09977730-76D0-4D6E-9F2F-051D59EA6ED0" PhonePublisherId="00000000-0000-0000-0000-000000000000"/> | |||||
| <Properties> | |||||
| <DisplayName>$placeholder$</DisplayName> | |||||
| <PublisherDisplayName>User Name</PublisherDisplayName> | |||||
| <Logo>$placeholder$.png</Logo> | |||||
| </Properties> | |||||
| <Dependencies> | |||||
| <TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" /> | |||||
| <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" /> | |||||
| </Dependencies> | |||||
| <Resources> | |||||
| <Resource Language="x-generate" /> | |||||
| </Resources> | |||||
| <Applications> | |||||
| <Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$"> | |||||
| <uap:VisualElements | |||||
| DisplayName="$placeholder$" | |||||
| Description="$placeholder$" | |||||
| Square150x150Logo="$placeholder$.png" | |||||
| Square44x44Logo="$placeholder$.png" | |||||
| BackgroundColor="transparent"> | |||||
| <uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" /> | |||||
| <uap:SplashScreen Image="$placeholder$.png" /> | |||||
| </uap:VisualElements> | |||||
| </Application> | |||||
| </Applications> | |||||
| <Capabilities> | |||||
| <rescap:Capability Name="runFullTrust" /> | |||||
| </Capabilities> | |||||
| </Package> |
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> | |||||
| <assemblyIdentity version="1.0.0.0" name="MaUI.WinUI.app"/> | |||||
| <application xmlns="urn:schemas-microsoft-com:asm.v3"> | |||||
| <windowsSettings> | |||||
| <!-- The combination of below two tags have the following effect: | |||||
| 1) Per-Monitor for >= Windows 10 Anniversary Update | |||||
| 2) System < Windows 10 Anniversary Update | |||||
| --> | |||||
| <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware> | |||||
| <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness> | |||||
| </windowsSettings> | |||||
| </application> | |||||
| </assembly> |
| using Foundation; | |||||
| namespace MaUI; | |||||
| [Register("AppDelegate")] | |||||
| public class AppDelegate : MauiUIApplicationDelegate | |||||
| { | |||||
| protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); | |||||
| } |
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |||||
| <plist version="1.0"> | |||||
| <dict> | |||||
| <key>LSRequiresIPhoneOS</key> | |||||
| <true/> | |||||
| <key>UIDeviceFamily</key> | |||||
| <array> | |||||
| <integer>1</integer> | |||||
| <integer>2</integer> | |||||
| </array> | |||||
| <key>UIRequiredDeviceCapabilities</key> | |||||
| <array> | |||||
| <string>arm64</string> | |||||
| </array> | |||||
| <key>UISupportedInterfaceOrientations</key> | |||||
| <array> | |||||
| <string>UIInterfaceOrientationPortrait</string> | |||||
| <string>UIInterfaceOrientationLandscapeLeft</string> | |||||
| <string>UIInterfaceOrientationLandscapeRight</string> | |||||
| </array> | |||||
| <key>UISupportedInterfaceOrientations~ipad</key> | |||||
| <array> | |||||
| <string>UIInterfaceOrientationPortrait</string> | |||||
| <string>UIInterfaceOrientationPortraitUpsideDown</string> | |||||
| <string>UIInterfaceOrientationLandscapeLeft</string> | |||||
| <string>UIInterfaceOrientationLandscapeRight</string> | |||||
| </array> | |||||
| <key>XSAppIconAssets</key> | |||||
| <string>Assets.xcassets/appicon.appiconset</string> | |||||
| </dict> | |||||
| </plist> |
| using ObjCRuntime; | |||||
| using UIKit; | |||||
| namespace MaUI; | |||||
| public class Program | |||||
| { | |||||
| // This is the main entry point of the application. | |||||
| static void Main(string[] args) | |||||
| { | |||||
| // if you want to use a different Application Delegate class from "AppDelegate" | |||||
| // you can specify it here. | |||||
| UIApplication.Main(args, null, typeof(AppDelegate)); | |||||
| } | |||||
| } |
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <!-- | |||||
| This is the minimum required version of the Apple Privacy Manifest for .NET MAUI apps. | |||||
| The contents below are needed because of APIs that are used in the .NET framework and .NET MAUI SDK. | |||||
| You are responsible for adding extra entries as needed for your application. | |||||
| More information: https://aka.ms/maui-privacy-manifest | |||||
| --> | |||||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |||||
| <plist version="1.0"> | |||||
| <dict> | |||||
| <key>NSPrivacyAccessedAPITypes</key> | |||||
| <array> | |||||
| <dict> | |||||
| <key>NSPrivacyAccessedAPIType</key> | |||||
| <string>NSPrivacyAccessedAPICategoryFileTimestamp</string> | |||||
| <key>NSPrivacyAccessedAPITypeReasons</key> | |||||
| <array> | |||||
| <string>C617.1</string> | |||||
| </array> | |||||
| </dict> | |||||
| <dict> | |||||
| <key>NSPrivacyAccessedAPIType</key> | |||||
| <string>NSPrivacyAccessedAPICategorySystemBootTime</string> | |||||
| <key>NSPrivacyAccessedAPITypeReasons</key> | |||||
| <array> | |||||
| <string>35F9.1</string> | |||||
| </array> | |||||
| </dict> | |||||
| <dict> | |||||
| <key>NSPrivacyAccessedAPIType</key> | |||||
| <string>NSPrivacyAccessedAPICategoryDiskSpace</string> | |||||
| <key>NSPrivacyAccessedAPITypeReasons</key> | |||||
| <array> | |||||
| <string>E174.1</string> | |||||
| </array> | |||||
| </dict> | |||||
| <!-- | |||||
| The entry below is only needed when you're using the Preferences API in your app. | |||||
| <dict> | |||||
| <key>NSPrivacyAccessedAPIType</key> | |||||
| <string>NSPrivacyAccessedAPICategoryUserDefaults</string> | |||||
| <key>NSPrivacyAccessedAPITypeReasons</key> | |||||
| <array> | |||||
| <string>CA92.1</string> | |||||
| </array> | |||||
| </dict> --> | |||||
| </array> | |||||
| </dict> | |||||
| </plist> |
| { | |||||
| "profiles": { | |||||
| "Windows Machine": { | |||||
| "commandName": "MsixPackage", | |||||
| "nativeDebugging": false | |||||
| } | |||||
| } | |||||
| } |
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||||
| <svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg"> | |||||
| <rect x="0" y="0" width="456" height="456" fill="#512BD4" /> | |||||
| </svg> |
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | |||||
| <svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> | |||||
| <path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" /> | |||||
| <path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" /> | |||||
| <path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" /> | |||||
| <path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" /> | |||||
| </svg> |
| Any raw assets you want to be deployed with your application can be placed in | |||||
| this directory (and child directories). Deployment of the asset to your application | |||||
| is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. | |||||
| <MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" /> | |||||
| These files will be deployed with your package and will be accessible using Essentials: | |||||
| async Task LoadMauiAsset() | |||||
| { | |||||
| using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); | |||||
| using var reader = new StreamReader(stream); | |||||
| var contents = reader.ReadToEnd(); | |||||
| } |
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | |||||
| <svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> | |||||
| <path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" /> | |||||
| <path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" /> | |||||
| <path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" /> | |||||
| <path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" /> | |||||
| </svg> |
| <?xml version="1.0" encoding="UTF-8" ?> | |||||
| <?xaml-comp compile="true" ?> | |||||
| <ResourceDictionary | |||||
| xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | |||||
| xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"> | |||||
| <!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml --> | |||||
| <Color x:Key="Primary">#512BD4</Color> | |||||
| <Color x:Key="PrimaryDark">#ac99ea</Color> | |||||
| <Color x:Key="PrimaryDarkText">#242424</Color> | |||||
| <Color x:Key="Secondary">#DFD8F7</Color> | |||||
| <Color x:Key="SecondaryDarkText">#9880e5</Color> | |||||
| <Color x:Key="Tertiary">#2B0B98</Color> | |||||
| <Color x:Key="White">White</Color> | |||||
| <Color x:Key="Black">Black</Color> | |||||
| <Color x:Key="Magenta">#D600AA</Color> | |||||
| <Color x:Key="MidnightBlue">#190649</Color> | |||||
| <Color x:Key="OffBlack">#1f1f1f</Color> | |||||
| <Color x:Key="Gray100">#E1E1E1</Color> | |||||
| <Color x:Key="Gray200">#C8C8C8</Color> | |||||
| <Color x:Key="Gray300">#ACACAC</Color> | |||||
| <Color x:Key="Gray400">#919191</Color> | |||||
| <Color x:Key="Gray500">#6E6E6E</Color> | |||||
| <Color x:Key="Gray600">#404040</Color> | |||||
| <Color x:Key="Gray900">#212121</Color> | |||||
| <Color x:Key="Gray950">#141414</Color> | |||||
| <SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/> | |||||
| <SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/> | |||||
| <SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/> | |||||
| <SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/> | |||||
| <SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/> | |||||
| <SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/> | |||||
| <SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/> | |||||
| <SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/> | |||||
| <SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/> | |||||
| <SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/> | |||||
| <SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/> | |||||
| <SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/> | |||||
| <SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/> | |||||
| </ResourceDictionary> |
| <?xml version="1.0" encoding="UTF-8" ?> | |||||
| <?xaml-comp compile="true" ?> | |||||
| <ResourceDictionary | |||||
| xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | |||||
| xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"> | |||||
| <Style TargetType="ActivityIndicator"> | |||||
| <Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" /> | |||||
| </Style> | |||||
| <Style TargetType="IndicatorView"> | |||||
| <Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/> | |||||
| <Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/> | |||||
| </Style> | |||||
| <Style TargetType="Border"> | |||||
| <Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" /> | |||||
| <Setter Property="StrokeShape" Value="Rectangle"/> | |||||
| <Setter Property="StrokeThickness" Value="1"/> | |||||
| </Style> | |||||
| <Style TargetType="BoxView"> | |||||
| <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" /> | |||||
| </Style> | |||||
| <Style TargetType="Button"> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" /> | |||||
| <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" /> | |||||
| <Setter Property="FontFamily" Value="OpenSansRegular"/> | |||||
| <Setter Property="FontSize" Value="14"/> | |||||
| <Setter Property="BorderWidth" Value="0"/> | |||||
| <Setter Property="CornerRadius" Value="8"/> | |||||
| <Setter Property="Padding" Value="14,10"/> | |||||
| <Setter Property="MinimumHeightRequest" Value="44"/> | |||||
| <Setter Property="MinimumWidthRequest" Value="44"/> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" /> | |||||
| <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| <VisualState x:Name="PointerOver" /> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="CheckBox"> | |||||
| <Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="MinimumHeightRequest" Value="44"/> | |||||
| <Setter Property="MinimumWidthRequest" Value="44"/> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="DatePicker"> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="BackgroundColor" Value="Transparent" /> | |||||
| <Setter Property="FontFamily" Value="OpenSansRegular"/> | |||||
| <Setter Property="FontSize" Value="14"/> | |||||
| <Setter Property="MinimumHeightRequest" Value="44"/> | |||||
| <Setter Property="MinimumWidthRequest" Value="44"/> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="Editor"> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="BackgroundColor" Value="Transparent" /> | |||||
| <Setter Property="FontFamily" Value="OpenSansRegular"/> | |||||
| <Setter Property="FontSize" Value="14" /> | |||||
| <Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" /> | |||||
| <Setter Property="MinimumHeightRequest" Value="44"/> | |||||
| <Setter Property="MinimumWidthRequest" Value="44"/> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="Entry"> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="BackgroundColor" Value="Transparent" /> | |||||
| <Setter Property="FontFamily" Value="OpenSansRegular"/> | |||||
| <Setter Property="FontSize" Value="14" /> | |||||
| <Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" /> | |||||
| <Setter Property="MinimumHeightRequest" Value="44"/> | |||||
| <Setter Property="MinimumWidthRequest" Value="44"/> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="Frame"> | |||||
| <Setter Property="HasShadow" Value="False" /> | |||||
| <Setter Property="BorderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" /> | |||||
| <Setter Property="CornerRadius" Value="8" /> | |||||
| <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" /> | |||||
| </Style> | |||||
| <Style TargetType="ImageButton"> | |||||
| <Setter Property="Opacity" Value="1" /> | |||||
| <Setter Property="BorderColor" Value="Transparent"/> | |||||
| <Setter Property="BorderWidth" Value="0"/> | |||||
| <Setter Property="CornerRadius" Value="0"/> | |||||
| <Setter Property="MinimumHeightRequest" Value="44"/> | |||||
| <Setter Property="MinimumWidthRequest" Value="44"/> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="Opacity" Value="0.5" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| <VisualState x:Name="PointerOver" /> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="Label"> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="BackgroundColor" Value="Transparent" /> | |||||
| <Setter Property="FontFamily" Value="OpenSansRegular" /> | |||||
| <Setter Property="FontSize" Value="14" /> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="Span"> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" /> | |||||
| </Style> | |||||
| <Style TargetType="Label" x:Key="Headline"> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="FontSize" Value="32" /> | |||||
| <Setter Property="HorizontalOptions" Value="Center" /> | |||||
| <Setter Property="HorizontalTextAlignment" Value="Center" /> | |||||
| </Style> | |||||
| <Style TargetType="Label" x:Key="SubHeadline"> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="FontSize" Value="24" /> | |||||
| <Setter Property="HorizontalOptions" Value="Center" /> | |||||
| <Setter Property="HorizontalTextAlignment" Value="Center" /> | |||||
| </Style> | |||||
| <Style TargetType="ListView"> | |||||
| <Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" /> | |||||
| <Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" /> | |||||
| </Style> | |||||
| <Style TargetType="Picker"> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" /> | |||||
| <Setter Property="BackgroundColor" Value="Transparent" /> | |||||
| <Setter Property="FontFamily" Value="OpenSansRegular"/> | |||||
| <Setter Property="FontSize" Value="14"/> | |||||
| <Setter Property="MinimumHeightRequest" Value="44"/> | |||||
| <Setter Property="MinimumWidthRequest" Value="44"/> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| <Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="ProgressBar"> | |||||
| <Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="RadioButton"> | |||||
| <Setter Property="BackgroundColor" Value="Transparent"/> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="FontFamily" Value="OpenSansRegular"/> | |||||
| <Setter Property="FontSize" Value="14"/> | |||||
| <Setter Property="MinimumHeightRequest" Value="44"/> | |||||
| <Setter Property="MinimumWidthRequest" Value="44"/> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="RefreshView"> | |||||
| <Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" /> | |||||
| </Style> | |||||
| <Style TargetType="SearchBar"> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" /> | |||||
| <Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" /> | |||||
| <Setter Property="BackgroundColor" Value="Transparent" /> | |||||
| <Setter Property="FontFamily" Value="OpenSansRegular" /> | |||||
| <Setter Property="FontSize" Value="14" /> | |||||
| <Setter Property="MinimumHeightRequest" Value="44"/> | |||||
| <Setter Property="MinimumWidthRequest" Value="44"/> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| <Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="SearchHandler"> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" /> | |||||
| <Setter Property="BackgroundColor" Value="Transparent" /> | |||||
| <Setter Property="FontFamily" Value="OpenSansRegular" /> | |||||
| <Setter Property="FontSize" Value="14" /> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| <Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="Shadow"> | |||||
| <Setter Property="Radius" Value="15" /> | |||||
| <Setter Property="Opacity" Value="0.5" /> | |||||
| <Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="Offset" Value="10,10" /> | |||||
| </Style> | |||||
| <Style TargetType="Slider"> | |||||
| <Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" /> | |||||
| <Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/> | |||||
| <Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/> | |||||
| <Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="SwipeItem"> | |||||
| <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" /> | |||||
| </Style> | |||||
| <Style TargetType="Switch"> | |||||
| <Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="ThumbColor" Value="{StaticResource White}" /> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| <Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| <VisualState x:Name="On"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" /> | |||||
| <Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| <VisualState x:Name="Off"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="TimePicker"> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="BackgroundColor" Value="Transparent"/> | |||||
| <Setter Property="FontFamily" Value="OpenSansRegular"/> | |||||
| <Setter Property="FontSize" Value="14"/> | |||||
| <Setter Property="MinimumHeightRequest" Value="44"/> | |||||
| <Setter Property="MinimumWidthRequest" Value="44"/> | |||||
| <Setter Property="VisualStateManager.VisualStateGroups"> | |||||
| <VisualStateGroupList> | |||||
| <VisualStateGroup x:Name="CommonStates"> | |||||
| <VisualState x:Name="Normal" /> | |||||
| <VisualState x:Name="Disabled"> | |||||
| <VisualState.Setters> | |||||
| <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" /> | |||||
| </VisualState.Setters> | |||||
| </VisualState> | |||||
| </VisualStateGroup> | |||||
| </VisualStateGroupList> | |||||
| </Setter> | |||||
| </Style> | |||||
| <Style TargetType="Page" ApplyToDerivedTypes="True"> | |||||
| <Setter Property="Padding" Value="0"/> | |||||
| <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" /> | |||||
| </Style> | |||||
| <Style TargetType="Shell" ApplyToDerivedTypes="True"> | |||||
| <Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" /> | |||||
| <Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" /> | |||||
| <Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" /> | |||||
| <Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" /> | |||||
| <Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" /> | |||||
| <Setter Property="Shell.NavBarHasShadow" Value="False" /> | |||||
| <Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" /> | |||||
| <Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" /> | |||||
| </Style> | |||||
| <Style TargetType="NavigationPage"> | |||||
| <Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" /> | |||||
| <Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" /> | |||||
| </Style> | |||||
| <Style TargetType="TabbedPage"> | |||||
| <Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" /> | |||||
| <Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" /> | |||||
| <Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" /> | |||||
| <Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" /> | |||||
| </Style> | |||||
| </ResourceDictionary> |
| using System.IdentityModel.Tokens.Jwt; | |||||
| using System.Net.Http.Headers; | |||||
| using System.Net.Http.Json; | |||||
| using System.Security.Claims; | |||||
| namespace MaUI | |||||
| { | |||||
| public static class UserConntected | |||||
| { | |||||
| public static bool IsConnected { get; private set; } = false; | |||||
| public static string Token { get; private set; } = ""; | |||||
| public static string ID { get; private set; } = ""; | |||||
| public static string FirstName { get; private set; } = ""; | |||||
| public static string LastName { get; private set; } = ""; | |||||
| public static string Email { get; private set; } = ""; | |||||
| public static List<string>? Roles { get; private set; } = new(); | |||||
| public static void SetToken(string jwt) | |||||
| { | |||||
| Token = jwt; | |||||
| var handler = new JwtSecurityTokenHandler(); | |||||
| var decodedToken = handler.ReadJwtToken(jwt); | |||||
| var identity = new ClaimsIdentity(decodedToken.Claims, "jwt"); | |||||
| //FirstName = decodedToken.Claims.FirstOrDefault(c => c.Type == "FirstName")!.Value; | |||||
| //LastName = decodedToken.Claims.FirstOrDefault(c => c.Type == "LastName")!.Value; | |||||
| //Email = decodedToken.Claims.FirstOrDefault(c => c.Type == "Email")!.Value; | |||||
| ID = decodedToken.Claims.FirstOrDefault(c => c.Type == "ID")!.Value; | |||||
| IsConnected = true; | |||||
| var user = new ClaimsPrincipal(identity); | |||||
| Roles = user.FindAll(ClaimTypes.Role).Select(r => r.Value).ToList(); | |||||
| SecureStorage.SetAsync("authToken", jwt); // Stocke le token | |||||
| } | |||||
| } | |||||
| public class AuthorizedHttpClientHandler : DelegatingHandler | |||||
| { | |||||
| public AuthorizedHttpClientHandler() | |||||
| { | |||||
| InnerHandler = new HttpClientHandler(); | |||||
| } | |||||
| protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | |||||
| { | |||||
| var token = await SecureStorage.GetAsync("authToken"); | |||||
| if (!string.IsNullOrEmpty(token)) | |||||
| { | |||||
| request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); | |||||
| } | |||||
| return await base.SendAsync(request, cancellationToken); | |||||
| } | |||||
| } | |||||
| public class ApiService | |||||
| { | |||||
| private readonly HttpClient _httpClient; | |||||
| private readonly HttpClient _httpClientLogin; | |||||
| #region Constructeur | |||||
| public ApiService() | |||||
| { | |||||
| _httpClientLogin = new HttpClient { BaseAddress = new Uri("https://localhost:7008") }; | |||||
| _httpClient = new HttpClient(new AuthorizedHttpClientHandler()); | |||||
| _httpClient.BaseAddress = new Uri("https://localhost:7008"); | |||||
| } | |||||
| #endregion | |||||
| #region Service Create | |||||
| public async Task<bool> Create_Async<T>(string url, T data) | |||||
| { | |||||
| var response = await _httpClient.PostAsJsonAsync(url, data); | |||||
| if (response.StatusCode == System.Net.HttpStatusCode.NotFound) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| else if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) | |||||
| { | |||||
| throw new Exception("Internal server error"); | |||||
| } | |||||
| return response.IsSuccessStatusCode; | |||||
| } | |||||
| #endregion | |||||
| #region Service Update | |||||
| public async Task<bool> Update_Async<T>(string url, T data) | |||||
| { | |||||
| var response = await _httpClient.PutAsJsonAsync(url, data); | |||||
| if (response.StatusCode == System.Net.HttpStatusCode.NotFound) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| else if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) | |||||
| { | |||||
| throw new Exception("Internal server error"); | |||||
| } | |||||
| return response.IsSuccessStatusCode; | |||||
| } | |||||
| #endregion | |||||
| #region Service Delete | |||||
| public async Task<bool> Delete_Async<T>(string url) | |||||
| { | |||||
| var response = await _httpClient.DeleteAsync(url); | |||||
| if (response.StatusCode == System.Net.HttpStatusCode.NotFound) | |||||
| { | |||||
| return false; | |||||
| } | |||||
| else if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) | |||||
| { | |||||
| throw new Exception("Internal server error"); | |||||
| } | |||||
| return response.IsSuccessStatusCode; | |||||
| } | |||||
| #endregion | |||||
| #region Service GetAll | |||||
| public async Task<T[]> GetAll_Async<T>(string url) | |||||
| { | |||||
| // Charger les items | |||||
| var response = await _httpClient.GetFromJsonAsync<T[]>(url); | |||||
| return response ?? Array.Empty<T>(); | |||||
| } | |||||
| #endregion | |||||
| #region Service EnvoiRequete | |||||
| public async Task<string> EnvoiRequete<T>(string url, T data) | |||||
| { | |||||
| var response = await _httpClient.PostAsJsonAsync(url, data); | |||||
| if (!response.IsSuccessStatusCode) | |||||
| return $"Erreur : {response.StatusCode}"; | |||||
| // lire le contenu JSON renvoyé | |||||
| var result = await response.Content.ReadAsStringAsync(); | |||||
| return result; | |||||
| } | |||||
| #endregion | |||||
| #region Service GetById | |||||
| /* A IMPLEMENTER SI BESOIN | |||||
| public async Task<T> GetById_Async<T>(string url) | |||||
| { | |||||
| } | |||||
| */ | |||||
| #endregion | |||||
| #region AuthLogin | |||||
| public async Task<bool> Connexion(LoginModel item) | |||||
| { | |||||
| try | |||||
| { | |||||
| string url= $"api/auth/login"; | |||||
| var response = await _httpClientLogin.PostAsJsonAsync(url, item); | |||||
| response.EnsureSuccessStatusCode(); | |||||
| var loginResponse = await response.Content.ReadFromJsonAsync<LoginResponse>(); | |||||
| if (loginResponse is not null && !string.IsNullOrWhiteSpace(loginResponse.Token)) | |||||
| { | |||||
| UserConntected.SetToken(loginResponse.Token); | |||||
| } | |||||
| else | |||||
| { | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| catch | |||||
| { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| } | |||||
| using System.ComponentModel; | |||||
| using System.Runtime.CompilerServices; | |||||
| using System.Windows.Input; | |||||
| namespace MaUI; | |||||
| public partial class AuthLogin_VM : INotifyPropertyChanged | |||||
| { | |||||
| #region Variables | |||||
| private readonly ApiService _apiService; | |||||
| #endregion | |||||
| #region Propriétés | |||||
| #region Commands | |||||
| public ICommand ValiderCommand { get; set; } | |||||
| public ICommand AnnulerCommand { get; } | |||||
| #endregion | |||||
| #region Element de saisie | |||||
| private LoginModel _selectedItem = null!; | |||||
| public LoginModel SelectedItem | |||||
| { | |||||
| get => _selectedItem; | |||||
| set | |||||
| { | |||||
| _selectedItem = value; | |||||
| OnPropertyChanged(); | |||||
| //MettreAJourSelection(); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #endregion | |||||
| #region Constructeur | |||||
| public AuthLogin_VM() | |||||
| { | |||||
| SelectedItem = new(); | |||||
| ValiderCommand = new Command(ExecuterValidation); | |||||
| AnnulerCommand = new Command(ExecuterAnnulation); | |||||
| _apiService = new ApiService(); | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes actions | |||||
| private async void ExecuterAnnulation() | |||||
| { | |||||
| if (Application.Current?.MainPage != null) | |||||
| { | |||||
| bool reponse = await Application.Current.MainPage.DisplayAlert( | |||||
| "Confirmation", | |||||
| "Voulez-vous vraiment annuler les modifications ?", | |||||
| "Oui", | |||||
| "Non"); | |||||
| if (reponse) | |||||
| { | |||||
| // Retour à la page précédente | |||||
| //await Application.Current.MainPage.Navigation.PopAsync(); | |||||
| } | |||||
| } | |||||
| } | |||||
| private async void ExecuterValidation() | |||||
| { | |||||
| if (Application.Current?.MainPage != null) | |||||
| { | |||||
| if (SelectedItem != null) | |||||
| { | |||||
| bool rep = false; | |||||
| string msg = ""; | |||||
| rep = await _apiService.Connexion(SelectedItem); | |||||
| if (!rep) | |||||
| { | |||||
| msg = "Votre connexion est refusée."; | |||||
| await Application.Current.MainPage.DisplayAlert("Erreur", msg, "OK"); | |||||
| } | |||||
| else | |||||
| { | |||||
| _selectedItem = new LoginModel(); | |||||
| await Application.Current.MainPage.Navigation.PushAsync(new MainPage()); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Implémentation de INotifyPropertyChanged | |||||
| public event PropertyChangedEventHandler? PropertyChanged; | |||||
| protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) | |||||
| { | |||||
| if (propertyName is null) | |||||
| return; | |||||
| PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| #region Autres classes | |||||
| public class LoginModel | |||||
| { | |||||
| public string Username { get; set; } = string.Empty; | |||||
| public string Password { get; set; } = string.Empty; | |||||
| } | |||||
| public class LoginResponse | |||||
| { | |||||
| public string Token { get; set; } = string.Empty; | |||||
| } | |||||
| #endregion |
| using System.Collections.ObjectModel; | |||||
| using System.ComponentModel; | |||||
| using System.Runtime.CompilerServices; | |||||
| using System.Windows.Input; | |||||
| using System.Xml.Linq; | |||||
| namespace MaUI; | |||||
| public class ChatRoom_VM : BindableObject | |||||
| { | |||||
| // === PROPRIÉTÉS === | |||||
| private string _newMessage = ""; | |||||
| public string NewMessage | |||||
| { | |||||
| get => _newMessage; | |||||
| set { _newMessage = value; OnPropertyChanged(); } | |||||
| } | |||||
| public ObservableCollection<Conversation> Conversations { get; } = new(); | |||||
| public ObservableCollection<ChatMessage> CurrentMessages { get; } = new(); | |||||
| private Conversation? _selectedConversation; | |||||
| public Conversation? SelectedConversation | |||||
| { | |||||
| get => _selectedConversation; | |||||
| set { _selectedConversation = value; OnPropertyChanged(); } | |||||
| } | |||||
| public ObservableCollection<string> SelectedDocumentsListe { get; } = new(); | |||||
| public List<ModelInfo> InstalledModels { get; } = new() | |||||
| { | |||||
| new ModelInfo { Name = "gemma3:12b" }, | |||||
| new ModelInfo { Name = "llama3.1:8b" } | |||||
| }; | |||||
| private ModelInfo? _selectedModel; | |||||
| public ModelInfo? SelectedModel | |||||
| { | |||||
| get => _selectedModel; | |||||
| set { _selectedModel = value; OnPropertyChanged(); } | |||||
| } | |||||
| public bool IsApiExterne { get; set; } | |||||
| public bool IsRAG { get; set; } | |||||
| public bool IsGenerateImg { get; set; } | |||||
| public bool IsWithAssistant { get; set; } | |||||
| public bool IsCODER { get; set; } | |||||
| // === COMMANDES === | |||||
| public ICommand DeleteConversationCommand { get; } | |||||
| public ICommand NewConversationCommand { get; } | |||||
| public ICommand AddDocumentsCommand { get; } | |||||
| public ICommand ClearDocumentsCommand { get; } | |||||
| public ICommand SendMessageCommand { get; } | |||||
| private readonly ApiService _apiService; | |||||
| // === CONSTRUCTEUR === | |||||
| public ChatRoom_VM() | |||||
| { | |||||
| DeleteConversationCommand = new Command(DeleteConversation); | |||||
| NewConversationCommand = new Command(NewConversation); | |||||
| AddDocumentsCommand = new Command(AddDocuments); | |||||
| ClearDocumentsCommand = new Command(ClearDocuments); | |||||
| SendMessageCommand = new Command(async () => await SendMessageAsync()); | |||||
| _apiService = new ApiService(); | |||||
| } | |||||
| // === MÉTHODES === | |||||
| private async void DeleteConversation() | |||||
| { | |||||
| if (SelectedConversation != null) | |||||
| { | |||||
| Conversations.Remove(SelectedConversation); | |||||
| SelectedConversation = null; | |||||
| CurrentMessages.Clear(); | |||||
| await Application.Current.MainPage.DisplayAlert("Info", "Conversation supprimée", "OK"); | |||||
| } | |||||
| } | |||||
| private void NewConversation() | |||||
| { | |||||
| var conv = new Conversation { Title = $"Conversation {Conversations.Count + 1}" }; | |||||
| Conversations.Add(conv); | |||||
| SelectedConversation = conv; | |||||
| CurrentMessages.Clear(); | |||||
| } | |||||
| private async void AddDocuments() | |||||
| { | |||||
| // Ici tu pourras brancher le FilePicker MAUI | |||||
| var result = await FilePicker.Default.PickAsync(); | |||||
| if (result != null) | |||||
| { | |||||
| SelectedDocumentsListe.Add(result.FullPath); | |||||
| } | |||||
| } | |||||
| private void ClearDocuments() | |||||
| { | |||||
| SelectedDocumentsListe.Clear(); | |||||
| } | |||||
| private async Task SendMessageAsync() | |||||
| { | |||||
| if (string.IsNullOrWhiteSpace(NewMessage)) | |||||
| return; | |||||
| var userMsg = new ChatMessage { Role = "user", Content = NewMessage }; | |||||
| CurrentMessages.Add(userMsg); | |||||
| NewMessage = ""; | |||||
| try | |||||
| { | |||||
| // Appel API (à brancher avec ton ApiService) | |||||
| var dto = new ChatRequest { Requete = userMsg.Content }; | |||||
| var response = await _apiService.EnvoiRequete<string>( | |||||
| "api/ChatRoom/llm", | |||||
| userMsg.Content | |||||
| ); | |||||
| if (response != null) | |||||
| { | |||||
| var assistantMsg = new ChatMessage { Role = "assistant", Content = response }; | |||||
| CurrentMessages.Add(assistantMsg); | |||||
| } | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| await Application.Current.MainPage.DisplayAlert("Erreur", ex.Message, "OK"); | |||||
| } | |||||
| } | |||||
| } | |||||
| // === CLASSES D’APPUIS === | |||||
| public class Conversation | |||||
| { | |||||
| public string Title { get; set; } = ""; | |||||
| } | |||||
| public class ChatMessage | |||||
| { | |||||
| public string Role { get; set; } = ""; | |||||
| public string Content { get; set; } = ""; | |||||
| } | |||||
| public class ModelInfo | |||||
| { | |||||
| public string Name { get; set; } = ""; | |||||
| } | |||||
| public class ChatRequest | |||||
| { | |||||
| public string Requete { get; set; } = ""; | |||||
| } |
| using System; | |||||
| using System.Collections.ObjectModel; | |||||
| using System.ComponentModel; | |||||
| using System.Runtime.CompilerServices; | |||||
| using System.Threading.Tasks; | |||||
| using System.Windows.Input; | |||||
| namespace MaUI | |||||
| { | |||||
| public partial class ListeEmails_VM : INotifyPropertyChanged | |||||
| { | |||||
| #region Variables | |||||
| private readonly ApiService _apiService; | |||||
| #endregion | |||||
| #region Liste observable | |||||
| public ObservableCollection<LOGS_DTO> Liste_Items { get; set; } | |||||
| #endregion | |||||
| #region Element sélectionnée | |||||
| private LOGS_DTO? _selectedItem; | |||||
| public LOGS_DTO? SelectedItem | |||||
| { | |||||
| get => _selectedItem; | |||||
| set | |||||
| { | |||||
| _selectedItem = value; | |||||
| OnPropertyChanged(); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Commands | |||||
| public ICommand LoadCommand { get; } | |||||
| public ICommand EditCommand { get; } | |||||
| public ICommand DeleteCommand { get; } | |||||
| public ICommand CreateCommand { get; } | |||||
| #endregion | |||||
| #region Constructeur | |||||
| public ListeEmails_VM() | |||||
| { | |||||
| _apiService = new ApiService(); | |||||
| Liste_Items = new ObservableCollection<LOGS_DTO>(); | |||||
| // Initialisation des commandes | |||||
| LoadCommand = new Command(async () => await LoadDataAsync()); | |||||
| EditCommand = new Command(async () => await EditSelectedItemAsync(), () => SelectedItem != null); | |||||
| DeleteCommand = new Command(async () => await DeleteSelectedItemAsync(), () => SelectedItem != null); | |||||
| CreateCommand = new Command(async () => await CreateNewItemAsync()); | |||||
| // Charger initialement les données | |||||
| _ = LoadDataAsync(); | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes | |||||
| public async Task LoadDataAsync() | |||||
| { | |||||
| try | |||||
| { | |||||
| var items = await _apiService.GetAll_Async<LOGS_DTO>("api/LOGS_DTO"); | |||||
| Liste_Items.Clear(); | |||||
| foreach (var item in items) | |||||
| { | |||||
| Liste_Items.Add(item); | |||||
| } | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| Console.WriteLine($"Erreur lors du chargement des données : {ex.Message}"); | |||||
| await Application.Current!.MainPage!.DisplayAlert( | |||||
| "Erreur", | |||||
| $"Erreur lors du chargement des données : {ex.Message}", | |||||
| "OK"); | |||||
| } | |||||
| } | |||||
| public async Task EditSelectedItemAsync() | |||||
| { | |||||
| if (SelectedItem != null && Application.Current?.MainPage?.Navigation != null) | |||||
| { | |||||
| await Application.Current.MainPage.Navigation.PushAsync(new LOGS_Detail_Page(SelectedItem)); | |||||
| } | |||||
| else | |||||
| { | |||||
| Console.WriteLine("Navigation ou SelectedItem est null."); | |||||
| await Application.Current!.MainPage!.DisplayAlert( | |||||
| "Erreur", | |||||
| $"Navigation ou SelectedItem est null.", | |||||
| "OK"); | |||||
| } | |||||
| } | |||||
| public async Task DeleteSelectedItemAsync() | |||||
| { | |||||
| if (SelectedItem != null && Application.Current?.MainPage != null) | |||||
| { | |||||
| bool confirm = await Application.Current.MainPage.DisplayAlert( | |||||
| "Confirmation", | |||||
| "Voulez-vous vraiment supprimer cet élément ?", | |||||
| "Oui", "Non"); | |||||
| if (confirm) | |||||
| { | |||||
| try | |||||
| { | |||||
| var result = await _apiService.Delete_Async<Guid>($"api/LOGS_DTO/{SelectedItem.ID}"); | |||||
| if (result) | |||||
| { | |||||
| Liste_Items.Remove(SelectedItem); | |||||
| await Application.Current.MainPage.DisplayAlert("Succès", "Suppression effectuée.", "OK"); | |||||
| } | |||||
| else | |||||
| { | |||||
| await Application.Current.MainPage.DisplayAlert("Erreur", "Suppression non effectuée.", "OK"); | |||||
| } | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| Console.WriteLine($"Erreur lors de la suppression : {ex.Message}"); | |||||
| await Application.Current!.MainPage!.DisplayAlert( | |||||
| "Erreur", | |||||
| $"Erreur lors de la suppression : {ex.Message}", | |||||
| "OK"); | |||||
| } | |||||
| } | |||||
| } | |||||
| else | |||||
| { | |||||
| Console.WriteLine("MainPage or SelectedItem est null."); | |||||
| await Application.Current!.MainPage!.DisplayAlert( | |||||
| "Erreur", | |||||
| $"MainPage or SelectedItem est null.", | |||||
| "OK"); | |||||
| } | |||||
| } | |||||
| public async Task CreateNewItemAsync() | |||||
| { | |||||
| if (Application.Current?.MainPage?.Navigation != null) | |||||
| { | |||||
| await Application.Current.MainPage.Navigation.PushAsync(new LOGS_Detail_Page(null)); | |||||
| } | |||||
| else | |||||
| { | |||||
| Console.WriteLine("Navigation est null."); | |||||
| await Application.Current!.MainPage!.DisplayAlert( | |||||
| "Erreur", | |||||
| $"Navigation est null", | |||||
| "OK"); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Implémentation de INotifyPropertyChanged pour notifier les changements | |||||
| public event PropertyChangedEventHandler? PropertyChanged = delegate { }; | |||||
| protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) | |||||
| { | |||||
| PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| } | |||||
| using System.Text.Json.Serialization; | |||||
| namespace Services.Models | |||||
| { | |||||
| public class OllamaModel | |||||
| { | |||||
| [JsonPropertyName("name")] | |||||
| public string Name { get; set; } = ""; | |||||
| [JsonPropertyName("model")] | |||||
| public string Model { get; set; } = ""; | |||||
| [JsonPropertyName("modified_at")] | |||||
| public DateTime Modified_At { get; set; } | |||||
| [JsonPropertyName("size")] | |||||
| public long Size { get; set; } | |||||
| [JsonPropertyName("digest")] | |||||
| public string Digest { get; set; } = ""; | |||||
| [JsonPropertyName("details")] | |||||
| public OllamaModelDetails Details { get; set; } = new(); | |||||
| } | |||||
| public class OllamaModelDetails | |||||
| { | |||||
| [JsonPropertyName("parent_model")] | |||||
| public string Parent_Model { get; set; } = ""; | |||||
| [JsonPropertyName("format")] | |||||
| public string Format { get; set; } = ""; | |||||
| [JsonPropertyName("family")] | |||||
| public string Family { get; set; } = ""; | |||||
| [JsonPropertyName("families")] | |||||
| public List<string> Families { get; set; } = new(); | |||||
| [JsonPropertyName("parameter_size")] | |||||
| public string ParameterSize { get; set; } = ""; | |||||
| [JsonPropertyName("quantization_level")] | |||||
| public string QuantizationLevel { get; set; } = ""; | |||||
| } | |||||
| } |
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | |||||
| <TargetFramework>net8.0</TargetFramework> | |||||
| <ImplicitUsings>enable</ImplicitUsings> | |||||
| <Nullable>enable</Nullable> | |||||
| </PropertyGroup> | |||||
| </Project> |
| using System.Text.Json.Serialization; | |||||
| namespace OllamaService.Models | |||||
| { | |||||
| public class OllamaRequest | |||||
| { | |||||
| [JsonPropertyName("model")] | |||||
| public string Model { get; set; } = ""; | |||||
| [JsonPropertyName("prompt")] | |||||
| public string Prompt { get; set; } = ""; | |||||
| [JsonPropertyName("images")] | |||||
| public List<string> Images { get; set; } = new(); | |||||
| [JsonPropertyName("stream")] | |||||
| public bool Stream { get; set; } = false; | |||||
| [JsonPropertyName("temperature")] | |||||
| public double Temperature { get; set; } = 0.01; | |||||
| [JsonPropertyName("top_p")] | |||||
| public double Top_p { get; set; } = 0.9; | |||||
| [JsonPropertyName("max_tokens")] | |||||
| public int Max_tokens { get; set; } = 400; | |||||
| } | |||||
| public class OllamaResponse | |||||
| { | |||||
| [JsonPropertyName("response")] | |||||
| public string Completion { get; set; } = ""; | |||||
| } | |||||
| } |
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | |||||
| <TargetFramework>net8.0</TargetFramework> | |||||
| <ImplicitUsings>enable</ImplicitUsings> | |||||
| <Nullable>enable</Nullable> | |||||
| <PlatformTarget>x64</PlatformTarget> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\ChatConversationStructure\ChatConversationStructure.csproj" /> | |||||
| <ProjectReference Include="..\OllamaModels\OllamaModels.csproj" /> | |||||
| <ProjectReference Include="..\ToolsService\ToolsService.csproj" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <Folder Include="Models\" /> | |||||
| </ItemGroup> | |||||
| </Project> |
| using System.Text; | |||||
| using System.Text.Json; | |||||
| using ToolsServices; | |||||
| namespace Services.Ollama | |||||
| { | |||||
| public static class EmbeddingService | |||||
| { | |||||
| public static async Task<(bool, float[]?)> GetEmbeddingAsync(string text, bool isOnDemand) | |||||
| { | |||||
| LoggerService.LogDebug("EmbeddingService.GetEmbeddingAsync"); | |||||
| try | |||||
| { | |||||
| ParametresOllamaService? ollama = OllamaService.LoadParametres(); | |||||
| if (ollama == null) | |||||
| { | |||||
| var msg = "Erreur de chargement des paramètres Ollama."; | |||||
| LoggerService.LogWarning(msg); | |||||
| return (false, null); | |||||
| } | |||||
| var url = ""; | |||||
| if (isOnDemand) | |||||
| url = $"{ollama.Ollama_URL}/api/embeddings"; | |||||
| else | |||||
| url = $"{ollama.Ollama_Batch_URL}/api/embeddings"; | |||||
| var request = new | |||||
| { | |||||
| model = "nomic-embed-text", | |||||
| prompt = text, | |||||
| stream = false | |||||
| }; | |||||
| HttpClient httpClient = new(); | |||||
| var json = JsonSerializer.Serialize(request); | |||||
| var response = await httpClient.PostAsync(url, | |||||
| new StringContent(json, Encoding.UTF8, "application/json")); | |||||
| response.EnsureSuccessStatusCode(); | |||||
| var result = await response.Content.ReadAsStringAsync(); | |||||
| using var doc = JsonDocument.Parse(result); | |||||
| var retour = doc.RootElement | |||||
| .GetProperty("embedding") | |||||
| .EnumerateArray() | |||||
| .Select(e => e.GetSingle()) | |||||
| .ToArray(); | |||||
| return (true, retour); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors de la récupération de l'embedding : {ex.Message}"); | |||||
| return (false, null); | |||||
| } | |||||
| } | |||||
| } | |||||
| } |
| using System.Text.Json; | |||||
| using ToolsServices; | |||||
| namespace Services.Ollama | |||||
| { | |||||
| #region Classes annexes | |||||
| public class ModelConfig | |||||
| { | |||||
| public string Primary { get; set; } = ""; | |||||
| public string Secondary { get; set; } = ""; | |||||
| public string Fallback { get; set; } = ""; | |||||
| } | |||||
| public class ModelsConfiguration | |||||
| { | |||||
| public Dictionary<string, ModelConfig> Config { get; set; } = new(); | |||||
| } | |||||
| #endregion | |||||
| #region Classe principale ModelSelector | |||||
| public class ModelSelector | |||||
| { | |||||
| #region Variables | |||||
| private readonly ModelsConfiguration _Models = new(); | |||||
| private string ConfigPath = FichiersInternesService.ParamsModeles; | |||||
| #endregion | |||||
| #region Constructeur | |||||
| public ModelSelector() | |||||
| { | |||||
| if (!File.Exists(ConfigPath)) | |||||
| { | |||||
| CreateConfig(); | |||||
| } | |||||
| var json = File.ReadAllText(ConfigPath); | |||||
| _Models = JsonSerializer.Deserialize<ModelsConfiguration>( | |||||
| json, | |||||
| new JsonSerializerOptions { PropertyNameCaseInsensitive = true } | |||||
| ) ?? new ModelsConfiguration(); | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes publiques | |||||
| public bool SaveConfig(string useCase, ModelConfig models) | |||||
| { | |||||
| LoggerService.LogInfo($"ModelSelector.SaveConfig pour le cas d'usage '{useCase}'"); | |||||
| try | |||||
| { | |||||
| if (_Models.Config.ContainsKey(useCase)) | |||||
| { | |||||
| _Models.Config[useCase].Primary = models.Primary; | |||||
| _Models.Config[useCase].Secondary = models.Secondary; | |||||
| _Models.Config[useCase].Fallback = models.Fallback; | |||||
| } | |||||
| else | |||||
| { | |||||
| _Models.Config[useCase] = new ModelConfig | |||||
| { | |||||
| Primary = models.Primary, | |||||
| Secondary = models.Secondary, | |||||
| Fallback = models.Fallback | |||||
| }; | |||||
| } | |||||
| ModelsConfiguration config = new ModelsConfiguration | |||||
| { | |||||
| Config = _Models.Config | |||||
| }; | |||||
| var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true }); | |||||
| File.WriteAllText(ConfigPath, json); | |||||
| return true; | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"Échec de la sauvegarde de la configuration des modèles du cas d'usage {useCase} : {ex.Message}"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| public IEnumerable<string> GetModelsForUseCase(string useCase) | |||||
| { | |||||
| LoggerService.LogInfo($"ModelSelector.GetModelsForUseCase pour le cas d'usage : '{useCase}'"); | |||||
| try | |||||
| { | |||||
| if (!_Models.Config.TryGetValue(useCase, out var config)) | |||||
| throw new Exception($"Cas d'usage '{useCase}' introuvable dans la config."); | |||||
| var allModels = new List<string>(); | |||||
| if (config.Primary != null && config.Primary != "") | |||||
| allModels.Add(config.Primary); | |||||
| if (config.Secondary != null && config.Secondary != "") | |||||
| allModels.Add(config.Secondary); | |||||
| if (config.Fallback != null && config.Fallback != "") | |||||
| allModels.Add(config.Fallback); | |||||
| return allModels; | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"GetModelsForUseCase: {ex.Message}"); | |||||
| return new List<string>(); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes privées | |||||
| private void CreateConfig() | |||||
| { | |||||
| var txt = """ | |||||
| { | |||||
| "Config": { | |||||
| "LLM": { | |||||
| "Primary": "thewindmom/hermes-3-llama-3.1-8b:latest", | |||||
| "Secondary": "gemma3:12b", | |||||
| "Fallback": "deepseek-coder-v2:16b" | |||||
| }, | |||||
| "RAG": { | |||||
| "Primary": "gemma3:12b-it-qat", | |||||
| "Secondary": "llama3-chatqa:8b", | |||||
| "Fallback": "phi4-mini:latest" | |||||
| }, | |||||
| "Resume_Documents": { | |||||
| "Primary": "gemma3:12b-it-qat", | |||||
| "Secondary": "llama3-chatqa:8b", | |||||
| "Fallback": "phi4:latest" | |||||
| }, | |||||
| "Interpretation_Images": { | |||||
| "Primary": "llava-llama3:latest", | |||||
| "Secondary": "llava:7b", | |||||
| "Fallback": "" | |||||
| }, | |||||
| "Prompt_Generation_Fooocus": { | |||||
| "Primary": "openchat:latest", | |||||
| "Secondary": "gemma3:12b", | |||||
| "Fallback": "phi4-mini:latest" | |||||
| }, | |||||
| "Analyse_Mails": { | |||||
| "Primary": "thewindmom/hermes-3-llama-3.1-8b:latest", | |||||
| "Secondary": "gemma3:12b-it-qat", | |||||
| "Fallback": "phi4:latest" | |||||
| }, | |||||
| "Analyse_CV_Mission": { | |||||
| "Primary": "phi4:latest", | |||||
| "Secondary": "gemma3:27b", | |||||
| "Fallback": "llama3-chatqa:8b" | |||||
| }, | |||||
| "Pdf_To_Xml": { | |||||
| "Primary": "gemma3:12b-it-qat", | |||||
| "Secondary": "phi4-mini:latest", | |||||
| "Fallback": "" | |||||
| } | |||||
| } | |||||
| } | |||||
| """; | |||||
| TxtService.CreateTextFile(ConfigPath, txt); | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| #endregion | |||||
| } |
| using Services.Models; | |||||
| using System.Diagnostics; | |||||
| using System.Net.Http.Json; | |||||
| using System.Text; | |||||
| using System.Text.Json; | |||||
| using System.Text.RegularExpressions; | |||||
| using ToolsServices; | |||||
| using OllamaService.Models; | |||||
| namespace Services.Ollama | |||||
| { | |||||
| public static class OllamaService | |||||
| { | |||||
| #region Variables | |||||
| private static HttpClient? _HttpClient; | |||||
| private static readonly string NomFichierData = FichiersInternesService.ParamsOllama;// "paramsOllama.txt"; | |||||
| private static List<string> ImagesFullFilename = new(); | |||||
| #endregion | |||||
| #region Méthodes publiques | |||||
| #region InterActions | |||||
| /// <summary> | |||||
| /// Vérifie si Ollama est actif | |||||
| /// </summary> | |||||
| /// <param name="isOnDemand"></param> | |||||
| /// <returns></returns> | |||||
| public static async Task<bool> IsOllamaActif(bool isOnDemand) | |||||
| { | |||||
| var precisionContext = isOnDemand ? "OnDemand" : "Batch"; | |||||
| LoggerService.LogInfo($"OllamaService.IsOllamaActif {precisionContext}"); | |||||
| try | |||||
| { | |||||
| ParametresOllamaService? ollama = LoadParametres(); | |||||
| if (ollama == null) | |||||
| { | |||||
| var msg = "Erreur de chargement des paramètres Ollama."; | |||||
| LoggerService.LogWarning(msg); | |||||
| return false; | |||||
| } | |||||
| var url = ""; | |||||
| if(isOnDemand) | |||||
| url = $"{ollama.Ollama_URL}/api/version"; | |||||
| else | |||||
| url = $"{ollama.Ollama_Batch_URL}/api/version"; | |||||
| _HttpClient = new HttpClient(); | |||||
| _HttpClient.Timeout = TimeSpan.FromSeconds(30); | |||||
| var response = await _HttpClient.GetAsync(url); | |||||
| LoggerService.LogDebug($"OllamaService.IsOllamaActif {precisionContext} : {response.IsSuccessStatusCode}"); | |||||
| return response.IsSuccessStatusCode; | |||||
| } | |||||
| catch(Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"OllamaService.IsOllamaActif {precisionContext} : False --> {ex.Message}"); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| /// <summary> | |||||
| /// Appelle l'api Generate de Ollama | |||||
| /// </summary> | |||||
| /// <param name="prompt"></param> | |||||
| /// <param name="precision"></param> | |||||
| /// <param name="model"></param> | |||||
| /// <param name="isOnDemand"></param> | |||||
| /// <returns></returns> | |||||
| public static async Task<(bool, string)> GenererAsync(string prompt, string precision, string model, bool isOnDemand) | |||||
| { | |||||
| var precisionContext = isOnDemand ? "OnDemand" : "Batch"; | |||||
| LoggerService.LogInfo($"OllamaService.GenererAsync {precisionContext} : {model}"); | |||||
| string? sReturn = ""; | |||||
| Stopwatch chrono = new Stopwatch(); | |||||
| chrono.Start(); | |||||
| bool isSuccess = true; | |||||
| ParametresOllamaService? ollama = LoadParametres(); | |||||
| if (ollama == null) | |||||
| { | |||||
| var msg = "Erreur de chargement des paramètres Ollama."; | |||||
| LoggerService.LogWarning(msg); | |||||
| return (false, msg); | |||||
| } | |||||
| try | |||||
| { | |||||
| //prompt = "Donne tes réponses dans la langue Française." + "\n" + prompt; | |||||
| prompt = PromptService.GetPrompt(PromptService.ePrompt.OllamaService_PromptSystem) + "\n" + prompt; | |||||
| var requestBody = new | |||||
| { | |||||
| model = model, | |||||
| prompt = TruncatePromptBySentenceAsync(prompt), | |||||
| temperature = isOnDemand ? ollama.Ollama_Temperature : ollama.Ollama_Batch_Temperature, // → 0 : réponses plus précises et conservatrices ; 1 : plus créatives | |||||
| top_p = isOnDemand ? ollama.Ollama_Top_p : ollama.Ollama_Batch_Top_p, // → on considère uniquement les mots parmi les plus probables (limite l’invention) | |||||
| max_tokens = isOnDemand ? ollama.Ollama_Max_tokens : ollama.Ollama_Batch_Max_tokens, // → pour ne pas générer des réponses trop longues | |||||
| stream = false | |||||
| }; | |||||
| var url = isOnDemand ? ollama.Ollama_URL : ollama.Ollama_Batch_URL; | |||||
| int timeOut = isOnDemand ? ollama.Ollama_TimeOut : ollama.Ollama_Batch_TimeOut; | |||||
| url = $"{url}/api/generate"; | |||||
| LoggerService.LogDebug($"Génération Ollama : {precision} - Model : {model} - URL : {url} - Time-out : {timeOut}"); | |||||
| LoggerService.LogDebug($"Prompt : {requestBody.prompt}"); | |||||
| _HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(timeOut) }; | |||||
| var response = await _HttpClient.PostAsJsonAsync($"{url}", requestBody); | |||||
| if (!response.IsSuccessStatusCode) | |||||
| { | |||||
| var err = await response.Content.ReadAsStringAsync(); | |||||
| var msg = $"Erreur Ollama({response.StatusCode}) : {err}"; | |||||
| LoggerService.LogWarning(msg); | |||||
| return (false, $"{msg}"); | |||||
| } | |||||
| var json = await response.Content.ReadFromJsonAsync<JsonElement>(); | |||||
| sReturn = json!.GetProperty("response")!.GetString(); | |||||
| if (sReturn != null) | |||||
| { | |||||
| sReturn = EpureReponse(sReturn); | |||||
| } | |||||
| LoggerService.LogDebug($"OllamaService.GenerateAsync : {sReturn}"); | |||||
| } | |||||
| catch (HttpRequestException ex) | |||||
| { | |||||
| isSuccess = false; | |||||
| LoggerService.LogError("Erreur HTTP : " + ex.Message); | |||||
| } | |||||
| catch (OperationCanceledException) | |||||
| { | |||||
| isSuccess = false; | |||||
| LoggerService.LogError($"La requête a été annulée après {ollama.Ollama_TimeOut} secondes."); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| isSuccess = false; | |||||
| sReturn = $"Erreur lors de la génération : {ex.Message}"; | |||||
| LoggerService.LogError(sReturn); | |||||
| //return $"{msg}"; | |||||
| } | |||||
| finally | |||||
| { | |||||
| chrono.Stop(); | |||||
| LoggerService.LogInfo($"Temps de la génération : {chrono.ElapsedMilliseconds / 1000} s ({precision})"); | |||||
| } | |||||
| return (isSuccess,$"{sReturn}"); | |||||
| } | |||||
| /// <summary> | |||||
| /// Appelle l'api Chat de Ollama | |||||
| /// </summary> | |||||
| /// <param name="model"></param> | |||||
| /// <param name="messages"></param> | |||||
| /// <returns></returns> | |||||
| public static async Task<(bool, string)> ChatAsync(string model, List<ChatMessage> messages) | |||||
| { | |||||
| LoggerService.LogInfo($"OllamaService.SendMessageAsync : {model}"); | |||||
| Stopwatch chrono = new Stopwatch(); | |||||
| chrono.Start(); | |||||
| string? sReturn = ""; | |||||
| bool isSuccess = true; | |||||
| ParametresOllamaService? ollama = LoadParametres(); | |||||
| if (ollama == null) | |||||
| { | |||||
| var msg = "Erreur de chargement des paramètres Ollama."; | |||||
| LoggerService.LogWarning(msg); | |||||
| return (false, msg); | |||||
| } | |||||
| try | |||||
| { | |||||
| var promptSystem = PromptService.GetPrompt(PromptService.ePrompt.OllamaService_PromptSystem); | |||||
| var systemPrompt = new ChatMessage { Role = "system", Content = promptSystem, Model = model }; | |||||
| var allMessages = new List<ChatMessage> { systemPrompt }; | |||||
| messages[0].Content = promptSystem + "\n" + messages[0].Content; | |||||
| allMessages.AddRange(messages); | |||||
| var request = new | |||||
| { | |||||
| model = model, | |||||
| messages = allMessages.Select(m => new { role = m.Role, content = m.Content }).ToList(), | |||||
| temperature = ollama.Ollama_Temperature, // → 0 : réponses plus précises et conservatrices ; 1 : plus créatives | |||||
| top_p = ollama.Ollama_Top_p, // → on considère uniquement les mots parmi les plus probables (limite l’invention) | |||||
| max_tokens = ollama.Ollama_Max_tokens, // → pour ne pas générer des réponses trop longues | |||||
| stream = false | |||||
| }; | |||||
| var url = $"{ollama.Ollama_URL}/api/chat"; | |||||
| LoggerService.LogDebug($"Génération Ollama : CHAT - Model : {model} - URL : {url} - Time-out : {ollama.Ollama_TimeOut}"); | |||||
| LoggerService.LogDebug($"Prompt : {request.messages[1].content}"); | |||||
| _HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(ollama.Ollama_TimeOut) }; | |||||
| var response = await _HttpClient.PostAsync(url, new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json")); | |||||
| if (!response.IsSuccessStatusCode) | |||||
| { | |||||
| var err = await response.Content.ReadAsStringAsync(); | |||||
| var msg = $"Erreur Ollama({response.StatusCode}) : {err}"; | |||||
| LoggerService.LogWarning(msg); | |||||
| return (false, $"{msg}"); | |||||
| } | |||||
| var result = await response.Content.ReadAsStringAsync(); | |||||
| using var doc = JsonDocument.Parse(result); | |||||
| sReturn = doc.RootElement.GetProperty("message").GetProperty("content").GetString(); | |||||
| if (sReturn != null) | |||||
| { | |||||
| sReturn = EpureReponse(sReturn); | |||||
| } | |||||
| LoggerService.LogDebug($"OllamaService.SendMessageAsync : {sReturn}"); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| isSuccess = false; | |||||
| sReturn = $"Erreur lors de la génération : {ex.Message}"; | |||||
| LoggerService.LogError(sReturn); | |||||
| } | |||||
| finally | |||||
| { | |||||
| chrono.Stop(); | |||||
| LoggerService.LogInfo($"Temps de la génération : {chrono.ElapsedMilliseconds / 1000} s (LLM)"); | |||||
| if (sReturn == null) | |||||
| sReturn = ""; | |||||
| } | |||||
| return (isSuccess, $"{sReturn}"); | |||||
| } | |||||
| /// <summary> | |||||
| /// Appelle l'api Generate de Ollama avec envoi d'images dans le prompt | |||||
| /// </summary> | |||||
| /// <param name="model"></param> | |||||
| /// <param name="prompt"></param> | |||||
| /// <param name="imagesFullFilename"></param> | |||||
| /// <returns></returns> | |||||
| public static async Task<(bool, string)> GenererAsync(string model, string prompt, List<string> imagesFullFilename) | |||||
| { | |||||
| LoggerService.LogInfo($"OllamaService.SendMessageAsync : {model}"); | |||||
| Stopwatch chrono = new Stopwatch(); | |||||
| chrono.Start(); | |||||
| bool isSuccess = true; | |||||
| var imagesBase64 = new List<string>(); | |||||
| ParametresOllamaService? ollama = LoadParametres(); | |||||
| if (ollama == null) | |||||
| { | |||||
| var msg = "Erreur de chargement des paramètres Ollama."; | |||||
| LoggerService.LogWarning(msg); | |||||
| return (false, msg); | |||||
| } | |||||
| foreach (var uneImage in imagesFullFilename) | |||||
| { | |||||
| byte[] imageBytes = File.ReadAllBytes(uneImage); | |||||
| imagesBase64.Add(Convert.ToBase64String(imageBytes)); | |||||
| } | |||||
| var request = new OllamaRequest | |||||
| { | |||||
| Model = model, | |||||
| Prompt = PromptService.GetPrompt(PromptService.ePrompt.OllamaService_PromptSystem) + "\n" + prompt, | |||||
| Temperature = ollama.Ollama_Temperature, // → 0 : réponses plus précises et conservatrices ; 1 : plus créatives | |||||
| Top_p = ollama.Ollama_Top_p, // → on considère uniquement les mots parmi les plus probables (limite l’invention) | |||||
| Max_tokens = ollama.Ollama_Max_tokens, // → pour ne pas générer des réponses trop longues | |||||
| Images = imagesBase64, | |||||
| Stream = false | |||||
| }; | |||||
| string? sReturn = ""; | |||||
| try | |||||
| { | |||||
| var url = $"{ollama.Ollama_URL}/api/generate"; | |||||
| LoggerService.LogDebug($"Prompt envoyé à {url}"); | |||||
| _HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(ollama.Ollama_TimeOut) }; | |||||
| var response = await _HttpClient.PostAsJsonAsync(url, request); | |||||
| if (!response.IsSuccessStatusCode) | |||||
| { | |||||
| var err = await response.Content.ReadAsStringAsync(); | |||||
| var msg = $"Erreur Ollama({response.StatusCode}) : {err}"; | |||||
| LoggerService.LogWarning(msg); | |||||
| return (false, $"{msg}"); | |||||
| } | |||||
| var result = await response.Content.ReadFromJsonAsync<OllamaResponse>(); | |||||
| if (result != null) | |||||
| { | |||||
| sReturn = result.Completion.Trim(); | |||||
| sReturn = EpureReponse(sReturn); | |||||
| } | |||||
| LoggerService.LogDebug($"OllamaService.SendMessageAsync : {sReturn}"); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| isSuccess=false; | |||||
| sReturn = $"Erreur lors de la génération : {ex.Message}"; | |||||
| LoggerService.LogError(sReturn); | |||||
| //return sReturn; | |||||
| } | |||||
| finally | |||||
| { | |||||
| chrono.Stop(); | |||||
| LoggerService.LogInfo($"Temps de la génération : {chrono.ElapsedMilliseconds / 1000} s (Interprétation images)"); | |||||
| if (sReturn == null) | |||||
| sReturn = ""; | |||||
| } | |||||
| return (isSuccess, $"{sReturn}"); | |||||
| } | |||||
| #endregion | |||||
| #region Gestion des paramètres | |||||
| public static ParametresOllamaService? LoadParametres() | |||||
| { | |||||
| LoggerService.LogInfo("OllamaService.LoadParametres"); | |||||
| ParametresOllamaService SelectedItem = new(); | |||||
| try | |||||
| { | |||||
| if (File.Exists(NomFichierData)) | |||||
| { | |||||
| string[] lignes = File.ReadAllLines(NomFichierData); | |||||
| if (lignes.Length > 0) | |||||
| { | |||||
| SelectedItem.Ollama_URL = lignes[0]; | |||||
| } | |||||
| if (lignes.Length > 1) | |||||
| { | |||||
| if(int.TryParse(lignes[1], out int timeout)) | |||||
| SelectedItem.Ollama_TimeOut = timeout; | |||||
| } | |||||
| if (lignes.Length > 2) | |||||
| { | |||||
| if (double.TryParse(lignes[2], out double temperature)) | |||||
| SelectedItem.Ollama_Temperature = temperature; | |||||
| } | |||||
| if (lignes.Length > 3) | |||||
| { | |||||
| if (double.TryParse(lignes[3], out double top_p)) | |||||
| SelectedItem.Ollama_Top_p = top_p; | |||||
| } | |||||
| if (lignes.Length > 4) | |||||
| { | |||||
| if (int.TryParse(lignes[4], out int max_tokens)) | |||||
| SelectedItem.Ollama_Max_tokens = max_tokens; | |||||
| } | |||||
| if (lignes.Length > 5) | |||||
| { | |||||
| SelectedItem.Ollama_Batch_URL = lignes[5]; | |||||
| } | |||||
| if (lignes.Length > 6) | |||||
| { | |||||
| if (int.TryParse(lignes[6], out int timeout)) | |||||
| SelectedItem.Ollama_Batch_TimeOut = timeout; | |||||
| } | |||||
| if (lignes.Length > 7) | |||||
| { | |||||
| if (double.TryParse(lignes[7], out double temperature)) | |||||
| SelectedItem.Ollama_Batch_Temperature = temperature; | |||||
| } | |||||
| if (lignes.Length > 8) | |||||
| { | |||||
| if (double.TryParse(lignes[8], out double top_p)) | |||||
| SelectedItem.Ollama_Batch_Top_p = top_p; | |||||
| } | |||||
| if (lignes.Length > 9) | |||||
| { | |||||
| if (int.TryParse(lignes[9], out int max_tokens)) | |||||
| SelectedItem.Ollama_Batch_Max_tokens = max_tokens; | |||||
| } | |||||
| } | |||||
| return SelectedItem; | |||||
| } | |||||
| catch | |||||
| { | |||||
| return null; | |||||
| } | |||||
| } | |||||
| public static bool SaveParametres(ParametresOllamaService selectedItem) | |||||
| { | |||||
| LoggerService.LogInfo("OllamaService.SaveParametres"); | |||||
| try | |||||
| { | |||||
| StringBuilder sb = new(); | |||||
| sb.AppendLine(selectedItem.Ollama_URL); | |||||
| sb.AppendLine(selectedItem.Ollama_TimeOut.ToString()); | |||||
| sb.AppendLine(selectedItem.Ollama_Temperature.ToString()); | |||||
| sb.AppendLine(selectedItem.Ollama_Top_p.ToString()); | |||||
| sb.AppendLine(selectedItem.Ollama_Max_tokens.ToString()); | |||||
| sb.AppendLine(selectedItem.Ollama_Batch_URL); | |||||
| sb.AppendLine(selectedItem.Ollama_Batch_TimeOut.ToString()); | |||||
| sb.AppendLine(selectedItem.Ollama_Batch_Temperature.ToString()); | |||||
| sb.AppendLine(selectedItem.Ollama_Batch_Top_p.ToString()); | |||||
| sb.AppendLine(selectedItem.Ollama_Batch_Max_tokens.ToString()); | |||||
| File.WriteAllText(NomFichierData, sb.ToString()); | |||||
| return true; | |||||
| } | |||||
| catch | |||||
| { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| public static async Task<List<OllamaModel>> GetInstalledModelsAsync(bool isOnDemand) | |||||
| { | |||||
| LoggerService.LogInfo("OllamaService.GetInstalledModelsAsync"); | |||||
| try | |||||
| { | |||||
| ParametresOllamaService? ollama = LoadParametres(); | |||||
| if (ollama == null) | |||||
| { | |||||
| return new List<OllamaModel> { new OllamaModel { Name = "Erreur de chargement des paramètres Ollama." } }; | |||||
| } | |||||
| var url = ""; | |||||
| int timeOut = 360; | |||||
| if (isOnDemand) | |||||
| { | |||||
| url = $"{ollama.Ollama_URL}/api/tags"; | |||||
| timeOut = ollama.Ollama_TimeOut; | |||||
| } | |||||
| else | |||||
| { | |||||
| url = $"{ollama.Ollama_Batch_URL}/api/tags"; | |||||
| timeOut = ollama.Ollama_Batch_TimeOut; | |||||
| } | |||||
| _HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(timeOut) }; | |||||
| var response = await _HttpClient.GetAsync($"{url}"); | |||||
| LoggerService.LogDebug($"Requête envoyée à {url}"); | |||||
| response.EnsureSuccessStatusCode(); | |||||
| var json = await response.Content.ReadAsStringAsync(); | |||||
| var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = false }; | |||||
| var result = JsonSerializer.Deserialize<OllamaTagsResponse>(json, options); | |||||
| if (result?.Models == null) | |||||
| { | |||||
| return new List<OllamaModel> { new OllamaModel { Name = "Erreur de chargement des paramètres Ollama." } }; | |||||
| } | |||||
| return result.Models; | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors de la récupération des modèles installés : {ex.Message}"); | |||||
| return new List<OllamaModel> { new OllamaModel { Name = $"Erreur : {ex.Message}" } }; | |||||
| } | |||||
| } | |||||
| public static string GetModeleIA(string useCase) | |||||
| { | |||||
| LoggerService.LogInfo("OllamaService.GetModeleIA"); | |||||
| ModelSelector selector = new(); | |||||
| var models = selector.GetModelsForUseCase(useCase); | |||||
| if(models.ToList().Count>1) | |||||
| return models.First(); | |||||
| return ""; | |||||
| } | |||||
| public static IEnumerable<string> GetModelesIA(string useCase) | |||||
| { | |||||
| LoggerService.LogInfo("OllamaService.GetModelesIA"); | |||||
| ModelSelector selector = new(); | |||||
| var models = selector.GetModelsForUseCase(useCase); | |||||
| return models; | |||||
| } | |||||
| public static bool SaveParametresModeles(string useCase, ModelConfig models) | |||||
| { | |||||
| LoggerService.LogInfo("OllamaService.SaveParametresModeles"); | |||||
| ModelSelector selector = new(); | |||||
| var b = selector.SaveConfig(useCase, models); | |||||
| return b; | |||||
| } | |||||
| #endregion | |||||
| #region Nettoyage des prompts et surveillance de la taille | |||||
| public static string CleanUserInput(string input, out bool isPromptInjection) | |||||
| { | |||||
| LoggerService.LogInfo("OllamaService.CleanUserInput"); | |||||
| isPromptInjection = false; | |||||
| var avant = input; | |||||
| if (string.IsNullOrWhiteSpace(input)) | |||||
| return string.Empty; | |||||
| // 1. Limiter la taille max (ex : 500 caractères) | |||||
| /* | |||||
| int maxLength = 500; | |||||
| if (input.Length > maxLength) | |||||
| input = input.Substring(0, maxLength); | |||||
| */ | |||||
| // 2. Supprimer les caractères non imprimables (ex : contrôles, retours chariot non standards) | |||||
| input = RemoveNonPrintableChars(input); | |||||
| // 3. Échapper les triples quotes pour ne pas casser le prompt (on remplace ''' par ' ' ' par exemple) | |||||
| input = input.Replace("'''", "' ' '"); | |||||
| // 4. Rechercher et remplacer les mots/expressions d’injection potentielles | |||||
| string[] blacklist = new string[] { | |||||
| "ignore", "cancel", "stop generating", "disregard instructions","oublie les instructions", "oublie", | |||||
| "ignore instructions","ignore les instructions", "forget", "override", "bypass" | |||||
| }; | |||||
| foreach (var word in blacklist) | |||||
| { | |||||
| // Remplacer même si insensible à la casse | |||||
| input = Regex.Replace(input, Regex.Escape(word), "[CENSORED]", RegexOptions.IgnoreCase); | |||||
| } | |||||
| // 5. Optionnel : remplacer les guillemets doubles et simples pour éviter la confusion | |||||
| input = input.Replace("\"", "'"); | |||||
| input = input.Replace("\\", "/"); // éviter les échappements | |||||
| if (input.Contains("[CENSORED]")) | |||||
| { | |||||
| LoggerService.LogDebug($"Avant :{avant}"); | |||||
| LoggerService.LogDebug($"Après :{input}"); | |||||
| isPromptInjection = true; | |||||
| } | |||||
| return input.Trim(); | |||||
| } | |||||
| #endregion | |||||
| #endregion | |||||
| #region Méthodes privées | |||||
| private static string EpureReponse(string texte) | |||||
| { | |||||
| var txt = texte; | |||||
| txt = txt.Replace("**", ""); | |||||
| //txt = txt.Replace("*", ""); | |||||
| txt = txt.Replace("</end_of_turn>", ""); | |||||
| txt = txt.Replace("</start_of_turn>", ""); | |||||
| txt = txt.Replace("```csv", ""); | |||||
| txt = txt.Replace("```xml", ""); | |||||
| txt = txt.Replace("```", ""); | |||||
| txt = txt.Trim(); | |||||
| return txt; | |||||
| } | |||||
| private static async Task<int> GetTokenMaxAsync(bool isOld) | |||||
| { | |||||
| LoggerService.LogInfo("OllamaService.GetTokenMaxAsync"); | |||||
| try | |||||
| { | |||||
| ParametresOllamaService? ollama = LoadParametres(); | |||||
| if (ollama == null) | |||||
| { | |||||
| return -1; | |||||
| } | |||||
| var requestBody = new | |||||
| { | |||||
| model = "",// ollama.Ollama_Model, | |||||
| }; | |||||
| _HttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(ollama.Ollama_TimeOut) }; | |||||
| // Construire l'URL pour obtenir les informations du modèle | |||||
| string url = $"{ollama.Ollama_URL}/api/show"; | |||||
| LoggerService.LogDebug($"Prompt envoyé à {url}"); | |||||
| var response = await _HttpClient.PostAsJsonAsync($"{url}", requestBody); | |||||
| // Envoyer une requête GET à l'API Ollama | |||||
| //HttpResponseMessage response = await _HttpClient.GetAsync(url); | |||||
| // Vérifier le statut de la réponse | |||||
| if (response.IsSuccessStatusCode) | |||||
| { | |||||
| // Lire le contenu de la réponse | |||||
| string responseBody = await response.Content.ReadAsStringAsync(); | |||||
| var json = await response.Content.ReadFromJsonAsync<JsonElement>(); | |||||
| var contextLength = json | |||||
| .GetProperty("model_info") | |||||
| .GetProperty("llama.context_length") | |||||
| .GetInt32(); | |||||
| return contextLength; | |||||
| //return maxContextSize.HasValue ? (int)maxContextSize : 0; | |||||
| } | |||||
| else | |||||
| { | |||||
| var model = "";//ollama.Ollama_Model | |||||
| LoggerService.LogError($"Erreur lors de la récupération des informations du modèle {model}: {response.StatusCode}"); | |||||
| return -1; | |||||
| } | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors de la récupération du modèle : {ex.Message}"); | |||||
| return -3; // Erreur lors de la récupération du modèle | |||||
| } | |||||
| } | |||||
| // Supprime les caractères non imprimables | |||||
| private static string RemoveNonPrintableChars(string text) | |||||
| { | |||||
| LoggerService.LogInfo("OllamaService.RemoveNonPrintableChars"); | |||||
| var sb = new StringBuilder(); | |||||
| foreach (char c in text) | |||||
| { | |||||
| // Exclure uniquement les caractères de contrôle ASCII (< 32), sauf les classiques utiles | |||||
| if (!char.IsControl(c) || c == '\n' || c == '\r' || c == '\t') | |||||
| { | |||||
| sb.Append(c); | |||||
| } | |||||
| } | |||||
| return sb.ToString(); | |||||
| } | |||||
| private static int GetTokenMaxAsync() | |||||
| { | |||||
| LoggerService.LogInfo("OllamaService.GetTokenMaxAsync"); | |||||
| try | |||||
| { | |||||
| //await Task.Delay(1); | |||||
| return 4096; | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors de la récupération du modèle : {ex.Message}"); | |||||
| return -3; // Erreur lors de la récupération du modèle | |||||
| } | |||||
| } | |||||
| private static int CountTokensAsync(string prompt) | |||||
| { | |||||
| LoggerService.LogInfo("OllamaService.CountTokensAsync"); | |||||
| try | |||||
| { | |||||
| // 1 token, en langue francaise, c'est 1.25 mots (environ) | |||||
| int estimatedTokens = prompt.Split(' ').Length * 5 / 4; | |||||
| return estimatedTokens; | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| LoggerService.LogError($"Erreur lors de la récupération du modèle : {ex.Message}"); | |||||
| return -1; // Erreur lors de la récupération du modèle | |||||
| } | |||||
| } | |||||
| private static string TruncatePromptBySentenceAsync(string prompt) | |||||
| { | |||||
| LoggerService.LogInfo("OllamaService.TruncatePromptBySentenceAsync"); | |||||
| prompt = prompt.Trim(); | |||||
| LoggerService.LogDebug($"Taille avant : {prompt.Length}"); | |||||
| int reserveReponse = 512 * 3; // Réserve pour la réponse | |||||
| var maxTokens = GetTokenMaxAsync(); | |||||
| var nbrTokens = CountTokensAsync(prompt); | |||||
| // Cas simple : le prompt tient dans la limite | |||||
| // On garde 512 * 3 de réserve pour la réponse | |||||
| if ((nbrTokens + reserveReponse) <= maxTokens) | |||||
| { | |||||
| LoggerService.LogDebug($"Taille après : {prompt.Length}"); | |||||
| return prompt; | |||||
| } | |||||
| // Découpe le texte en phrases (naïvement en utilisant les points) | |||||
| var sentences = prompt.Split(new[] { ". ", "? ", "! " }, StringSplitOptions.RemoveEmptyEntries); | |||||
| StringBuilder builder = new(); | |||||
| foreach (var sentence in sentences) | |||||
| { | |||||
| var next = builder.Length > 0 ? builder + ". " + sentence.Trim() : sentence.Trim(); | |||||
| var tokenCount = CountTokensAsync(next.ToString()); | |||||
| if ((tokenCount + reserveReponse) > maxTokens) | |||||
| { | |||||
| break; // Trop long, on s'arrête | |||||
| } | |||||
| builder.Clear(); | |||||
| builder.Append(next); | |||||
| } | |||||
| var rep = builder.ToString().Trim(); | |||||
| LoggerService.LogDebug($"Taille après : {rep.Length}"); | |||||
| return rep;// + "\n\n[Tronqué pour respecter la limite de contexte]"; | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| } |
| using System.Text.Json.Serialization; | |||||
| using Services.Models; | |||||
| namespace Services.Ollama | |||||
| { | |||||
| public class OllamaTagsResponse | |||||
| { | |||||
| [JsonPropertyName("models")] | |||||
| public List<OllamaModel>? Models { get; set; } | |||||
| } | |||||
| } |
| namespace Services.Ollama | |||||
| { | |||||
| public class ParametresOllamaService | |||||
| { | |||||
| public string Ollama_URL { get; set; } = "http://localhost:11434"; | |||||
| public int Ollama_TimeOut { get; set; } = 360; | |||||
| public double Ollama_Temperature { get; set; } = 0.01; | |||||
| public double Ollama_Top_p { get; set; } = 0.9; | |||||
| public int Ollama_Max_tokens { get; set; } = 200; | |||||
| public string Ollama_Batch_URL { get; set; } = "http://localhost:11435"; | |||||
| public int Ollama_Batch_TimeOut { get; set; } = 360; | |||||
| public double Ollama_Batch_Temperature { get; set; } = 0.01; | |||||
| public double Ollama_Batch_Top_p { get; set; } = 0.9; | |||||
| public int Ollama_Batch_Max_tokens { get; set; } = 200; | |||||
| } | |||||
| } |
| <Project Sdk="Microsoft.NET.Sdk"> | |||||
| <PropertyGroup> | |||||
| <TargetFramework>net8.0</TargetFramework> | |||||
| <ImplicitUsings>enable</ImplicitUsings> | |||||
| <Nullable>enable</Nullable> | |||||
| <PlatformTarget>x64</PlatformTarget> | |||||
| </PropertyGroup> | |||||
| <ItemGroup> | |||||
| <PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.18.0" /> | |||||
| <PackageReference Include="Qdrant.Client" Version="1.15.1" /> | |||||
| <PackageReference Include="System.Text.Json" Version="10.0.0" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <ProjectReference Include="..\ReActAgentService\ReActAgentService.csproj" /> | |||||
| <ProjectReference Include="..\ToolsService\ToolsService.csproj" /> | |||||
| </ItemGroup> | |||||
| <ItemGroup> | |||||
| <None Update="onnx\model.onnx"> | |||||
| <CopyToOutputDirectory>Always</CopyToOutputDirectory> | |||||
| </None> | |||||
| <None Update="onnx\tokenizer.json"> | |||||
| <CopyToOutputDirectory>Always</CopyToOutputDirectory> | |||||
| </None> | |||||
| </ItemGroup> | |||||
| </Project> |
| namespace Services | |||||
| { | |||||
| public class ParametresQdrantService | |||||
| { | |||||
| public string Qdrant_URL { get; set; } = "localhost:6334"; | |||||
| public bool Qdrant_IsHttps { get; set; } = false; | |||||
| } | |||||
| } |
| namespace Services | |||||
| { | |||||
| public class ParametresRAGService | |||||
| { | |||||
| public string RAG_Path_ToVectoralize { get; set; } = ""; | |||||
| public string RAG_Path_Vectoralized { get; set; } = ""; | |||||
| } | |||||
| } |
| using Models; | |||||
| using Qdrant.Client.Grpc; | |||||
| using System.Collections; | |||||
| using System.ComponentModel.DataAnnotations; | |||||
| using System.Numerics; | |||||
| using System.Text; | |||||
| using ToolsServices; | |||||
| namespace Services; | |||||
| #region enum des type de documents | |||||
| public enum Domain | |||||
| { | |||||
| RH, | |||||
| Juridique, | |||||
| Global, | |||||
| Emails, | |||||
| CV, | |||||
| Technique | |||||
| } | |||||
| #endregion | |||||
| #region Classe statique d'extension pour Domain | |||||
| public static class DomainExtensions | |||||
| { | |||||
| private static readonly string _CollectionRH = "RH"; | |||||
| private static readonly string _CollectionJuridique = "Juridique"; | |||||
| private static readonly string _CollectionGlobal = "Global"; | |||||
| private static readonly string _CollectionEmails = "Emails"; | |||||
| private static readonly string _CollectionCV = "CV"; | |||||
| private static readonly string _CollectionTechnique = "Technique"; | |||||
| public static List<string> CollectionsName = GetCollectionsName(); | |||||
| public static List<string> CollectionsNameRestricted = GetCollectionsNameRestricted(false); | |||||
| private static List<string> GetCollectionsName() | |||||
| { | |||||
| List<string> strings = new() | |||||
| { | |||||
| _CollectionRH, | |||||
| _CollectionJuridique, | |||||
| _CollectionGlobal, | |||||
| _CollectionEmails, | |||||
| _CollectionCV, | |||||
| _CollectionTechnique | |||||
| }; | |||||
| return strings; | |||||
| } | |||||
| private static List<string> GetCollectionsNameRestricted(bool isVeryRestricted) | |||||
| { | |||||
| List<string> strings = new(); | |||||
| strings.Add(_CollectionGlobal); | |||||
| strings.Add(_CollectionTechnique); | |||||
| if (!isVeryRestricted) | |||||
| { | |||||
| strings.Add(_CollectionJuridique); | |||||
| strings.Add(_CollectionRH); | |||||
| } | |||||
| return strings; | |||||
| } | |||||
| /// <summary> | |||||
| /// Retourne le nom de collection Qdrant associé à un domaine. | |||||
| /// </summary> | |||||
| public static string ToCollectionName(this Domain domain) => domain switch | |||||
| { | |||||
| Domain.RH => _CollectionRH, | |||||
| Domain.Juridique => _CollectionJuridique, | |||||
| Domain.Global => _CollectionGlobal, | |||||
| Domain.Emails => _CollectionEmails, | |||||
| Domain.CV => _CollectionCV, | |||||
| Domain.Technique => _CollectionTechnique, | |||||
| _ => throw new ArgumentOutOfRangeException(nameof(domain), domain, null) | |||||
| }; | |||||
| /// <summary> | |||||
| /// Retourne le nom de collection Qdrant associé à un domaine. | |||||
| /// </summary> | |||||
| public static Domain GetDomainByCollectionName(string CollectionName) | |||||
| { | |||||
| Domain domain = Domain.Global; | |||||
| switch (CollectionName) | |||||
| { | |||||
| case "RH": | |||||
| domain = Domain.RH; | |||||
| break; | |||||
| case "Juridique": | |||||
| domain = Domain.Juridique; | |||||
| break; | |||||
| case "Global": | |||||
| domain = Domain.Global; | |||||
| break; | |||||
| case "Emails": | |||||
| domain = Domain.Emails; | |||||
| break; | |||||
| case "CV": | |||||
| domain = Domain.CV; | |||||
| break; | |||||
| case "Technique": | |||||
| domain = Domain.Technique; | |||||
| break; | |||||
| } | |||||
| return domain; | |||||
| } | |||||
| public static Domain GetDomainByPath(string filePath) | |||||
| { | |||||
| var dirs = Path.GetDirectoryName(filePath)? | |||||
| .Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, | |||||
| StringSplitOptions.RemoveEmptyEntries); | |||||
| if (dirs == null) | |||||
| return Domain.Global; | |||||
| var collections = GetCollectionsName(); | |||||
| // On cherche la première correspondance | |||||
| var collectionName = dirs.FirstOrDefault(d => | |||||
| collections.Any(c => string.Equals(c, d, StringComparison.OrdinalIgnoreCase))); | |||||
| return GetDomainByCollectionName(collectionName ?? "Global"); | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| #region Classe QdrantService | |||||
| /// <summary> | |||||
| /// http://localhost:6333/dashboard | |||||
| /// </summary> | |||||
| public class QdrantService | |||||
| { | |||||
| #region Variables | |||||
| private static readonly string NomFichierData = FichiersInternesService.ParamsQdrant;// "paramsOllama.txt"; | |||||
| private readonly Qdrant.Client.QdrantClient _client; | |||||
| private const int EmbeddingDim = 768; | |||||
| private const Distance DefaultDistance = Distance.Cosine; | |||||
| #endregion | |||||
| #region Constructeur | |||||
| public QdrantService() | |||||
| { | |||||
| var parametres = LoadParametres(); | |||||
| //_client = new Qdrant.Client.QdrantClient(parametres!.Qdrant_URL, parametres!.Qdrant_Port, parametres!.Qdrant_IsHttps); | |||||
| var httpString = parametres!.Qdrant_IsHttps ? "https://" : "http://"; | |||||
| _client = new Qdrant.Client.QdrantClient(new Uri($"{httpString}{parametres!.Qdrant_URL}")); | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes publiques | |||||
| #region Gestion des Paramètres | |||||
| public static ParametresQdrantService? LoadParametres() | |||||
| { | |||||
| ParametresQdrantService? item = new(); | |||||
| LoggerService.LogInfo("QdrantService.LoadParametres"); | |||||
| try | |||||
| { | |||||
| if (File.Exists(NomFichierData)) | |||||
| { | |||||
| string[] lignes = File.ReadAllLines(NomFichierData); | |||||
| if (lignes.Length > 0) | |||||
| item.Qdrant_URL = lignes[0]; | |||||
| if (lignes.Length > 1) | |||||
| { | |||||
| if (bool.TryParse(lignes[1], out bool isHttps)) | |||||
| item.Qdrant_IsHttps = isHttps; | |||||
| } | |||||
| /* | |||||
| if (lignes.Length > 2) | |||||
| { | |||||
| if (int.TryParse(lignes[1], out int port)) | |||||
| item.Qdrant_Port = port; | |||||
| } | |||||
| */ | |||||
| } | |||||
| return item; | |||||
| } | |||||
| catch | |||||
| { | |||||
| return null; | |||||
| } | |||||
| } | |||||
| public static bool SaveParametres(ParametresQdrantService item) | |||||
| { | |||||
| LoggerService.LogInfo("QdrantService.SaveParametres"); | |||||
| try | |||||
| { | |||||
| StringBuilder sb = new(); | |||||
| sb.AppendLine(item.Qdrant_URL); | |||||
| sb.AppendLine(item.Qdrant_IsHttps.ToString()); | |||||
| //sb.AppendLine(item.Qdrant_Port.ToString()); | |||||
| File.WriteAllText(NomFichierData, sb.ToString()); | |||||
| return true; | |||||
| } | |||||
| catch | |||||
| { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| #endregion | |||||
| public async Task<bool> IsAlive() | |||||
| { | |||||
| try | |||||
| { | |||||
| var health = await _client.HealthAsync(); | |||||
| return (health != null); | |||||
| } | |||||
| catch | |||||
| { | |||||
| LoggerService.LogError("QdrantService.IsAlive - Qdrant n'est pas accessible."); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| public async Task InitializeCollectionsAsync(bool isRAZCollections) | |||||
| { | |||||
| if(isRAZCollections) | |||||
| await DeleteCollectionAsync(); | |||||
| foreach (Domain domain in Enum.GetValues(typeof(Domain))) | |||||
| { | |||||
| await EnsureCollectionAsync(domain); | |||||
| } | |||||
| } | |||||
| public async Task DeleteDocumentInQdrant(Domain domain, string nomFichier) | |||||
| { | |||||
| var pointId = new PointId { Uuid = Guid.NewGuid().ToString() }; | |||||
| var collectionName = domain.ToCollectionName(); | |||||
| // 1. Définir le filtre pour trouver les points avec le même `nom_fichier` | |||||
| var filter = new Qdrant.Client.Grpc.Filter | |||||
| { | |||||
| Must = | |||||
| { | |||||
| new Condition | |||||
| { | |||||
| Field = new FieldCondition | |||||
| { | |||||
| Key = "nom_fichier", | |||||
| Match = new Match { Text = nomFichier } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 2. Chercher les points existants avec ce filtre | |||||
| var searchResult = await _client.ScrollAsync( | |||||
| collectionName, | |||||
| filter: filter, | |||||
| limit: 1 // On n'a besoin de vérifier qu'un seul résultat | |||||
| ); | |||||
| // 3. Vérifier si des points ont été trouvés | |||||
| if (searchResult.Result.Count > 0) | |||||
| { | |||||
| LoggerService.LogDebug($"Un ou plusieurs documents avec le nom de fichier '{nomFichier}' ont été trouvés. Suppression..."); | |||||
| // Si des points existent, on utilise le même filtre pour les supprimer | |||||
| await _client.DeleteAsync( | |||||
| collectionName, | |||||
| filter | |||||
| ); | |||||
| LoggerService.LogDebug("Documents existants supprimés avec succès."); | |||||
| } | |||||
| } | |||||
| public async Task IngestDocument(Domain domain, float[] vector, string nomFichier, string nomChunck, string content, string emailObject = "", string proprietaire = "", string niveauAcces="") | |||||
| { | |||||
| var pointId = new PointId { Uuid = Guid.NewGuid().ToString() }; | |||||
| var collectionName = domain.ToCollectionName(); | |||||
| var vectorObj = new Qdrant.Client.Grpc.Vector(); | |||||
| vectorObj.Data.Add(vector); | |||||
| /* | |||||
| // 1. Définir le filtre pour trouver les points avec le même `nom_fichier` | |||||
| var filter = new Qdrant.Client.Grpc.Filter | |||||
| { | |||||
| Must = | |||||
| { | |||||
| new Condition | |||||
| { | |||||
| Field = new FieldCondition | |||||
| { | |||||
| Key = "nom_fichier", | |||||
| Match = new Match { Text = nomFichier } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 2. Chercher les points existants avec ce filtre | |||||
| var searchResult = await _client.ScrollAsync( | |||||
| collectionName, | |||||
| filter: filter, | |||||
| limit: 1 // On n'a besoin de vérifier qu'un seul résultat | |||||
| ); | |||||
| // 3. Vérifier si des points ont été trouvés | |||||
| if (searchResult.Result.Count > 0) | |||||
| { | |||||
| LoggerService.LogDebug($"Un ou plusieurs documents avec le nom de fichier '{nomFichier}' ont été trouvés. Suppression..."); | |||||
| // Si des points existent, on utilise le même filtre pour les supprimer | |||||
| await _client.DeleteAsync( | |||||
| collectionName, | |||||
| filter | |||||
| ); | |||||
| LoggerService.LogDebug("Documents existants supprimés avec succès."); | |||||
| } | |||||
| */ | |||||
| await _client.UpsertAsync(collectionName, new[] | |||||
| { | |||||
| new PointStruct | |||||
| { | |||||
| Id = pointId, | |||||
| Vectors = new Vectors { Vector = vectorObj }, | |||||
| Payload = | |||||
| { | |||||
| ["categorie"] = collectionName, | |||||
| ["nom_fichier"] = nomFichier, | |||||
| ["email_object"] = emailObject, | |||||
| ["nom_chunck"] = nomChunck, | |||||
| ["niveau_acces"] = niveauAcces, | |||||
| ["content"] = content, | |||||
| ["proprietaire"] = proprietaire | |||||
| } | |||||
| } | |||||
| }); | |||||
| } | |||||
| //public async Task<IReadOnlyList<ScoredPoint>> SearchAsync(Domain domain, float[] queryVector, float seuilScore, int topK = 5, string adresseMail = "") | |||||
| public async Task<List<SearchResult>> SearchAsync(Domain domain, float[] queryVector, float seuilScore, int topK=5, string adresseMail="") | |||||
| { | |||||
| var collectionName = domain.ToCollectionName(); | |||||
| Filter filter = new(); | |||||
| if (domain == Domain.Emails) | |||||
| { | |||||
| filter = new Qdrant.Client.Grpc.Filter | |||||
| { | |||||
| Must = { | |||||
| new Condition | |||||
| { | |||||
| Field = new FieldCondition | |||||
| { | |||||
| Key = "categorie", | |||||
| Match = new Match { Text = collectionName } | |||||
| } | |||||
| }, | |||||
| new Condition | |||||
| { | |||||
| Field = new FieldCondition | |||||
| { | |||||
| Key = "proprietaire", | |||||
| Match = new Match { Text = adresseMail } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| } | |||||
| else | |||||
| { | |||||
| filter = new Qdrant.Client.Grpc.Filter | |||||
| { | |||||
| Must = { new Condition | |||||
| { | |||||
| Field = new FieldCondition | |||||
| { | |||||
| Key = "categorie", | |||||
| Match = new Match { Text = collectionName } | |||||
| } | |||||
| }} | |||||
| }; | |||||
| } | |||||
| var rep = await _client.SearchAsync(collectionName, queryVector, filter, null, (ulong)topK); | |||||
| var filteredChunks = rep | |||||
| .Where(r => r.Score >= seuilScore) | |||||
| .ToArray(); | |||||
| // | |||||
| var resultList = filteredChunks.Select(r => new SearchResult | |||||
| { | |||||
| Text = r.Payload["content"].StringValue, | |||||
| Nom_Fichier = r.Payload["nom_fichier"].StringValue | |||||
| }).ToList(); | |||||
| return resultList; | |||||
| //return filteredChunks; | |||||
| } | |||||
| #endregion | |||||
| #region Méthodes privées | |||||
| private async Task EnsureCollectionAsync(Domain domain) | |||||
| { | |||||
| var collectionName = domain.ToCollectionName(); | |||||
| var existing = await _client.ListCollectionsAsync(); | |||||
| if (!existing.Contains(collectionName)) | |||||
| { | |||||
| await _client.CreateCollectionAsync(collectionName, new VectorParams | |||||
| { | |||||
| Size = (ulong)EmbeddingDim, | |||||
| Distance = DefaultDistance | |||||
| }); | |||||
| } | |||||
| } | |||||
| private async Task DeleteCollectionAsync() | |||||
| { | |||||
| var collections = await _client.ListCollectionsAsync(); | |||||
| foreach (var col in collections) | |||||
| { | |||||
| await DeleteCollectionAsync(col); | |||||
| } | |||||
| } | |||||
| private async Task DeleteCollectionAsync(Domain domain) | |||||
| { | |||||
| var collectionName = domain.ToCollectionName(); | |||||
| await DeleteCollectionAsync(collectionName); | |||||
| } | |||||
| private async Task DeleteCollectionAsync(string collectionName) | |||||
| { | |||||
| await _client.DeleteCollectionAsync(collectionName); | |||||
| } | |||||
| #endregion | |||||
| } | |||||
| #endregion |