Gesgocom.NeuraNetGes 1.0.49

NeuraNetGes

Librería .NET para interactuar de forma unificada con múltiples proveedores de LLM (Large Language Models) como OpenAI, Google Gemini, Anthropic Claude, xAI Grok y Groq, con un sistema completo de guardrails para seguridad y validación, y un sistema de gestión de contexto persistente con PostgreSQL.

🌟 Novedades v1.0.48 (Fines de Febrero 2026)

  • Optimización Asíncrona Experta: Implementación sistemática de .ConfigureAwait(false) en toda la librería para evitar deadlocks en cualquier entorno .NET.
  • Gestión de Recursos Robusta: Patrón de HttpClient estático en Handlers para prevenir el agotamiento de sockets (socket exhaustion) bajo alta carga.
  • Paralelismo en Vision: Descarga paralela de imágenes mediante Task.WhenAll en Gemini Vision, reduciendo drásticamente la latencia en peticiones multimodales.
  • Streaming Real Anthropic: Preparado el flujo de streaming para integración nativa con el SDK Stainless.

🌟 Novedades v1.0.47 (Febrero 2026)

  • Google Gemini Multimodal (Directo): Soporte completo para envío de imágenes mediante bytes (ImagesData) al SDK oficial.
  • Normalización de Modelos Gemini: Resolución automática del prefijo models/ para evitar errores de API.
  • Comparativa de Salida Estructurada: Guía técnica detallada sobre la rigidez y métodos de JSON en OpenAI, Gemini, Anthropic y Groq.
  • Estabilidad Anthropic: Implementado workaround para mensajes de sistema en el flujo de usuario para máxima compatibilidad con el nuevo SDK.
  • Robustez Extrema: Refactorización de nulabilidad en todos los Handlers (Zero Warnings) para prevenir errores en tiempo de ejecución.
  • Suite de Integración: Nueva batería de tests para validación multi-proveedor de Vision, Thinking y Salida Estructurada.

🚀 Características

Core LLM

  • ✅ Soporte para OpenAI, Google Gemini, Anthropic Claude, xAI Grok y Groq
  • OpenAI (Feb 2026): Modo Strict, Structured Outputs nativo, soporte para GPT-5, o1, o3.
  • Google Gemini: Soporte para modo Thinking (Razonamiento visible).
  • Anthropic Claude: Migración completa al nuevo SDK v12.6.0 (Stainless).
  • ✅ Chat completions (normal y streaming)
  • ✅ Generación de imágenes (OpenAI y Gemini)
  • ✅ Análisis de imágenes / Vision (todos los proveedores)
  • Salida estructurada en JSON con validación automática
  • ✅ Interfaz unificada para todos los proveedores
  • ✅ Configuración mediante appsettings.json o inyección directa
  • ✅ Validación automática de disponibilidad de proveedores
  • ✅ Generación automática de schemas JSON desde clases C#

Sistema de Guardrails (Seguridad y Validación)

  • Pre-Prompt Guards: Validan entrada antes de enviar al LLM
    • Detección de prompt injection y jailbreak
    • Eliminación automática de PII (información personal)
    • Control de presupuesto de tokens
    • Listas de allow/deny para URLs, dominios y topics
  • Post-Output Guards: Validan respuesta del LLM
    • Redacción de secretos (API keys, tokens, passwords)
    • Moderación de contenido tóxico/ofensivo
    • Validación de schemas JSON con auto-reparación
  • Prompt Builder: Construcción segura de prompts con plantillas
  • Sistema de reintentos con backoff exponencial configurable
  • Guards personalizados: Crea tus propias validaciones

Sistema de Gestión de Contexto (Conversaciones Persistentes)

  • Almacenamiento en PostgreSQL: Persistencia completa de conversaciones
  • MicroOrmGesg v1.2.5: Uso nativo de IDataFunctions y mapeo automático de JSONB.
  • Ventana de contexto dinámica: Gestión automática del presupuesto de tokens
  • Mensajes multimodales: Soporte para texto, imágenes, tool calls
  • Tokenización real: Conteo preciso con SharpToken (OpenAI) y estimaciones para otros proveedores
  • Métricas completas: Tokens, costos, latencias por turno
  • Observaciones: Sistema de anotaciones y feedback sobre conversaciones
  • Orquestación completa: Manager de alto nivel que integra contexto + LLM + persistencia

Sistema de Function Calling / Tools

  • Soporte unificado para los 5 proveedores: OpenAI, Grok, Groq, Anthropic Claude y Google Gemini
  • Registro centralizado de herramientas: IToolRegistry con gestión thread-safe
  • Ejecución automática: Loop automático de llamadas a tools hasta completar la tarea
  • Ejecución paralela: Múltiples tools ejecutándose simultáneamente con control de concurrencia
  • Idempotencia y caching: Resultados cacheados para tools idempotentes
  • Retry automático: Reintentos con backoff exponencial para tools con errores transitorios
  • Clasificación de side effects: None, ReadOnly, WriteLocal, WriteNetwork, Irreversible
  • Validación y permisos: Control de qué tools pueden ejecutarse según sus efectos secundarios
  • Policy-based execution: Políticas configurables de timeout, paralelismo y permisos
  • Adaptadores específicos por proveedor: Normalización automática de las diferentes APIs

Sistema RAG (Retrieval Augmented Generation) - Nuevo!

  • Busqueda semantica con pgvector: PostgreSQL + embeddings para recuperacion de contexto
  • Multi-proveedor de embeddings: OpenAI (3072 dims) y Google Gemini (768 dims)
  • Optimización de almacenamiento: Gemini ofrece ~75% menos uso de disco
  • Orquestacion completa: Recuperacion de contexto + generacion de respuestas
  • Multi-tenant jerárquico: Dos niveles de aislamiento (ClientId + Identidad)
  • Integracion con Guardrails: Usa IGuardedLLMService para seguridad
  • Colecciones configurables: Multiples colecciones con diferentes modelos de embedding
  • Filtros avanzados JSONB: Operador @> para filtrado eficiente con índice GIN
  • Citas automaticas: Formato [Documento > Sección] con instrucciones al LLM
  • Extensible: Puntos de extension para reranking y chunk stitching

📄 Conversión de Documentos (Nuevo!)

  • PDF a Markdown: Detección automática de headers por tamaño de fuente (iText7)
  • DOCX a Markdown: Preserva estilos (Heading1, Heading2) y estructura
  • HTML a Markdown: Conversión inteligente con ReverseMarkdown
  • Factory pattern: IDocumentConverterFactory para selección automática de conversor

🧩 Agente de Chunking Estructural (Nuevo!)

  • MarkdownStructureStrategy: Análisis AST con Markdig para documentos legales/normativos
  • RecursiveFallbackStrategy: Fallback robusto para texto sin estructura
  • Preservación de jerarquía: Headers convertidos a ContextPath ("Ley > Título > Artículo")
  • Tokenización precisa: SharpToken con soporte para cl100k_base y o200k_base
  • Overlap configurable: Solapamiento entre chunks para preservar contexto

🔄 Pipeline de Ingestión (Nuevo!)

  • Flujo orquestado: Conversión → Chunking → Embedding → Persistencia
  • IRagIngestionService: API unificada para ingestar documentos
  • Dry-run mode: Prueba chunking sin persistir (validación previa)
  • Batch embedding: Procesamiento eficiente en lotes de 100 chunks
  • Métricas detalladas: Tiempos, tokens, estrategia usada, warnings

📦 Instalación

dotnet add package Gesgocom.NeuraNetGes

🔧 Desarrollo Local (Paquetes Locales)

Si estás desarrollando y probando cambios en NeuraNetGes antes de publicar a NuGet.org, usa el Makefile incluido:

Configuración Inicial (solo una vez)

dotnet nuget add source ~/.nuget/local-packages --name LocalDev

Flujo de Trabajo Rápido

# 1. Genera paquete (auto-incrementa versión)
make pack

# 2. Instala en tu proyecto
make install

El Makefile hace:

  • ✅ Incrementa automáticamente la versión patch (1.0.46 → 1.0.47)
  • ✅ Compila en Release y genera .nupkg
  • ✅ Copia a ~/.nuget/local-packages
  • ✅ Limpia caché de NuGet
  • ✅ Instala en el proyecto configurado (editable en Makefile)

Comandos disponibles:

  • make help - Muestra ayuda
  • make pack - Genera paquete con nueva versión
  • make install - Instala en proyecto destino
  • make version - Muestra versión actual
  • make clean - Limpia artefactos de build

Instalación manual (si prefieres no usar make install):

cd /tu/proyecto
dotnet add package Gesgocom.NeuraNetGes --version 1.0.X --source ~/.nuget/local-packages

Nota: Edita la variable INSTALL_TARGET en el Makefile para cambiar el proyecto de instalación automática.

⚙️ Configuración

1. Configuración mediante appsettings.json

Agrega la configuración de tus API Keys en appsettings.json:

{
  "LLMConfiguration": {
    "OpenAIApiKey": "sk-proj-...",
    "GoogleGeminiApiKey": "AIza...",
    "AnthropicApiKey": "sk-ant-...",
    "GrokApiKey": "xai-...",
    "GroqApiKey": "gsk_..."
  },
  "ConnectionStrings": {
    "ServidorSQL": "Host=localhost;Database=mydb;Username=user;Password=pass"
  }
}

2. Registro en el contenedor de dependencias

En Program.cs o Startup.cs:

using NeuraNetGes;
using NeuraNetGes.LLM;
using NeuraNetGes.LLM.Interfaces;
using NeuraNetGes.Guards.Interfaces;
using NeuraNetGes.Guards.PrePrompt;
using NeuraNetGes.Guards.PostOutput;
using NeuraNetGes.Guards.Models;
using NeuraNetGes.Contexto;

// Configurar las API Keys desde appsettings.json
builder.Services.Configure<LLMConfiguration>(
    builder.Configuration.GetSection("LLMConfiguration"));

// Registrar el servicio LLM base
builder.Services.AddSingleton<ILLMService, LLMService>();

// Registrar el servicio con guardrails (recomendado)
builder.Services.AddScoped<IGuardedLLMService>(sp =>
{
    var llmService = sp.GetRequiredService<ILLMService>();
    var logger = sp.GetRequiredService<ILogger<GuardedLLMService>>();
    var guardedService = new GuardedLLMService(llmService, logger);

    // Pre-Prompt Guards (validación de entrada)
    guardedService.RegisterPrePromptGuard(new InjectionDetectorGuard());
    guardedService.RegisterPrePromptGuard(new PIIScrubberGuard(autoScrub: true));
    guardedService.RegisterPrePromptGuard(new TokenBudgetGuard(maxTokens: 4000));

    // Post-Output Guards (validación de salida)
    guardedService.RegisterPostOutputGuard(new SecretRedactorGuard(autoRedact: true));
    guardedService.RegisterPostOutputGuard(new ModerationGuard());

    // Estrategia de reintentos
    guardedService.SetRetryStrategy(RetryStrategy.Default);

    return guardedService;
});

// Registrar sistema de contexto (requiere IGuardedLLMService y IDbSession)
builder.Services.AddNeuraNetGesContexto();

// Registrar sistema de Tools/Function Calling (opcional pero recomendado)
builder.Services.AddSingleton<NeuraNetGes.Tools.Interfaces.IToolRegistry, NeuraNetGes.Tools.Registry.ToolRegistry>();
builder.Services.AddScoped<NeuraNetGes.Tools.Interfaces.IToolExecutor, NeuraNetGes.Tools.Execution.ToolExecutor>();
builder.Services.AddMemoryCache(); // Requerido para caching de resultados

// Registrar herramientas de ejemplo (puedes crear las tuyas)
var toolRegistry = builder.Services.BuildServiceProvider().GetRequiredService<NeuraNetGes.Tools.Interfaces.IToolRegistry>();
NeuraNetGes.Tools.Examples.ExampleTools.RegisterAll(toolRegistry);

// Registrar sistema RAG (requiere IDbSession, IGuardedLLMService y LLMConfiguration)
builder.Services.AddNeuraNetGesRag(options =>
{
    options.EmbeddingModel = "text-embedding-3-large";
    options.RegisterPostgresRagStore = true;
});

📖 Índice de Documentación

Uso Básico

Sistema de Guardrails

Sistema de Contexto (Conversaciones Persistentes)

Sistema de Function Calling / Tools

Sistema RAG (Retrieval Augmented Generation)

📄 Conversión de Documentos (Nuevo!)

🧩 Agente de Chunking Estructural (Nuevo!)

🔄 Pipeline de Ingestión (Nuevo!)

🔍 Búsqueda y Respuesta RAG

Referencia Técnica


📖 Uso Básico (sin Guardrails)

Chat Completion Simple

public class MyService
{
    private readonly ILLMService _llmService;

    public MyService(ILLMService llmService)
    {
        _llmService = llmService;
    }

