CognxSafeTrack Claude Sonnet 4.6 commited on
Commit
98240fd
·
1 Parent(s): 9061927

feat(agentic-week1): conversational memory, RAG threshold, wallet alerts, weekly reports, campaign scheduling

Browse files

- AIAgentHandler: Redis sliding window (20 entries, TTL 24h) for conversation
history injected into system prompt — AI_AGENT now remembers context
- IndexingService.searchRelevantContext: cosine threshold 0.70 — returns '' if
no chunk is relevant so agent responds honestly instead of hallucinating
- add-hnsw-index.ts: one-shot HNSW index script (m=16, ef=64) for ~10x faster
pgvector cosine search on large knowledge bases
- scheduler.ts: hourly wallet alert (email via Brevo if < 3 days runway,
6h Redis suppression) + weekly report every Monday 07:00 UTC with trend vs
previous week and color-coded wallet status
- queue.ts + campaigns.ts: sendAt ISO-8601 parameter on broadcast/campaign
routes — BullMQ delay option schedules jobs natively, no cron needed
- docs/agentic/: roadmap updated with Semaine 1 marked complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

apps/api/src/routes/campaigns.ts CHANGED
@@ -29,23 +29,34 @@ export default async function campaignRoutes(fastify: FastifyInstance) {
29
  }
30
  });
31
 
32
- // Send Campaign to Broadcast List
33
  fastify.post('/:id/campaigns/send', async (req, reply) => {
 
 
 
 
 
 
 
 
34
  const { id: organizationId } = req.params as { id: string };
35
- const { listId, message } = req.body as { listId: string, message: string };
36
 
37
- if (!listId || !message) {
38
- return reply.code(400).send({ error: 'listId and message are required' });
39
  }
40
 
41
  try {
42
  const { scheduleBroadcast } = await import('../services/queue');
43
- await scheduleBroadcast({ organizationId, listId, message });
44
 
45
- return reply.code(202).send({
46
- ok: true,
47
- status: 'queued',
48
- message: 'Campagne en cours d\'envoi en arrière-plan'
 
 
 
49
  });
50
  } catch (err) {
51
  fastify.log.error(err);
@@ -74,34 +85,46 @@ export default async function campaignRoutes(fastify: FastifyInstance) {
74
  }
75
  });
76
 
77
- // New Broadcast Campaign Route
78
  fastify.post('/:id/campaigns/broadcast', async (req, reply) => {
 
 
 
 
 
 
 
 
 
 
79
  const { id: organizationId } = req.params as { id: string };
80
- const { message, listId, templateName, templateLanguage } = req.body as {
81
- message: string,
82
- listId?: string,
83
- templateName?: string,
84
- templateLanguage?: string
85
- };
86
 
87
  if (!message && !templateName) {
88
  return reply.code(400).send({ error: 'Message content or templateName is required' });
89
  }
 
 
 
90
 
91
  try {
92
  const { scheduleCampaign } = await import('../services/queue');
93
- await scheduleCampaign({
94
- organizationId,
95
- messageContent: message,
96
  listId,
97
  templateName,
98
- templateLanguage
 
99
  });
100
 
101
  return {
102
  ok: true,
103
- status: 'queued',
104
- message: 'Votre campagne a été mise en file d\'attente pour une diffusion progressive.'
 
 
 
105
  };
106
  } catch (err) {
107
  fastify.log.error(err);
 
29
  }
30
  });
31
 
32
+ // Send Campaign to Broadcast List (with optional sendAt scheduling)
33
  fastify.post('/:id/campaigns/send', async (req, reply) => {
34
+ const schema = z.object({
35
+ listId: z.string().uuid(),
36
+ message: z.string().min(1),
37
+ sendAt: z.string().datetime({ offset: true }).optional(),
38
+ });
39
+ const parsed = schema.safeParse(req.body);
40
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
41
+
42
  const { id: organizationId } = req.params as { id: string };
43
+ const { listId, message, sendAt } = parsed.data;
44
 
45
+ if (sendAt && new Date(sendAt) <= new Date()) {
46
+ return reply.code(400).send({ error: 'sendAt must be in the future' });
47
  }
48
 
49
  try {
50
  const { scheduleBroadcast } = await import('../services/queue');
51
+ await scheduleBroadcast({ organizationId, listId, message, sendAt });
52
 
53
+ return reply.code(202).send({
54
+ ok: true,
55
+ status: sendAt ? 'scheduled' : 'queued',
56
+ scheduledFor: sendAt ?? null,
57
+ message: sendAt
58
+ ? `Campagne programmée pour le ${new Date(sendAt).toLocaleString('fr-FR')}`
59
+ : "Campagne en cours d'envoi en arrière-plan",
60
  });
61
  } catch (err) {
62
  fastify.log.error(err);
 
85
  }
86
  });
87
 
88
+ // New Broadcast Campaign Route (with optional sendAt scheduling)
89
  fastify.post('/:id/campaigns/broadcast', async (req, reply) => {
90
+ const schema = z.object({
91
+ message: z.string().optional(),
92
+ listId: z.string().uuid().optional(),
93
+ templateName: z.string().optional(),
94
+ templateLanguage: z.string().optional(),
95
+ sendAt: z.string().datetime({ offset: true }).optional(),
96
+ });
97
+ const parsed = schema.safeParse(req.body);
98
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
99
+
100
  const { id: organizationId } = req.params as { id: string };
101
+ const { message, listId, templateName, templateLanguage, sendAt } = parsed.data;
 
 
 
 
 
102
 
103
  if (!message && !templateName) {
104
  return reply.code(400).send({ error: 'Message content or templateName is required' });
105
  }
106
+ if (sendAt && new Date(sendAt) <= new Date()) {
107
+ return reply.code(400).send({ error: 'sendAt must be in the future' });
108
+ }
109
 
110
  try {
111
  const { scheduleCampaign } = await import('../services/queue');
112
+ await scheduleCampaign({
113
+ organizationId,
114
+ messageContent: message ?? '',
115
  listId,
116
  templateName,
117
+ templateLanguage,
118
+ sendAt,
119
  });
120
 
121
  return {
122
  ok: true,
123
+ status: sendAt ? 'scheduled' : 'queued',
124
+ scheduledFor: sendAt ?? null,
125
+ message: sendAt
126
+ ? `Campagne programmée pour le ${new Date(sendAt).toLocaleString('fr-FR')}`
127
+ : "Votre campagne a été mise en file d'attente pour une diffusion progressive.",
128
  };
129
  } catch (err) {
130
  fastify.log.error(err);
apps/api/src/services/queue.ts CHANGED
@@ -113,25 +113,37 @@ export async function scheduleInboundMessage(payload: { phone: string, text: str
113
  });
114
  }
115
 
116
- /** 📢 BROADCAST: Enqueue a mass message task. */
117
- export async function scheduleBroadcast(payload: { organizationId: string, listId: string, message: string }) {
118
- await whatsappQueue.add('send-broadcast', payload, {
119
- attempts: 1, // We handle retry logic within the loop if needed, but the whole job shouldn't necessarily retry
120
- removeOnComplete: true
 
 
 
 
 
 
 
 
121
  });
122
  }
123
 
124
- /** 🚀 CAMPAIGN: Enqueue a mass campaign task for all contacts or a specific list. */
125
- export async function scheduleCampaign(payload: {
126
- organizationId: string,
127
- messageContent: string,
128
- listId?: string,
129
- templateName?: string,
130
- templateLanguage?: string
 
131
  }) {
132
- await whatsappQueue.add('process-campaign', payload, {
 
 
133
  attempts: 1,
134
- removeOnComplete: true
 
135
  });
136
  }
137
 
 
113
  });
114
  }
115
 
116
+ /** 📢 BROADCAST: Enqueue a mass message task with optional future scheduling. */
117
+ export async function scheduleBroadcast(payload: {
118
+ organizationId: string;
119
+ listId: string;
120
+ message: string;
121
+ sendAt?: string; // ISO 8601 — if set, job is delayed until this time
122
+ }) {
123
+ const { sendAt, ...data } = payload;
124
+ const delayMs = sendAt ? Math.max(0, new Date(sendAt).getTime() - Date.now()) : 0;
125
+ await whatsappQueue.add('send-broadcast', data, {
126
+ attempts: 1,
127
+ removeOnComplete: true,
128
+ ...(delayMs > 0 ? { delay: delayMs } : {}),
129
  });
130
  }
131
 
132
+ /** 🚀 CAMPAIGN: Enqueue a mass campaign task with optional future scheduling. */
133
+ export async function scheduleCampaign(payload: {
134
+ organizationId: string;
135
+ messageContent: string;
136
+ listId?: string;
137
+ templateName?: string;
138
+ templateLanguage?: string;
139
+ sendAt?: string; // ISO 8601 — if set, job is delayed until this time
140
  }) {
141
+ const { sendAt, ...data } = payload;
142
+ const delayMs = sendAt ? Math.max(0, new Date(sendAt).getTime() - Date.now()) : 0;
143
+ await whatsappQueue.add('process-campaign', data, {
144
  attempts: 1,
145
+ removeOnComplete: true,
146
+ ...(delayMs > 0 ? { delay: delayMs } : {}),
147
  });
148
  }
149
 
apps/whatsapp-worker/src/handlers/AIAgentHandler.ts CHANGED
@@ -1,56 +1,94 @@
1
  import { MessageContext, MessageHandler } from './types';
2
  import { logger } from '../logger';
3
  import { AIPedagogyService } from '../services/ai-pedagogy';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  export class AIAgentHandler implements MessageHandler {
6
  async canHandle(ctx: MessageContext): Promise<boolean> {
7
- // Only handle if the organization mode is explicitly AI_AGENT
8
  return ctx.organization?.mode === 'AI_AGENT';
9
  }
10
 
11
  async handle(ctx: MessageContext): Promise<boolean> {
12
  const { phone, text, organization, whatsappQueue, traceId } = ctx;
13
-
14
  if (!organization) return false;
15
 
16
  logger.info(`${traceId} Processing via AIAgentHandler for Org: ${organization.id}`);
17
 
18
  try {
19
- // 1. Prepare the system prompt
20
- const userLang = ctx.user?.language || 'FR';
21
- let systemPrompt = organization.customPrompt || "Tu es un assistant virtuel utile et poli.";
 
 
 
22
  systemPrompt += `\n\nIMPORTANT: Réponds TOUJOURS en langue: ${userLang}.`;
23
-
24
- // 2. RAG / Knowledge Base logic
 
 
 
 
 
 
 
 
 
25
  if (organization.knowledgeBaseUrl) {
26
  const { IndexingService } = await import('../services/indexing');
27
  const context = await IndexingService.searchRelevantContext(organization.id, text);
28
-
29
  if (context) {
30
- systemPrompt += `\n\nCONTEXTE RELEVANT DE LA BASE DE CONNAISSANCES:\n${context}\n\nUtilise uniquement ce contexte pour répondre si la question concerne les produits ou services de l'entreprise.`;
31
  }
32
  }
33
 
34
- // 3. Generate response via API
35
  const responseText = await AIPedagogyService.generateChat(systemPrompt, text, organization.id);
36
 
37
- // 4. Send response back to user
 
 
 
38
  await whatsappQueue.add('send-message-direct', {
39
  phone,
40
  text: responseText,
41
- organizationId: organization.id
42
  });
43
 
44
  return true;
45
-
46
  } catch (error) {
47
  logger.error(`${traceId} AIAgentHandler failed: ${error}`);
48
  await whatsappQueue.add('send-message-direct', {
49
  phone,
50
- text: "Désolé, je rencontre une difficulté technique. Veuillez réessayer plus tard.",
51
- organizationId: organization.id
52
  });
53
- return true; // We handled the error, stop propagation
54
  }
55
  }
56
  }
 
1
  import { MessageContext, MessageHandler } from './types';
2
  import { logger } from '../logger';
3
  import { AIPedagogyService } from '../services/ai-pedagogy';
4
+ import { redis } from '../lib/redis';
5
+
6
+ const CONV_HISTORY_LIMIT = 20; // Max entries in Redis list (10 exchanges)
7
+ const CONV_TTL_SECONDS = 86_400; // 24h TTL — conversation expires after inactivity
8
+
9
+ interface ConvMessage { role: 'user' | 'assistant'; content: string }
10
+
11
+ async function loadHistory(key: string): Promise<ConvMessage[]> {
12
+ try {
13
+ const raw = await redis.lrange(key, 0, CONV_HISTORY_LIMIT - 1);
14
+ return raw.reverse().map(r => JSON.parse(r) as ConvMessage);
15
+ } catch {
16
+ return [];
17
+ }
18
+ }
19
+
20
+ async function saveHistory(key: string, user: string, assistant: string): Promise<void> {
21
+ try {
22
+ const userEntry = JSON.stringify({ role: 'user', content: user });
23
+ const assistantEntry = JSON.stringify({ role: 'assistant', content: assistant });
24
+ await redis.lpush(key, assistantEntry, userEntry);
25
+ await redis.ltrim(key, 0, CONV_HISTORY_LIMIT - 1);
26
+ await redis.expire(key, CONV_TTL_SECONDS);
27
+ } catch (err) {
28
+ logger.warn({ err }, '[AIAgent] Failed to persist conversation history');
29
+ }
30
+ }
31
 
