Blueprint private_bp enregistré sans url_prefix (les routes déclarent /api/private-impact/... en entier)
/prepare stocke les métadonnées (graph_id, sim_requirement, agent_count…) dans private_meta.json dans le sim_dir
/prepare appelle ZepEntityReader.get_entities_by_type() en boucle sur les types relationnels puis PrivateImpactProfileGenerator.generate_profiles_from_entities()
/start lit private_simulation_config.json via PrivateImpactRunner.start_simulation()
/report réutilise ReportAgent avec simulation_id=sim_id et graph_id lu depuis private_meta.json
/cleanup délègue entièrement à PrivateImpactRunner.cleanup()
get_private_logger() ajouté à SimulationLogManager — même pattern que get_twitter_logger() / get_reddit_logger() ; fallback supprimé dans run_private_simulation.py
ModeSelector intégré dans Home.vue (right panel, au-dessus de .console-box) ; mode stocké dans sessionStorage (pendingSimMode) — MainView.vue (N°11) doit lire ce flag et rediriger vers /private/:projectId après création du projet
RELATIONAL_TYPES frontend (ouvrier_production, technicien, etc.) : cohérent avec le parseur import — pas de désynchronisation backend (les types backend employee/manager sont utilisés dans le profil interne, non exposés au frontend)
run_private_simulation.py : détecte le format de config par présence de total_simulation_days (PrivateImpactConfigGenerator) vs total_simulation_hours (OASIS) — pas de breaking change
get_initial_exposed_agents : fallback ultime "expose tous les agents" — évite une simulation silencieuse à 0 activité
getPrivateReportStatus dans private.js plutôt que modification de report.js — préserve le flux Public Opinion existant
pollReport récupère report_id final depuis res.data.result.report_id (retourné par task.to_dict() quand completed)
markdown_content en ~3min, contenu pertinent et ancré dans le scénario
Cleanup
✅ OK
cleaned_files: ["run_state.json"]
Bugs identifiés et corrigés — Session 5
#
Sévérité
Fichier
Bug
Fix
5
BLOQUANT
backend/app/services/zep_tools.py:802
get_entities_by_type compare en case-sensitive → "manager" ne match pas "Manager" dans les labels Zep → "No relational entities found"
any(lbl.lower() == entity_type.lower() for lbl in node.labels)
6
Mineur
backend/app/api/private.py:95,243,417
request.get_json() or {} lève 400 si body vide avec Content-Type: application/json
request.get_json(silent=True) or {} sur les 3 endpoints
Observations non-bloquantes
#
Description
A
round=None dans /actions et recent_actions de /status — JSONL contient le champ, mais _read_action_log ne le remonte pas
B
PrivateImpactConfigGenerator override la config event envoyée dans /prepare — initial_exposed_agent_ids et total_simulation_days sont régénérés par le LLM
C
report.content est vide dans la réponse /api/report/<id> — le contenu est dans markdown_content (non dans content) — frontend doit lire le bon champ
D
Un seul agent actif sur 4 : comportement attendu si initial_exposed_agent_ids: [0] (distance 1 = Sophie uniquement) — les agents 1-3 ne reçoivent pas le contexte de décision
Fichiers modifiés — Session 5
Fichier
Action
backend/app/services/zep_tools.py
Bug 5 : matching case-insensitif dans get_entities_by_type
backend/app/api/private.py
Bug 6 : get_json(silent=True) sur 3 endpoints (prepare, start, report)
Rapport : remplace reportResult.title/.summary/.sections (inexistants) par reportResult.outline?.sections + fallback <pre class="report-markdown">{{ reportResult.markdown_content }}</pre>
Fix 2
(non nécessaire)
round=None dans mes scripts de test était une erreur de clé (round vs round_num) — to_dict() et le template utilisent bien round_num
Fix 3
backend/scripts/run_private_simulation.py
get_initial_exposed_agents simplifié : expose TOUS les agents dès le round 1 — le LLM ne filtre plus via initial_exposed_agent_ids (paramètre structurel, pas LLM)
Fix 4
backend/app/api/private.py
Vérifié : 3/3 endpoints déjà en get_json(silent=True) depuis N°14
/api/private-impact/prepare → 404 (en fait 400) sur le projet fromagerie-auriac
Diagnostic : les routes sont correctement enregistrées (flask routes confirme /api/private-impact/prepare POST)
Cause réelle : l'ontologie LLM génère des types domaine-spécifiques (ProductionWorker, CheeseTechnician, FoodRetailer, FamilyMember, etc.) — aucun ne matche _RELATIONAL_ENTITY_TYPES (même avec fix case-insensitif du Bug 5)
Bug corrigé
#
Sévérité
Fichier
Bug
Fix
7
BLOQUANT
backend/app/api/private.py:136
entity_types hardcodé (employee, manager, etc.) — l'ontologie LLM génère des types domaine-spécifiques jamais en liste → 0 entités, "No relational entities found"
Résolution en 3 niveaux : 1) entity_types explicite dans la requête 2) types de l'ontologie du projet (avec filtre _is_structural_type) 3) _RELATIONAL_ENTITY_TYPES en dernier recours
筛选完成: 总节点 14, 符合条件 0, entité_types: set() — graphe mirofish_80371e75ec2844b3 a 14 nœuds mais 0 match
Cause 1 : filter_defined_entities ligne 264 fait l in defined_entity_types (case-sensitive) — les labels Zep peuvent différer en casse
Cause 2 : même avec case-insensitif, les 14 nœuds ont uniquement les labels ["Entity", "Node"] (pas de labels typés) → 0 entités retournées
Le fallback Bug 7 ne s'activait pas car le 404 était retourné par le code précédent avant le fallback synthétique
Cause 3 : la boucle for etype in entity_types: reader.get_entities_by_type(...) faisait N appels Zep (N = nombre de types) — chacun lit tous les nœuds + toutes les arêtes
Bugs corrigés
#
Sévérité
Fichier
Bug
Fix
8
MINEUR
backend/app/services/zep_entity_reader.py:264
l in defined_entity_types case-sensitive → labels Zep ProductionWorker vs type productionworker ne matchent pas
defined_lower = {t.lower() for t in defined_entity_types}; matching_labels = [l for l in custom_labels if l.lower() in defined_lower]
9
BLOQUANT
backend/app/api/private.py:180
Quand 0 entités matchent, retourne 404 immédiatement sans alternative
Remplacé par : _build_synthetic_entities() — crée des EntityNode synthétiques par type d'ontologie (LLM enrichit les profils sans ancrage Zep) ; ajout helper _build_synthetic_entities() ; appel Zep optimisé (filter_defined_entities 1 fois pour tous les types au lieu de N appels)
Résultat
Test unitaire Python confirmé : 7 agents synthétiques créés pour proj_d86ee68acfa3 (types : ProductionWorker, CheeseMaster, SalesRepresentative, ExecutiveTeam, RetailClient, FamilyMember, UnionRepresentative)
EntityNode import ajouté dans private.py
La génération HTTP complète prend plusieurs minutes (3 LLM call groups dans PrivateImpactConfigGenerator.generate_config() — comportement attendu)
Fichiers modifiés — Session 8
Fichier
Action
backend/app/services/zep_entity_reader.py
Bug 8 : case-insensitive dans filter_defined_entities
Ce PR ajoute un second mode de simulation à MiroFish : Private Impact.
Contrairement au mode Opinion Publique (Twitter/Reddit), Private Impact simule l'impact d'une décision privée (ex. achat d'un bien de luxe, licenciement, choix stratégique) dans un réseau relationnel fermé : employés, managers, clients, partenaires, famille.
Ce que ça fait
Pipeline complet : Prepare → Start → Status polling → Actions → Rapport → Cleanup
Agents relationnels : profils enrichis avec relational_link_type, trust_level, seniority_years, encodés dans le persona LLM
Orchestrateur : subprocess + monitoring + état en mémoire
backend/app/api/private.py
Blueprint Flask — 7 routes /api/private-impact/*
frontend/src/api/private.js
Client API Private Impact (7 fonctions)
frontend/src/components/ModeSelector.vue
Sélecteur Public / Private Impact
frontend/src/views/PrivateImpactView.vue
Wizard 5 étapes + graphe D3
Fichiers modifiés
Fichier
Modification
backend/app/__init__.py
Enregistrement private_bp avec url_prefix='/api'
backend/app/api/__init__.py
Import + export private_bp
backend/app/services/simulation_runner.py
7 zones private (champs, start, monitor, log, check, actions, cleanup)
backend/app/services/zep_tools.py
get_entities_by_type — matching case-insensitif
backend/scripts/action_logger.py
get_private_logger() + simulated_day dans log_round_end
frontend/src/router/index.js
Route /private/:projectId
frontend/src/views/Home.vue
Intégration ModeSelector
frontend/src/views/MainView.vue
await startBuildGraph() avant redirect private
Bugs connus résiduels (non bloquants)
PrivateImpactConfigGenerator peut générer un total_simulation_days différent de celui envoyé dans la requête (comportement LLM — override délibéré du générateur)
Le rapport affiché dans l'accordéon utilise outline.sections sans titres de sections — les sections s'affichent "Section 01, 02…" si pas de titre ; le contenu complet est toujours accessible via le fallback markdown_content
Checklist
Test d'intégration end-to-end réalisé (scénario PDG + Rolls-Royce, 4 agents, 90 rounds, 31 actions)
Bug : redirection vers /private/:projectId se faisait après l'ontologie, avant startBuildGraph().
Fix : await startBuildGraph() ajouté avant router.push() dans le bloc pendingMode === 'private'.
Robustesse : PrivateImpactView polle getProject() jusqu'à ce que graph_id soit disponible.
2026-04-17 — Session 3
Prompt N°18 — Correction de 5 bugs + 1 fragilité
Bugs corrigés
#
Bug
Correctif
Fichier(s)
1
Rapport section 01 en chinois
Règle de langue centralisée dans get_language_instruction() : override forçant l'alignement sur simulation_requirement, fallback français
backend/app/utils/locale.py
2
Graphe D3 sans arêtes
Endpoint /status augmenté avec agents + relational_edges issus de cascade_influence. Frontend : nœuds statiques + merge arêtes cascade (grises pointillées si pas d'action, pleines si activées)
Corrigé côté frontend seulement (body aligné sur interviews: [{agent_id, prompt}] + parsing réponse result.results)
La route chat pour Private Impact n'existe pas dans private.py. Le frontend tape /api/simulation/interview/batch qui exige SimulationRunner.check_env_alive() — incompatible avec PrivateImpactRunner. Le 400 "require interviews" est corrigé, mais env not running reste à traiter par une nouvelle route dédiée côté backend (hors périmètre : pas de nouveaux fichiers autorisés)
Fichiers modifiés
backend/app/utils/locale.py — directive de langue universelle
backend/app/api/private.py — status endpoint renvoie agents + relational_edges
frontend/src/views/PrivateImpactView.vue — graphe cascade, export .md, chat body corrigé
PR vers main : feature/private-impact — regrouper cette session avec les sessions 1–2.
À anticiper post-PR : créer une route /api/private-impact/chat/<sim_id> dédiée pour finaliser le bug 4 (le runner privé n'a pas d'IPC de type send_batch_interview, donc un chat direct via ReportAgent.chat ou un LLM call sur le profil d'agent est sans doute plus pertinent).
form et agentCounts restent des reactive dans le parent et sont passés en props aux enfants — les mutations (v-model) se propagent naturellement grâce à la réactivité Vue 3.
Le watcher form.relationalTypes → agentCounts vit dans Step2PrivateDecision (avec deep: true pour capter les mutations d'array).
Le cycle de vie D3 (initGraph, updateGraph, simulation.stop()) vit dans Step3PrivateSim via onMounted / onUnmounted — plus besoin du watcher currentStep === 3 dans le parent.
Les styles communs (.btn-primary, .btn-secondary, .mono, .loading-ring) sont dupliqués dans chaque composant scoped pour rester autonomes ; le parent conserve uniquement les styles réellement utilisés par son template (header, steps-bar, error banner, centered-panel, prepare-results).
Le chargement initial de chatAgents reste dans le parent (loadChatAgents déclenché par le watcher currentStep === 5) — Step5PrivateInteraction reçoit la liste en prop et n'appelle jamais getPrivateActions.
Step 2 (Prepare Results, ~60 lignes) reste inline dans l'orchestrateur — trop couplé à l'état parent pour justifier un 5ème composant.
Prochaine étape
Prompt N°24 — Refactor MainView.vue avec bifurcation mode=public / mode=private après le graph build (wizard partagé qui ré-utilise les 4 sous-composants privés + leurs équivalents publics).
Fusionner les deux wizards (Public / Private) dans un unique MainView.vue qui bifurque selon route.query.mode après l'étape 1 (graph build). PrivateImpactView.vue devient un simple passthrough de redirection vers /process/:projectId?mode=private.
Fichiers modifiés
Fichier
Modification
frontend/src/components/Step1GraphBuild.vue
Ajout prop mode: { type: String, default: 'public' }. Si mode === 'private', handleEnterEnvSetup émet next-step sans créer de simulation OASIS (pas de createSimulation ni de router.push('/simulation/...')). Le comportement public reste strictement inchangé.
frontend/src/views/MainView.vue
Refactor complet : isPrivateMode computed (route.query.mode === 'private'), ajout de tout l'état Private (privateForm, privateAgentCounts, privateSimId, privateSimStatus, privatePrepareResult, privatePrepareReady, privateReportResult, privateIsLoading, privateError, privateReportProgress, privateRecentActions, privateChatAgents, timers privatePollingTimer + privateReportPollingTimer). Méthodes Private migrées depuis PrivateImpactView : runPrivatePrepare, runPrivateStart, pollPrivateStatus, handlePrivateStop, runPrivateReport, pollPrivateReport, loadPrivateChatAgents. Template bifurqué : Step 1 commun (split layout, mode prop), Steps 2–5 branchés selon le mode. onBeforeRouteLeave, onBeforeRouteUpdate, onUnmounted cleanupent tous les timers (publics + privés). watch(isPrivateMode) reset currentStep = 1 et privatePrepareReady = false. handleNewProject propage désormais query: { mode } dans router.replace({ name: 'Process', ... }) au lieu de rediriger vers /private/:projectId.
frontend/src/views/PrivateImpactView.vue
Réduit à un composant de redirection : onMounted → router.replace({ name: 'Process', params: { projectId }, query: { mode: 'private' } }).
frontend/src/components/ModeSelector.vue
Suppression du router.push({ name: 'PrivateImpact' }) (le routing est désormais déclenché par Home.vue via selectedMode.value === 'private' ? { mode: 'private' } : {}).
Décisions d'architecture — Session 5
Step 1 inchangé pour le public : ajout d'un prop mode avec default 'public'. Le public branche exécute exactement l'ancien code (createSimulation + router.push vers /simulation/:id). Seule la branche private est nouvelle (émet next-step).
Étiquettes des étapes différentes par mode :
Public : stepNames venant de tm('main.stepNames') (i18n)
Étape 2 privée (Requirement + Prepare) : privatePrepareReady est un flag local dans MainView qui permet d'afficher le formulaire (false) ou le résultat preparePrivateSimulation (true). Le bouton « Back » remet le flag à false (retour au formulaire avec données conservées).
Steps bar Private : affichée uniquement à partir de currentStep >= 2 (étape 1 = graph build commun, avec son propre UI). Le breadcrumb couvre les étapes 2→5 (['Requirement', 'Run', 'Report', 'Interact']).
Cleanup timers : un seul point de vérité cleanupAllTimers() appelé dans onBeforeRouteLeave, onBeforeRouteUpdate (si projectId change) et onUnmounted. watch(isPrivateMode) appelle uniquement cleanupPrivateTimers() (le changement de mode seul ne doit pas tuer les timers publics).
currentStep jamais persisté : reset automatique à 1 dès que isPrivateMode change. Pas de localStorage / sessionStorage.
PrivateImpactView.vue : maintenu comme simple redirecteur pour préserver la compatibilité des URLs /private et /private/:projectId (ModeSelector legacy, liens externes éventuels). À supprimer dans un prompt futur si plus utilisé.
Validation
npx vite build → succès (704 modules, 1.96s, seuls les warnings préexistants persistent : chunk > 500 kB et dynamic import de pendingUpload.js).
Flow public : Home → /process/new → Step1GraphBuild (mode=public) → createSimulation → /simulation/:id — inchangé.
Compatibilité : /private/:projectId → redirige vers /process/:projectId?mode=private.
Prochaine étape
PR feature/private-impact → main (regroupe Sessions 1 à 5).
Cleanup optionnel : supprimer complètement les routes /private et /private/:projectId si PR marchée en production (et que les liens externes sont migrés).
Finaliser l'intégration du mode selector : plus aucun état de mode hors URL, suppression définitive des routes legacy /private / /private/:projectId et du fichier passthrough PrivateImpactView.vue.
Fichiers modifiés
Fichier
Modification
frontend/src/components/ModeSelector.vue
Refactor complet. Nouveaux props : projectId: { type: String, default: 'new' } et disabled: { type: Boolean, default: false }. selectMode(mode) appelle emit('mode-selected', mode) (synchrone — permet au parent de stocker la pending upload avant la navigation) puis router.push({ path: '/process/${projectId}', query: { mode } }). Cards ont désormais :disabled natif + classe .is-disabled (opacity 0.45, cursor not-allowed).
frontend/src/views/Home.vue
Suppression du bouton start-engine-btn (ModeSelector devient la CTA). Suppression de selectedMode ref, de startSimulation(), de useRouter import, de error ref non utilisé, du mode-selector-wrapper (déplacé dans console-box). ModeSelector est maintenant dans une .console-section.mode-selector-section après la textarea avec :projectId="'new'" + `:disabled="!canSubmit
frontend/src/router/index.js
Suppression de l'import PrivateImpactView et des deux entrées de route /private (PrivateImpact) et /private/:projectId (PrivateImpactWithProject). Une URL legacy /private/... donne désormais une 404 du router (comportement Vue Router par défaut).
Fichier supprimé
frontend/src/views/PrivateImpactView.vue — le passthrough de redirection Session 5 devient obsolète une fois le ModeSelector reconnecté et les routes legacy retirées.
Décisions d'architecture — Session 6
Mode dans l'URL uniquement : aucun sessionStorage, aucun localStorage, aucune ref Home persistée. La query param ?mode=public|private est la source de vérité.
Timing de setPendingUpload : le parent (Home.vue) écoute mode-selected. L'emit Vue est synchrone et se déclenche AVANT le router.push dans le même selectMode. Le handler handleModeSelected s'exécute donc avant la navigation, garantissant que getPendingUpload() côté MainView.handleNewProject trouve bien les fichiers.
UX ModeSelector : cards désactivées tant que canSubmit === false (pas de fichiers OU pas de prompt de simulation). Opacity réduite + cursor not-allowed pour indiquer l'état.
404 sur /private* : choix délibéré pour nettoyer l'API publique du frontend. Aucun lien externe documenté dans le repo ne pointe vers ces URLs (confirmé par grep -rn "/private" restreint aux URLs frontend : 0 occurrence hors api/private.js backend et imports locaux */private/* côté fichiers). Si un lien externe casse, la route pourra être rétablie en alias explicite dans une future PR.
Bouton « Start Engine » supprimé : redondant avec ModeSelector une fois que celui-ci navigue directement. Deux CTAs pour la même action = ambiguïté UX. L'animation pulse-border disparaît avec le bouton.
Résultats des greps — AVANT (état pré-modification)
Migrer les stepNames et modeBadge Private hors des hardcodes vers les fichiers i18n. Unifier la mécanique du compteur Step X/Y via stepNames.length (déjà en place depuis N°24). Ne pas toucher au flux Public fonctionnel.
Audit i18n — état AVANT modifications
Langues supportées (fichiers présents dans locales/) : EN (en.json), ZH (zh.json). Pas de fr.json malgré la présence de fr dans locales/languages.json (langue référencée mais sans pack). Langue par défaut : zh.
Clés existantes pertinentes pour le header / wizard Public :
main.stepNames (array 5 entrées) — utilisé par MainView.vue:288, SimulationView.vue:28, SimulationRunView.vue:28, ReportView.vue:28, InteractionView.vue:28
main.layoutGraph|layoutSplit|layoutWorkbench — utilisé par les 5 vues ci-dessus
common.ready|running|completed|failed|processing|error — présents mais statusText dans MainView.vue reste hardcodé EN (hors périmètre de ce prompt — aucune divergence cross-mode)
Aucune clé Private préexistante (tout était hardcodé dans le JS MainView.vue).
Fichiers modifiés
Fichier
Modification
locales/en.json
Ajout section public (stepNames copie de main.stepNames + modeBadge: "PUBLIC OPINION") et section private (stepNames: ["Requirement", "Prepare", "Run", "Report", "Interact"] + modeBadge: "PRIVATE IMPACT"). main.stepNames conservé — utilisé par 4 autres vues (SimulationView, SimulationRunView, ReportView, InteractionView) hors scope.
Template : PRIVATE IMPACT → {{ t('private.modeBadge') }}. Script : publicStepNames lit désormais tm('public.stepNames') (aligné sur la nouvelle clé). privateStepNames passe d'array statique à computed(() => tm('private.stepNames')). privateBreadcrumb passe d'array statique à computed(() => privateStepNames.value.slice(1)) — dérivé, toujours synchronisé. currentStepNames adapté pour .value sur la computed privée. Compteur Step {{ currentStep }}/{{ currentStepNames.length }} déjà robuste (N°24) — pas de modification.
Clés i18n ajoutées
public.stepNames (EN, ZH) — mirror de main.stepNames
public.modeBadge (EN, ZH) — pour usage futur si badge Public affiché (non rendu actuellement dans le template puisque <div v-if="isPrivateMode" class="mode-badge">)
private.stepNames (EN, ZH)
private.modeBadge (EN, ZH)
Décisions — Session 7
Coexistence main.stepNames ↔ public.stepNames : les deux clés contiennent actuellement la même liste. main.stepNames reste la source de vérité pour les 4 vues sub-étape (SimulationView, SimulationRunView, ReportView, InteractionView) qui lisent $tm('main.stepNames')[N]. public.stepNames est lu uniquement par MainView pour la symétrie avec private.stepNames. Migration complète vers public.stepNames hors périmètre de ce prompt (toucherait 4 vues non mentionnées).
Écart spec vs UI : le prompt liste ['Requirement', 'Prepare', 'Run', 'Report', 'Interact'] mais Step 1 dans la UI est le composant Step1GraphBuild commun aux deux modes. La valeur « Requirement » pour Step 1 Private est donc sémantiquement le cadrage (l'utilisateur fournit les docs requirement qui alimentent la construction du graphe), pas la construction graph elle-même. Choix : suivre le prompt littéralement — le label affiché dans le header pour Step 1 en mode Private est « Requirement » (ZH : 需求). Le composant sous-jacent ne change pas.
Badge Public non affiché : public.modeBadge est ajoutée pour cohérence API i18n (symétrie avec private.modeBadge) mais le template <div v-if="isPrivateMode" class="mode-badge"> n'affiche pas de badge en mode Public. Comportement inchangé vs avant N°27. Activation future = retrait du v-if.
Pas de migration statusText : les labels Ready|Running|Completed|Failed|Error|Processing|Building Graph|Generating Ontology|Initializing restent hardcodés EN dans MainView.vue:368-385. Ils sont identiques pour les deux modes (non-divergents). Per prompt section 5 : « Si aucune divergence supplémentaire trouvée au-delà de stepNames et modeBadge, c'est OK ». Migration i18n complète du statusText = hors périmètre (un prompt futur pourra le traiter).
Validation
npx vite build → succès (701 modules, 1.13s, aucun warning nouveau).
Lecture code : currentStepNames.length === 5 dans les deux modes (header Step X/5 cohérent).
Changement de langue via LanguageSwitcher : tm('private.stepNames') et t('private.modeBadge') sont réactifs via vue-i18n → bascule EN/ZH immédiate sans perte d'état (watch interne vue-i18n).
Changement de mode (URL ?mode=public ↔ ?mode=private) : la langue active ne bouge pas (stockée dans localStorage par i18n/index.js, indépendante de la query param).
Non-régressions constatées
Aucune modification dans les 4 vues qui lisent main.stepNames[N] (SimulationView, SimulationRunView, ReportView, InteractionView) — leur header conserve son label EN/ZH existant.
Public flow : publicStepNames renvoie tm('public.stepNames') dont le contenu est identique à main.stepNames → aucun changement visuel.
Prochaine étape
Prompt N°28 — Commit feature/private-impact + push + mise à jour PR #544.
Session refactoring wizard — Terminée le 2026-04-17