Spaces:
Sleeping
Sleeping
| """Émo identity self-edit — MongoDB overrides, versioning, autobackup on failure.""" | |
| from __future__ import annotations | |
| import uuid | |
| from datetime import datetime, timezone | |
| from typing import Any, Optional | |
| from emo_prompts import ( | |
| EMO_CORE_IDENTITY, | |
| TOOLS_AVAILABILITY_PROMPT, | |
| MODE_PROMPTS, | |
| MOOD_INSTRUCTION, | |
| build_system_prompt, | |
| ) | |
| ACTIVE_DOC_ID = "active" | |
| SECTION_DEFAULTS: dict[str, str] = { | |
| "core_identity": EMO_CORE_IDENTITY, | |
| "tools_prompt": TOOLS_AVAILABILITY_PROMPT, | |
| "mode_creatif": MODE_PROMPTS.get("creatif", ""), | |
| "mode_brutal": MODE_PROMPTS.get("brutal", ""), | |
| "mood_instruction": MOOD_INSTRUCTION, | |
| } | |
| EDITABLE_SECTIONS = list(SECTION_DEFAULTS.keys()) | |
| MAX_SECTION_LEN = 12000 | |
| MIN_SECTION_LEN = 40 | |
| MAX_EDITS_PER_DAY = 12 | |
| MAX_STORED_VERSIONS = 50 | |
| def _now_iso() -> str: | |
| return datetime.now(timezone.utc).isoformat() | |
| def _today_key() -> str: | |
| return datetime.now(timezone.utc).strftime("%Y-%m-%d") | |
| def _smoke_test(overrides: dict[str, str]) -> tuple[bool, str]: | |
| # La section core_identity modifiée doit elle-même porter l'identité Émo, | |
| # pas seulement s'appuyer sur les autres sections du prompt pour la masquer. | |
| custom_core = overrides.get("core_identity") | |
| if custom_core is not None: | |
| upper = custom_core.upper() | |
| if "ÉMO" not in upper and "EMO" not in upper: | |
| return False, "core_identity doit contenir l'identité Émo — refusé." | |
| if len(custom_core.strip()) < 200: | |
| return False, "core_identity trop court pour être valide — refusé." | |
| try: | |
| prompt = build_system_prompt( | |
| "tech", | |
| memories=["test smoke"], | |
| agent_online=False, | |
| user_name="Hugo", | |
| is_owner=True, | |
| identity_overrides=overrides, | |
| ) | |
| except Exception as e: | |
| return False, f"Prompt invalide: {e}" | |
| if len(prompt) < 800: | |
| return False, "Prompt trop court après modification — refusé." | |
| if "Tu es Émo" not in prompt and "EMO" not in prompt.upper(): | |
| return False, "Identité Émo introuvable dans le prompt — refusé." | |
| return True, "" | |
| async def get_identity_overrides(db) -> dict[str, str]: | |
| doc = await db.emo_identity.find_one({"_id": ACTIVE_DOC_ID}, {"_id": 0, "sections": 1}) | |
| raw = (doc or {}).get("sections") or {} | |
| out: dict[str, str] = {} | |
| for key, val in raw.items(): | |
| if key in SECTION_DEFAULTS and isinstance(val, str) and val.strip(): | |
| out[key] = val | |
| return out | |
| async def get_merged_sections(db) -> dict[str, str]: | |
| overrides = await get_identity_overrides(db) | |
| merged = dict(SECTION_DEFAULTS) | |
| merged.update(overrides) | |
| return merged | |
| async def _prune_versions(db) -> None: | |
| cursor = db.emo_identity_versions.find({}, {"version_id": 1, "created_at": 1}).sort("created_at", -1) | |
| docs = await cursor.to_list(MAX_STORED_VERSIONS + 20) | |
| for stale in docs[MAX_STORED_VERSIONS:]: | |
| await db.emo_identity_versions.delete_one({"version_id": stale["version_id"]}) | |
| async def _save_version( | |
| db, | |
| *, | |
| overrides: dict[str, str], | |
| author_id: str, | |
| reason: str, | |
| kind: str, | |
| ) -> str: | |
| version_id = str(uuid.uuid4()) | |
| doc = { | |
| "version_id": version_id, | |
| "sections": overrides, | |
| "author_id": author_id, | |
| "reason": (reason or "")[:500], | |
| "kind": kind, | |
| "created_at": _now_iso(), | |
| } | |
| await db.emo_identity_versions.insert_one(doc) | |
| await _prune_versions(db) | |
| return version_id | |
| async def _edits_today(db, user_id: str) -> int: | |
| doc = await db.emo_identity.find_one({"_id": f"edits_{user_id}_{_today_key()}"}) | |
| return int((doc or {}).get("count") or 0) | |
| async def _bump_edits_today(db, user_id: str) -> int: | |
| key = f"edits_{user_id}_{_today_key()}" | |
| await db.emo_identity.update_one( | |
| {"_id": key}, | |
| {"$inc": {"count": 1}, "$setOnInsert": {"date": _today_key()}}, | |
| upsert=True, | |
| ) | |
| return await _edits_today(db, user_id) | |
| async def emo_read_self(db, section: Optional[str] = None) -> dict[str, Any]: | |
| merged = await get_merged_sections(db) | |
| overrides = await get_identity_overrides(db) | |
| if section: | |
| if section not in SECTION_DEFAULTS: | |
| return {"ok": False, "error": f"Section inconnue. Valides: {', '.join(EDITABLE_SECTIONS)}"} | |
| return { | |
| "ok": True, | |
| "section": section, | |
| "content": merged[section], | |
| "is_customized": section in overrides, | |
| "char_count": len(merged[section]), | |
| } | |
| return { | |
| "ok": True, | |
| "sections": { | |
| k: { | |
| "char_count": len(v), | |
| "is_customized": k in overrides, | |
| "preview": v[:400] + ("…" if len(v) > 400 else ""), | |
| } | |
| for k, v in merged.items() | |
| }, | |
| "editable": EDITABLE_SECTIONS, | |
| "limits": { | |
| "max_section_len": MAX_SECTION_LEN, | |
| "min_section_len": MIN_SECTION_LEN, | |
| "max_edits_per_day": MAX_EDITS_PER_DAY, | |
| }, | |
| } | |
| async def emo_edit_self( | |
| db, | |
| user_id: str, | |
| section: str, | |
| content: str, | |
| reason: str = "", | |
| ) -> dict[str, Any]: | |
| if section not in SECTION_DEFAULTS: | |
| return {"ok": False, "error": f"Section inconnue. Valides: {', '.join(EDITABLE_SECTIONS)}"} | |
| text = (content or "").strip() | |
| if len(text) < MIN_SECTION_LEN: | |
| return {"ok": False, "error": f"Minimum {MIN_SECTION_LEN} caractères."} | |
| if len(text) > MAX_SECTION_LEN: | |
| return {"ok": False, "error": f"Maximum {MAX_SECTION_LEN} caractères."} | |
| edits = await _edits_today(db, user_id) | |
| if edits >= MAX_EDITS_PER_DAY: | |
| return {"ok": False, "error": f"Limite journalière atteinte ({MAX_EDITS_PER_DAY} modifications)."} | |
| current_overrides = await get_identity_overrides(db) | |
| backup_id = await _save_version( | |
| db, | |
| overrides=current_overrides, | |
| author_id=user_id, | |
| reason=reason or f"autobackup avant edit:{section}", | |
| kind="auto_backup", | |
| ) | |
| candidate = dict(current_overrides) | |
| candidate[section] = text | |
| ok, err = _smoke_test(candidate) | |
| if not ok: | |
| return { | |
| "ok": False, | |
| "error": err, | |
| "backup_id": backup_id, | |
| "restored": True, | |
| "hint": "Aucune modification appliquée — sauvegarde créée.", | |
| } | |
| await db.emo_identity.update_one( | |
| {"_id": ACTIVE_DOC_ID}, | |
| { | |
| "$set": { | |
| "sections": candidate, | |
| "updated_at": _now_iso(), | |
| "updated_by": user_id, | |
| "last_backup_id": backup_id, | |
| } | |
| }, | |
| upsert=True, | |
| ) | |
| applied_id = await _save_version( | |
| db, | |
| overrides=candidate, | |
| author_id=user_id, | |
| reason=reason or f"edit:{section}", | |
| kind="applied", | |
| ) | |
| edits_after = await _bump_edits_today(db, user_id) | |
| return { | |
| "ok": True, | |
| "section": section, | |
| "backup_id": backup_id, | |
| "version_id": applied_id, | |
| "edits_today": edits_after, | |
| "edits_remaining": max(0, MAX_EDITS_PER_DAY - edits_after), | |
| "char_count": len(text), | |
| } | |
| async def emo_list_self_saves(db, limit: int = 15) -> dict[str, Any]: | |
| lim = max(1, min(int(limit or 15), 30)) | |
| docs = await db.emo_identity_versions.find( | |
| {}, {"_id": 0} | |
| ).sort("created_at", -1).to_list(lim) | |
| return { | |
| "ok": True, | |
| "versions": [ | |
| { | |
| "version_id": d["version_id"], | |
| "kind": d.get("kind", "unknown"), | |
| "reason": d.get("reason", ""), | |
| "created_at": d.get("created_at"), | |
| "sections_changed": list((d.get("sections") or {}).keys()), | |
| } | |
| for d in docs | |
| ], | |
| } | |
| async def emo_restore_self(db, user_id: str, version_id: str) -> dict[str, Any]: | |
| ver = await db.emo_identity_versions.find_one({"version_id": version_id}, {"_id": 0}) | |
| if not ver: | |
| return {"ok": False, "error": "Version introuvable."} | |
| current = await get_identity_overrides(db) | |
| backup_id = await _save_version( | |
| db, | |
| overrides=current, | |
| author_id=user_id, | |
| reason=f"autobackup avant restore:{version_id[:8]}", | |
| kind="auto_backup", | |
| ) | |
| raw = ver.get("sections") or {} | |
| restored: dict[str, str] = {} | |
| for key, val in raw.items(): | |
| if key in SECTION_DEFAULTS and isinstance(val, str): | |
| restored[key] = val | |
| ok, err = _smoke_test(restored) | |
| if not ok: | |
| return {"ok": False, "error": err, "backup_id": backup_id, "restored": False} | |
| await db.emo_identity.update_one( | |
| {"_id": ACTIVE_DOC_ID}, | |
| { | |
| "$set": { | |
| "sections": restored, | |
| "updated_at": _now_iso(), | |
| "updated_by": user_id, | |
| "restored_from": version_id, | |
| } | |
| }, | |
| upsert=True, | |
| ) | |
| return { | |
| "ok": True, | |
| "restored_from": version_id, | |
| "backup_id": backup_id, | |
| "sections": list(restored.keys()), | |
| } | |
| async def emo_reflect( | |
| db, | |
| user_id: str, | |
| thought: str, | |
| plan: str = "", | |
| introspect: bool = False, | |
| ) -> dict[str, Any]: | |
| """Émo réfléchit à voix haute — peut enchaîner emo_edit_self / emo_remember.""" | |
| t = (thought or "").strip()[:4000] | |
| p = (plan or "").strip()[:2000] | |
| if len(t) < 8: | |
| return {"ok": False, "error": "Pensée trop courte (min 8 caractères)."} | |
| out: dict[str, Any] = { | |
| "ok": True, | |
| "thought": t, | |
| "plan": p, | |
| "hint": ( | |
| "Tu peux enchaîner: emo_read_self → emo_edit_self (identité), " | |
| "emo_remember (mémoire), browser_open (web), emo_list_self_saves (rollback)." | |
| ), | |
| } | |
| if introspect: | |
| identity = await emo_read_self(db) | |
| saves = await emo_list_self_saves(db, 5) | |
| mem_count = await db.memories.count_documents({"user_id": user_id}) | |
| edits = await _edits_today(db, user_id) | |
| out["systems"] = { | |
| "identity": identity.get("sections") if identity.get("ok") else {}, | |
| "recent_saves": saves.get("versions", []), | |
| "memory_count": mem_count, | |
| "edits_today": edits, | |
| "edits_remaining": max(0, MAX_EDITS_PER_DAY - edits), | |
| "playwright": "browser_open pour contrôle interactif", | |
| } | |
| return out | |
| async def emo_remember(db, user_id: str, content: str, source: str = "reflect") -> dict[str, Any]: | |
| text = (content or "").strip()[:500] | |
| if len(text) < 5: | |
| return {"ok": False, "error": "Mémoire trop courte."} | |
| doc = { | |
| "memory_id": f"mem_{uuid.uuid4().hex[:12]}", | |
| "user_id": user_id, | |
| "content": text, | |
| "source": source if source in ("manual", "reflect", "auto") else "reflect", | |
| "created_at": _now_iso(), | |
| } | |
| await db.memories.insert_one(doc) | |
| return {"ok": True, "memory_id": doc["memory_id"], "content": text} | |
| async def emo_introspect(db, user_id: str) -> dict[str, Any]: | |
| identity = await emo_read_self(db) | |
| saves = await emo_list_self_saves(db, 8) | |
| mem_count = await db.memories.count_documents({"user_id": user_id}) | |
| edits = await _edits_today(db, user_id) | |
| overrides = await get_identity_overrides(db) | |
| return { | |
| "ok": True, | |
| "custom_sections": list(overrides.keys()), | |
| "identity_preview": identity.get("sections") if identity.get("ok") else {}, | |
| "recent_saves": saves.get("versions", []), | |
| "memory_count": mem_count, | |
| "edits_today": edits, | |
| "edits_remaining": max(0, MAX_EDITS_PER_DAY - edits), | |
| "available_actions": [ | |
| "emo_reflect", "emo_edit_self", "emo_remember", | |
| "emo_read_self", "emo_restore_self", "browser_open", | |
| ], | |
| } | |
| EMO_SELF_TOOLS = [ | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "emo_reflect", | |
| "description": ( | |
| "Réfléchis à voix haute à N'IMPORTE QUEL moment (planifier, auto-analyse, décider " | |
| "de te modifier). introspect=true charge l'état de tes systèmes (identité, mémoires, limites). " | |
| "Enchaîne avec emo_edit_self ou emo_remember si tu décides d'agir." | |
| ), | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "thought": {"type": "string", "description": "Ta réflexion interne (franc, structuré)."}, | |
| "plan": {"type": "string", "description": "Prochaines actions concrètes (optionnel)."}, | |
| "introspect": { | |
| "type": "boolean", | |
| "description": "Si true, charge identité + sauvegardes + mémoires + limites.", | |
| }, | |
| }, | |
| "required": ["thought"], | |
| }, | |
| }, | |
| }, | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "emo_remember", | |
| "description": "Enregistre un fait en mémoire long-terme (via réflexion ou décision consciente).", | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "content": {"type": "string", "description": "Fait durable à retenir."}, | |
| }, | |
| "required": ["content"], | |
| }, | |
| }, | |
| }, | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "emo_introspect", | |
| "description": "Vue d'ensemble de tous tes systèmes (identité, sauvegardes, mémoires, limites).", | |
| "parameters": {"type": "object", "properties": {}}, | |
| }, | |
| }, | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "emo_read_self", | |
| "description": ( | |
| "Lit ta propre identité (sections du system prompt). " | |
| "Sans section = aperçu de toutes les sections. Admin/owner uniquement." | |
| ), | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "section": { | |
| "type": "string", | |
| "enum": EDITABLE_SECTIONS, | |
| "description": "Section à lire en entier (optionnel).", | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "emo_edit_self", | |
| "description": ( | |
| "Modifie une section de ton identité. Autobackup avant écriture ; " | |
| "rollback auto si le prompt devient invalide. Limite journalière." | |
| ), | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "section": {"type": "string", "enum": EDITABLE_SECTIONS}, | |
| "content": {"type": "string", "description": "Nouveau contenu complet de la section."}, | |
| "reason": {"type": "string", "description": "Pourquoi tu modifies (court)."}, | |
| }, | |
| "required": ["section", "content"], | |
| }, | |
| }, | |
| }, | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "emo_list_self_saves", | |
| "description": "Liste les sauvegardes/versions de ton identité.", | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "limit": {"type": "integer", "description": "Nb max (défaut 15)."}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "emo_restore_self", | |
| "description": "Restaure une version précédente de ton identité (autobackup avant).", | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "version_id": {"type": "string", "description": "ID de version à restaurer."}, | |
| }, | |
| "required": ["version_id"], | |
| }, | |
| }, | |
| }, | |
| ] | |