|
|
@@ -10,6 +10,8 @@ import { createTask } from '../tasks/service.js';
|
|
|
|
|
|
|
|
|
|
|
|
const MAX_ACTIVITIES = 15;
|
|
|
|
const MAX_ACTIVITIES = 15;
|
|
|
|
const MAX_TASKS = 10;
|
|
|
|
const MAX_TASKS = 10;
|
|
|
|
|
|
|
|
// Lo que se le pide a la IA (menos actividades = menos tokens de salida); MAX_ACTIVITIES es el tope duro.
|
|
|
|
|
|
|
|
const SUGGESTED_ACTIVITIES = 8;
|
|
|
|
export const MIN_OBJECTIVE_LENGTH = 20;
|
|
|
|
export const MIN_OBJECTIVE_LENGTH = 20;
|
|
|
|
const MAX_OBJECTIVE_LENGTH = 2000;
|
|
|
|
const MAX_OBJECTIVE_LENGTH = 2000;
|
|
|
|
|
|
|
|
|
|
|
@@ -35,7 +37,19 @@ const ACTIVITIES_SCHEMA = {
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const PLANNER_SYSTEM = (language) => `Eres un jefe de proyectos de TI de una empresa. Solo planificas proyectos: propones actividades realistas en orden de ejecucion, cada una con duracion en dias habiles y de 2 a ${MAX_TASKS} tareas concretas y accionables (las tareas son lo mas importante: ninguna actividad sin tareas). Responde en ${language}.`;
|
|
|
|
const PLANNER_SYSTEM = (language) => `Eres un jefe de proyectos con experiencia en todas las areas de una empresa (tecnologia, operaciones, finanzas, talento humano, comercial, marketing, infraestructura, etc.). Adapta el plan, el vocabulario y las tareas al area y al tipo de proyecto: no agregues tareas de sistemas si el proyecto no las necesita. Solo planificas proyectos: propones actividades realistas en orden de ejecucion, cada una con duracion en dias habiles y de 2 a 5 tareas concretas y accionables (las tareas son lo mas importante: ninguna actividad sin tareas). Responde en ${language}.`;
|
|
|
|
|
|
|
|
// Contexto del solicitante (cargo y departamento del usuario, y area que pide el proyecto) para
|
|
|
|
|
|
|
|
// orientar el plan sin preguntarle nada. Unas pocas palabras por consulta.
|
|
|
|
|
|
|
|
async function requesterContext(user, requestingDepartmentName) {
|
|
|
|
|
|
|
|
const [[u]] = await pool.query(
|
|
|
|
|
|
|
|
'SELECT u.position, d.name AS department FROM users u LEFT JOIN departments d ON d.id = u.department_id WHERE u.id = ?', [user.id]);
|
|
|
|
|
|
|
|
const who = [u?.position, u?.department && `departamento ${u.department}`].filter(Boolean).join(', ');
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
|
|
who ? `Quien lo solicita: ${clip(who, 150)}.` : '',
|
|
|
|
|
|
|
|
requestingDepartmentName ? `Area que solicita el proyecto: ${clip(requestingDepartmentName, 100)}.` : '',
|
|
|
|
|
|
|
|
].filter(Boolean).join(' ');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const languageFor = (lang) => (String(lang || '').startsWith('en') ? 'English' : 'español neutro');
|
|
|
|
const languageFor = (lang) => (String(lang || '').startsWith('en') ? 'English' : 'español neutro');
|
|
|
|
|
|
|
|
|
|
|
|
export async function getStatus() {
|
|
|
|
export async function getStatus() {
|
|
|
@@ -111,12 +125,13 @@ export async function generateProjectPlan(projectId, description, lang, user) {
|
|
|
|
const result = await askStructured({
|
|
|
|
const result = await askStructured({
|
|
|
|
system: PLANNER_SYSTEM(languageFor(lang)),
|
|
|
|
system: PLANNER_SYSTEM(languageFor(lang)),
|
|
|
|
prompt: [
|
|
|
|
prompt: [
|
|
|
|
|
|
|
|
await requesterContext(user, project.requesting_department_name),
|
|
|
|
`Proyecto: ${project.name}`,
|
|
|
|
`Proyecto: ${project.name}`,
|
|
|
|
project.description ? `Descripcion: ${project.description}` : '',
|
|
|
|
project.description ? `Descripcion: ${project.description}` : '',
|
|
|
|
project.objective ? `Objetivo: ${project.objective}` : '',
|
|
|
|
project.objective ? `Objetivo: ${project.objective}` : '',
|
|
|
|
extra ? `Indicaciones del usuario: ${extra}` : '',
|
|
|
|
extra ? `Indicaciones del usuario: ${extra}` : '',
|
|
|
|
existing.length ? `Actividades que YA existen (no las repitas): ${existing.map((r) => r.name).join('; ')}` : '',
|
|
|
|
existing.length ? `Actividades que YA existen (no las repitas): ${existing.map((r) => r.name).join('; ')}` : '',
|
|
|
|
`Propon entre 3 y ${MAX_ACTIVITIES} actividades nuevas.`,
|
|
|
|
`Propon entre 3 y ${SUGGESTED_ACTIVITIES} actividades nuevas (solo las necesarias, agrupando lo afin).`,
|
|
|
|
].filter(Boolean).join('\n'),
|
|
|
|
].filter(Boolean).join('\n'),
|
|
|
|
toolName: 'plan_de_proyecto',
|
|
|
|
toolName: 'plan_de_proyecto',
|
|
|
|
usage: { userId: user.id, action: 'project_plan' },
|
|
|
|
usage: { userId: user.id, action: 'project_plan' },
|
|
|
@@ -161,7 +176,7 @@ function parseObjective(value) {
|
|
|
|
return objective;
|
|
|
|
return objective;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function proposeNewProject({ objective, startDate, lang }, user) {
|
|
|
|
export async function proposeNewProject({ objective, startDate, requestingDepartmentId, lang }, user) {
|
|
|
|
requireCreator(user);
|
|
|
|
requireCreator(user);
|
|
|
|
const goal = parseObjective(objective);
|
|
|
|
const goal = parseObjective(objective);
|
|
|
|
const start = parseStartDate(startDate);
|
|
|
|
const start = parseStartDate(startDate);
|
|
|
@@ -173,11 +188,13 @@ export async function proposeNewProject({ objective, startDate, lang }, user) {
|
|
|
|
const result = await askStructured({
|
|
|
|
const result = await askStructured({
|
|
|
|
system: `${PLANNER_SYSTEM(languageFor(lang))} Si el texto del usuario NO describe un proyecto o iniciativa de trabajo (saludos, preguntas generales, chistes, temas ajenos), devuelve isProject=false y no inventes nada.`,
|
|
|
|
system: `${PLANNER_SYSTEM(languageFor(lang))} Si el texto del usuario NO describe un proyecto o iniciativa de trabajo (saludos, preguntas generales, chistes, temas ajenos), devuelve isProject=false y no inventes nada.`,
|
|
|
|
prompt: [
|
|
|
|
prompt: [
|
|
|
|
|
|
|
|
await requesterContext(user, requestingDepartmentId
|
|
|
|
|
|
|
|
? (await pool.query('SELECT name FROM departments WHERE id = ?', [Number(requestingDepartmentId)]))[0][0]?.name : null),
|
|
|
|
`Objetivo del proyecto descrito por el usuario: """${goal}"""`,
|
|
|
|
`Objetivo del proyecto descrito por el usuario: """${goal}"""`,
|
|
|
|
`Fecha de inicio: ${isoDate(start)}`,
|
|
|
|
`Fecha de inicio: ${isoDate(start)}`,
|
|
|
|
typeNames.length ? `Tipos de proyecto disponibles (elige uno exacto): ${typeNames.join('; ')}` : '',
|
|
|
|
typeNames.length ? `Tipos de proyecto disponibles (elige uno exacto): ${typeNames.join('; ')}` : '',
|
|
|
|
candidates.length ? `Trabajo que YA existe (clave | tipo | nombre | proyecto):\n${candidates.map((c) => `${c.key} | ${c.kind} | ${clip(c.itemName, 150)} | ${c.code} ${clip(c.projectName, 100)}${c.kind === 'proyecto' && c.objective ? ` - ${clip(c.objective, 150)}` : ''}`).join('\n')}\nEn duplicateOf pon las claves de lo que YA cubra el MISMO objetivo que pide el usuario (aunque use otras palabras o sinonimos): un proyecto igual, o una actividad/tarea que ya hace eso. No incluyas lo que solo comparte tema.` : '',
|
|
|
|
candidates.length ? `Trabajo que YA existe (clave | tipo | nombre | proyecto):\n${candidates.map((c) => `${c.key} | ${c.kind} | ${clip(c.itemName, 150)} | ${c.code} ${clip(c.projectName, 100)}${c.kind === 'proyecto' && c.objective ? ` - ${clip(c.objective, 150)}` : ''}`).join('\n')}\nEn duplicateOf pon las claves de lo que YA cubra el MISMO objetivo que pide el usuario (aunque use otras palabras o sinonimos): un proyecto igual, o una actividad/tarea que ya hace eso. No incluyas lo que solo comparte tema.` : '',
|
|
|
|
`Si es un proyecto: propone un nombre corto (max 80 caracteres); un objetivo (una sola oracion que empiece con verbo en infinitivo y diga QUE se quiere lograr y para que, max 250 caracteres); una descripcion (alcance y contexto: que incluye y como se hara, 2-3 oraciones); y entre 3 y ${MAX_ACTIVITIES} actividades.`,
|
|
|
|
`Si es un proyecto: propone un nombre corto (max 80 caracteres); un objetivo (una sola oracion que empiece con verbo en infinitivo y diga QUE se quiere lograr y para que, max 250 caracteres); una descripcion (alcance y contexto: que incluye y como se hara, 2-3 oraciones); y entre 3 y ${SUGGESTED_ACTIVITIES} actividades (solo las necesarias, agrupando lo afin).`,
|
|
|
|
].filter(Boolean).join('\n'),
|
|
|
|
].filter(Boolean).join('\n'),
|
|
|
|
toolName: 'propuesta_de_proyecto',
|
|
|
|
toolName: 'propuesta_de_proyecto',
|
|
|
|
usage: { userId: user.id, action: 'new_project' },
|
|
|
|
usage: { userId: user.id, action: 'new_project' },
|
|
|
@@ -332,8 +349,9 @@ export async function generateProjectReport(projectId, lang, user) {
|
|
|
|
a.overdue_tasks ? ` | tareas vencidas: ${clip(a.overdue_tasks, 300)}` : '',
|
|
|
|
a.overdue_tasks ? ` | tareas vencidas: ${clip(a.overdue_tasks, 300)}` : '',
|
|
|
|
].join(''));
|
|
|
|
].join(''));
|
|
|
|
const result = await askStructured({
|
|
|
|
const result = await askStructured({
|
|
|
|
system: `Eres un jefe de proyectos de TI que redacta informes de estado claros y honestos para la jefatura. Usa solo los datos dados, no inventes hechos ni cifras. Se concreto: menciona actividades y fechas. Responde en ${languageFor(lang)}.`,
|
|
|
|
system: `Eres un jefe de proyectos que redacta informes de estado claros y honestos para la jefatura, con el vocabulario del area del proyecto. Usa solo los datos dados, no inventes hechos ni cifras. Se concreto: menciona actividades y fechas. Responde en ${languageFor(lang)}.`,
|
|
|
|
prompt: [
|
|
|
|
prompt: [
|
|
|
|
|
|
|
|
await requesterContext(user, project.requesting_department_name),
|
|
|
|
`Fecha de hoy: ${today}`,
|
|
|
|
`Fecha de hoy: ${today}`,
|
|
|
|
`Proyecto: ${project.code} ${project.name} | estado: ${project.status_name || '-'} | avance: ${project.progress_pct ?? 0}%`,
|
|
|
|
`Proyecto: ${project.code} ${project.name} | estado: ${project.status_name || '-'} | avance: ${project.progress_pct ?? 0}%`,
|
|
|
|
`Fechas: inicio ${fmt(project.start_date)}, fin estimado ${fmt(project.estimated_end_date)}`,
|
|
|
|
`Fechas: inicio ${fmt(project.start_date)}, fin estimado ${fmt(project.estimated_end_date)}`,
|
|
|
|