From 3a5b1495cf6ea2bd793cfdab86032010319eb022 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 17 Jul 2026 11:49:06 -0500 Subject: [PATCH 1/3] add criteria --- .../Agents/Models/AgentRule.cs | 4 +- .../Rules/Constants/BuiltInRuleCriteria.cs | 19 + .../Rules/IRuleCriteriaEvaluator.cs | 25 + .../BotSharp.Abstraction/Rules/IRuleEngine.cs | 23 +- .../Rules/Models/RuleCriteriaContext.cs | 23 + .../Rules/Options/RuleTriggerOptions.cs | 38 +- .../BotSharp.Core.Rules.csproj | 1 + .../Criteria/Code/CodeCriteriaEvaluator.cs | 125 ++ .../Criteria/Code/CodeCriteriaSettings.cs | 34 + .../Criteria/Llm/LlmCriteriaEvaluator.cs | 166 ++ .../Criteria/Llm/LlmCriteriaSettings.cs | 29 + .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 1345 ++++++++--------- .../BotSharp.Core.Rules/RulesPlugin.cs | 6 + .../Models/AgentRuleMongoElement.cs | 6 +- 14 files changed, 1142 insertions(+), 702 deletions(-) create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Constants/BuiltInRuleCriteria.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteriaEvaluator.cs create mode 100644 src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Criteria/Code/CodeCriteriaEvaluator.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Criteria/Code/CodeCriteriaSettings.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Criteria/Llm/LlmCriteriaEvaluator.cs create mode 100644 src/Infrastructure/BotSharp.Core.Rules/Criteria/Llm/LlmCriteriaSettings.cs diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs index 3ae15d104..f5aed9fec 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/AgentRule.cs @@ -15,6 +15,6 @@ public class AgentRule public class RuleConfig { - [JsonPropertyName("topology_name")] - public string? TopologyName { get; set; } + [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 new file mode 100644 index 000000000..7ddba841e --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Constants/BuiltInRuleCriteria.cs @@ -0,0 +1,19 @@ +namespace BotSharp.Abstraction.Rules.Constants; + +/// +/// Built-in rule criteria types. Each value maps to an +/// registered in DI. +/// Plugins may introduce additional string types. +/// +public static class BuiltInRuleCriteria +{ + /// + /// Evaluate a code script (e.g. Python) that returns a boolean result. + /// + public const string Code = "code"; + + /// + /// Ask an LLM whether the rule applies to the request. + /// + public const string Llm = "llm"; +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteriaEvaluator.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteriaEvaluator.cs new file mode 100644 index 000000000..b54a5a6e3 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleCriteriaEvaluator.cs @@ -0,0 +1,25 @@ +using BotSharp.Abstraction.Rules.Models; + +namespace BotSharp.Abstraction.Rules; + +/// +/// Decides whether a rule should be executed for the current request. +/// Implementations are resolved by in the rule engine, +/// so new criteria mechanisms can be added without changing the engine. +/// +public interface IRuleCriteriaEvaluator +{ + /// + /// The criteria type this evaluator handles + /// + string Type { get; } + + /// + /// Evaluate the criteria for a single agent's rule. + /// + /// The agent whose rule is being considered + /// The rule trigger + /// The per-request criteria context + /// True if the rule should be executed for this request. + Task EvaluateAsync(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context); +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs index 16101a920..d8ac4927d 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs @@ -1,6 +1,3 @@ -using BotSharp.Abstraction.Graph; -using BotSharp.Abstraction.Rules.Models; - namespace BotSharp.Abstraction.Rules; public interface IRuleEngine @@ -17,14 +14,14 @@ public interface IRuleEngine Task> Triggered(IRuleTrigger trigger, string text, IEnumerable? states = null, RuleTriggerOptions? options = null) => throw new NotImplementedException(); - /// - /// Execute rule graph node - /// - /// - /// - /// - /// - /// - /// - Task ExecuteGraphNode(FlowNode node, FlowGraph graph, string agentId, IRuleTrigger trigger, RuleNodeExecutionOptions options); + ///// + ///// Execute rule graph node + ///// + ///// + ///// + ///// + ///// + ///// + ///// + //Task ExecuteGraphNode(FlowNode node, FlowGraph graph, string agentId, IRuleTrigger trigger, RuleNodeExecutionOptions options); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs new file mode 100644 index 000000000..1fd1c52f6 --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleCriteriaContext.cs @@ -0,0 +1,23 @@ +namespace BotSharp.Abstraction.Rules.Models; + +/// +/// The per-request context passed to an +/// to decide whether a rule should be executed. +/// +public class RuleCriteriaContext +{ + /// + /// The trigger message text. + /// + public string Text { get; set; } = string.Empty; + + /// + /// The criteria options (evaluator type and its arguments). + /// + public CriteriaOptions Options { get; set; } = new(); + + /// + /// The conversation states carried with the request. + /// + public IEnumerable? States { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs index 46e35fe86..15b0a1a48 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleTriggerOptions.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Repositories.Filters; +using BotSharp.Abstraction.Rules.Constants; using System.Text.Json; namespace BotSharp.Abstraction.Rules.Options; @@ -11,12 +12,39 @@ public class RuleTriggerOptions public AgentFilter? AgentFilter { get; set; } /// - /// Json serializer options + /// Criteria /// - public JsonSerializerOptions? JsonOptions { get; set; } + public CriteriaOptions? Criteria { get; set; } +} +public class CriteriaOptions +{ /// - /// Rule flow options + /// How the criteria is evaluated (see ). + /// Selects which IRuleCriteriaEvaluator handles this criteria. /// - public RuleFlowOptions? Flow { get; set; } -} + public string Type { get; set; } = BuiltInRuleCriteria.Code; + + /// + /// Evaluator-specific settings, kept as raw JSON so each evaluator can + /// deserialize it into its own strongly-typed settings model. + /// Use to read it. + /// + public JsonElement? Data { get; set; } + + /// + /// Deserialize into an evaluator-specific settings type. + /// Returns default (null) when no data is provided. + /// + public T? GetData(JsonSerializerOptions? options = null) + { + if (Data == null || Data.Value.ValueKind == JsonValueKind.Null || Data.Value.ValueKind == JsonValueKind.Undefined) + { + return default; + } + + return Data.Value.Deserialize(options ?? _webJsonOptions); + } + + private static readonly JsonSerializerOptions _webJsonOptions = new(JsonSerializerDefaults.Web); +} \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core.Rules/BotSharp.Core.Rules.csproj b/src/Infrastructure/BotSharp.Core.Rules/BotSharp.Core.Rules.csproj index 38b59c483..442912f11 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/BotSharp.Core.Rules.csproj +++ b/src/Infrastructure/BotSharp.Core.Rules/BotSharp.Core.Rules.csproj @@ -24,6 +24,7 @@ + diff --git a/src/Infrastructure/BotSharp.Core.Rules/Criteria/Code/CodeCriteriaEvaluator.cs b/src/Infrastructure/BotSharp.Core.Rules/Criteria/Code/CodeCriteriaEvaluator.cs new file mode 100644 index 000000000..5a086c99c --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Criteria/Code/CodeCriteriaEvaluator.cs @@ -0,0 +1,125 @@ +namespace BotSharp.Core.Rules.Criteria.Code; + +/// +/// Evaluates rule trigger criteria by running an agent code script (e.g. Python) +/// that returns a boolean ("true"/"false") result. +/// +public class CodeCriteriaEvaluator : IRuleCriteriaEvaluator +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly CodingSettings _codingSettings; + + public CodeCriteriaEvaluator( + IServiceProvider services, + ILogger logger, + CodingSettings codingSettings) + { + _services = services; + _logger = logger; + _codingSettings = codingSettings; + } + + public string Type => BuiltInRuleCriteria.Code; + + public async Task EvaluateAsync(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) + { + var settings = context.Options.GetData() ?? new(); + var provider = settings.CodeProcessor ?? BuiltInCodeProcessor.PyInterpreter; + var processor = _services.GetServices().FirstOrDefault(x => x.Provider.IsEqualTo(provider)); + if (processor == null) + { + _logger.LogWarning($"Unable to find code processor: {provider}."); + return true; + } + + var agentService = _services.GetRequiredService(); + var scriptName = settings.CodeScriptName ?? $"{trigger.Name}_rule.py"; + var codeScript = await agentService.GetAgentCodeScript(agent.Id, scriptName, scriptType: AgentCodeScriptType.Src); + + var msg = $"rule trigger ({trigger.Name}) code script ({scriptName}) in agent ({agent.Name}) => args: {settings.ArgumentContent?.RootElement.GetRawText()}."; + + if (codeScript == null || string.IsNullOrWhiteSpace(codeScript.Content)) + { + _logger.LogWarning($"Unable to find {msg}."); + return true; + } + + try + { + var hooks = _services.GetHooks(agent.Id); + + var arguments = BuildArguments(settings.ArgumentName, settings.ArgumentContent); + var codeContext = new CodeExecutionContext + { + CodeScript = codeScript, + Arguments = arguments + }; + + foreach (var hook in hooks) + { + await hook.BeforeCodeExecution(agent, codeContext); + } + + var (useLock, useProcess, timeoutSeconds) = CodingUtil.GetCodeExecutionConfig(_codingSettings); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); + var response = processor.Run(codeScript.Content, options: new() + { + ScriptName = scriptName, + Arguments = arguments, + UseLock = useLock, + UseProcess = useProcess + }, cancellationToken: cts.Token); + + var codeResponse = new CodeExecutionResponseModel + { + CodeProcessor = processor.Provider, + CodeScript = codeScript, + Arguments = arguments.DistinctBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value ?? string.Empty), + ExecutionResult = response + }; + + foreach (var hook in hooks) + { + await hook.AfterCodeExecution(agent, codeContext, codeResponse); + } + + if (response == null || !response.Success) + { + _logger.LogWarning($"Failed to handle {msg}"); + return false; + } + + bool result; + LogLevel logLevel; + if (response.Result.IsEqualTo("true")) + { + logLevel = LogLevel.Information; + result = true; + } + else + { + logLevel = LogLevel.Warning; + result = false; + } + + _logger.Log(logLevel, $"Code script execution result ({response}) from {msg}"); + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when handling {msg}"); + return false; + } + } + + private List BuildArguments(string? name, JsonDocument? args) + { + var keyValues = new List(); + if (args != null) + { + keyValues.Add(new KeyValue(name ?? "trigger_args", args.RootElement.GetRawText())); + } + return keyValues; + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Criteria/Code/CodeCriteriaSettings.cs b/src/Infrastructure/BotSharp.Core.Rules/Criteria/Code/CodeCriteriaSettings.cs new file mode 100644 index 000000000..679a8c71f --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Criteria/Code/CodeCriteriaSettings.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Core.Rules.Criteria.Code; + +/// +/// Settings for , parsed from +/// CriteriaOptions.Data. +/// +public class CodeCriteriaSettings +{ + /// + /// Code processor provider (defaults to the Python interpreter). + /// + [JsonPropertyName("code_processor")] + public string? CodeProcessor { get; set; } + + /// + /// Code script name. + /// + [JsonPropertyName("code_script_name")] + public string? CodeScriptName { get; set; } + + /// + /// Argument name as an input key to the code script. + /// + [JsonPropertyName("argument_name")] + public string? ArgumentName { get; set; } + + /// + /// Json arguments as an input value to the code script. + /// + [JsonPropertyName("argument_content")] + public JsonDocument? ArgumentContent { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Criteria/Llm/LlmCriteriaEvaluator.cs b/src/Infrastructure/BotSharp.Core.Rules/Criteria/Llm/LlmCriteriaEvaluator.cs new file mode 100644 index 000000000..67550514c --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Criteria/Llm/LlmCriteriaEvaluator.cs @@ -0,0 +1,166 @@ +using BotSharp.Abstraction.Templating; +using BotSharp.Core.Infrastructures; + +namespace BotSharp.Core.Rules.Criteria.Llm; + +/// +/// Evaluates rule trigger criteria by asking an LLM whether the request meets a +/// natural-language condition. Renders the "criteria_check" template (which instructs +/// the model to answer "1" for met / "0" for not met) as the system prompt and calls +/// the chat completion provider directly. +/// +/// Note: LLM evaluation is non-deterministic and network-dependent. It fails closed +/// (returns false) on any error, empty response, or unparseable answer. +/// +public class LlmCriteriaEvaluator : IRuleCriteriaEvaluator +{ + private const string DefaultTemplateName = "criteria_check"; + + private readonly IServiceProvider _services; + private readonly ILogger _logger; + + public LlmCriteriaEvaluator( + IServiceProvider services, + ILogger logger) + { + _services = services; + _logger = logger; + } + + public string Type => BuiltInRuleCriteria.Llm; + + public async Task EvaluateAsync(Agent agent, IRuleTrigger trigger, RuleCriteriaContext context) + { + var settings = context.Options.GetData() ?? new(); + var rule = agent.Rules.FirstOrDefault(x => x.TriggerName.IsEqualTo(trigger.Name)); + + // The Rules agent hosts the criteria-check template by default. + var agentId = !string.IsNullOrWhiteSpace(settings.AgentId) ? settings.AgentId! : BuiltInAgentId.RulesInterpreter; + var templateName = !string.IsNullOrWhiteSpace(settings.TemplateName) ? settings.TemplateName! : DefaultTemplateName; + + var input = BuildInput(rule?.Config, settings); + var msg = $"rule trigger ({trigger.Name}) llm criteria (agent {agentId}, template {templateName})."; + + try + { + var agentService = _services.GetRequiredService(); + var innerAgent = await agentService.GetAgent(agentId); + if (innerAgent == null) + { + _logger.LogWarning($"Unable to find agent for {msg}"); + return true; + } + + // Render the template as the system instruction, exposing the request states. + var render = _services.GetRequiredService(); + var template = innerAgent.Templates.FirstOrDefault(x => x.Name.IsEqualTo(templateName)); + if (template == null || string.IsNullOrWhiteSpace(template.Content)) + { + _logger.LogWarning($"Unable to find agent template for {msg}"); + return true; + } + + var instruction = render.Render(template.Content, BuildRenderData(context)); + + // Prefer the template's own LLM config when it is fully specified. + var llmConfig = innerAgent.LlmConfig; + if (template.LlmConfig?.IsValid == true) + { + llmConfig = new AgentLlmConfig(template.LlmConfig); + } + + var completer = CompletionProvider.GetChatCompletion(_services, agentConfig: llmConfig); + if (completer == null) + { + _logger.LogWarning($"Unable to resolve chat completion provider for {msg}"); + return false; + } + + var response = await completer.GetChatCompletions(new Agent + { + Id = innerAgent.Id, + Name = innerAgent.Name, + Instruction = instruction, + LlmConfig = llmConfig + }, new List + { + new RoleDialogModel(AgentRole.User, input) + }); + + var answer = response?.Content?.Trim() ?? string.Empty; + if (string.IsNullOrEmpty(answer)) + { + _logger.LogWarning($"Empty llm response for {msg}"); + return false; + } + + var isTriggered = ParseResult(answer); + _logger.Log(isTriggered ? LogLevel.Information : LogLevel.Warning, + $"Llm criteria result ({answer}) => {isTriggered} for {msg}"); + return isTriggered; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error when handling {msg}"); + return false; + } + } + + private static Dictionary BuildRenderData(RuleCriteriaContext context) + { + var data = new Dictionary(); + if (context.States.IsNullOrEmpty()) + { + return data; + } + + foreach (var state in context.States!) + { + if (string.IsNullOrEmpty(state.Key)) + { + continue; + } + + data[state.Key] = state.Value; + } + + return data; + } + + private static string BuildInput(RuleConfig? ruleConfig, LlmCriteriaSettings settings) + { + var sb = new StringBuilder(); + + if (!string.IsNullOrWhiteSpace(ruleConfig?.Criteria)) + { + sb.AppendLine("## Rule"); + sb.AppendLine(ruleConfig.Criteria); + sb.AppendLine(); + } + + var arguments = settings.ArgumentContent?.RootElement.GetRawText(); + if (!string.IsNullOrWhiteSpace(arguments) && arguments != "{}") + { + sb.AppendLine("## Input"); + sb.AppendLine(arguments); + } + + return sb.ToString().Trim(); + } + + private static bool ParseResult(string answer) + { + if (answer.IsEqualTo("1") || answer.IsEqualTo("true") || answer.IsEqualTo("yes")) + { + return true; + } + + if (answer.IsEqualTo("0") || answer.IsEqualTo("false") || answer.IsEqualTo("no")) + { + return false; + } + + // Fall back to the leading token; the template constrains output to "1"/"0". + return answer.StartsWith("1", StringComparison.Ordinal); + } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Criteria/Llm/LlmCriteriaSettings.cs b/src/Infrastructure/BotSharp.Core.Rules/Criteria/Llm/LlmCriteriaSettings.cs new file mode 100644 index 000000000..1bb0bd4d1 --- /dev/null +++ b/src/Infrastructure/BotSharp.Core.Rules/Criteria/Llm/LlmCriteriaSettings.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace BotSharp.Core.Rules.Criteria.Llm; + +/// +/// Settings for , parsed from +/// CriteriaOptions.Data. +/// +public class LlmCriteriaSettings +{ + /// + /// The agent that hosts the criteria-check template. + /// Defaults to the built-in Rules agent. + /// + [JsonPropertyName("agent_id")] + public string? AgentId { get; set; } + + /// + /// The template used as the system prompt. Defaults to "criteria_check". + /// + [JsonPropertyName("template_name")] + public string? TemplateName { get; set; } + + /// + /// Json arguments as an input value + /// + [JsonPropertyName("argument_content")] + public JsonDocument? ArgumentContent { get; set; } +} diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 536937952..7ce393dda 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Graph.Models; - namespace BotSharp.Core.Rules.Engines; public class RuleEngine : IRuleEngine @@ -29,6 +27,18 @@ public async Task> Triggered(IRuleTrigger trigger, string te } }); + // Resolve the criteria evaluator + IRuleCriteriaEvaluator? criteriaEvaluator = null; + if (options?.Criteria != null) + { + criteriaEvaluator = _services.GetServices() + .FirstOrDefault(x => x.Type.IsEqualTo(options.Criteria.Type)); + if (criteriaEvaluator == null) + { + _logger.LogWarning($"Unable to find rule criteria evaluator for type ({options.Criteria.Type})."); + } + } + // Trigger agents var filteredAgents = agents.Items.Where(x => x.Rules.Exists(r => r.TriggerName.IsEqualTo(trigger.Name) && !x.Disabled)).ToList(); foreach (var agent in filteredAgents) @@ -39,695 +49,672 @@ public async Task> Triggered(IRuleTrigger trigger, string te continue; } - var ruleConfig = rule.Config; - var ruleFlowTopologyName = options?.Flow?.TopologyName ?? ruleConfig?.TopologyName; - - if (!string.IsNullOrEmpty(ruleFlowTopologyName)) - { - // Execute graph - // 1. Load graph - var graph = await LoadGraph(ruleFlowTopologyName, agent, trigger, options?.Flow); - if (graph == null) - { - continue; - } - - // 2. Get root node - var param = options?.Flow?.Parameters; - var rootNodeName = param != null ? param.GetValueOrDefault("root_node_name")?.ToString() : null; - var root = graph.GetRootNode(rootNodeName); - if (root == null) - { - graph.Clear(); - continue; - } - - // 3. Execute graph - var execResults = new List(); - await ExecuteGraphNode(root, graph, agent, trigger, text, states, null, options, execResults); - graph.Clear(); - - // Get conversation id to support legacy features - var convIds = execResults.Where(x => x.Success && x.Data.TryGetValue("conversation_id", out _)) - .Select(x => x.Data.GetValueOrDefault("conversation_id", string.Empty)) - .Where(x => !string.IsNullOrEmpty(x)) - .ToList(); - - newConversationIds.AddRange(convIds); - } - else - { - var convId = await SendMessageToAgent(agent, trigger, text, states); - newConversationIds.Add(convId); - } - } - - return newConversationIds; - } - - 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 + if (criteriaEvaluator != null) { - 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) + var criteriaContext = new RuleCriteriaContext { - 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 ?? []); + Text = text, + Options = options!.Criteria!, + States = states + }; - if (actionResult == null) + var isTriggered = await criteriaEvaluator.EvaluateAsync(agent, trigger, criteriaContext); + if (!isTriggered) { - 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; + var convId = await SendMessageToAgent(agent, trigger, text, states); + newConversationIds.Add(convId); } - 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 - }; - } + return newConversationIds; } - // 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 +// 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 diff --git a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs index ee50c9b7d..eb2a6552b 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/RulesPlugin.cs @@ -1,5 +1,7 @@ using BotSharp.Core.Rules.Actions; using BotSharp.Core.Rules.Conditions; +using BotSharp.Core.Rules.Criteria.Code; +using BotSharp.Core.Rules.Criteria.Llm; using BotSharp.Core.Rules.Engines; using BotSharp.Core.Rules.Root; @@ -38,6 +40,10 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) services.AddScoped(); services.AddScoped(); + // Register rule criteria evaluators + services.AddScoped(); + services.AddScoped(); + #if DEBUG // Register rule trigger services.AddScoped(); diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs index b481394a2..978a797a6 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentRuleMongoElement.cs @@ -33,7 +33,7 @@ public static AgentRule ToDomainElement(AgentRuleMongoElement rule) [BsonIgnoreExtraElements(Inherited = true)] public class RuleConfigMongoModel { - public string? TopologyName { get; set; } + public string? Criteria { get; set; } public static RuleConfigMongoModel? ToMongoModel(RuleConfig? config) { @@ -44,7 +44,7 @@ public class RuleConfigMongoModel return new RuleConfigMongoModel { - TopologyName = config.TopologyName + Criteria = config.Criteria }; } @@ -57,7 +57,7 @@ public class RuleConfigMongoModel return new RuleConfig { - TopologyName = config.TopologyName + Criteria = config.Criteria }; } } From 6348ca6b959d548bda48d97ee3b7f4a32b84e67e Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 17 Jul 2026 16:48:54 -0500 Subject: [PATCH 2/3] refine code script generation --- .../Coding/Options/CodeGenerationOptions.cs | 2 +- .../agent.json | 4 +- ...e-trigger-code-generate_instruction.liquid | 2 +- .../Agent/AgentController.Coding.cs | 2 - .../Services/PyCodeInterpreter.cs | 65 ++++++++++++++----- 5 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenerationOptions.cs b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenerationOptions.cs index de886f5ef..2cc77b35a 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenerationOptions.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Coding/Options/CodeGenerationOptions.cs @@ -1,6 +1,6 @@ namespace BotSharp.Abstraction.Coding.Options; -public class CodeGenerationOptions : LlmConfigBase +public class CodeGenerationOptions { /// /// Agent id to get instruction diff --git a/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/agent.json b/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/agent.json index 31c38e574..87cb9185a 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/agent.json +++ b/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/agent.json @@ -11,8 +11,8 @@ "llmConfig": { "is_inherit": false, "provider": "openai", - "model": "gpt-5", + "model": "gpt-5.4-mini", "max_recursion_depth": 3, - "reasoning_effort_level": "minimal" + "reasoning_effort_level": "low" } } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/templates/rule-trigger-code-generate_instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/templates/rule-trigger-code-generate_instruction.liquid index 2293ede7b..fac006e6d 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/templates/rule-trigger-code-generate_instruction.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/templates/rule-trigger-code-generate_instruction.liquid @@ -3,7 +3,7 @@ Based on user's request, help generate a refined python code of function definit User's request is {{user_request}} Couple of notes to address: -1. You need to generate a function named check_trigger_criterion(args), where input is a json object. The example of this json object is {{args_example}}. +1. You need to generate a function named check_trigger_criterion(args), where input is a json object. {% if args_example != empty %} The example of this json object is {{args_example}}. {% endif %} 2. The input to this function comes from the arguments. You must use "ArgumentParser" to take an argument named "trigger_args", and then use "parse_known_args" to get the raw args. 3. After getting the raw args, you need to use 'clean_args = bytes(raw_args, "utf-8").decode("unicode_escape")' to clean the raw argument, and then use 'args = json.loads(clean_args)' to get the json args. 4. Based on the user's request and input args, generate the function logic and ONLY return a boolean value. diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Coding.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Coding.cs index 40800d35c..03a03189b 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Coding.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/Agent/AgentController.Coding.cs @@ -61,8 +61,6 @@ public async Task GenerateAgentCodeScript([FromRoute] stri var states = request.Options?.Data?.ToList(); var state = _services.GetRequiredService(); states?.ForEach(x => state.SetState(x.Key, x.Value, source: StateSource.External)); - state.SetState("code_processor", request.Options?.Processor, source: StateSource.External); - state.SetState("programming_language", request.Options?.ProgrammingLanguage, source: StateSource.External); var result = await _agentService.GenerateCodeScript(agentId, request.Text, request?.Options); return result; diff --git a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs index 42bc1480c..3da35b4b8 100644 --- a/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs +++ b/src/Plugins/BotSharp.Plugin.PythonInterpreter/Services/PyCodeInterpreter.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Templating; using Microsoft.Extensions.Logging; using Python.Runtime; using System.Diagnostics; @@ -41,8 +42,8 @@ public async Task GenerateCodeScriptAsync(string text, Cod { Agent? agent = null; - var agentId = options?.AgentId; - var templateName = options?.TemplateName; + var agentId = options?.AgentId ?? BuiltInAgentId.AIProgrammer; + var templateName = options?.TemplateName ?? "rule-trigger-code-generate_instruction"; if (!string.IsNullOrEmpty(agentId)) { @@ -50,26 +51,42 @@ public async Task GenerateCodeScriptAsync(string text, Cod agent = await agentService.GetAgent(agentId); } - var instruction = string.Empty; - if (agent != null && !string.IsNullOrEmpty(templateName)) + var template = agent?.Templates?.FirstOrDefault(x => x.Name.IsEqualTo(templateName)); + var llmConfig = agent?.LlmConfig; + if (template?.LlmConfig?.IsValid == true) { - instruction = agent.Templates?.FirstOrDefault(x => x.Name.IsEqualTo(templateName))?.Content; + llmConfig = new AgentLlmConfig(template.LlmConfig); } - var (provider, model) = GetLlmProviderModel(); - var innerAgent = new Agent + if (llmConfig == null) { - Id = agent?.Id ?? BuiltInAgentId.AIProgrammer, - Name = agent?.Name ?? "AI Programmer", - Instruction = instruction, - LlmConfig = new AgentLlmConfig + var (provider, model) = GetLlmProviderModel(); + llmConfig = new AgentLlmConfig { - Provider = options?.Provider ?? provider, - Model = options?.Model ?? model, - MaxOutputTokens = options?.MaxOutputTokens ?? _settings?.CodeGeneration?.MaxOutputTokens, - ReasoningEffortLevel = options?.ReasoningEffortLevel ?? _settings?.CodeGeneration?.ReasoningEffortLevel - }, - TemplateDict = options?.Data ?? new() + Provider = provider, + Model = model + }; + } + + var instruction = template?.Content ?? agent?.Instruction; + if (string.IsNullOrWhiteSpace(instruction)) + { + return new CodeGenerationResult + { + Success = false + }; + } + + var renderData = CollectRenderData(options?.Data); + var render = _services.GetRequiredService(); + var rendered = render.Render(instruction, renderData); + var innerAgent = new Agent + { + Id = agent!.Id, + Name = agent!.Name, + Instruction = rendered, + LlmConfig = llmConfig, + TemplateDict = renderData }; text = text.IfNullOrEmptyAs("Please follow the instruction to generate code script.")!; @@ -326,6 +343,20 @@ private async Task CoreRunProcess(string codeScript, Code } } + private Dictionary CollectRenderData(Dictionary? data = null) + { + var renderData = new Dictionary(data ?? []); + + var stateService = _services.GetRequiredService(); + var states = stateService.GetStates(); + foreach (var state in states) + { + renderData[state.Key] = state.Value; + } + + return renderData; + } + private (string, string) GetLlmProviderModel() { var provider = _settings.CodeGeneration?.Provider; From 41e21fb02fdb9ced4408b45187d52d94450ed591 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 17 Jul 2026 17:15:23 -0500 Subject: [PATCH 3/3] refine instruction --- .../rule-trigger-code-generate_instruction.liquid | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/templates/rule-trigger-code-generate_instruction.liquid b/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/templates/rule-trigger-code-generate_instruction.liquid index fac006e6d..c72cbcf53 100644 --- a/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/templates/rule-trigger-code-generate_instruction.liquid +++ b/src/Infrastructure/BotSharp.Core/data/agents/c2a2faf6-b8b5-47fe-807b-f4714cf25dd4/templates/rule-trigger-code-generate_instruction.liquid @@ -1,10 +1,9 @@ -Based on user's request, help generate a refined python code of function definition, only using base python packages, that can return a boolean value of true or false to determine if based on known states, the function can determine if conditions are met. - +Strictly based on user's request, help generate a refined python code of function definition, only using base python packages, that can return a boolean value of true or false. User's request is {{user_request}} Couple of notes to address: -1. You need to generate a function named check_trigger_criterion(args), where input is a json object. {% if args_example != empty %} The example of this json object is {{args_example}}. {% endif %} -2. The input to this function comes from the arguments. You must use "ArgumentParser" to take an argument named "trigger_args", and then use "parse_known_args" to get the raw args. +1. You need to generate a function named check_trigger_criterion(args), where input is a json object. {% if args_example %} The example of this json object is {{args_example}}. {% endif %} +2. The input to this function comes from the arguments. You must use "ArgumentParser" to take a named argument called "trigger_args" that allows pass from command line, and then use "parse_known_args" to get the raw args. 3. After getting the raw args, you need to use 'clean_args = bytes(raw_args, "utf-8").decode("unicode_escape")' to clean the raw argument, and then use 'args = json.loads(clean_args)' to get the json args. 4. Based on the user's request and input args, generate the function logic and ONLY return a boolean value. 5. You must only call check_trigger_criterion with the parsed json args in "if __name__ == '__main__'" block, and print only the function output. If any error occurs, print "Error". @@ -12,4 +11,4 @@ Couple of notes to address: 7. You can use try-except blocks to catch any errors, and return "Error" if any error occurs. Do not use sys.exit(0) to exit the program. 8. Use as fewer comments and try-except blocks as possible. -Output the executable code directly in order to directly save it to a py file. \ No newline at end of file +Output the executable code directly without any markup symbols. \ No newline at end of file