feat: version 3.2.0 -- Integracion IA: configuracion y plan de actividades
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { pool } from '../../config/db.js';
|
||||
import { HttpError } from '../../utils/http.js';
|
||||
import { askStructured } from '../../utils/ai.js';
|
||||
import { logAudit } from '../../utils/audit.js';
|
||||
import { getAiConfig } from '../settings/service.js';
|
||||
import { getProject, canWriteProject } from '../projects/service.js';
|
||||
import { createActivity } from '../activities/service.js';
|
||||
import { createTask } from '../tasks/service.js';
|
||||
|
||||
const MAX_ACTIVITIES = 15;
|
||||
const MAX_TASKS = 10;
|
||||
|
||||
const PLAN_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
activities: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
durationDays: { type: 'integer', minimum: 1 },
|
||||
tasks: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['name', 'durationDays', 'tasks'],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['activities'],
|
||||
};
|
||||
|
||||
export async function getStatus() {
|
||||
const { configured } = await getAiConfig();
|
||||
return { enabled: configured };
|
||||
}
|
||||
|
||||
async function getWritableProject(projectId, user) {
|
||||
const project = await getProject(projectId, user);
|
||||
if (!(await canWriteProject(user, project))) throw new HttpError(403, 'No tienes permisos sobre este proyecto');
|
||||
return project;
|
||||
}
|
||||
|
||||
// Normaliza lo que devuelve la IA: recorta, limita cantidades y descarta entradas vacias.
|
||||
function cleanPlan(activities) {
|
||||
return (Array.isArray(activities) ? activities : []).slice(0, MAX_ACTIVITIES).map((a) => ({
|
||||
name: String(a?.name || '').trim().slice(0, 200),
|
||||
description: String(a?.description || '').trim().slice(0, 1000),
|
||||
durationDays: Math.min(365, Math.max(1, Number.parseInt(a?.durationDays, 10) || 1)),
|
||||
tasks: (Array.isArray(a?.tasks) ? a.tasks : []).map((t) => String(t || '').trim().slice(0, 200)).filter(Boolean).slice(0, MAX_TASKS),
|
||||
})).filter((a) => a.name);
|
||||
}
|
||||
|
||||
export async function generateProjectPlan(projectId, description, lang, user) {
|
||||
const project = await getWritableProject(projectId, user);
|
||||
const extra = String(description || '').trim().slice(0, 2000);
|
||||
const [existing] = await pool.query('SELECT name FROM activities WHERE project_id = ? AND deleted_at IS NULL', [projectId]);
|
||||
const language = String(lang || '').startsWith('en') ? 'English' : 'español neutro';
|
||||
const result = await askStructured({
|
||||
system: `Eres un jefe de proyectos de TI. Propones planes de trabajo realistas y concretos para un area de TI corporativa. Responde en ${language}.`,
|
||||
prompt: [
|
||||
`Proyecto: ${project.name}`,
|
||||
project.description ? `Descripcion: ${project.description}` : '',
|
||||
project.objective ? `Objetivo: ${project.objective}` : '',
|
||||
extra ? `Indicaciones del usuario: ${extra}` : '',
|
||||
existing.length ? `Actividades que YA existen (no las repitas): ${existing.map((r) => r.name).join('; ')}` : '',
|
||||
`Propon entre 3 y ${MAX_ACTIVITIES} actividades en orden de ejecucion, cada una con una duracion estimada en dias habiles y de 2 a ${MAX_TASKS} tareas concretas.`,
|
||||
].filter(Boolean).join('\n'),
|
||||
toolName: 'plan_de_proyecto',
|
||||
schema: PLAN_SCHEMA,
|
||||
maxTokens: 4000,
|
||||
});
|
||||
await logAudit({ userId: user.id, action: 'ai_plan', entityType: 'project', entityId: projectId });
|
||||
return { activities: cleanPlan(result.activities) };
|
||||
}
|
||||
|
||||
function addDays(date, days) {
|
||||
const d = new Date(date);
|
||||
d.setDate(d.getDate() + days);
|
||||
return d;
|
||||
}
|
||||
const isoDate = (d) => d.toISOString().slice(0, 10);
|
||||
|
||||
// Crea las actividades/tareas que el usuario acepto, encadenadas desde hoy, con la prioridad
|
||||
// media y el primer estado abierto del catalogo. Reusa los servicios normales (permisos,
|
||||
// auditoria, notificaciones y recalculo de avance incluidos).
|
||||
export async function applyProjectPlan(projectId, activities, user) {
|
||||
await getWritableProject(projectId, user);
|
||||
const plan = cleanPlan(activities);
|
||||
if (!plan.length) throw new HttpError(400, 'No hay actividades para crear');
|
||||
const [priorities] = await pool.query('SELECT id FROM priorities ORDER BY sort_order, id');
|
||||
const [statuses] = await pool.query('SELECT id FROM activity_statuses WHERE is_closed = 0 ORDER BY sort_order, id LIMIT 1');
|
||||
if (!priorities.length || !statuses.length) throw new HttpError(400, 'Faltan prioridades o estados de actividad en Configuracion');
|
||||
const priorityId = priorities[Math.floor((priorities.length - 1) / 2)].id;
|
||||
let start = new Date();
|
||||
let created = 0;
|
||||
for (const a of plan) {
|
||||
const due = addDays(start, a.durationDays - 1);
|
||||
const activity = await createActivity(projectId, {
|
||||
name: a.name, description: a.description, priorityId, statusId: statuses[0].id,
|
||||
startDate: isoDate(start), dueDate: isoDate(due),
|
||||
}, user);
|
||||
for (const name of a.tasks) await createTask(activity.id, { name }, user);
|
||||
start = addDays(due, 1);
|
||||
created += 1;
|
||||
}
|
||||
return { created };
|
||||
}
|
||||
Reference in New Issue
Block a user