diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/RuleGraph.cs index fa7a411f3..e12fcc019 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/RuleGraph.cs @@ -114,7 +114,7 @@ public void AddEdge(RuleNode from, RuleNode to, EdgeItemPayload payload) Type = payload.Type, Labels = [.. payload.Labels ?? []], Weight = payload.Weight, - Purpose = payload.Purpose, + Alias = payload.Alias, Description = payload.Description, Config = new(payload.Config ?? []) }); @@ -234,9 +234,15 @@ public class GraphItem public virtual string Type { get; set; } = null!; public virtual IEnumerable Labels { get; set; } = []; public virtual double Weight { get; set; } = 1.0; - public virtual string? Purpose { get; set; } public virtual string? Description { get; set; } public virtual Dictionary Config { get; set; } = []; + + private string? _alias; + public virtual string Alias + { + get => string.IsNullOrEmpty(_alias) ? Name : _alias; + set => _alias = value; + } } public class NodeItem : GraphItem diff --git a/src/Infrastructure/BotSharp.Core.Rules/Conditions/LogicGateCondition.cs b/src/Infrastructure/BotSharp.Core.Rules/Conditions/LogicGateCondition.cs index eb678c63d..364c67ff6 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Conditions/LogicGateCondition.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Conditions/LogicGateCondition.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using BotSharp.Core.Rules.Models; namespace BotSharp.Core.Rules.Conditions; @@ -16,11 +15,13 @@ namespace BotSharp.Core.Rules.Conditions; /// (A AND B) OR (C AND NOT D) /// /// Leaf node format: -/// { "node": "node_name", "key": "data_key" } -/// - "node": The Name of a parent condition node whose result to inspect. -/// - "key": The key in the parent node's RuleNodeResult.Data dictionary that holds -/// a boolean string ("true"/"false"). If omitted, falls back to the parent -/// node's RuleNodeResult.Success flag. +/// { "node_alias": "node_alias", "key": "data_key" } +/// - "node_alias": The Alias of a parent condition node whose result to inspect. +/// Using Alias instead of Name avoids collisions when multiple nodes +/// share the same Name (e.g. several "http_request" nodes). +/// - "key": The key in the parent node's RuleNodeResult.Data dictionary that holds +/// a boolean string ("true"/"false"). If omitted, falls back to the parent +/// node's RuleNodeResult.Success flag. /// /// Node config: /// "expression" - A JSON-encoded LogicExpression tree. @@ -30,20 +31,20 @@ namespace BotSharp.Core.Rules.Conditions; /// Example: work_order_valid AND (client_name_valid OR NOT affiliate_name_valid) /// /// Given three parent condition nodes: -/// - Node A ("check_work_order") returns Data["work_order_valid"] = "true" -/// - Node B ("check_client") returns Data["client_name_valid"] = "false" -/// - Node C ("check_affiliate") returns Data["affiliate_name_valid"] = "false" +/// - Node A (node_alias "check_work_order") returns Data["work_order_valid"] = "true" +/// - Node B (node_alias "check_client") returns Data["client_name_valid"] = "false" +/// - Node C (node_alias "check_affiliate") returns Data["affiliate_name_valid"] = "false" /// /// The gate node config would be: /// { /// "expression": { /// "op": "and", /// "children": [ -/// { "node": "check_work_order", "key": "work_order_valid" }, +/// { "node_alias": "check_work_order", "key": "work_order_valid" }, /// { "op": "or", "children": [ -/// { "node": "check_client", "key": "client_name_valid" }, +/// { "node_alias": "check_client", "key": "client_name_valid" }, /// { "op": "not", "children": [ -/// { "node": "check_affiliate", "key": "affiliate_name_valid" } +/// { "node_alias": "check_affiliate", "key": "affiliate_name_valid" } /// ]} /// ]} /// ] @@ -142,10 +143,10 @@ public async Task EvaluateAsync( var defaultValue = currentNode.Config?.GetValueOrDefault("default_value") ?? "false"; - // 3. Build lookup: parent node name → its latest RuleFlowStepResult + // 3. Build lookup: parent node alias → its latest RuleFlowStepResult var parentResults = (context.PrevStepResults ?? []) .Where(r => parentNodeIds.Contains(r.Node.Id)) - .GroupBy(r => r.Node.Name, StringComparer.OrdinalIgnoreCase) + .GroupBy(r => r.Node.Alias, StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Key, g => g.Last(), StringComparer.OrdinalIgnoreCase); // 4. Evaluate the expression tree @@ -167,13 +168,13 @@ private bool Evaluate( Dictionary parentResults, string defaultValue) { - // Leaf node: look up a specific parent's result - if (!string.IsNullOrEmpty(expr.Node)) + // Leaf node: look up a specific parent's result by alias + if (!string.IsNullOrEmpty(expr.NodeAlias)) { - if (!parentResults.TryGetValue(expr.Node, out var stepResult)) + if (!parentResults.TryGetValue(expr.NodeAlias, out var stepResult)) { - _logger.LogWarning("Logic gate: parent node '{Node}' not found in results, using default '{Default}'.", - expr.Node, defaultValue); + _logger.LogWarning("Logic gate: parent node alias '{Alias}' not found in results, using default '{Default}'.", + expr.NodeAlias, defaultValue); return ParseBool(defaultValue); } diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 4345e902e..6d4143c1c 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -234,7 +234,7 @@ private async Task ExecuteGraphTraversal( Edge = nextEdge, Graph = graph, Text = text, - Parameters = BuildParameters(nextNode.Config, states, innerData), + Parameters = BuildParameters(states, innerData), PrevStepResults = results, JsonOptions = options?.JsonOptions }; @@ -702,17 +702,11 @@ private void ValidateGraphSchema(RuleGraph graph) #region Private methods private Dictionary BuildParameters( - Dictionary? config, IEnumerable? states, Dictionary? param = null) { var dict = new Dictionary(); - if (config != null) - { - dict = new(config); - } - if (!states.IsNullOrEmpty()) { foreach (var state in states!) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Models/LogicExpression.cs b/src/Infrastructure/BotSharp.Core.Rules/Models/LogicExpression.cs index 7d865e5f2..d1992294f 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Models/LogicExpression.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Models/LogicExpression.cs @@ -4,21 +4,22 @@ namespace BotSharp.Core.Rules.Models; /// /// Represents a node in a logic expression tree used by LogicGateCondition. -/// -/// Leaf node: references a parent node's result by name and an optional custom data key. -/// e.g. { "node": "check_work_order", "key": "work_order_valid" } -/// +/// +/// Leaf node: references a parent node's result by alias and an optional custom data key. +/// e.g. { "node_alias": "check_work_order", "key": "work_order_valid" } +/// /// Operator node: combines children with "and", "or", or "not". /// e.g. { "op": "and", "children": [ ... ] } /// public class LogicExpression { /// - /// For leaf nodes: the parent node name whose result to inspect. + /// For leaf nodes: the parent node alias whose result to inspect. + /// Using Alias avoids collisions when multiple nodes share the same Name. /// - [JsonPropertyName("node")] + [JsonPropertyName("node_alias")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Node { get; set; } + public string? NodeAlias { get; set; } /// /// For leaf nodes: the key in the parent node's Data dictionary that holds the boolean value. diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs index 534ce06b7..6d0fb2924 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/DemoRuleGraph.cs @@ -168,7 +168,7 @@ private RuleGraph BuildGraph(GraphQueryResult result) Weight = sourceNodeWeight, Name = GetGraphItemAttribute(sourceNodeProps, key: "name", defaultValue: "node"), Type = GetGraphItemAttribute(sourceNodeProps, key: "type", defaultValue: "action"), - Purpose = GetGraphItemAttribute(sourceNodeProps, key: "purpose", defaultValue: ""), + Alias = GetGraphItemAttribute(sourceNodeProps, key: "alias", defaultValue: ""), Description = GetGraphItemAttribute(sourceNodeProps, key: "description", defaultValue: ""), Config = GetConfig(sourceNodeProps) }; @@ -181,7 +181,7 @@ private RuleGraph BuildGraph(GraphQueryResult result) Weight = targetNodeWeight, Name = GetGraphItemAttribute(targetNodeProps, key: "name", defaultValue: "node"), Type = GetGraphItemAttribute(targetNodeProps, key: "type", defaultValue: "action"), - Purpose = GetGraphItemAttribute(targetNodeProps, key: "purpose", defaultValue: ""), + Alias = GetGraphItemAttribute(targetNodeProps, key: "alias", defaultValue: ""), Description = GetGraphItemAttribute(targetNodeProps, key: "description", defaultValue: ""), Config = GetConfig(targetNodeProps) }; @@ -192,7 +192,7 @@ private RuleGraph BuildGraph(GraphQueryResult result) Id = edgeId ?? Guid.NewGuid().ToString(), Name = GetGraphItemAttribute(edgeProps, key: "name", defaultValue: "edge"), Type = GetGraphItemAttribute(edgeProps, key: "type", defaultValue: "next"), - Purpose = GetGraphItemAttribute(edgeProps, key: "purpose", defaultValue: ""), + Alias = GetGraphItemAttribute(edgeProps, key: "alias", defaultValue: ""), Description = GetGraphItemAttribute(edgeProps, key: "description", defaultValue: ""), Weight = edgeWeight, Config = GetConfig(edgeProps)