Agent-Node / agent_zero_framework /python /api /persona_customize.py
Oldmangrizzz's picture
Upload folder using huggingface_hub
dff1e71 verified
Raw History Blame Contribute Delete
13.7 kB
"""
Persona Customization API
Handles RSI (avatar) upload and voice manifest customization.
"""
import os
import json
from pathlib import Path
try:
from flask import Request
except Exception:
from typing import Any as Request # type: ignore
from python.helpers.api import ApiHandler
import logging
from datetime import datetime, timezone
logger = logging.getLogger(__name__)
class PersonaCustomization(ApiHandler):
"""Handler for persona customization operations."""
@classmethod
def requires_auth(cls) -> bool:
return False
@classmethod
def requires_csrf(cls) -> bool:
return False
@classmethod
def get_methods(cls) -> list[str]:
return ["POST"]
async def process(self, input: dict, request: Request | None = None) -> dict:
"""
Handle persona customization.
Accepts:
- rsi_upload: File upload for avatar image
- rsi_mode: "generate", "upload", or "skip"
- voice_preset: "generated", "minimal", "expressive", or "custom"
- custom_tokens: Custom style tokens (when voice_preset="custom")
Returns:
Dictionary with status and updated paths.
"""
persona_dir = os.environ.get("AGENT_PROMPTS_DIR", "")
persona_root = Path(persona_dir).parent if persona_dir else None
# Enforce Emergence Gate edits policy if present (identity-only protection under TALK_ONLY)
# Build list of fields being edited from explicit 'fields' key and any other indicators
fields_being_edited = []
if isinstance(input.get("fields"), dict):
fields_being_edited.extend(list(input.get("fields", {}).keys()))
# infer field edits from higher-level inputs
if "voice_preset" in input:
fields_being_edited.append("voice_preset")
if "rsi_mode" in input or (request and hasattr(request, "files") and "rsi_upload" in getattr(request, "files", {})):
fields_being_edited.append("avatar")
# Write an incoming debug record to aid diagnostics in tests
try:
if persona_root:
dbg_in = persona_root / "persona_customize_incoming.jsonl"
with open(dbg_in, "a", encoding="utf-8") as f:
f.write(json.dumps({"ts": datetime.now(timezone.utc).isoformat(), "input": input, "fields_being_edited": fields_being_edited}) + "\n")
except Exception:
logger.exception("Failed to write incoming debug record")
# Helper to append debug lines under persona root
def _append_persona_debug(name: str, data: dict):
try:
if persona_root:
dbg = persona_root / name
with open(dbg, "a", encoding="utf-8") as f:
f.write(json.dumps({"ts": datetime.now(timezone.utc).isoformat(), **data}) + "\n")
except Exception:
logger.exception("Failed to write persona debug %s", name)
try:
from python.helpers.emergence_check import edits_allowed_fields, get_gate_for_persona
allowed, message, rejected = edits_allowed_fields(persona_root, fields_being_edited or None)
override_used = False
# If edits are not allowed, check for admin override
if not allowed:
gate = get_gate_for_persona(persona_root)
override_payload_json = input.get("override_payload")
override_signature = input.get("override_signature")
if gate and override_payload_json and override_signature:
logger.debug("PersonaCustomization: attempting admin override verification")
parsed = gate.verify_admin_override(override_payload_json, override_signature)
logger.debug("PersonaCustomization: override parsed=%s", parsed)
# Write debug trace to persona root for easier test inspection
try:
if persona_root:
dbg_path = persona_root / "persona_customize_debug.jsonl"
with open(dbg_path, "a", encoding="utf-8") as dbg:
dbg.write(json.dumps({
"ts": datetime.now(timezone.utc).isoformat(),
"fields_being_edited": fields_being_edited,
"override_payload_present": bool(override_payload_json),
"override_signature_present": bool(override_signature),
"parsed": parsed,
}) + "\n")
except Exception:
logger.exception("Failed to write persona_customize debug")
if parsed and parsed.get("action") == "override_edit":
allowed_fields = parsed.get("fields", [])
logger.debug("PersonaCustomization: override allowed_fields=%s", allowed_fields)
if "*" in allowed_fields or all(f in allowed_fields for f in (fields_being_edited or [])):
try:
gate.record_override_usage(persona_root, fields_being_edited, parsed, override_signature)
except Exception:
logger.exception("Failed to record override usage")
allowed = True
rejected = []
override_used = True
if not allowed:
# Record rejected edit attempts for auditing
try:
if gate:
gate.record_edit_rejection(persona_root, rejected or [], message or "edits disabled in TALK_ONLY")
except Exception:
logger.exception("Failed to record edit rejection")
return {"error": message, "status": "forbidden", "rejected_fields": rejected}
except Exception:
# If emergence_check is unavailable, we proceed as before
pass
# Keep override usage flag available for downstream logic
_override_used = override_used if 'override_used' in locals() else False
# If edits were allowed (including via override), apply top-level 'fields' edits if present
applied_fields = []
if input.get("fields") and isinstance(input.get("fields"), dict):
try:
config_path = persona_root / "persona_config.yaml"
# Try to read existing YAML/JSON
try:
import yaml
if config_path.exists():
with open(config_path, 'r', encoding='utf-8') as cf:
config = yaml.safe_load(cf) or {}
else:
config = {}
except Exception:
# Fallback to JSON if PyYAML not available
if config_path.exists():
with open(config_path, 'r', encoding='utf-8') as cf:
config = json.load(cf)
else:
config = {}
# Apply field edits
for k, v in input.get("fields", {}).items():
config[k] = v
applied_fields.append(k)
# Write back using YAML if available
try:
import yaml
with open(config_path, 'w', encoding='utf-8') as cf:
yaml.safe_dump(config, cf)
except Exception:
with open(config_path, 'w', encoding='utf-8') as cf:
json.dump(config, cf, indent=2)
# Log applied edits
try:
gate = None
try:
from python.helpers.emergence_check import get_gate_for_persona
gate = get_gate_for_persona(persona_root)
except Exception:
gate = None
if gate:
gate._append_event_log({"type": "edit_applied", "fields": applied_fields, "persona_root": str(persona_root)})
except Exception:
logger.exception("Failed to log applied edits to emergence gate")
except Exception:
logger.exception("Failed to apply persona field edits")
# If edits were allowed/override_used, ensure subsequent manifest updates do not re-check and block
edits_globally_allowed = True if (fields_being_edited or []) and (not message or message is None) else False
if override_used:
edits_globally_allowed = True
if not persona_root or not persona_root.exists():
return {"error": "Persona directory not found"}
result = {"status": "success", "rsi_path": None, "voice_updated": False}
# Handle RSI (Avatar) customization
rsi_mode = input.get("rsi_mode", "skip")
if rsi_mode == "upload":
if "rsi_upload" in request.files:
file = request.files["rsi_upload"]
avatar_dir = persona_root / "persona_data"
avatar_dir.mkdir(parents=True, exist_ok=True)
avatar_path = avatar_dir / "avatar.png"
file.save(str(avatar_path))
result["rsi_path"] = str(avatar_path)
result["rsi_mode"] = "upload"
elif rsi_mode == "generate":
result["rsi_mode"] = "generate"
result["message"] = "RSI will be generated from soul anchor during launch"
else:
result["rsi_mode"] = "skip"
result["message"] = "Using default avatar placeholder"
# Handle Voice Manifest customization
voice_preset = input.get("voice_preset", "generated")
tts_manifest_path = persona_root / "tts_voice_manifest.json"
# Validate whether the requested fields are allowed under current gate state
# If no specific fields provided in the input, we assume whole-file update (fields=None)
fields_being_edited = None
if isinstance(input.get("fields"), dict):
fields_being_edited = list(input.get("fields", {}).keys())
# If edits are disallowed, edits_allowed_fields will return rejected fields and message
try:
from python.helpers.emergence_check import edits_allowed_fields, get_gate_for_persona
allowed, message, rejected = edits_allowed_fields(persona_root, fields_being_edited)
if not allowed:
gate = get_gate_for_persona(persona_root)
override_payload_json = input.get("override_payload")
override_signature = input.get("override_signature")
if gate and override_payload_json and override_signature:
parsed = gate.verify_admin_override(override_payload_json, override_signature)
if parsed and parsed.get("action") == "override_edit":
allowed_fields = parsed.get("fields", [])
if "*" in allowed_fields or all(f in allowed_fields for f in (fields_being_edited or [])):
try:
gate.record_override_usage(persona_root, fields_being_edited, parsed, override_signature)
except Exception:
logger.exception("Failed to record override usage")
allowed = True
rejected = []
if not allowed:
# Record rejected edit attempts for auditing
try:
if gate:
gate.record_edit_rejection(persona_root, rejected or [], message or "edits disabled in TALK_ONLY")
except Exception:
logger.exception("Failed to record edit rejection")
return {"error": message, "status": "forbidden", "rejected_fields": rejected}
except Exception:
# if emergence_check is unavailable, we proceed as before
pass
if tts_manifest_path.exists():
try:
with open(tts_manifest_path, 'r') as f:
manifest = json.load(f)
# Update voice preset
if voice_preset in ["minimal", "expressive", "custom"]:
manifest["voice_preset"] = voice_preset
# If custom tokens provided
if voice_preset == "custom" and "custom_tokens" in input:
manifest["style_tokens"] = [token.strip() for token in input["custom_tokens"].split(",")]
# Update engine if it was "generated"
manifest["engine"] = "neutts-air"
# Save updated manifest
with open(tts_manifest_path, 'w') as f:
json.dump(manifest, f, indent=2)
result["voice_updated"] = True
result["voice_preset"] = voice_preset
except Exception as e:
result["error"] = f"Failed to update voice manifest: {str(e)}"
return result