32
  export class AIAgentHandler implements MessageHandler {
33
  async canHandle(ctx: MessageContext): Promise<boolean> {
 
34
  return ctx.organization?.mode === 'AI_AGENT';
35
  }
36
 
37
  async handle(ctx: MessageContext): Promise<boolean> {
38
  const { phone, text, organization, whatsappQueue, traceId } = ctx;
 
39
  if (!organization) return false;
40
 
41
  logger.info(`${traceId} Processing via AIAgentHandler for Org: ${organization.id}`);
42
 
43
  try {
44
+ const userLang = ctx.user?.language || 'FR';
45
+ const userId = ctx.user?.id ?? phone;
46
+ const historyKey = `conv:${userId}:${organization.id}`;
47
+
48
+ // 1. Prepare system prompt
49
+ let systemPrompt = organization.customPrompt || 'Tu es un assistant virtuel utile et poli.';
50
  systemPrompt += `\n\nIMPORTANT: Réponds TOUJOURS en langue: ${userLang}.`;
51
+
52
+ // 2. Load conversation history and inject as context
53
+ const history = await loadHistory(historyKey);
54
+ if (history.length > 0) {
55
+ const historyText = history
56
+ .map(m => `${m.role === 'user' ? 'Client' : 'Toi'}: ${m.content}`)
57
+ .join('\n');
58
+ systemPrompt += `\n\nHISTORIQUE DE LA CONVERSATION (du plus ancien au plus récent):\n${historyText}\n\nContinue la conversation de façon cohérente avec cet historique.`;
59
+ }
60
+
61
+ // 3. RAG — Knowledge Base context (filtered by relevance threshold)
62
  if (organization.knowledgeBaseUrl) {
63
  const { IndexingService } = await import('../services/indexing');
64
  const context = await IndexingService.searchRelevantContext(organization.id, text);
 
65
  if (context) {
66
+ systemPrompt += `\n\nCONTEXTE DE LA BASE DE CONNAISSANCES:\n${context}\n\nUtilise ce contexte pour répondre si la question concerne les produits ou services de l'entreprise.`;
67
  }
68
  }
69
 
70
+ // 4. Generate response
71
  const responseText = await AIPedagogyService.generateChat(systemPrompt, text, organization.id);
72
 
73
+ // 5. Persist exchange to Redis history (fire-and-forget)
74
+ saveHistory(historyKey, text, responseText);
75
+
76
+ // 6. Send response
77
  await whatsappQueue.add('send-message-direct', {
78
  phone,
79
  text: responseText,
80
+ organizationId: organization.id,
81
  });
82
 
83
  return true;
 
84
  } catch (error) {
85
  logger.error(`${traceId} AIAgentHandler failed: ${error}`);
86
  await whatsappQueue.add('send-message-direct', {
87
  phone,
88
+ text: 'Désolé, je rencontre une difficulté technique. Veuillez réessayer plus tard.',
89
+ organizationId: organization.id,
90
  });
91
+ return true;
92
  }
93
  }
94
  }
apps/whatsapp-worker/src/index.ts CHANGED
@@ -257,9 +257,11 @@ const start = async () => {
257
  logger.info(`🚀 WhatsApp Worker + Bridge listening on port ${PORT}`);
258
 
259
  // Start the daily cron scheduler + token expiry monitor
260
- const { startDailyScheduler, startTokenExpiryMonitor } = await import('./scheduler');
261
  startDailyScheduler();
262
  startTokenExpiryMonitor();
 
 
263
  } catch (err) {
264
  logger.error('Failed to start worker server:', err);
265
  process.exit(1);
 
257
  logger.info(`🚀 WhatsApp Worker + Bridge listening on port ${PORT}`);
258
 
259
  // Start the daily cron scheduler + token expiry monitor
260
+ const { startDailyScheduler, startTokenExpiryMonitor, startWalletAlertMonitor, startWeeklyReportScheduler } = await import('./scheduler');
261
  startDailyScheduler();
262
  startTokenExpiryMonitor();
263
+ startWalletAlertMonitor();
264
+ startWeeklyReportScheduler();
265
  } catch (err) {
266
  logger.error('Failed to start worker server:', err);
267
  process.exit(1);
apps/whatsapp-worker/src/scheduler.ts CHANGED
@@ -2,6 +2,7 @@ import { logger } from './logger';
2
  import cron from 'node-cron';
3
  import { prisma } from './services/prisma';
4
  import { whatsappQueue } from './lib/queues';
 
5
 
6
  export function startDailyScheduler() {
7
  // Runs at 08:00 AM every day (Dakar time = UTC+0 in winter, so 8 UTC = 8 Dakar)
@@ -120,3 +121,177 @@ export function startTokenExpiryMonitor() {
120
 
121
  logger.info('[TOKEN-MONITOR] Meta token expiry monitor initialized (cron: every Monday 09:00 UTC).');
122
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import cron from 'node-cron';
3
  import { prisma } from './services/prisma';
4
  import { whatsappQueue } from './lib/queues';
5
+ import { EmailService } from './services/email';
6
 
7
  export function startDailyScheduler() {
8
  // Runs at 08:00 AM every day (Dakar time = UTC+0 in winter, so 8 UTC = 8 Dakar)
 
121
 
122
  logger.info('[TOKEN-MONITOR] Meta token expiry monitor initialized (cron: every Monday 09:00 UTC).');
123
  }
124
+
125
+ // ─── Wallet Alert Monitor ────────────────────────────────────────────────────
126
+ // Runs every hour. Emails the org admin if wallet < 3 days of runway left.
127
+
128
+ async function findOrgAdminEmail(organizationId: string): Promise<{ email: string; name: string } | null> {
129
+ const admin = await prisma.user.findFirst({
130
+ where: { organizationId, role: { in: ['ORG_ADMIN', 'ADMIN'] }, email: { not: null } },
131
+ select: { email: true, name: true },
132
+ });
133
+ return admin ? { email: admin.email!, name: admin.name ?? 'Administrateur' } : null;
134
+ }
135
+
136
+ export function startWalletAlertMonitor() {
137
+ cron.schedule('0 * * * *', async () => {
138
+ logger.info('[WALLET-MONITOR] Running hourly wallet check...');
139
+ try {
140
+ const now = new Date();
141
+ const weekAgo = new Date(now.getTime() - 7 * 86_400_000);
142
+
143
+ const orgsAtRisk = await prisma.organization.findMany({
144
+ where: { walletBalance: { lte: 500 }, isHardStopped: false, subscriptionStatus: 'ACTIVE' },
145
+ select: { id: true, name: true, walletBalance: true },
146
+ });
147
+
148
+ for (const org of orgsAtRisk) {
149
+ // Calculate 7-day burn rate
150
+ const debits = await prisma.walletTransaction.aggregate({
151
+ where: { organizationId: org.id, amount: { lt: 0 }, createdAt: { gte: weekAgo } },
152
+ _sum: { amount: true },
153
+ });
154
+ const weeklyDebit = Math.abs(debits._sum.amount ?? 0);
155
+ const dailyBurn = weeklyDebit / 7;
156
+ const daysLeft = dailyBurn > 0 ? org.walletBalance / dailyBurn : null;
157
+
158
+ // Only alert if < 3 days of runway (and we have a burn rate)
159
+ if (daysLeft === null || daysLeft > 3) continue;
160
+
161
+ const admin = await findOrgAdminEmail(org.id);
162
+ if (!admin) continue;
163
+
164
+ const alertKey = `wallet:alert:${org.id}`;
165
+ // Avoid re-alerting within 6 hours for same org
166
+ const { redis } = await import('./lib/redis');
167
+ const alreadyAlerted = await redis.get(alertKey);
168
+ if (alreadyAlerted) continue;
169
+
170
+ logger.warn({ organizationId: org.id, daysLeft: daysLeft.toFixed(1) }, '[WALLET-MONITOR] Sending low-wallet alert');
171
+ await EmailService.sendEmail({
172
+ to: admin.email,
173
+ subject: `⚠️ Solde faible — ${org.name} (${Math.round(daysLeft)} jour(s) restant${daysLeft < 1 ? ' — SERVICE EN DANGER' : ''})`,
174
+ htmlContent: `
175
+ <div style="font-family:sans-serif;max-width:600px;margin:auto;padding:20px;border:1px solid #eee;border-radius:10px;">
176
+ <h2 style="color:${daysLeft < 1 ? '#dc2626' : '#d97706'};">
177
+ ${daysLeft < 1 ? '🚨 Service en danger' : '⚠️ Solde faible'} — ${org.name}
178
+ </h2>
179
+ <p>Bonjour ${admin.name},</p>
180
+ <p>À votre rythme de consommation actuel (<strong>${Math.round(dailyBurn)} crédits/jour</strong>),
181
+ il reste environ <strong>${daysLeft < 1 ? 'moins d\'1 jour' : `${Math.round(daysLeft)} jour(s)`}</strong>
182
+ avant que le service soit suspendu.</p>
183
+ <div style="background:#fef3c7;border:1px solid #fbbf24;border-radius:8px;padding:16px;margin:20px 0;">
184
+ <p style="margin:0;font-size:1.25rem;font-weight:bold;">Solde actuel : ${org.walletBalance} crédits (= ${org.walletBalance * 10} FCFA)</p>
185
+ </div>
186
+ <p>Rechargez votre wallet pour éviter toute interruption de service.</p>
187
+ <a href="https://admin.xamle.studio/billing" style="display:inline-block;padding:12px 24px;background:#059669;color:white;text-decoration:none;border-radius:8px;font-weight:bold;">Recharger mon wallet</a>
188
+ <p style="color:#64748b;font-size:0.875rem;margin-top:40px;">L'équipe Xamlé Studio</p>
189
+ </div>`,
190
+ }).catch(e => logger.error({ e }, '[WALLET-MONITOR] Alert email failed'));
191
+
192
+ await redis.set(alertKey, '1', 'EX', 6 * 3600); // Suppress for 6h
193
+ }
194
+ } catch (err) {
195
+ logger.error({ err }, '[WALLET-MONITOR] Hourly check failed');
196
+ }
197
+ });
198
+
199
+ logger.info('[WALLET-MONITOR] Hourly wallet alert monitor initialized.');
200
+ }
201
+
202
+ // ─── Weekly Report ───────────────────────────────────────────────────────────
203
+ // Runs every Monday at 07:00 UTC. Sends a usage summary to each org admin.
204
+
205
+ const FEATURE_LABELS: Record<string, string> = {
206
+ LESSON: 'Leçons', FEEDBACK: 'Feedbacks', DEEPDIVE: 'Approfondissements',
207
+ TRANSCRIPTION: 'Transcriptions audio', IMAGE_ANALYSIS: 'Analyses image',
208
+ CAMPAIGN: 'Campagnes', ONBOARDING: 'Onboarding', OTHER: 'Autres',
209
+ };
210
+
211
+ export function startWeeklyReportScheduler() {
212
+ cron.schedule('0 7 * * 1', async () => {
213
+ logger.info('[WEEKLY-REPORT] Generating weekly reports...');
214
+ try {
215
+ const weekAgo = new Date(Date.now() - 7 * 86_400_000);
216
+ const prevWeekAgo = new Date(Date.now() - 14 * 86_400_000);
217
+
218
+ const orgs = await prisma.organization.findMany({
219
+ where: { subscriptionStatus: 'ACTIVE' },
220
+ select: { id: true, name: true, walletBalance: true, whatsappMessagesSent: true, aiCreditsUsed: true, aiCreditsLimit: true },
221
+ });
222
+
223
+ for (const org of orgs) {
224
+ const admin = await findOrgAdminEmail(org.id);
225
+ if (!admin) continue;
226
+
227
+ const [thisWeek, prevWeek, breakdown] = await Promise.all([
228
+ prisma.usageEvent.aggregate({
229
+ where: { organizationId: org.id, createdAt: { gte: weekAgo }, type: { not: 'WHATSAPP_SENT' } },
230
+ _sum: { costUsd: true }, _count: { id: true },
231
+ }),
232
+ prisma.usageEvent.aggregate({
233
+ where: { organizationId: org.id, createdAt: { gte: prevWeekAgo, lt: weekAgo }, type: { not: 'WHATSAPP_SENT' } },
234
+ _count: { id: true },
235
+ }),
236
+ prisma.usageEvent.groupBy({
237
+ by: ['feature'],
238
+ where: { organizationId: org.id, createdAt: { gte: weekAgo }, type: { not: 'WHATSAPP_SENT' } },
239
+ _count: { id: true },
240
+ orderBy: { _count: { id: 'desc' } },
241
+ take: 1,
242
+ }),
243
+ ]);
244
+
245
+ const calls = thisWeek._count.id;
246
+ const prevCalls = prevWeek._count.id;
247
+ const costFcfa = Math.round((thisWeek._sum.costUsd ?? 0) * 600);
248
+ const topFeat = FEATURE_LABELS[breakdown[0]?.feature ?? ''] ?? 'N/A';
249
+ const trend = prevCalls > 0 ? Math.round(((calls - prevCalls) / prevCalls) * 100) : 0;
250
+ const trendStr = trend >= 0 ? `+${trend}%` : `${trend}%`;
251
+ const trendColor = trend >= 0 ? '#059669' : '#dc2626';
252
+
253
+ if (calls === 0 && org.walletBalance > 500) continue; // Skip inactive orgs with healthy balance
254
+
255
+ await EmailService.sendEmail({
256
+ to: admin.email,
257
+ subject: `📊 Rapport hebdomadaire Xamlé — ${org.name}`,
258
+ htmlContent: `
259
+ <div style="font-family:sans-serif;max-width:600px;margin:auto;padding:20px;border:1px solid #eee;border-radius:10px;">
260
+ <h2 style="color:#1e293b;">📊 Rapport hebdomadaire — ${org.name}</h2>
261
+ <p>Bonjour ${admin.name}, voici votre résumé de la semaine écoulée.</p>
262
+ <table style="width:100%;border-collapse:collapse;margin:20px 0;">
263
+ <tr style="background:#f8fafc;">
264
+ <td style="padding:10px;border:1px solid #e2e8f0;">Appels IA cette semaine</td>
265
+ <td style="padding:10px;border:1px solid #e2e8f0;font-weight:bold;">${calls.toLocaleString('fr-FR')} <span style="color:${trendColor};font-size:0.875rem;">(${trendStr} vs sem. précédente)</span></td>
266
+ </tr>
267
+ <tr>
268
+ <td style="padding:10px;border:1px solid #e2e8f0;">Coût IA</td>
269
+ <td style="padding:10px;border:1px solid #e2e8f0;font-weight:bold;">${costFcfa.toLocaleString('fr-FR')} FCFA</td>
270
+ </tr>
271
+ <tr style="background:#f8fafc;">
272
+ <td style="padding:10px;border:1px solid #e2e8f0;">Fonctionnalité principale</td>
273
+ <td style="padding:10px;border:1px solid #e2e8f0;font-weight:bold;">${topFeat}</td>
274
+ </tr>
275
+ <tr>
276
+ <td style="padding:10px;border:1px solid #e2e8f0;">Solde wallet</td>
277
+ <td style="padding:10px;border:1px solid #e2e8f0;font-weight:bold;color:${org.walletBalance < 200 ? '#dc2626' : '#059669'};">${org.walletBalance} crédits (${org.walletBalance * 10} FCFA)</td>
278
+ </tr>
279
+ <tr style="background:#f8fafc;">
280
+ <td style="padding:10px;border:1px solid #e2e8f0;">Crédits IA (mois en cours)</td>
281
+ <td style="padding:10px;border:1px solid #e2e8f0;font-weight:bold;">${org.aiCreditsUsed} / ${org.aiCreditsLimit}</td>
282
+ </tr>
283
+ </table>
284
+ <a href="https://admin.xamle.studio/analytics" style="display:inline-block;padding:12px 24px;background:#4f46e5;color:white;text-decoration:none;border-radius:8px;font-weight:bold;">Voir le détail complet</a>
285
+ <p style="color:#64748b;font-size:0.875rem;margin-top:40px;">L'équipe Xamlé Studio</p>
286
+ </div>`,
287
+ }).catch(e => logger.error({ e, orgId: org.id }, '[WEEKLY-REPORT] Email failed'));
288
+ }
289
+
290
+ logger.info('[WEEKLY-REPORT] Weekly reports sent.');
291
+ } catch (err) {
292
+ logger.error({ err }, '[WEEKLY-REPORT] Failed');
293
+ }
294
+ });
295
+
296
+ logger.info('[WEEKLY-REPORT] Weekly report scheduler initialized (cron: every Monday 07:00 UTC).');
297
+ }
apps/whatsapp-worker/src/services/indexing.ts CHANGED
@@ -198,27 +198,41 @@ export class IndexingService {
198
  }
199
 
200
  /**
201
- * Searches for relevant chunks based on a query string.
 
 
202
  */
203
- static async searchRelevantContext(organizationId: string, query: string, limit: number = 3): Promise<string> {
 
 
 
 
 
204
  try {
205
  const queryEmbedding = await this.generateEmbedding(query);
206
  // Prisma.raw is safe here: embedding is machine-generated floats from OpenAI, not user input
207
  const vecRaw = Prisma.raw(`'[${queryEmbedding.join(',')}]'::vector`);
208
 
209
- // Cosine similarity search using pgvector
210
- const results: any[] = await prisma.$queryRaw(Prisma.sql`
211
- SELECT content, 1 - (embedding <=> ${vecRaw}) as similarity
212
  FROM "KnowledgeBaseEntry"
213
  WHERE "organizationId" = ${organizationId}
 
214
  ORDER BY embedding <=> ${vecRaw}
215
  LIMIT ${limit}
216
  `);
217
 
 
 
 
 
 
 
218
  return results.map(r => r.content).join('\n\n---\n\n');
219
  } catch (error) {
220
  logger.error(`[RETRIEVAL] Search failed: ${error}`);
221
- return "";
222
  }
223
  }
224
  }
 
198
  }
199
 
200
  /**
201
+ * Searches for relevant chunks using cosine similarity with a minimum relevance threshold.
202
+ * Returns empty string if no chunk exceeds the threshold — the agent will then respond
203
+ * honestly that it doesn't have specific information on the topic.
204
  */
205
+ static async searchRelevantContext(
206
+ organizationId: string,
207
+ query: string,
208
+ limit: number = 3,
209
+ threshold: number = 0.70
210
+ ): Promise<string> {
211
  try {
212
  const queryEmbedding = await this.generateEmbedding(query);
213
  // Prisma.raw is safe here: embedding is machine-generated floats from OpenAI, not user input
214
  const vecRaw = Prisma.raw(`'[${queryEmbedding.join(',')}]'::vector`);
215
 
216
+ // Cosine similarity search via HNSW index (if created) — threshold filters irrelevant chunks
217
+ const results: Array<{ content: string; similarity: number }> = await prisma.$queryRaw(Prisma.sql`
218
+ SELECT content, 1 - (embedding <=> ${vecRaw}) AS similarity
219
  FROM "KnowledgeBaseEntry"
220
  WHERE "organizationId" = ${organizationId}
221
+ AND 1 - (embedding <=> ${vecRaw}) > ${threshold}
222
  ORDER BY embedding <=> ${vecRaw}
223
  LIMIT ${limit}
224
  `);
225
 
226
+ if (results.length === 0) {
227
+ logger.debug(`[RETRIEVAL] No chunks above threshold ${threshold} for org ${organizationId}`);
228
+ return '';
229
+ }
230
+
231
+ logger.debug(`[RETRIEVAL] Found ${results.length} relevant chunks (best: ${results[0]?.similarity?.toFixed(3)})`);
232
  return results.map(r => r.content).join('\n\n---\n\n');
233
  } catch (error) {
234
  logger.error(`[RETRIEVAL] Search failed: ${error}`);
235
+ return '';
236
  }
237
  }
238
  }
