CreaRack-SL

Modelo AIPrompt: prompt del AI Brain por organización

Descripción

AIPrompt es un modelo Django que almacena prompts del AI Brain (el motor de IA que analiza imágenes para Auto-Plan).

Antes de s105: un único prompt global en fichero (prompts/blueprint_analyst.md) compartido por TODOS los tenants (violación de RLS + interferencia cross-tenant).

Desde s105: prompts por organización en BD. El fichero queda como semilla read-only.


Campos

class AIPrompt(models.Model):
    organization = ForeignKey(Organization)
    name = CharField(max_length=255)  # Nombre del prompt ("__brain_active__" o nombre custom)
    content = TextField()              # Contenido del prompt (markdown/instrucciones)
    is_default = BooleanField(default=False)
    created_at = DateTimeField(auto_now_add=True)
    updated_at = DateTimeField(auto_now=True)

    class Meta:
        unique_together = [[organization-name]]

Invariantes:

  • Cada organización puede tener múltiples prompts (saved prompts custom) + un prompt activo (reservado).
  • El prompt activo tiene nombre reservado: __brain_active__.
  • Los prompts custom (“saved”) no pueden llamarse __brain_active__ (rechazado en validación).

Roles del prompt

1. Prompt activo (Brain AI del Auto-Plan)

Nombre: __brain_active__ (constante blueprints.api.prompt.ACTIVE_PROMPT_NAME)

El prompt que usa Auto-Plan cuando analiza una imagen:

# blueprints/api/autoplan.py
prompt = get_active_prompt_text(org)
ai_data = AutoPlanService.analyze_blueprint_image(api_key, file_path, prompt, provider=provider)

Endpoints:

  • GET /api/blueprints/prompt: obtiene el prompt activo (o la semilla si no existe).
  • POST /api/blueprints/prompt/save: guarda/actualiza el prompt activo de la org.
  • POST /api/blueprints/prompt/reset: borra el registro activo, vuelve a la semilla.

2. Saved prompts (custom, del usuario)

Nombres: cualquiera excepto __brain_active__.

Prompts que el usuario puede guardar, etiquetar y reutilizar en sesiones del editor.

Endpoints:

  • GET /api/blueprints/prompt/saved: lista todos los saved prompts de la org (excluye __brain_active__).
  • POST /api/blueprints/prompt/save-as: crea un nuevo saved prompt con nombre custom.
  • GET /api/blueprints/prompt/saved/{id}: obtiene un saved prompt por ID.
  • DELETE /api/blueprints/prompt/saved/{id}: borra un saved prompt.

Funciones helper

_seed_prompt_text() -> str | None

Lee la semilla read-only del fichero prompts/blueprint_analyst.md (default/fallback).

def _seed_prompt_text() -> str | None:
    """Default del AI Brain si la org no ha personalizado."""
    prompt_file = os.path.join(settings.BASE_DIR, "prompts", "blueprint_analyst.md")
    try:
        with open(prompt_file, encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        return None

get_active_prompt_text(org) -> str | None

Obtiene el prompt activo de una org:

  1. Intenta leer el registro AIPrompt(organization=org, name="__brain_active__").
  2. Si no existe, retorna la semilla read-only.
def get_active_prompt_text(org) -> str | None:
    if org is not None:
        p = AIPrompt.objects.filter(organization=org, name=ACTIVE_PROMPT_NAME).first()
        if p:
            return p.content
    return _seed_prompt_text()

Usado por:

  • get_prompt_content(): endpoint GET.
  • magic_import_blueprint(): Auto-Plan.

Cambios en s105

AntesAhora
Único fichero global prompts/blueprint_analyst.mdBD: uno por org (con fallback a semilla)
/prompt/save escribía fichero (pisa a otros tenants)Actualiza AIPrompt.__brain_active__ de la org
/prompt/reset restauraba desde _default.mdBorra el registro, vuelve a _seed_prompt_text()
No había aislamiento de tenantsAIPrompt.unique_together = (org, name)

Ejemplo de uso

from blueprints.models import AIPrompt
from core.models import Organization

# Organización A personaliza su prompt
org_a = Organization.objects.get(id=1)
AIPrompt.objects.update_or_create(
    organization=org_a,
    name="__brain_active__",
    defaults={"content": "Eres un experto en racks de servidores...", "is_default": False}
)

# Organización B sigue con la semilla (no tiene registro activo)
org_b = Organization.objects.get(id=2)
# get_active_prompt_text(org_b) retorna _seed_prompt_text()

# Ambas pueden guardar prompts custom
AIPrompt.objects.create(
    organization=org_a,
    name="Análisis detallado v1",
    content="Prompt custom 1...",
    is_default=False
)

Impacto para devs

  • ✅ Auto-Plan siempre consulta get_active_prompt_text(org) (no lee fichero).
  • ✅ Endpoints /prompt/* ahora trabajan con BD, no ficheros.
  • ✅ Validación: no permitir nombre __brain_active__ en /save-as ni en UI.
  • ✅ Filtro en listado: exclude(name=ACTIVE_PROMPT_NAME) para “saved prompts”.
  • ⚠️ Atención: el modelo ya existía (antes para “saved prompts” solo), ahora se resemantiza para incluir el prompt activo.

Véase también

  • [[feature—blueprints—auditoria-s105]]
  • [[feature—blueprints—auto-plan]]
  • [[concept—saas—multi-tenancy]]
  • [[entity—blueprints—service—autoplan]]