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
};
}
}