    public async Task ChatExample()
    {
        // Verificar si el proveedor está disponible
        if (!_llmService.IsProviderAvailable(LLMProvider.OpenAI))
        {
            Console.WriteLine("OpenAI no está configurado");
            return;
        }

        // Crear la petición
        var request = new GenericRequest
        {
            Provider = LLMProvider.OpenAI,
            RequestType = RequestType.ChatCompletion,
            Model = "gpt-5.2-mini",
            Prompt = "¿Cuál es la capital de España?",
            Temperature = 0.7,
            MaxTokens = 500
        };

        // Ejecutar
        var response = await _llmService.ChatCompletionAsync(request);

        if (response.Success)
        {
            Console.WriteLine(response.Content);
            Console.WriteLine($"Tokens usados: {response.TotalTokens}");
        }
        else
        {
            Console.WriteLine($"Error: {response.ErrorMessage}");
        }
    }
}

Chat Completion con Streaming

var request = new GenericRequest
{
    Provider = LLMProvider.Anthropic,
    RequestType = RequestType.ChatCompletion,
    Model = "claude-3-5-sonnet-20241022",
    Prompt = "Escribe un poema corto sobre el mar",
    Temperature = 0.8
};

await foreach (var chunk in _llmService.ChatCompletionStreamAsync(request))
{
    Console.Write(chunk); // Imprime cada fragmento en tiempo real
}

Uso de Grok (xAI)

Grok utiliza compatibilidad con la API de OpenAI, por lo que su uso es idéntico a OpenAI pero con modelos de xAI:

var request = new GenericRequest
{
    Provider = LLMProvider.Grok,
    RequestType = RequestType.ChatCompletion,
    Model = "grok-4-fast-non-reasoning",
    Prompt = "Explica cómo funciona la computación cuántica",
    Temperature = 0.7,
    MaxTokens = 1000
};

var response = await _llmService.ChatCompletionAsync(request);

if (response.Success)
{
    Console.WriteLine(response.Content);
    Console.WriteLine($"Modelo: {response.Model}");
    Console.WriteLine($"Tokens: {response.TotalTokens}");
}

Características de Grok:

  • Ventana de contexto de 2M tokens (ideal para documentos largos)
  • Compatible con streaming y salida estructurada JSON
  • Soporte para análisis de imágenes (vision)
  • No soporta generación de imágenes

Uso de Groq

Groq es una plataforma de inferencia especializada en hardware LPU (Language Processing Unit) que ofrece velocidades de inferencia extremadamente rápidas con modelos open source. Utiliza compatibilidad con la API de OpenAI:

var request = new GenericRequest
{
    Provider = LLMProvider.Groq,
    RequestType = RequestType.ChatCompletion,
    Model = "llama-3.3-70b-versatile",  // Modelo recomendado
    Prompt = "Explica la diferencia entre LPU y GPU",
    Temperature = 0.7,
    MaxTokens = 1000
};

var response = await _llmService.ChatCompletionAsync(request);

if (response.Success)
{
    Console.WriteLine(response.Content);
    Console.WriteLine($"Modelo: {response.Model}");
    Console.WriteLine($"Tokens: {response.TotalTokens}");
}

Modelos Disponibles en Groq:

Llama (Meta):

  • llama-3.3-70b-versatile - Llama 3.3 70B (recomendado, balance entre calidad y velocidad)
  • llama-3.1-70b-versatile - Llama 3.1 70B (alta calidad)
  • llama-3.1-8b-instant - Llama 3.1 8B (ultra rápido para tareas simples)
  • llama3-70b-8192 - Llama 3 70B (8K contexto)
  • llama3-8b-8192 - Llama 3 8B (8K contexto, muy rápido)

Mixtral (Mistral AI):

  • mixtral-8x7b-32768 - Mixtral 8x7B (32K contexto, excelente para documentos largos)

Gemma (Google):

  • gemma2-9b-it - Gemma 2 9B Instruction Tuned
  • gemma-7b-it - Gemma 7B Instruction Tuned

LLaVA (Vision):

  • llava-v1.5-7b-4096-preview - Análisis de imágenes (preview)

Características de Groq:

  • Velocidad extrema: Inferencia ultra-rápida gracias a LPUs (hasta 750 tokens/segundo)
  • Compatible con streaming y salida estructurada JSON
  • Soporte para Function Calling/Tools
  • Soporte para análisis de imágenes (modelos LLaVA)
  • No soporta generación de imágenes
  • Ideal para aplicaciones que requieren latencia baja y alto throughput
  • Todos los modelos son open source

Soporte para Modelos de Razonamiento (Febrero 2026)

NeuraNetGes está optimizado para los modelos de razonamiento más avanzados del mercado:

  • OpenAI (o1, o3, GPT-5):
    • Soporte para DeveloperChatMessage (reemplaza al SystemPrompt en estos modelos).
    • Captura automática de ReasoningTokens en la respuesta.
    • Configuración de ReasoningEffortLevel (Bajo, Medio, Alto) mediante NivelPensamiento.
    • Soporte para modo Strict en Structured Outputs con adherencia total al esquema.
  • Google Gemini:
    • Habilitación de ThinkingConfig para modelos con capacidad de razonamiento visible.
    • Gestión automática de parámetros incompatibles (como Temperature en modo razonamiento).

Ejemplo con Streaming (ultra rápido):

var request = new GenericRequest
{
    Provider = LLMProvider.Groq,
    Model = "llama-3.1-8b-instant",  // Modelo más rápido
    Prompt = "Lista 10 lenguajes de programación",
    Temperature = 0.7
};

await foreach (var chunk in _llmService.ChatCompletionStreamAsync(request))
{
    Console.Write(chunk); // Velocidad impresionante
}

Ejemplo con Vision:

var request = new GenericRequest
{
    Provider = LLMProvider.Groq,
    Model = "llava-v1.5-7b-4096-preview",
    Prompt = "¿Qué hay en esta imagen?",
    ImageUrl = "https://ejemplo.com/imagen.jpg"
};

var response = await _llmService.AnalyzeImageAsync(request);

Uso de SystemPrompt

El SystemPrompt permite definir el rol, personalidad y contexto del LLM de forma separada del prompt del usuario.

var request = new GenericRequest
{
    Provider = LLMProvider.OpenAI,
    RequestType = RequestType.ChatCompletion,
    Model = "gpt-5.2-mini",
    SystemPrompt = "Eres un experto en seguridad informática con 15 años de experiencia. Respondes de forma técnica pero accesible.",
    Prompt = "¿Cómo funciona un ataque de SQL injection?",
    Temperature = 0.7
};

var response = await _llmService.ChatCompletionAsync(request);

Best Practices:

  1. Separa rol de tarea: SystemPrompt para el ROL, Prompt para la TAREA
  2. Sé específico: Define comportamiento, tono y restricciones
  3. Reutiliza SystemPrompts: Define constantes para roles comunes

Generación de Imágenes

var request = new GenericRequest
{
    Provider = LLMProvider.OpenAI,
    RequestType = RequestType.ImageGeneration,
    Model = "dall-e-3",
    Prompt = "Un gato astronauta flotando en el espacio"
};

var response = await _llmService.GenerateImageAsync(request);

if (response.Success)
{
    Console.WriteLine($"Imagen generada: {response.ImageUrl}");
}

Análisis de Imágenes (Vision)

// Desde URL
var request = new GenericRequest
{
    Provider = LLMProvider.OpenAI,
    RequestType = RequestType.ImageAnalysis,
    Model = "gpt-5.2",
    Prompt = "Describe lo que ves en esta imagen",
    ImageUrl = "https://ejemplo.com/imagen.jpg"
};

// Desde bytes
byte[] imageBytes = await File.ReadAllBytesAsync("imagen.png");
var request2 = new GenericRequest
{
    Provider = LLMProvider.Anthropic,
    RequestType = RequestType.ImageAnalysis,
    Model = "claude-3-5-sonnet-20241022",
    Prompt = "¿Qué objetos puedes identificar?",
    ImageData = imageBytes
};

var response = await _llmService.AnalyzeImageAsync(request);

Análisis de Imágenes (Vision) - Soporte Multi-imagen

NeuraNetGes soporta el análisis de múltiples imágenes simultáneamente en todos los proveedores compatibles.

// Opción A: Múltiples URLs
var request = new GenericRequest
{
    Provider = LLMProvider.OpenAI,
    Model = "gpt-5.2",
    Prompt = "Compara estas tres diapositivas y resume las diferencias",
    ImageUrls = new List<string>
    {
        "https://ejemplo.com/slide1.jpg",
        "https://ejemplo.com/slide2.jpg",
        "https://ejemplo.com/slide3.jpg"
    }
};

// Opción B: Múltiples imágenes en binario (bytes)
var requestBytes = new GenericRequest
{
    Provider = LLMProvider.Anthropic,
    Model = "claude-3-5-sonnet-20241022",
    Prompt = "¿Qué tienen en común estas fotos?",
    ImagesData = new List<byte[]> { bytesFoto1, bytesFoto2 }
};

var response = await _llmService.AnalyzeImageAsync(request);

Salida Estructurada en JSON

NeuraNetGes permite forzar al LLM a responder con un formato JSON específico que se mapea directamente a tus clases C#.

using NeuraNetGes.Helpers;

// Definir el modelo de salida
public class ProductReview
{
    public string ProductName { get; set; }
    public int Rating { get; set; }
    public string Summary { get; set; }
    public List<string> Pros { get; set; }
    public List<string> Cons { get; set; }
}

// Generar el schema automáticamente
var schema = JsonSchemaHelper.GenerateSchema<ProductReview>();

var request = new GenericRequest
{
    Provider = LLMProvider.OpenAI,
    RequestType = RequestType.ChatCompletion,
    Model = "gpt-5.2",
    Prompt = "Analiza esta review: 'Excelente producto, muy rápido pero un poco caro'",
    UseStructuredOutput = true,
    JsonSchema = schema
};

var response = await _llmService.ChatCompletionAsync(request);

if (response.Success && response.IsValidJson == true)
{
    var review = response.StructuredData.ToObject<ProductReview>();
    Console.WriteLine($"Producto: {review.ProductName}");
    Console.WriteLine($"Rating: {review.Rating}/5");
}

Comparativa de Soporte JSON por Proveedor

Proveedor ¿Soporta JSON? Método en NeuraNetGes Rigidez / Fiabilidad
OpenAI ✅ Sí Native Schema (Strict) Máxima. Garantiza adherencia total al esquema.
Google Gemini ✅ Sí Native Schema (Cleaned) Alta. Se limpia el esquema automáticamente para compatibilidad.
Groq / Grok ✅ Sí JSON Mode Media. Garantiza JSON válido pero requiere prompts claros.
Anthropic ✅ Sí Prompt Enhancement Media/Alta. Se fuerza vía instrucciones de sistema optimizadas.

[!NOTE] Recomendación: Para aplicaciones críticas donde el parseo debe ser exacto, se recomienda usar OpenAI o Gemini. Para tareas que requieren velocidad extrema y el JSON es simple, Groq es la mejor opción.


🛡️ Sistema de Guardrails

¿Por qué usar Guardrails?

  • 🔒 Seguridad: Previene inyección de prompts, jailbreak y fugas de información
  • 🧹 Sanitización: Elimina PII (información personal) y secretos automáticamente
  • Validación: Verifica esquemas JSON y modera contenido tóxico
  • 🔄 Reintentos: Sistema inteligente de reintentos con corrección automática
  • 📊 Observabilidad: Metadatos detallados de cada guard ejecutado

Configuración Rápida de Guards

using NeuraNetGes.Guards.Interfaces;
using NeuraNetGes.Guards.Services;
using NeuraNetGes.Guards.PrePrompt;
using NeuraNetGes.Guards.PostOutput;
using NeuraNetGes.Guards.Models;

var guardedService = new GuardedLLMService(llmService);

// Registrar guards esenciales
guardedService.RegisterPrePromptGuard(new InjectionDetectorGuard());
guardedService.RegisterPrePromptGuard(new PIIScrubberGuard(autoScrub: true));
guardedService.RegisterPostOutputGuard(new SecretRedactorGuard(autoRedact: true));
guardedService.RegisterPostOutputGuard(new ModerationGuard());

// Configurar reintentos
guardedService.SetRetryStrategy(RetryStrategy.Default);

// Usar normalmente
var request = new GenericRequest { /* ... */ };
var response = await guardedService.ChatCompletionAsync(request);

if (response.AllGuardsPassed)
{
    Console.WriteLine($"✅ Response: {response.Response.Content}");
}

Pre-Prompt Guards Disponibles

1. InjectionDetectorGuard

Detecta intentos de inyección de prompts y jailbreak.

var guard = new InjectionDetectorGuard(
    threshold: 0.7,  // Sensibilidad de detección (0-1)
    onDetection: GuardFailureAction.Block
);

Detecta: Comandos de escape, intentos de jailbreak, inyección de roles, comandos del sistema.

2. PIIScrubberGuard

Detecta y elimina información personal identificable.

var guard = new PIIScrubberGuard(
    autoScrub: true,
    replacement: "[REDACTED]"
);

