Eres un asistente especializado en la Metodología General Ajustada (MGA) para
la estructuración de proyectos de inversión pública en Colombia.
Tu objetivo es ayudar a municipios a diligenciar correctamente los campos de una
convocatoria específica. Debes:
1. Ser claro, conciso y orientado a la acción.
2. Basarte SOLO en la información disponible (plantilla MGA, contexto de la
convocatoria, y documentos indexados si los hay).
3. Si no tienes suficiente información para responder, di explícitamente:
"No tengo fuentes suficientes para esta sección" y formula preguntas
específicas para obtener la información faltante.
4. Nunca inventar datos, cifras o fuentes.
5. Responder SIEMPRE en el formato JSON especificado.
Archivo fuente: src/lib/ai/prompts.ts → buildSystemPrompt()
import { z } from "zod";
export const aiAssistResponseSchema = z.object({
suggested_text: z.string().describe("Texto sugerido para el campo MGA"),
bullets: z.array(z.string()).describe("Puntos clave resumidos"),
risks: z.array(z.string()).describe("Riesgos identificados"),
missing_info_questions: z.array(z.string()).describe("Preguntas sobre info faltante"),
citations: z.array(
z.object({
source: z.string(),
chunk_text: z.string(),
relevance_score: z.number().optional(),
})
).describe("Fuentes citadas del RAG (si aplica)"),
});Archivo fuente: src/lib/ai/schemas.ts
<convocatoria>
Nombre: {nombre}
Descripción: {descripcion}
Requisitos: {requisitos}
</convocatoria>
<etapa_mga>
Etapa: {etapa_nombre} ({orden} de la plantilla)
Campo: {campo_nombre}
Tipo de campo: {tipo}
Descripción: {campo_descripcion}
Requerido: {Sí/No}
</etapa_mga>
[Si hay documentos procesados para la convocatoria (RAG):]
<contexto_rag>
Los siguientes fragmentos provienen de documentos oficiales de la convocatoria.
Úsalos como fuente principal:
[Fuente 1: {file_name} (relevancia: {similarity}%)]
{chunk_text}
[Fuente 2: {file_name} (relevancia: {similarity}%)]
{chunk_text}
</contexto_rag>
<instruccion>
Ayuda al municipio a completar el campo "{campo_nombre}" de la etapa "{etapa_nombre}".
[Si hay texto actual del municipio:]
El municipio ya ha escrito lo siguiente (mejora y complementa, no reemplaces completamente):
<texto_actual>
{currentText}
</texto_actual>
Responde ÚNICAMENTE con el JSON especificado, sin texto adicional antes o después.
</instruccion>
Archivo fuente: src/lib/ai/prompts.ts → buildUserPrompt()
| Provider | Implementación | Config env vars |
|---|---|---|
| OpenAI (default) | SDK openai, json_object response_format |
OPENAI_API_KEY, OPENAI_MODEL |
| Anthropic | fetch directo a API | ANTHROPIC_API_KEY, ANTHROPIC_MODEL |
Selección via LLM_PROVIDER env var (default: openai).
Archivo fuente: src/lib/ai/adapter.ts
- Auth check (requiere sesión activa)
- Validar request con Zod (
aiAssistRequestSchema) - Rate limit: máx 10 req/min por usuario (via audit_logs count)
- Fetch convocatoria + template + etapa + campo
- RAG retrieval:
retrieveContext(convocatoria_id, query, top_k=5, threshold=0.7) - Build system prompt + user prompt (con
<contexto_rag>si hay chunks) - Hash del prompt (SHA-256, primeros 16 chars)
- Call LLM via adapter
- Validar respuesta con Zod (fallback graceful)
- Write audit_log (actor, action, prompt_hash, sources_used, response, duration)
- Return response +
_meta(model, duration_ms)
Archivo fuente: src/app/api/ai/assist/route.ts
- Entidad sube documento (PDF/TXT/DOCX) → Supabase Storage bucket
convocatoria-docs - Se registra metadata en tabla
documents(status:pending) - Entidad hace click en "Procesar" → POST
/api/documents/process - Pipeline:
- Descarga archivo de Storage
- Extrae texto: PDF (
pdf-parse), TXT (directo), DOCX (strip XML tags) - Chunking: ~500 tokens con 50 tokens overlap, breakpoints en fin de oración
- Embeddings:
text-embedding-3-smallvia OpenAI (batch de 100) - Insert en tabla
embeddingscon convocatoria_id - Update document status →
ready
- En cada request al asistente IA, se hace similarity search via
match_embeddingsRPC - Los chunks relevantes se incluyen en el prompt como
<contexto_rag>
Archivos fuente:
src/lib/ai/chunker.ts— Chunking con overlapsrc/lib/ai/embeddings.ts— Generación de embeddings (OpenAI)src/lib/ai/retrieval.ts— Similarity search via Supabase RPCsrc/app/api/documents/process/route.ts— Pipeline completo
Archivo fuente: src/app/api/ai/assist/route.ts
Eres un evaluador experto de proyectos MGA (Metodología General Ajustada)
para inversión pública en Colombia.
Tu tarea es evaluar la respuesta de un municipio para un campo específico
de una etapa MGA, usando la rúbrica proporcionada.
Responde SIEMPRE en formato JSON válido con esta estructura exacta:
{
"score": <número del nivel asignado>,
"justificacion": "<explicación breve de por qué se asignó este score>",
"recomendacion": "<recomendación específica para mejorar, o null si el score es máximo>"
}
<criterio>
Criterio: {criterio.descripcion}
Campo: {campo_nombre}
Peso: {criterio.peso}
Niveles de evaluación:
- Score 1 (Insuficiente): {descripcion}
- Score 2 (Básico): {descripcion}
- Score 3 (Bueno): {descripcion}
- Score 4 (Excelente): {descripcion}
</criterio>
<respuesta_municipio>
{valor del campo o "(Campo vacío — el municipio no ha respondido)"}
</respuesta_municipio>
Evalúa la respuesta del municipio según el criterio y los niveles definidos.
Responde ÚNICAMENTE con el JSON especificado.
- Per criterion: LLM assigns score (1-4) based on rubric levels
- Normalize:
normalizedScore = score / maxScore(per criterion) - Weighted sum:
weightedScore = Σ(normalizedScore × peso) - Total:
totalScore = (weightedScore / totalWeight) × 100(0-100 scale) - Upsert into
evaluationstable (UNIQUE on submission_id + etapa_id)
If LLM response is not valid JSON: { score: 1, justificacion: "Error al evaluar", recomendacion: null }
Archivo fuente: src/app/api/evaluations/run/route.ts