597 lines
40 KiB
C#
597 lines
40 KiB
C#
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<AiChatResponse> ChatAsync(AiChatRequest request)
|
||
{
|
||
var messages = new List<ChatMessage>();
|
||
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<AiCardDTO>? cards = null;
|
||
|
||
var tools = new List<AITool>
|
||
{
|
||
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<ManagerService.Data.SubSection.SectionAgenda>()
|
||
.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<List<RemoteEventAgendaDTO>>(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<ManagerService.Data.SubSection.SectionMap>()
|
||
.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<SectionAgenda>().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<List<RemoteEventAgendaDTO>>(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<ManagerService.Data.SubSection.SectionMap>()
|
||
.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<AITool>
|
||
{
|
||
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<ManagerService.Data.SubSection.SectionAgenda>()
|
||
.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<List<RemoteEventAgendaDTO>>(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<ManagerService.Data.SubSection.SectionMap>()
|
||
.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<SectionAgenda>().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<List<RemoteEventAgendaDTO>>(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<ManagerService.Data.SubSection.SectionMap>()
|
||
.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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|