Feature: Tracking des Coûts par Conversation Agent IA
Objectif
Permettre de suivre les coûts liés à l'utilisation de l'Agent IA par vendeuse/utilisateur et par conversation, afin d'analyser le ROI (retour sur investissement) de l'IA.
Cas d'usage
- Un administrateur veut voir combien coûte l'Agent IA par mois
- Une vendeuse veut voir ses coûts individuels de conversations
- Analyser le coût moyen d'une conversation qui se transforme en commande
- Comparer les coûts entre différentes vendeuses
- Calculer le ROI: coût des conversations vs valeur des commandes générées
1. Modifications de la Base de Données (Supabase)
1.1 Table agent_messages
Ajouter les colonnes suivantes:
ALTER TABLE agent_messages
ADD COLUMN input_tokens INTEGER,
ADD COLUMN output_tokens INTEGER,
ADD COLUMN total_tokens INTEGER,
ADD COLUMN cost DECIMAL(10, 6),
ADD COLUMN model_used VARCHAR(50);
Colonnes:
input_tokens: Nombre de tokens en entrée (prompt envoyé à l'IA)output_tokens: Nombre de tokens en sortie (réponse de l'IA)total_tokens: Total des tokens (input + output)cost: Coût du message en euros (calculé selon le modèle et les tokens)model_used: Modèle IA utilisé (ex: "gpt-4", "gpt-3.5-turbo", "claude-3-opus")
1.2 Table agent_conversations
Ajouter les colonnes suivantes:
ALTER TABLE agent_conversations
ADD COLUMN total_input_tokens INTEGER DEFAULT 0,
ADD COLUMN total_output_tokens INTEGER DEFAULT 0,
ADD COLUMN total_tokens INTEGER DEFAULT 0,
ADD COLUMN total_cost DECIMAL(10, 6) DEFAULT 0;
Colonnes:
total_input_tokens: Somme des input_tokens de tous les messagestotal_output_tokens: Somme des output_tokens de tous les messagestotal_tokens: Total des tokens de la conversationtotal_cost: Coût total de la conversation
1.3 Vue Agrégée (optionnel mais recommandé)
Créer une vue pour faciliter les requêtes de statistiques:
CREATE VIEW agent_conversation_costs AS
SELECT
c.user_id,
c.id as conversation_id,
c.status,
c.platform,
c.order_id,
c.total_tokens,
c.total_cost,
c.created_at,
c.updated_at,
CASE
WHEN c.order_id IS NOT NULL THEN true
ELSE false
END as converted_to_order
FROM agent_conversations c;
2. Modifications du Code Flutter
2.1 Entités Domain
lib/features/agent_ia/domain/entities/agent_message.dart
Ajouter les propriétés:
class AgentMessage {
final String id;
final String conversationId;
final String role;
final String content;
final String? externalMessageId;
final DateTime createdAt;
final List<DateSuggestion>? dateSuggestions;
// NOUVEAUX CHAMPS
final int? inputTokens;
final int? outputTokens;
final int? totalTokens;
final double? cost;
final String? modelUsed;
// ... reste du code
}
lib/features/agent_ia/domain/entities/agent_conversation.dart
Ajouter les propriétés:
class AgentConversation {
// ... propriétés existantes
// NOUVEAUX CHAMPS
final int? totalInputTokens;
final int? totalOutputTokens;
final int? totalTokens;
final double? totalCost;
// Getters utiles
double get averageCostPerMessage =>
(messagesCount != null && messagesCount! > 0 && totalCost != null)
? totalCost! / messagesCount!
: 0.0;
bool get hasOrder => orderId != null;
}
lib/features/agent_ia/domain/entities/conversation_cost_stats.dart (NOUVEAU)
Créer une nouvelle entité pour les statistiques de coûts:
class ConversationCostStats {
final String userId;
final int totalConversations;
final int convertedConversations;
final double totalCost;
final double averageCostPerConversation;
final double averageCostPerConvertedConversation;
final int totalTokens;
final DateTime periodStart;
final DateTime periodEnd;
const ConversationCostStats({
required this.userId,
required this.totalConversations,
required this.convertedConversations,
required this.totalCost,
required this.averageCostPerConversation,
required this.averageCostPerConvertedConversation,
required this.totalTokens,
required this.periodStart,
required this.periodEnd,
});
double get conversionRate =>
totalConversations > 0
? (convertedConversations / totalConversations) * 100
: 0.0;
}
2.2 Modèles Data
lib/features/agent_ia/data/models/agent_message_model.dart
Ajouter le parsing des nouveaux champs dans fromJson:
factory AgentMessageModel.fromJson(Map<String, dynamic> json) {
return AgentMessageModel(
// ... champs existants
inputTokens: json['input_tokens'] as int?,
outputTokens: json['output_tokens'] as int?,
totalTokens: json['total_tokens'] as int?,
cost: json['cost'] != null ? (json['cost'] as num).toDouble() : null,
modelUsed: json['model_used'] as String?,
);
}
2.3 Repository
lib/features/agent_ia/domain/repositories/agent_repository.dart
Ajouter les méthodes:
abstract class AgentRepository {
// ... méthodes existantes
/// Récupère les statistiques de coûts pour un utilisateur sur une période
Future<ConversationCostStats> getCostStats({
required String userId,
DateTime? startDate,
DateTime? endDate,
});
/// Récupère les conversations avec leurs coûts triées par coût décroissant
Future<List<AgentConversation>> getConversationsByCost({
required String userId,
int limit = 20,
});
}
lib/features/agent_ia/data/repositories/agent_repository_impl.dart
Implémenter les méthodes:
@override
Future<ConversationCostStats> getCostStats({
required String userId,
DateTime? startDate,
DateTime? endDate,
}) async {
final start = startDate ?? DateTime.now().subtract(Duration(days: 30));
final end = endDate ?? DateTime.now();
final response = await _client
.from('agent_conversations')
.select()
.eq('user_id', userId)
.gte('created_at', start.toIso8601String())
.lte('created_at', end.toIso8601String());
final conversations = (response as List)
.map((json) => AgentConversationModel.fromJson(json))
.toList();
final totalConversations = conversations.length;
final convertedConversations =
conversations.where((c) => c.orderId != null).length;
final totalCost = conversations.fold<double>(
0.0, (sum, c) => sum + (c.totalCost ?? 0.0));
final totalTokens = conversations.fold<int>(
0, (sum, c) => sum + (c.totalTokens ?? 0));
final avgCostPerConversation =
totalConversations > 0 ? totalCost / totalConversations : 0.0;
final avgCostPerConverted =
convertedConversations > 0
? conversations
.where((c) => c.orderId != null)
.fold<double>(0.0, (sum, c) => sum + (c.totalCost ?? 0.0)) /
convertedConversations
: 0.0;
return ConversationCostStats(
userId: userId,
totalConversations: totalConversations,
convertedConversations: convertedConversations,
totalCost: totalCost,
averageCostPerConversation: avgCostPerConversation,
averageCostPerConvertedConversation: avgCostPerConverted,
totalTokens: totalTokens,
periodStart: start,
periodEnd: end,
);
}
2.4 UI - Page de Statistiques des Coûts
lib/features/agent_ia/presentation/pages/cost_stats_page.dart (NOUVEAU)
Créer une nouvelle page pour afficher les statistiques:
Sections de la page:
-
Header avec période sélectionnée
- Sélecteur de période (7j, 30j, 90j, personnalisé)
- Date picker pour période personnalisée
-
Carte récapitulatif
- Coût total de la période
- Nombre de conversations
- Coût moyen par conversation
- Badge avec tendance (hausse/baisse vs période précédente)
-
Carte ROI
- Conversations converties en commandes
- Taux de conversion
- Coût moyen par conversion
- Valeur totale des commandes générées
- ROI calculé (valeur commandes / coût conversations)
-
Graphique de l'évolution des coûts
- Graphique linéaire montrant l'évolution du coût par jour/semaine
- Utiliser un package comme
fl_chart
-
Liste des conversations les plus coûteuses
- Top 10 des conversations par coût
- Indication si converti en commande ou non
- Badge avec le coût
Structure de base:
class CostStatsPage extends StatefulWidget {
const CostStatsPage({super.key});
@override
State<CostStatsPage> createState() => _CostStatsPageState();
}
class _CostStatsPageState extends State<CostStatsPage> {
late AgentRepository _repository;
ConversationCostStats? _stats;
bool _isLoading = true;
DateTime _startDate = DateTime.now().subtract(Duration(days: 30));
DateTime _endDate = DateTime.now();
@override
void initState() {
super.initState();
_repository = AgentRepositoryImpl(SupabaseConfig.client);
_loadStats();
}
Future<void> _loadStats() async {
setState(() => _isLoading = true);
try {
final userId = SupabaseConfig.client.auth.currentUser!.id;
final stats = await _repository.getCostStats(
userId: userId,
startDate: _startDate,
endDate: _endDate,
);
setState(() {
_stats = stats;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
// Gérer l'erreur
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Statistiques des Coûts'),
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: SingleChildScrollView(
child: Column(
children: [
_buildPeriodSelector(),
_buildSummaryCard(),
_buildROICard(),
_buildCostChart(),
_buildTopConversations(),
],
),
),
);
}
Widget _buildSummaryCard() {
// Implémenter la carte récapitulatif
}
Widget _buildROICard() {
// Implémenter la carte ROI
}
// ... autres méthodes
}
2.5 Navigation
Ajouter un bouton dans agent_ia_page.dart pour accéder aux statistiques de coûts:
actions: [
IconButton(
icon: Icon(Icons.analytics_outlined),
tooltip: 'Statistiques des coûts',
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CostStatsPage(),
),
);
},
),
// ... autres actions
]
3. Backend - Calcul et Enregistrement des Coûts
3.1 Tarification des Modèles (à mettre à jour régulièrement)
Créer un fichier de configuration:
// lib/features/agent_ia/domain/config/ai_pricing.dart
class AIPricing {
static const Map<String, ModelPricing> models = {
'gpt-4': ModelPricing(
inputCostPer1kTokens: 0.03, // $0.03 per 1K tokens
outputCostPer1kTokens: 0.06, // $0.06 per 1K tokens
),
'gpt-3.5-turbo': ModelPricing(
inputCostPer1kTokens: 0.0015, // $0.0015 per 1K tokens
outputCostPer1kTokens: 0.002, // $0.002 per 1K tokens
),
'claude-3-opus': ModelPricing(
inputCostPer1kTokens: 0.015,
outputCostPer1kTokens: 0.075,
),
'claude-3-sonnet': ModelPricing(
inputCostPer1kTokens: 0.003,
outputCostPer1kTokens: 0.015,
),
};
static double calculateCost({
required String model,
required int inputTokens,
required int outputTokens,
}) {
final pricing = models[model];
if (pricing == null) return 0.0;
final inputCost = (inputTokens / 1000) * pricing.inputCostPer1kTokens;
final outputCost = (outputTokens / 1000) * pricing.outputCostPer1kTokens;
return inputCost + outputCost;
}
}
class ModelPricing {
final double inputCostPer1kTokens;
final double outputCostPer1kTokens;
const ModelPricing({
required this.inputCostPer1kTokens,
required this.outputCostPer1kTokens,
});
}
3.2 Service Backend
Le backend (qui appelle l'API de l'IA) doit:
- Enregistrer les tokens retournés par l'API
- Calculer le coût avec
AIPricing.calculateCost() - Sauvegarder dans la base de données
- Mettre à jour les totaux de la conversation
Exemple de fonction backend (pseudo-code):
// Backend function (Supabase Edge Function ou autre)
async function sendMessageToAI(conversationId, userMessage) {
// 1. Appeler l'API de l'IA
const aiResponse = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [...conversationHistory, userMessage],
});
const inputTokens = aiResponse.usage.prompt_tokens;
const outputTokens = aiResponse.usage.completion_tokens;
const totalTokens = aiResponse.usage.total_tokens;
// 2. Calculer le coût
const cost = calculateCost({
model: "gpt-3.5-turbo",
inputTokens,
outputTokens,
});
// 3. Enregistrer le message avec les metrics
await supabase.from('agent_messages').insert({
conversation_id: conversationId,
role: 'assistant',
content: aiResponse.choices[0].message.content,
input_tokens: inputTokens,
output_tokens: outputTokens,
total_tokens: totalTokens,
cost: cost,
model_used: "gpt-3.5-turbo",
});
// 4. Mettre à jour les totaux de la conversation
await supabase.rpc('update_conversation_cost', {
p_conversation_id: conversationId,
p_tokens_to_add: totalTokens,
p_cost_to_add: cost,
});
return aiResponse;
}
3.3 Fonction SQL pour Mise à Jour
CREATE OR REPLACE FUNCTION update_conversation_cost(
p_conversation_id UUID,
p_tokens_to_add INTEGER,
p_cost_to_add DECIMAL
)
RETURNS void AS $$
BEGIN
UPDATE agent_conversations
SET
total_tokens = COALESCE(total_tokens, 0) + p_tokens_to_add,
total_cost = COALESCE(total_cost, 0) + p_cost_to_add,
updated_at = NOW()
WHERE id = p_conversation_id;
END;
$$ LANGUAGE plpgsql;
4. Tests à Effectuer
-
Tests unitaires:
- Calcul du coût avec différents modèles
- Agrégation des statistiques
- Parsing des nouveaux champs JSON
-
Tests d'intégration:
- Enregistrement d'un message avec coûts
- Mise à jour des totaux de conversation
- Récupération des statistiques
-
Tests UI:
- Affichage correct des coûts
- Navigation vers la page de statistiques
- Changement de période
5. Ordre d'Implémentation Recommandé
-
✅ Phase 1: Base de données
- Ajouter les colonnes à Supabase
- Créer les fonctions SQL
- Créer la vue agrégée
-
✅ Phase 2: Entités et Modèles
- Modifier
AgentMessageetAgentConversation - Créer
ConversationCostStats - Mettre à jour les modèles
- Modifier
-
✅ Phase 3: Repository
- Ajouter les méthodes dans le repository
- Implémenter
getCostStats()
-
✅ Phase 4: Backend
- Créer
AIPricing - Modifier le backend pour enregistrer les tokens et coûts
- Créer
-
✅ Phase 5: UI
- Créer
CostStatsPage - Ajouter les widgets de statistiques
- Ajouter la navigation
- Créer
-
✅ Phase 6: Tests
- Tests unitaires
- Tests d'intégration
- Tests UI
6. Améliorations Futures
- Export des données: Permettre d'exporter les statistiques en CSV/PDF
- Alertes: Notifier quand le coût dépasse un seuil
- Prédictions: Prédire les coûts du mois basé sur l'utilisation actuelle
- Comparaisons: Comparer les performances entre différentes périodes
- Dashboard admin: Vue globale pour tous les utilisateurs (si multi-tenant)
- Optimisation des coûts: Suggestions pour réduire les coûts (ex: utiliser un modèle moins cher)
Notes Importantes
- Les coûts sont en USD (dollars américains) car c'est la devise des APIs d'IA
- Mettre à jour régulièrement les tarifs dans
AIPricingcar ils changent - Prévoir une conversion USD -> EUR si nécessaire pour l'affichage
- Les tokens peuvent varier selon la langue (le français utilise plus de tokens que l'anglais)