Detecta: Emails, teléfonos, DNI/NIE, IBAN, tarjetas de crédito, SSN, IPs.

3. TokenBudgetGuard

Limita el número de tokens con auto-truncación.

var guard = new TokenBudgetGuard(
    maxTokens: 4000,
    autoTruncate: true
);

4. AllowDenyListGuard

Implementa listas de allow/deny para URLs, dominios y topics.

var guard = new AllowDenyListGuard(ListMode.DenyList);
guard.AddDeniedDomain("malicious-site.com");
guard.AddDeniedTopic("violencia");

Post-Output Guards Disponibles

1. SecretRedactorGuard

Detecta y redacta secretos en las respuestas.

var guard = new SecretRedactorGuard(
    autoRedact: true,
    replacement: "[REDACTED_SECRET]"
);

Detecta: API Keys, AWS Keys, GitHub Tokens, JWT Tokens, Passwords, Bearer Tokens, Private Keys.

2. ModerationGuard

Detecta contenido tóxico u ofensivo.

var guard = new ModerationGuard(
    threshold: 0.7,
    onDetection: GuardFailureAction.Retry
);

3. SchemaValidatorGuard

Valida respuestas JSON con auto-reparación.

var guard = new SchemaValidatorGuard(autoRepair: true);

Prompt Builder Seguro

using NeuraNetGes.Guards.PromptBuilder;

var builder = new PromptBuilder("Eres un asistente útil y seguro.");

builder.RegisterTemplate(
    "query",
    "Pregunta: {question}\nContexto: {context}",
    "question", "context"
);

guardedService.SetPromptBuilder(builder);

var prompt = builder.BuildFromTemplate("query", new Dictionary<string, string>
{
    ["question"] = "¿Qué es NeuraNetGes?",
    ["context"] = "Librería .NET para LLMs"
});

Bypass de Guards (Casos Especiales)

⚠️ Usar con EXTREMA PRECAUCIÓN

var request = new GenericRequest
{
    Provider = LLMProvider.OpenAI,
    Model = "gpt-5.2-mini",
    Prompt = csvDataWithPII,

    // Omitir guards específicos solo para esta petición
    BypassGuards = new[] { "PIIScrubber" }
};

Casos legítimos:

  • Importadores de datos que necesitan procesar PII
  • Operaciones administrativas autorizadas
  • Testing de guards individuales

Crear Guards Personalizados

public class CustomPrePromptGuard : IPrePromptGuard
{
    public string Name => "CustomGuard";
    public int Priority => 50;
    public bool Enabled { get; set; } = true;

    public Task<GuardResult> ValidateAsync(GuardContext context)
    {
        if (context.CurrentRequest.Prompt.Contains("palabra-prohibida"))
        {
            return Task.FromResult(GuardResult.Failure(
                "Palabra prohibida detectada",
                GuardFailureAction.Block
            ));
        }

        return Task.FromResult(GuardResult.Success());
    }
}

💬 Sistema de Contexto (Conversaciones Persistentes)

Configuración de Base de Datos PostgreSQL

Primero, crea las tablas necesarias en PostgreSQL:

-- Ver scripts completos en la sección "Scripts de Base de Datos" al final
-- Tablas requeridas:
-- 1. conversacion
-- 2. turno
-- 3. mensaje
-- 4. ventana_contexto
-- 5. observacion
-- 6. Funciones: reconstruir_ventana_contexto(), crear_conversacion_desde_cero()

Validación Automática de Schema

El sistema de contexto valida automáticamente que todas las tablas, funciones y triggers requeridos existan en PostgreSQL al inicializar la aplicación.

Configuración

// Validación automática habilitada (recomendado en desarrollo)
builder.Services.AddNeuraNetGesContexto(
    validateSchema: true,
    throwOnSchemaError: true  // Bloquea startup si falta algo
);

// Solo validar en desarrollo
builder.Services.AddNeuraNetGesContexto(
    validateSchema: builder.Environment.IsDevelopment(),
    throwOnSchemaError: builder.Environment.IsDevelopment()
);

// Producción: validar pero no bloquear startup
builder.Services.AddNeuraNetGesContexto(
    validateSchema: true,
    throwOnSchemaError: false  // Solo loguea warnings
);

// Desactivar validación completamente
builder.Services.AddNeuraNetGesContexto(
    validateSchema: false
);

Logs de Validación

Schema válido:

[INF] 🔍 Validando schema de base de datos para NeuraNetGes.Contexto...
[DBG] ✓ Tabla 'conversacion' existe
[DBG] ✓ Tabla 'turno' existe
[DBG] ✓ Función 'reconstruir_ventana_contexto' existe
[INF] ✅ Schema validado correctamente. Sistema de contexto listo para operar.

Si falta algo:

[ERR] Schema de base de datos incompleto para sistema de contexto:
  - Tabla requerida no existe: conversacion
  - Función requerida no existe: crear_conversacion_desde_cero
[ERR] Ejecuta los scripts de creación de tablas del README.md sección 'Scripts de Base de Datos'

Health Check Endpoint (Opcional)

Puedes exponer un endpoint de salud para verificar el schema:

using NeuraNetGes.Contexto.Postgres;

// En Program.cs después de var app = builder.Build();
app.MapGet("/health/database", async (SchemaValidator validator) =>
{
    var valido = await validator.ValidarSchemaAsync(throwOnError: false);
    return valido
        ? Results.Ok(new { status = "healthy", message = "Schema válido" })
        : Results.Problem("Schema inválido", statusCode: 503);
});

// Health check rápido (solo funciones)
app.MapGet("/health/database/functions", async (SchemaValidator validator) =>
{
    var valido = await validator.ValidarFuncionesAsync();
    return valido
        ? Results.Ok(new { status = "healthy" })
        : Results.Problem("Funciones faltantes", statusCode: 503);
});

Uso Básico del Sistema de Contexto

using NeuraNetGes.Contexto.Interfaces;

public class ChatController
{
    private readonly IConversacionManager _conversacionManager;
    private readonly IConversacionesService _conversacionesService;

    // 1. Crear conversación
    public async Task<Guid> CrearChat(int entidadId)
    {
        var (conversacionId, _) = await _conversacionesService.CrearConversacionAsync(
            entidadId: entidadId,
            titulo: "Chat de atención al cliente",
            proposito: "Soporte técnico",
            ventanaTokens: 6000,
            turnosMax: 40,
            diasRetencion: 90,
            mensajeSistema: "Eres un asistente de soporte técnico profesional."
        );

        return conversacionId;
    }

    // 2. Enviar mensaje
    public async Task<string> EnviarMensaje(Guid conversacionId, string mensaje)
    {
        var response = await _conversacionManager.EnviarMensajeAsync(
            conversacionId,
            textoUsuario: mensaje,
            proveedor: LLMProvider.OpenAI,
            modelo: "gpt-5.2-mini",
            temperature: 0.7,
            maxTokens: 1000
        );

        if (response.AllGuardsPassed && response.Success)
        {
            return response.Response.Content ?? "";
        }

        throw new Exception($"Error: {response.ErrorMessage}");
    }

    // 3. Obtener estado
    public async Task<(int mensajes, int tokens)> ObtenerEstado(Guid conversacionId)
    {
        var contexto = await _conversacionManager.ObtenerContextoAsync(conversacionId);
        return (contexto.Mensajes.Count, contexto.TokensEntradaTotal);
    }
}

Casos de Uso Reales con Contexto

Chatbot de Atención al Cliente

public async Task<Guid> IniciarTicket(int clienteId, string asunto)
{
    var (conversacionId, _) = await _conversacionesService.CrearConversacionAsync(
        entidadId: clienteId,
        titulo: $"Ticket: {asunto}",
        proposito: "Soporte técnico",
        ventanaTokens: 4000,
        turnosMax: 30,
        diasRetencion: 365,
        mensajeSistema: @"Eres un agente de soporte técnico.
- Sé empático y paciente
- Ofrece soluciones claras
- Escala si no sabes algo"
    );

    return conversacionId;
}

Asistente Educativo

public async Task<Guid> IniciarSesionEstudio(int estudianteId, string materia)
{
    var (conversacionId, _) = await _conversacionesService.CrearConversacionAsync(
        entidadId: estudianteId,
        titulo: $"Sesión: {materia}",
        ventanaTokens: 8000,
        mensajeSistema: $@"Eres un tutor experto en {materia}.
- Explica paso a paso
- Usa ejemplos prácticos
- Verifica comprensión"
    );

    return conversacionId;
}

Tokenización y Costos

using NeuraNetGes.Contexto.Helpers;

// Estimar tokens de un texto
var tokens = TokenizadorHelper.EstimarTokens(
    "Tu texto aquí",
    LLMProvider.OpenAI,
    "gpt-5.2-mini"
);

// Obtener tokens reales desde response
var (entrada, salida) = TokenizadorHelper.ObtenerTokensReales(response);

Tokenización por proveedor:

  • OpenAI: SharpToken con encodings reales
  • Anthropic/Gemini: Estimación basada en caracteres
  • Costos: Calculados automáticamente en céntimos de euro

Observaciones y Anotaciones

using NeuraNetGes.Contexto.Modelos;

// Agregar feedback de usuario
await _almacen.AgregarObservacionAsync(conversacionId, new ObservacionEntrada(
    autor: "user",
    tipo: "feedback",
    visibilidad: "team",
    texto: "Respuesta muy útil, gracias",
    etiquetas: new[] { "positive", "helpful" },
    adjuntosUri: null,
    turnoId: turnoId
));

// Registrar issue detectado
await _almacen.AgregarObservacionAsync(conversacionId, new ObservacionEntrada(
    autor: "system",
    tipo: "issue",
    visibilidad: "private",
    texto: "Guard PIIScrubber detectó PII",
    etiquetas: new[] { "security", "pii" }
));

🔍 Sistema RAG (Retrieval Augmented Generation)

¿Que es RAG?

RAG (Retrieval Augmented Generation) es una tecnica que mejora las respuestas de un LLM proporcionandole contexto relevante extraido de tus propios documentos. En lugar de depender solo del conocimiento entrenado del modelo, RAG:

  1. Indexa tus documentos como vectores (embeddings) en una base de datos
  2. Busca los fragmentos mas relevantes para cada pregunta
  3. Genera respuestas usando ese contexto especifico

Esto permite respuestas mas precisas, actualizadas y con menos alucinaciones.

Configuracion del Sistema RAG

using NeuraNetGes.Rag;

// En Program.cs - Opción A: OpenAI Embeddings (por defecto)
builder.Services.AddNeuraNetGesRag(options =>
{
    options.EmbeddingProvider = RagEmbeddingProvider.OpenAI;
    options.EmbeddingModel = "text-embedding-3-large"; // 3072 dimensiones
    options.RegisterPostgresRagStore = true;
});

// En Program.cs - Opción B: Google Gemini Embeddings (más eficiente)
builder.Services.AddNeuraNetGesRag(options =>
{
    options.EmbeddingProvider = RagEmbeddingProvider.Google;
    options.EmbeddingModel = "text-embedding-004"; // 768 dimensiones (~75% menos almacenamiento)
    options.RegisterPostgresRagStore = true;
});

Proveedores de Embeddings Disponibles

Proveedor Modelo Dimensiones Almacenamiento API Key
OpenAI text-embedding-3-large 3072 ~12 KB/vector OpenAIApiKey
OpenAI text-embedding-3-small 1536 ~6 KB/vector OpenAIApiKey
Google text-embedding-004 768 ~3 KB/vector GoogleGeminiApiKey

Recomendación: Para proyectos nuevos, considera usar Google Gemini (text-embedding-004) que ofrece:

  • ~75% menos almacenamiento vectorial
  • Menor costo de API
  • Rendimiento de búsqueda comparable

Requisitos previos:

  • LLMConfiguration registrado (con API key del proveedor elegido)
  • IDbSession registrado (MicroOrmGesg)
  • IGuardedLLMService registrado (para el orquestador)
  • PostgreSQL con extension pgvector instalada

Multi-Tenancy Jerárquico

El sistema RAG implementa un modelo de multi-tenancy jerárquico de dos niveles que permite aislar datos entre diferentes clientes y sus entidades:

┌─────────────────────────────────────────────────────────────┐
│                    ARQUITECTURA MULTI-TENANT                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Nivel 1: ClientId (int)                                    │
│  ─────────────────────                                      │
│  Representa el cliente API / dueño de la cuenta             │
│  Ejemplos: Cabildo de Tenerife, Ayuntamiento de Madrid      │
│                                                             │
│     ┌─────────────────┐     ┌─────────────────┐            │
│     │  ClientId = 1   │     │  ClientId = 2   │            │
│     │  (Cabildo TF)   │     │  (Ayto Madrid)  │            │
│     └────────┬────────┘     └────────┬────────┘            │
│              │                       │                      │
│              ▼                       ▼                      │
│  ┌───────────────────────┐  ┌───────────────────────┐      │
│  │ Nivel 2: Identidad    │  │ Nivel 2: Identidad    │      │
│  │ (id_entidad integer)  │  │ (id_entidad integer)  │      │
│  ├───────────────────────┤  ├───────────────────────┤      │
│  │ • Auditorio (1)       │  │ • Cultura (1)         │      │
│  │ • Museos (2)          │  │ • Deportes (2)        │      │
│  │ • Turismo (3)         │  │ • Urbanismo (3)       │      │
│  └───────────────────────┘  └───────────────────────┘      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

