Gesgocom.NeuraNetGes 1.1.1
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.1.0 (Enterprise RAG Update)
- ✅ Búsqueda Híbrida Integligente (Hybrid Search): Fusión algorítmica de búsqueda Semántica (Vectorial con
pgvector) y Textual Exacta (Full-Text Search contsvector) mediante Reciprocal Rank Fusion (RRF). - ✅ Recuperación Jerárquica (Small-to-Big): Indexación de Nodos Padre grandes, buscando por Nodos Hijo precisos, para maximizar exactitud de recuperación pero proveyendo contexto perimetrico enorme al LLM.
- ✅ Expansión de Consultas (Query Expansion): Reescritura semántica al-vuelo de preguntas cortas empleando una instancia del LLM local, emitiendo multi-queries simultáneas para mejorar severamente el Recall.
- ✅ Filtros Dinámicos (Self-Querying Agent): Traducción automatizada del lenguaje natural humano a consultas crudas SQL/JSONB (ej. extraer cláusulas
YearoCompany) antes de realizar la evaluación vectorial. - ✅ Graph RAG Experimental: Fase inicial construida en base a Nodos/Aristas (
rag_graph_node,rag_graph_edge) extraidos por el LLM en tiempo de ingesta, para habilitar futuras evaluaciones topológicas.
🌟 Novedades v1.0.51 (Marzo 2026)
- ✅ Suite de Seguridad Avanzada: Nueva suite de 13 tests de seguridad que validan inyecciones, PII y secretos con escenarios adversarios.
- ✅ Fortalecimiento de Guardrails: Mejora crítica en
InjectionDetectorGuardcon normalización de texto y patrones multilingües (ES/EN). - ✅ Redacción de Secretos Moderna: Soporte para nuevas claves
sk-proj-de OpenAI y protección mejorada para AWS, GitHub y Slack. - ✅ Conteo de Tokens Gemini Rápido: Refactorización del conteo de tokens en Gemini a llamadas REST nativas para mayor estabilidad y rendimiento.
- ✅ Estado Zero Warnings: Código 100% limpio de advertencias en librería y tests integrales.
🌟 Novedades v1.0.50 (Marzo 2026)
- ✅ Soporte Nativo de Tokens: Implementado conteo preciso de tokens para OpenAI, Gemini y Groq/Grok.
- ✅ Auditoría de Calidad RAG: Aplicación sistemática de
.ConfigureAwait(false)y mejoras en la conversión de documentos. - ✅ Guards con Inyección: Los guardrails ahora pueden acceder a servicios de infraestructura mediante
GuardContext. - ✅ TokenBudgetGuard Real: El control de presupuesto de tokens ahora utiliza métricas reales en lugar de estimaciones aproximadas.
🌟 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
HttpClientestático en Handlers para prevenir el agotamiento de sockets (socket exhaustion) bajo alta carga. - ✅ Paralelismo en Vision: Descarga paralela de imágenes mediante
Task.WhenAllen 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.jsono 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
IDataFunctionsy 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:
IDocumentConverterFactorypara 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 ayudamake pack- Genera paquete con nueva versiónmake install- Instala en proyecto destinomake version- Muestra versión actualmake 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_TARGETen 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
- Chat Completion Simple
- Chat Completion con Streaming
- Uso de SystemPrompt
- Generación de Imágenes
- Análisis de Imágenes (Vision)
- Análisis Multi-imagen
- Salida Estructurada en JSON
Sistema de Guardrails
- ¿Por qué usar Guardrails?
- Configuración Rápida de Guards
- Pre-Prompt Guards Disponibles
- Post-Output Guards Disponibles
- Prompt Builder Seguro
- Bypass de Guards (Casos Especiales)
- Crear Guards Personalizados
Sistema de Contexto (Conversaciones Persistentes)
- Configuración de Base de Datos
- Uso Básico del Contexto
- Casos de Uso Reales
- Tokenización y Costos
- Observaciones y Anotaciones
Sistema de Function Calling / Tools
- ¿Qué son Tools/Function Calling?
- Configuración del Sistema de Tools
- Crear una Herramienta Personalizada
- Usar Tools con el LLM
- Clasificación de Side Effects
- Políticas de Ejecución
- Ejemplos de Tools Comunes
Sistema RAG (Retrieval Augmented Generation)
- ¿Que es RAG?
- Configuracion del Sistema RAG
- Proveedores de Embeddings (OpenAI vs Google)
- Esquema de Base de Datos RAG
- Aviso Crítico: Dimensiones de Vectores
- Crear una Coleccion
📄 Conversión de Documentos (Nuevo!)
- Formatos Soportados
- Registrar Servicios de Conversión
- Convertir un PDF a Markdown
- Detección Automática de Estructura
🧩 Agente de Chunking Estructural (Nuevo!)
- Estrategias de Chunking
- MarkdownStructureStrategy para Documentos Legales
- RecursiveFallbackStrategy para Texto General
- Preservación de Jerarquía (Headers)
🔄 Pipeline de Ingestión (Nuevo!)
🔍 Búsqueda y Respuesta RAG
- Hacer Preguntas con RAG
- Sistema de Citas Automáticas
- Filtros Avanzados por Metadatos
- Busqueda Directa
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 Tunedgemma-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
ReasoningTokensen la respuesta. - Configuración de
ReasoningEffortLevel(Bajo, Medio, Alto) medianteNivelPensamiento. - Soporte para modo Strict en Structured Outputs con adherencia total al esquema.
- Soporte para
- Google Gemini:
- Habilitación de
ThinkingConfigpara modelos con capacidad de razonamiento visible. - Gestión automática de parámetros incompatibles (como Temperature en modo razonamiento).
- Habilitación de
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:
- Separa rol de tarea:
SystemPromptpara el ROL,Promptpara la TAREA - Sé específico: Define comportamiento, tono y restricciones
- 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:
- Indexa tus documentos como vectores (embeddings) en una base de datos
- Busca los fragmentos mas relevantes para cada pregunta
- 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 |
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:
LLMConfigurationregistrado (con API key del proveedor elegido)IDbSessionregistrado (MicroOrmGesg)IGuardedLLMServiceregistrado (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 |
string → int |
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 |
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:
- Almacenamiento: Se guarda en
local_titlederag_chunk - Citación: El LLM puede citar como
[Ley 30/2025 > Artículo 4] - 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:
- Usuario hace pregunta →
IRagOrchestrator - Orquestador llama a
IRagRetriever.RetrieveAsync() - Retriever genera embedding de la pregunta con
IEmbeddingService - Retriever busca en
IRagStore(PostgreSQL + pgvector) - Retriever devuelve chunks relevantes
- Orquestador construye prompt con contexto
- Orquestador llama a
IGuardedLLMService(con guardrails) - LLM genera respuesta basada en el contexto
- Orquestador devuelve
RagAnswercon respuesta y fuentes
Best Practices para RAG
- Calidad de chunks: Trocear documentos en partes semanticamente coherentes (parrafos completos, secciones logicas)
- Tamano de chunks: 200-500 tokens es ideal. Muy pequeno pierde contexto, muy grande diluye relevancia
- Overlap: Considera solapamiento entre chunks para no perder contexto en los bordes
- Metadatos: Usa metadatos para filtrar (categoria, version, fecha)
- TopK apropiado: Empieza con 5-10, ajusta segun calidad de respuestas
- Temperature baja: Usa 0.1-0.3 para respuestas mas precisas y menos creativas
- 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
- Compatible con API de OpenAI (endpoint:
🏗️ 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 ClaudeOpenAI(2.6.0) - Para OpenAI y Gemini (vía API compatible)Newtonsoft.Json(13.0.4) - Serialización JSONGesgocom.MicroOrmGesg(1.0.2) - ORM para PostgreSQLMicrosoft.Extensions.DependencyInjection(9.0.10) - Inyección de dependenciasMicrosoft.Extensions.Hosting.Abstractions(9.0.10) - Abstracciones de hostingSharpToken(2.0.4) - Tokenización para conteo de tokensNpgsql(9.0.4) - Driver PostgreSQLDapper(2.1.66) - Micro ORM
Lifetimes de Servicios
Es importante entender los lifetimes de los servicios registrados:
Core LLM
ILLMService→ Singleton: Reutilizable, sin estadoIGuardedLLMService→ Scoped: Contiene guards que pueden tener estado
Sistema de Contexto
IAlmacenContexto→ Scoped: UsaIDbSession(conexiones de BD)IConversacionesService→ Scoped: UsaIAlmacenContextoIConversacionManager→ Scoped: UsaIGuardedLLMServiceSchemaValidator→ Scoped: UsaIDbSession
Sistema RAG
IEmbeddingService→ Singleton: Thread-safe, sin estadoIRagStore→ Scoped: UsaIDbSession(conexiones de BD)IRagRetriever→ Scoped: UsaIRagStoreIRagOrchestrator→ Scoped: UsaIRagRetriever+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
SchemaValidationHostedService→ Singleton (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
- Crea scopes manuales para resolver
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
- Valida disponibilidad de proveedores con
IsProviderAvailable() - Maneja errores apropiadamente (rate limits, timeouts, etc.)
- Usa structured output para datos estructurados
- Usa
IGuardedLLMServiceen producción
Guardrails
- Asigna prioridades apropiadas (seguridad: 1-10, validación: 11-30, optimización: 31-50)
- Usa
GuardFailureAction.Fixpara problemas solucionables,Blockpara críticos - Revisa metadatos y warnings
- Prueba guards con inputs maliciosos
Sistema de Contexto
- Ajusta
ventanaTokenssegún caso de uso (3000-10000) - Monitorea tokens en tiempo real
- Implementa reintentos con backoff exponencial
- 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:
SchemaValidatores 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:
- Reduce número de guards
- Usa modelos más rápidos (
gpt-5.2-nano,gemini-2.5-flash-lite) - Optimiza guards personalizados
Ventana se llena rápido
Solución:
- Aumenta
ventanaTokens - Usa modelos con ventanas más grandes
- 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.jsoncon User Secrets en desarrollo - En producción, usa Azure Key Vault o servicios similares
- Usa
IGuardedLLMServicepara 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:
- Un nombre descriptivo
- Una descripción de lo que hace (el LLM usa esto para decidir cuándo llamarla)
- Un schema JSON describiendo los parámetros
- 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:
- Usuario hace pregunta → LLM Service
- LLM decide qué tools necesita → Tool Adapter extrae los calls
- Tool Executor valida permisos y ejecuta (paralelo si es posible)
- Resultados → Tool Adapter los formatea para el proveedor
- LLM recibe resultados → Genera respuesta final
- 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.
.NET 10.0
- Anthropic (>= 12.6.0)
- itext7 (>= 9.5.0)
- SharpToken (>= 2.0.4)
- ReverseMarkdown (>= 5.0.0)
- OpenAI (>= 2.8.0)
- Npgsql (>= 10.0.1)
- Newtonsoft.Json (>= 13.0.4)
- Microsoft.ML.Tokenizers.Data.O200kBase (>= 2.0.0)
- Microsoft.ML.Tokenizers.Data.Cl100kBase (>= 2.0.0)
- Microsoft.ML.Tokenizers (>= 2.0.0)
- Microsoft.Extensions.Options (>= 10.0.3)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.3)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.3)
- Microsoft.Extensions.DependencyInjection (>= 10.0.3)
- Microsoft.Extensions.Caching.Memory (>= 10.0.3)
- Markdig (>= 0.44.0)
- Google.GenAI (>= 1.1.0)
- Gesgocom.MicroOrmGesg (>= 1.2.5)
- DocumentFormat.OpenXml (>= 3.4.1)
- Dapper (>= 2.1.66)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.3)
| 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 |