NALU/src/Nalu.Api/Services/DeterministicLayer.cs
Ricardo Carneiro ea6cdb5395 Initial commit — NALU AI web platform
- ASP.NET Core 9 Razor Pages + Minimal API hybrid
- 14 validators (CPF, CEP, CNPJ, email, phone, name, yes-no, birthdate, handoff, cancel-intent, company-name, plate-br, postal-code, validate_reply)
- OAuth login (Google, Microsoft, GitHub) + cookie auth
- MongoDB usage tracking + CEP cache collection
- Stripe checkout with inline PriceData (no Price IDs)
- MCP server for Claude Code / Cursor integration
- Playground (10 calls/IP/day, no auth)
- Docs: Quickstart, API Reference, N8N, MCP, Créditos, Erros, Fluxos
- Credit system: 3 cr standard validators, 5 cr validate_reply
- SmartSuggestion: contextual re-ask via IA when obtained=false
- Per-IP rate limiting + daily cap + shared-IP abuse detection
- Lightbox for comic images
- Validadores page split: Brasileiros / Universais + Em breve

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:39:04 -03:00

126 lines
4.2 KiB
C#

using System.Text.RegularExpressions;
using Nalu.Web.Models;
namespace Nalu.Web.Services;
public enum DeterministicOutcome
{
Unresolved,
Rejected,
Accepted,
ConstraintFailed
}
public record DeterministicResult
{
public DeterministicOutcome Outcome { get; init; }
public string? ExtractedValue { get; init; }
public string? Reasoning { get; init; }
}
public class DeterministicLayer
{
public DeterministicResult Evaluate(ValidatorDefinition validator, string userInput, string language = "pt-BR")
{
var normalized = userInput.Trim().ToLowerInvariant().TrimEnd('.', '!', '?', ',', ';');
// Use localized stop words when available, fall back to flat set
var stopWords = validator.LocalizedStopWords.TryGetValue(language, out var localized)
? localized
: validator.StopWords;
if (stopWords.Contains(normalized))
{
return new DeterministicResult
{
Outcome = DeterministicOutcome.Rejected,
Reasoning = "Usuário respondeu com saudação ou palavra de parada"
};
}
// Reject patterns
foreach (var pattern in validator.RejectPatterns)
{
try
{
if (Regex.IsMatch(normalized, pattern, RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100)))
{
return new DeterministicResult
{
Outcome = DeterministicOutcome.Rejected,
Reasoning = "Resposta corresponde a padrão de rejeição"
};
}
}
catch (RegexMatchTimeoutException) { /* skip on timeout */ }
}
// Accept patterns — capture group 1 is the extracted value
foreach (var pattern in validator.AcceptPatterns)
{
try
{
var m = Regex.Match(normalized, pattern, RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100));
if (!m.Success) continue;
var extracted = m.Groups.Count > 1 && m.Groups[1].Success
? m.Groups[1].Value.Trim()
: userInput.Trim();
var violation = CheckConstraints(validator.Constraints, extracted);
if (violation is not null)
{
return new DeterministicResult
{
Outcome = DeterministicOutcome.ConstraintFailed,
ExtractedValue = extracted,
Reasoning = violation
};
}
return new DeterministicResult
{
Outcome = DeterministicOutcome.Accepted,
ExtractedValue = extracted,
Reasoning = "Padrão de aceitação encontrado"
};
}
catch (RegexMatchTimeoutException) { /* skip on timeout */ }
}
return new DeterministicResult { Outcome = DeterministicOutcome.Unresolved };
}
private static string? CheckConstraints(Dictionary<string, string> constraints, string value)
{
if (constraints.TryGetValue("min_length", out var minStr) && int.TryParse(minStr, out var min))
{
if (value.Length < min)
return $"Valor muito curto (mínimo {min} caracteres)";
}
if (constraints.TryGetValue("max_length", out var maxStr) && int.TryParse(maxStr, out var max))
{
if (value.Length > max)
return $"Valor muito longo (máximo {max} caracteres)";
}
if (constraints.TryGetValue("max_digits", out var maxDigStr) && int.TryParse(maxDigStr, out var maxDig))
{
var digitCount = value.Count(char.IsDigit);
if (digitCount > maxDig)
return $"Número de dígitos excede o máximo permitido ({maxDig})";
}
if (constraints.TryGetValue("must_have_alpha", out var alphaStr)
&& bool.TryParse(alphaStr, out var mustHaveAlpha)
&& mustHaveAlpha)
{
if (!value.Any(char.IsLetter))
return "Valor deve conter letras";
}
return null;
}
}