diff --git a/ManagerService.Tests/Controllers/AiControllerTests.cs b/ManagerService.Tests/Controllers/AiControllerTests.cs index 781700b..665a4ab 100644 --- a/ManagerService.Tests/Controllers/AiControllerTests.cs +++ b/ManagerService.Tests/Controllers/AiControllerTests.cs @@ -105,6 +105,56 @@ namespace ManagerService.Tests.Controllers Assert.IsType(result); } + // ── JOURNALISATION DES QUESTIONS ───────────────────────────────────── + + /// + /// Un tour déclenché par le mode proactif ne doit pas apparaître dans + /// « Ce que demandent vos visiteurs » : sa « question » est une consigne que le + /// système s'est écrite à lui-même. Mais ses jetons sont bien consommés, donc + /// bien comptés — c'est la paire qui compte, pas chaque moitié isolément. + /// + [Fact] + public async Task Chat_AutoTriggered_CountsTokensButLogsNoVisitorQuestion() + { + using var db = DbContextFactory.Create(); + SeedAssistantInstance(db); + + var request = MakeRequest("i1"); + request.IsAutoTriggered = true; + await BuildController(db).Chat(request); + + Assert.Empty(db.VisitorQuestions); + Assert.Equal(42, db.Instances.First().AiTokensThisMonth); + } + + [Fact] + public async Task Chat_AskedByVisitor_LogsVisitorQuestion() + { + using var db = DbContextFactory.Create(); + SeedAssistantInstance(db); + + await BuildController(db).Chat(MakeRequest("i1")); + + var logged = Assert.Single(db.VisitorQuestions); + Assert.Equal("Bonjour", logged.Question); + Assert.Equal(42, db.Instances.First().AiTokensThisMonth); + } + + private static void SeedAssistantInstance(MyInfoMateDbContext db) + { + db.Instances.Add(new Instance + { + Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow, + AiTokensPerMonth = 1_000, AiUsageMonthKey = DateTime.UtcNow.ToString("yyyy-MM") + }); + db.ApplicationInstances.Add(new ApplicationInstance + { + Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true, + Languages = new List() + }); + db.SaveChanges(); + } + // ── QUOTA COUNTER ──────────────────────────────────────────────────── [Fact] diff --git a/ManagerService/Controllers/AiController.cs b/ManagerService/Controllers/AiController.cs index 392f337..2496939 100644 --- a/ManagerService/Controllers/AiController.cs +++ b/ManagerService/Controllers/AiController.cs @@ -378,8 +378,16 @@ namespace ManagerService.Controllers var result = await _assistantService.ChatAsync(request); + // Les jetons sont comptés dans tous les cas — un tour proactif coûte de + // l'argent réel au quota du client, l'exclure du compteur serait pire que + // le bruit qu'on retire juste en dessous. RecordUsage(instance, result.TokensUsed); - RecordVisitorQuestion(request, result); + + // Mais un tour que le système s'est déclenché à lui-même n'est pas une + // question de visiteur : il ne va ni dans « Ce que demandent vos visiteurs », + // ni dans les trous de contenu, ni dans les thèmes du lot J. + if (!request.IsAutoTriggered) + RecordVisitorQuestion(request, result); return Ok(result); } diff --git a/ManagerService/DTOs/AiChatDTO.cs b/ManagerService/DTOs/AiChatDTO.cs index 2049760..7d73430 100644 --- a/ManagerService/DTOs/AiChatDTO.cs +++ b/ManagerService/DTOs/AiChatDTO.cs @@ -24,6 +24,18 @@ namespace ManagerService.DTOs /// dates en toutes lettres, réponses courtes audio-friendly. /// public bool IsVoice { get; set; } = false; + /// + /// true = le tour a été déclenché par le système, pas demandé par le visiteur — + /// c'est le cas du mode proactif, où l'approche d'un point d'intérêt fait parler + /// le guide tout seul. + /// + /// ⚠️ Ces tours ne sont **pas journalisés** dans VisitorQuestion : leur + /// « question » est une consigne que le système s'est écrite à lui-même + /// (« Tu es un guide audio de musée. Le visiteur vient d'entrer dans la zone X… »), + /// et l'onglet Guide IA existe pour montrer ce que les **humains** demandent. + /// Leurs jetons, eux, restent comptés : ils sont réellement consommés. + /// + public bool IsAutoTriggered { get; set; } = false; } public class AiChatMessage