| @@ -0,0 +1,68 @@ | |||
| ## 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 | |||
| @@ -0,0 +1,16 @@ | |||
| <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> | |||
| @@ -0,0 +1,316 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,346 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,27 @@ | |||
| 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 ""; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| 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(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <PropertyGroup> | |||
| <TargetFramework>net8.0</TargetFramework> | |||
| <ImplicitUsings>enable</ImplicitUsings> | |||
| <Nullable>enable</Nullable> | |||
| </PropertyGroup> | |||
| </Project> | |||
| @@ -0,0 +1,14 @@ | |||
| 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; } = ""; | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| <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> | |||
| @@ -0,0 +1,235 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,52 @@ | |||
| <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> | |||
| @@ -0,0 +1,245 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,157 @@ | |||
| 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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,127 @@ | |||
| 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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1 @@ | |||
| Mettre ici les exe et dll de ffmpeg | |||
| @@ -0,0 +1,21 @@ | |||
| <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> | |||
| @@ -0,0 +1,544 @@ | |||
| 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 | |||
| } | |||
| @@ -0,0 +1,36 @@ | |||
| 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 | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,123 @@ | |||
| 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}"; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,179 @@ | |||
| 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 | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| <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> | |||
| @@ -0,0 +1,36 @@ | |||
| <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> | |||
| @@ -0,0 +1,529 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| Fichiers a mettre dans ce répertoire : | |||
| - face_detection_yunet_2023mar.onnx | |||
| - haarcascade_frontalface_alt.xml | |||
| - haarcascade_frontalface_default.xml | |||
| - model_reconnaissance.yml | |||
| @@ -0,0 +1,8 @@ | |||
| namespace FaceRecognition | |||
| { | |||
| public class ParametresFaceRecognitionService | |||
| { | |||
| public string Path_TrainSet { get; set; } = ""; | |||
| public string Path_TestSet { get; set; } = ""; | |||
| } | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| <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> | |||
| @@ -0,0 +1,241 @@ | |||
| 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; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,346 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| <?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> | |||
| @@ -0,0 +1,11 @@ | |||
| namespace MaUI; | |||
| public partial class App : Application | |||
| { | |||
| public App() | |||
| { | |||
| InitializeComponent(); | |||
| MainPage = new AppShell(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| <?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> | |||
| @@ -0,0 +1,9 @@ | |||
| namespace MaUI; | |||
| public partial class AppShell : Shell | |||
| { | |||
| public AppShell() | |||
| { | |||
| InitializeComponent(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,84 @@ | |||
| <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> | |||
| @@ -0,0 +1,8 @@ | |||
| <?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> | |||
| @@ -0,0 +1,15 @@ | |||
| <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> | |||
| @@ -0,0 +1,48 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| 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(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,22 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,46 @@ | |||
| <?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> | |||
| @@ -0,0 +1,13 @@ | |||
| namespace MaUI; | |||
| public partial class AuthLogin : ContentPage | |||
| { | |||
| AuthLogin_VM _VM; | |||
| public AuthLogin() | |||
| { | |||
| InitializeComponent(); | |||
| _VM = new AuthLogin_VM(); | |||
| BindingContext = _VM; | |||
| } | |||
| } | |||
| @@ -0,0 +1,83 @@ | |||
| <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> | |||
| @@ -0,0 +1,18 @@ | |||
| namespace MaUI; | |||
| public partial class ChatRoomPage : ContentPage | |||
| { | |||
| ChatRoom_VM _VM; | |||
| public ChatRoomPage() | |||
| { | |||
| InitializeComponent(); | |||
| _VM = new ChatRoom_VM(); | |||
| BindingContext = _VM; | |||
| } | |||
| } | |||
| @@ -0,0 +1,30 @@ | |||
| <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> | |||
| @@ -0,0 +1,15 @@ | |||
| namespace MaUI; | |||
| public partial class ListeEmails : ContentPage | |||
| { | |||
| ListeEmails_VM _VM; | |||
| public ListeEmails() | |||
| { | |||
| InitializeComponent(); | |||
| _VM = new ListeEmails_VM(); | |||
| BindingContext = _VM; | |||
| } | |||
| } | |||
| @@ -0,0 +1,6 @@ | |||
| <?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> | |||
| @@ -0,0 +1,10 @@ | |||
| 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 | |||
| { | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| 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(); | |||
| } | |||
| @@ -0,0 +1,6 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <resources> | |||
| <color name="colorPrimary">#512BD4</color> | |||
| <color name="colorPrimaryDark">#2B0B98</color> | |||
| <color name="colorAccent">#2B0B98</color> | |||
| </resources> | |||
| @@ -0,0 +1,9 @@ | |||
| using Foundation; | |||
| namespace MaUI; | |||
| [Register("AppDelegate")] | |||
| public class AppDelegate : MauiUIApplicationDelegate | |||
| { | |||
| protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| <?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> | |||
| @@ -0,0 +1,38 @@ | |||
| <?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> | |||
| @@ -0,0 +1,15 @@ | |||
| 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)); | |||
| } | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| 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); | |||
| } | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| <?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> | |||
| @@ -0,0 +1,8 @@ | |||
| <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> | |||
| @@ -0,0 +1,24 @@ | |||
| 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(); | |||
| } | |||
| @@ -0,0 +1,46 @@ | |||
| <?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> | |||
| @@ -0,0 +1,15 @@ | |||
| <?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> | |||
| @@ -0,0 +1,9 @@ | |||
| using Foundation; | |||
| namespace MaUI; | |||
| [Register("AppDelegate")] | |||
| public class AppDelegate : MauiUIApplicationDelegate | |||
| { | |||
| protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); | |||
| } | |||
| @@ -0,0 +1,32 @@ | |||
| <?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> | |||
| @@ -0,0 +1,15 @@ | |||
| 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)); | |||
| } | |||
| } | |||
| @@ -0,0 +1,51 @@ | |||
| <?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> | |||
| @@ -0,0 +1,8 @@ | |||
| { | |||
| "profiles": { | |||
| "Windows Machine": { | |||
| "commandName": "MsixPackage", | |||
| "nativeDebugging": false | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,4 @@ | |||
| <?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> | |||
| @@ -0,0 +1,8 @@ | |||
| <?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> | |||
| @@ -0,0 +1,15 @@ | |||
| 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(); | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| <?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> | |||
| @@ -0,0 +1,45 @@ | |||
| <?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> | |||
| @@ -0,0 +1,427 @@ | |||
| <?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> | |||
| @@ -0,0 +1,180 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,122 @@ | |||
| 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 | |||
| @@ -0,0 +1,160 @@ | |||
| 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; } = ""; | |||
| } | |||
| @@ -0,0 +1,167 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,46 @@ | |||
| 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; } = ""; | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
| <PropertyGroup> | |||
| <TargetFramework>net8.0</TargetFramework> | |||
| <ImplicitUsings>enable</ImplicitUsings> | |||
| <Nullable>enable</Nullable> | |||
| </PropertyGroup> | |||
| </Project> | |||
| @@ -0,0 +1,35 @@ | |||
| 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; } = ""; | |||
| } | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| <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> | |||
| @@ -0,0 +1,56 @@ | |||
| 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); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,167 @@ | |||
| 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 | |||
| } | |||
| @@ -0,0 +1,721 @@ | |||
| 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 | |||
| } | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| using System.Text.Json.Serialization; | |||
| using Services.Models; | |||
| namespace Services.Ollama | |||
| { | |||
| public class OllamaTagsResponse | |||
| { | |||
| [JsonPropertyName("models")] | |||
| public List<OllamaModel>? Models { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| 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; | |||
| } | |||
| } | |||
| @@ -0,0 +1,31 @@ | |||
| <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> | |||
| @@ -0,0 +1,8 @@ | |||
| namespace Services | |||
| { | |||
| public class ParametresQdrantService | |||
| { | |||
| public string Qdrant_URL { get; set; } = "localhost:6334"; | |||
| public bool Qdrant_IsHttps { get; set; } = false; | |||
| } | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| namespace Services | |||
| { | |||
| public class ParametresRAGService | |||
| { | |||
| public string RAG_Path_ToVectoralize { get; set; } = ""; | |||
| public string RAG_Path_Vectoralized { get; set; } = ""; | |||
| } | |||
| } | |||
| @@ -0,0 +1,449 @@ | |||
| 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 | |||