docs/agentic/audit_agentic_complet_2026.md ADDED
@@ -0,0 +1,1339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Audit Complet Xamlé — Fonctionnalités, IA Agentique & Avancées Technologiques
2
+
3
+ > **Date :** 13 mai 2026
4
+ > **Scope :** Audit exhaustif de toute la plateforme (frontend, backend, worker, IA, database, UX/UI) + roadmap agentique complète
5
+ > **Objectif :** Cartographier tout ce qui existe, identifier tout ce qui peut être automatisé, et exploiter les dernières avancées technologiques disponibles aujourd'hui
6
+
7
+ ---
8
+
9
+ ## Table des matières
10
+
11
+ 1. [Vue d'ensemble de la plateforme](#1-vue-densemble)
12
+ 2. [Inventaire complet — Base de données](#2-base-de-données)
13
+ 3. [Inventaire complet — Backend (API Fastify)](#3-backend-api-fastify)
14
+ 4. [Inventaire complet — Worker BullMQ](#4-worker-bullmq)
15
+ 5. [Inventaire complet — SDK IA](#5-sdk-ia)
16
+ 6. [Inventaire complet — Frontend Admin](#6-frontend-admin)
17
+ 7. [Inventaire complet — Prompts & Templates IA](#7-prompts--templates-ia)
18
+ 8. [Tarification & Wallet](#8-tarification--wallet)
19
+ 9. [Lacunes & dette technique identifiées](#9-lacunes--dette-technique)
20
+ 10. [Roadmap IA Agentique — tout ce qui peut être automatisé](#10-roadmap-ia-agentique)
21
+ 11. [Dernières avancées technologiques applicables](#11-dernières-avancées-technologiques)
22
+ 12. [Matrice de priorisation](#12-matrice-de-priorisation)
23
+
24
+ ---
25
+
26
+ ## 1. Vue d'ensemble
27
+
28
+ ### Qu'est-ce que Xamlé ?
29
+
30
+ Xamlé est une **plateforme SaaS multi-tenant d'automatisation WhatsApp Business** destinée aux entreprises et organisations africaines. Elle permet de gérer la relation client, la formation, et le support via WhatsApp en combinant IA générative, workflows métier et facturation à la consommation.
31
+
32
+ ### Architecture globale
33
+
34
+ ```
35
+ ┌──────────────────────────────────────────────────────────────────┐
36
+ │ Meta WhatsApp Business API │
37
+ └─────────────────────────┬────────────────────────────────────────┘
38
+ │ Webhook POST
39
+
40
+ ┌──────────────────────────────────────────────────────────────────┐
41
+ │ apps/api (Fastify v4 — port 3000) │
42
+ │ • Répond 200 OK < 100ms (règle absolue) │
43
+ │ • Vérifie X-Hub-Signature-256 │
44
+ │ • Enfile le job dans BullMQ via bridge localhost:8082 │
45
+ └─────────────────────────┬────────────────────────────────────────┘
46
+ │ HTTP → BullMQ
47
+
48
+ ┌──────────────────────────────────────────────────────────────────┐
49
+ │ apps/whatsapp-worker (BullMQ consumer) │
50
+ │ • Handlers en chaîne : AIAgent → Onboarding → Command → │
51
+ │ Navigation → Exercise │
52
+ │ • Toute la logique métier ici │
53
+ └──────────┬─────────────────────────────────┬────────────────────┘
54
+ │ │
55
+ ▼ ▼
56
+ ┌──────────────────────┐ ┌─────────────────────────────────┐
57
+ │ packages/database │ │ packages/ai-sdk │
58
+ │ (Prisma + pgvector) │ │ OpenAI / Gemini / BYOK │
59
+ │ Neon PostgreSQL │ │ Whisper STT / DALL-E / TTS │
60
+ └──────────────────────┘ └─────────────────────────────────┘
61
+
62
+
63
+ ┌──────────────────────────────────────────────────────────────────┐
64
+ │ apps/admin (React/Vite — port 5173) │
65
+ │ Dashboard multi-tenant pour les super admins et org admins │
66
+ └──────────────────────────────────────────────────────────────────┘
67
+ ```
68
+
69
+ ### 4 modes d'opération
70
+
71
+ | Mode | Usage | Différenciateur |
72
+ |------|-------|-----------------|
73
+ | **EDTECH** | Formations structurées 21 jours via WhatsApp | Parcours pédagogique jour/jour + feedback IA |
74
+ | **CRM_MARKETING** | Campagnes broadcast, gestion contacts | Listes de diffusion + templates approuvés Meta |
75
+ | **AI_AGENT** | Bot autonome 24h/24 | RAG sur base de connaissance + personnalité configurée |
76
+ | **CUSTOMER_SERVICE** | Support client avec escalade humaine | Mix bot + agent humain |
77
+
78
+ ---
79
+
80
+ ## 2. Base de données
81
+
82
+ **Technologie :** PostgreSQL (Neon serverless) + Prisma ORM + pgvector extension
83
+
84
+ ### 2.1 Modèles principaux
85
+
86
+ #### Organization (Racine multi-tenant)
87
+ ```
88
+ id, name, slug
89
+ mode: EDTECH | WEBHOOK | AI_AGENT | CRM_MARKETING | PEDAGOGY | CUSTOMER_SERVICE
90
+ wabaId, metaBusinessId, systemUserToken, systemUserTokenIssuedAt
91
+ customPrompt, personalityConfig (JSON), flowConfig (JSON), brandingData (JSON)
92
+ subscriptionPlan: STARTER | GROWTH | SCALE | ENTERPRISE
93
+ aiCreditsUsed, aiCreditsLimit, walletBalance, isHardStopped
94
+ openAiApiKey, googleAiApiKey (BYOK — chiffré au repos)
95
+ ```
96
+
97
+ #### User (Apprenants & membres)
98
+ ```
99
+ phone (unique par org), email, name
100
+ role: STUDENT | ADMIN | ORG_MEMBER | ORG_ADMIN | SUPER_ADMIN
101
+ language: FR | EN | ES | PT | WOLOF
102
+ activity (secteur), currentStreak, longestStreak, lastActivityAt
103
+ businessProfile → JSON (données entrepreneuriales)
104
+ ```
105
+
106
+ #### Track & TrackDay (Contenu pédagogique)
107
+ ```
108
+ Track: title, durationDays, language, isPremium, price
109
+ TrackDay:
110
+ dayNumber (float — permet jour 1.5 "bis")
111
+ lessonText / audioUrl / imageUrl / videoUrl
112
+ exerciseType: TEXT | AUDIO | BUTTON
113
+ exercisePrompt, exerciseCriteria (JSON)
114
+ buttonsJson (choix interactifs multi-langue)
115
+ unlockCondition
116
+ ```
117
+
118
+ #### Enrollment (État d'apprentissage)
119
+ ```
120
+ userId + trackId (unique)
121
+ status: ACTIVE | COMPLETED | DROPPED
122
+ currentDay (float), lastActivityAt
123
+ startedAt, completedAt
124
+ ```
125
+
126
+ #### UserProgress (Machine à états exercice)
127
+ ```
128
+ exerciseStatus: PENDING → PENDING_REMEDIATION → COMPLETED
129
+ → PENDING_DEEPDIVE → COMPLETED
130
+ → PENDING_REVIEW (révision humaine)
131
+ badges (JSON), behavioralScoring (JSON)
132
+ confidenceScore, iterationCount
133
+ adminTranscription, overrideAudioUrl
134
+ previousResponses (JSON array)
135
+ ```
136
+
137
+ #### KnowledgeBaseEntry (RAG — Agent IA)
138
+ ```
139
+ content (chunk texte), embedding (pgvector)
140
+ metadata (JSON : source, page, title)
141
+ organizationId
142
+ createdAt
143
+ ```
144
+
145
+ #### Message & Contact
146
+ ```
147
+ Message: userId | contactId, direction (INBOUND/OUTBOUND)
148
+ mediaUrl, mediaId, status (SENT/DELIVERED/READ)
149
+ content, createdAt
150
+ Contact: phoneNumber, name, attributes (JSON colonnes Excel dynamiques)
151
+ language, organizationId
152
+ ```
153
+
154
+ #### WalletTransaction & UsageEvent
155
+ ```
156
+ WalletTransaction: amount, type (TOP_UP/DEBIT_AI/DEBIT_BROADCAST)
157
+ balanceAfter, byok (flag BYOK)
158
+ UsageEvent: type (AI_TEXT/AI_AUDIO/AI_IMAGE/WHATSAPP_SENT)
159
+ feature (LESSON/FEEDBACK/DEEPDIVE/TRANSCRIPTION/CAMPAIGN...)
160
+ provider (GEMINI/OPENAI/META)
161
+ tokensIn, tokensOut, costUsd, durationMs
162
+ ```
163
+
164
+ #### CampaignHistory & AnalyticsLog
165
+ ```
166
+ CampaignHistory: status (SENT/DELIVERED/READ/FAILED), whatsappMessageId
167
+ AnalyticsLog: eventType (CLICK/READ/RESPONSE), metadata
168
+ ```
169
+
170
+ ### 2.2 Enums clés
171
+ - `Language` : FR, EN, ES, PT, WOLOF
172
+ - `ExerciseStatus` : PENDING, PENDING_REMEDIATION, PENDING_REVIEW, COMPLETED, PENDING_DEEPDIVE
173
+ - `SubscriptionPlan` : STARTER, GROWTH, SCALE, ENTERPRISE
174
+ - `UsageFeature` : LESSON, FEEDBACK, DEEPDIVE, TRANSCRIPTION, IMAGE_ANALYSIS, CAMPAIGN, ONBOARDING, OTHER
175
+
176
+ ### 2.3 Particularités techniques
177
+ - **pgvector** : Colonne `embedding` de type `Unsupported("vector")` — recherche cosinus pour RAG
178
+ - **Multi-tenant via AsyncLocalStorage** : `tenantContext` injecte `organizationId` dans toutes les requêtes Prisma automatiquement
179
+ - **Extension Prisma** : `packages/database/src/extension.ts` — auto-filtre chaque query par `organizationId`
180
+ - **Modèles exclus du filtre tenant** : `Organization`, `TrainingData`, `NormalizationRule`
181
+
182
+ ---
183
+
184
+ ## 3. Backend — API Fastify
185
+
186
+ **Localisation :** `apps/api/src/routes/`
187
+
188
+ ### 3.1 Authentification & Middleware
189
+
190
+ ```
191
+ x-api-key (ADMIN_API_KEY, min 32 chars) → bypass JWT (appels worker→API)
192
+ JWT via @fastify/jwt → { id, role, organizationId }
193
+ rateLimit (plugin Fastify) → par route
194
+ enforceOrgIsolation.ts → vérifie que l'org du JWT = l'org de la requête
195
+ ```
196
+
197
+ ### 3.2 Routes WhatsApp (`/v1/whatsapp/`)
198
+
199
+ | Méthode | Route | Fonction |
200
+ |---------|-------|----------|
201
+ | GET | `/webhook` | Vérification Meta (hub.verify_token) |
202
+ | POST | `/webhook` | Réception messages entrants (200 OK immédiat + queue) |
203
+ | GET | `/templates` | Liste templates Meta pour l'org |
204
+ | POST | `/templates` | Création template WhatsApp Business |
205
+
206
+ **Fonctionnalités critiques :**
207
+ - Vérification HMAC `X-Hub-Signature-256` sur le corps brut
208
+ - Mise à jour `CampaignHistory` sur les status updates (DELIVERED/READ)
209
+ - Forward asynchrone vers worker bridge (localhost:8082)
210
+
211
+ ### 3.3 Routes Admin (`/v1/admin/`)
212
+
213
+ | Méthode | Route | Fonction |
214
+ |---------|-------|----------|
215
+ | GET | `/stats` | Stats dashboard (users, actifs, complétés, revenue) |
216
+ | GET | `/users` | Liste paginée des utilisateurs |
217
+ | GET | `/users/:id/messages` | Historique conversation |
218
+ | GET | `/enrollments` | Inscriptions paginées |
219
+ | GET | `/live-feed` | Exercices PENDING_REVIEW (révision humaine) |
220
+ | POST | `/override-feedback` | Transcription manuelle + override audio admin |
221
+ | GET/POST/PUT/DELETE | `/tracks` | CRUD parcours de formation |
222
+ | GET/POST/PUT/DELETE | `/tracks/:id/days` | CRUD jours de formation |
223
+ | GET | `/training/audios` | Audios en attente de correction STT |
224
+ | POST | `/training/submit` | Soumission correction manuelle |
225
+ | GET | `/training/suggestions` | Suggestions d'amélioration WER |
226
+ | POST | `/training/apply-suggestions` | Application batch des règles |
227
+ | POST | `/training/recalculate-wer` | Recalcul global WER avec règles |
228
+ | POST | `/training/upload` | Upload + transcription audio |
229
+
230
+ ### 3.4 Routes IA (`/v1/ai/`)
231
+
232
+ | Méthode | Route | Fonction |
233
+ |---------|-------|----------|
234
+ | POST | `/onepager` | Génération PDF one-pager + image IA |
235
+ | POST | `/deck` | Génération PPTX pitch deck avec images IA |
236
+ | POST | `/personalize-lesson` | Réécriture leçon selon activité utilisateur |
237
+ | POST | `/tts` | Synthèse vocale texte → audio |
238
+ | POST | `/transcribe` | Transcription audio → texte (confidence + isSuspect) |
239
+ | POST | `/store-audio` | Archivage média vers R2 |
240
+ | POST | `/generate-feedback` | Feedback exercice complet (2 branches) |
241
+ | POST | `/extract-profile` | Extraction profil business depuis texte libre |
242
+ | POST | `/chat` | Prompt système + question → réponse |
243
+ | POST | `/crm/generate-campaign` | Message personnalisé par contact |
244
+ | POST | `/crm/command` | Classification intention + routing action |
245
+ | POST | `/crm/voice-command` | Commande vocale → action CRM |
246
+ | POST | `/crm/send-bulk` | Envoi messages liste contacts |
247
+
248
+ ### 3.5 Routes Analytics (`/v1/analytics/`)
249
+
250
+ | GET `/usage` | Messages, tokens estimés, coût |
251
+ |---|---|
252
+ | GET `/pedagogy` | Taux complétion, score moyen, temps moyen |
253
+ | GET `/campaigns` | Funnel campagne (SENT → DELIVERED → READ) |
254
+
255
+ ### 3.6 Routes Organisations (`/v1/organizations/`)
256
+
257
+ | CRUD orgs | `/` → liste, POST créer, GET/:id, PUT/:id |
258
+ |---|---|
259
+ | WhatsApp | `/:id/whatsapp-setup` — échange token OAuth Meta |
260
+ | WhatsApp | `/:id/whatsapp-status` — validité token |
261
+ | Personnalité | `PATCH /:id/personality` — mission, ton, nom bot |
262
+ | Base de connaissance | `POST /:id/upload-kb` — upload + indexation |
263
+ | KB stats | `GET /:id/kb-stats` — chunks, coverage |
264
+ | KB gestion | `GET /:id/kb`, `DELETE /:id/kb/:entryId` |
265
+ | Contacts | `POST /:id/contacts/import` — import Excel |
266
+ | Contacts | `GET/DELETE /:id/contacts` — CRUD contacts |
267
+ | Messages | `GET /:id/messages`, `POST /:id/messages/reply` |
268
+ | Campagnes | `GET /:id/campaign-history` |
269
+
270
+ ### 3.7 Routes Billing (`/v1/billing/`)
271
+
272
+ | GET `/summary` | Usage période courante + wallet |
273
+ |---|---|
274
+ | GET `/history?days=30` | Détail jour par jour |
275
+ | GET `/breakdown` | Ventilation par fonctionnalité |
276
+ | **POST `/chat`** | **Copilote IA admin (agentique)** |
277
+ | POST `/template-generate` | Générateur template IA |
278
+ | POST `/agent-test` | Test personnalité agent IA |
279
+ | GET `/wallet` | Solde + 20 dernières transactions |
280
+ | POST `/admin/allocate` | Recharge wallet (SUPER_ADMIN) |
281
+
282
+ ### 3.8 Routes Paiements (`/v1/payments/`)
283
+
284
+ - `POST /initiate` — Initialiser session paiement
285
+ - `POST /verify` — Vérifier paiement (via webhook gateway)
286
+ - `GET /history` — Historique paiements org
287
+
288
+ ---
289
+
290
+ ## 4. Worker BullMQ
291
+
292
+ **Localisation :** `apps/whatsapp-worker/src/`
293
+
294
+ ### 4.1 Queues
295
+
296
+ | Queue | Jobs |
297
+ |-------|------|
298
+ | `whatsapp-queue` | inbound-message, inbound-media, send-message, send-content, enroll, nudge, broadcast, kb-process, generate-feedback, send-admin-audio-override |
299
+ | `notification-queue` | email |
300
+
301
+ **Retry policy :** 3 tentatives avec backoff exponentiel
302
+
303
+ ### 4.2 Chaîne de handlers (ordre strict)
304
+
305
+ ```
306
+ WhatsAppLogic.handleIncomingMessage()
307
+ 1. EntityResolver.resolve() → User/Contact/Org/Enrollment
308
+ 2. Message log (async, non-bloquant)
309
+ 3. Credit guard → WalletExhaustedError si solde = 0
310
+ 4. AIAgentHandler.canHandle() → mode === AI_AGENT
311
+ 5. OnboardingHandler.canHandle() → INSCRIPTION / sélection langue / secteur
312
+ 6. CommandHandler.canHandle() → SEED / RECHARGE / DAY{N}_{ACTION}
313
+ 7. NavigationHandler.canHandle() → SUITE / APPROFONDIR
314
+ 8. ExerciseHandler.canHandle() → réponse exercice (texte/audio/image)
315
+ 9. Fallback → message de bienvenue
316
+ ```
317
+
318
+ ### 4.3 Handler AIAgentHandler
319
+
320
+ **Condition :** `organization.mode === 'AI_AGENT'`
321
+
322
+ **Flux :**
323
+ 1. Construire system prompt depuis `customPrompt` + directive langue
324
+ 2. Si KB existe → `IndexingService.searchRelevantContext()` (top 3 chunks cosinus)
325
+ 3. Appel `AIPedagogyService.generateChat()` → réponse
326
+ 4. Envoi via `whatsapp-queue`
327
+
328
+ **Lacunes :**
329
+ - Pas de classification d'intention avant la recherche KB
330
+ - Pas de seuil de pertinence (retourne toujours top-3, même peu pertinents)
331
+ - Pas de mémoire conversationnelle (chaque message est traité indépendamment)
332
+ - Pas de handoff humain automatique si confidence < seuil
333
+
334
+ ### 4.4 Handler OnboardingHandler
335
+
336
+ **Flux :**
337
+ 1. Mot-clé `INSCRIPTION` → réinitialisation cascade (supprime enrollments/progress/responses)
338
+ 2. Sélection langue (LANG_FR, LANG_WO, LANG_EN, LANG_ES, LANG_PT)
339
+ 3. Sélection secteur (liste prédéfinie ou saisie libre)
340
+ 4. Auto-enroll via `defaultTrackId` (flowConfig)
341
+
342
+ **Lacunes :**
343
+ - Reset INSCRIPTION = suppression définitive (pas de soft-delete)
344
+ - Secteurs hardcodés ou via flowConfig (fragile)
345
+ - Pas de vérification téléphone / OTP
346
+
347
+ ### 4.5 Handler ExerciseHandler
348
+
349
+ **Flux complexe :**
350
+ 1. Fetch `userProgress` pour le track actif
351
+ 2. Résolution du jour effectif (time-travel Redis OU currentDay)
352
+ 3. Validation longueur réponse (min 3 mots sauf boutons/images)
353
+ 4. Envoi message "spinner" (feedback en cours)
354
+ 5. Queue `generate-feedback` avec 40+ paramètres de contexte
355
+
356
+ **Paramètres envoyés au générateur de feedback :**
357
+ - exercicePrompt, exerciseCriteria, userResponse
358
+ - previousResponses, businessProfile, language
359
+ - iterationCount, exerciseStatus, dayNumber
360
+ - trackTitle, userActivity, isDeepDive
361
+
362
+ **Lacunes :**
363
+ - Pas de détection de doublon dans les 30 secondes
364
+ - Validation longueur simpliste (split whitespace)
365
+
366
+ ### 4.6 Handler FeedbackHandler
367
+
368
+ **Flux :**
369
+ 1. `aiService.generateFeedback()` avec contexte complet
370
+ 2. **2 branches selon isQualified :**
371
+ - ❌ Échec : Message de relance + indication pour réessayer
372
+ - ✅ Succès : Feedback enrichi + conseils actionnables + prompt deep-dive
373
+ 3. Update atomique `exerciseStatus` AVANT envoi message (règle d'atomicité)
374
+ 4. Envoi via queue
375
+ 5. **Jour 11 spécial :** Extraction membre équipe depuis image si qualifié
376
+
377
+ ### 4.7 Scheduler (Tâches planifiées)
378
+
379
+ ```
380
+ UTC 08:00 quotidien (= 08:00 Dakar) :
381
+ Pour chaque enrollment ACTIVE :
382
+ Si exercice PENDING + 24h sans activité → Nudge ENCOURAGEMENT
383
+ Si exercice PENDING + 72h sans activité → Nudge RESURRECTION
384
+ Sinon → Queue send-content (leçon du jour suivant)
385
+
386
+ Chaque lundi 09:00 UTC :
387
+ Pour chaque org avec systemUserToken :
388
+ 50 jours → Alerte WARNING (token va expirer dans ~10j)
389
+ 55 jours → Alerte CRITICAL
390
+ 60 jours → Alerte EXPIRÉ
391
+ ```
392
+
393
+ **Lacunes :**
394
+ - Nudges non personnalisés (même message par langue)
395
+ - Pas de tentative de renouvellement token automatique
396
+ - Alertes token uniquement dans les logs (pas d'email/push)
397
+
398
+ ### 4.8 Scoring comportemental
399
+
400
+ **4 dimensions (0-100) :**
401
+ - `discipline_financiere` — Gestion finances
402
+ - `organisation` — Structure opérationnelle
403
+ - `relation_client` — Qualité service client
404
+ - `risque_management` — Gestion des risques
405
+
406
+ **Niveaux :** Informel → Structuration → Organisé → Avancé
407
+
408
+ ### 4.9 ContentHandler (Livraison leçons)
409
+
410
+ **Flux :**
411
+ 1. `sendLessonDay()` depuis `pedagogy.ts`
412
+ 2. Personnalisation IA (timeout 15s, fallback texte brut)
413
+ 3. Envoi visuel (vidéo → fallback image)
414
+ 4. Audio (TTS généré ou pré-enregistré)
415
+ 5. Boutons exercice interactifs
416
+ 6. **Logique de graduation :** Si pas de jour N+1 → COMPLETED + auto-enroll T{N+1}-LANG
417
+
418
+ ### 4.10 KBProcessor (Indexation base de connaissance)
419
+
420
+ **Flux :**
421
+ 1. Parse document (PDF, Excel, site web HTML)
422
+ 2. Découpage en chunks (1000 chars, overlap 200)
423
+ 3. Génération embeddings via OpenAI batch
424
+ 4. Insertion pgvector via SQL brut (`prisma.$executeRaw`)
425
+
426
+ **Lacunes :**
427
+ - Pas de déduplication de chunks
428
+ - Ré-indexation efface les anciens (pas de versioning)
429
+ - Pas de progress tracking pour gros fichiers
430
+ - Crawl web limité (depth 2, 10 liens/page)
431
+
432
+ ---
433
+
434
+ ## 5. SDK IA
435
+
436
+ **Localisation :** `packages/ai-sdk/`
437
+
438
+ ### 5.1 Architecture multi-provider
439
+
440
+ ```
441
+ AIService
442
+ └── ProviderRegistry
443
+ ├── GeminiProvider (priority: 100) → TEXT, VISION, AUDIO
444
+ ├── OpenAIProvider (priority: 50) → TEXT, AUDIO, IMAGE, SPEECH
445
+ └── TenantProviders (priority: 1000) → BYOK par org
446
+ ```
447
+
448
+ **Failover automatique :** Si le provider principal échoue, le suivant est essayé.
449
+
450
+ ### 5.2 Capacités par provider
451
+
452
+ | Capacité | Gemini | OpenAI |
453
+ |----------|--------|--------|
454
+ | Texte (TEXT) | Flash/Pro | GPT-4o |
455
+ | Vision (IMAGE) | Flash (inlineData base64) | GPT-4o Vision |
456
+ | Transcription audio | — | Whisper |
457
+ | Génération parole (TTS) | — | TTS-1 |
458
+ | Génération image | — | DALL-E 3 |
459
+ | Recherche web | Grounding API | — |
460
+
461
+ **Note Gemini :** Toujours base64 `inlineData` pour la vision (jamais URL — instable).
462
+
463
+ ### 5.3 Coûts modèles (Mai 2026)
464
+
465
+ | Modèle | Input (/1M tokens) | Output (/1M tokens) |
466
+ |--------|-------------------|---------------------|
467
+ | GPT-4o | $7.50 | $15.00 |
468
+ | GPT-4o-mini | $0.15 | $0.60 |
469
+ | Gemini Flash | $0.075 | $0.30 |
470
+ | Gemini Pro | $3.50 | $10.50 |
471
+ | Whisper | $0.006/min | — |
472
+
473
+ ### 5.4 Cache tenant
474
+
475
+ - Configuration personnalité mise en cache Redis (TTL 1h)
476
+ - Cache template PromptLoader en mémoire (durée de vie du processus)
477
+ - **Pas** de cache au niveau requête LLM
478
+
479
+ ### 5.5 BYOK (Bring Your Own Key)
480
+
481
+ - Plan SCALE uniquement
482
+ - `openAiApiKey` et `googleAiApiKey` par org dans Prisma (chiffrés)
483
+ - Providers tenant créés dynamiquement avec ces clés (priority 1000)
484
+ - Débit wallet flagué `byok: true` (non facturé en crédits normaux)
485
+
486
+ ---
487
+
488
+ ## 6. Frontend Admin
489
+
490
+ **Technologie :** React 18 + Vite + Tailwind CSS + react-i18next (FR/EN/ES/PT)
491
+
492
+ ### 6.1 Pages et composants
493
+
494
+ #### Dashboard (`DashboardPage.tsx`)
495
+ - Cartes stats : utilisateurs totaux, actifs, complétés, tracks, revenue
496
+ - Table inscriptions paginée + tri
497
+ - Export CSV inscriptions
498
+ - Sélecteur d'organisation (multi-tenant)
499
+ - Timeout 15s avec retry
500
+
501
+ #### Analytics (`AnalyticsPage.tsx`)
502
+ - Graphiques Recharts (Bar, Pie)
503
+ - Usage IA : appels, tokens, coût FCFA
504
+ - Pédagogie : taux complétion, score moyen, temps moyen
505
+ - Campagnes : funnel SENT → DELIVERED → READ (4 couleurs)
506
+
507
+ #### AI Agent Setup (`AIAgentSetup.tsx`)
508
+ - Éditeur mission (coreMission)
509
+ - Sélecteur de ton (Professionnel/Amical/Direct/Pédagogue) avec descriptions i18n
510
+ - Recommandation ton selon mode org
511
+ - Upload base de connaissance (PDF/DOCX/XLSX/CSV)
512
+ - Stats KB : nb chunks, % coverage
513
+ - Chat test en temps réel
514
+ - Indicateur qualité KB (Excellent / Bon / Insuffisant)
515
+
516
+ #### Billing (`BillingPage.tsx`)
517
+ - Résumé wallet (crédits, FCFA, statut)
518
+ - Graphique historique 30j
519
+ - Ventilation par fonctionnalité
520
+ - Alertes solde bas / service suspendu
521
+
522
+ #### Contacts CRM (`ContactsPage.tsx`)
523
+ - Import Excel avec colonnes dynamiques
524
+ - Table contacts avec recherche/filtre
525
+ - Envoi message direct depuis l'interface
526
+ - Suppression bulk
527
+
528
+ #### Templates (`TemplatesPage.tsx`)
529
+ - Liste templates Meta avec statut (PENDING/APPROVED/REJECTED)
530
+ - Création template avec générateur IA
531
+ - Variables `{{1}}`, `{{2}}` dans éditeur
532
+ - Aperçu avant soumission
533
+ - Sélection catégorie (MARKETING/UTILITY)
534
+
535
+ #### Tracks (`TrackListPage.tsx` + `TrackFormPage.tsx` + `TrackDaysPage.tsx`)
536
+ - CRUD complet parcours de formation
537
+ - Gestion jours (leçon + exercice + boutons)
538
+ - Support multi-langue par contenu
539
+ - Éditeur jours avec drag & drop (jour 1.5 supporté)
540
+
541
+ #### Users (`UserListPage.tsx`)
542
+ - Liste utilisateurs avec filtres
543
+ - Détail conversation par utilisateur
544
+ - Actions : override feedback, voir historique
545
+
546
+ #### Training Lab (`TrainingLab.tsx`)
547
+ - Révision manuelle transcriptions Whisper
548
+ - Correction + calcul WER (Word Error Rate)
549
+ - Suggestions normalization (règles de post-traitement)
550
+ - Application batch des règles + recalcul global
551
+ - Upload audio manuel pour test
552
+
553
+ #### Knowledge Base (`KnowledgeBasePage.tsx`)
554
+ - Gestion chunks KB
555
+ - Suppression chunks individuels
556
+ - Stats indexation
557
+
558
+ #### Settings (`SettingsPage.tsx`)
559
+ - Configuration mode, WhatsApp (WABA ID, Business ID, token)
560
+ - Gestion clés API BYOK (plan SCALE)
561
+ - Configuration avancée JSON (flowConfig)
562
+ - Statut token WhatsApp avec alerte expiration
563
+
564
+ #### CRM Inbox (`CrmInbox.tsx`)
565
+ - Conversations temps réel
566
+ - Réponse directe depuis l'interface
567
+ - Statuts messages (lu/délivré)
568
+
569
+ #### AdminChat (`AdminChat.tsx`)
570
+ - Copilote IA contextuel par page
571
+ - 6 pages : billing, settings, templates, agent, onboarding, general
572
+ - Questions suggérées pré-remplies par page
573
+ - **Agentic :** Peut changer le mode, mettre à jour la personnalité, lire la config
574
+ - 4 langues (FR/EN/ES/PT)
575
+
576
+ #### Campaign History (`CampaignHistoryPage.tsx`)
577
+ - Historique campagnes avec funnel
578
+ - Détail par campagne (SENT/DELIVERED/READ/FAILED)
579
+
580
+ ### 6.2 Internationalisation
581
+ - 4 langues : FR (défaut), EN, ES, PT
582
+ - Fichiers : `apps/admin/src/locales/{fr,en,es,pt}.json`
583
+ - Hook `useTranslation()` partout
584
+ - `LanguageSwitcher` composant global
585
+
586
+ ### 6.3 État de l'art UX actuel
587
+
588
+ **Points forts :**
589
+ - Design cohérent Tailwind CSS
590
+ - Multi-tenant natif (sélecteur org)
591
+ - Copilote IA intégré sur chaque page
592
+ - Internationalisation complète
593
+
594
+ **Lacunes UX :**
595
+ - Pas de notifications temps réel (polling manuel)
596
+ - Pas de mode sombre
597
+ - Pas de raccourcis clavier
598
+ - Pas de tour guidé (onboarding admin)
599
+ - Pas de visualisation de la progression utilisateur en temps réel
600
+ - Tableau de bord non personnalisable (widgets fixes)
601
+ - Pas d'export PDF des rapports
602
+
603
+ ---
604
+
605
+ ## 7. Prompts & Templates IA
606
+
607
+ **Localisation :** `packages/prompts/src/templates/`
608
+
609
+ ### 7.1 Templates disponibles
610
+
611
+ | Fichier | Usage |
612
+ |---------|-------|
613
+ | `feedback-base.md` | Feedback exercice — structure de base |
614
+ | `action-feedback-standard.md` | Feedback exercice — variante action |
615
+ | `personalized-lesson.md` | Réécriture leçon selon profil utilisateur |
616
+ | `business-profile-extraction.md` | Extraction profil business depuis texte |
617
+ | `crm-campaign.md` | Génération message campagne personnalisé |
618
+ | `crm-assistant-system.md` | System prompt assistant CRM |
619
+ | `broadcast-router.md` | Routage messages broadcast |
620
+ | `one-pager.md` | Génération PDF one-pager |
621
+ | `pitch-deck.md` | Génération PPTX pitch deck |
622
+
623
+ ### 7.2 Système de compilation
624
+
625
+ ```typescript
626
+ PromptLoader.compile(templateName, variables, personality)
627
+ // Inject variables: {{variableName}}
628
+ // Inject personality: {{botName}}, {{coreMission}}, {{toneDescription}}, {{constraints}}
629
+ // Cache en mémoire (durée de vie du processus)
630
+ ```
631
+
632
+ ### 7.3 Personnalité par défaut (fallback)
633
+
634
+ ```
635
+ botName: "XAMLÉ COACH"
636
+ coreMission: "expert business pour entrepreneurs d'Afrique de l'Ouest"
637
+ toneDescription: "direct, dynamique et encourageant. Style WhatsApp (gras *texte*, emojis)"
638
+ constraints: ["JAMAIS ANGLAIS", "Ne jamais citer 'Manga Deaf'"]
639
+ ```
640
+
641
+ ---
642
+
643
+ ## 8. Tarification & Wallet
644
+
645
+ **Table de prix :** `packages/database/src/credit-pricing.ts`
646
+
647
+ ```
648
+ 1 crédit = 10 FCFA
649
+
650
+ WHATSAPP_CONVERSATION : 1 crédit (tout message entrant ou sortant)
651
+ AI_TEXT : 3 crédits (génération texte, non-BYOK)
652
+ AI_AUDIO : 2 crédits (transcription Whisper, non-BYOK)
653
+ BROADCAST_PER_USER : 3 crédits (par destinataire campagne)
654
+
655
+ Seuils d'alerte wallet :
656
+ LOW : 200 crédits (bannière orange)
657
+ CRITICAL : 50 crédits (alerte rouge urgente)
658
+ ```
659
+
660
+ **Plans :**
661
+
662
+ | Plan | Crédits IA/mois | BYOK | SLA |
663
+ |------|----------------|------|-----|
664
+ | STARTER | 500 | ❌ | Standard |
665
+ | GROWTH | 3 000 | ❌ | Standard |
666
+ | SCALE | 10 000 | ✅ | Prioritaire |
667
+ | ENTERPRISE | Illimité | ✅ | Dédié |
668
+
669
+ ---
670
+
671
+ ## 9. Lacunes & Dette technique
672
+
673
+ ### 9.1 Sécurité (priorité haute)
674
+
675
+ | # | Problème | Impact | Solution recommandée |
676
+ |---|----------|--------|---------------------|
677
+ | S1 | `organizationId` depuis header (non JWT) | Usurpation d'identité possible | Extraire de `req.user.organizationId` uniquement |
678
+ | S2 | Pas de rate-limiting webhook Meta | DDoS vulnérable | `@fastify/rate-limit` avec whitelist Meta IPs |
679
+ | S3 | Tools IA agentiques sans garde-fous de rôle | N'importe quel admin change le mode | Exiger SUPER_ADMIN pour `change_organization_mode` |
680
+ | S4 | Pas d'audit log | Impossible de tracer qui a changé quoi | Middleware Prisma → AuditLog sur toutes les updates critiques |
681
+
682
+ ### 9.2 Données & intégrité
683
+
684
+ | # | Problème | Impact | Solution |
685
+ |---|----------|--------|----------|
686
+ | D1 | Pas de soft-delete | Perte irréversible sur INSCRIPTION reset | `deletedAt` sur User/Enrollment/UserProgress |
687
+ | D2 | Pas de contrôle de concurrence | Double soumission exercice possible | Version field (optimistic locking) ou Redis lock |
688
+ | D3 | Coûts IA estimés, pas réels | Billing inexact | API usage OpenAI/Google pour coûts réels |
689
+ | D4 | Pas de validation schemas JSON | `personalityConfig`, `flowConfig` peuvent être malformés | Zod validation à l'entrée |
690
+
691
+ ### 9.3 IA & LLM
692
+
693
+ | # | Problème | Impact | Solution |
694
+ |---|----------|--------|----------|
695
+ | A1 | Pas de retry sur échec LLM | Silence si timeout | Exponential backoff + dead-letter queue |
696
+ | A2 | Pas de cache prompts | Recoût inutile sur leçons identiques | Redis cache par (lesson_id, activity) |
697
+ | A3 | RAG naïf (top-3 sans seuil) | Réponses hors-sujet si KB sparse | Relevance threshold (cosine > 0.75) |
698
+ | A4 | Pas de validation output LLM | `isQualified` pourrait être mal parsé | Structured outputs / JSON mode strict |
699
+ | A5 | Pas de mémoire conversationnelle AI_AGENT | Contexte perdu entre messages | Redis sliding window (5 derniers messages) |
700
+ | A6 | Transcription sans détection langue | Wolof mal transcrit | Whisper `language` param basé sur `user.language` |
701
+
702
+ ### 9.4 Performance & scalabilité
703
+
704
+ | # | Problème | Impact | Solution |
705
+ |---|----------|--------|----------|
706
+ | P1 | N+1 dans `sendLessonDay()` | Lenteur avec > 100 inscriptions actives | Batch fetch avec Prisma `include` |
707
+ | P2 | Embeddings synchrones pour gros fichiers | Timeout worker sur upload KB | Chunking + embeddings en batch asynchrone |
708
+ | P3 | Pas de connection pooling explicite | Saturation pool Prisma (défaut 5) | `connection_limit` selon nb threads worker |
709
+
710
+ ### 9.5 Observabilité
711
+
712
+ | # | Problème | Impact | Solution |
713
+ |---|----------|--------|----------|
714
+ | O1 | Pas de traceId propagé aux jobs async | Impossible de tracer une requête bout-en-bout | Passer traceId dans `job.data` |
715
+ | O2 | Pas de distributed tracing | Vision nulle sur latences inter-services | OpenTelemetry (OTEL) collector |
716
+ | O3 | Quota alerts dans logs seulement | Admin ne reçoit pas d'alerte solde bas | Email + push notification temps réel |
717
+
718
+ ### 9.6 Fonctionnalités manquantes
719
+
720
+ | # | Fonctionnalité | Valeur | Effort |
721
+ |---|---------------|--------|--------|
722
+ | F1 | Scheduling campagnes (`sendAt`) | Élevée | Faible |
723
+ | F2 | Segmentation contacts par tags | Élevée | Moyen |
724
+ | F3 | A/B testing feedback prompts | Moyenne | Moyen |
725
+ | F4 | Bulk enroll via API | Élevée | Faible |
726
+ | F5 | Notifications temps réel admin (WebSocket/SSE) | Élevée | Moyen |
727
+ | F6 | Export PDF rapports | Moyenne | Faible |
728
+ | F7 | Mémoire conversationnelle AI_AGENT | Élevée | Faible |
729
+ | F8 | Versioning KB | Moyenne | Moyen |
730
+ | F9 | Rapport hebdomadaire par email | Élevée | Faible |
731
+ | F10 | Tableau de bord personnalisable | Moyenne | Élevé |
732
+
733
+ ---
734
+
735
+ ## 10. Roadmap IA Agentique
736
+
737
+ > **Définition :** Un agent IA est un système qui perçoit son environnement, prend des décisions, et exécute des actions de manière autonome — en boucle, avec des outils, sans intervention humaine sur chaque étape.
738
+
739
+ ### 10.1 Ce qui est déjà agentique (en production)
740
+
741
+ | Fonctionnalité | Description | Outils utilisés |
742
+ |---------------|-------------|-----------------|
743
+ | **Copilote Admin** | Change le mode org, met à jour la personnalité | `change_organization_mode`, `update_ai_agent_personality`, `get_organization_settings` |
744
+ | **Feedback exercice 2-branches** | Décide qualifié/non, génère feedback personnalisé | `generateFeedback()` → mise à jour DB → envoi message |
745
+ | **RAG Agent IA** | Cherche dans la KB, formule réponse contextuelle | `searchRelevantContext()` → `generateChat()` |
746
+ | **Graduation automatique** | Détecte fin de track, inscrit au niveau suivant | `ContentHandler` + détection T{N}→T{N+1} |
747
+ | **Scheduler Nudge** | Analyse inactivité, envoie relances | `scheduler.ts` → BullMQ jobs |
748
+
749
+ ### 10.2 Agentique — Gains immédiats (0-4 semaines)
750
+
751
+ #### 10.2.1 Mémoire conversationnelle pour AI_AGENT
752
+
753
+ **Problème actuel :** Chaque message est traité indépendamment — l'agent "oublie" ce qu'il vient de dire.
754
+
755
+ **Solution :**
756
+ ```typescript
757
+ // Dans AIAgentHandler.ts
758
+ const conversationHistory = await redis.lrange(`conv:${userId}`, 0, 9); // 10 derniers messages
759
+ const messages = [
760
+ { role: 'system', content: systemPrompt },
761
+ ...conversationHistory.map(m => JSON.parse(m)),
762
+ { role: 'user', content: text }
763
+ ];
764
+ await redis.lpush(`conv:${userId}`, JSON.stringify({ role: 'user', content: text }));
765
+ // Après réponse :
766
+ await redis.lpush(`conv:${userId}`, JSON.stringify({ role: 'assistant', content: answer }));
767
+ await redis.ltrim(`conv:${userId}`, 0, 19); // Garder 20 messages max
768
+ await redis.expire(`conv:${userId}`, 86400); // TTL 24h
769
+ ```
770
+
771
+ **Impact :** Conversations cohérentes, expérience client transformée.
772
+
773
+ #### 10.2.2 Alertes intelligentes solde wallet
774
+
775
+ **Problème actuel :** L'admin apprend que le service est suspendu quand c'est trop tard.
776
+
777
+ **Solution — Agent de surveillance financière :**
778
+ ```typescript
779
+ // Dans scheduler.ts — ajouter toutes les heures
780
+ const orgsAtRisk = await prisma.organization.findMany({
781
+ where: { walletBalance: { lte: 200 }, isHardStopped: false }
782
+ });
783
+ for (const org of orgsAtRisk) {
784
+ // Calculer burn rate 7 derniers jours
785
+ const weeklyDebit = ...; const daysLeft = org.walletBalance / (weeklyDebit / 7);
786
+ if (daysLeft < 3) await sendAlertEmail(org, daysLeft);
787
+ if (daysLeft < 1) await sendUrgentPush(org);
788
+ }
789
+ ```
790
+
791
+ #### 10.2.3 Rapport hebdomadaire automatique
792
+
793
+ **Un email tous les lundis avec :**
794
+ - Stats semaine (messages, complétion, coût)
795
+ - Comparaison avec semaine précédente (+/- %)
796
+ - Top 3 utilisateurs les plus actifs
797
+ - Recommandation IA : "Votre taux de complétion a baissé de 15% — pensez à vérifier le contenu du Jour 7"
798
+
799
+ **Implémentation :** Job cron lundi 07:00, génération HTML email via GPT-4o-mini, envoi via `notification-queue`.
800
+
801
+ #### 10.2.4 Seuil de pertinence RAG
802
+
803
+ ```typescript
804
+ // Dans AIAgentHandler.ts
805
+ const chunks = await indexingService.searchRelevantContext(text, organizationId, { threshold: 0.75 });
806
+ if (chunks.length === 0) {
807
+ // Répondre honnêtement "Je n'ai pas d'information sur ce sujet"
808
+ return "Je ne dispose pas d'informations précises sur ce sujet. Puis-je vous aider autrement ?";
809
+ }
810
+ ```
811
+
812
+ #### 10.2.5 Détection et handoff humain automatique
813
+
814
+ **Pour mode CUSTOMER_SERVICE :**
815
+ ```typescript
816
+ const needsHuman = await detectEscalation(text); // Sentiment très négatif, "parler à un humain", etc.
817
+ if (needsHuman) {
818
+ await sendToHumanQueue(userId, organizationId, text);
819
+ await sendMessage(phone, "Je vous transfère à un conseiller humain. Merci de patienter.");
820
+ }
821
+ ```
822
+
823
+ ### 10.3 Agentique — Gains stratégiques (1-3 mois)
824
+
825
+ #### 10.3.1 Agent Créateur de Contenu
826
+
827
+ **Problème :** Créer un Track de 21 jours prend des semaines manuellement.
828
+
829
+ **Vision :** L'admin décrit son objectif pédagogique → l'agent génère tout le curriculum.
830
+
831
+ **Outils :**
832
+ ```typescript
833
+ tools: [
834
+ { name: 'create_track', description: 'Crée un Track avec titre et durée' },
835
+ { name: 'create_track_day', description: 'Crée un jour (leçon + exercice + critères)' },
836
+ { name: 'generate_lesson_content', description: 'Génère le texte de la leçon' },
837
+ { name: 'generate_exercise', description: 'Génère l\'exercice et ses critères de validation' },
838
+ { name: 'validate_curriculum', description: 'Vérifie la cohérence pédagogique du parcours' }
839
+ ]
840
+ ```
841
+
842
+ **Flux agent :**
843
+ 1. Admin : "Crée un programme de 7 jours sur la gestion financière pour commerçants s��négalais"
844
+ 2. Agent → `generate_curriculum_outline()` → 7 thèmes progressifs
845
+ 3. Agent → `create_track()` → Track créé
846
+ 4. Boucle 7 fois → `create_track_day()` avec contenu généré
847
+ 5. Agent → `validate_curriculum()` → Vérification cohérence
848
+ 6. Agent → Rapport final : "Programme créé. 7 leçons, 7 exercices. Prêt à être publié."
849
+
850
+ #### 10.3.2 Agent Optimiseur de Campagnes
851
+
852
+ **Vision :** Analyse les performances passées et suggère les meilleures heures/jours/messages pour les prochaines campagnes.
853
+
854
+ **Outils :**
855
+ ```typescript
856
+ tools: [
857
+ { name: 'get_campaign_analytics', description: 'Récupère les métriques des campagnes passées' },
858
+ { name: 'segment_contacts', description: 'Segmente les contacts par comportement' },
859
+ { name: 'generate_message_variants', description: 'Génère des variantes de messages A/B' },
860
+ { name: 'schedule_campaign', description: 'Programme la campagne au meilleur moment' },
861
+ { name: 'measure_ab_results', description: 'Compare les résultats des variantes' }
862
+ ]
863
+ ```
864
+
865
+ **Exemple d'analyse :**
866
+ - "Les messages envoyés entre 10h-12h ont 34% de taux de lecture vs 12% le soir"
867
+ - "Les messages < 80 caractères ont 2x plus de réponses"
868
+ - "Le segment Dakar répond 40% mieux aux offres en wolof"
869
+
870
+ #### 10.3.3 Agent Conseiller Pédagogique
871
+
872
+ **Vision :** Analyse les résultats des apprenants et propose des interventions personnalisées à l'admin.
873
+
874
+ **Outils :**
875
+ ```typescript
876
+ tools: [
877
+ { name: 'get_cohort_analytics', description: 'Analyse les résultats d\'une cohorte' },
878
+ { name: 'identify_at_risk_students', description: 'Identifie les apprenants en difficulté' },
879
+ { name: 'suggest_intervention', description: 'Propose une action pour chaque apprenant à risque' },
880
+ { name: 'queue_personalized_nudge', description: 'Envoie un nudge personnalisé' },
881
+ { name: 'adjust_track_content', description: 'Modifie le contenu d\'un jour si trop difficile' }
882
+ ]
883
+ ```
884
+
885
+ **Rapport hebdomadaire automatique :**
886
+ ```
887
+ 📊 Analyse de votre cohorte (semaine du 6-12 mai)
888
+ - 23 apprenants actifs, 8 inactifs depuis > 3 jours
889
+ - Jour 7 : 67% d'échec → Le critère est trop strict ou le contenu insuffisant
890
+ - Recommandation : Assouplir les critères du Jour 7 OU envoyer un message d'encouragement
891
+ - Action automatique : Nudge personnalisé envoyé aux 8 inactifs ✅
892
+ ```
893
+
894
+ #### 10.3.4 Agent de Configuration Onboarding
895
+
896
+ **Vision :** Guider un nouvel admin (organistion) à travers toute la configuration en conversation naturelle.
897
+
898
+ **Flux conversationnel :**
899
+ ```
900
+ Agent: "Bonjour ! Je vais vous aider à configurer Xamlé. Quel est votre secteur d'activité ?"
901
+ Admin: "Nous faisons de la formation en comptabilité"
902
+ Agent: "Parfait ! Je recommande le mode EDTECH. Avez-vous déjà un compte WhatsApp Business ?"
903
+ Admin: "Non"
904
+ Agent: → tool: create_whatsapp_embedded_signup_link()
905
+ Agent: "Cliquez sur ce lien pour créer votre compte WhatsApp Business en 5 minutes : [lien]"
906
+ Admin: [connecté]
907
+ Agent: → tool: verify_whatsapp_connection()
908
+ Agent: "✅ WhatsApp connecté ! Voulez-vous créer votre premier programme de formation maintenant ?"
909
+ Admin: "Oui, sur les bases de la comptabilité, 10 jours"
910
+ Agent: → tool: generate_curriculum(topic="comptabilité", days=10)
911
+ Agent: "J'ai créé un programme de 10 jours. Voici le plan :
912
+ Jour 1 : Introduction aux bilans...
913
+ Approuvez-vous ce plan ?"
914
+ ```
915
+
916
+ #### 10.3.5 Agent Détection de Fraude / Anomalies
917
+
918
+ **Vision :** Surveiller automatiquement les comportements anormaux.
919
+
920
+ **Signaux surveillés :**
921
+ - INSCRIPTION répétée par même numéro (> 3x en 24h) → possible bot
922
+ - Consommation crédits anormalement haute (> 2x moyenne 7j)
923
+ - Messages envoyés depuis IPs non-Meta → webhook forgé
924
+ - Contact importé en masse avec même numéro → déduplication manquante
925
+
926
+ **Actions automatiques :**
927
+ - Log d'alerte avec contexte complet
928
+ - Notification email super admin
929
+ - Possible suspension temporaire de l'org si score de fraude > seuil
930
+
931
+ #### 10.3.6 Agent Génération de Knowledge Base
932
+
933
+ **Vision :** L'admin décrit son activité en langage naturel → l'agent génère une FAQ et l'indexe automatiquement.
934
+
935
+ ```
936
+ Admin: "Je gère un restaurant à Dakar. Nous servons des plats sénégalais traditionnels.
937
+ Prix : Thieboudienne 3500 FCFA, Yassa 2500 FCFA, Mafé 3000 FCFA.
938
+ Livraison disponible dans un rayon de 5km. Horaires : 11h-22h."
939
+
940
+ Agent → generate_faq(description)
941
+ → "Q: Quels sont vos horaires ? R: Nous sommes ouverts de 11h à 22h."
942
+ → "Q: Faites-vous la livraison ? R: Oui, dans un rayon de 5km de notre restaurant."
943
+ → "Q: Quel est le prix du Thieboudienne ? R: 3500 FCFA."
944
+ → tool: index_knowledge_base(chunks=[...])
945
+ Agent: "✅ Base de connaissance créée avec 12 questions/réponses. Votre agent IA est prêt !"
946
+ ```
947
+
948
+ #### 10.3.7 Agent Multimodal — Analyse Qualité Exercices
949
+
950
+ **Vision :** Analyser automatiquement la qualité des exercices et suggérer des améliorations.
951
+
952
+ **Données analysées :**
953
+ - Taux d'échec par exercice (> 40% → trop difficile ou critères trop stricts)
954
+ - Temps moyen de réponse par exercice (> 30min → exercice trop complexe)
955
+ - Distribution des scores (bimodale → exercice polarisant)
956
+ - Mots les plus fréquents dans les réponses échouées → identifier les points de blocage
957
+
958
+ **Sorties :**
959
+ - Rapport "Top 3 exercices à améliorer cette semaine"
960
+ - Suggestions de reformulation des critères
961
+ - Proposition de contenu remédial (leçon supplémentaire)
962
+
963
+ ### 10.4 Agentique — Vision à long terme (3-12 mois)
964
+
965
+ #### 10.4.1 Agent Autonome Multi-Étapes (ReAct Pattern)
966
+
967
+ Implémentation du pattern **ReAct** (Reasoning + Acting) pour des workflows admin complexes :
968
+
969
+ ```
970
+ Pensée : "L'utilisateur veut lancer une campagne de relance sur les clients inactifs"
971
+ Action : get_inactive_contacts(days=30)
972
+ Observation : 145 contacts inactifs
973
+ Pensée : "Je dois segmenter par langue pour personnaliser les messages"
974
+ Action : segment_by_language(contacts)
975
+ Observation : 89 FR, 34 WOLOF, 22 EN
976
+ Pensée : "Générer 3 variantes de message"
977
+ Action : generate_messages(segments, goal="réactivation")
978
+ Observation : messages générés
979
+ Pensée : "Valider les templates puis envoyer"
980
+ Action : validate_template_compliance(messages)
981
+ Action : schedule_broadcast(contacts, messages, sendAt="2026-05-14T10:00:00Z")
982
+ Résultat : "Campagne planifiée pour demain 10h. 145 messages. Coût estimé : 435 crédits."
983
+ ```
984
+
985
+ #### 10.4.2 Personnalisation Adaptive (Apprentissage par renforcement léger)
986
+
987
+ - Analyser quels styles de feedback → meilleurs résultats (score final)
988
+ - A/B test automatique entre 2 variantes de prompt
989
+ - Converger vers le prompt gagnant sans intervention humaine
990
+ - **Technologie :** DSPy (Declarative Self-improving Python) ou OPRO (Optimizing Prompts by RL)
991
+
992
+ #### 10.4.3 Agent Multilingue avec Détection Automatique
993
+
994
+ - Détecter la langue de l'utilisateur sans qu'il le choisisse
995
+ - Whisper `detect_language` + LangDetect sur texte
996
+ - Basculer automatiquement (même en wolof)
997
+
998
+ #### 10.4.4 Agent de Benchmarking Inter-Organisations (anonymisé)
999
+
1000
+ - Comparer anonymement les métriques de performance entre orgs similaires
1001
+ - "Votre taux de complétion (42%) est en dessous de la médiane (61%) pour les orgs EDTECH du secteur Finance"
1002
+ - Recommandations basées sur ce qui fonctionne pour les orgs similaires les plus performantes
1003
+
1004
+ #### 10.4.5 Voice-First Admin Interface
1005
+
1006
+ - Commandes vocales complètes pour gérer l'organisation
1007
+ - "Envoie un message de relance à tous les clients inactifs depuis 7 jours"
1008
+ - Transcription + interprétation + confirmation + exécution
1009
+ - Accessibilité pour admins sans formation technique
1010
+
1011
+ ---
1012
+
1013
+ ## 11. Dernières avancées technologiques applicables
1014
+
1015
+ ### 11.1 LLM & Génération
1016
+
1017
+ #### Claude 4 (Anthropic, 2026)
1018
+
1019
+ **Disponible aujourd'hui.** Models : `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5`
1020
+
1021
+ - **Prompt caching natif** (jusqu'à 90% de réduction de coût sur prompts répétitifs)
1022
+ - Application directe : leçons personnalisées (même leçon, différents utilisateurs)
1023
+ - Cache le system prompt + PLATFORM_KNOWLEDGE → économie ~$0.10/1000 appels
1024
+ - **Extended Thinking** (Opus 4.7) : Raisonnement profond pour évaluation complexe exercices
1025
+ - Application : Scoring multi-critères exercices avancés (business plan, étude de marché)
1026
+ - **Computer Use** : Claude peut naviguer une interface web
1027
+ - Application future : Agent qui vérifie le statut d'approbation Meta directement
1028
+
1029
+ **Considération :** Passer progressivement les appels critiques (feedback) de GPT-4o à Claude Sonnet 4.6 (moins cher, aussi performant sur le français).
1030
+
1031
+ #### GPT-4o-mini avec Structured Outputs
1032
+
1033
+ **Disponible maintenant.** JSON Schema strict dans les réponses.
1034
+
1035
+ ```typescript
1036
+ const completion = await openai.chat.completions.create({
1037
+ model: 'gpt-4o-mini',
1038
+ response_format: {
1039
+ type: 'json_schema',
1040
+ json_schema: {
1041
+ name: 'feedback_result',
1042
+ schema: {
1043
+ type: 'object',
1044
+ properties: {
1045
+ isQualified: { type: 'boolean' },
1046
+ score: { type: 'integer', minimum: 0, maximum: 100 },
1047
+ mainFeedback: { type: 'string', maxLength: 500 },
1048
+ improvements: { type: 'array', items: { type: 'string' } }
1049
+ },
1050
+ required: ['isQualified', 'score', 'mainFeedback']
1051
+ },
1052
+ strict: true
1053
+ }
1054
+ }
1055
+ });
1056
+ ```
1057
+
1058
+ **Impact :** Éliminer tous les `try { JSON.parse(...) } catch {}` actuels dans le codebase.
1059
+
1060
+ #### Gemini 2.0 Flash (Google, 2026)
1061
+
1062
+ **Disponible maintenant.** 2x moins cher que Flash 1.5, plus rapide.
1063
+
1064
+ - **Multimodal natif** : Image + texte + audio en une seule requête
1065
+ - Application : Évaluer exercices photo (contenu + mise en page + professionnalisme en 1 appel)
1066
+ - **2M tokens context window** : Peut ingérer un track complet entier pour personnalisation cohérente
1067
+ - **Grounding avec Google Search** : Réponses factuelles vérifiées en temps réel
1068
+ - Application : Agent IA qui répond à des questions avec des données récentes (prix marché, actualités secteur)
1069
+
1070
+ #### Whisper v3 Large + Timestamps
1071
+
1072
+ - Transcription avec horodatage mot par mot
1073
+ - Application : Identifier précisément où l'apprenant bute dans sa réponse orale
1074
+ - `language` parameter : Améliore la précision sur le wolof
1075
+
1076
+ ### 11.2 RAG & Base de Connaissance
1077
+
1078
+ #### RAG Hiérarchique (HyDE + Reranking)
1079
+
1080
+ **Technologie :** Cohere Rerank API ou cross-encoder local (bge-reranker)
1081
+
1082
+ **Amélioration RAG actuelle :**
1083
+ ```
1084
+ Actuel : query → embedding → cosinus → top-3 chunks
1085
+ Amélioré: query → HyDE (génère une réponse hypothétique) → embedding → cosinus top-20
1086
+ → Reranker (cross-encoder) → top-3 pertinents
1087
+ ```
1088
+
1089
+ **Impact :** Précision RAG +40% sur les questions ambiguës.
1090
+
1091
+ #### pgvector 0.7 (Déjà disponible sur Neon)
1092
+
1093
+ - **HNSW indexing** : Recherche 10x plus rapide sur > 100k vecteurs
1094
+ ```sql
1095
+ CREATE INDEX ON "KnowledgeBaseEntry" USING hnsw (embedding vector_cosine_ops)
1096
+ WITH (m = 16, ef_construction = 64);
1097
+ ```
1098
+ - **Quantisation des vecteurs** : Réduction 4x de la taille (int8 vs float32)
1099
+
1100
+ #### Chunking Contextuel (Anthropic Contextual Retrieval, 2025)
1101
+
1102
+ Au lieu de chunker naïvement par taille, ajouter le contexte du document à chaque chunk :
1103
+
1104
+ ```
1105
+ Chunk brut : "Le taux de TVA est 18%."
1106
+ Chunk contextuel : "Document: Guide fiscal Sénégal 2026 — Chapitre: TVA
1107
+ Le taux de TVA est 18%."
1108
+ ```
1109
+
1110
+ **Impact :** Réduction des "missed retrievals" de 67%.
1111
+
1112
+ ### 11.3 Infrastructure & Déploiement
1113
+
1114
+ #### BullMQ 5.x — Worker Threads
1115
+
1116
+ **Disponible maintenant.** Workers dans des threads Node.js séparés.
1117
+
1118
+ ```typescript
1119
+ new Worker('whatsapp-queue', processor, {
1120
+ concurrency: 50,
1121
+ useWorkerThreads: true, // ← Nouveau dans BullMQ 5
1122
+ workerThreadsOptions: { execArgv: ['--max-old-space-size=512'] }
1123
+ });
1124
+ ```
1125
+
1126
+ **Impact :** +3x débit sur les jobs CPU-intensifs (embedding generation).
1127
+
1128
+ #### Neon Branching pour tests
1129
+
1130
+ - Créer une branche DB identique à la production en < 1s (copy-on-write)
1131
+ - Application : Tests d'intégration sur données réelles sans risque
1132
+ ```bash
1133
+ neon branch create --name test-$(date +%Y%m%d)
1134
+ ```
1135
+
1136
+ #### Cloudflare Workers pour le webhook Edge
1137
+
1138
+ - Déplacer la réception webhook Meta sur Cloudflare Workers (< 10ms au lieu de ~100ms)
1139
+ - Validation HMAC à l'edge, mise en queue directe Redis
1140
+ - Zéro cold start, 100ms global response time
1141
+
1142
+ #### OpenTelemetry (OTEL) — Tracing distribué
1143
+
1144
+ ```typescript
1145
+ import { NodeSDK } from '@opentelemetry/sdk-node';
1146
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
1147
+
1148
+ const sdk = new NodeSDK({
1149
+ traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_ENDPOINT }),
1150
+ instrumentations: [getNodeAutoInstrumentations()]
1151
+ });
1152
+ ```
1153
+
1154
+ **Impact :** Visibilité complète webhook → queue → worker → DB → IA → réponse.
1155
+
1156
+ ### 11.4 UX / Frontend
1157
+
1158
+ #### Server-Sent Events (SSE) pour updates temps réel
1159
+
1160
+ Remplacer le polling manuel par SSE (sans WebSocket, plus simple avec Fastify) :
1161
+
1162
+ ```typescript
1163
+ // API
1164
+ fastify.get('/v1/admin/live', async (req, reply) => {
1165
+ reply.raw.writeHead(200, { 'Content-Type': 'text/event-stream' });
1166
+ const unsubscribe = pubsub.subscribe(`org:${orgId}:events`, (event) => {
1167
+ reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
1168
+ });
1169
+ req.raw.on('close', unsubscribe);
1170
+ });
1171
+
1172
+ // Frontend
1173
+ const eventSource = new EventSource('/v1/admin/live');
1174
+ eventSource.onmessage = (e) => updateDashboard(JSON.parse(e.data));
1175
+ ```
1176
+
1177
+ **Cas d'usage :**
1178
+ - Nouveau message reçu → update CRM Inbox en temps réel
1179
+ - Exercice complété → update compteur dashboard
1180
+ - Solde wallet change → alert immédiate
1181
+
1182
+ #### React Server Components + Suspense
1183
+
1184
+ Pour les pages analytics lentes (gros volumes de données) — rendu progressif sans skeleton loaders manuels.
1185
+
1186
+ #### AI-powered Search dans le dashboard
1187
+
1188
+ Recherche en langage naturel sur les données de l'org :
1189
+ - "Montre-moi les utilisateurs inactifs depuis 2 semaines"
1190
+ - "Quels sont les 5 exercices les plus échoués ce mois ?"
1191
+ - "Combien de crédits ont été consommés en transcription audio ?"
1192
+
1193
+ **Technologie :** Text-to-SQL via GPT-4o-mini sur le schéma Prisma.
1194
+
1195
+ ### 11.5 IA Agentique — Frameworks
1196
+
1197
+ #### Vercel AI SDK (avec tools)
1198
+
1199
+ **Recommandé pour remplacer les appels OpenAI directs.** Supporte :
1200
+ - Streaming natif
1201
+ - Tool calling avec retry automatique
1202
+ - Multi-provider (OpenAI, Anthropic, Google)
1203
+ - `generateObject` avec Zod validation
1204
+
1205
+ ```typescript
1206
+ import { generateText } from 'ai';
1207
+ import { openai } from '@ai-sdk/openai';
1208
+
1209
+ const result = await generateText({
1210
+ model: openai('gpt-4o-mini'),
1211
+ tools: { changeMode, updatePersonality, getSettings },
1212
+ maxSteps: 5, // Boucle agentique max 5 étapes
1213
+ messages: [...],
1214
+ });
1215
+ ```
1216
+
1217
+ #### LangGraph (pour agents multi-étapes complexes)
1218
+
1219
+ Pour les workflows agentiques avec état persistant (ex: création curriculum) :
1220
+
1221
+ ```typescript
1222
+ const graph = new StateGraph({ channels: { messages, currentStep, orgId } })
1223
+ .addNode('plan', planCurriculumNode)
1224
+ .addNode('create', createTrackDayNode)
1225
+ .addNode('validate', validateCurriculumNode)
1226
+ .addEdge('plan', 'create')
1227
+ .addConditionalEdges('validate', shouldContinue);
1228
+ ```
1229
+
1230
+ **Avantage :** Reprise sur erreur (le graph sauvegarde son état), debuggable.
1231
+
1232
+ #### DSPy pour optimisation des prompts
1233
+
1234
+ Au lieu de modifier manuellement les prompts, DSPy les optimise automatiquement :
1235
+
1236
+ ```python
1237
+ class FeedbackModule(dspy.Module):
1238
+ def forward(self, response, criteria):
1239
+ return self.generate(response=response, criteria=criteria)
1240
+
1241
+ # Optimiser automatiquement sur 50 exemples validés
1242
+ optimizer = dspy.MIPROv2()
1243
+ optimized = optimizer.compile(FeedbackModule(), trainset=training_examples)
1244
+ ```
1245
+
1246
+ **Impact :** Amélioration mesurable du taux de qualification exercices sans ajustement manuel.
1247
+
1248
+ ### 11.6 Sécurité & Conformité
1249
+
1250
+ #### Chiffrement BYOK avec AWS KMS ou Vault
1251
+
1252
+ Les clés API des orgs doivent être chiffrées :
1253
+
1254
+ ```typescript
1255
+ // Utiliser une clé AES-256 dérivée par org
1256
+ const encryptedKey = await kms.encrypt({
1257
+ KeyId: `alias/org-${organizationId}`,
1258
+ Plaintext: Buffer.from(apiKey)
1259
+ });
1260
+ await prisma.organization.update({
1261
+ where: { id: organizationId },
1262
+ data: { openAiApiKey: encryptedKey.CiphertextBlob.toString('base64') }
1263
+ });
1264
+ ```
1265
+
1266
+ #### PII Redaction avant logs
1267
+
1268
+ Les numéros de téléphone et clés API ne doivent jamais apparaître dans les logs :
1269
+
1270
+ ```typescript
1271
+ // Middleware de sanitisation
1272
+ const sanitize = (obj: any) => JSON.stringify(obj).replace(/\+?\d{10,15}/g, '[PHONE]');
1273
+ ```
1274
+
1275
+ ---
1276
+
1277
+ ## 12. Matrice de priorisation
1278
+
1279
+ ### Impact × Effort
1280
+
1281
+ | # | Fonctionnalité | Impact | Effort | Priorité |
1282
+ |---|---------------|--------|--------|----------|
1283
+ | 1 | **Mémoire conversationnelle AI_AGENT** | 🔴 Critique | 🟢 Faible | **P0 — Immédiat** |
1284
+ | 2 | **Structured outputs (isQualified JSON strict)** | 🔴 Critique | 🟢 Faible | **P0 — Immédiat** |
1285
+ | 3 | **Alertes wallet temps réel (email + push)** | 🔴 Critique | 🟢 Faible | **P0 — Immédiat** |
1286
+ | 4 | **Rapport hebdomadaire auto** | 🟠 Élevé | 🟢 Faible | **P1 — Cette semaine** |
1287
+ | 5 | **SSE temps réel dashboard** | 🟠 Élevé | 🟡 Moyen | **P1 — Cette semaine** |
1288
+ | 6 | **HNSW index pgvector** | 🟠 Élevé | 🟢 Faible | **P1 — Cette semaine** |
1289
+ | 7 | **Seuil pertinence RAG (0.75)** | 🟠 Élevé | 🟢 Faible | **P1 — Cette semaine** |
1290
+ | 8 | **Passage Claude Sonnet 4.6 pour feedback** | 🟠 Élevé | 🟡 Moyen | **P1 — Cette semaine** |
1291
+ | 9 | **Scheduling campagnes (sendAt)** | 🟠 Élevé | 🟢 Faible | **P1 — Cette semaine** |
1292
+ | 10 | **Soft-delete + audit trail** | 🟠 Élevé | 🟡 Moyen | **P2 — Ce mois** |
1293
+ | 11 | **Agent Créateur de Contenu** | 🔴 Critique | 🟡 Moyen | **P2 — Ce mois** |
1294
+ | 12 | **Détection handoff humain CUSTOMER_SERVICE** | 🟠 Élevé | 🟡 Moyen | **P2 — Ce mois** |
1295
+ | 13 | **Segmentation contacts par tags** | 🟠 Élevé | 🟡 Moyen | **P2 — Ce mois** |
1296
+ | 14 | **Text-to-SQL search dashboard** | 🟡 Moyen | 🟡 Moyen | **P3 — Q3 2026** |
1297
+ | 15 | **Agent Conseiller Pédagogique** | 🔴 Critique | 🔴 Élevé | **P3 — Q3 2026** |
1298
+ | 16 | **A/B testing feedback prompts (DSPy)** | 🟡 Moyen | 🔴 Élevé | **P4 — Q4 2026** |
1299
+ | 17 | **OpenTelemetry distributed tracing** | 🟠 Élevé | 🔴 Élevé | **P4 — Q4 2026** |
1300
+ | 18 | **ReAct Pattern campagnes autonomes** | 🔴 Critique | 🔴 Élevé | **P4 — Q4 2026** |
1301
+ | 19 | **Benchmarking inter-orgs anonymisé** | 🟡 Moyen | 🔴 Élevé | **Backlog** |
1302
+ | 20 | **Voice-first admin interface** | 🟡 Moyen | 🔴 Élevé | **Backlog** |
1303
+
1304
+ ### Prochaines 4 semaines — Plan d'action
1305
+
1306
+ **Semaine 1 :** ✅ Complétée le 13/05/2026
1307
+ - [x] **Mémoire conversationnelle AI_AGENT** — Redis sliding window 20 messages (TTL 24h), historique injecté dans system prompt. → `apps/whatsapp-worker/src/handlers/AIAgentHandler.ts`
1308
+ - [x] **Structured outputs** — Déjà implémenté via `zodResponseFormat` + `FeedbackSchema` Zod en production dans `OpenAIProvider`.
1309
+ - [x] **HNSW index pgvector** — Script one-shot prêt → `packages/database/scripts/add-hnsw-index.ts`. Commande : `pnpm --filter @repo/database exec tsx scripts/add-hnsw-index.ts`
1310
+ - [x] **Seuil pertinence RAG (0.70)** — `searchRelevantContext()` filtre `WHERE similarity > 0.70`. Retourne `''` si vide → agent répond honnêtement. → `apps/whatsapp-worker/src/services/indexing.ts`
1311
+ - [x] **Alertes wallet** — Scheduler horaire + email Brevo si < 3 jours runway. Suppression 6h anti-spam Redis. → `apps/whatsapp-worker/src/scheduler.ts`
1312
+ - [x] **Rapport hebdomadaire** — Scheduler lundi 07:00 UTC : résumé appels IA, coût FCFA, trend vs semaine précédente, solde wallet. → `apps/whatsapp-worker/src/scheduler.ts`
1313
+
1314
+ **Semaine 2 :** (en cours)
1315
+ - [ ] Scheduling campagnes (`sendAt` dans BroadcastHandler)
1316
+ - [ ] SSE pour updates temps réel CRM Inbox + Dashboard
1317
+ - [ ] Segmentation contacts par tags
1318
+
1319
+ **Semaine 3 :** (à venir)
1320
+ - [ ] Passage Claude Sonnet 4.6 pour les appels feedback (coût / qualité)
1321
+ - [ ] Agent Créateur de Contenu (prototype — génère curriculum depuis description)
1322
+
1323
+ **Semaine 4 :** (à venir)
1324
+ - [ ] Détection handoff humain (mots-clés + sentiment négatif)
1325
+ - [ ] Soft-delete User/Enrollment/UserProgress
1326
+
1327
+ ---
1328
+
1329
+ ## Conclusion
1330
+
1331
+ Xamlé est une plateforme mature avec une architecture solide. Les opportunités de valeur les plus importantes se trouvent dans **trois axes** :
1332
+
1333
+ 1. **Fiabilité IA** : Mémoire conversationnelle + structured outputs + seuil RAG → transformation de l'expérience utilisateur AI_AGENT (aujourd'hui stateless et parfois hors-sujet, demain cohérent et précis).
1334
+
1335
+ 2. **Proactivité admin** : Alertes temps réel + rapports automatiques + agent conseiller → l'admin n'a plus besoin d'aller chercher l'information, elle vient à lui.
1336
+
1337
+ 3. **Agent Créateur de Contenu** : Générer un curriculum complet en 5 minutes au lieu de 2 semaines → démultiplicateur de productivité massif pour les orgs EDTECH.
1338
+
1339
+ Ces trois axes combinés font de Xamlé une plateforme où **l'IA fait le travail à la place de l'admin**, pas seulement un outil que l'admin utilise.
packages/database/scripts/add-hnsw-index.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * One-time migration: adds HNSW index on KnowledgeBaseEntry.embedding
3
+ * for fast cosine-similarity search via pgvector.
4
+ *
5
+ * Run once against production:
6
+ * pnpm --filter @repo/database exec tsx scripts/add-hnsw-index.ts
7
+ *
8
+ * Safe to re-run — uses CREATE INDEX IF NOT EXISTS.
9
+ * CONCURRENTLY means it does not lock the table during creation.
10
+ */
11
+ import { PrismaClient } from '@prisma/client';
12
+
13
+ const prisma = new PrismaClient();
14
+
15
+ async function run() {
16
+ console.log('[HNSW] Ensuring pgvector extension is enabled...');
17
+ await prisma.$executeRawUnsafe(`CREATE EXTENSION IF NOT EXISTS vector;`);
18
+
19
+ console.log('[HNSW] Creating HNSW index on KnowledgeBaseEntry.embedding (this may take a minute)...');
20
+ await prisma.$executeRawUnsafe(`
21
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS kb_embedding_hnsw_idx
22
+ ON "KnowledgeBaseEntry" USING hnsw (embedding vector_cosine_ops)
23
+ WITH (m = 16, ef_construction = 64);
24
+ `);
25
+
26
+ console.log('[HNSW] Setting ef_search for query-time accuracy/speed trade-off...');
27
+ await prisma.$executeRawUnsafe(`SET hnsw.ef_search = 40;`);
28
+
29
+ console.log('[HNSW] ✅ Index created. Cosine search is now ~10x faster on large knowledge bases.');
30
+ await prisma.$disconnect();
31
+ }
32
+
33
+ run().catch(err => {
34
+ console.error('[HNSW] ❌ Failed:', err);
35
+ process.exit(1);
36
+ });