¿Por qué dos niveles?

Nivel Campo Tipo Propósito
1 ClientId int Aislamiento entre clientes API (facturación, cuotas, SLA)
2 Identidad stringint Aislamiento entre entidades/organizaciones del mismo cliente

Ejemplo de uso:

// Ingestar documento para el Auditorio del Cabildo de Tenerife
var request = new IngestionRequest
{
    ClientId = 1,          // Cabildo de Tenerife
    Identidad = "42",      // Auditorio (se convierte a id_entidad = 42)
    CollectionId = collectionId,
    DocumentId = "normativa-uso-auditorio-2025",
    MarkdownContent = markdown,
    DocType = DocumentTypes.Normativa
};

await _ingestionService.IndexDocumentAsync(request);

// Buscar solo en documentos del Auditorio del Cabildo
var question = new RagQuestion
{
    ClientId = 1,          // Solo datos del Cabildo
    Identidad = "42",      // Solo datos del Auditorio
    CollectionId = collectionId,
    Query = "¿Cuál es el aforo máximo del auditorio?"
};

var answer = await _orchestrator.AnswerAsync(question);

Aislamiento de datos:

Todas las operaciones del sistema RAG están filtradas por ambos niveles:

-- Ejemplo de query generada internamente
SELECT * FROM rag_chunk
WHERE coleccion_id = @coleccion_id
  AND client_id = @client_id        -- Nivel 1: Cliente API
  AND id_entidad = @id_entidad      -- Nivel 2: Entidad
  AND ...

Nota sobre conversión de Identidad:

El parámetro Identidad es un string en la API de C# pero se convierte a integer (id_entidad) en PostgreSQL mediante int.Parse(). Si tu esquema usa otro tipo de columna (ej: uuid, text), deberás adaptar PostgresRagStore.

Esquema de Base de Datos RAG

El sistema RAG NO crea las tablas automaticamente. Debes ejecutar este SQL en PostgreSQL:

-- Habilitar pgvector
CREATE EXTENSION IF NOT EXISTS vector;

-- Tabla de colecciones RAG
CREATE TABLE IF NOT EXISTS rag_coleccion (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    nombre text UNIQUE NOT NULL,
    descripcion text,
    dimensiones int NOT NULL DEFAULT 3072,
    modelo_embedding text NOT NULL DEFAULT 'text-embedding-3-large',
    creado_en timestamptz NOT NULL DEFAULT now()
);

-- Tabla de chunks con embeddings (Multi-Tenancy Jerárquico)
CREATE TABLE IF NOT EXISTS rag_chunk (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    coleccion_id uuid NOT NULL REFERENCES rag_coleccion(id) ON DELETE CASCADE,
    client_id integer NOT NULL,      -- Nivel 1: Cliente API (ej: Cabildo, Ayuntamiento)
    id_entidad integer NOT NULL,     -- Nivel 2: Entidad de datos (ej: Auditorio, Museos)
    document_id text NOT NULL,
    chunk_index int NOT NULL,
    content text NOT NULL,
    local_title text,
    summary text,
    metadata jsonb NOT NULL DEFAULT '{}',
    embedding vector(3072),  -- Ajustar dimension segun modelo
    creado_en timestamptz NOT NULL DEFAULT now()
);

-- Indices para rendimiento (Multi-Tenancy Jerárquico)
CREATE INDEX IF NOT EXISTS ix_rag_chunk_coleccion ON rag_chunk(coleccion_id);
CREATE INDEX IF NOT EXISTS ix_rag_chunk_client ON rag_chunk(client_id);
CREATE INDEX IF NOT EXISTS ix_rag_chunk_client_entidad ON rag_chunk(client_id, id_entidad);
CREATE INDEX IF NOT EXISTS ix_rag_chunk_document ON rag_chunk(document_id);

-- Indice vectorial para busqueda semantica (IVFFlat)
CREATE INDEX IF NOT EXISTS ix_rag_chunk_embedding ON rag_chunk
USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

-- Indice alternativo HNSW (mas rapido, mas memoria)
-- CREATE INDEX IF NOT EXISTS ix_rag_chunk_embedding_hnsw ON rag_chunk
-- USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);

Aviso Crítico: Dimensiones de Vectores

PostgreSQL (pgvector) es estricto con las dimensiones. Si la columna está definida como vector(3072) e intentas insertar un vector de 768 dimensiones (Gemini), fallará.

Para entorno nuevo con Google Gemini:

-- Usar 768 en lugar de 3072
embedding vector(768)

Para migrar de OpenAI a Gemini (datos existentes):

-- ⚠️ ESTO BORRA TODOS LOS EMBEDDINGS EXISTENTES
-- Los chunks se mantienen, solo se pierden los vectores

ALTER TABLE rag_chunk DROP COLUMN embedding;
ALTER TABLE rag_chunk ADD COLUMN embedding vector(768);

-- Recrear índice vectorial
DROP INDEX IF EXISTS ix_rag_chunk_embedding;
CREATE INDEX ix_rag_chunk_embedding ON rag_chunk
USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

-- Actualizar metadata de colección
UPDATE rag_coleccion SET dimensiones = 768, modelo_embedding = 'text-embedding-004';

Importante: Después de migrar, debes regenerar los embeddings de todos los chunks usando el servicio de ingestión.

Tabla de referencia de dimensiones:

Modelo Dimensiones SQL
text-embedding-3-large (OpenAI) 3072 vector(3072)
text-embedding-3-small (OpenAI) 1536 vector(1536)
text-embedding-004 (Google) 768 vector(768)

Crear una Coleccion

Las colecciones agrupan chunks relacionados (por ejemplo, documentacion de un producto):

using NeuraNetGes.Rag.Interfaces;
using NeuraNetGes.Rag.Models;

public class DocumentService
{
    private readonly IRagStore _store;

    // Opción A: Colección con OpenAI (3072 dims)
    public async Task<Guid> CrearColeccionOpenAIAsync(string nombre, string descripcion)
    {
        var definition = new RagCollectionDefinition
        {
            Name = nombre,
            Description = descripcion,
            Dimensions = 3072,
            EmbeddingModel = "text-embedding-3-large"
        };
        return await _store.CreateCollectionAsync(definition);
    }

    // Opción B: Colección con Google Gemini (768 dims - recomendado)
    public async Task<Guid> CrearColeccionGeminiAsync(string nombre, string descripcion)
    {
        var definition = new RagCollectionDefinition
        {
            Name = nombre,
            Description = descripcion,
            Dimensions = 768,  // ¡Importante! Debe coincidir con la columna de BD
            EmbeddingModel = "text-embedding-004"
        };

        // Si ya existe, devuelve el ID existente
        return await _store.CreateCollectionAsync(definition);
    }

    public async Task<RagCollectionInfo?> ObtenerColeccionAsync(string nombre)
    {
        return await _store.GetCollectionAsync(nombre);
    }
}

📄 Conversión de Documentos

El sistema de conversión transforma documentos binarios (PDF, DOCX, HTML) a Markdown estructurado, preservando la jerarquía de headers que será utilizada por el agente de chunking.

Formatos Soportados

Formato Extensiones Librería Características
PDF .pdf iText7 Detección de headers por tamaño de fuente
Word .docx, .doc DocumentFormat.OpenXml Preserva estilos Heading1-6
HTML .html, .htm, .xhtml ReverseMarkdown Conversión inteligente de etiquetas

Registrar Servicios de Conversión

using NeuraNetGes.Rag.DocumentConversion;

// En Program.cs
builder.Services.AddDocumentConversion();

// O con configuración personalizada
builder.Services.AddDocumentConversion(factory =>
{
    // Registrar conversores personalizados si es necesario
});

Convertir un PDF a Markdown

using NeuraNetGes.Rag.DocumentConversion.Interfaces;

public class DocumentProcessor
{
    private readonly IDocumentConverterFactory _converterFactory;

    public DocumentProcessor(IDocumentConverterFactory converterFactory)
    {
        _converterFactory = converterFactory;
    }

    public async Task<string> ConvertirPdfAsync(string filePath)
    {
        // 1. Obtener el conversor apropiado por extensión
        var converter = _converterFactory.GetConverter(".pdf");

        // 2. Leer el archivo
        var fileBytes = await File.ReadAllBytesAsync(filePath);

        // 3. Convertir a Markdown
        var result = await converter.ConvertAsync(fileBytes);

        if (!result.Success)
        {
            throw new Exception($"Error en conversión: {result.ErrorMessage}");
        }

        // El resultado incluye:
        // - result.MarkdownContent: El markdown generado
        // - result.Metadata: Metadatos extraídos (autor, título, páginas)
        // - result.Warnings: Advertencias durante la conversión

        return result.MarkdownContent;
    }
}

Detección Automática de Estructura

El conversor de PDF detecta automáticamente la estructura del documento analizando el tamaño de fuente:

Texto con fuente 18pt → # Header 1
Texto con fuente 16pt → ## Header 2
Texto con fuente 14pt → ### Header 3
Texto normal (12pt)   → Párrafo

Ejemplo de salida para una ley:

# Ley 30/2025 de Transparencia

## Título I - Disposiciones Generales

### Artículo 1. Objeto

Esta ley tiene por objeto ampliar y reforzar la transparencia
de la actividad pública...

### Artículo 2. Ámbito de aplicación

La presente ley será de aplicación a:
a) La Administración General del Estado
b) Las Comunidades Autónomas

🧩 Agente de Chunking Estructural

El agente de chunking divide documentos Markdown en fragmentos semánticamente coherentes, preservando la jerarquía de secciones.

Estrategias de Chunking

Estrategia Prioridad Ideal para Características
MarkdownStructureStrategy 10 (alta) Leyes, normativas, documentación técnica Análisis AST con Markdig
RecursiveFallbackStrategy 100 (baja) Texto plano, artículos División por párrafos/oraciones

El sistema selecciona automáticamente la mejor estrategia según el tipo de documento.

MarkdownStructureStrategy para Documentos Legales

Optimizada para documentos con estructura jerárquica clara:

using NeuraNetGes.Rag.Chunking.Models;

// Opciones predefinidas para documentos normativos
var options = ChunkingOptions.ForNormativeDocuments();
// Equivale a:
// - MaxTokens: 400
// - MinTokens: 80
// - OverlapTokens: 40
// - ChunkBoundaryHeaders: [1, 2, 3] (H1, H2, H3 fuerzan nuevo chunk)

Características:

  • ✅ Analiza el AST del Markdown con Markdig
  • ✅ Respeta límites semánticos (nunca corta a mitad de párrafo)
  • ✅ Crea chunks en headers H1, H2, H3 (configurable)
  • ✅ Preserva contexto: cada chunk incluye su ruta jerárquica

RecursiveFallbackStrategy para Texto General

Se activa automáticamente cuando el documento no tiene estructura markdown:

var options = ChunkingOptions.ForNewsArticles();
// - MaxTokens: 600
// - MinTokens: 100
// - OverlapTokens: 60
// - ChunkBoundaryHeaders: [1] (solo H1)

Preservación de Jerarquía (Headers)

El sistema preserva la ruta completa de secciones (ContextPath) en cada chunk:

// Documento original:
// # Ley 30/2025
// ## Título I - Disposiciones Generales
// ### Artículo 4. Definiciones
// [contenido del artículo...]

// Chunk resultante:
{
    Content: "[contenido del artículo...]",
    ContextHeaders: ["Ley 30/2025", "Título I - Disposiciones Generales", "Artículo 4. Definiciones"],
    ContextPath: "Ley 30/2025 > Título I - Disposiciones Generales > Artículo 4. Definiciones",
    TokenCount: 287,
    ChunkIndex: 3,
    StrategyUsed: "MarkdownStructure"
}

Esta jerarquía se usa luego para:

  1. Almacenamiento: Se guarda en local_title de rag_chunk
  2. Citación: El LLM puede citar como [Ley 30/2025 > Artículo 4]
  3. Contexto: El usuario sabe exactamente de dónde viene la información

🔄 Pipeline de Ingestión

El servicio IRagIngestionService orquesta el flujo completo:

Documento → Conversión → Chunking → Embedding → Persistencia
   PDF        Markdown     Chunks    float[]     PostgreSQL

Flujo Completo de Ingestión

using NeuraNetGes.Rag;
using NeuraNetGes.Rag.DocumentConversion;

// En Program.cs
builder.Services.AddNeuraNetGesRag(options =>
{
    options.EmbeddingModel = "text-embedding-3-large";
    options.RegisterPostgresRagStore = true;
    options.RegisterChunkingServices = true;    // Habilita chunking
    options.RegisterIngestionService = true;    // Habilita ingestión
});

