Sistema de prompts dinámicos: prompts editables desde Settings + tabla Dexie + fallback a defaults
- prompts-db.ts: CRUD de prompts con 5 tipos (transcription, project_gap, session, qa, chat)
- db.ts: schema v9 con tabla prompts (&key)
- App.vue: initPrompts() al arrancar la app
- Servicios AI modificados: usan getPrompt(key) con fallback a default hardcodeado
- AiProjectChat.vue: prompt con {{variables}} reemplazadas por datos reales
- SettingsView: editor de prompts con textarea + guardar/restaurar
This commit is contained in:
@@ -3,6 +3,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useProjectsStore } from '@/stores/projects'
|
import { useProjectsStore } from '@/stores/projects'
|
||||||
import { storage } from '@/services/storage'
|
import { storage } from '@/services/storage'
|
||||||
|
import { initPrompts } from '@/services/prompts-db'
|
||||||
import LoginView from '@/views/LoginView.vue'
|
import LoginView from '@/views/LoginView.vue'
|
||||||
import NewDashboardView from '@/views/NewDashboardView.vue'
|
import NewDashboardView from '@/views/NewDashboardView.vue'
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ const projectsStore = useProjectsStore()
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await storage.init()
|
await storage.init()
|
||||||
|
await initPrompts()
|
||||||
if (auth.isAuthenticated) {
|
if (auth.isAuthenticated) {
|
||||||
projectsStore.fetchProjects()
|
projectsStore.fetchProjects()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { chatWithAI } from '@/services/ai'
|
import { chatWithAI } from '@/services/ai'
|
||||||
|
import { getPrompt } from '@/services/prompts-db'
|
||||||
import { storage } from '@/services/storage'
|
import { storage } from '@/services/storage'
|
||||||
import { useSettingsStore, AVAILABLE_MODELS, PROVIDER_CONFIG, hasProviderApiKey, type AIProvider } from '@/stores/settings'
|
import { useSettingsStore, AVAILABLE_MODELS, PROVIDER_CONFIG, hasProviderApiKey, type AIProvider } from '@/stores/settings'
|
||||||
import { useWorkItemsStore } from '@/stores/workitems'
|
import { useWorkItemsStore } from '@/stores/workitems'
|
||||||
@@ -122,24 +123,14 @@ async function sendPrompt() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const context = await buildContext()
|
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}.
|
const msgs = [{ role: 'user' as const, content: `Datos del proyecto:\n${context || '{}'}\n\n${text}` }]
|
||||||
|
|
||||||
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 result = await chatWithAI(msgs, systemPrompt)
|
const result = await chatWithAI(msgs, systemPrompt)
|
||||||
response.value = result
|
response.value = result
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
+3
-27
@@ -1,5 +1,6 @@
|
|||||||
import { PROVIDER_CONFIG, getProviderApiKey, type AIProvider } from '@/stores/settings'
|
import { PROVIDER_CONFIG, getProviderApiKey, type AIProvider } from '@/stores/settings'
|
||||||
import { storage } from '@/services/storage'
|
import { storage } from '@/services/storage'
|
||||||
|
import { getPrompt } from '@/services/prompts-db'
|
||||||
|
|
||||||
export interface AIExtractedHU {
|
export interface AIExtractedHU {
|
||||||
title: string
|
title: string
|
||||||
@@ -134,32 +135,6 @@ export async function chatWithAI(
|
|||||||
return callAI(msgs, 0.7, 2048, signal)
|
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(
|
export async function analyzeTranscription(
|
||||||
text: string,
|
text: string,
|
||||||
projectName?: string,
|
projectName?: string,
|
||||||
@@ -171,9 +146,10 @@ export async function analyzeTranscription(
|
|||||||
|
|
||||||
console.log(`[Alpha] AI analyze — text: ${text.length} chars`)
|
console.log(`[Alpha] AI analyze — text: ${text.length} chars`)
|
||||||
|
|
||||||
|
const systemPrompt = await getPrompt('analysis_transcription')
|
||||||
const content = await callAI(
|
const content = await callAI(
|
||||||
[
|
[
|
||||||
{ role: 'system', content: SYSTEM_PROMPT },
|
{ role: 'system', content: systemPrompt },
|
||||||
{ role: 'user', content: userContent },
|
{ role: 'user', content: userContent },
|
||||||
],
|
],
|
||||||
0.3,
|
0.3,
|
||||||
|
|||||||
+10
-1
@@ -128,6 +128,13 @@ export interface DexieUserStory {
|
|||||||
created_at: string | null
|
created_at: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PromptRecord {
|
||||||
|
key: string
|
||||||
|
content: string
|
||||||
|
label: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
const db = new Dexie('alpha-core') as Dexie & {
|
const db = new Dexie('alpha-core') as Dexie & {
|
||||||
settings: Dexie.Table<SettingEntry, string>
|
settings: Dexie.Table<SettingEntry, string>
|
||||||
project_docs: Dexie.Table<ProjectDocRecord, number>
|
project_docs: Dexie.Table<ProjectDocRecord, number>
|
||||||
@@ -141,9 +148,10 @@ const db = new Dexie('alpha-core') as Dexie & {
|
|||||||
cells: Dexie.Table<CellRecord, string>
|
cells: Dexie.Table<CellRecord, string>
|
||||||
cell_members: Dexie.Table<CellMemberRecord, string>
|
cell_members: Dexie.Table<CellMemberRecord, string>
|
||||||
user_stories: Dexie.Table<DexieUserStory, number>
|
user_stories: Dexie.Table<DexieUserStory, number>
|
||||||
|
prompts: Dexie.Table<PromptRecord, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
db.version(8).stores({
|
db.version(9).stores({
|
||||||
settings: '&key',
|
settings: '&key',
|
||||||
project_docs: '&projectId, projectName, updatedAt',
|
project_docs: '&projectId, projectName, updatedAt',
|
||||||
sessions: '++id, projectId, date',
|
sessions: '++id, projectId, date',
|
||||||
@@ -156,6 +164,7 @@ db.version(8).stores({
|
|||||||
cells: '&id',
|
cells: '&id',
|
||||||
cell_members: '[cellId+userId], cellId, userId',
|
cell_members: '[cellId+userId], cellId, userId',
|
||||||
user_stories: '&id, initiative_id',
|
user_stories: '&id, initiative_id',
|
||||||
|
prompts: '&key',
|
||||||
})
|
})
|
||||||
|
|
||||||
export default db
|
export default db
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { callAI } from '@/services/ai'
|
import { callAI } from '@/services/ai'
|
||||||
|
import { getPrompt } from '@/services/prompts-db'
|
||||||
import { getSessionsByProject, getSessionSummary, getProjectState } from '@/services/transcriptions-db'
|
import { getSessionsByProject, getSessionSummary, getProjectState } from '@/services/transcriptions-db'
|
||||||
import { saveDraft, createDraftId } from '@/services/hu-drafts-db'
|
import { saveDraft, createDraftId } from '@/services/hu-drafts-db'
|
||||||
import { generateAndSavePlan } from '@/services/qa-analyzer'
|
import { generateAndSavePlan } from '@/services/qa-analyzer'
|
||||||
@@ -25,40 +26,6 @@ interface AnalysisResult {
|
|||||||
summary: string
|
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(
|
export async function analyzeProject(
|
||||||
projectId: number,
|
projectId: number,
|
||||||
projectName: string,
|
projectName: string,
|
||||||
@@ -96,8 +63,9 @@ export async function analyzeProject(
|
|||||||
|
|
||||||
console.log(`[Alpha] Project analysis — ${projectId}, ${existingHUs.length} HUs existentes, ${sessions.length} sesiones`)
|
console.log(`[Alpha] Project analysis — ${projectId}, ${existingHUs.length} HUs existentes, ${sessions.length} sesiones`)
|
||||||
|
|
||||||
|
const systemPrompt = await getPrompt('project_gap')
|
||||||
const content = await callAI(
|
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,
|
0.3, 8192, signal,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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<PromptKey, { label: string; content: string }> = {
|
||||||
|
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<PromptKey, string>()
|
||||||
|
|
||||||
|
export async function initPrompts(): Promise<void> {
|
||||||
|
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<string> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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 ?? ''
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { callAI } from '@/services/ai'
|
import { callAI } from '@/services/ai'
|
||||||
|
import { getPrompt } from '@/services/prompts-db'
|
||||||
import db from '@/services/db'
|
import db from '@/services/db'
|
||||||
|
|
||||||
export interface QATestCase {
|
export interface QATestCase {
|
||||||
@@ -18,34 +19,11 @@ export interface HUQAPlan {
|
|||||||
criticalTests: string[]
|
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<HUQAPlan> {
|
export async function generateQAPlan(huTitle: string, huDescription: string, acceptanceCriteria: string): Promise<HUQAPlan> {
|
||||||
const userContent = `HU: ${huTitle}\nDescripción: ${huDescription}\nCriterios: ${acceptanceCriteria}`
|
const userContent = `HU: ${huTitle}\nDescripción: ${huDescription}\nCriterios: ${acceptanceCriteria}`
|
||||||
|
const systemPrompt = await getPrompt('qa')
|
||||||
const content = await callAI(
|
const content = await callAI(
|
||||||
[{ role: 'system', content: QA_PROMPT }, { role: 'user', content: userContent }],
|
[{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }],
|
||||||
0.3, 4096,
|
0.3, 4096,
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { callAI } from '@/services/ai'
|
import { callAI } from '@/services/ai'
|
||||||
|
import { getPrompt } from '@/services/prompts-db'
|
||||||
|
|
||||||
export interface SessionExtraction {
|
export interface SessionExtraction {
|
||||||
sessionDate: string // YYYY-MM-DD
|
sessionDate: string // YYYY-MM-DD
|
||||||
@@ -12,40 +13,6 @@ export interface SessionExtraction {
|
|||||||
keyPoints: string[]
|
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(
|
export async function analyzeSession(
|
||||||
transcription: string,
|
transcription: string,
|
||||||
projectName: string,
|
projectName: string,
|
||||||
@@ -55,9 +22,10 @@ export async function analyzeSession(
|
|||||||
|
|
||||||
console.log(`[Alpha] Session analyze — project: ${projectName}, text: ${transcription.length} chars`)
|
console.log(`[Alpha] Session analyze — project: ${projectName}, text: ${transcription.length} chars`)
|
||||||
|
|
||||||
|
const systemPrompt = await getPrompt('session')
|
||||||
const content = await callAI(
|
const content = await callAI(
|
||||||
[
|
[
|
||||||
{ role: 'system', content: SESSION_SYSTEM_PROMPT },
|
{ role: 'system', content: systemPrompt },
|
||||||
{ role: 'user', content: userContent },
|
{ role: 'user', content: userContent },
|
||||||
],
|
],
|
||||||
0.3,
|
0.3,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useSettingsStore, PROVIDER_CONFIG, type AIProvider } from '@/stores/settings'
|
import { useSettingsStore, PROVIDER_CONFIG, type AIProvider } from '@/stores/settings'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { getAllPromptKeys, getPrompt, savePrompt, resetPrompt, type PromptKey } from '@/services/prompts-db'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -31,6 +32,42 @@ const { t } = useI18n()
|
|||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
// ─── Prompt editor ────────────────────────────────────
|
||||||
|
const promptKeys = getAllPromptKeys()
|
||||||
|
const promptContents = ref<Record<string, string>>({})
|
||||||
|
const promptSaving = ref<Record<string, boolean>>({})
|
||||||
|
const promptDirty = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
async function loadPrompts() {
|
||||||
|
for (const { key } of promptKeys) {
|
||||||
|
promptContents.value[key] = await getPrompt(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePromptHandler(key: PromptKey) {
|
||||||
|
promptSaving.value[key] = true
|
||||||
|
try {
|
||||||
|
await savePrompt(key, promptContents.value[key])
|
||||||
|
promptDirty.value[key] = false
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('[Alpha] Error saving prompt:', e)
|
||||||
|
} finally {
|
||||||
|
promptSaving.value[key] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetPromptHandler(key: PromptKey) {
|
||||||
|
await resetPrompt(key)
|
||||||
|
promptContents.value[key] = await getPrompt(key)
|
||||||
|
promptDirty.value[key] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function markDirty(key: string) {
|
||||||
|
promptDirty.value[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadPrompts)
|
||||||
|
|
||||||
const newKey = ref('')
|
const newKey = ref('')
|
||||||
const keySaved = ref(false)
|
const keySaved = ref(false)
|
||||||
|
|
||||||
@@ -246,6 +283,51 @@ const tierColors: Record<string, string> = {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<!-- Prompts -->
|
||||||
|
<Card id="settings-prompts" class="border-dashed">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="text-sm font-medium flex items-center gap-2">
|
||||||
|
<Sparkles class="size-4" />
|
||||||
|
Prompts de IA
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription class="text-xs">
|
||||||
|
Editá los prompts que usa la IA para cada función. Los cambios se aplican inmediatamente.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div v-for="{ key, label } in promptKeys" :key="key" class="space-y-1.5">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Label class="text-xs font-medium">{{ label }}</Label>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<Button
|
||||||
|
v-if="promptDirty[key]"
|
||||||
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
class="text-xs h-6 px-2"
|
||||||
|
:disabled="promptSaving[key]"
|
||||||
|
@click="savePromptHandler(key as PromptKey)"
|
||||||
|
>
|
||||||
|
{{ promptSaving[key] ? 'Guardando...' : 'Guardar' }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
class="text-xs h-6 px-2 text-muted-foreground"
|
||||||
|
@click="resetPromptHandler(key as PromptKey)"
|
||||||
|
>
|
||||||
|
Restaurar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
:value="promptContents[key]"
|
||||||
|
@input="(e: any) => { promptContents[key] = e.target.value; markDirty(key) }"
|
||||||
|
class="w-full h-32 rounded-md border border-input bg-background p-2 text-xs font-mono outline-none focus:border-ring focus:ring-1 focus:ring-ring resize-y"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<!-- Account -->
|
<!-- Account -->
|
||||||
<Card id="settings-account">
|
<Card id="settings-account">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user