feat: version 4.2.1 -- IA para proyectos de cualquier area y propuestas mas compactas

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-23 18:30:38 -05:00
parent 9d50bc8564
commit 664d111faf
6 changed files with 36 additions and 9 deletions
+9
View File
@@ -1,5 +1,14 @@
# Changelog - project.linkedcorp.net # Changelog - project.linkedcorp.net
## [4.2.1] - 2026-10-23
### Changed
- La IA ya no está orientada solo a TI: planifica e informa proyectos de cualquier área
(talento humano, comercial, finanzas, operaciones...) con su propio vocabulario y sin tareas
de sistemas innecesarias. Recibe como contexto el cargo y departamento del usuario y el área
solicitante del proyecto (sin preguntarle nada).
- Propuestas más compactas: entre 3 y 8 actividades con 2 a 5 tareas (antes hasta 15 y 10). En la
prueba bajó la salida de ~4.000 a ~2.000 tokens (casi la mitad del costo por propuesta).
## [4.2.0] - 2026-10-23 ## [4.2.0] - 2026-10-23
### Added ### Added
- "Informe con IA" en el detalle de cada proyecto (con la IA configurada y permiso de lectura): - "Informe con IA" en el detalle de cada proyecto (con la IA configurada y permiso de lectura):
+1 -1
View File
@@ -1 +1 @@
4.2.0 4.2.1
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "project-linkedcorp-backend", "name": "project-linkedcorp-backend",
"version": "4.2.0", "version": "4.2.1",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/server.js", "main": "src/server.js",
+23 -5
View File
@@ -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)}`,
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "project-linkedcorp-frontend", "name": "project-linkedcorp-frontend",
"private": true, "private": true,
"version": "4.2.0", "version": "4.2.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -40,7 +40,7 @@ export function NewProjectAiDialog({ open, deptOptions, minLength = 20, onClose,
async function generate() { async function generate() {
setBusy(true); setError(''); setBusy(true); setError('');
try { try {
const p = await proposeNewProject({ objective, startDate, lang }); const p = await proposeNewProject({ objective, startDate, requestingDepartmentId: departmentId, lang });
setProposal(p); setProposal(p);
setName(p.name); setName(p.name);
setSelected(p.activities.map((_, i) => i)); setSelected(p.activities.map((_, i) => i));