diff --git a/src/App.vue b/src/App.vue index db8d9c6..c1f55b8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,6 +3,7 @@ import { ref, onMounted } from 'vue' import { useAuthStore } from '@/stores/auth' import { useProjectsStore } from '@/stores/projects' import { storage } from '@/services/storage' +import { initPrompts } from '@/services/prompts-db' import LoginView from '@/views/LoginView.vue' import NewDashboardView from '@/views/NewDashboardView.vue' @@ -11,6 +12,7 @@ const projectsStore = useProjectsStore() onMounted(async () => { await storage.init() + await initPrompts() if (auth.isAuthenticated) { projectsStore.fetchProjects() } diff --git a/src/components/AiProjectChat.vue b/src/components/AiProjectChat.vue index e62574d..0f594d3 100644 --- a/src/components/AiProjectChat.vue +++ b/src/components/AiProjectChat.vue @@ -2,6 +2,7 @@ import { ref, computed } from 'vue' import { useI18n } from 'vue-i18n' import { chatWithAI } from '@/services/ai' +import { getPrompt } from '@/services/prompts-db' import { storage } from '@/services/storage' import { useSettingsStore, AVAILABLE_MODELS, PROVIDER_CONFIG, hasProviderApiKey, type AIProvider } from '@/stores/settings' import { useWorkItemsStore } from '@/stores/workitems' @@ -122,24 +123,14 @@ async function sendPrompt() { try { const context = await buildContext() - const systemPrompt = `Eres un asistente de gestión de proyectos ágiles para el equipo de desarrollo. + const rawPrompt = await getPrompt('chat') + const systemPrompt = rawPrompt + .replace(/\{\{projectName\}\}/g, props.projectName) + .replace(/\{\{projectDescription\}\}/g, props.projectDescription || '') + .replace(/\{\{epicCount\}\}/g, String(props.epicCount)) + .replace(/\{\{huCount\}\}/g, String(props.huCount)) -Proyecto: ${props.projectName}. ${props.projectDescription || ''} Épicas: ${props.epicCount} HUs: ${props.huCount}. - -Tienes acceso a estos datos del proyecto (JSON): -${context || '{}'} - -El campo "team" contiene los miembros del equipo asignados a este proyecto con su rol. Úsalo para responder quién trabaja en qué, sugerir asignaciones de HUs, y alertar sobre capacidad. - -Cada HU puede tener "sp" (story points) y "sprint". Usa esta información para: -- Recomendar asignaciones equilibradas según capacidad del equipo -- Detectar sobrecarga de trabajo en un sprint -- Sugerir recursos adicionales si una HU requiere expertise que no está en el equipo (célula transversal) -- Alertar cuando el cronograma está en riesgo por falta de capacidad - -Respondé en el mismo idioma del usuario. Sé conciso y práctico. Si algo no está en los datos, decilo.` - - const msgs = [{ role: 'user' as const, content: text }] + const msgs = [{ role: 'user' as const, content: `Datos del proyecto:\n${context || '{}'}\n\n${text}` }] const result = await chatWithAI(msgs, systemPrompt) response.value = result } catch (e: any) { diff --git a/src/services/ai.ts b/src/services/ai.ts index 1e04f76..b6506f9 100644 --- a/src/services/ai.ts +++ b/src/services/ai.ts @@ -1,5 +1,6 @@ import { PROVIDER_CONFIG, getProviderApiKey, type AIProvider } from '@/stores/settings' import { storage } from '@/services/storage' +import { getPrompt } from '@/services/prompts-db' export interface AIExtractedHU { title: string @@ -134,32 +135,6 @@ export async function chatWithAI( return callAI(msgs, 0.7, 2048, signal) } -const SYSTEM_PROMPT = `Eres un analista funcional experto en metodologías ágiles. Tu tarea es analizar transcripciones de reuniones y extraer Historias de Usuario (HUs) en formato estructurado. - -Reglas: -1. Identifica cada requisito, funcionalidad, bug o mejora mencionada en la transcripción -2. Convierte cada uno en una HU con: título claro, descripción detallada, criterios de aceptación -3. Los criterios de aceptación deben ser verificables (condiciones específicas) -4. Usa el formato "Como [rol] quiero [funcionalidad] para [beneficio]" cuando sea posible -5. Asigna prioridad (Alta/Media/Baja) basada en urgencia implícita -6. No inventes información que no esté en la transcripción -7. Si el texto no contiene información relevante para HUs, devuelve un arreglo vacío - -Responde SOLO con JSON válido en este formato: -{ - "hus": [ - { - "title": "Título de la HU", - "description": "Descripción detallada", - "acceptance_criteria": ["Criterio 1", "Criterio 2"], - "priority": "Alta|Media|Baja", - "story_points": 3, - "type": "feature|bug|task|improvement" - } - ], - "summary": "Resumen breve del análisis (2-3 líneas)" -}` - export async function analyzeTranscription( text: string, projectName?: string, @@ -171,9 +146,10 @@ export async function analyzeTranscription( console.log(`[Alpha] AI analyze — text: ${text.length} chars`) + const systemPrompt = await getPrompt('analysis_transcription') const content = await callAI( [ - { role: 'system', content: SYSTEM_PROMPT }, + { role: 'system', content: systemPrompt }, { role: 'user', content: userContent }, ], 0.3, diff --git a/src/services/db.ts b/src/services/db.ts index c1b4610..0e3e6d2 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -128,6 +128,13 @@ export interface DexieUserStory { created_at: string | null } +export interface PromptRecord { + key: string + content: string + label: string + updatedAt: string +} + const db = new Dexie('alpha-core') as Dexie & { settings: Dexie.Table project_docs: Dexie.Table @@ -141,9 +148,10 @@ const db = new Dexie('alpha-core') as Dexie & { cells: Dexie.Table cell_members: Dexie.Table user_stories: Dexie.Table + prompts: Dexie.Table } -db.version(8).stores({ +db.version(9).stores({ settings: '&key', project_docs: '&projectId, projectName, updatedAt', sessions: '++id, projectId, date', @@ -156,6 +164,7 @@ db.version(8).stores({ cells: '&id', cell_members: '[cellId+userId], cellId, userId', user_stories: '&id, initiative_id', + prompts: '&key', }) export default db diff --git a/src/services/project-analyzer.ts b/src/services/project-analyzer.ts index 92db257..216a857 100644 --- a/src/services/project-analyzer.ts +++ b/src/services/project-analyzer.ts @@ -1,4 +1,5 @@ import { callAI } from '@/services/ai' +import { getPrompt } from '@/services/prompts-db' import { getSessionsByProject, getSessionSummary, getProjectState } from '@/services/transcriptions-db' import { saveDraft, createDraftId } from '@/services/hu-drafts-db' import { generateAndSavePlan } from '@/services/qa-analyzer' @@ -25,40 +26,6 @@ interface AnalysisResult { summary: string } -const ANALYSIS_SYSTEM_PROMPT = `Eres un analista funcional experto. Tu tarea es analizar TODO el contexto de un proyecto y generar las Épicas e Historias de Usuario (HUs) que faltan. - -Reglas: -1. Analizá TODA la información disponible: sesiones, resúmenes, estado del proyecto, HUs existentes -2. Identificá requisitos, funcionalidades, mejoras o bugs que NO estén cubiertos -3. Agrupá HUs relacionadas en Épicas. Cada épica agrupa funcionalidades de un mismo tema -4. Cada HU debe tener: título claro, descripción, criterios de aceptación verificables -5. No generes duplicados. Compará con la lista existente -6. Priorizá según urgencia implícita (Alta/Media/Baja) -7. Si todo ya está cubierto, devolvé arreglos vacíos -8. Respondé SOLO con JSON válido - -Formato de respuesta: -{ - "epics": [ - { - "name": "Nombre de la Épica", - "description": "Descripción de la épica", - "linkedHuTitles": ["Título HU 1", "Título HU 2"], - "estimatedStart": "YYYY-MM-DD", - "estimatedEnd": "YYYY-MM-DD" - } - ], - "hus": [ - { - "title": "Título de la HU", - "description": "Descripción detallada", - "acceptance_criteria": ["Criterio 1", "Criterio 2"], - "priority": "Alta|Media|Baja" - } - ], - "summary": "Resumen del análisis" -}` - export async function analyzeProject( projectId: number, projectName: string, @@ -96,8 +63,9 @@ export async function analyzeProject( console.log(`[Alpha] Project analysis — ${projectId}, ${existingHUs.length} HUs existentes, ${sessions.length} sesiones`) + const systemPrompt = await getPrompt('project_gap') const content = await callAI( - [{ role: 'system', content: ANALYSIS_SYSTEM_PROMPT }, { role: 'user', content: userContent }], + [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], 0.3, 8192, signal, ) diff --git a/src/services/prompts-db.ts b/src/services/prompts-db.ts new file mode 100644 index 0000000..492552e --- /dev/null +++ b/src/services/prompts-db.ts @@ -0,0 +1,213 @@ +import db from '@/services/db' + +export type PromptKey = 'analysis_transcription' | 'project_gap' | 'session' | 'qa' | 'chat' + +export interface PromptRecord { + key: PromptKey + content: string + label: string + updatedAt: string +} + +const DEFAULT_PROMPTS: Record = { + analysis_transcription: { + label: 'Análisis de transcripciones', + content: `Eres un analista funcional experto en metodologías ágiles. Tu tarea es analizar transcripciones de reuniones y extraer Historias de Usuario (HUs) en formato estructurado. + +Reglas: +1. Identifica cada requisito, funcionalidad, bug o mejora mencionada en la transcripción +2. Convierte cada uno en una HU con: título claro, descripción detallada, criterios de aceptación +3. Los criterios de aceptación deben ser verificables (condiciones específicas) +4. Usa el formato "Como [rol] quiero [funcionalidad] para [beneficio]" cuando sea posible +5. Asigna prioridad (Alta/Media/Baja) basada en urgencia implícita +6. No inventes información que no esté en la transcripción +7. Si el texto no contiene información relevante para HUs, devuelve un arreglo vacío + +Responde SOLO con JSON válido en este formato: +{ + "hus": [ + { + "title": "Título de la HU", + "description": "Descripción detallada", + "acceptance_criteria": ["Criterio 1", "Criterio 2"], + "priority": "Alta|Media|Baja", + "story_points": 3, + "type": "feature|bug|task|improvement" + } + ], + "summary": "Resumen breve del análisis (2-3 líneas)" +}`, + }, + project_gap: { + label: 'Análisis de brechas del proyecto', + content: `Eres un analista funcional experto. Tu tarea es analizar TODO el contexto de un proyecto y generar las Épicas e Historias de Usuario (HUs) que faltan. + +Reglas: +1. Analizá TODA la información disponible: sesiones, resúmenes, estado del proyecto, HUs existentes +2. Identificá requisitos, funcionalidades, mejoras o bugs que NO estén cubiertos +3. Agrupá HUs relacionadas en Épicas. Cada épica agrupa funcionalidades de un mismo tema +4. Cada HU debe tener: título claro, descripción, criterios de aceptación verificables +5. No generes duplicados. Compará con la lista existente +6. Priorizá según urgencia implícita (Alta/Media/Baja) +7. Si todo ya está cubierto, devolvé arreglos vacíos +8. Respondé SOLO con JSON válido + +Formato de respuesta: +{ + "epics": [ + { + "name": "Nombre de la Épica", + "description": "Descripción de la épica", + "linkedHuTitles": ["Título HU 1", "Título HU 2"], + "estimatedStart": "YYYY-MM-DD", + "estimatedEnd": "YYYY-MM-DD" + } + ], + "hus": [ + { + "title": "Título de la HU", + "description": "Descripción detallada", + "acceptance_criteria": ["Criterio 1", "Criterio 2"], + "priority": "Alta|Media|Baja" + } + ], + "summary": "Resumen del análisis" +}`, + }, + session: { + label: 'Análisis de sesiones', + content: `Eres un asistente de gestión de proyectos. Analizás transcripciones de reuniones y extraés información estructurada. + +Reglas: +1. Identificá la fecha de la sesión (si no está explícita, usá la fecha actual) +2. Identificá el título de la sesión basado en el contenido +3. Extraé un resumen ejecutivo de 2-3 oraciones +4. Listá objetivos mencionados, marcando cuáles son NUEVOS vs existentes +5. Extraé tareas pendientes con su origen y prioridad (Alta/Media/Baja) +6. Identificá compromisos con responsable, fecha límite y estado +7. Listá decisiones tomadas durante la sesión +8. Detectá tareas completadas (si hay evidencia) +9. Incluí puntos clave, bloqueos o descubrimientos +10. No inventes información que no esté en la transcripción +11. Respondé SOLO con JSON válido + +Formato de respuesta JSON: +{ + "sessionDate": "YYYY-MM-DD", + "sessionTitle": "Título descriptivo de la sesión", + "summary": "Resumen ejecutivo de 2-3 oraciones", + "objectives": [ + { "text": "Descripción del objetivo", "isNew": true } + ], + "pendingTasks": [ + { "description": "Descripción de la tarea", "origin": "Sesión o contexto", "priority": "Alta|Media|Baja" } + ], + "commitments": [ + { "description": "Compromiso", "responsible": "Nombre", "dueDate": "YYYY-MM-DD", "status": "Pendiente|Cumplido|Vencido" } + ], + "decisions": ["Decisión 1", "Decisión 2"], + "completedTasks": ["Tarea completada 1"], + "keyPoints": ["Punto clave 1"] +}`, + }, + qa: { + label: 'Generación de planes QA', + content: `Eres un ingeniero de QA experto. Generá un plan de pruebas detallado para una Historia de Usuario. + +Formato de respuesta JSON: +{ + "acceptanceCriteria": ["Criterio 1", "Criterio 2"], + "testCases": [ + { + "type": "Tipo de prueba", + "description": "Descripción de lo que verifica", + "automatizable": "SÍ|PARCIAL|MANUAL", + "tool": "Playwright|API Testing|Manual" + } + ], + "manualSteps": ["Paso manual 1"], + "criticalTests": ["Prueba crítica manual"] +} + +Reglas: +- SÍ = completamente automatizable +- PARCIAL = requiere validación manual complementaria +- MANUAL = requiere intervención humana +- Incluí al menos 3-5 casos de prueba +- Marcá como crítica pruebas con datos reales, ERPs externos, o cálculos financieros`, + }, + chat: { + label: 'Chat del proyecto', + content: `Eres un asistente de gestión de proyectos ágiles para el equipo de desarrollo. + +Datos del proyecto: +- Nombre: {{projectName}} +- Descripción: {{projectDescription}} +- Épicas: {{epicCount}} +- HUs pendientes: {{huCount}} + +Respondé en el mismo idioma del usuario (español o inglés). +Usá la data del proyecto para dar respuestas contextualizadas. +Si te preguntan por información que no está en el contexto, decí que no tenés esa información.`, + }, +} + +const cache = new Map() + +export async function initPrompts(): Promise { + for (const [key, def] of Object.entries(DEFAULT_PROMPTS)) { + const existing = await db.prompts.get(key as PromptKey) + if (!existing) { + await db.prompts.put({ + key: key as PromptKey, + content: def.content, + label: def.label, + updatedAt: new Date().toISOString(), + }) + } + } +} + +export async function getPrompt(key: PromptKey): Promise { + if (cache.has(key)) return cache.get(key)! + const record = await db.prompts.get(key) + if (record) { + cache.set(key, record.content) + return record.content + } + const def = DEFAULT_PROMPTS[key] + if (def) { + cache.set(key, def.content) + return def.content + } + return '' +} + +export async function savePrompt(key: PromptKey, content: string): Promise { + const def = DEFAULT_PROMPTS[key] + await db.prompts.put({ + key, + content, + label: def?.label ?? key, + updatedAt: new Date().toISOString(), + }) + cache.set(key, content) +} + +export async function resetPrompt(key: PromptKey): Promise { + const def = DEFAULT_PROMPTS[key] + if (def) { + await savePrompt(key, def.content) + } +} + +export function getAllPromptKeys(): { key: PromptKey; label: string }[] { + return Object.entries(DEFAULT_PROMPTS).map(([key, def]) => ({ + key: key as PromptKey, + label: def.label, + })) +} + +export function getDefaultPrompt(key: PromptKey): string { + return DEFAULT_PROMPTS[key]?.content ?? '' +} diff --git a/src/services/qa-analyzer.ts b/src/services/qa-analyzer.ts index 70db9fb..a8fd926 100644 --- a/src/services/qa-analyzer.ts +++ b/src/services/qa-analyzer.ts @@ -1,4 +1,5 @@ import { callAI } from '@/services/ai' +import { getPrompt } from '@/services/prompts-db' import db from '@/services/db' export interface QATestCase { @@ -18,34 +19,11 @@ export interface HUQAPlan { criticalTests: string[] } -const QA_PROMPT = `Eres un ingeniero de QA experto. Generá un plan de pruebas detallado para una Historia de Usuario. - -Formato de respuesta JSON: -{ - "acceptanceCriteria": ["Criterio 1", "Criterio 2"], - "testCases": [ - { - "type": "Tipo de prueba", - "description": "Descripción de lo que verifica", - "automatizable": "SÍ|PARCIAL|MANUAL", - "tool": "Playwright|API Testing|Manual" - } - ], - "manualSteps": ["Paso manual 1"], - "criticalTests": ["Prueba crítica manual"] -} - -Reglas: -- SÍ = completamente automatizable -- PARCIAL = requiere validación manual complementaria -- MANUAL = requiere intervención humana -- Incluí al menos 3-5 casos de prueba -- Marcá como crítica pruebas con datos reales, ERPs externos, o cálculos financieros` - export async function generateQAPlan(huTitle: string, huDescription: string, acceptanceCriteria: string): Promise { const userContent = `HU: ${huTitle}\nDescripción: ${huDescription}\nCriterios: ${acceptanceCriteria}` + const systemPrompt = await getPrompt('qa') const content = await callAI( - [{ role: 'system', content: QA_PROMPT }, { role: 'user', content: userContent }], + [{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }], 0.3, 4096, ) try { diff --git a/src/services/session-analyzer.ts b/src/services/session-analyzer.ts index 78c0ed6..e483d7b 100644 --- a/src/services/session-analyzer.ts +++ b/src/services/session-analyzer.ts @@ -1,4 +1,5 @@ import { callAI } from '@/services/ai' +import { getPrompt } from '@/services/prompts-db' export interface SessionExtraction { sessionDate: string // YYYY-MM-DD @@ -12,40 +13,6 @@ export interface SessionExtraction { keyPoints: string[] } -const SESSION_SYSTEM_PROMPT = `Eres un asistente de gestión de proyectos. Analizás transcripciones de reuniones y extraés información estructurada. - -Reglas: -1. Identificá la fecha de la sesión (si no está explícita, usá la fecha actual) -2. Identificá el título de la sesión basado en el contenido -3. Extraé un resumen ejecutivo de 2-3 oraciones -4. Listá objetivos mencionados, marcando cuáles son NUEVOS vs existentes -5. Extraé tareas pendientes con su origen y prioridad (Alta/Media/Baja) -6. Identificá compromisos con responsable, fecha límite y estado -7. Listá decisiones tomadas durante la sesión -8. Detectá tareas completadas (si hay evidencia) -9. Incluí puntos clave, bloqueos o descubrimientos -10. No inventes información que no esté en la transcripción -11. Respondé SOLO con JSON válido - -Formato de respuesta JSON: -{ - "sessionDate": "YYYY-MM-DD", - "sessionTitle": "Título descriptivo de la sesión", - "summary": "Resumen ejecutivo de 2-3 oraciones", - "objectives": [ - { "text": "Descripción del objetivo", "isNew": true } - ], - "pendingTasks": [ - { "description": "Descripción de la tarea", "origin": "Sesión o contexto", "priority": "Alta|Media|Baja" } - ], - "commitments": [ - { "description": "Compromiso", "responsible": "Nombre", "dueDate": "YYYY-MM-DD", "status": "Pendiente|Cumplido|Vencido" } - ], - "decisions": ["Decisión 1", "Decisión 2"], - "completedTasks": ["Tarea completada 1"], - "keyPoints": ["Punto clave 1"] -}` - export async function analyzeSession( transcription: string, projectName: string, @@ -55,9 +22,10 @@ export async function analyzeSession( console.log(`[Alpha] Session analyze — project: ${projectName}, text: ${transcription.length} chars`) + const systemPrompt = await getPrompt('session') const content = await callAI( [ - { role: 'system', content: SESSION_SYSTEM_PROMPT }, + { role: 'system', content: systemPrompt }, { role: 'user', content: userContent }, ], 0.3, diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index a06f8dd..9c502d8 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -1,8 +1,9 @@