using ManagerService.Data; using ManagerService.DTOs; using Microsoft.Extensions.AI; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using System; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Manager.DTOs; using ManagerService.Data.SubSection; namespace ManagerService.Services { public class AssistantService { private readonly IChatClient _chatClient; private readonly MyInfoMateDbContext _context; private const int MaxHistoryMessages = 10; public AssistantService(IChatClient chatClient, MyInfoMateDbContext context) { _chatClient = chatClient; _context = context; } private static string StripHtml(string? html) => string.IsNullOrEmpty(html) ? "" : System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", "").Trim(); private static DateTime? ParseFilterDate(string? s) { if (string.IsNullOrEmpty(s)) return null; // Priorité au format ISO yyyy-MM-dd (language-neutral), puis fallbacks if (DateTime.TryParseExact(s, new[] { "yyyy-MM-dd", "dd/MM/yyyy", "MM/dd/yyyy", "dd-MM-yyyy" }, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dt1)) return dt1; if (DateTime.TryParse(s, out var dt2)) return dt2; return null; } private static (DateTime saturday, DateTime sunday) GetCurrentWeekend() { var now = DateTime.Now.Date; // Si on est dimanche, le weekend en cours a commencé hier if (now.DayOfWeek == DayOfWeek.Sunday) return (now.AddDays(-1), now); var daysToSat = ((int)DayOfWeek.Saturday - (int)now.DayOfWeek + 7) % 7; var sat = now.AddDays(daysToSat); return (sat, sat.AddDays(1)); } public async Task ChatAsync(AiChatRequest request) { var messages = new List(); ChatOptions options; var (weekendSat, weekendSun) = GetCurrentWeekend(); if (request.ConfigurationId != null) { // Scope configuration : l'IA connaît uniquement les sections de cette configuration var sections = _context.Sections.Where(s => s.ConfigurationId == request.ConfigurationId).ToList(); var config = _context.Configurations.FirstOrDefault(c => c.Id == request.ConfigurationId); var sectionsSummary = string.Join("\n", sections.Select(s => { var title = StripHtml(s.Title?.FirstOrDefault(t => t.language == request.Language)?.value ?? s.Title?.FirstOrDefault()?.value ?? s.Label); return $"- id:{s.Id} | type:{s.Type} | titre:\"{title}\""; })); messages.Add(new ChatMessage(ChatRole.System, $""" Tu es l'assistant de visite de l'application "{config?.Label ?? "cette application"}". Aujourd'hui nous sommes le {DateTime.Now:dddd dd MMMM yyyy}. Le weekend en cours (ou prochain) : samedi {weekendSat:dd/MM/yyyy} – dimanche {weekendSun:dd/MM/yyyy}. Ton but est d'accompagner le visiteur dans son expérience. Tu réponds en {request.Language}. Tu es chaleureux, concis et proactif. Voici les sections disponibles : {sectionsSummary} RÈGLES STRICTES — tu dois les respecter sans exception : 1. Tu ne réponds JAMAIS "je ne peux pas" ou "je n'ai pas accès" si un outil existe pour répondre. Tu appelles l'outil, tu reçois les données, TU les présentes. 2. Toute question sur des événements, le planning, les dates, les activités → appelle "GetUpcomingEvents" IMMÉDIATEMENT. Si l'utilisateur précise une période, calcule les dates et passe-les en paramètres dateFrom/dateTo au format ISO yyyy-MM-dd. Exemples : "ce weekend" → dateFrom={weekendSat:yyyy-MM-dd}&dateTo={weekendSun:yyyy-MM-dd} ; "la semaine prochaine" → lundi au dimanche suivant. 3. Pour présenter une liste d'événements, appelle TOUJOURS "show_cards" avec les titres et dates — ne liste jamais les événements en texte brut. 4. Après avoir présenté des résultats d'événements, appelle TOUJOURS "navigate_to_section" avec l'ID de la section agenda trouvée dans la liste ci-dessus. 5. Si l'utilisateur veut voir les activités ou lieux → utilise "GetMapPoints". 6. Pour les détails d'un item spécifique → utilise "GetItemDetails". - Ne mentionne JAMAIS les identifiants techniques (id, guid) dans tes réponses finales. - NE POSE JAMAIS de question à la fin de ta réponse après avoir présenté des résultats. - NE TE RÉPÈTE PAS : dis les choses une seule fois de manière fluide. """)); foreach (var h in request.History.TakeLast(MaxHistoryMessages)) messages.Add(new ChatMessage(h.Role == "user" ? ChatRole.User : ChatRole.Assistant, h.Content)); messages.Add(new ChatMessage(ChatRole.User, request.Message)); NavigationActionDTO? navigation = null; List? cards = null; var tools = new List { AIFunctionFactory.Create( (string sectionId) => { var section = sections.FirstOrDefault(s => s.Id == sectionId); if (section == null) return "Section non trouvée."; var title = section.Title?.FirstOrDefault(t => t.language == request.Language)?.value ?? section.Label; var desc = section.Description?.FirstOrDefault(t => t.language == request.Language)?.value ?? ""; return $"Titre: {title}\nDescription: {desc}\nType: {section.Type}"; }, "GetSectionDetail", "Récupère le titre et la description d'une section." ), AIFunctionFactory.Create( async (string? dateFrom, string? dateTo) => { var today = DateTime.UtcNow.Date; var horizon = today.AddMonths(3); var filterFrom = ParseFilterDate(dateFrom); var filterTo = ParseFilterDate(dateTo); var agendaSections = _context.Sections .OfType() .Where(s => s.ConfigurationId == request.ConfigurationId) .ToList(); var allEvents = new List<(DateTime sort, string line)>(); foreach (var agenda in agendaSections) { try { if (agenda.IsOnlineAgenda) { var resourceId = agenda.AgendaResourceIds?.FirstOrDefault(r => r.language == request.Language)?.value ?? agenda.AgendaResourceIds?.FirstOrDefault()?.value; if (resourceId != null) { var resource = _context.Resources.FirstOrDefault(r => r.Id == resourceId); if (resource?.Url != null) { using var client = new System.Net.Http.HttpClient(); var json = await client.GetStringAsync(resource.Url); var remoteEvents = Newtonsoft.Json.JsonConvert.DeserializeObject>(json, RemoteAgendaJsonSettings.Lenient); if (remoteEvents != null) { foreach (var e in remoteEvents) { var dtFrom = e.GetDateFrom(); var dtTo = e.GetDateTo(); var end = dtTo ?? dtFrom; if (end == null || end.Value.Date < today || dtFrom?.Date > horizon) continue; if (filterFrom.HasValue && end.Value.Date < filterFrom.Value) continue; if (filterTo.HasValue && dtFrom?.Date > filterTo.Value) continue; var dateStr = dtFrom.HasValue ? dtFrom.Value.ToString("dd/MM/yyyy") : "Date non précisée"; if (dtTo.HasValue && dtTo.Value.Date != dtFrom?.Date) dateStr += $" → {dtTo.Value.ToString("dd/MM/yyyy")}"; allEvents.Add((dtFrom ?? DateTime.MaxValue, $"- [En ligne] AgendaId:{agenda.Id} | Titre:{e.name ?? "Sans titre"} | Date:{dateStr}")); } } } } } else { var qFrom = filterFrom ?? today; var qTo = filterTo ?? horizon; var events = _context.EventAgendas .Where(e => e.SectionAgendaId == agenda.Id && (e.DateTo ?? e.DateFrom) >= qFrom && e.DateFrom <= qTo) .OrderBy(e => e.DateFrom) .ToList(); foreach (var e in events) { var title = e.Label?.FirstOrDefault(t => t.language == request.Language)?.value ?? e.Label?.FirstOrDefault()?.value ?? "Sans titre"; var dateStr = e.DateFrom.HasValue ? e.DateFrom.Value.ToString("dd/MM/yyyy") : "Date non précisée"; if (e.DateTo.HasValue && e.DateTo.Value.Date != e.DateFrom?.Date) dateStr += $" → {e.DateTo.Value.ToString("dd/MM/yyyy")}"; allEvents.Add((e.DateFrom ?? DateTime.MaxValue, $"- SectionId:{agenda.Id} | EventId:{e.Id} | Titre:{title} | Date:{dateStr}")); } } } catch (Exception ex) { allEvents.Add((DateTime.MaxValue, $"- [ERREUR agenda {agenda.Label}] {ex.Message}")); } } if (!allEvents.Any()) { var period = filterFrom.HasValue ? $" du {filterFrom.Value:dd/MM/yyyy} au {(filterTo ?? filterFrom.Value):dd/MM/yyyy}" : " dans les 3 prochains mois"; return $"Aucun événement à venir{period}."; } var lines = allEvents.OrderBy(x => x.sort).Take(50).Select(x => x.line); return string.Join("\n", lines); }, "GetUpcomingEvents", "Retourne les événements à venir (max 50, triés par date). Paramètres optionnels dateFrom et dateTo au format dd/MM/yyyy pour filtrer côté serveur (ex: ce weekend → dateFrom=21/03/2026&dateTo=22/03/2026, la semaine prochaine → lundi au dimanche suivant)." ), AIFunctionFactory.Create( () => { var points = _context.Sections .OfType() .Where(s => s.ConfigurationId == request.ConfigurationId) .SelectMany(s => s.MapPoints) .ToList(); if (!points.Any()) return "Aucun point d'intérêt trouvé."; return string.Join("\n", points.Select(p => { var title = p.Title?.FirstOrDefault(t => t.language == request.Language)?.value ?? p.Title?.FirstOrDefault()?.value ?? "Sans titre"; return $"- ID:{p.Id} | Titre:{title}"; })); }, "GetMapPoints", "Liste tous les points d'intérêt (musées, monuments, activités) disponibles sur la carte." ), AIFunctionFactory.Create( async (string type, string id) => { if (id.StartsWith("remote:")) { // Format: remote:[agendaId]:[eventId] var parts = id.Split(':'); if (parts.Length < 3) return "Format d'ID distant invalide."; var agendaId = parts[1]; var eventId = parts[2]; var agenda = _context.Sections.OfType().FirstOrDefault(s => s.Id == agendaId); if (agenda == null) return "Agenda non trouvé."; var resourceId = agenda.AgendaResourceIds?.FirstOrDefault(r => r.language == request.Language)?.value ?? agenda.AgendaResourceIds?.FirstOrDefault()?.value; if (resourceId == null) return "Ressource de l'agenda non trouvée."; var resource = _context.Resources.FirstOrDefault(r => r.Id == resourceId); if (resource?.Url == null) return "URL de l'agenda non trouvée."; try { using var client = new System.Net.Http.HttpClient(); var json = await client.GetStringAsync(resource.Url); var remoteEvents = Newtonsoft.Json.JsonConvert.DeserializeObject>(json, RemoteAgendaJsonSettings.Lenient); var e = remoteEvents?.FirstOrDefault(ev => ev.name == eventId); if (e == null) return "Événement distant non trouvé."; var title = e.name ?? "Sans titre"; var desc = e.description ?? ""; var dtStart = e.GetDateFrom(); var dtEnd = e.GetDateTo(); var dateStr = dtStart.HasValue ? $"Du {dtStart.Value.ToString("dd/MM/yyyy HH:mm")}" : ""; dateStr += dtEnd.HasValue ? $" au {dtEnd.Value.ToString("dd/MM/yyyy HH:mm")}" : ""; return $"[ÉVÉNEMENT (EN LIGNE)]\nTitre: {title}\nDate: {dateStr}\nDescription: {desc}\nContact: {e.email} / {e.phone}\nSite: {e.website}"; } catch (Exception ex) { return $"Erreur lors de la récupération des détails : {ex.Message}"; } } if (type.ToLower() == "event") { int.TryParse(id, out var intId); var e = _context.EventAgendas.FirstOrDefault(ev => ev.Id == intId); if (e == null) return "Événement non trouvé."; var title = e.Label?.FirstOrDefault(t => t.language == request.Language)?.value ?? "Sans titre"; var desc = e.Description?.FirstOrDefault(t => t.language == request.Language)?.value ?? ""; var dateStr = e.DateFrom.HasValue ? $"Du {e.DateFrom.Value.ToString("dd/MM/yyyy HH:mm")}" : ""; dateStr += e.DateTo.HasValue ? $" au {e.DateTo.Value.ToString("dd/MM/yyyy HH:mm")}" : ""; return $"[ÉVÉNEMENT]\nTitre: {title}\nDate: {dateStr}\nDescription: {desc}\nContact: {e.Email} / {e.Phone}\nSite: {e.Website}"; } else { int.TryParse(id, out var intId); var p = _context.Sections.OfType() .SelectMany(s => s.MapPoints) .FirstOrDefault(pt => pt.Id == intId); if (p == null) return "Point d'intérêt non trouvé."; var title = p.Title?.FirstOrDefault(t => t.language == request.Language)?.value ?? "Sans titre"; var desc = p.Description?.FirstOrDefault(t => t.language == request.Language)?.value ?? ""; var prices = p.Prices?.FirstOrDefault(t => t.language == request.Language)?.value ?? "Non spécifié"; var schedules = p.Schedules?.FirstOrDefault(t => t.language == request.Language)?.value ?? "Non spécifié"; var contact = $"{p.Email?.FirstOrDefault()?.value} / {p.Phone?.FirstOrDefault()?.value}"; var site = p.Site?.FirstOrDefault()?.value; return $"[LIEU/ACTIVITÉ]\nTitre: {title}\nDescription: {desc}\nPrix: {prices}\nHoraires: {schedules}\nContact: {contact}\nSite: {site}"; } }, "GetItemDetails", "Récupère les détails complets (prix, horaires, contact, site) d'un événement (type='event') ou d'un point d'intérêt (type='poi') via son ID." ), AIFunctionFactory.Create( (string sectionId, string sectionTitle, string sectionType) => { var imageUrl = sections.FirstOrDefault(s => s.Id == sectionId)?.ImageSource; navigation = new NavigationActionDTO { SectionId = sectionId, SectionTitle = sectionTitle, SectionType = sectionType, ImageUrl = imageUrl }; return Task.FromResult("Navigation proposée à l'utilisateur."); }, "navigate_to_section", "Propose navigation to a specific section when the user wants to go there or see it." ), AIFunctionFactory.Create( (string[] titles, string[] subtitles, string[]? icons) => { cards = titles.Select((t, i) => new AiCardDTO { Title = t, Subtitle = i < subtitles.Length ? subtitles[i] : "", Icon = icons != null && i < icons.Length ? icons[i] : null }).ToList(); return Task.FromResult("Cartes affichées."); }, "show_cards", "Display structured info cards in the chat. Use for lists of items (events, activities...) that benefit from visual presentation." ) }; try { options = new ChatOptions { Tools = tools }; var response = await _chatClient.GetResponseAsync(messages, options); return new AiChatResponse { Reply = response.Text ?? "", Cards = cards, Navigation = navigation }; } catch (System.ClientModel.ClientResultException ex) { // Log plus de détails si possible sur l'erreur 400 de Google/OpenAI throw new System.Exception($"AI Service Error (Status: {ex.Status}): {ex.Message}", ex); } } else { // Scope instance : l'IA connaît les configurations disponibles pour cette application spécifiquement (AppType) var configurations = _context.AppConfigurationLinks .Where(l => l.ApplicationInstance.InstanceId == request.InstanceId && l.ApplicationInstance.AppType == request.AppType && l.IsActive) .Select(l => l.Configuration) .ToList(); var configIds = configurations.Select(c => c.Id).ToList(); // Récupère les types de sections par configuration pour aider l'IA à choisir var typesByConfig = _context.Sections .Where(s => configIds.Contains(s.ConfigurationId)) .Select(s => new { s.ConfigurationId, s.Type }) .ToList() .GroupBy(s => s.ConfigurationId) .ToDictionary(g => g.Key, g => g.Select(s => s.Type).Distinct().ToList()); var configsSummary = string.Join("\n", configurations.Select(c => { var title = StripHtml(c.Title?.FirstOrDefault(t => t.language == request.Language)?.value ?? c.Title?.FirstOrDefault()?.value ?? c.Label); typesByConfig.TryGetValue(c.Id, out var types); var typesStr = types != null && types.Any() ? $" | types disponibles: {string.Join(", ", types)}" : ""; return $"- id:{c.Id} | titre:\"{title}\"{typesStr}"; })); messages.Add(new ChatMessage(ChatRole.System, $""" Tu es l'assistant de visite principal. Ton rôle est d'orienter l'utilisateur vers la bonne expérience de visite. Aujourd'hui nous sommes le {DateTime.Now:dddd dd MMMM yyyy}. Le weekend en cours (ou prochain) : samedi {weekendSat:dd/MM/yyyy} – dimanche {weekendSun:dd/MM/yyyy}. Tu réponds en {request.Language}. Tu es chaleureux, concis et proactif. Voici les expériences (configurations) disponibles : {configsSummary} RÈGLES STRICTES — tu dois les respecter sans exception : 1. Tu ne réponds JAMAIS "je ne peux pas" ou "je n'ai pas accès" si un outil existe pour répondre. Tu appelles l'outil, tu reçois les données, TU les présentes. 2. Toute question sur des événements, le planning, les dates → appelle "SearchEventsGlobal" IMMÉDIATEMENT. Si l'utilisateur précise une période, calcule les dates et passe-les en paramètres dateFrom/dateTo au format ISO yyyy-MM-dd. Exemples : "ce weekend" → dateFrom={weekendSat:yyyy-MM-dd}&dateTo={weekendSun:yyyy-MM-dd} ; "la semaine prochaine" → lundi au dimanche suivant. 3. Pour présenter une liste d'événements, appelle TOUJOURS "show_cards" avec les titres et dates — ne liste jamais les événements en texte brut. 4. Après avoir présenté des résultats d'événements, appelle TOUJOURS "navigate_to_configuration" avec l'ID de la visite concernée pour que l'utilisateur puisse y accéder directement. 5. Pour orienter vers une visite spécifique (jeu, quiz...) → "navigate_to_configuration". 6. Pour les détails d'un item → "GetItemDetailsGlobal". - NE POSE JAMAIS de question à la fin de ta réponse après avoir présenté des résultats. - NE TE RÉPÈTE PAS : dis les choses une seule fois de manière fluide. """)); foreach (var h in request.History.TakeLast(MaxHistoryMessages)) messages.Add(new ChatMessage(h.Role == "user" ? ChatRole.User : ChatRole.Assistant, h.Content)); messages.Add(new ChatMessage(ChatRole.User, request.Message)); NavigationActionDTO? navigation = null; var tools = new List { AIFunctionFactory.Create( async (string? dateFrom, string? dateTo) => { var today = DateTime.UtcNow.Date; var horizon = today.AddMonths(3); var filterFrom = ParseFilterDate(dateFrom); var filterTo = ParseFilterDate(dateTo); var agendaSections = _context.Sections .OfType() .Where(s => configIds.Contains(s.ConfigurationId)) .ToList(); var allEvents = new List<(DateTime sort, string line)>(); foreach (var agenda in agendaSections) { try { if (agenda.IsOnlineAgenda) { var resourceId = agenda.AgendaResourceIds?.FirstOrDefault(r => r.language == request.Language)?.value ?? agenda.AgendaResourceIds?.FirstOrDefault()?.value; if (resourceId != null) { var resource = _context.Resources.FirstOrDefault(r => r.Id == resourceId); if (resource?.Url != null) { using var client = new System.Net.Http.HttpClient(); var json = await client.GetStringAsync(resource.Url); var remoteEvents = Newtonsoft.Json.JsonConvert.DeserializeObject>(json, RemoteAgendaJsonSettings.Lenient); if (remoteEvents != null) { var config = configurations.FirstOrDefault(c => c.Id == agenda.ConfigurationId); var configTitle = StripHtml(config?.Title?.FirstOrDefault(t => t.language == request.Language)?.value ?? config?.Label ?? "Inconnue"); foreach (var e in remoteEvents) { var dtFrom = e.GetDateFrom(); var dtTo = e.GetDateTo(); var end = dtTo ?? dtFrom; if (end == null || end.Value.Date < today || dtFrom?.Date > horizon) continue; if (filterFrom.HasValue && end.Value.Date < filterFrom.Value) continue; if (filterTo.HasValue && dtFrom?.Date > filterTo.Value) continue; var dateStr = dtFrom.HasValue ? dtFrom.Value.ToString("dd/MM/yyyy") : "Date non précisée"; if (dtTo.HasValue && dtTo.Value.Date != dtFrom?.Date) dateStr += $" → {dtTo.Value.ToString("dd/MM/yyyy")}"; allEvents.Add((dtFrom ?? DateTime.MaxValue, $"- [Visite: {configTitle}] ConfigId:{agenda.ConfigurationId} | Titre:{e.name ?? "Sans titre"} | Date:{dateStr}")); } } } } } else { var qFrom = filterFrom ?? today; var qTo = filterTo ?? horizon; var events = _context.EventAgendas .Where(e => configIds.Contains(e.SectionAgenda.ConfigurationId) && (e.DateTo ?? e.DateFrom) >= qFrom && e.DateFrom <= qTo) .OrderBy(e => e.DateFrom) .ToList(); foreach (var e in events) { var config = configurations.FirstOrDefault(c => c.Id == e.SectionAgenda.ConfigurationId); var configTitle = StripHtml(config?.Title?.FirstOrDefault(t => t.language == request.Language)?.value ?? config?.Label ?? "Inconnue"); var title = e.Label?.FirstOrDefault(t => t.language == request.Language)?.value ?? e.Label?.FirstOrDefault()?.value ?? "Sans titre"; var dateStr = e.DateFrom.HasValue ? e.DateFrom.Value.ToString("dd/MM/yyyy") : "Date non précisée"; if (e.DateTo.HasValue && e.DateTo.Value.Date != e.DateFrom?.Date) dateStr += $" → {e.DateTo.Value.ToString("dd/MM/yyyy")}"; allEvents.Add((e.DateFrom ?? DateTime.MaxValue, $"- [Visite: {configTitle}] ConfigId:{e.SectionAgenda.ConfigurationId} | EventId:{e.Id} | Titre:{title} | Date:{dateStr}")); } } } catch (Exception ex) { allEvents.Add((DateTime.MaxValue, $"- [ERREUR agenda {agenda.Label}] {ex.Message}")); } } if (!allEvents.Any()) { var period = filterFrom.HasValue ? $" du {filterFrom.Value:dd/MM/yyyy} au {(filterTo ?? filterFrom.Value):dd/MM/yyyy}" : " dans les 3 prochains mois"; return $"Aucun événement à venir{period}."; } var lines = allEvents.OrderBy(x => x.sort).Take(50).Select(x => x.line); return string.Join("\n", lines); }, "SearchEventsGlobal", "Retourne les événements à venir (max 50, triés par date). Paramètres optionnels dateFrom et dateTo au format dd/MM/yyyy pour filtrer côté serveur (ex: ce weekend → dateFrom=21/03/2026&dateTo=22/03/2026, la semaine prochaine → lundi au dimanche suivant)." ), AIFunctionFactory.Create( () => { var points = _context.Sections .OfType() .Where(s => configIds.Contains(s.ConfigurationId)) .SelectMany(s => s.MapPoints) .ToList(); if (!points.Any()) return "Aucun point d'intérêt trouvé."; return string.Join("\n", points.Select(p => { var title = p.Title?.FirstOrDefault(t => t.language == request.Language)?.value ?? p.Title?.FirstOrDefault()?.value ?? "Sans titre"; return $"- ID:{p.Id} | Titre:{title}"; })); }, "GetMapPointsGlobal", "Liste tous les points d'intérêt disponibles sur les cartes autorisées." ), AIFunctionFactory.Create( async (string type, string id) => { if (id.StartsWith("remote:")) { // Format: remote:[agendaId]:[eventId] var parts = id.Split(':'); if (parts.Length < 3) return "Format d'ID distant invalide."; var agendaId = parts[1]; var eventId = parts[2]; var agenda = _context.Sections.OfType().FirstOrDefault(s => s.Id == agendaId && configIds.Contains(s.ConfigurationId)); if (agenda == null) return "Agenda non trouvé ou non autorisé."; var resourceId = agenda.AgendaResourceIds?.FirstOrDefault(r => r.language == request.Language)?.value ?? agenda.AgendaResourceIds?.FirstOrDefault()?.value; if (resourceId == null) return "Ressource de l'agenda non trouvée."; var resource = _context.Resources.FirstOrDefault(r => r.Id == resourceId); if (resource?.Url == null) return "URL de l'agenda non trouvée."; try { using var client = new System.Net.Http.HttpClient(); var json = await client.GetStringAsync(resource.Url); var remoteEvents = Newtonsoft.Json.JsonConvert.DeserializeObject>(json, RemoteAgendaJsonSettings.Lenient); var e = remoteEvents?.FirstOrDefault(ev => ev.name == eventId); if (e == null) return "Événement distant non trouvé."; var title = e.name ?? "Sans titre"; var desc = e.description ?? ""; var dtStart = e.GetDateFrom(); var dtEnd = e.GetDateTo(); var dateStr = dtStart.HasValue ? $"Du {dtStart.Value.ToString("dd/MM/yyyy HH:mm")}" : ""; dateStr += dtEnd.HasValue ? $" au {dtEnd.Value.ToString("dd/MM/yyyy HH:mm")}" : ""; return $"[ÉVÉNEMENT (EN LIGNE)]\nTitre: {title}\nDate: {dateStr}\nDescription: {desc}\nContact: {e.email} / {e.phone}\nSite: {e.website}"; } catch (Exception ex) { return $"Erreur lors de la récupération des détails : {ex.Message}"; } } if (type.ToLower() == "event") { int.TryParse(id, out var intId); var e = _context.EventAgendas .Include(ev => ev.SectionAgenda) .FirstOrDefault(ev => ev.Id == intId && configIds.Contains(ev.SectionAgenda.ConfigurationId)); if (e == null) return "Événement non trouvé ou non autorisé."; var title = e.Label?.FirstOrDefault(t => t.language == request.Language)?.value ?? "Sans titre"; var desc = e.Description?.FirstOrDefault(t => t.language == request.Language)?.value ?? ""; var dateStr = e.DateFrom.HasValue ? $"Du {e.DateFrom.Value.ToString("dd/MM/yyyy HH:mm")}" : ""; dateStr += e.DateTo.HasValue ? $" au {e.DateTo.Value.ToString("dd/MM/yyyy HH:mm")}" : ""; return $"[ÉVÉNEMENT]\nTitre: {title}\nDate: {dateStr}\nDescription: {desc}\nContact: {e.Email} / {e.Phone}\nSite: {e.Website}"; } else { int.TryParse(id, out var intId); var p = _context.Sections.OfType() .Where(s => configIds.Contains(s.ConfigurationId)) .SelectMany(s => s.MapPoints) .FirstOrDefault(pt => pt.Id == intId); if (p == null) return "Point d'intérêt non trouvé ou non autorisé."; var title = p.Title?.FirstOrDefault(t => t.language == request.Language)?.value ?? "Sans titre"; var desc = p.Description?.FirstOrDefault(t => t.language == request.Language)?.value ?? ""; var prices = p.Prices?.FirstOrDefault(t => t.language == request.Language)?.value ?? "Non spécifié"; var schedules = p.Schedules?.FirstOrDefault(t => t.language == request.Language)?.value ?? "Non spécifié"; var contact = $"{p.Email?.FirstOrDefault()?.value} / {p.Phone?.FirstOrDefault()?.value}"; var site = p.Site?.FirstOrDefault()?.value; return $"[LIEU/ACTIVITÉ]\nTitre: {title}\nDescription: {desc}\nPrix: {prices}\nHoraires: {schedules}\nContact: {contact}\nSite: {site}"; } }, "GetItemDetailsGlobal", "Détails d'un lieu ou événement au niveau instance." ), AIFunctionFactory.Create( (string configurationId, string configurationTitle) => { var imageUrl = configurations.FirstOrDefault(c => c.Id == configurationId)?.ImageSource; navigation = new NavigationActionDTO { SectionId = configurationId, SectionTitle = configurationTitle, SectionType = "Configuration", ImageUrl = imageUrl }; return Task.FromResult("Navigation vers la visite proposée."); }, "navigate_to_configuration", "Propose navigation to a specific visit/configuration when the user wants to start or explore it." ) }; try { options = new ChatOptions { Tools = tools }; var response = await _chatClient.GetResponseAsync(messages, options); return new AiChatResponse { Reply = response.Text ?? "", Navigation = navigation }; } catch (System.ClientModel.ClientResultException ex) { // Log plus de détails si possible sur l'erreur 400 de Google/OpenAI throw new System.Exception($"AI Service Error (Status: {ex.Status}): {ex.Message}", ex); } } } } }