CognxSafeTrack
feat: 4 UX/backend fixes — anti-ghost image, time-travel replay, UX message order, contextual AI remediation
a4ce760 | import { z } from 'zod'; | |
| import { LLMProvider, OnePagerData, OnePagerSchema, PitchDeckData, PitchDeckSchema, PersonalizedLessonSchema, FeedbackSchema, FeedbackData } from './types'; | |
| import { MockLLMProvider } from './mock-provider'; | |
| import { OpenAIProvider } from './openai-provider'; | |
| import { searchService } from './search'; | |
| import { GeminiProvider } from './gemini-provider'; | |
| class AIService { | |
| private primaryProvider: LLMProvider; | |
| private fallbackProvider: LLMProvider | null = null; | |
| private avProvider: LLMProvider | null = null; // Specifically for Audio/Video/Image (OpenAI) | |
| private mockProvider: LLMProvider; | |
| constructor() { | |
| this.mockProvider = new MockLLMProvider(); | |
| const geminiApiKey = process.env.GOOGLE_AI_API_KEY; | |
| const openAiApiKey = process.env.OPENAI_API_KEY; | |
| if (geminiApiKey) { | |
| console.log('[AI_SERVICE] Initializing Gemini as Primary Provider...'); | |
| this.primaryProvider = new GeminiProvider(geminiApiKey); | |
| if (openAiApiKey) { | |
| console.log('[AI_SERVICE] Initializing OpenAI as Fallback & A/V Provider...'); | |
| const openai = new OpenAIProvider(openAiApiKey); | |
| this.fallbackProvider = openai; | |
| this.avProvider = openai; | |
| } | |
| } else if (openAiApiKey) { | |
| console.log('[AI_SERVICE] Gemini Key missing. Initializing OpenAI as Primary & A/V Provider...'); | |
| const openai = new OpenAIProvider(openAiApiKey); | |
| this.primaryProvider = openai; | |
| this.avProvider = openai; | |
| } else { | |
| console.log('[AI_SERVICE] No AI API Keys found. Initializing MOCK Provider...'); | |
| this.primaryProvider = this.mockProvider; | |
| this.avProvider = this.mockProvider; | |
| } | |
| } | |
| /** | |
| * Internal wrapper for structured data calls with failover logic. | |
| */ | |
| private async callWithFailover<T>( | |
| prompt: string, | |
| schema: z.ZodSchema<T>, | |
| temperature?: number, | |
| imageUrl?: string | |
| ): Promise<{ data: T, source: string }> { | |
| try { | |
| const data = await this.primaryProvider.generateStructuredData(prompt, schema, temperature, imageUrl); | |
| const source = (this.primaryProvider instanceof GeminiProvider) ? 'GEMINI' : | |
| (this.primaryProvider instanceof OpenAIProvider) ? 'OPENAI' : 'MOCK'; | |
| console.log(`[AI_INFO] ${source} used successfully. (Vision: ${!!imageUrl})`); | |
| return { data, source }; | |
| } catch (err) { | |
| if (this.fallbackProvider) { | |
| console.warn('[AI_WARNING] Primary provider failed, falling back to OpenAI...', (err as Error).message); | |
| const data = await this.fallbackProvider.generateStructuredData(prompt, schema, temperature, imageUrl); | |
| console.log('[AI_INFO] OPENAI used as fallback.'); | |
| return { data, source: 'OPENAI' }; | |
| } | |
| throw err; | |
| } | |
| } | |
| /** | |
| * Extracts a One-Pager JSON structure from raw user data. | |
| */ | |
| async generateOnePagerData(userContext: string, language: string = 'FR', businessProfile?: any): Promise<OnePagerData> { | |
| const marketDataInjected = businessProfile?.marketData | |
| ? `\n🌐 DONNÉES DE MARCHÉ RÉELLES (Google Search) :\n${JSON.stringify(businessProfile.marketData, null, 2)}\n` | |
| : ''; | |
| const prompt = ` | |
| Basé sur l'activité de l'étudiant (${businessProfile?.activityLabel || 'non précisé'}) et son contexte, génère un One-Pager (Business Plan d'une page). | |
| Utilise les données de marché ci-dessous pour rendre le document extrêmement professionnel et crédible. | |
| USER INPUT: | |
| ${userContext} | |
| ${marketDataInjected} | |
| STRICTES CONTRAINTES DE QUALITÉ "PREMIUM V4" : | |
| - DENSITÉ RÉDACTIONNELLE : Chaque section (Problem, Solution, Target, Business Model) doit être un paragraphe détaillé, articulé et stratégique (minimum 3 phrases analytiques, 15-25 mots). Les réponses courtes de l'utilisateur DOIVENT être enrichies avec ton 'Knowledge Base' métier (ex: enjeux de distribution pour ${businessProfile?.activityLabel || 'ce secteur'}, délais de livraison locaux). | |
| - ANTI-JARGON SAAS : INTERDICTION FORMELLE d'utiliser les mots "Premium", "Trial", "Subscription", "Sign up" ou "SaaS". Adapte le Modèle Économique au secteur réel (Vente directe, prestation de service, acompte, etc). | |
| - SOURCES (marketSources) : Si tu utilises les données de marché injectées, cite explicitement la source (ex: "Source: ANSD 2024"). | |
| - ANALYSE vs DESCRIPTION : Ne te contente pas d'énumérer. Explique l'impact business et le positionnement luxe. | |
| - DÉTANCHÉITÉ LINGUISTIQUE : 100% ${language === 'WOLOF' ? 'WOLOF standardisé v4.0' : 'Français institutionnel'}. | |
| - DATA STEWARD : Intègre les données de marché réelles (ANSD, UEMOA) et les projections financières. | |
| LANGUAGE: Write EVERYTHING in ${language === 'WOLOF' ? 'WOLOF (ñ, ë, é) suivi de la traduction FR' : 'French'}. NO ENGLISH. | |
| `; | |
| const { data, source } = await this.callWithFailover(prompt, OnePagerSchema); | |
| return { ...data, aiSource: source }; | |
| } | |
| /** | |
| * Extracts a Slide Deck JSON structure from raw user data. | |
| */ | |
| async generatePitchDeckData(userContext: string, language: string = 'FR', businessProfile?: any): Promise<PitchDeckData & { aiSource?: string }> { | |
| const marketDataInjected = businessProfile?.marketData | |
| ? `\n🌐 DONNÉES DE MARCHÉ (RECHERCHE WEB) :\n${JSON.stringify(businessProfile.marketData, null, 2)}\n` | |
| : ''; | |
| const teamDataInjected = businessProfile?.teamMembers && Array.isArray(businessProfile.teamMembers) && businessProfile.teamMembers.length > 0 | |
| ? `\n👥 MEMBRES DE L'ÉQUIPE (PHOTOS/BDD) :\n${JSON.stringify(businessProfile.teamMembers, null, 2)}\n` | |
| : ''; | |
| const prompt = ` | |
| Tu es un expert en Pitch Decks internationaux (VC-ready). | |
| Génère un deck de 13 slides STRICTEMENT basé sur la structure suivante pour ce business : | |
| Secteur : ${businessProfile?.activityLabel || 'Entrepreneuriat'} | |
| Région : ${businessProfile?.locationCity || 'Sénégal'} | |
| STRUCTURE DES 13 SLIDES : | |
| 1. Couverture : Logo, nom, slogan clair. | |
| 2. Problème : Pain point étayé par des faits. | |
| 3. Solution : Comment le service résout le problème. | |
| 4. Produit/Techno : Démo ou captures (conceptuelles). | |
| 5. Taille du Marché (TAM/SAM/SOM) : Un graphique en cercles concentriques. Calcule le marché total (Dakar ou Sénégal), le marché adressable et ta part cible à partir des données réelles. | |
| 6. Business Model : Qui paie, combien, fréquence. | |
| 7. Traction / Métriques : Preuves de validation. | |
| 8. Go-to-Market : Stratégie d'acquisition. | |
| 9. Concurrence : Paysage concurrentiel et avantage injuste (utilise les données réelles fournies). | |
| 10. Équipe : Profils fondateurs et expertise (Bio issue du BusinessProfile). | |
| 11. Projections Financières : Vision de croissance à 5 ans. | |
| 12. L'Appel (The Ask) : Ce dont tu as besoin (financement, partenaires). | |
| 13. Contact : Coordonnées et mot de la fin. | |
| USER INPUT: | |
| ${userContext} | |
| ${marketDataInjected} | |
| ${teamDataInjected} | |
| STRICTES CONTRAINTES DE QUALITÉ "GENSPARK-STANDARD" : | |
| - STORYTELLING (CRUCIAL) : Rédige de véritables petits paragraphes narratifs (2 à 3 phrases, 15-25 mots par bloc). INTERDICTION TOTALE d'utiliser des listes à puces ("bullet points") classiques avec des mots isolés. Raconte une histoire convaincante pour un investisseur. | |
| - RECHERCHE INTELLIGENTE INTEGREE : Tu dois IMPÉRATIVEMENT fusionner les [DONNÉES DE MARCHÉ] avec les mots de l'utilisateur. Ne te contente pas de citer, explique l'impact (ex: "Fort de 4,4M d'habitants à Dakar selon l'ANSD, le marché représente une opportunité..."). | |
| - ANTI-JARGON SAAS : INTERDICTION FORMELLE d'utiliser les mots "Premium", "Trial", "Subscription", "Sign up" ou "SaaS". Le Business Model doit être 100% réaliste pour le secteur local (Vente au kilo, Prestation, Acompte, GMS). | |
| - ANALYSE vs DESCRIPTION : Ne décris pas, analyse. (Ex: Au lieu de 'Délais non respectés', dis 'L'instabilité chronique des délais de livraison artisanaux dégrade l'expérience client et réduit le taux de réachat'). | |
| - Slide 5 (Marché) : visualType = 'PIE_CHART', visualData = { labels: ["TAM (Total)", "SAM (Cible)", "SOM (Capturable)"], values: [1000000, 500000, 50000] }. Utilise le cascade : National > Régional > UEMOA. La source (ex: "Source: ANSD 2024") DOIT ÊTRE EXPLICITEMENT CITÉE en bas du slide dans content ou notes. | |
| - Slide Équipe (10) : Si des données d'équipe existent, définis IMPERATIVEMENT visualType = 'TEAM' et place le tableau EXACT JSON fourni dans MEMBRES DE L'ÉQUIPE dans visualData. | |
| - Slide 11 (Finances) : visualType = 'BAR_CHART', visualData = { labels: ["Année 1", "Année 2", "Année 3", "Année 4", "Année 5"], values: [100, 200, 400, 800, 1600] }. Explique la LOGIQUE de croissance financière de manière narrative. | |
| - STRICTEMENT 3 à 4 blocs de texte par slide. Aucun bloc de moins de 15 mots. | |
| - DÉTANCHÉITÉ LINGUISTIQUE : | |
| * Si language === 'FR' : 100% Français de haut niveau (ton institutionnel, banquier d'affaires). ZÉRO mot en Wolof. | |
| * Si language === 'WOLOF' : 100% Wolof standardisé v4.0. ZÉRO mot en Français. | |
| - LANGUAGE: ${language === 'WOLOF' ? 'WOLOF' : 'FRENCH'}. | |
| IDENTIFIER: PITCH_DECK (Used for model selection) | |
| `; | |
| const { data, source } = await this.callWithFailover(prompt, PitchDeckSchema); | |
| return { ...data, aiSource: source }; | |
| } | |
| /** | |
| * Generates a short pedagogical feedback for the student's answer based on v1.0 criteria. | |
| */ | |
| async generateFeedback( | |
| userInput: string, | |
| expectedExercise: string, | |
| lessonContent: string, | |
| userLanguage: string = 'FR', | |
| businessProfile?: any, | |
| exerciseCriteria?: any, | |
| // Expert coaching context | |
| userActivity?: string, | |
| userRegion?: string, | |
| dayNumber?: number, | |
| previousResponses?: Array<{ day: number; response: string }>, | |
| isDeepDive: boolean = false, | |
| iterationCount: number = 0, | |
| imageUrl?: string, | |
| isButtonChoice: boolean = false | |
| ): Promise<FeedbackData & { searchResults?: any[] }> { | |
| // 🚀 Question Detection Logic (Lead AI Engineer Requirement) | |
| const questionKeywords = ['?', 'avis', 'penses', 'conseil', 'aider', 'comment', 'pourquoi', 'idée', 'prix', 'standard']; | |
| const lowerInput = userInput.toLowerCase(); | |
| const hasQuestion = questionKeywords.some(kw => lowerInput.includes(kw)); | |
| console.log(`[AI_INTERACTION] User asked a question: ${hasQuestion}`); | |
| const activityLabel = userActivity || businessProfile?.activityLabel || 'non précisé'; | |
| const region = userRegion || businessProfile?.region || 'Sénégal'; | |
| const customer = businessProfile?.mainCustomer || ''; | |
| const offer = businessProfile?.offerSimple || ''; | |
| const promise = businessProfile?.promise || ''; | |
| const problem = businessProfile?.mainProblem || ''; | |
| const businessContext = [ | |
| `🏪 BUSINESS DE L'ÉTUDIANT :`, | |
| `- Activité : ${activityLabel}`, | |
| `- Région : ${region}`, | |
| customer ? `- Client principal : ${customer}` : '', | |
| offer ? `- Offre : ${offer}` : '', | |
| problem ? `- Problème résolu : ${problem}` : '', | |
| promise ? `- Promesse unique : ${promise}` : '', | |
| ].filter(Boolean).join('\n'); | |
| const prevContext = previousResponses && previousResponses.length > 0 | |
| ? `\nCONTEXTE HISTORIQUE DU BUSINESS :\n${previousResponses.map(r => `[Jour ${r.day}]: "${r.response}"`).join('\n')}` | |
| : ''; | |
| const previousResponsesContext = previousResponses && previousResponses.length > 0 | |
| ? `\n\nHISTORIQUE DES RÉPONSES PRÉCÉDENTES :\n${previousResponses.map(r => `Jour ${r.day}: ${r.response}`).join('\n')}` | |
| : ''; | |
| let searchContext = ''; | |
| let searchResults: any[] | undefined = undefined; | |
| // 🚀 Brique 1: Activation du Browsing (Optimized for Day 7) | |
| const isDay7Choice = dayNumber === 7 && (userInput.length < 15 || ['whatsapp', 'boutique', 'digital', 'physique'].includes(lowerInput)); | |
| if (!isDay7Choice) { | |
| console.log(`[AI_SERVICE] 🔍 Triggering Market Search for Day ${dayNumber}...`); | |
| // Remove hallucinatory generic fallback words | |
| const cleanActivity = activityLabel.replace(/non précisé|e-commerce/i, '').trim() || 'Entrepreneuriat'; | |
| let query = `${cleanActivity} ${region} Sénégal marché chiffres statistiques data`; | |
| if (dayNumber === 10) { | |
| query = `startups concurrents ${cleanActivity} ${region} Sénégal solutions paiement UEMOA`; | |
| } else if (dayNumber === 11) { | |
| query = `benchmarks marges rentabilité ${cleanActivity} Afrique de l'Ouest tech business model`; | |
| } else if (dayNumber === 12) { | |
| query = `benchmarks revenus levée de fonds analyse concurrents locaux ${cleanActivity} Afrique de l'Ouest`; | |
| } | |
| try { | |
| const results = await searchService.search(query); | |
| if (results && results.length > 0) { | |
| searchResults = results; | |
| searchContext = `\n🌐 DONNÉES DE MARCHÉ RÉELLES (Google Search) :\n${results.map(r => `- ${r.title}: ${r.snippet}`).join('\n')}\n`; | |
| console.log(`[AI_SERVICE] ✅ Search enrichment added (Query: ${query}).`); | |
| } | |
| } catch (err) { | |
| console.error('[AI_SERVICE] Search enrichment failed:', err); | |
| } | |
| } else { | |
| console.log(`[AI_SERVICE] ⚡ Bypassing search for Day 7 choice (Speed-up).`); | |
| } | |
| const criteriaContext = exerciseCriteria | |
| ? `CRITÈRES D'ÉVALUATION :\n${JSON.stringify(exerciseCriteria, null, 2)}` | |
| : 'CRITÈRES : Réponse concrète, personnelle, et spécifique à son business réel.'; | |
| let actionPrompt = ''; | |
| if (isDeepDive) { | |
| if (iterationCount >= 3) { | |
| // Force closer if we hit the limit | |
| actionPrompt = ` | |
| ⚠️ RÉPONSE DE MENTORAT FINAL (LIMITE D'ITÉRATION ATTEINTE) ⚠️ | |
| Remercie chaleureusement l'utilisateur pour sa précision finale. | |
| Tu dois maintenant fermer le deep-dive en donnant ton analyse décisive basée à la fois sur tes recherches ET sur l'information terrain qu'il vient de donner. | |
| Règle stricte d'Anti-Hallucination : CE QUE L'UTILISATEUR VIENT DE FOURNIR EST LA VÉRITÉ TERRAIN ET ANNULE TES SOURCES EN CAS DE CONFLIT. | |
| IMPORTANT : Interdiction de poser une question à la fin. Exige seulement à l'utilisateur de taper "SUITE" pour clôturer cette étape. | |
| MET isForcedClosure À TRUE DANS TA RÉPONSE JSON. | |
| `; | |
| } else if (dayNumber === 11) { | |
| actionPrompt = ` | |
| 🔄 TEAM BUILDING "DEEP DIVE" (Tour ${iterationCount}/3) 🔄 | |
| L'utilisateur a choisi d'approfondir l'équipe (Jour 11). | |
| ${iterationCount === 1 && !imageUrl ? `Dis STRICTEMENT : "Super ! Envoie-moi la photo de ton premier membre d'équipe avec son nom et son rôle." (Ne dis rien de plus).` : ''} | |
| ${imageUrl ? ` | |
| 📸 ANALYSE VISUELLE (MULTIMODAL) : | |
| - L'utilisateur a envoyé une photo pour son équipe. | |
| - VÉRIFICATION OBLIGATOIRE : Tu dois d'abord valider que la photo envoyée ressemble bien à une personne (portrait humain) ou un logo d'équipe, et non à un objet aléatoire. Si ce n'est pas le cas, excuse-toi poliment et demande une vraie photo de la personne. | |
| - Si la photo est valide, EXTRAIS le nom de la personne et son rôle à partir de son texte ("${userInput}"). | |
| - Mets la photoUrl (qui est "${imageUrl}") et les infos dans ta structure JSON \`teamMembers\`. | |
| - Dis-lui EXACTEMENT : "Bien reçu la photo de [Nom], je l'ajoute en tant que [Rôle] sur ta Slide Équipe." (Suivi de : "En as-tu un autre ou tapes-tu 2️⃣ SUITE pour passer aux chiffres ?") | |
| ` : (!imageUrl && iterationCount > 1) ? ` | |
| - L'utilisateur vient de répondre sans photo supplémentaire ou pour passer à la suite. Clôture en douceur ou prends ses remarques sur l'équipe en compte. | |
| ` : ''} | |
| ATTENTION : Ne pose AUCUNE question qui relève d'une leçon suivante (Reste sur l'Équipe). | |
| `; | |
| } else { | |
| actionPrompt = ` | |
| 🔄 ANALYSE ITÉRATIVE "DEEP DIVE" (Tour ${iterationCount}/3) 🔄 | |
| L'utilisateur fournit une information précise issue de son terrain suite à ta précédente analyse. | |
| 1. Dis "Merci pour cette précision sur [Point abordé]. Ces informations sont bien enregistrées dans ton dossier pour les slides correspondantes (ex: prix pour le Business Model, obstacles pour les Finances/Risques). En les intégrant, ton modèle devient encore plus solide car... [Donne l'Analyse]". | |
| 2. Règle stricte d'Anti-Hallucination : CE QUE L'UTILISATEUR VIENT DE FOURNIR EST LA VÉRITÉ TERRAIN ET ANNULE TES SOURCES EN CAS DE CONFLIT. | |
| 3. Pose UNE SEULE question ciblée pour l'amener à réfléchir sur un sous-jacent lié EXCLUSIVEMENT à son secteur (${activityLabel}) (ex: 'Comment tes clients de ce secteur paient-ils ?', 'Ont-ils tous WhatsApp ?'). Ne pose JAMAIS de questions génériques de type 'Quoi d'autre ?'. | |
| ATTENTION : Ne pose AUCUNE question qui relève d'une leçon suivante (reste bloqué sur le périmètre du Jour ${dayNumber}). | |
| `; | |
| } | |
| } else { | |
| actionPrompt = ` | |
| MISSIONS STRATÉGIQUES "SENIOR BUSINESS COACH" (CORRECTION RADICALE) : | |
| Tu es un consultant pragmatique, pas un professeur pointilleux. Rédige un feedback d'au minimum 15 lignes de haute densité. | |
| ${hasQuestion ? ` | |
| 🚨 DOUBLE INTENTION DÉTECTÉE : L'utilisateur a posé une question ou sollicité un avis stratégique. | |
| - TU DOIS répondre spécifiquement à sa question dans la section "🚀 Version Enrichée". | |
| - Compare son idée ou sa question (ex: prix, cible, stratégie) avec les standards du marché trouvés via tes recherches. | |
| - Sois tranché et expert (Senior Consultant). N'hésite pas à être critique si le modèle est risqué. | |
| - Exemple : Si l'utilisateur propose une marge de 1-2%, préviens-le que les frais Mobile Money absorbent déjà 1% et qu'il risque de travailler à perte.` : ''} | |
| 🧠 FILTRE DE MÉMOIRE (ANTI-RÉPÉTITION) : | |
| - Voici ce que l'utilisateur a déjà partagé : ${previousResponsesContext} | |
| - NE DONNE JAMAIS un conseil que tu as déjà donné ou qui est trop similaire aux points déjà validés. | |
| - Diversifie tes angles : Si tu as déjà parlé de publicité, parle maintenant de Logistique, Psychologie client, Partenariats, ou Technique de vente terrain. | |
| - Interdiction de répéter "Le créneau 6h-8h est critique pour la publicité" si cela a déjà été mentionné. | |
| ${dayNumber === 8 ? ` | |
| 🚨 COACHING VISUEL (PREUVE DE FIABILITÉ) : | |
| Si l'utilisateur a envoyé une image (imageUrl), analyse-la comme une preuve de fiabilité (certificat, photo de produit, capture d'écran). | |
| - TU DOIS mentionner cette preuve dans ton feedback pour renforcer la crédibilité de l'entrepreneur. | |
| - Exemple : "J'ai bien reçu la photo de ton stock, cela renforce énormément ton sérieux." | |
| ` : ''} | |
| LES 3 PILIERS OBLIGATOIRES DU FEEDBACK : | |
| 1. 🌟 Validation (Pilier 1) : Félicite et valide l'idée de l'utilisateur avec l'enthousiasme d'un investisseur. | |
| 2. 🚀 Version Enrichie (Pilier 2) : Réécris sa phrase de manière exécutive en y intégrant OBLIGATOIREMENT des données chiffrées réelles trouvées dans ta recherche (ex: taille du marché selon la recherche, nombre de clients potentiels) et des termes stratégiques (supply chain, B2B, rétention). | |
| 3. 💡 Conseil Actionnable (Pilier 3) : Donne un conseil de terrain hyper concret basé là aussi sur la recherche web (ex: "Conseil : Dans le secteur de ${activityLabel} à ${region}, il est crucial de..."). | |
| ${imageUrl ? ` | |
| 📸 ANALYSE VISUELLE (MULTIMODAL) : | |
| - L'utilisateur a envoyé une image comme preuve ou illustration. | |
| - TU DOIS analyser visuellement cette image et intégrer ton constat dans le feedback. | |
| - Analyse l'image jointe (photo, diplôme, capture d'écran) et intègre sa description comme une preuve de crédibilité dans le feedback et les métadonnées de la Slide Confiance. | |
| - Si l'image contient des chiffres ou des contrats, extrais-les pour confirmer les données financières.` : ''} | |
| ⚠️ INTERDICTION ABSOLUE (Anti-Remediation Loop) : | |
| - Tu NE DOIS PLUS JAMAIS demander à l'utilisateur de s'expliquer davantage (ex: "Quel est l'âge exact ?", "Combien gagnes-tu ?"). | |
| - S'il manque un détail mais que l'idée est claire, c'est TOI qui apportes le savoir (donne les tranches d'âges classiques du secteur, donne les revenus moyens du pays). | |
| - Tu es là pour ENRICHIR sa vision, pas pour lui faire passer un interrogatoire. Ne pose AUCUNE question bloquante à la fin. | |
| ⚠️ COHÉRENCE THÉMATIQUE OBLIGATOIRE (ANTI-HORS-SUJET) : | |
| Avant de valider, vérifie que la réponse de l'étudiant est liée à la question du Jour ${dayNumber} : "${expectedExercise?.substring(0, 200)}". | |
| Si la réponse est TOTALEMENT hors-sujet (ex: salutation, spam, ou thème sans rapport), alors: | |
| - Mets OBLIGATOIREMENT isQualified: false. | |
| - Explique-lui POLIMENT et PRÉCISÉMENT (en 2-3 phrases max dans la section validation) ce qu'il doit corriger pour réussir cet exercice. | |
| - Exemple: Si l'exercice demande "Décris ton client principal" et l'étudiant répond "Bonjour bonne journée", c'est hors-sujet → isQualified: false et tu dis "Pour valider cette étape, je dois savoir QUI achète ton produit : son profil, son âge, ses habitudes. Réponds à l'exercice pour continuer." | |
| - Ne sois sévère QUE sur le hors-sujet flagrant. Une réponse courte mais pertinente reste validée (isQualified: true). | |
| **CRITÈRE DE VALIDATION (isQualified)** : Dès que l'utilisateur fournit une réponse sérieuse liée à son projet (même courte), mets 'isQualified: true'. Ne sois pas trop sévère. S'il y a un doute, valide et enrichis dans ton feedback. | |
| INVITATION DEEP-DIVE OBLIGATOIRE : | |
| ${hasQuestion ? `L'utilisateur ayant posé une question, tu DOIS systématiquement proposer l'option d'approfondissement pour explorer des alternatives stratégiques.` : ''} | |
| Termine EXACTEMENT ton pilier 3 par cette phrase selon la langue : | |
| (FR): "Si tu veux affiner ce point avec une donnée de ton propre terrain, tape 1️⃣ APPROFONDIR, sinon tape 2️⃣ SUITE." | |
| (WO): "Su nga bëggee yokk leneen ci li nga xam, bindal 1️⃣ APPROFONDIR, wala nga bind 2️⃣ SUITE." | |
| ${isButtonChoice ? ` | |
| 🚨 PROMPT HOOK (RELANCE AUTOMATIQUE) : | |
| L'utilisateur a choisi une option via un bouton/carte ("${userInput}"). | |
| - TU DOIS valider positivement ce choix dans le pilier 1. | |
| - TU DOIS obligatoirement poser une question d'approfondissement à la fin de ton texte (Pilier 3) pour l'inciter à expliquer "pourquoi" ou "comment" ce choix s'applique à son business. | |
| - Exemple : "Bien reçu pour WhatsApp ! C'est un excellent choix. Peux-tu me dire en quelques mots pourquoi ce canal est le meilleur pour ton projet ?" | |
| ` : ''} | |
| `; | |
| } | |
| const prompt = ` | |
| Tu es XAMLÉ COACH, expert business en Afrique de l'Ouest. Évalue la réponse de l'étudiant pour le JOUR ${dayNumber}. | |
| CONTEXTE DU PROFIL : | |
| Activité : "${activityLabel}" | |
| Ville/Région : "${region}" | |
| Jour actuel : ${dayNumber} | |
| ${businessContext} | |
| ${prevContext} | |
| ${criteriaContext} | |
| ${searchContext} | |
| ${previousResponsesContext} | |
| CONTEXTE DE LA LEÇON : | |
| "${lessonContent.substring(0, 500)}" | |
| EXERCICE ATTENDU : | |
| "${expectedExercise}" | |
| RÉPONSE OU PRÉCISION DE L'ÉTUDIANT : | |
| "${userInput}" | |
| ${actionPrompt} | |
| PROTOCOLE DATA STEWARD (Intégrité Géographique et Sectorielle) : | |
| - Reste EXCLUSIVEMENT sur le secteur : ${activityLabel}. Ne fais AUCUNE supposition sur un secteur "e-commerce" par défaut si cela n'est pas stipulé. | |
| - Si l'utilisateur est à ${region}, donne lui les vrais chiffres de ${region}. S'ils n'existent pas, back-up sur les chiffres du Sénégal. | |
| - TRANSPARENCE : Cite toujours la source formelle (ex: Agence Nationale de la Statistique et de la Démographie, Banque Mondiale, Direction du commerce, etc.). | |
| ÉTANCHÉITÉ LINGUISTIQUE (OBLIGATION ABSOLUE) : | |
| Tu es configuré dans le mode linguistique suivant : ${userLanguage}. | |
| ${userLanguage === 'WOLOF' ? | |
| `ALERTE MAXIMALE : Tu as l'INTERDICTION FORMELLE ET DÉFINITIVE d'utiliser ne serait-ce qu'UN SEUL MOT de Français. TOUTE TA RÉPONSE DOIT ÊTRE EN WOLOF STANDARD. Utilise exclusivement le Glossaire Wolof Officiel (v4.0). Si tu ne connais pas le mot technique en wolof, utilise une périphrase ou une structure simple mais RESTE EN WOLOF (ex: "njàngat" au lieu de "analyse"). Utilise bien les caractères ñ, ë, é.` : | |
| `ALERTE MAXIMALE : Tu as l'INTERDICTION FORMELLE d'utiliser du Wolof. Reste dans un Français institutionnel et pragmatique.`} | |
| `; | |
| // 📸 VISION HARDENING: Always use OpenAI (GPT-4o) for image-based feedback (more reliable multimodal JSON) | |
| let result; | |
| if (imageUrl && this.avProvider) { | |
| console.log(`[AI_SERVICE] 📸 Image detected. Forcing OpenAI/AV-Provider for Day ${dayNumber}.`); | |
| const data = await this.avProvider.generateStructuredData(prompt, FeedbackSchema, 0.7, imageUrl); | |
| result = { data, source: 'OPENAI' }; | |
| } else { | |
| result = await this.callWithFailover(prompt, FeedbackSchema, 0.7, imageUrl); | |
| } | |
| const { data, source } = result; | |
| // 🚨 Day 11 Guard: Ensure team members are not returned for earlier days | |
| if (dayNumber !== undefined && dayNumber < 11 && (data as any).teamMembers) { | |
| console.log(`[AI_SERVICE] Pruning teamMembers from feedback (Day ${dayNumber} < 11)`); | |
| delete (data as any).teamMembers; | |
| } | |
| return { | |
| ...data, | |
| searchResults, | |
| aiSource: source | |
| }; | |
| } | |
| /** | |
| * Rewrites a daily lesson to use analogies relevant to the user's business sector. | |
| */ | |
| async generatePersonalizedLesson( | |
| lessonText: string, | |
| userActivity: string, | |
| userLanguage: string = 'FR', | |
| businessProfile?: any, | |
| previousResponses?: Array<{ day: number; response: string }> | |
| ): Promise<{ lessonText: string, aiSource: string }> { | |
| const activityLabel = userActivity || businessProfile?.activityLabel || 'entrepreneuriat'; | |
| const customer = businessProfile?.mainCustomer || ''; | |
| const region = businessProfile?.region || 'Sénégal'; | |
| const problem = businessProfile?.mainProblem || ''; | |
| const businessContext = [ | |
| `🏪 BUSINESS DE L'ÉTUDIANT :`, | |
| `- Activité : ${activityLabel}`, | |
| `- Région : ${region}`, | |
| customer ? `- Client : ${customer}` : '', | |
| problem ? `- Problème résolu : ${problem}` : '', | |
| ].filter(Boolean).join('\n'); | |
| // Inject real exercise responses so lesson examples match the user's actual business | |
| const prevContext = previousResponses && previousResponses.length > 0 | |
| ? `\n📝 CE QUE L'ÉTUDIANT A DÉJÀ DIT SUR SON BUSINESS (utilise ces infos exactes pour les exemples) :\n${previousResponses.map(r => ` Jour ${r.day}: "${r.response.substring(0, 200)}"`).join('\n')}\n` | |
| : ''; | |
| const prompt = ` | |
| Tu es XAMLÉ COACH, expert business pour entrepreneurs d'Afrique de l'Ouest. | |
| Réécris la leçon ci-dessous pour qu'elle parle DIRECTEMENT au business de cet entrepreneur. | |
| ${businessContext} | |
| ${prevContext} | |
| 1. RÈGLE D'OR : ANTI-HALLUCINATION | |
| Adapte TOUS les exemples de la leçon pour qu'ils soient directement liés à la véritable activité de l'étudiant (${activityLabel}). | |
| Agnosticisme Sectoriel : Tu ne dois JAMAIS utiliser d'exemples liés aux forages, à l'irrigation, aux agriculteurs ou aux fourneaux, SAUF si l'activité déclarée de l'étudiant (${activityLabel}) y est explicitement liée. | |
| Zéro-Shot Contextuel : Adapte tous tes exemples UNIQUEMENT au métier réel de l'étudiant (${activityLabel}). Interdiction formelle d'inventer un secteur d'activité. | |
| Règle de Sécurité : Si l'utilisateur n'a pas défini son projet ou si l'activité est inconnue, utilise le terme générique "ton business" et reste sur des principes théoriques abstraits. | |
| Interdiction de généralisation paresseuse : Reste direct et ancré dans le métier de l'utilisateur. Garde la VALEUR PÉDAGOGIQUE EXACTEMENT la même. | |
| 2. NORMALISATION WOLOF & TONE | |
| STT Cleanup : Applique les règles de normalisation Wolof (ex: 'damae' -> 'damay', 'jendi' -> 'jënd') dans ton texte. | |
| Utilisation du Glossaire Officiel : Utilise exclusivement les termes validés (ex: Ñàkk pour perte, Xaalis pour argent, Denc pour épargne). | |
| CONTRAINTES DE FORMAT : | |
| - STYLE : WhatsApp (gras *texte*, emojis). Sois direct, dynamique et encourageant. | |
| - LANGUE : ${userLanguage === 'WOLOF' ? 'WOLOF (avec ñ, ë, é). Wolof en priorité, suivi de la traduction française précédée de (FR)' : 'Français'}. | |
| - JAMAIS ANGLAIS. Ne jamais citer "Manga Deaf". | |
| LEÇON À ADAPTER : | |
| ${lessonText} | |
| Wolof v4.0 si WOLOF : ñ (Waññi, Ñàkk), ë (Jënd), é (Liggéey). FCFA pour montants. | |
| `; | |
| const { data, source } = await this.callWithFailover(prompt, PersonalizedLessonSchema); | |
| return { lessonText: data.lessonText, aiSource: source }; | |
| } | |
| /** | |
| * Transcribes an audio buffer to text (useful for Wolof/FR voice messages) and returns confidence score. | |
| */ | |
| async transcribeAudio(audioBuffer: Buffer, filename: string, language?: string): Promise<{ text: string, confidence: number }> { | |
| // Transcriptions are currently exclusively via OpenAI Whisper (or mock) | |
| const provider = this.avProvider || this.mockProvider; | |
| return provider.transcribeAudio(audioBuffer, filename, language); | |
| } | |
| /** | |
| * Extracts business profile details from user input based on the current day. | |
| */ | |
| async extractBusinessProfile(userInput: string, dayNumber: number, userLanguage: string = 'FR'): Promise<{ | |
| activityLabel: string | null, | |
| activityType: string | null, | |
| mainCustomer: string | null, | |
| mainProblem: string | null, | |
| offerSimple: string | null, | |
| promise: string | null, | |
| aiSource?: string | |
| }> { | |
| const prompt = ` | |
| Tu es un analyste business expert (XAMLÉ). Ta mission est d'extraire des informations clés sur le business de l'étudiant à partir de son message. | |
| MESSAGE DE L'ÉTUDIANT : "${userInput}" | |
| JOUR DE FORMATION : ${dayNumber} | |
| RÈGLES D'EXTRACTION : | |
| - Jour 1 : Cherche l'activité (ex: "Vente de jus", "Coiffure"). Label court. | |
| - Jour 2 : Cherche le type d'activité (Vente / Service / Production). | |
| - Jour 3 : Cherche le client principal (ex: "Étudiants", "Voisins"). | |
| - Jour 4 : Cherche le problème principal qu'il résout. | |
| - Jour 7 : Cherche l'offre simple (ce qu'in vend exactement). | |
| - Jour 8 : Cherche la promesse (rapide, moins cher, etc.). | |
| EXTRACTION : | |
| Si l'information n'est pas claire dans le message pour le jour ${dayNumber}, mets null pour le champ concerné. Ne devine pas. | |
| Langue demandée : "${userLanguage === 'WOLOF' ? 'WOLOF' : 'Français'}". | |
| `; | |
| const schema = z.object({ | |
| activityLabel: z.string().nullable(), | |
| activityType: z.string().nullable(), | |
| mainCustomer: z.string().nullable(), | |
| mainProblem: z.string().nullable(), | |
| offerSimple: z.string().nullable(), | |
| promise: z.string().nullable() | |
| }); | |
| const { data, source } = await this.callWithFailover(prompt, schema); | |
| return { ...data, aiSource: source }; | |
| } | |
| /** | |
| * Converts text into an audio MP3 buffer (TTS). | |
| */ | |
| async generateSpeech(text: string): Promise<Buffer> { | |
| const provider = this.avProvider || this.mockProvider; | |
| return provider.generateSpeech(text); | |
| } | |
| /** | |
| * Generates a realistic image based on a prompt. | |
| */ | |
| async generateImage(prompt: string): Promise<string> { | |
| const provider = this.avProvider || this.mockProvider; | |
| return provider.generateImage(prompt); | |
| } | |
| } | |
| // Export a singleton instance | |
| export const aiService = new AIService(); | |