SilkroadBot / src /SilkroadBot.AI /Guardrails /CommandValidator.cs
Ahmedramadan24's picture
Add src/SilkroadBot.AI/Guardrails/CommandValidator.cs
a2aa13e verified
using Microsoft.Extensions.Logging;
using SilkroadBot.Domain.Models;
using SilkroadBot.Plugins.SDK.Interfaces;
namespace SilkroadBot.AI.Guardrails;
/// <summary>
/// Command Validation Layer (Sanity Check).
/// Every AI-generated action must pass through this layer before execution.
/// Prevents 'hallucinated' or illogical decisions that could lead to character loss.
/// </summary>
public class CommandValidator
{
private readonly ILogger<CommandValidator> _logger;
private readonly List<IValidationRule> _rules = new();
public CommandValidator(ILogger<CommandValidator> logger)
{
_logger = logger;
RegisterDefaultRules();
}
/// <summary>
/// Register a validation rule.
/// </summary>
public void RegisterRule(IValidationRule rule)
{
_rules.Add(rule);
_logger.LogDebug("Registered validation rule: {Name}", rule.Name);
}
/// <summary>
/// Validate an AI decision against the current game state.
/// Returns a validated result with any modifications or rejections.
/// </summary>
public ValidationResult Validate(AIDecision decision, GameStateSnapshot gameState)
{
if (!decision.IsValid)
return ValidationResult.Rejected("AI decision marked as invalid");
foreach (var rule in _rules.OrderBy(r => r.Priority))
{
var result = rule.Validate(decision, gameState);
if (!result.IsApproved)
{
_logger.LogWarning("Action '{Action}' rejected by rule '{Rule}': {Reason}",
decision.Action, rule.Name, result.Reason);
return result;
}
}
_logger.LogDebug("Action '{Action}' passed all validation rules", decision.Action);
return ValidationResult.Approved();
}
private void RegisterDefaultRules()
{
RegisterRule(new DeathPreventionRule());
RegisterRule(new PositionSafetyRule());
RegisterRule(new HealthThresholdRule());
RegisterRule(new CombatSafetyRule());
RegisterRule(new ConfidenceThresholdRule());
RegisterRule(new ActionExistsRule());
}
}
/// <summary>
/// Interface for validation rules.
/// </summary>
public interface IValidationRule
{
string Name { get; }
int Priority { get; }
ValidationResult Validate(AIDecision decision, GameStateSnapshot gameState);
}
/// <summary>
/// Result of a validation check.
/// </summary>
public record ValidationResult
{
public bool IsApproved { get; init; }
public string Reason { get; init; } = string.Empty;
public string? SuggestedAction { get; init; }
public static ValidationResult Approved() => new() { IsApproved = true };
public static ValidationResult Rejected(string reason) => new() { IsApproved = false, Reason = reason };
public static ValidationResult RedirectTo(string action, string reason) =>
new() { IsApproved = false, Reason = reason, SuggestedAction = action };
}
// ==================== Default Validation Rules ====================
/// <summary>
/// Prevents actions that would be executed while dead.
/// </summary>
public class DeathPreventionRule : IValidationRule
{
public string Name => "Death Prevention";
public int Priority => 0;
public ValidationResult Validate(AIDecision decision, GameStateSnapshot state)
{
if (state.IsDead && decision.Action != "resurrect" && decision.Action != "wait")
{
return ValidationResult.RedirectTo("wait",
"Character is dead. Only resurrect or wait actions are valid.");
}
return ValidationResult.Approved();
}
}
/// <summary>
/// Ensures HP is above a safe threshold before aggressive actions.
/// </summary>
public class HealthThresholdRule : IValidationRule
{
public string Name => "Health Threshold";
public int Priority => 10;
private const double CriticalHPThreshold = 20.0;
private const double LowHPThreshold = 40.0;
private static readonly HashSet<string> _aggressiveActions = new()
{
"attack", "pull_monster", "engage", "skill_attack", "aoe_attack"
};
public ValidationResult Validate(AIDecision decision, GameStateSnapshot state)
{
if (state.HPPercentage < CriticalHPThreshold && _aggressiveActions.Contains(decision.Action))
{
return ValidationResult.RedirectTo("heal",
$"HP is critically low ({state.HPPercentage:F1}%). Must heal before attacking.");
}
if (state.HPPercentage < LowHPThreshold && decision.Action == "pull_monster")
{
return ValidationResult.RedirectTo("heal",
$"HP too low ({state.HPPercentage:F1}%) to pull new monsters.");
}
return ValidationResult.Approved();
}
}
/// <summary>
/// Validates position safety - prevents walking into dangerous areas.
/// </summary>
public class PositionSafetyRule : IValidationRule
{
public string Name => "Position Safety";
public int Priority => 20;
public ValidationResult Validate(AIDecision decision, GameStateSnapshot state)
{
// If moving, check that destination is reasonable
if (decision.Action == "move" && decision.Parameters.TryGetValue("distance", out var dist))
{
if (dist is double distance && distance > 1000)
{
return ValidationResult.Rejected(
$"Movement distance ({distance}) exceeds safety limit. Potential hallucinated coordinates.");
}
}
return ValidationResult.Approved();
}
}
/// <summary>
/// Prevents engagement in combat when conditions are unsafe.
/// </summary>
public class CombatSafetyRule : IValidationRule
{
public string Name => "Combat Safety";
public int Priority => 15;
public ValidationResult Validate(AIDecision decision, GameStateSnapshot state)
{
// Don't engage if already in combat with too many entities
if (state.IsInCombat && decision.Action == "pull_monster" && state.NearbyEntityCount > 5)
{
return ValidationResult.Rejected(
$"Already in combat with {state.NearbyEntityCount} nearby entities. Too dangerous to pull more.");
}
return ValidationResult.Approved();
}
}
/// <summary>
/// Rejects decisions with low confidence scores.
/// </summary>
public class ConfidenceThresholdRule : IValidationRule
{
public string Name => "Confidence Threshold";
public int Priority => 5;
private const double MinConfidence = 0.3;
public ValidationResult Validate(AIDecision decision, GameStateSnapshot state)
{
if (decision.Confidence < MinConfidence)
{
return ValidationResult.RedirectTo("wait",
$"AI confidence ({decision.Confidence:F2}) below threshold ({MinConfidence}). Defaulting to safe action.");
}
return ValidationResult.Approved();
}
}
/// <summary>
/// Validates that the action is a known/supported action.
/// </summary>
public class ActionExistsRule : IValidationRule
{
public string Name => "Action Exists";
public int Priority => 1;
private static readonly HashSet<string> _validActions = new()
{
"attack", "heal", "buff", "move", "wait", "loot",
"pull_monster", "skill_attack", "aoe_attack", "flee",
"resurrect", "use_potion", "equip", "sell", "buy",
"teleport", "party_join", "party_leave", "none"
};
public ValidationResult Validate(AIDecision decision, GameStateSnapshot state)
{
if (!_validActions.Contains(decision.Action.ToLowerInvariant()))
{
return ValidationResult.RedirectTo("wait",
$"Unknown action '{decision.Action}'. Not in valid action set.");
}
return ValidationResult.Approved();
}
}