manager-service/ManagerService/Services/AgendaSyncService.cs
Thomas Fransolet 18e4240f0f RAG: pipeline d'ingestion, endpoints du guide IA, journalisation RGPD
Ingestion et indexation
- IIngestionService/IngestionService : chargement des collections filles par
  sous-type, un jeu de morceaux par langue, ChunkIndex continu.
- SectionIndexingInterceptor retenu comme unique déclencheur : les 5
  sous-contrôleurs totalisaient 30 SaveChanges et 0 Enqueue, donc ajouter des
  points d'intérêt à une carte ne réindexait rien.
- HTML retiré avant l'embedding et lignes trop longues recoupées : sans cela un
  article dépassait l'entrée max du modèle et emportait son lot de 50 morceaux.
- Gabarits de LanguageInit filtrés, DistinctBy(Text) avant le Take : ils
  occupaient les cinq premiers résultats d'une recherche en néerlandais.

Endpoints du guide IA
- GET /api/Ai/knowledge/{id} : agrégats sur ContentEmbedding, donc sur ce qui
  est réellement indexé — compter les sections publiées serait plus flatteur et faux.
- GET /api/Ai/insights/{id} : miroir de GuideIaInsights côté manager-app, c'est
  l'écran qui a fixé la forme pour que le job de thèmes la remplisse.

RGPD
- VisitorQuestion journalisée dans AiController.Chat. HasAnswer se déduit des
  sources du retrieval, pas du texte : un repli poli ressemble à une réponse.
  L'écriture n'échoue jamais la réponse au visiteur.
- VisitorQuestionPurgeService, 90 jours, actif sans condition de configuration :
  une durée écrite dans les CGU n'est pas un réglage commercial.

Corrections
- Updateinstance ne recopiait pas les quotas du nouveau plan.
- CheckQuota ne bloquait ni ne comptait à quota 0 — IA gratuite non comptée.
- StoragePath et SizeBytes renseignés à Create, types URL exclus.

dotnet build 0 erreur, dotnet test 130/130.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:48:10 +02:00

142 lines
5.8 KiB
C#

using Manager.DTOs;
using ManagerService.Data;
using ManagerService.Data.SubSection;
using ManagerService.DTOs;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace ManagerService.Services
{
public class AgendaSyncService
{
private readonly ILogger<AgendaSyncService> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IHttpClientFactory _httpClientFactory;
public AgendaSyncService(ILogger<AgendaSyncService> logger, IServiceScopeFactory scopeFactory, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
_httpClientFactory = httpClientFactory;
}
public async Task SyncAllAsync()
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MyInfoMateDbContext>();
var sections = db.Sections.OfType<SectionAgenda>()
.Where(sa => sa.IsOnlineAgenda && sa.AgendaResourceIds != null && sa.AgendaResourceIds.Count > 0)
.Select(sa => sa.Id)
.ToList();
foreach (var id in sections)
{
try { await SyncSectionAsync(id); }
catch (Exception ex) { _logger.LogError(ex, "Error syncing agenda section {Id}", id); }
}
}
public async Task SyncSectionAsync(string sectionAgendaId)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MyInfoMateDbContext>();
var section = db.Sections.OfType<SectionAgenda>()
.Include(sa => sa.EventAgendas)
.FirstOrDefault(sa => sa.Id == sectionAgendaId);
if (section == null || !section.IsOnlineAgenda || section.AgendaResourceIds == null)
return;
var http = _httpClientFactory.CreateClient();
foreach (var resourceRef in section.AgendaResourceIds)
{
if (string.IsNullOrEmpty(resourceRef.value) || string.IsNullOrEmpty(resourceRef.language))
continue;
var resource = db.Resources.FirstOrDefault(r => r.Id == resourceRef.value);
if (resource == null || string.IsNullOrEmpty(resource.Url))
continue;
List<RemoteEventAgendaDTO> remoteEvents;
try
{
var json = await http.GetStringAsync(resource.Url);
remoteEvents = JsonConvert.DeserializeObject<List<RemoteEventAgendaDTO>>(json, RemoteAgendaJsonSettings.Lenient) ?? new();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch agenda JSON for section {Id} language {Lang}", sectionAgendaId, resourceRef.language);
continue;
}
foreach (var remote in remoteEvents)
{
var dateFrom = remote.GetDateFrom();
// Match by date + name in the current language
var existing = section.EventAgendas.FirstOrDefault(ea =>
ea.IsSynced &&
ea.DateFrom.HasValue &&
dateFrom.HasValue &&
ea.DateFrom.Value.Date == dateFrom.Value.Date &&
ea.Label.Any(l => l.language == resourceRef.language && l.value == remote.name));
if (existing == null)
{
existing = new EventAgenda
{
Label = new List<TranslationDTO>(),
Description = new List<TranslationDTO>(),
SectionAgendaId = sectionAgendaId,
IsSynced = true,
};
section.EventAgendas.Add(existing);
db.EventAgendas.Add(existing);
}
// Update / set translation for this language
SetTranslation(existing.Label, resourceRef.language, remote.name);
SetTranslation(existing.Description, resourceRef.language, remote.description);
// Non-translated fields (last language wins, acceptable)
existing.DateFrom = dateFrom;
existing.DateTo = remote.GetDateTo();
existing.Phone = remote.phone;
existing.Email = remote.email;
existing.Website = remote.website;
existing.IdVideoYoutube = remote.id_video_youtube;
existing.IsSynced = true;
if (!string.IsNullOrEmpty(remote.image) && string.IsNullOrEmpty(existing.SyncedImageUrl))
existing.SyncedImageUrl = remote.image;
}
}
// Les EventAgenda modifiés ci-dessus déclenchent la ré-indexation via
// SectionIndexingInterceptor : elle porte donc sur les dates fraîches, pas
// sur celles d'avant la synchro.
db.SaveChanges();
_logger.LogInformation("Synced agenda section {Id}", sectionAgendaId);
}
private static void SetTranslation(List<TranslationDTO> list, string language, string? value)
{
var existing = list.FirstOrDefault(t => t.language == language);
if (existing != null)
existing.value = value ?? "";
else
list.Add(new TranslationDTO { language = language, value = value ?? "" });
}
}
}