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