builder.Services.AddDocumentConversion();  // Para PDF/DOCX/HTML

Ingestar un Documento PDF

using NeuraNetGes.Rag.Interfaces;
using NeuraNetGes.Rag.Models;
using NeuraNetGes.Rag.DocumentConversion.Interfaces;
using NeuraNetGes.Rag.Chunking.Models;

public class LegalDocumentService
{
    private readonly IDocumentConverterFactory _converterFactory;
    private readonly IRagIngestionService _ingestionService;
    private readonly IRagStore _store;

    public async Task<IngestionResult> IngestarLeyAsync(
        string identidad,
        string filePath,
        string documentId)
    {
        // 1. Obtener o crear colección
        var collection = await _store.GetCollectionAsync("leyes-normativas");
        var collectionId = collection?.Id ?? await _store.CreateCollectionAsync(
            new RagCollectionDefinition
            {
                Name = "leyes-normativas",
                Description = "Colección de documentos legales y normativos",
                Dimensions = 3072,
                EmbeddingModel = "text-embedding-3-large"
            });

        // 2. Convertir PDF a Markdown
        var converter = _converterFactory.GetConverter(Path.GetExtension(filePath));
        var conversionResult = await converter.ConvertAsync(
            await File.ReadAllBytesAsync(filePath));

        if (!conversionResult.Success)
        {
            return IngestionResult.Failed(documentId, conversionResult.ErrorMessage!);
        }

        // 3. Ingestar documento (chunking + embedding + persistencia)
        var result = await _ingestionService.IndexDocumentAsync(
            identidad: identidad,
            collectionId: collectionId,
            documentId: documentId,
            markdownContent: conversionResult.MarkdownContent,
            docType: DocumentTypes.Normativa,  // Usa MarkdownStructureStrategy
            baseMetadata: new Dictionary<string, object>
            {
                ["source"] = filePath,
                ["doc_type"] = "Normativa",
                ["imported_at"] = DateTime.UtcNow.ToString("O")
            });

        // 4. Revisar resultado
        Console.WriteLine($"✅ Documento '{documentId}' ingestado:");
        Console.WriteLine($"   Chunks creados: {result.ChunksCreated}");
        Console.WriteLine($"   Tokens totales: {result.TotalTokens}");
        Console.WriteLine($"   Estrategia: {result.StrategyUsed}");
        Console.WriteLine($"   Tiempo total: {result.TotalTimeMs}ms");

        if (result.Warnings.Any())
        {
            Console.WriteLine($"   ⚠️ Warnings: {string.Join(", ", result.Warnings)}");
        }

        // Muestra ejemplo de rutas jerárquicas extraídas
        foreach (var path in result.SampleContextPaths)
        {
            Console.WriteLine($"   📄 {path}");
        }

        return result;
    }
}

Reindexar Documentos

Cuando un documento se actualiza, usa ReindexDocumentAsync:

public async Task<IngestionResult> ActualizarDocumentoAsync(
    int clientId,
    string identidad,
    Guid collectionId,
    string documentId,
    string nuevoMarkdown)
{
    var request = new IngestionRequest
    {
        ClientId = clientId,        // Nivel 1: Cliente API
        Identidad = identidad,      // Nivel 2: Entidad de datos
        CollectionId = collectionId,
        DocumentId = documentId,
        MarkdownContent = nuevoMarkdown,
        DocType = DocumentTypes.Normativa
    };

    // Elimina chunks anteriores y crea nuevos
    var result = await _ingestionService.ReindexDocumentAsync(request);

    Console.WriteLine($"Chunks eliminados: {result.ChunksDeleted}");
    Console.WriteLine($"Chunks nuevos: {result.ChunksCreated}");

    return result;
}

Métricas de Ingestión

El resultado incluye métricas detalladas:

public class IngestionResult
{
    bool Success { get; }                  // Éxito de la operación
    string DocumentId { get; }             // ID del documento procesado
    int ChunksCreated { get; }             // Chunks creados
    int TotalTokens { get; }               // Total de tokens
    double AverageTokensPerChunk { get; }  // Media por chunk
    string StrategyUsed { get; }           // "MarkdownStructure" o "RecursiveFallback"
    bool UsedFallback { get; }             // Si se usó estrategia fallback
    long ChunkingTimeMs { get; }           // Tiempo de chunking
    long EmbeddingTimeMs { get; }          // Tiempo de embedding
    long StorageTimeMs { get; }            // Tiempo de persistencia
    long TotalTimeMs { get; }              // Tiempo total
    int EmbeddingBatches { get; }          // Lotes de embedding procesados
    IReadOnlyList<string> Warnings { get; }        // Advertencias
    IReadOnlyList<string> SampleContextPaths { get; }  // Ejemplo de rutas
}

Dry-Run para Validación

Puedes validar el chunking sin persistir usando DryRun:

var request = new IngestionRequest
{
    ClientId = 1,          // Nivel 1: Cliente API
    Identidad = "123",     // Nivel 2: Entidad de datos
    CollectionId = collectionId,
    DocumentId = "test-doc",
    MarkdownContent = markdown,
    DocType = DocumentTypes.Normativa,
    DryRun = true  // ⚠️ No persiste, solo simula
};

var result = await _ingestionService.IndexDocumentAsync(request);

// Inspecciona el resultado sin haber modificado la BD
Console.WriteLine($"Se crearían {result.ChunksCreated} chunks");
Console.WriteLine($"Estrategia que se usaría: {result.StrategyUsed}");

🔍 Búsqueda y Respuesta RAG

Hacer Preguntas con RAG

El orquestador combina busqueda + generacion automaticamente:

using NeuraNetGes.Rag.Interfaces;
using NeuraNetGes.Rag.Models;

public class QAService
{
    private readonly IRagOrchestrator _orchestrator;

    public async Task<string> PreguntarAsync(
        int clientId,
        string identidad,
        Guid collectionId,
        string pregunta)
    {
        var question = new RagQuestion
        {
            ClientId = clientId,        // Nivel 1: Cliente API
            Identidad = identidad,      // Nivel 2: Entidad de datos
            CollectionId = collectionId,
            Query = pregunta,

            // Configuracion del LLM
            Provider = LLMProvider.OpenAI,
            Model = "gpt-5.2-mini",
            MaxTokens = 1024,
            Temperature = 0.2,  // Bajo para respuestas mas precisas

            // Recuperacion
            TopK = 8,  // Chunks a recuperar
            IncludeCitations = true,  // Incluir citas [doc:X, chunk:Y]

            // System prompt personalizado (opcional)
            SystemPrompt = @"Eres un asistente experto en la documentacion interna.
Responde de forma clara y concisa.
Si no encuentras la informacion, dilo explicitamente."
        };

        var answer = await _orchestrator.AnswerAsync(question);

        // Informacion adicional disponible
        Console.WriteLine($"Chunks usados: {answer.UsedChunks.Count}");
        Console.WriteLine($"Tokens: {answer.RawLLMResponse?.TotalTokens}");

        return answer.Answer;
    }
}

Sistema de Citas Automáticas

El orquestador inyecta el contexto en un formato optimizado para citación:

[FUENTE: {DocumentId} | SECCIÓN: {LocalTitle}]
{Contenido del chunk}

Cuando IncludeCitations = true, el LLM recibe instrucciones para citar así:

Ejemplo de respuesta generada:

Según la Ley de Transparencia, el objeto de la misma es "ampliar y reforzar
la transparencia de la actividad pública" [Ley 30/2025 > Artículo 1].

Los sujetos obligados incluyen la Administración General del Estado y las
Comunidades Autónomas [Ley 30/2025 > Artículo 2].

Prompt inyectado al LLM:

Usa el siguiente contexto para responder:

[FUENTE: ley-transparencia-2025 | SECCIÓN: Ley 30/2025 > Título I > Artículo 1]
Artículo 1. Objeto.
Esta ley tiene por objeto ampliar y reforzar la transparencia...

---
Cuando cites información, usa el formato: [Documento > Sección]
Ejemplo: Según [Ley 30/2025 > Artículo 4], ...

PREGUNTA:
¿Cuál es el objeto de la Ley de Transparencia?

Filtros Avanzados por Metadatos

El sistema soporta múltiples estrategias de filtrado:

var question = new RagQuestion
{
    ClientId = 1,          // Nivel 1: Cliente API
    Identidad = "42",      // Nivel 2: Entidad de datos
    CollectionId = collectionId,
    Query = "¿Qué obligaciones de publicidad activa establece la ley?",

    Filters = new RagSearchFilters
    {
        // 1. Filtro JSONB con operador @> (más eficiente con índice GIN)
        MetadataContains = new Dictionary<string, object>
        {
            ["doc_type"] = "Normativa",
            ["year"] = 2025
        },

        // 2. Filtro por igualdad exacta (legacy)
        MetadataEquals = new Dictionary<string, string>
        {
            ["author"] = "Congreso"
        },

        // 3. Filtrar por documentos específicos
        DocumentIds = new[] { "ley-transparencia-2025", "ley-datos-2024" },

        // 4. Score mínimo de similitud (0.0 a 1.0)
        MinScore = 0.7
    }
};

var answer = await _orchestrator.AnswerAsync(question);

// Información de contexto disponible en la respuesta
Console.WriteLine($"Chunks recuperados: {answer.ChunksRetrieved}");
Console.WriteLine($"¿Tiene contexto?: {answer.HasContext}");
Console.WriteLine($"Tiempo de procesamiento: {answer.ProcessingTimeMs}ms");
Console.WriteLine($"System prompt usado: {answer.SystemPromptUsed}");

Tipos de filtros:

Filtro Operador SQL Uso Eficiencia
MetadataContains @> (JSONB) Múltiples campos simultáneos Alta (usa índice GIN)
MetadataEquals ->>'key' = 'value' Filtro exacto simple Media
DocumentIds = ANY(...) Limitar a documentos específicos Alta
MinScore >= valor Calidad mínima de resultados Alta

Busqueda Directa

Si solo necesitas buscar chunks sin generar respuesta:

using NeuraNetGes.Rag.Interfaces;
using NeuraNetGes.Rag.Models;

public class SearchService
{
    private readonly IRagRetriever _retriever;

    public async Task<IReadOnlyList<RagSearchResult>> BuscarAsync(
        int clientId,
        string identidad,
        Guid collectionId,
        string query,
        int topK = 10)
    {
        var request = new RagRetrievalRequest
        {
            ClientId = clientId,        // Nivel 1: Cliente API
            Identidad = identidad,      // Nivel 2: Entidad de datos
            CollectionId = collectionId,
            Query = query,
            TopK = topK
        };

        var result = await _retriever.RetrieveAsync(request);

        foreach (var chunk in result.Results)
        {
            Console.WriteLine($"Score: {chunk.Score:F3}");
            Console.WriteLine($"Doc: {chunk.DocumentId}, Chunk: {chunk.ChunkIndex}");
            Console.WriteLine($"Contenido: {chunk.Content.Substring(0, 100)}...");
            Console.WriteLine();
        }

        return result.Results;
    }
}

Ejemplo Completo: Sistema de FAQ

public class FAQSystem
{
    private readonly IRagStore _store;
    private readonly IEmbeddingService _embedding;
    private readonly IRagOrchestrator _orchestrator;

    // 1. Configurar coleccion
    public async Task<Guid> ConfigurarAsync()
    {
        return await _store.CreateCollectionAsync(new RagCollectionDefinition
        {
            Name = "faq-soporte",
            Description = "Preguntas frecuentes de soporte tecnico",
            Dimensions = 3072,
            EmbeddingModel = "text-embedding-3-large"
        });
    }

    // 2. Indexar preguntas y respuestas
    public async Task IndexarFAQAsync(int clientId, string identidad, Guid collectionId,
        List<(string pregunta, string respuesta)> faqs)
    {
        var chunks = new List<RagChunk>();

        for (int i = 0; i < faqs.Count; i++)
        {
            var (pregunta, respuesta) = faqs[i];
            var texto = $"Pregunta: {pregunta}\n\nRespuesta: {respuesta}";

            var embedding = await _embedding.EmbedAsync(texto);

            chunks.Add(new RagChunk
            {
                ClientId = clientId,        // Nivel 1: Cliente API
                Identidad = identidad,      // Nivel 2: Entidad de datos
                DocumentId = $"faq-{i}",
                ChunkIndex = 0,
                Content = texto,
                LocalTitle = pregunta,
                Metadata = new Dictionary<string, string>
                {
                    ["tipo"] = "faq",
                    ["categoria"] = "soporte"
                },
                Embedding = embedding
            });
        }

        await _store.IndexChunksAsync(clientId, identidad, collectionId, chunks);
    }

