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:
- Intenta leer el registro
AIPrompt(organization=org, name="__brain_active__"). - 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
| Antes | Ahora |
|---|---|
Único fichero global prompts/blueprint_analyst.md | BD: 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.md | Borra el registro, vuelve a _seed_prompt_text() |
| No había aislamiento de tenants | AIPrompt.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-asni 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]]
Referenciado desde
- Auditoría Suprema s105: Blueprints / Auto-Plan (22 fixes)
- Auto-Plan LLM Resilience — log provider, retry JSON, regex glitch s49
- Auto-Plan s46: criterio NODAL funcional + flags visión llama-server (calidad 70 → 95%)
- Auto-Plan: Driver OpenRouter refactorizado con providers nuevos y params Gemma 4 (s48)
- Auto-Plan: proveedor google_genai (Gemma 4 directo via Google AI Studio)
- Auto-Plan: Resiliencia OpenRouter — retry JSONDecodeError + logging diagnóstico + regex clave faltante
- AutoPlan OpenRouter — Fixes C1+C2+C3 Audit s49 (varianza Vertex)
- AutoPlanService: Digitalizaci\u00f3n de planos con IA
- Decisiones de la Auditoría Suprema s105 (Blueprints/Auto-Plan)
- Endpoint POST /api/blueprints/autoplan/import
- Feature: AutoPlan — Provider google_genai (Gemma 4 directo, Google AI Studio Paid)
- Hito F: Auto-Plan refactor + investigación de calidad (Plan Hardening post-Máster)
- Incident s49 — Varianza extrema Auto-Plan en Vertex (C1+C2+C3)
- Incidente: AutoPlan falla con JSONDecodeError ante salida LLM malformada
- Incidente: Bucles repetitivos de Gemma 4 E4B-it en Auto-Plan (sampling greedy)