Aller au contenu principal

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 messages
  • total_output_tokens: Somme des output_tokens de tous les messages
  • total_tokens: Total des tokens de la conversation
  • total_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:

  1. Header avec période sélectionnée

    • Sélecteur de période (7j, 30j, 90j, personnalisé)
    • Date picker pour période personnalisée
  2. 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)
  3. 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)
  4. 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
  5. 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:

  1. Enregistrer les tokens retournés par l'API
  2. Calculer le coût avec AIPricing.calculateCost()
  3. Sauvegarder dans la base de données
  4. 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

  1. Tests unitaires:

    • Calcul du coût avec différents modèles
    • Agrégation des statistiques
    • Parsing des nouveaux champs JSON
  2. Tests d'intégration:

    • Enregistrement d'un message avec coûts
    • Mise à jour des totaux de conversation
    • Récupération des statistiques
  3. Tests UI:

    • Affichage correct des coûts
    • Navigation vers la page de statistiques
    • Changement de période

5. Ordre d'Implémentation Recommandé

  1. Phase 1: Base de données

    • Ajouter les colonnes à Supabase
    • Créer les fonctions SQL
    • Créer la vue agrégée
  2. Phase 2: Entités et Modèles

    • Modifier AgentMessage et AgentConversation
    • Créer ConversationCostStats
    • Mettre à jour les modèles
  3. Phase 3: Repository

    • Ajouter les méthodes dans le repository
    • Implémenter getCostStats()
  4. Phase 4: Backend

    • Créer AIPricing
    • Modifier le backend pour enregistrer les tokens et coûts
  5. Phase 5: UI

    • Créer CostStatsPage
    • Ajouter les widgets de statistiques
    • Ajouter la navigation
  6. 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 AIPricing car 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)