diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index f5aed9fec..2b17c43d8 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -8,13 +8,29 @@ public class AgentRule [JsonPropertyName("disabled")] public bool Disabled { get; set; } - [JsonPropertyName("config")] + /// + /// Message sent to agent + /// + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("criteria")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public RuleConfig? Config { get; set; } + public RuleCriteria? Criteria { get; set; } } -public class RuleConfig +public class RuleCriteria { + /// + /// Criteria mode: llm, python script, etc. + /// Takes precedence over the mode carried on the trigger options. + /// + [JsonPropertyName("mode")] + public string? Mode { get; set; } + + /// + /// Criteria text + /// [JsonPropertyName("criteria")] public string? Criteria { get; set; } -} \ No newline at end of file +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Constants/BuiltInRuleCriteria.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Constants/BuiltInRuleCriteria.cs index 7ddba841e..35992f884 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Constants/BuiltInRuleCriteria.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Constants/BuiltInRuleCriteria.cs @@ -10,7 +10,7 @@ public static class BuiltInRuleCriteria /// /// Evaluate a code script (e.g. Python) that returns a boolean result. /// - public const string Code = "code"; + public const string PythonScript = "python_script"; /// /// Ask an LLM whether the rule applies to the request. diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs index 1fd1c52f6..066ed57bd 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs @@ -6,11 +6,6 @@ namespace BotSharp.Abstraction.Rules.Models; /// public class RuleCriteriaContext { - /// - /// The trigger message text. - /// - public string Text { get; set; } = string.Empty; - /// /// The criteria options (evaluator type and its arguments). /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 15b0a1a48..174274d06 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -23,7 +23,7 @@ public class CriteriaOptions /// How the criteria is evaluated (see ). /// Selects which IRuleCriteriaEvaluator handles this criteria. /// - public string Type { get; set; } = BuiltInRuleCriteria.Code; + public string? Mode { get; set; } /// /// Evaluator-specific settings, kept as raw JSON so each evaluator can diff --git a/src/Infrastructure/BotSharp.Core.Rules/Criteria/Code/CodeCriteriaEvaluator.cs b/src/Infrastructure/BotSharp.Core.Rules/Criteria/Code/CodeCriteriaEvaluator.cs index 93a9b0512..31a712944 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Criteria/Code/CodeCriteriaEvaluator.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Criteria/Code/CodeCriteriaEvaluator.cs @@ -23,7 +23,7 @@ public CodeCriteriaEvaluator( _codingSettings = codingSettings; } - public string Type => BuiltInRuleCriteria.Code; + public string Type => BuiltInRuleCriteria.PythonScript; public async Task EvaluateAsync(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Criteria/Llm/LlmCriteriaEvaluator.cs b/src/Infrastructure/BotSharp.Core.Rules/Criteria/Llm/LlmCriteriaEvaluator.cs index 131e2a94d..319befe47 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Criteria/Llm/LlmCriteriaEvaluator.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Criteria/Llm/LlmCriteriaEvaluator.cs @@ -40,7 +40,7 @@ public LlmCriteriaEvaluator( var templateName = !string.IsNullOrWhiteSpace(settings.TemplateName) ? settings.TemplateName! : (agentId == BuiltInAgentId.RulesInterpreter ? DefaultTemplateName : $"{trigger.Name}_criteria"); - var input = BuildInput(rule?.Config, settings); + var input = BuildInput(rule?.Criteria, settings); var msg = $"rule trigger ({trigger.Name}) llm criteria (agent {agentId}, template {templateName})."; try @@ -129,14 +129,14 @@ private static Dictionary BuildRenderData(RuleCriteriaContext co return data; } - private static string BuildInput(RuleConfig? ruleConfig, LlmCriteriaSettings settings) + private static string BuildInput(RuleCriteria? ruleCriteria, LlmCriteriaSettings settings) { var sb = new StringBuilder(); - if (!string.IsNullOrWhiteSpace(ruleConfig?.Criteria)) + if (!string.IsNullOrWhiteSpace(ruleCriteria?.Criteria)) { sb.AppendLine("## Rule"); - sb.AppendLine(ruleConfig.Criteria); + sb.AppendLine(ruleCriteria.Criteria); sb.AppendLine(); } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 7f42a4e8b..af47cdf22 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Templating; + namespace BotSharp.Core.Rules.Engines; public class RuleEngine : IRuleEngine @@ -31,10 +33,10 @@ public async Task> Triggered(IRuleTrigger trigger, string te IRuleCriteriaEvaluator? criteriaEvaluator = null; if (options?.Criteria != null) { - criteriaEvaluator = ResolveCriteriaEvaluator(options.Criteria.Type); + criteriaEvaluator = ResolveCriteriaEvaluator(options.Criteria.Mode); if (criteriaEvaluator == null) { - _logger.LogWarning($"Unable to find rule criteria evaluator for type ({options.Criteria.Type})."); + _logger.LogWarning($"Unable to find rule criteria evaluator for type ({options.Criteria.Mode})."); } } @@ -48,23 +50,26 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } - if (criteriaEvaluator != null) + // The rule's own mode wins over the mode carried on the trigger options, so an agent can + // pick how its criteria is judged without the caller knowing. + var evaluator = ResolveCriteriaEvaluator(rule.Criteria?.Mode) ?? criteriaEvaluator; + if (evaluator != null && options?.Criteria != null) { var criteriaContext = new RuleCriteriaContext { - Text = text, - Options = options!.Criteria!, + Options = options.Criteria, States = states }; - var isTriggered = await EvaluateCriteria(criteriaEvaluator, agent, trigger, criteriaContext); + var isTriggered = await EvaluateCriteria(evaluator, agent, trigger, criteriaContext); if (!isTriggered) { continue; } } - var convId = await SendMessageToAgent(agent, trigger, text, states); + var msg = !string.IsNullOrWhiteSpace(rule.Message) ? rule.Message : text; + var convId = await SendMessageToAgent(agent, trigger, text, msg, states); newConversationIds.Add(convId); } @@ -72,9 +77,14 @@ public async Task> Triggered(IRuleTrigger trigger, string te } #region Criteria - private IRuleCriteriaEvaluator? ResolveCriteriaEvaluator(string type) + private IRuleCriteriaEvaluator? ResolveCriteriaEvaluator(string? mode) { - return _services.GetServices().FirstOrDefault(x => x.Type.IsEqualTo(type)); + if (string.IsNullOrWhiteSpace(mode)) + { + return null; + } + + return _services.GetServices().FirstOrDefault(x => x.Type.IsEqualTo(mode)); } /// @@ -112,664 +122,17 @@ private async Task EvaluateCriteria( } #endregion -// public async Task ExecuteGraphNode(FlowNode node, FlowGraph graph, string agentId, IRuleTrigger trigger, RuleNodeExecutionOptions options) -// { -// if (node == null || graph == null || options == null) -// { -// return; -// } - -// var agentService = _services.GetRequiredService(); -// var agent = await agentService.GetAgent(agentId); - -// var triggerOptions = new RuleTriggerOptions -// { -// Flow = options.Flow, -// JsonOptions = options.JsonOptions -// }; - -// var execResults = new List(); -// await ExecuteGraphNode( -// node, graph, -// agent, trigger, -// options.Text, -// options.States, -// null, -// triggerOptions, -// execResults); -// graph.Clear(); -// } - -// #region Graph -// private async Task LoadGraph(string name, Agent agent, IRuleTrigger trigger, RuleFlowOptions? options) -// { -// var flow = _services.GetServices>().FirstOrDefault(x => x.Name.IsEqualTo(name)); -// if (flow == null) -// { -// return null; -// } - -// try -// { -// var config = await flow.GetTopologyConfigAsync(options: new() -// { -// TopologyName = name -// }); - -// var topologyId = config?.TopologyId; -// if (string.IsNullOrEmpty(topologyId)) -// { -// return null; -// } - - -// var param = new Dictionary(options?.Parameters ?? []); -// param["agent"] = param.GetValueOrDefault("agent", agent.Name); -// param["agent_id"] = param.GetValueOrDefault("agent_id", agent.Id); -// param["trigger"] = param.GetValueOrDefault("trigger", trigger.Name); - -// var graph = await flow.GetTopologyAsync(topologyId, options: new() -// { -// Query = options?.Query, -// Parameters = param -// }); - -// if (graph != null) -// { -// // Apply input/output schemas from node config to the node -// LoadConfigSchemas(graph); - -// // Validate input/output schema compatibility between connected nodes -// if (options?.SkipValidation != true) -// { -// ValidateGraphSchema(graph); -// } -// } - -// return graph; -// } -// catch (Exception ex) -// { -// _logger.LogError(ex, $"Error when loading graph (name: {name}, agent: {agent}, trigger: {trigger?.Name})"); -// return null; -// } -// } - -// private async Task ExecuteGraphNode( -// FlowNode node, -// FlowGraph graph, -// Agent agent, -// IRuleTrigger trigger, -// string text, -// IEnumerable? states, -// Dictionary? data, -// RuleTriggerOptions? options, -// List results) -// { -// try -// { -// await ExecuteGraphTraversal(node, graph, agent, trigger, text, states, data, options, results); -// } -// catch { } -// } - -// /// -// /// Unified graph traversal that uses a swappable frontier. -// /// Stack frontier → DFS, Queue frontier → BFS. -// /// A node or edge can request a mid-traversal switch via its -// /// Config["traversal_algorithm"] value ("dfs" or "bfs"). -// /// -// private async Task ExecuteGraphTraversal( -// FlowNode root, -// FlowGraph graph, -// Agent agent, -// IRuleTrigger trigger, -// string text, -// IEnumerable? states, -// Dictionary? data, -// RuleTriggerOptions? options, -// List results) -// { -// var flow = options?.Flow; -// var maxRecursion = flow?.MaxRecursion > 0 ? flow.MaxRecursion : RuleConstant.MAX_GRAPH_RECURSION; -// var innerData = new Dictionary(data ?? []); - -// // Choose initial frontier based on the global option -// var useBfs = options?.Flow?.TraversalAlgorithm?.IsEqualTo("bfs") == true; -// IFrontier<(FlowNode Node, FlowEdge Edge)> frontier = useBfs -// ? new QueueFrontier<(FlowNode, FlowEdge)>() -// : new StackFrontier<(FlowNode, FlowEdge)>(); - -// EnqueueChildren(frontier, graph, root); - -// while (frontier.Count > 0) -// { -// if (results.Count >= maxRecursion) -// { -// _logger.LogWarning("Exceed max graph nodes {MaxNodes} (agent {Agent} and trigger {Trigger}).", -// maxRecursion, agent.Name, trigger.Name); -// break; -// } - -// var (nextNode, nextEdge) = frontier.Remove(); - -// // Check whether node requests a traversal switch -// frontier = SwitchFrontier(frontier, nextNode); - -// // Build context -// var context = new RuleFlowContext -// { -// Node = nextNode, -// Edge = nextEdge, -// Graph = graph, -// Text = text, -// Parameters = BuildParameters(states, innerData), -// PrevStepResults = results, -// JsonOptions = options?.JsonOptions -// }; - -// if (RuleConstant.CONDITION_NODE_TYPES.Contains(nextNode.Type, StringComparer.OrdinalIgnoreCase)) -// { -// var conditionResult = await ExecuteCondition(nextNode, nextEdge, graph, agent, trigger, context); -// innerData = new(context.Parameters ?? []); - -// if (conditionResult == null) -// { -// results.Add(RuleFlowStepResult.FromResult(new() -// { -// Success = false, -// ErrorMessage = $"Unable to find condition {nextNode.Name}." -// }, nextNode)); -// continue; -// } - -// results.Add(RuleFlowStepResult.FromResult(conditionResult, nextNode)); - -// if (conditionResult.Success) -// { -// EnqueueChildren(frontier, graph, nextNode); -// } -// else -// { -// _logger.LogInformation("Condition {ConditionName} evaluated to false, skipping next node (agent {Agent} and trigger {Trigger}).", -// nextNode.Name, agent.Name, trigger.Name); -// } -// } -// else if (RuleConstant.ACTION_NODE_TYPES.Contains(nextNode.Type, StringComparer.OrdinalIgnoreCase) -// || RuleConstant.ROOT_NODE_TYPES.Contains(nextNode.Type, StringComparer.OrdinalIgnoreCase) -// || RuleConstant.END_NODE_TYPES.Contains(nextNode.Type, StringComparer.OrdinalIgnoreCase)) -// { -// var actionResult = await ExecuteAction(nextNode, nextEdge, graph, agent, trigger, context); -// innerData = new(context.Parameters ?? []); - -// if (actionResult == null) -// { -// results.Add(RuleFlowStepResult.FromResult(new() -// { -// Success = false, -// ErrorMessage = $"Unable to find action {nextNode.Name}." -// }, nextNode)); -// continue; -// } - -// results.Add(RuleFlowStepResult.FromResult(actionResult, nextNode)); - -// if (!actionResult.IsDelayed) -// { -// EnqueueChildren(frontier, graph, nextNode); -// } -// } -// else -// { -// results.Add(RuleFlowStepResult.FromResult(new() -// { -// Success = true, -// Response = $"Pass through node {nextNode.Name}." -// }, nextNode)); - -// EnqueueChildren(frontier, graph, nextNode); -// } -// } -// } - -// /// -// /// If the node carries a traversal_algorithm config value -// /// that differs from the current frontier type, swap to the requested one -// /// and drain all pending items into the new frontier. -// /// -// private static IFrontier<(FlowNode, FlowEdge)> SwitchFrontier( -// IFrontier<(FlowNode, FlowEdge)> current, -// FlowNode? node) -// { -// // Edge config takes precedence over node config -// var hint = node?.Config?.GetValueOrDefault("traversal_algorithm"); - -// if (string.IsNullOrEmpty(hint)) -// { -// return current; -// } - -// var requireBfs = hint.Equals("bfs", StringComparison.OrdinalIgnoreCase); -// var currentBfs = current is QueueFrontier<(FlowNode, FlowEdge)>; - -// if (requireBfs == currentBfs) -// { -// return current; -// } - -// IFrontier<(FlowNode, FlowEdge)> next = requireBfs -// ? new QueueFrontier<(FlowNode, FlowEdge)>() -// : new StackFrontier<(FlowNode, FlowEdge)>(); - -// current.DrainTo(next); -// return next; -// } - -// private static void EnqueueChildren( -// IFrontier<(FlowNode Node, FlowEdge Edge)> frontier, -// FlowGraph graph, -// FlowNode parent) -// { -// var sortAscending = frontier is StackFrontier<(FlowNode, FlowEdge)>; -// foreach (var child in graph.GetChildrenNodes(parent, sortAscending)) -// { -// frontier.Add(child); -// } -// } -// #endregion - - -// #region Schema Validation -// /// -// /// Reads "input_schema" and "output_schema" from each node's Config, -// /// deserializes them into FlowUnitSchema, and sets them on the FlowNode. -// /// If a node has no config schema, the code-defined schema from the -// /// resolved IRuleFlowUnit is used as fallback during validation. -// /// -// private void LoadConfigSchemas(FlowGraph graph) -// { -// var nodes = graph.GetNodes(); -// if (nodes == null) -// { -// return; -// } - -// foreach (var node in nodes) -// { -// if (node.Config.IsNullOrEmpty()) -// { -// continue; -// } - -// if (node.Config!.TryGetValue(RuleConstant.INPUT_SCHEMA_KEY, out var inputJson) -// && !string.IsNullOrEmpty(inputJson)) -// { -// try -// { -// node.InputSchema = JsonSerializer.Deserialize(inputJson); -// } -// catch (Exception ex) -// { -// _logger.LogWarning(ex, "Failed to deserialize input_schema from config of node [{NodeName}].", node.Name); -// } -// } - -// if (node.Config!.TryGetValue(RuleConstant.OUTPUT_SCHEMA_KEY, out var outputJson) -// && !string.IsNullOrEmpty(outputJson)) -// { -// try -// { -// node.OutputSchema = JsonSerializer.Deserialize(outputJson); -// } -// catch (Exception ex) -// { -// _logger.LogWarning(ex, "Failed to deserialize output_schema from config of node [{NodeName}].", node.Name); -// } -// } -// } -// } - -// /// -// /// Validates that for every edge in the graph, the downstream node's required input fields -// /// can be satisfied by the upstream node's output or the downstream node's own config. -// /// Node-level schemas (from config) take precedence over code-defined schemas. -// /// -// private void ValidateGraphSchema(FlowGraph graph) -// { -// var edges = graph.GetEdges(); -// if (edges == null || !edges.Any()) -// { -// return; -// } - -// foreach (var edge in edges) -// { -// if (edge.From == null || edge.To == null) -// { -// continue; -// } - -// var sourceUnit = ResolveFlowUnit(edge.From); -// var targetUnit = ResolveFlowUnit(edge.To); - -// // Config-defined schema on the node takes precedence over code-defined -// var targetInputSchema = edge.To.InputSchema ?? targetUnit?.InputSchema; -// if (targetInputSchema?.Required == null || targetInputSchema.Required.Count == 0) -// { -// continue; -// } - -// // Collect available keys from upstream output and downstream node's own config -// var availableKeys = new HashSet(StringComparer.OrdinalIgnoreCase); - -// var sourceOutputSchema = edge.From.OutputSchema ?? sourceUnit?.OutputSchema; -// if (sourceOutputSchema?.Properties != null && !sourceOutputSchema.Properties.Keys.IsNullOrEmpty()) -// { -// foreach (var key in sourceOutputSchema.Properties.Keys) -// { -// availableKeys.Add(key); -// } -// } - -// if (edge.To.Config != null && !edge.To.Config.Keys.IsNullOrEmpty()) -// { -// foreach (var key in edge.To.Config.Keys) -// { -// availableKeys.Add(key); -// } -// } - -// // Check each required input field -// foreach (var key in targetInputSchema.Required) -// { -// if (!availableKeys.Contains(key)) -// { -// _logger.Log( -//#if DEBUG -// LogLevel.Critical, -//#else -// LogLevel.Warning, -//#endif -// "Schema validation: edge [{SourceNode}] -> [{TargetNode}]: " + -// "required input '{Key}' is not provided by upstream output or node config.", -// edge.From.Name, edge.To.Name, key); -// } -// // Validate type compatibility when both schemas define the property -// else if (sourceOutputSchema?.Properties != null -// && sourceOutputSchema.Properties.TryGetValue(key, out var sourceProp) -// && targetInputSchema.Properties.TryGetValue(key, out var targetProp) -// && !string.IsNullOrEmpty(sourceProp.Type) -// && !string.IsNullOrEmpty(targetProp.Type) -// && !sourceProp.Type.Equals(targetProp.Type, StringComparison.OrdinalIgnoreCase)) -// { -// _logger.Log( -//#if DEBUG -// LogLevel.Critical, -//#else -// LogLevel.Warning, -//#endif -// "Schema validation: edge [{SourceNode}] -> [{TargetNode}]: " + -// "type mismatch for '{Key}' — upstream produces '{SourceType}' but downstream expects '{TargetType}'.", -// edge.From.Name, edge.To.Name, key, sourceProp.Type, targetProp.Type); -// } -// } -// } -// } - -// /// -// /// Resolves the IRuleFlowUnit (action or condition) implementation for a given node. -// /// -// private IRuleFlowUnit? ResolveFlowUnit(FlowNode node) -// { -// if (node == null || string.IsNullOrEmpty(node.Name)) -// { -// return null; -// } - -// if (RuleConstant.ROOT_NODE_TYPES.Contains(node.Type, StringComparer.OrdinalIgnoreCase)) -// { -// return _services.GetServices() -// .FirstOrDefault(x => x.Name.IsEqualTo(node.Name)); -// } - -// if (RuleConstant.END_NODE_TYPES.Contains(node.Type, StringComparer.OrdinalIgnoreCase)) -// { -// return _services.GetServices() -// .FirstOrDefault(x => x.Name.IsEqualTo(node.Name)); -// } - -// if (RuleConstant.ACTION_NODE_TYPES.Contains(node.Type, StringComparer.OrdinalIgnoreCase)) -// { -// return _services.GetServices() -// .FirstOrDefault(x => x.Name.IsEqualTo(node.Name)); -// } - -// if (RuleConstant.CONDITION_NODE_TYPES.Contains(node.Type, StringComparer.OrdinalIgnoreCase)) -// { -// return _services.GetServices() -// .FirstOrDefault(x => x.Name.IsEqualTo(node.Name)); -// } - -// return null; -// } -//#endregion - - -// #region Action -// private async Task ExecuteAction( -// FlowNode node, -// FlowEdge incomingEdge, -// FlowGraph graph, -// Agent agent, -// IRuleTrigger trigger, -// RuleFlowContext context) -// { -// try -// { -// // Find the matching action -// var foundAction = GetRuleAction(node, agent, trigger); -// if (foundAction == null) -// { -// var errorMsg = $"No rule action {node?.Name} is found"; -// _logger.LogWarning(errorMsg); -// return null; -// } - -// _logger.LogInformation("Start execution rule action {ActionName} for agent {AgentId} with trigger {TriggerName}", -// foundAction.Name, agent.Id, trigger.Name); - -// var hooks = _services.GetHooks(agent.Id); -// foreach (var hook in hooks) -// { -// await hook.BeforeRuleActionExecuting(agent, node, incomingEdge, trigger, context); -// } - -// // Execute action -// context.Parameters ??= []; -// var result = await foundAction.ExecuteAsync(agent, trigger, context); - -// foreach (var hook in hooks) -// { -// await hook.AfterRuleActionExecuted(agent, node, incomingEdge, trigger, context, result); -// } - -// return result; -// } -// catch (Exception ex) -// { -// _logger.LogError(ex, "Error executing rule action {ActionName} for agent {AgentId}", node?.Name, agent.Id); -// return new RuleNodeResult -// { -// Success = false, -// ErrorMessage = ex.Message -// }; -// } -// } - -// // Find the matching action -// private IRuleAction? GetRuleAction(FlowNode node, Agent agent, IRuleTrigger trigger) -// { -// var actions = _services.GetServices() -// .Where(x => x.Name.IsEqualTo(node?.Name)) -// .ToList(); - -// var found = actions.FirstOrDefault(x => !string.IsNullOrEmpty(x.AgentId) && x.AgentId.IsEqualTo(agent.Id) && x.Triggers?.Contains(trigger.Name) == true); -// if (found != null) -// { -// return found; -// } - -// found = actions.FirstOrDefault(x => !string.IsNullOrEmpty(x.AgentId) && x.AgentId.IsEqualTo(agent.Id)); -// if (found != null) -// { -// return found; -// } - -// found = actions.FirstOrDefault(x => x.Triggers?.Contains(trigger.Name, StringComparer.OrdinalIgnoreCase) == true); -// if (found != null) -// { -// return found; -// } - -// found = actions.FirstOrDefault(); -// if (found != null) -// { -// return found; -// } - -// return null; -// } -// #endregion - - -// #region Condition -// private async Task ExecuteCondition( -// FlowNode node, -// FlowEdge incomingEdge, -// FlowGraph graph, -// Agent agent, -// IRuleTrigger trigger, -// RuleFlowContext context) -// { -// try -// { -// // Find the matching condition -// var foundCondition = GetRuleCondition(node, agent, trigger); -// if (foundCondition == null) -// { -// var errorMsg = $"No rule condition {node?.Name} is found"; -// _logger.LogWarning(errorMsg); -// return null; -// } - -// _logger.LogInformation("Start execution rule condition {ConditionName} for agent {AgentId} with trigger {TriggerName}", -// foundCondition.Name, agent.Id, trigger.Name); - -// var hooks = _services.GetHooks(agent.Id); -// foreach (var hook in hooks) -// { -// await hook.BeforeRuleConditionExecuting(agent, node, incomingEdge, trigger, context); -// } - -// // Execute condition -// context.Parameters ??= []; -// var result = await foundCondition.EvaluateAsync(agent, trigger, context); - -// foreach (var hook in hooks) -// { -// await hook.AfterRuleConditionExecuted(agent, node, incomingEdge, trigger, context, result); -// } - -// return result; -// } -// catch (Exception ex) -// { -// _logger.LogError(ex, "Error executing rule condition {ConditionName} for agent {AgentId}", node?.Name, agent.Id); -// return new RuleNodeResult -// { -// Success = false, -// ErrorMessage = ex.Message -// }; -// } -// } - -// // Find the matching condition -// private IRuleCondition? GetRuleCondition(FlowNode node, Agent agent, IRuleTrigger trigger) -// { -// var conditions = _services.GetServices() -// .Where(x => x.Name.IsEqualTo(node?.Name)) -// .ToList(); - -// var found = conditions.FirstOrDefault(x => !string.IsNullOrEmpty(x.AgentId) && x.AgentId.IsEqualTo(agent.Id) && x.Triggers?.Contains(trigger.Name) == true); -// if (found != null) -// { -// return found; -// } - -// found = conditions.FirstOrDefault(x => !string.IsNullOrEmpty(x.AgentId) && x.AgentId.IsEqualTo(agent.Id)); -// if (found != null) -// { -// return found; -// } - -// found = conditions.FirstOrDefault(x => x.Triggers?.Contains(trigger.Name, StringComparer.OrdinalIgnoreCase) == true); -// if (found != null) -// { -// return found; -// } - -// found = conditions.FirstOrDefault(); -// if (found != null) -// { -// return found; -// } - -// return null; -// } -// #endregion - - -// #region Private methods -// private Dictionary BuildParameters( -// IEnumerable? states, -// Dictionary? param = null) -// { -// var dict = new Dictionary(); - -// if (!states.IsNullOrEmpty()) -// { -// foreach (var state in states!) -// { -// dict[state.Key] = state.Value?.ConvertToString(); -// } -// } - -// if (!param.IsNullOrEmpty()) -// { -// foreach (var pair in param!) -// { -// dict[pair.Key] = pair.Value; -// } -// } - -// return dict; -// } -// #endregion - - - #region Legacy conversation - private async Task SendMessageToAgent(Agent agent, IRuleTrigger trigger, string text, IEnumerable? states = null) + #region Send message to agent + private async Task SendMessageToAgent(Agent agent, IRuleTrigger trigger, string title, string msg, IEnumerable? states = null) { var convService = _services.GetRequiredService(); var conv = await convService.NewConversation(new Conversation { Channel = trigger.Channel, - Title = text, + Title = title, AgentId = agent.Id }); - var message = new RoleDialogModel(AgentRole.User, text); - var allStates = new List { new("channel", trigger.Channel) @@ -780,6 +143,8 @@ private async Task SendMessageToAgent(Agent agent, IRuleTrigger trigger, allStates.AddRange(states!); } + var message = new RoleDialogModel(AgentRole.User, RenderMessage(msg, allStates)); + await convService.SetConversationId(conv.Id, allStates); await convService.SendMessage(agent.Id, message, @@ -790,5 +155,35 @@ await convService.SendMessage(agent.Id, return conv.Id; } + + private string RenderMessage(string msg, IEnumerable states) + { + if (string.IsNullOrWhiteSpace(msg)) + { + return msg; + } + + try + { + var data = new Dictionary(); + foreach (var state in states) + { + if (string.IsNullOrEmpty(state.Key)) + { + continue; + } + + data[state.Key] = state.Value; + } + + var render = _services.GetRequiredService(); + return render.Render(msg, data); + } + catch (Exception ex) + { + _logger.LogWarning(ex, $"Unable to render the rule message template, falling back to the raw message ({msg})."); + return msg; + } + } #endregion } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index 978a797a6..6af863aa7 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -7,7 +7,8 @@ public class AgentRuleMongoElement { public string TriggerName { get; set; } = default!; public bool Disabled { get; set; } - public RuleConfigMongoModel? Config { get; set; } + public string? Message { get; set; } + public RuleCriteriaMongoModel? Criteria { get; set; } public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { @@ -15,7 +16,8 @@ public static AgentRuleMongoElement ToMongoElement(AgentRule rule) { TriggerName = rule.TriggerName, Disabled = rule.Disabled, - Config = RuleConfigMongoModel.ToMongoModel(rule.Config) + Message = rule.Message, + Criteria = RuleCriteriaMongoModel.ToMongoModel(rule.Criteria) }; } @@ -25,39 +27,43 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) { TriggerName = rule.TriggerName, Disabled = rule.Disabled, - Config = RuleConfigMongoModel.ToDomainModel(rule.Config) + Message = rule.Message, + Criteria = RuleCriteriaMongoModel.ToDomainModel(rule.Criteria) }; } } [BsonIgnoreExtraElements(Inherited = true)] -public class RuleConfigMongoModel +public class RuleCriteriaMongoModel { + public string? Mode { get; set; } public string? Criteria { get; set; } - public static RuleConfigMongoModel? ToMongoModel(RuleConfig? config) + public static RuleCriteriaMongoModel? ToMongoModel(RuleCriteria? criteria) { - if (config == null) + if (criteria == null) { return null; } - return new RuleConfigMongoModel + return new RuleCriteriaMongoModel { - Criteria = config.Criteria + Mode = criteria.Mode, + Criteria = criteria.Criteria }; } - public static RuleConfig? ToDomainModel(RuleConfigMongoModel? config) + public static RuleCriteria? ToDomainModel(RuleCriteriaMongoModel? criteria) { - if (config == null) + if (criteria == null) { return null; } - return new RuleConfig + return new RuleCriteria { - Criteria = config.Criteria + Mode = criteria.Mode, + Criteria = criteria.Criteria }; } }