Some checks failed
NALU Deployment Pipeline / Run Tests (push) Successful in 4m16s
NALU Deployment Pipeline / PR Validation (push) Has been skipped
NALU Deployment Pipeline / Build and Push Image (push) Failing after 13s
NALU Deployment Pipeline / Deploy naluai.dev (push) Has been skipped
NALU Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
- DeterministicLayer: accept patterns now use IgnoreCase (was None) - Test: CPF ExtractedValue is raw formatted match, not stripped digits - CI filter: also exclude McpServerTests (require live server) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
137 lines
4.6 KiB
C#
137 lines
4.6 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.
|
|
// Matched case-insensitively against the original (trimmed) input.
|
|
var original = userInput.Trim().TrimEnd('.', '!', '?', ',', ';');
|
|
|
|
foreach (var pattern in validator.AcceptPatterns)
|
|
{
|
|
try
|
|
{
|
|
var m = Regex.Match(original, 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()
|
|
: original;
|
|
|
|
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";
|
|
}
|
|
|
|
if (constraints.TryGetValue("must_have_space", out var spaceStr)
|
|
&& bool.TryParse(spaceStr, out var mustHaveSpace)
|
|
&& mustHaveSpace)
|
|
{
|
|
if (!value.Contains(' '))
|
|
return "Valor deve conter ao menos duas palavras";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|