diff --git a/facilitator_v9.py b/facilitator_v9.py index 1f98cf5..1434e47 100644 --- a/facilitator_v9.py +++ b/facilitator_v9.py @@ -228,6 +228,9 @@ class Project: "nom": self.name, "cree_le": datetime.now().isoformat(timespec="seconds"), "dernier_markdown": "", + "dernier_plan_compact": "", + "plan_modifie_le": "", # horodatage dernière modif du plan + "formalise_le": "", # horodatage dernière formalisation "dernier_yaml": "", "dernier_pptx": "", "nb_sessions": 0, @@ -239,10 +242,34 @@ class Project: self.state_file.write_text( json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8") - def has_state(self) -> bool: - """Vrai si le projet a déjà produit au moins un narratif formalisé.""" + def save_plan_compact(self, plan: str): + """Sauvegarde continue du plan compact (source de vérité de la structure).""" + state = self.load_state() + state["dernier_plan_compact"] = plan + state["plan_modifie_le"] = datetime.now().isoformat(timespec="seconds") + self.save_state(state) + + def mark_formalised(self, markdown: str): + """Enregistre le Markdown formalisé + l'horodatage de formalisation.""" + state = self.load_state() + state["dernier_markdown"] = markdown + state["formalise_le"] = datetime.now().isoformat(timespec="seconds") + self.save_state(state) + + def plan_diverge(self) -> bool: + """Vrai si le plan compact a été modifié APRÈS la dernière formalisation.""" st = self.load_state() - return bool(st.get("dernier_markdown")) + pm, fm = st.get("plan_modifie_le", ""), st.get("formalise_le", "") + if not pm: + return False + if not fm: + return bool(st.get("dernier_plan_compact")) + return pm > fm + + def has_state(self) -> bool: + """Vrai si le projet a déjà un plan compact OU un narratif formalisé.""" + st = self.load_state() + return bool(st.get("dernier_plan_compact") or st.get("dernier_markdown")) # ── Journal de décisions (Solution D) ─────────────────────────────────── def log(self, entry: str): @@ -531,34 +558,60 @@ class AgentSession: def reprime_narrator(session: AgentSession, proj: Project) -> None: """ - Réamorce une session Narrator neuve avec le contexte d'un projet - déjà travaillé : dernier Markdown formalisé + journal des décisions. - (Solution C — on ne restaure pas la conversation exacte, on réinforme.) + Réamorce une session Narrator avec le contexte d'un projet déjà travaillé. + + Priorité au PLAN COMPACT persisté (source de vérité) : on le RECHARGE + à l'identique et on le réinjecte comme état de départ — l'agent ne + réinvente rien. Repli sur la reconstruction depuis le Markdown + uniquement si aucun plan compact n'a été sauvegardé (projets antérieurs + à la persistance du plan). """ state = proj.load_state() + plan = state.get("dernier_plan_compact", "") last_md = state.get("dernier_markdown", "") + summary = proj.journal_summary() + + if plan: + # RECHARGEMENT EXACT — pas de régénération + reprime = ( + "Nous reprenons un projet de présentation déjà travaillé. " + "Voici le PLAN COMPACT EXACT sur lequel nous nous étions arrêtés. " + "C'est l'état de référence : tu le conserves tel quel et tu " + "travailles dessus. Ne le reformule pas, ne le réinvente pas, " + "ne renumérotes que si je te demande une modification de flux.\n\n" + "=== PLAN COMPACT ACTUEL ===\n\n" + f"{plan}\n\n" + ) + if summary: + reprime += f"=== JOURNAL DES DÉCISIONS ===\n\n{summary}\n\n" + reprime += ( + "Confirme en réaffichant ce plan compact À L'IDENTIQUE, puis " + "attends mes instructions (modes f: flux / d: deep dive). " + "N'ajoute aucun commentaire." + ) + info("Réamorçage — plan compact rechargé à l'identique.") + out = session.start(reprime) + display_narrator(out) + return + + # Repli : pas de plan compact persisté → reconstruction depuis le Markdown if not last_md: return - summary = proj.journal_summary() reprime = ( - "Nous reprenons un projet de présentation déjà travaillé. " - "Voici l'état actuel pour te remettre en contexte.\n\n" - "=== DERNIER NARRATIF FORMALISÉ (état actuel du deck) ===\n\n" + "Nous reprenons un projet déjà travaillé, mais seul le narratif " + "formalisé est disponible (pas de plan compact sauvegardé).\n\n" + "=== DERNIER NARRATIF FORMALISÉ ===\n\n" f"{last_md}\n\n" ) if summary: - reprime += ( - "=== JOURNAL DES DÉCISIONS (historique du projet) ===\n\n" - f"{summary}\n\n" - ) + reprime += f"=== JOURNAL DES DÉCISIONS ===\n\n{summary}\n\n" reprime += ( - "Reconstruis à partir de ce narratif le PLAN COMPACT de la présentation " - "(format hiérarchique : chapitres, slides numérotées en continu, tags " - "core/OPT-XXX et audience). Affiche uniquement ce plan compact, sans " - "autre commentaire. On repartira de ce plan pour la révision, avec les " - "mêmes modes f: (flux) et d: (deep dive)." + "Reconstruis le PLAN COMPACT à partir de ce narratif (chapitres, " + "slides numérotées en continu, tags core/OPT-XXX et audience). " + "Affiche uniquement ce plan compact. On repartira de lui (modes f:/d:)." ) - info("Réamorçage du Narrator — reconstruction du plan compact...") + warn("Aucun plan compact sauvegardé — reconstruction depuis le narratif " + "(peut différer légèrement).") out = session.start(reprime) display_narrator(out) @@ -585,12 +638,20 @@ def run_narrator(session: AgentSession, proj: Project) -> Optional[str]: print("\n Documents présents dans inputs/ — /lire pour les injecter.") loaded_docs: set = set() - last_plan: str = "" # dernier plan compact (mode flux) + # Précharge le plan compact persisté (reprise) — source de vérité exacte + last_plan: str = proj.load_state().get("dernier_plan_compact", "") last_response: str = "" # dernière réponse quelconque formalized_md: Optional[str] = None pending_context: str = "" mode: Optional[str] = None # MODE_FLUX / MODE_DEEP / None + if last_plan and not session.started: + info("Plan compact rechargé depuis la session précédente :") + print("\n" + textwrap.indent(last_plan, " ")) + if proj.plan_diverge(): + warn("Le plan a été modifié après la dernière formalisation — " + "pense à /formalise avant de générer.") + def send_with_mode(msg: str, current_mode: Optional[str]) -> str: """Préfixe le message d'une consigne de mode pour l'agent.""" if current_mode == MODE_FLUX: @@ -646,6 +707,7 @@ def run_narrator(session: AgentSession, proj: Project) -> Optional[str]: last_response = session.send(send_with_mode(pasted, mode)) if mode == MODE_FLUX or mode is None: last_plan = last_response + proj.save_plan_compact(last_plan) display_narrator(last_response) continue @@ -666,6 +728,7 @@ def run_narrator(session: AgentSession, proj: Project) -> Optional[str]: "en développant le contenu de chaque slide. Inclus les " "slides tiroir en les marquant [OPT-XXX].") formalized_md = last_response + proj.mark_formalised(formalized_md) display_narrator(last_response) ok("Formalisé. /valider pour passer au Designer, ou continue à ajuster.") continue @@ -716,6 +779,7 @@ def run_narrator(session: AgentSession, proj: Project) -> Optional[str]: # Mémoriser le plan si on est en flux (ou tout premier plan) if mode == MODE_FLUX or (mode is None and not last_plan): last_plan = last_response + proj.save_plan_compact(last_plan) display_narrator(last_response)