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

164 lines
6.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Manager.DTOs;
using ManagerService.DTOs;
using NetTopologySuite.Geometries;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using ManagerService.Helpers;
namespace ManagerService.Data.SubSection
{
public class GuidedStep // Étape dun parcours
{
[Key]
public string Id { get; set; }
[Required]
public string GuidedPathId { get; set; }
[ForeignKey("GuidedPathId")]
public GuidedPath GuidedPath { get; set; }
public int Order { get; set; }
[Required]
[Column(TypeName = "jsonb")]
public List<TranslationDTO> Title { get; set; }
[Column(TypeName = "jsonb")]
public List<TranslationDTO> Description { get; set; }
public Geometry? Geometry { get; set; } // Polygon ou centre du cercle
// Option : si true, cette étape doit être atteinte géographiquement (zone/rayon) pour être validée
public bool IsGeoTriggered { get; set; } = false;
public double? ZoneRadiusMeters { get; set; } // Optionnel, utile si zone cercle ou point
public string ImageUrl { get; set; }
// Rich content (calqué sur SectionArticle)
[Column(TypeName = "jsonb")]
public List<TranslationDTO> AudioIds { get; set; } = new();
[Column(TypeName = "jsonb")]
public List<ContentDTO> Contents { get; set; } = new();
// Exemple pour escape game
public List<QuizQuestion>? QuizQuestions { get; set; } // One or multiple question
// Option : si true, cette étape a un compte à rebourds, false sinon
public bool IsStepTimer { get; set; } = false;
// Timer en secondes (durée max pour valider cette étape, optionnel)
public int? TimerSeconds { get; set; }
// Option : message ou action à effectuer si timer expire (ex: afficher aide, fin du jeu...)
[Column(TypeName = "jsonb")]
public List<TranslationDTO> TimerExpiredMessage { get; set; }
// Même règle que SectionQuiz : les réponses des énigmes ne sont pas indexées.
public string GetEmbeddableText(string language) =>
SectionText.JoinText(new[]
{
SectionText.Translate(Title, language),
SectionText.Translate(Description, language),
SectionText.TranslateContents(Contents, language),
SectionText.Translate(TimerExpiredMessage, language)
}
.Concat((QuizQuestions ?? new List<QuizQuestion>())
.OrderBy(q => q.Order)
.Select(q => SectionText.Translate(q.Label, language)))
.ToArray());
// ImageUrl est une URL absolue, pas un id : rien à rapprocher d'une ligne Resource.
public IEnumerable<string> GetReferencedResourceIds(string language = null) =>
SectionText.ResourceIdsFromValues(AudioIds, language)
.Concat(SectionText.ResourceIds(Contents))
.Concat((QuizQuestions ?? new List<QuizQuestion>())
.SelectMany(q => q.GetReferencedResourceIds(language)));
public GuidedStepDTO ToDTO()
{
return new GuidedStepDTO
{
id = Id,
guidedPathId = GuidedPathId,
order = Order,
title = Title,
description = Description,
geometry = Geometry?.ToDto(),
isGeoTriggered = IsGeoTriggered,
zoneRadiusMeters = ZoneRadiusMeters,
imageUrl = ImageUrl,
audioIds = AudioIds,
contents = Contents,
isStepTimer = IsStepTimer,
timerSeconds = TimerSeconds,
timerExpiredMessage = TimerExpiredMessage,
quizQuestions = QuizQuestions
};
}
public GuidedStep FromDTO(GuidedStepDTO dto)
{
if (dto == null) return null;
GuidedPathId = dto.guidedPathId;
Title = dto.title ?? new List<TranslationDTO>();
Description = dto.description ?? new List<TranslationDTO>();
Geometry = dto.geometry.FromDto();
IsGeoTriggered = dto.isGeoTriggered;
ZoneRadiusMeters = dto.zoneRadiusMeters;
ImageUrl = dto.imageUrl;
AudioIds = dto.audioIds ?? new List<TranslationDTO>();
Contents = dto.contents ?? new List<ContentDTO>();
IsStepTimer = dto.isStepTimer;
TimerSeconds = dto.timerSeconds;
TimerExpiredMessage = dto.timerExpiredMessage ?? new List<TranslationDTO>();
Order = dto.order.GetValueOrDefault();
SyncQuizQuestions(dto.quizQuestions);
return this;
}
// Merges incoming quiz questions into the tracked QuizQuestions collection by Id,
// instead of replacing the reference outright — a blind reassignment would make EF
// treat every incoming question (including already-persisted ones) as a brand new
// row to insert, which fails with a duplicate key error as soon as a step already
// has a saved question and a new one is added alongside it.
public void SyncQuizQuestions(List<QuizQuestion>? dtoQuestions)
{
dtoQuestions ??= new List<QuizQuestion>();
QuizQuestions ??= new List<QuizQuestion>();
var dtoIds = dtoQuestions.Where(q => q.Id != 0).Select(q => q.Id).ToHashSet();
QuizQuestions.RemoveAll(q => !dtoIds.Contains(q.Id));
foreach (var qDto in dtoQuestions)
{
var existing = qDto.Id != 0 ? QuizQuestions.FirstOrDefault(q => q.Id == qDto.Id) : null;
if (existing != null)
{
existing.Label = qDto.Label;
existing.ResourceId = qDto.ResourceId;
existing.Order = qDto.Order;
existing.Responses = qDto.Responses;
existing.ValidationQuestionType = qDto.ValidationQuestionType;
existing.PuzzleImageId = qDto.PuzzleImageId;
existing.PuzzleRows = qDto.PuzzleRows;
existing.PuzzleCols = qDto.PuzzleCols;
existing.IsSlidingPuzzle = qDto.IsSlidingPuzzle;
}
else
{
qDto.Id = 0;
QuizQuestions.Add(qDto);
}
}
}
}
}