Thomas Fransolet 118813af58 Les prompts du mode proactif ne sont plus journalisés comme des questions de visiteur
RecordVisitorQuestion écrivait Question = request.Message. Or le prompt d'un
déclenchement automatique est une consigne que le système s'écrit à lui-même
(« Tu es un guide audio de musée. Le visiteur vient d'entrer dans la zone X… ») :
chaque passage devant une œuvre aurait ajouté une fausse question de visiteur
dans l'onglet Guide IA, l'écran fait pour montrer ce que les humains demandent.
Et HasAnswer se déduisant des sources, un prompt d'accueil sans citation aurait
gonflé le bloc ambre des questions sans réponse. Même famille que
WeatherSyncService noyant le journal d'audit.

AiChatRequest.IsAutoTriggered, et Chat qui saute la journalisation.

Tranché : ne rien journaliser, plutôt que marquer d'un drapeau. Une ligne
VisitorQuestion sert au rapport de trous de contenu et aux thèmes du lot J ; un
prompt machine n'alimente ni l'un ni l'autre, et le garder marqué obligerait
chaque futur agrégat à penser à l'exclure — la classe de panne qu'on venait de
fermer sur AuditedTypes.

Mais les jetons restent comptés : ces tours coûtent de l'argent réel au quota du
client, et un audioguide proactif peut en consommer beaucoup. Les 2 tests fixent
la paire — pas de ligne journalisée, compteur incrémenté — parce que c'est la
combinaison qui compte, pas chaque moitié isolément.

dotnet test : 199 passés, 14 sautés, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 13:14:01 +02:00

106 lines
4.2 KiB
C#

using ManagerService.Data;
using System.Collections.Generic;
namespace ManagerService.DTOs
{
public class AiChatRequest
{
public string Message { get; set; }
public string InstanceId { get; set; }
public AppType AppType { get; set; }
public string ConfigurationId { get; set; } // null = scope instance, fourni = scope configuration
public string Language { get; set; }
/// <summary>
/// GUID généré par le client, stable sur une session de conversation.
/// History étant reconstruit côté client à chaque appel, c'est le seul moyen
/// de relier deux questions d'un même visiteur côté serveur.
/// Optionnel : un appel sans ConversationId reste valide, la ligne journalisée est isolée.
/// </summary>
public string ConversationId { get; set; }
public List<AiChatMessage> History { get; set; } = new();
/// <summary>
/// true = interaction vocale (lunettes, assistant vocal).
/// Le backend adapte le prompt : pas de markdown, pas de navigation,
/// dates en toutes lettres, réponses courtes audio-friendly.
/// </summary>
public bool IsVoice { get; set; } = false;
/// <summary>
/// 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 <c>VisitorQuestion</c> : 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.
/// </summary>
public bool IsAutoTriggered { get; set; } = false;
}
public class AiChatMessage
{
public string Role { get; set; } // "user" | "assistant"
public string Content { get; set; }
}
public class AiCardDTO
{
public string Title { get; set; }
public string Subtitle { get; set; }
public string? Icon { get; set; }
}
public class NavigationActionDTO
{
public string SectionId { get; set; }
public string SectionTitle { get; set; }
public string SectionType { get; set; }
public string? ImageUrl { get; set; }
}
public class AiChatResponse
{
public string Reply { get; set; }
public List<AiCardDTO>? Cards { get; set; }
public NavigationActionDTO? Navigation { get; set; }
/// <summary>
/// false = le LLM ne s'attend pas à une réponse du visiteur (info pure, hors-sujet, politesse, beacon).
/// Le front peut désactiver l'écoute automatique après le TTS.
/// </summary>
public bool ExpectsReply { get; set; } = true;
public long TokensUsed { get; set; }
/// <summary>
/// Contenus sur lesquels la réponse s'appuie, relevés côté serveur à chaque appel de
/// SearchKnowledge. Ils ne passent pas par le modèle : la règle « ne mentionne jamais
/// les identifiants techniques » lui interdit précisément de les recopier.
/// Destinés au client, pas au visiteur — c'est ce qui rend une réponse vérifiable
/// quand un client conteste ce que dit son guide.
/// </summary>
public List<AiSourceDTO>? Sources { get; set; }
}
public class AiSourceDTO
{
public string ContentId { get; set; }
public string ContentType { get; set; }
/// <summary>Null hors document paginé.</summary>
public int? PageNumber { get; set; }
}
public class AiTranslateRequest
{
public string Text { get; set; }
public string SourceLang { get; set; }
public List<string> TargetLangs { get; set; } = new();
}
public class AiTranslateResponse
{
public Dictionary<string, string> Translations { get; set; } = new();
public long TokensUsed { get; set; }
}
}