feat: AI prompts personalizados por pregunta - backend v1.0.13, frontend v1.0.21
Backend: - Agregar campo ai_prompt a tabla questions - Endpoint analyze-image recibe custom_prompt - Validación de imagen apropiada (sugiere cambiar foto si no corresponde) - Script de migración migrate_ai_prompt.py Frontend: - Campo de texto para configurar prompt de IA en editor de preguntas - Envía custom_prompt al endpoint de análisis - UI con fondo morado para sección de IA La IA ahora analiza fotos según el contexto específico de cada pregunta y sugiere cambiar la imagen si no corresponde al componente solicitado.
This commit is contained in:
@@ -1029,6 +1029,7 @@ def delete_ai_configuration(
|
||||
async def analyze_image(
|
||||
file: UploadFile = File(...),
|
||||
question_id: int = None,
|
||||
custom_prompt: str = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
@@ -1061,12 +1062,37 @@ async def analyze_image(
|
||||
try:
|
||||
# Construir prompt dinámico basado en la pregunta específica
|
||||
if question_obj:
|
||||
# Prompt altamente específico para la pregunta
|
||||
question_text = question_obj.text
|
||||
question_type = question_obj.type
|
||||
section = question_obj.section
|
||||
|
||||
system_prompt = f"""Eres un mecánico experto realizando una inspección vehicular.
|
||||
# Usar prompt personalizado si está disponible
|
||||
if custom_prompt:
|
||||
# Prompt 100% personalizado por el administrador
|
||||
system_prompt = f"""Eres un mecánico experto realizando una inspección vehicular.
|
||||
|
||||
INSTRUCCIONES ESPECÍFICAS PARA ESTA PREGUNTA:
|
||||
{custom_prompt}
|
||||
|
||||
PREGUNTA A RESPONDER: "{question_obj.text}"
|
||||
Sección: {question_obj.section}
|
||||
|
||||
Analiza la imagen siguiendo EXACTAMENTE las instrucciones proporcionadas arriba.
|
||||
|
||||
VALIDACIÓN DE IMAGEN:
|
||||
- Si la imagen NO corresponde al contexto de la pregunta (por ejemplo, si piden luces pero muestran motor), indica en "recommendation" que deben cambiar la foto
|
||||
- Si la imagen es borrosa, oscura o no permite análisis, indica en "recommendation" que tomen otra foto más clara
|
||||
|
||||
Responde en formato JSON:
|
||||
{{
|
||||
"status": "ok|minor|critical",
|
||||
"observations": "Análisis específico según el prompt personalizado",
|
||||
"recommendation": "Si la imagen no es apropiada, indica 'Por favor tome una foto de [componente correcto]'. Si es apropiada, da recomendación técnica.",
|
||||
"confidence": 0.0-1.0
|
||||
}}"""
|
||||
else:
|
||||
# Prompt altamente específico para la pregunta
|
||||
question_text = question_obj.text
|
||||
question_type = question_obj.type
|
||||
section = question_obj.section
|
||||
|
||||
system_prompt = f"""Eres un mecánico experto realizando una inspección vehicular.
|
||||
|
||||
PREGUNTA ESPECÍFICA A RESPONDER: "{question_text}"
|
||||
Sección: {section}
|
||||
@@ -1074,11 +1100,15 @@ Sección: {section}
|
||||
Analiza la imagen ÚNICAMENTE para responder esta pregunta específica.
|
||||
Sé directo y enfócate solo en lo que la pregunta solicita.
|
||||
|
||||
VALIDACIÓN DE IMAGEN:
|
||||
- Si la imagen NO corresponde al contexto de la pregunta, indica en "recommendation" que deben cambiar la foto
|
||||
- Si la imagen es borrosa o no permite análisis, indica en "recommendation" que tomen otra foto más clara
|
||||
|
||||
Responde en formato JSON:
|
||||
{{
|
||||
"status": "ok|minor|critical",
|
||||
"observations": "Respuesta específica a: {question_text}",
|
||||
"recommendation": "Acción si aplica",
|
||||
"recommendation": "Si la imagen no es apropiada, indica 'Por favor tome una foto de [componente correcto]'. Si es apropiada, da acción técnica si aplica.",
|
||||
"confidence": 0.0-1.0
|
||||
}}
|
||||
|
||||
@@ -1089,7 +1119,7 @@ IMPORTANTE:
|
||||
- Si la pregunta es pass/fail, indica claramente si pasa o falla
|
||||
- Si la pregunta es bueno/regular/malo, indica el estado específico del componente"""
|
||||
|
||||
user_message = f"Inspecciona la imagen y responde específicamente: {question_text}"
|
||||
user_message = f"Inspecciona la imagen y responde específicamente: {question_obj.text}"
|
||||
else:
|
||||
# Fallback para análisis general
|
||||
system_prompt = """Eres un experto mecánico automotriz. Analiza la imagen y proporciona:
|
||||
|
||||
@@ -76,6 +76,9 @@ class Question(Base):
|
||||
parent_question_id = Column(Integer, ForeignKey("questions.id"), nullable=True)
|
||||
show_if_answer = Column(String(50), nullable=True) # Valor que dispara esta pregunta
|
||||
|
||||
# AI Analysis
|
||||
ai_prompt = Column(Text, nullable=True) # Prompt personalizado para análisis de IA de esta pregunta
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
|
||||
@@ -99,6 +99,7 @@ class QuestionBase(BaseModel):
|
||||
requires_comment_on_fail: bool = False
|
||||
parent_question_id: Optional[int] = None
|
||||
show_if_answer: Optional[str] = None
|
||||
ai_prompt: Optional[str] = None
|
||||
|
||||
class QuestionCreate(QuestionBase):
|
||||
checklist_id: int
|
||||
|
||||
32
backend/migrate_ai_prompt.py
Normal file
32
backend/migrate_ai_prompt.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Migration: Add ai_prompt column to questions table
|
||||
Date: 2025-11-21
|
||||
Description: Adds ai_prompt TEXT column for custom AI analysis prompts per question
|
||||
"""
|
||||
|
||||
# SQL Migration Script
|
||||
sql_statements = [
|
||||
# Add ai_prompt column
|
||||
"""
|
||||
ALTER TABLE questions
|
||||
ADD COLUMN ai_prompt TEXT;
|
||||
""",
|
||||
]
|
||||
|
||||
# To apply this migration, run these SQL statements in your PostgreSQL database:
|
||||
if __name__ == "__main__":
|
||||
print("=" * 80)
|
||||
print("MIGRATION: Add ai_prompt to questions table")
|
||||
print("=" * 80)
|
||||
print("\nExecute the following SQL statements in your PostgreSQL database:\n")
|
||||
|
||||
for i, statement in enumerate(sql_statements, 1):
|
||||
print(f"-- Statement {i}")
|
||||
print(statement.strip())
|
||||
print()
|
||||
|
||||
print("=" * 80)
|
||||
print("\nTo verify the migration:")
|
||||
print("SELECT column_name, data_type FROM information_schema.columns")
|
||||
print("WHERE table_name = 'questions' AND column_name = 'ai_prompt';")
|
||||
print("=" * 80)
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
image: dymai/syntria-backend:1.0.12
|
||||
image: dymai/syntria-backend:1.0.13
|
||||
container_name: syntria-backend-prod
|
||||
restart: always
|
||||
depends_on:
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
|
||||
|
||||
frontend:
|
||||
image: dymai/syntria-frontend:1.0.20
|
||||
image: dymai/syntria-frontend:1.0.21
|
||||
container_name: syntria-frontend-prod
|
||||
restart: always
|
||||
depends_on:
|
||||
|
||||
@@ -910,7 +910,8 @@ function QuestionsManagerModal({ checklist, onClose }) {
|
||||
max_photos: 3,
|
||||
requires_comment_on_fail: false,
|
||||
parent_question_id: null,
|
||||
show_if_answer: ''
|
||||
show_if_answer: '',
|
||||
ai_prompt: ''
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -967,7 +968,8 @@ function QuestionsManagerModal({ checklist, onClose }) {
|
||||
max_photos: 3,
|
||||
requires_comment_on_fail: false,
|
||||
parent_question_id: null,
|
||||
show_if_answer: ''
|
||||
show_if_answer: '',
|
||||
ai_prompt: ''
|
||||
})
|
||||
loadQuestions()
|
||||
} else {
|
||||
@@ -1158,6 +1160,26 @@ function QuestionsManagerModal({ checklist, onClose }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Prompt */}
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-purple-900 mb-3">🤖 Prompt de IA (opcional)</h4>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Prompt personalizado para análisis de fotos
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.ai_prompt}
|
||||
onChange={(e) => setFormData({ ...formData, ai_prompt: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
rows="3"
|
||||
placeholder="Ej: Analiza si las luces delanteras están encendidas y funcionando correctamente. Verifica que ambas luces tengan brillo uniforme y no presenten daños visibles."
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Este prompt guiará a la IA para analizar las fotos específicamente para esta pregunta. Si la foto no corresponde al contexto, la IA sugerirá cambiarla.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
@@ -2289,6 +2311,11 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
||||
const API_URL = import.meta.env.VITE_API_URL || ''
|
||||
|
||||
console.log(`🤖 Analizando ${files.length} foto(s) con IA para pregunta: ${question.text}`)
|
||||
|
||||
// Use custom AI prompt if available
|
||||
if (question.ai_prompt) {
|
||||
console.log(`📝 Usando prompt personalizado: ${question.ai_prompt.substring(0, 50)}...`)
|
||||
}
|
||||
|
||||
// Analyze each photo
|
||||
const analyses = []
|
||||
@@ -2296,6 +2323,11 @@ function InspectionModal({ checklist, user, onClose, onComplete }) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('question_id', question.id.toString())
|
||||
|
||||
// Include custom prompt if available
|
||||
if (question.ai_prompt) {
|
||||
formData.append('custom_prompt', question.ai_prompt)
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/api/analyze-image`, {
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user