AssistantService.chat porte isAutoTriggered, et geo_beacon_trigger_service le passe à true. Sans lui, le prompt que le service s'écrit à lui-même atterrissait dans VisitorQuestion, donc dans « Ce que demandent vos visiteurs », et gonflait le bloc des questions sans réponse. Les jetons restent comptés côté serveur. C'était le dernier bloquant avant d'activer le mode chez un client. flutter analyze lib sans erreur, flutter build apk --debug --flavor dev vert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
132 lines
4.3 KiB
Dart
132 lines
4.3 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:manager_api_new/api.dart';
|
|
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
|
import 'package:mymuseum_visitapp/Models/AssistantResponse.dart';
|
|
|
|
/// Levée quand l'instance a épuisé son quota IA du mois (HTTP 429).
|
|
///
|
|
/// Distincte d'une panne : le visiteur ne doit pas être invité à réessayer, ça
|
|
/// le ferait boucler sur un mur. L'appelant affiche un message neutre, sans
|
|
/// jamais mentionner le motif — c'est une affaire entre le client et nous.
|
|
class AssistantUnavailableException implements Exception {
|
|
const AssistantUnavailableException();
|
|
}
|
|
|
|
class AssistantService {
|
|
final VisitAppContext visitAppContext;
|
|
|
|
/// Nombre maximum de messages conservés dans l'historique envoyé au backend.
|
|
final int maxHistory;
|
|
|
|
/// Durée d'inactivité après laquelle l'historique est automatiquement vidé.
|
|
/// null = pas de vidage automatique.
|
|
final Duration? inactivityTimeout;
|
|
|
|
final List<AiChatMessage> _history = [];
|
|
Timer? _inactivityTimer;
|
|
|
|
AssistantService({
|
|
required this.visitAppContext,
|
|
this.maxHistory = 10,
|
|
this.inactivityTimeout = const Duration(minutes: 5),
|
|
});
|
|
|
|
Future<AssistantResponse> chat({
|
|
required String message,
|
|
String? configurationId,
|
|
bool isVoice = false,
|
|
bool isAutoTriggered = false,
|
|
}) => chatWithAppType(
|
|
message: message,
|
|
configurationId: configurationId,
|
|
isVoice: isVoice,
|
|
isAutoTriggered: isAutoTriggered,
|
|
);
|
|
|
|
/// [isAutoTriggered] : le tour vient du mode proactif, pas d'une question du visiteur.
|
|
/// Le serveur compte les jetons mais ne le journalise pas dans `VisitorQuestion`.
|
|
Future<AssistantResponse> chatWithAppType({
|
|
required String message,
|
|
String? configurationId,
|
|
AppType appType = AppType.Mobile,
|
|
bool isVoice = false,
|
|
bool isAutoTriggered = false,
|
|
}) async {
|
|
_resetInactivityTimer();
|
|
|
|
final request = AiChatRequest(
|
|
message: message,
|
|
instanceId: visitAppContext.instanceId,
|
|
appType: appType,
|
|
configurationId: configurationId,
|
|
language: visitAppContext.language?.toUpperCase() ?? 'FR',
|
|
history: List.from(_history),
|
|
isVoice: isVoice,
|
|
isAutoTriggered: isAutoTriggered,
|
|
);
|
|
|
|
final AiChatResponse? response;
|
|
try {
|
|
response = await visitAppContext.clientAPI.aiApi!.aiChat(request);
|
|
} on ApiException catch (e) {
|
|
if (e.code == 429) throw const AssistantUnavailableException();
|
|
rethrow;
|
|
}
|
|
|
|
if (response == null) {
|
|
throw Exception('Empty response from assistant');
|
|
}
|
|
|
|
debugPrint("AI raw response: reply='${response.reply}' navigation.sectionId='${response.navigation?.sectionId}' navigation.sectionTitle='${response.navigation?.sectionTitle}' navigation.sectionType='${response.navigation?.sectionType}' cards=${response.cards?.length}");
|
|
|
|
final result = AssistantResponse(
|
|
reply: response.reply ?? '',
|
|
cards: response.cards
|
|
?.map((c) => AiCard(
|
|
title: c.title ?? '',
|
|
subtitle: c.subtitle ?? '',
|
|
icon: c.icon,
|
|
))
|
|
.toList(),
|
|
navigation: response.navigation != null
|
|
? AssistantNavigationAction(
|
|
sectionId: response.navigation!.sectionId ?? '',
|
|
sectionTitle: response.navigation!.sectionTitle ?? '',
|
|
sectionType: response.navigation!.sectionType ?? '',
|
|
imageUrl: response.navigation!.imageUrl,
|
|
)
|
|
: null,
|
|
expectsReply: response.expectsReply ?? true,
|
|
);
|
|
|
|
_history.add(AiChatMessage(role: 'user', content: message));
|
|
_history.add(AiChatMessage(role: 'assistant', content: result.reply));
|
|
|
|
// Cap local — inutile de garder plus que maxHistory côté client
|
|
if (_history.length > maxHistory) {
|
|
_history.removeRange(0, _history.length - maxHistory);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void _resetInactivityTimer() {
|
|
if (inactivityTimeout == null) return;
|
|
_inactivityTimer?.cancel();
|
|
_inactivityTimer = Timer(inactivityTimeout!, () {
|
|
debugPrint('[AssistantService] Inactivity timeout — clearing history');
|
|
clearHistory();
|
|
});
|
|
}
|
|
|
|
void clearHistory() {
|
|
_history.clear();
|
|
_inactivityTimer?.cancel();
|
|
}
|
|
|
|
void dispose() {
|
|
_inactivityTimer?.cancel();
|
|
}
|
|
}
|