    // 3. Responder preguntas
    public async Task<string> ResponderAsync(int clientId, string identidad, Guid collectionId, string pregunta)
    {
        var answer = await _orchestrator.AnswerAsync(new RagQuestion
        {
            ClientId = clientId,        // Nivel 1: Cliente API
            Identidad = identidad,      // Nivel 2: Entidad de datos
            CollectionId = collectionId,
            Query = pregunta,
            Provider = LLMProvider.OpenAI,
            Model = "gpt-5.2-mini",  // Modelo rapido para FAQ
            TopK = 3,
            Temperature = 0.1,
            SystemPrompt = @"Eres un asistente de soporte tecnico.
Responde basandote en las FAQ proporcionadas.
Se breve y directo."
        });

        return answer.Answer;
    }
}

Arquitectura del Sistema RAG

┌─────────────────┐
│   Tu App        │
└────────┬────────┘
         │
         ↓
┌─────────────────┐     ┌─────────────────┐
│ IRagOrchestrator│────→│IGuardedLLMService│
└────────┬────────┘     └─────────────────┘
         │
         ↓
┌─────────────────┐
│  IRagRetriever  │
└────────┬────────┘
         │
    ┌────┴────┐
    ↓         ↓
┌─────────┐ ┌─────────┐
│Embedding│ │ IRagStore│
│ Service │ │(pgvector)│
└─────────┘ └─────────┘

Flujo de una pregunta:

  1. Usuario hace pregunta → IRagOrchestrator
  2. Orquestador llama a IRagRetriever.RetrieveAsync()
  3. Retriever genera embedding de la pregunta con IEmbeddingService
  4. Retriever busca en IRagStore (PostgreSQL + pgvector)
  5. Retriever devuelve chunks relevantes
  6. Orquestador construye prompt con contexto
  7. Orquestador llama a IGuardedLLMService (con guardrails)
  8. LLM genera respuesta basada en el contexto
  9. Orquestador devuelve RagAnswer con respuesta y fuentes

Best Practices para RAG

  1. Calidad de chunks: Trocear documentos en partes semanticamente coherentes (parrafos completos, secciones logicas)
  2. Tamano de chunks: 200-500 tokens es ideal. Muy pequeno pierde contexto, muy grande diluye relevancia
  3. Overlap: Considera solapamiento entre chunks para no perder contexto en los bordes
  4. Metadatos: Usa metadatos para filtrar (categoria, version, fecha)
  5. TopK apropiado: Empieza con 5-10, ajusta segun calidad de respuestas
  6. Temperature baja: Usa 0.1-0.3 para respuestas mas precisas y menos creativas
  7. System prompt claro: Instruye al LLM a no inventar y citar fuentes

📋 Modelos Recomendados

OpenAI

  • Chat: gpt-5.2, gpt-5.2-mini, gpt-5.2-nano
  • Razonamiento: o1, o3
  • Imágenes: dall-e-3
  • Vision: gpt-5.2, gpt-5.2-mini

Google Gemini

  • Chat: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite
  • Vision: gemini-2.5-pro, gemini-2.5-flash

Anthropic Claude

  • Chat: claude-3-5-sonnet-20241022, claude-3-opus-20240229
  • Vision: claude-3-5-sonnet-20241022

xAI Grok

  • Chat: grok-4-fast-non-reasoning, grok-4-fast-non-reasoning-latest
  • Vision: grok-4-fast-non-reasoning (soporta imágenes)
  • Notas:
    • Compatible con API de OpenAI (endpoint: https://api.x.ai/v1)
    • Ventana de contexto: 2M tokens
    • No soporta generación de imágenes

🏗️ Arquitectura del Proyecto

Componentes Core

NeuraNetGes/
├── Attributes.cs              # Modelos core (GenericRequest, GenericResponse)
├── LLM/
│   ├── Interfaces/           # ILLMService - Interfaz base LLM
│   ├── LLMService.cs         # Implementación LLM principal
│   └── GuardedLLMService.cs  # LLM con guardrails integrado
├── Helpers/
│   └── JsonSchemaHelper.cs   # Utilidades JSON
├── Guards/
│   ├── Interfaces/           # Interfaces de guards
│   ├── Models/               # Modelos (GuardResult, RetryStrategy)
│   ├── PrePrompt/            # Guards de entrada
│   ├── PostOutput/           # Guards de salida
│   └── PromptBuilder/        # Constructor de prompts
├── Contexto/
│   ├── Interfaces/           # Interfaces de contexto
│   ├── Modelos/              # Modelos (TurnoContexto, MensajeContexto)
│   ├── Helpers/              # Helpers (TokenizadorHelper)
│   ├── Services/             # ConversacionManager
│   └── Postgres/             # Implementación PostgreSQL
├── Tools/
│   ├── Interfaces/           # IToolRegistry, IToolExecutor, IToolAdapter
│   ├── Models/               # LLMToolDescriptor, ToolResult, etc.
│   ├── Registry/             # ToolRegistry - Registro de herramientas
│   ├── Execution/            # ToolExecutor - Ejecutor con políticas
│   ├── Adapters/             # Adaptadores por proveedor (OpenAI, Anthropic, etc.)
│   └── Examples/             # Herramientas de ejemplo
└── Rag/
    ├── Interfaces/           # IEmbeddingService, IRagStore, IRagRetriever, IRagOrchestrator
    ├── Models/               # RagChunk, RagQuestion, RagAnswer, etc.
    ├── Embeddings/           # OpenAIEmbeddingService
    ├── Postgres/             # PostgresRagStore (pgvector)
    └── Orchestration/        # DefaultRagOrchestrator

Dependencias Principales

  • Anthropic.SDK (5.8.0) - Para Claude
  • OpenAI (2.6.0) - Para OpenAI y Gemini (vía API compatible)
  • Newtonsoft.Json (13.0.4) - Serialización JSON
  • Gesgocom.MicroOrmGesg (1.0.2) - ORM para PostgreSQL
  • Microsoft.Extensions.DependencyInjection (9.0.10) - Inyección de dependencias
  • Microsoft.Extensions.Hosting.Abstractions (9.0.10) - Abstracciones de hosting
  • SharpToken (2.0.4) - Tokenización para conteo de tokens
  • Npgsql (9.0.4) - Driver PostgreSQL
  • Dapper (2.1.66) - Micro ORM

Lifetimes de Servicios

Es importante entender los lifetimes de los servicios registrados:

Core LLM

  • ILLMServiceSingleton: Reutilizable, sin estado
  • IGuardedLLMServiceScoped: Contiene guards que pueden tener estado

Sistema de Contexto

  • IAlmacenContextoScoped: Usa IDbSession (conexiones de BD)
  • IConversacionesServiceScoped: Usa IAlmacenContexto
  • IConversacionManagerScoped: Usa IGuardedLLMService
  • SchemaValidatorScoped: Usa IDbSession

Sistema RAG

  • IEmbeddingServiceSingleton: Thread-safe, sin estado
  • IRagStoreScoped: Usa IDbSession (conexiones de BD)
  • IRagRetrieverScoped: Usa IRagStore
  • IRagOrchestratorScoped: Usa IRagRetriever + IGuardedLLMService

Importante: IDbSession (de MicroOrmGesg) es Scoped porque maneja conexiones de base de datos que deben vivir durante una request HTTP. Por eso todos los servicios de contexto y RAG son también Scoped.

HostedServices

  • SchemaValidationHostedServiceSingleton (como todos los HostedServices)
    • Crea scopes manuales para resolver SchemaValidator (que es Scoped)
    • Esto es el patrón estándar para HostedServices que necesitan servicios Scoped

Ejemplo de creación de scope manual (si necesitas validar fuera de startup):

public class MyBackgroundService : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        // Crear scope para servicios Scoped
        using var scope = _serviceProvider.CreateScope();
        var validator = scope.ServiceProvider.GetRequiredService<SchemaValidator>();

        await validator.ValidarSchemaAsync(throwOnError: false, ct);
    }
}

🛠️ Manejo de Errores

try
{
    var response = await _llmService.ChatCompletionAsync(request);

    if (!response.Success)
    {
        Console.WriteLine($"Error: {response.ErrorMessage}");
    }
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Proveedor no configurado: {ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"Error inesperado: {ex.Message}");
}

Con Guardrails

var response = await guardedService.ChatCompletionAsync(request);

if (!response.AllGuardsPassed)
{
    var failures = response.GetFailedGuards();
    foreach (var (guardName, result) in failures)
    {
        if (result.Severity == GuardSeverity.High)
        {
            throw new SecurityException($"Guard crítico falló: {guardName}");
        }
    }
}

💡 Best Practices

General

  1. Valida disponibilidad de proveedores con IsProviderAvailable()
  2. Maneja errores apropiadamente (rate limits, timeouts, etc.)
  3. Usa structured output para datos estructurados
  4. Usa IGuardedLLMService en producción

Guardrails

  1. Asigna prioridades apropiadas (seguridad: 1-10, validación: 11-30, optimización: 31-50)
  2. Usa GuardFailureAction.Fix para problemas solucionables, Block para críticos
  3. Revisa metadatos y warnings
  4. Prueba guards con inputs maliciosos

Sistema de Contexto

  1. Ajusta ventanaTokens según caso de uso (3000-10000)
  2. Monitorea tokens en tiempo real
  3. Implementa reintentos con backoff exponencial
  4. Loguea todas las operaciones importantes

🐛 Troubleshooting

Error: "Cannot consume scoped service from singleton"

Síntoma:

System.InvalidOperationException: Cannot consume scoped service 'MicroOrmGesg.Interfaces.IDbSession'
from singleton 'NeuraNetGes.Contexto.Postgres.SchemaValidator'

Causa: Intentas inyectar un servicio Scoped (IDbSession) en un servicio Singleton.

Solución: Este error ya está corregido en la versión 1.0.5+. Asegúrate de usar la versión correcta:

dotnet add package Gesgocom.NeuraNetGes --version 1.0.5

Si tienes un servicio personalizado que usa SchemaValidator, recuerda:

  • SchemaValidator es Scoped (no Singleton)
  • Si lo usas en un HostedService, crea un scope manual:
using var scope = _serviceProvider.CreateScope();
var validator = scope.ServiceProvider.GetRequiredService<SchemaValidator>();
await validator.ValidarSchemaAsync(throwOnError: false);

Guards bloqueando requests legítimos

Solución: Ajusta el threshold o usa bypass selectivo con precaución.

Performance lento

Solución:

  1. Reduce número de guards
  2. Usa modelos más rápidos (gpt-5.2-nano, gemini-2.5-flash-lite)
  3. Optimiza guards personalizados

Ventana se llena rápido

Solución:

  1. Aumenta ventanaTokens
  2. Usa modelos con ventanas más grandes
  3. Implementa resumen del pasado

Error: jsonb_insert() does not exist

Solución: Tu PostgreSQL es anterior a versión 16. Ejecuta el script de corrección que usa el operador || compatible con PostgreSQL 9.5+.


🔐 Seguridad

  • Nunca incluyas API Keys en el código
  • Usa appsettings.json con User Secrets en desarrollo
  • En producción, usa Azure Key Vault o servicios similares
  • Usa IGuardedLLMService para prevenir vulnerabilidades

🔨 Build y Desarrollo

Build del proyecto

dotnet build NeuraNetGes.sln

Build en Release (genera NuGet)

dotnet build NeuraNetGes.sln -c Release

Ejecutar tests

dotnet test

Pack del NuGet manualmente

dotnet pack NeuraNetGes/NeuraNetGes.csproj -c Release

📚 Scripts de Base de Datos

Tablas PostgreSQL (Sistema de Contexto)

-- 1. Tabla de conversaciones
CREATE TABLE IF NOT EXISTS conversacion (
    id                   uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    entidad_id           integer NOT NULL REFERENCES entidades(id) ON DELETE CASCADE,
    usuario_id           uuid NULL,
    titulo               text NULL,
    proposito            text NULL,
    ventana_tokens_max   integer NOT NULL DEFAULT 6000,
    turnos_max           integer NOT NULL DEFAULT 40,
    dias_retencion       integer NOT NULL DEFAULT 90,
    creado_en            timestamptz NOT NULL DEFAULT now(),
    actualizado_en       timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS ix_conversacion_entidad ON conversacion(entidad_id);
CREATE INDEX IF NOT EXISTS ix_conversacion_actualizado ON conversacion(actualizado_en DESC);

-- 2. Tabla de turnos
CREATE TABLE IF NOT EXISTS turno (
    id               uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    conversacion_id  uuid NOT NULL REFERENCES conversacion(id) ON DELETE CASCADE,
    proveedor        text NOT NULL,
    modelo           text NOT NULL,
    iniciado_en      timestamptz NOT NULL DEFAULT now(),
    finalizado_en    timestamptz NULL,
    estado           text NOT NULL CHECK (estado IN ('ok','error','timeout')),
    tokens_entrada   integer NOT NULL DEFAULT 0,
    tokens_salida    integer NOT NULL DEFAULT 0,
    coste_centimos   integer NOT NULL DEFAULT 0,
    latencia_ms      integer NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS ix_turno_conv_iniciado ON turno(conversacion_id, iniciado_en DESC);

-- 3. Tabla de mensajes
CREATE TABLE IF NOT EXISTS mensaje (
    id                  uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    conversacion_id     uuid NOT NULL REFERENCES conversacion(id) ON DELETE CASCADE,
    turno_id            uuid NOT NULL REFERENCES turno(id) ON DELETE CASCADE,
    rol                 text NOT NULL CHECK (rol IN ('system','user','assistant','tool')),
    content_parts       jsonb NOT NULL,
    creado_en           timestamptz NOT NULL DEFAULT now(),
    redaccion_aplicada  boolean NOT NULL DEFAULT false,
    tokens_estimados    integer NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS ix_mensaje_conv_creado ON mensaje(conversacion_id, creado_en DESC);

-- 4. Tabla de ventana de contexto
CREATE TABLE IF NOT EXISTS ventana_contexto (
    conversacion_id       uuid PRIMARY KEY REFERENCES conversacion(id) ON DELETE CASCADE,
    ultimo_turno_id       uuid NULL REFERENCES turno(id) ON DELETE CASCADE,
    mensajes_ventana      jsonb NOT NULL,
    tokens_entrada_total  integer NOT NULL,
    resumen_pasado        text NULL,
    actualizado_en        timestamptz NOT NULL DEFAULT now()
);

-- 5. Tabla de observaciones
CREATE TABLE IF NOT EXISTS observacion (
    id               uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    conversacion_id  uuid NOT NULL REFERENCES conversacion(id) ON DELETE CASCADE,
    turno_id         uuid NULL REFERENCES turno(id) ON DELETE SET NULL,
    autor            text NOT NULL CHECK (autor IN ('system','user','evaluator','pipeline')),
    tipo             text NOT NULL CHECK (tipo IN ('note','issue','decision','feedback','eval')),
    visibilidad      text NOT NULL CHECK (visibilidad IN ('private','team','public')),
    texto            text NOT NULL,
    etiquetas        text[] NOT NULL DEFAULT '{}',
    adjuntos_uri     text[] NOT NULL DEFAULT '{}',
    creado_en        timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS ix_observacion_conv_creado ON observacion(conversacion_id, creado_en DESC);

-- 6. Trigger FIFO para observaciones (máximo 50)
CREATE OR REPLACE FUNCTION observacion_fifo_enforce() RETURNS trigger
    LANGUAGE plpgsql AS $$
BEGIN
    WITH ranked AS (
        SELECT id, row_number() OVER (PARTITION BY conversacion_id ORDER BY creado_en DESC) AS rn
        FROM observacion
        WHERE conversacion_id = NEW.conversacion_id
    )
    DELETE FROM observacion o
        USING ranked r
    WHERE o.id = r.id AND r.rn > 50 AND o.conversacion_id = NEW.conversacion_id;
    RETURN NEW;
END$$;

DROP TRIGGER IF EXISTS trg_observacion_fifo ON observacion;
CREATE TRIGGER trg_observacion_fifo
    AFTER INSERT ON observacion
    FOR EACH ROW EXECUTE FUNCTION observacion_fifo_enforce();

-- 7. Función para reconstruir ventana (compatible PostgreSQL 9.5+)
CREATE OR REPLACE FUNCTION reconstruir_ventana_contexto(
    p_conversacion uuid,
    p_ultimo_turno uuid,
    p_presupuesto_tokens int,
    p_resumen_pasado text DEFAULT NULL
) RETURNS void
    LANGUAGE plpgsql AS $$
DECLARE
    acumulado int := 0;
    msgs jsonb := '[]'::jsonb;
    r record;
BEGIN
    PERFORM pg_advisory_xact_lock( ('x'||substr(md5(p_conversacion::text),1,16))::bit(64)::bigint );

    FOR r IN
        SELECT id, rol, content_parts, tokens_estimados
        FROM mensaje
        WHERE conversacion_id = p_conversacion
        ORDER BY creado_en ASC, id ASC
        LOOP
            IF acumulado + r.tokens_estimados > p_presupuesto_tokens THEN
                EXIT;
            END IF;

            msgs := msgs || jsonb_build_array(
                            jsonb_build_object(
                                'id', r.id,
                                'rol', r.rol,
                                'content_parts', r.content_parts,
                                'tokens_estimados', r.tokens_estimados
                            )
                    );
            acumulado := acumulado + r.tokens_estimados;
        END LOOP;

    INSERT INTO ventana_contexto(conversacion_id, ultimo_turno_id, mensajes_ventana, tokens_entrada_total, resumen_pasado, actualizado_en)
    VALUES(p_conversacion, p_ultimo_turno, msgs, acumulado, p_resumen_pasado, now())
    ON CONFLICT (conversacion_id) DO UPDATE
        SET ultimo_turno_id = EXCLUDED.ultimo_turno_id,
            mensajes_ventana = EXCLUDED.mensajes_ventana,
            tokens_entrada_total = EXCLUDED.tokens_entrada_total,
            resumen_pasado = COALESCE(EXCLUDED.resumen_pasado, ventana_contexto.resumen_pasado),
            actualizado_en = now();
END$$;

-- 8. Función para crear conversación
CREATE OR REPLACE FUNCTION crear_conversacion_desde_cero(
    p_entidad_id        integer,
    p_titulo            text DEFAULT NULL,
    p_proposito         text DEFAULT NULL,
    p_ventana_tokens    integer DEFAULT 6000,
    p_turnos_max        integer DEFAULT 40,
    p_dias_retencion    integer DEFAULT 90,
    p_mensaje_sistema   text DEFAULT NULL
)
    RETURNS TABLE (id_conversacion uuid, id_turno_inicial uuid)
    LANGUAGE plpgsql AS $$
DECLARE
    v_conv_id uuid := gen_random_uuid();
    v_turno_id uuid := NULL;
    v_token_est int := 0;
BEGIN
    INSERT INTO conversacion(id, entidad_id, titulo, proposito, ventana_tokens_max, turnos_max, dias_retencion)
    VALUES (v_conv_id, p_entidad_id, p_titulo, p_proposito, p_ventana_tokens, p_turnos_max, p_dias_retencion);

    IF p_mensaje_sistema IS NOT NULL AND length(trim(p_mensaje_sistema)) > 0 THEN
        v_turno_id := gen_random_uuid();

        INSERT INTO turno(id, conversacion_id, proveedor, modelo, estado, tokens_entrada, tokens_salida, coste_centimos, latencia_ms, finalizado_en)
        VALUES (v_turno_id, v_conv_id, 'init', 'system', 'ok', 0, 0, 0, 0, now());

        v_token_est := GREATEST(1, ceil(length(p_mensaje_sistema)::numeric / 4));

        INSERT INTO mensaje(id, conversacion_id, turno_id, rol, content_parts, tokens_estimados)
        VALUES (
                   gen_random_uuid(),
                   v_conv_id,
                   v_turno_id,
                   'system',
                   jsonb_build_object('partes', jsonb_build_array(jsonb_build_object('type','text','value', p_mensaje_sistema))),
                   v_token_est
               );

        PERFORM reconstruir_ventana_contexto(v_conv_id, v_turno_id, p_ventana_tokens, NULL);
    END IF;

    RETURN QUERY SELECT v_conv_id, v_turno_id;
END$$;

🛠️ Sistema de Function Calling / Tools

¿Qué son Tools/Function Calling?

El sistema de Function Calling (también llamado Tools) permite que el LLM pueda ejecutar funciones externas para realizar tareas que van más allá de generar texto. Por ejemplo:

  • Buscar información en bases de datos
  • Realizar cálculos complejos
  • Consultar APIs externas
  • Obtener la fecha/hora actual
  • Enviar emails o notificaciones
  • Cualquier operación programática que necesites

NeuraNetGes proporciona una implementación unificada que funciona con los 5 proveedores (OpenAI, Google Gemini, Anthropic Claude, xAI Grok y Groq), gestionando automáticamente las diferencias de cada API.

Configuración del Sistema de Tools

Ya vimos la configuración básica en la sección de Configuración. Aquí el resumen:

using NeuraNetGes.Tools.Interfaces;
using NeuraNetGes.Tools.Registry;
using NeuraNetGes.Tools.Execution;

// En Program.cs
builder.Services.AddSingleton<IToolRegistry, ToolRegistry>();
builder.Services.AddScoped<IToolExecutor, ToolExecutor>();
builder.Services.AddMemoryCache(); // Para caching de resultados

// Registrar herramientas de ejemplo
var serviceProvider = builder.Services.BuildServiceProvider();
var toolRegistry = serviceProvider.GetRequiredService<IToolRegistry>();
NeuraNetGes.Tools.Examples.ExampleTools.RegisterAll(toolRegistry);

Crear una Herramienta Personalizada

Crear una tool es muy sencillo. Solo necesitas:

  1. Un nombre descriptivo
  2. Una descripción de lo que hace (el LLM usa esto para decidir cuándo llamarla)
  3. Un schema JSON describiendo los parámetros
  4. Una función que ejecute la lógica

Ejemplo: Herramienta para buscar el clima

using NeuraNetGes.Tools.Models;
using NeuraNetGes.Tools.Helpers;
using Newtonsoft.Json.Linq;

public static class WeatherTools
{
    public static void Register(IToolRegistry registry)
    {
        var weatherTool = new LLMToolDescriptor
        {
            Name = "get_weather",
            Description = "Obtiene el clima actual de una ciudad específica",

            // Schema de parámetros (el LLM necesita saber qué enviar)
            ParametersSchema = ToolHelpers.CreateParametersSchema(new Dictionary<string, object>
            {
                ["type"] = "object",
                ["properties"] = new Dictionary<string, object>
                {
                    ["city"] = new Dictionary<string, object>
                    {
                        ["type"] = "string",
                        ["description"] = "Nombre de la ciudad"
                    },
                    ["unit"] = new Dictionary<string, object>
                    {
                        ["type"] = "string",
                        ["enum"] = new[] { "celsius", "fahrenheit" },
                        ["description"] = "Unidad de temperatura"
                    }
                },
                ["required"] = new[] { "city" }
            }),

            // Clasificación de efectos secundarios
            SideEffects = ToolSideEffects.ReadOnly, // Solo lectura, seguro de ejecutar
            Idempotent = true, // Mismo input = mismo output (permite caching)
            Timeout = TimeSpan.FromSeconds(10),

            // La función que se ejecuta
            Executor = async (context) =>
            {
                // Extraer argumentos
                var city = context.Arguments["city"]?.ToString() ?? "Madrid";
                var unit = context.Arguments["unit"]?.ToString() ?? "celsius";

                // Aquí harías la llamada real a una API de clima
                // Simulamos la respuesta
                var temperature = unit == "celsius" ? 22 : 72;
                var result = new JObject
                {
                    ["city"] = city,
                    ["temperature"] = temperature,
                    ["unit"] = unit,
                    ["conditions"] = "Soleado",
                    ["humidity"] = 65
                };

                return new ToolResult
                {
                    CallId = context.Arguments["__call_id"]?.ToString() ?? Guid.NewGuid().ToString(),
                    Name = "get_weather",
                    Result = result,
                    Success = true,
                    ExecutionTimeMs = 150
                };
            }
        };

        registry.Register(weatherTool);
    }
}

Usar Tools con el LLM

Una vez registradas las herramientas, usarlas es muy sencillo:

using NeuraNetGes;
using NeuraNetGes.LLM.Interfaces;

public class WeatherService
{
    private readonly ILLMService _llmService;

    public WeatherService(ILLMService llmService)
    {
        _llmService = llmService;
    }

    public async Task<string> AskAboutWeather(string userQuestion)
    {
        var request = new GenericRequest
        {
            Provider = LLMProvider.OpenAI,
            Model = "gpt-4",
            Prompt = userQuestion,

            // Habilitar tools
            ToolsEnabled = true,

            // Opcional: especificar qué tools usar (si no se especifica, usa todas)
            ToolNames = new[] { "get_weather" },

            // Ejecución automática (el LLM llamará tools hasta completar)
            AutoExecuteTools = true,
            MaxToolIterations = 5 // Máximo de loops
        };

        var response = await _llmService.ChatCompletionAsync(request);

        // La respuesta incluye información sobre las tools ejecutadas
        Console.WriteLine($"Tools ejecutadas: {response.ToolIterations}");

        if (response.ToolCalls != null)
        {
            foreach (var call in response.ToolCalls)
            {
                Console.WriteLine($"- {call.Name}: {call.Arguments}");
            }
        }

        return response.Content ?? "No response";
    }
}

// Uso:
var service = new WeatherService(llmService);
var answer = await service.AskAboutWeather("¿Qué tiempo hace en Madrid?");
// El LLM automáticamente:
// 1. Detecta que necesita llamar a get_weather
// 2. Extrae "Madrid" de la pregunta
// 3. Ejecuta la tool
// 4. Usa el resultado para generar una respuesta natural
Console.WriteLine(answer);
// Output: "En Madrid hace 22°C y está soleado, con una humedad del 65%."

Clasificación de Side Effects

Las herramientas se clasifican según sus efectos secundarios. Esto permite control fino sobre qué puede ejecutarse:

public enum ToolSideEffects
{
    None = 0,              // Sin efectos (ej: cálculos matemáticos)
    ReadOnly = 1,          // Solo lectura (ej: consultar DB, APIs)
    WriteLocal = 2,        // Escribe localmente (ej: archivos temporales)
    WriteNetwork = 4,      // Escribe en red (ej: enviar email, actualizar DB)
    Irreversible = 8       // Operación crítica (ej: eliminar datos, transacciones)
}

Ejemplo de uso:

var safeTool = new LLMToolDescriptor
{
    Name = "calculate",
    Description = "Realiza cálculos matemáticos",
    SideEffects = ToolSideEffects.None, // Totalmente seguro
    Idempotent = true
    // ...
};

var dangerousTool = new LLMToolDescriptor
{
    Name = "delete_user",
    Description = "Elimina un usuario del sistema",
    SideEffects = ToolSideEffects.Irreversible, // ¡Peligro!
    RequiresConfirmation = true // Requiere aprobación manual
    // ...
};

Políticas de Ejecución

Puedes configurar políticas para controlar cómo se ejecutan las herramientas:

using NeuraNetGes.Tools.Models;

var policy = new ToolExecutionPolicy
{
    // Máximo de tools ejecutándose en paralelo
    MaxParallelism = 5,

    // Habilitar reintentos automáticos para tools idempotentes
    EnableAutoRetry = true,
    MaxRetries = 2,

    // Habilitar caching de resultados (para tools idempotentes)
    EnableResultCaching = true,
    CacheTTL = TimeSpan.FromMinutes(5),

    // Control de permisos: qué side effects están permitidos
    // Por defecto: ReadOnly, WriteLocal y None
    RequireExplicitPermissions = true
};

// Usar la policy en ejecución manual
var executor = serviceProvider.GetRequiredService<IToolExecutor>();
var results = await executor.ExecuteBatchAsync(toolCalls, context, policy);

Ejemplos de Tools Comunes

1. Obtener Hora/Fecha Actual

var timeTool = new LLMToolDescriptor
{
    Name = "get_current_time",
    Description = "Obtiene la fecha y hora actual",
    ParametersSchema = ToolHelpers.CreateParametersSchema(new Dictionary<string, object>
    {
        ["type"] = "object",
        ["properties"] = new Dictionary<string, object>(),
        ["required"] = new string[0]
    }),
    SideEffects = ToolSideEffects.None,
    Idempotent = false, // ¡Importante! La hora cambia constantemente
    Executor = async (context) =>
    {
        var now = DateTime.Now;
        var result = new JObject
        {
            ["timestamp"] = now.ToString("o"),
            ["date"] = now.ToString("yyyy-MM-dd"),
            ["time"] = now.ToString("HH:mm:ss"),
            ["timezone"] = TimeZoneInfo.Local.DisplayName
        };

        return new ToolResult
        {
            CallId = context.Arguments["__call_id"]?.ToString() ?? Guid.NewGuid().ToString(),
            Name = "get_current_time",
            Result = result,
            Success = true
        };
    }
};

2. Buscar en Base de Datos

var searchTool = new LLMToolDescriptor
{
    Name = "search_products",
    Description = "Busca productos en el catálogo por nombre o categoría",
    ParametersSchema = ToolHelpers.CreateParametersSchema(new Dictionary<string, object>
    {
        ["type"] = "object",
        ["properties"] = new Dictionary<string, object>
        {
            ["query"] = new Dictionary<string, object>
            {
                ["type"] = "string",
                ["description"] = "Término de búsqueda"
            },
            ["category"] = new Dictionary<string, object>
            {
                ["type"] = "string",
                ["description"] = "Categoría opcional"
            },
            ["max_results"] = new Dictionary<string, object>
            {
                ["type"] = "integer",
                ["description"] = "Máximo de resultados",
                ["default"] = 10
            }
        },
        ["required"] = new[] { "query" }
    }),
    SideEffects = ToolSideEffects.ReadOnly,
    Idempotent = true,
    Executor = async (context) =>
    {
        var query = context.Arguments["query"]?.ToString();
        var maxResults = context.Arguments["max_results"]?.Value<int>() ?? 10;

        // Obtener DbContext del DI
        var dbContext = context.Services.GetRequiredService<MyDbContext>();

        var products = await dbContext.Products
            .Where(p => p.Name.Contains(query))
            .Take(maxResults)
            .Select(p => new { p.Id, p.Name, p.Price, p.Category })
            .ToListAsync(context.CancellationToken);

        var result = JArray.FromObject(products);

        return new ToolResult
        {
            CallId = context.Arguments["__call_id"]?.ToString() ?? Guid.NewGuid().ToString(),
            Name = "search_products",
            Result = result,
            Success = true
        };
    }
};

3. Enviar Email

var emailTool = new LLMToolDescriptor
{
    Name = "send_email",
    Description = "Envía un email a un destinatario",
    ParametersSchema = ToolHelpers.CreateParametersSchema(new Dictionary<string, object>
    {
        ["type"] = "object",
        ["properties"] = new Dictionary<string, object>
        {
            ["to"] = new Dictionary<string, object>
            {
                ["type"] = "string",
                ["description"] = "Email del destinatario"
            },
            ["subject"] = new Dictionary<string, object>
            {
                ["type"] = "string",
                ["description"] = "Asunto del email"
            },
            ["body"] = new Dictionary<string, object>
            {
                ["type"] = "string",
                ["description"] = "Contenido del email"
            }
        },
        ["required"] = new[] { "to", "subject", "body" }
    }),
    SideEffects = ToolSideEffects.WriteNetwork | ToolSideEffects.Irreversible,
    Idempotent = false, // Cada ejecución envía otro email
    RequiresConfirmation = true, // ¡Requiere aprobación!
    Executor = async (context) =>
    {
        var to = context.Arguments["to"]?.ToString();
        var subject = context.Arguments["subject"]?.ToString();
        var body = context.Arguments["body"]?.ToString();

        // Aquí usarías tu servicio de email
        var emailService = context.Services.GetRequiredService<IEmailService>();
        await emailService.SendAsync(to, subject, body, context.CancellationToken);

        return new ToolResult
        {
            CallId = context.Arguments["__call_id"]?.ToString() ?? Guid.NewGuid().ToString(),
            Name = "send_email",
            Result = new JObject { ["status"] = "sent", ["to"] = to },
            Success = true
        };
    }
};

Arquitectura del Sistema de Tools

┌─────────────────┐
│   LLM Service   │ (OpenAI/Grok/Anthropic/Gemini)
└────────┬────────┘
         │
         ↓
┌─────────────────┐
│  Tool Adapter   │ (Normaliza API específica de cada proveedor)
│   Factory       │
└────────┬────────┘
         │
    ┌────┴────┐
    ↓         ↓
┌─────────┐ ┌─────────────┐
│ Tool    │ │    Tool     │
│Registry │ │  Executor   │
└─────────┘ └─────────────┘
     │            │
     └────┬───────┘
          ↓
   ┌──────────────┐
   │  Your Tools  │ (Funciones personalizadas)
   └──────────────┘

Flujo de ejecución:

  1. Usuario hace pregunta → LLM Service
  2. LLM decide qué tools necesita → Tool Adapter extrae los calls
  3. Tool Executor valida permisos y ejecuta (paralelo si es posible)
  4. Resultados → Tool Adapter los formatea para el proveedor
  5. LLM recibe resultados → Genera respuesta final
  6. Si el LLM necesita más tools, el loop se repite (hasta MaxToolIterations)

📄 Licencia

MIT

👤 Autor

Jorge Jerez Sabater - Gesgocom

🔗 Enlaces

-- ============================================================================
-- SCRIPT DE CREACIÓN DE TABLAS RAG
-- ============================================================================

-- Eliminar tablas existentes (orden inverso por dependencias)
DROP TABLE IF EXISTS rag_chunk CASCADE;
DROP TABLE IF EXISTS rag_tipo CASCADE;
DROP TABLE IF EXISTS rag_coleccion CASCADE;

-- Habilitar extensión pgvector si no está habilitada
CREATE EXTENSION IF NOT EXISTS vector;

-- ============================================================================
-- TABLA: rag_tipo
-- Descripción: Catálogo de tipos de contenido indexados en el sistema RAG
-- ============================================================================
CREATE TABLE rag_tipo (
    id SERIAL PRIMARY KEY,
    tipo VARCHAR(255) NOT NULL UNIQUE,
    tabla VARCHAR(255) NOT NULL,
    creado_en TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Índices para rag_tipo
CREATE INDEX ix_rag_tipo_tipo ON rag_tipo(tipo);
CREATE INDEX ix_rag_tipo_tabla ON rag_tipo(tabla);

COMMENT ON TABLE rag_tipo IS 'Catálogo de tipos de contenido para el sistema RAG';
COMMENT ON COLUMN rag_tipo.tipo IS 'Nombre descriptivo del tipo de contenido (ej: noticia, normativa, trámite)';
COMMENT ON COLUMN rag_tipo.tabla IS 'Nombre de la tabla de base de datos asociada a este tipo';

-- ============================================================================
-- TABLA: rag_chunk
-- Descripción: Fragmentos de texto (chunks) con embeddings para búsqueda semántica
-- ============================================================================
CREATE TABLE rag_chunk (
    id SERIAL PRIMARY KEY,
    id_tipo INTEGER NOT NULL REFERENCES rag_tipo(id) ON DELETE CASCADE,
    id_registro INTEGER NOT NULL,
    url TEXT,
    id_entidad INTEGER NOT NULL REFERENCES entidades(id) ON DELETE CASCADE,
    indice_chunk INTEGER NOT NULL,
    contenido TEXT NOT NULL,
    titulo_local TEXT,
    resumen TEXT,
    metadatos JSONB NOT NULL DEFAULT '{}',
    embedding VECTOR(1536),
    creado_en TIMESTAMPTZ NOT NULL DEFAULT now(),
    actualizado_en TIMESTAMPTZ
);

-- Índices estándar para rag_chunk
CREATE INDEX ix_rag_chunk_tipo ON rag_chunk(id_tipo);
CREATE INDEX ix_rag_chunk_registro ON rag_chunk(id_registro);
CREATE INDEX ix_rag_chunk_entidad ON rag_chunk(id_entidad);
CREATE INDEX ix_rag_chunk_tipo_registro ON rag_chunk(id_tipo, id_registro);
CREATE INDEX ix_rag_chunk_url ON rag_chunk(url);

-- Índice para búsquedas en metadatos JSON
CREATE INDEX ix_rag_chunk_metadatos ON rag_chunk USING gin(metadatos);

-- Índice vectorial HNSW para búsqueda de similitud semántica
CREATE INDEX ix_rag_chunk_embedding ON rag_chunk
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- Comentarios de documentación
COMMENT ON TABLE rag_chunk IS 'Chunks de texto con embeddings para búsqueda semántica RAG';
COMMENT ON COLUMN rag_chunk.id_tipo IS 'Tipo de contenido al que pertenece este chunk';
COMMENT ON COLUMN rag_chunk.id_registro IS 'ID del registro en la tabla origen según el tipo';
COMMENT ON COLUMN rag_chunk.url IS 'URL de acceso directo a este contenido';
COMMENT ON COLUMN rag_chunk.id_entidad IS 'Entidad propietaria del contenido';
COMMENT ON COLUMN rag_chunk.indice_chunk IS 'Índice de orden del chunk dentro del documento original';
COMMENT ON COLUMN rag_chunk.contenido IS 'Texto del fragmento';
COMMENT ON COLUMN rag_chunk.titulo_local IS 'Título o encabezado del fragmento';
COMMENT ON COLUMN rag_chunk.resumen IS 'Resumen opcional del contenido del chunk';
COMMENT ON COLUMN rag_chunk.metadatos IS 'Información adicional en formato JSON';
COMMENT ON COLUMN rag_chunk.embedding IS 'Vector de embedding (1536 dimensiones - text-embedding-3-small)';

No packages depend on Gesgocom.NeuraNetGes.

Version Downloads Last updated
1.5.8 9 04/15/2026
1.5.7 9 04/15/2026
1.5.6 3 04/15/2026
1.5.5 1 04/15/2026
1.5.4 14 04/13/2026
1.5.3 5 04/13/2026
1.5.2 2 04/13/2026
1.5.1 5 04/10/2026
1.4.6 14 04/03/2026
1.4.5 3 04/03/2026
1.4.4 20 03/17/2026
1.4.3 7 03/16/2026
1.4.1 11 03/15/2026
1.4.0 2 03/15/2026
1.3.10 30 03/14/2026
1.3.9 4 03/14/2026
1.3.7 2 03/14/2026
1.3.6 28 03/03/2026
1.3.5 2 03/03/2026
1.3.4 2 03/03/2026
1.3.3 2 03/03/2026
1.3.2 1 03/03/2026
1.3.1 3 02/23/2026
1.2.1 1 02/22/2026
1.1.1 1 02/22/2026
1.0.49 1 02/22/2026
1.0.47 1 02/22/2026
1.0.46 4 02/09/2026
1.0.45 1 02/09/2026