From a85f9716bf837c8b36eb05c9a82cb6b91319d8ed Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 28 May 2026 18:21:35 -0500 Subject: [PATCH 1/6] add membase util --- .../Agents/IInstructionResolver.cs | 2 +- .../Templating/RenderConfiguration.cs | 10 +- .../BotSharp.Plugin.Membase.csproj | 10 ++ .../Functions/UtilAdvanceWorkflowCursorFn.cs | 48 ++++++ .../Hooks/MembaseUtilityHook.cs | 29 ++++ .../BotSharp.Plugin.Membase/MembasePlugin.cs | 4 + .../Services/MembaseInstructionResolver.cs | 117 +++++++++++++++ src/Plugins/BotSharp.Plugin.Membase/Using.cs | 2 + .../Utils/GraphBuilder.cs | 140 ++++++++++++++++++ ...util-workflow-advance_workflow_cursor.json | 16 ++ 10 files changed, 372 insertions(+), 6 deletions(-) create mode 100644 src/Plugins/BotSharp.Plugin.Membase/Functions/UtilAdvanceWorkflowCursorFn.cs create mode 100644 src/Plugins/BotSharp.Plugin.Membase/Hooks/MembaseUtilityHook.cs create mode 100644 src/Plugins/BotSharp.Plugin.Membase/Services/MembaseInstructionResolver.cs create mode 100644 src/Plugins/BotSharp.Plugin.Membase/Utils/GraphBuilder.cs create mode 100644 src/Plugins/BotSharp.Plugin.Membase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-workflow-advance_workflow_cursor.json diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IInstructionResolver.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IInstructionResolver.cs index f1c65b488..ed8c3949e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IInstructionResolver.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IInstructionResolver.cs @@ -2,7 +2,7 @@ namespace BotSharp.Abstraction.Agents; public interface IInstructionResolver { - string Name { get; } + string Provider { get; } Task ResolveAsync(Agent agent, string instruction, IEnumerable args, IDictionary kwArgs) => Task.FromResult(instruction); diff --git a/src/Infrastructure/BotSharp.Core/Templating/RenderConfiguration.cs b/src/Infrastructure/BotSharp.Core/Templating/RenderConfiguration.cs index b82a4b93b..63527918a 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/RenderConfiguration.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/RenderConfiguration.cs @@ -46,9 +46,9 @@ public RenderConfiguration( return RenderLinkTag(expression, writer, encoder, context); }); - _parser.RegisterExpressionBlock("resolve", (Expression expression, IReadOnlyList statements, TextWriter writer, TextEncoder encoder, TemplateContext context) => + _parser.RegisterExpressionBlock("render_graph", (Expression expression, IReadOnlyList statements, TextWriter writer, TextEncoder encoder, TemplateContext context) => { - return RenderResolveBlock(expression, statements, writer, encoder, context); + return RenderGraphBlock(expression, statements, writer, encoder, context); }); } @@ -153,7 +153,7 @@ private static async ValueTask RenderLinkTag( return Completion.Normal; } - private static async ValueTask RenderResolveBlock( + private static async ValueTask RenderGraphBlock( Expression expression, IReadOnlyList statements, TextWriter writer, @@ -164,10 +164,10 @@ private static async ValueTask RenderResolveBlock( { var value = await expression.EvaluateAsync(context); var spec = AsSpec(value); - var resolverName = spec.Name; + var provider = spec.Name; var resolver = GetServiceProvider(context)?.GetServices() - .FirstOrDefault(x => x.Name.IsEqualTo(resolverName)); + .FirstOrDefault(x => x.Provider.IsEqualTo(provider)); var passThrough = resolver != null; using var blockWriter = new StringWriter(); diff --git a/src/Plugins/BotSharp.Plugin.Membase/BotSharp.Plugin.Membase.csproj b/src/Plugins/BotSharp.Plugin.Membase/BotSharp.Plugin.Membase.csproj index 864ce6db8..0cb6f4d67 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/BotSharp.Plugin.Membase.csproj +++ b/src/Plugins/BotSharp.Plugin.Membase/BotSharp.Plugin.Membase.csproj @@ -9,6 +9,16 @@ $(SolutionDir)packages + + + + + + + PreserveNewest + + + diff --git a/src/Plugins/BotSharp.Plugin.Membase/Functions/UtilAdvanceWorkflowCursorFn.cs b/src/Plugins/BotSharp.Plugin.Membase/Functions/UtilAdvanceWorkflowCursorFn.cs new file mode 100644 index 000000000..7115a5329 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Membase/Functions/UtilAdvanceWorkflowCursorFn.cs @@ -0,0 +1,48 @@ +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Functions; +using System.Text.Json.Serialization; + +namespace BotSharp.Plugin.Membase.Functions; + +public class UtilAdvanceWorkflowCursorFn : IFunctionCallback +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly IConversationStateService _states; + + public UtilAdvanceWorkflowCursorFn( + IServiceProvider services, + ILogger logger, + IConversationStateService states) + { + _services = services; + _logger = logger; + _states = states; + } + + public string Name => "util-workflow-advance_workflow_cursor"; + + public string Indication => "Advancing workflow step"; + + public async Task Execute(RoleDialogModel message) + { + var args = JsonSerializer.Deserialize(message.FunctionArgs ?? "{}"); + var nextNodeId = args?.NextNodeId; + + if (string.IsNullOrWhiteSpace(nextNodeId)) + { + nextNodeId = _states.GetState("start_node_id", ""); + _states.SetState("next_node_id", nextNodeId); + } + + message.Content = $"The next node id is '{nextNodeId}'"; + return true; + } +} + +public class WorkflowCursorArgs +{ + [JsonPropertyName("next_node_id")] + public string NextNodeId { get; set; } +} \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Membase/Hooks/MembaseUtilityHook.cs b/src/Plugins/BotSharp.Plugin.Membase/Hooks/MembaseUtilityHook.cs new file mode 100644 index 000000000..93aafed53 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Membase/Hooks/MembaseUtilityHook.cs @@ -0,0 +1,29 @@ +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Agents.Models; + +namespace BotSharp.Plugin.Membase.Hooks; + +public class MembaseUtilityHook : IAgentUtilityHook +{ + private const string ADVANCE_CURSOR_FN = "util-workflow-advance_workflow_cursor"; + + public void AddUtilities(List utilities) + { + var items = new List + { + new AgentUtility + { + Category = "workflow", + Name = "advance-workflow-cursor", + Items = [ + new UtilityItem + { + FunctionName = ADVANCE_CURSOR_FN + } + ] + } + }; + + utilities.AddRange(items); + } +} diff --git a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs index 6b38cd84d..cb828a3b2 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/MembasePlugin.cs @@ -1,6 +1,8 @@ +using BotSharp.Abstraction.Agents; using BotSharp.Abstraction.Plugins.Models; using BotSharp.Plugin.Membase.GraphDb; using BotSharp.Plugin.Membase.Handlers; +using BotSharp.Plugin.Membase.Hooks; using Refit; namespace BotSharp.Plugin.Membase; @@ -36,6 +38,8 @@ public void RegisterDI(IServiceCollection services, IConfiguration config) }); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); _membaseCredential = config.GetValue("Membase:ApiKey") ?? string.Empty; _membaseProjectId = config.GetValue("Membase:ProjectId") ?? string.Empty; diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseInstructionResolver.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseInstructionResolver.cs new file mode 100644 index 000000000..61700372d --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseInstructionResolver.cs @@ -0,0 +1,117 @@ +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Templating; +using BotSharp.Abstraction.Templating.Constants; +using BotSharp.Abstraction.Utilities; + +namespace BotSharp.Plugin.Membase.Services; + +public class MembaseInstructionResolver : IInstructionResolver +{ + private readonly IServiceProvider _services; + private readonly ILogger _logger; + private readonly IConversationStateService _states; + + private const int LIMIT = 300; + + public string Provider => "membase"; + + public MembaseInstructionResolver( + IServiceProvider services, + ILogger logger, + IConversationStateService states) + { + _services = services; + _logger = logger; + _states = states; + } + + public async Task ResolveAsync(Agent agent, string instruction, IEnumerable args, IDictionary kwArgs) + { + string? graphId = kwArgs?.TryGetValue("graphId", out string? gId) == true ? gId : null; + string? format = kwArgs?.TryGetValue("format", out string? f) == true ? f : null; + string? startNodeId = kwArgs?.TryGetValue("startNodeId", out string? s) == true ? s : null; + int hop = kwArgs?.TryGetValue("hop", out var h) == true && h != null ? Convert.ToInt32(h) : 0; + + if (string.IsNullOrEmpty(startNodeId)) + { + return instruction; + } + + _states.SetState("start_node_id", startNodeId); + var graphDb = _services.GetServices().First(x => x.Provider.IsEqualTo(Provider)); + + if (format.IsEqualTo("complete graph")) + { + var query = """ + MATCH path = (p1)-[r*0..]->(p2) + WHERE p1.id = $startNodeId + UNWIND relationships(path) AS rel + RETURN DISTINCT startNode(rel) AS sourceNode, rel as edge, endNode(rel) AS targetNode + LIMIT $limit + """; + + var result = await graphDb.ExecuteQueryAsync(query, options: new() + { + GraphId = graphId, + Arguments = new() + { + ["startNodeId"] = startNodeId, + ["limit"] = LIMIT + } + }); + + var data = _states.GetStates(); + var graph = GraphBuilder.Build(result, data); + var renderData = data.ToDictionary(x => x.Key, x => (object)x.Value); + renderData["workflow_graph"] = JsonSerializer.Serialize(graph.GetGraphInfo(), BotSharpOptions.defaultJsonOptions); + renderData[TemplateRenderConstant.RENDER_AGENT] = agent; + + var render = _services.GetRequiredService(); + instruction = render.Render(instruction, renderData); + } + else if (format.IsEqualTo("partial graph")) + { + var nextNodeId = _states.GetState("next_node_id"); + if (string.IsNullOrEmpty(nextNodeId)) + { + nextNodeId = startNodeId; + _states.SetState("next_node_id", nextNodeId); + } + + hop = hop > 0 ? hop : 1; + var query = $""" + MATCH path = (p1)-[r*0..{hop}]->(p2) + WHERE p1.id = $startNodeId + UNWIND relationships(path) AS rel + RETURN DISTINCT startNode(rel) AS sourceNode, rel as edge, endNode(rel) AS targetNode + LIMIT $limit + """; + + var result = await graphDb.ExecuteQueryAsync(query, options: new() + { + GraphId = graphId, + Arguments = new() + { + ["startNodeId"] = nextNodeId, + ["limit"] = LIMIT + } + }); + + if (!result.Values.IsNullOrEmpty()) + { + var data = _states.GetStates(); + var graph = GraphBuilder.Build(result, data); + var renderData = data.ToDictionary(x => x.Key, x => (object)x.Value); + renderData["workflow_graph"] = JsonSerializer.Serialize(graph.GetGraphInfo(), BotSharpOptions.defaultJsonOptions); + renderData[TemplateRenderConstant.RENDER_AGENT] = agent; + + var render = _services.GetRequiredService(); + instruction = render.Render(instruction, renderData); + } + } + + return instruction; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Membase/Using.cs b/src/Plugins/BotSharp.Plugin.Membase/Using.cs index 675b129e8..dbd3365fa 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Using.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Using.cs @@ -24,3 +24,5 @@ global using BotSharp.Abstraction.Graph.Requests; global using BotSharp.Abstraction.Options; global using BotSharp.Plugin.Membase.Interfaces; +global using BotSharp.Plugin.Membase.Utils; +global using BotSharp.Plugin.Membase.Services; \ No newline at end of file diff --git a/src/Plugins/BotSharp.Plugin.Membase/Utils/GraphBuilder.cs b/src/Plugins/BotSharp.Plugin.Membase/Utils/GraphBuilder.cs new file mode 100644 index 000000000..f050e7be6 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Membase/Utils/GraphBuilder.cs @@ -0,0 +1,140 @@ +using BotSharp.Abstraction.Rules; +using BotSharp.Abstraction.Utilities; +using System.Text.RegularExpressions; + +namespace BotSharp.Plugin.Membase.Utils; + +public static class GraphBuilder +{ + public static RuleGraph Build(GraphQueryResult result, IDictionary? data = null) + { + var graph = RuleGraph.Init(); + if (result.Values.IsNullOrEmpty()) + { + return graph; + } + + foreach (var item in result.Values) + { + // Try to deserialize nodes and edge from the dictionary + if (!item.TryGetValue("sourceNode", out var sourceNodeElement) || + !item.TryGetValue("targetNode", out var targetNodeElement) || + !item.TryGetValue("edge", out var edgeElement)) + { + continue; + } + + // Parse source node + var sourceNodeId = sourceNodeElement.TryGetProperty("id", out var sId) ? sId.GetString() : null; + var sourceNodeLabels = sourceNodeElement.TryGetProperty("labels", out var sLabels) + ? sLabels.EnumerateArray().Select(x => x.GetString() ?? "").ToList() + : []; + var sourceNodeProps = sourceNodeElement.TryGetProperty("properties", out var sProps) + ? sProps + : default; + + // Parse target node + var targetNodeId = targetNodeElement.TryGetProperty("id", out var tId) ? tId.GetString() : null; + var targetNodeLabels = targetNodeElement.TryGetProperty("labels", out var tLabels) + ? tLabels.EnumerateArray().Select(x => x.GetString() ?? "").ToList() + : []; + var targetNodeProps = targetNodeElement.TryGetProperty("properties", out var tProps) + ? tProps + : default; + + // Parse edge + var edgeId = edgeElement.TryGetProperty("id", out var eId) ? eId.GetString() : null; + var edgeProps = edgeElement.TryGetProperty("properties", out var eProps) + ? eProps + : default; + + // Create source node + var sourceNode = new RuleNode() + { + Id = sourceNodeId ?? Guid.NewGuid().ToString(), + Labels = sourceNodeLabels, + Name = GetGraphItemAttribute(sourceNodeProps, key: "name", data: data), + Type = GetGraphItemAttribute(sourceNodeProps, key: "type", data: data), + Description = GetGraphItemAttribute(sourceNodeProps, key: "description", data: data), + Config = GetConfig(sourceNodeProps, data) + }; + + // Create target node + var targetNode = new RuleNode() + { + Id = targetNodeId ?? Guid.NewGuid().ToString(), + Labels = targetNodeLabels, + Name = GetGraphItemAttribute(targetNodeProps, key: "name", data: data), + Type = GetGraphItemAttribute(targetNodeProps, key: "type", data: data), + Description = GetGraphItemAttribute(targetNodeProps, key: "description", data: data), + Config = GetConfig(targetNodeProps, data) + }; + + // Create edge payload + var edgePayload = new EdgeItemPayload() + { + Id = edgeId ?? Guid.NewGuid().ToString(), + Name = GetGraphItemAttribute(edgeProps, key: "name", data: data), + Type = GetGraphItemAttribute(edgeProps, key: "type", defaultValue: "NEXT", data: data), + Description = GetGraphItemAttribute(edgeProps, key: "description", data: data), + Config = GetConfig(edgeProps, data) + }; + + // Add edge to graph + graph.AddEdge(sourceNode, targetNode, edgePayload); + } + + return graph; + } + + private static string GetGraphItemAttribute(JsonElement? properties, string key, string defaultValue = null, IDictionary? data = null) + { + if (properties == null || properties.Value.ValueKind == JsonValueKind.Undefined) + { + return defaultValue; + } + + if (properties.Value.TryGetProperty(key, out var name) && name.ValueKind == JsonValueKind.String) + { + return ReplacePlaceholders(name.GetString(), data) ?? defaultValue; + } + + return defaultValue; + } + + private static Dictionary GetConfig(JsonElement? properties, IDictionary? data = null) + { + var config = new Dictionary(); + + if (properties == null || properties.Value.ValueKind == JsonValueKind.Undefined) + { + return config; + } + + // Convert all properties to config dictionary + foreach (var prop in properties.Value.EnumerateObject()) + { + config[prop.Name] = ReplacePlaceholders(prop.Value.ConvertToString(), data); + } + + return config; + } + + private static readonly Regex PlaceholderRegex = new(@"\{\{(\w+)\}\}", RegexOptions.Compiled); + + private static string? ReplacePlaceholders(string? value, IDictionary? data) + { + if (data == null || string.IsNullOrEmpty(value) || !value.Contains("{{")) + { + return value; + } + + return PlaceholderRegex.Replace(value, match => + { + var key = match.Groups[1].Value; + return data.TryGetValue(key, out var replacement) && replacement != null + ? replacement + : match.Value; + }); + } +} diff --git a/src/Plugins/BotSharp.Plugin.Membase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-workflow-advance_workflow_cursor.json b/src/Plugins/BotSharp.Plugin.Membase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-workflow-advance_workflow_cursor.json new file mode 100644 index 000000000..edb760378 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Membase/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-workflow-advance_workflow_cursor.json @@ -0,0 +1,16 @@ +{ + "name": "util-workflow-advance_workflow_cursor", + "description": "Call this when you finish the current workflow step and transition to a next one in the same turn. Do NOT call it if you remain on the current step (e.g., asking a clarifying question or repeating because input was unclear). Call it once per transition, not once per step you ultimately want to reach.", + "parameters": { + "type": "object", + "properties": { + "next_node_id": { + "type": "string", + "description": "The id of the workflow node you are moving INTO. Must be a node id present in the workflow view or the overall start node id." + } + }, + "required": [ + "next_node_id" + ] + } +} \ No newline at end of file From f9001e561562063ae14ac2bef6bd741a7a071dc4 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 28 May 2026 18:28:58 -0500 Subject: [PATCH 2/6] relocate graph builder --- .../BotSharp.Abstraction/Graph}/Utils/GraphBuilder.cs | 5 +++-- src/Plugins/BotSharp.Plugin.Membase/Using.cs | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) rename src/{Plugins/BotSharp.Plugin.Membase => Infrastructure/BotSharp.Abstraction/Graph}/Utils/GraphBuilder.cs (98%) diff --git a/src/Plugins/BotSharp.Plugin.Membase/Utils/GraphBuilder.cs b/src/Infrastructure/BotSharp.Abstraction/Graph/Utils/GraphBuilder.cs similarity index 98% rename from src/Plugins/BotSharp.Plugin.Membase/Utils/GraphBuilder.cs rename to src/Infrastructure/BotSharp.Abstraction/Graph/Utils/GraphBuilder.cs index f050e7be6..2988d5a42 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Utils/GraphBuilder.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Graph/Utils/GraphBuilder.cs @@ -1,8 +1,9 @@ +using BotSharp.Abstraction.Graph.Models; using BotSharp.Abstraction.Rules; -using BotSharp.Abstraction.Utilities; +using System.Text.Json; using System.Text.RegularExpressions; -namespace BotSharp.Plugin.Membase.Utils; +namespace BotSharp.Abstraction.Graph.Utils; public static class GraphBuilder { diff --git a/src/Plugins/BotSharp.Plugin.Membase/Using.cs b/src/Plugins/BotSharp.Plugin.Membase/Using.cs index dbd3365fa..0d7e7b783 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Using.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Using.cs @@ -22,7 +22,8 @@ global using BotSharp.Abstraction.Graph.Models; global using BotSharp.Abstraction.Graph.Options; global using BotSharp.Abstraction.Graph.Requests; +global using BotSharp.Abstraction.Graph.Utils; global using BotSharp.Abstraction.Options; + global using BotSharp.Plugin.Membase.Interfaces; -global using BotSharp.Plugin.Membase.Utils; global using BotSharp.Plugin.Membase.Services; \ No newline at end of file From 96d0c1e3548ac0133898c9f5073a02f3d3293c81 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 28 May 2026 18:59:24 -0500 Subject: [PATCH 3/6] rename --- .../RuleGraph.cs => Graph/FlowGraph.cs} | 62 ++++++++--------- .../{Rules => Graph}/Models/FlowUnitSchema.cs | 4 +- .../Graph/Utils/GraphBuilder.cs | 8 +-- .../Rules/Hooks/IRuleTriggerHook.cs | 9 +-- .../BotSharp.Abstraction/Rules/IRuleEngine.cs | 3 +- .../Rules/IRuleFlowUnit.cs | 2 +- .../Rules/Models/RuleFlowContext.cs | 7 +- .../Rules/Models/RuleFlowStepResult.cs | 8 ++- .../BotSharp.Core.Rules/Actions/ChatAction.cs | 2 + .../Actions/HttpRequestAction.cs | 1 + .../Actions/ToolCallAction.cs | 1 + .../Conditions/AllVisitedRuleCondition.cs | 2 + .../Conditions/LogicGateCondition.cs | 1 + .../Conditions/LoopingCondition.cs | 1 + .../BotSharp.Core.Rules/Engines/RuleEngine.cs | 68 ++++++++++--------- .../BotSharp.Core.Rules/Root/EndAction.cs | 2 + .../BotSharp.Core.Rules/Root/StartAction.cs | 2 + 17 files changed, 100 insertions(+), 83 deletions(-) rename src/Infrastructure/BotSharp.Abstraction/{Rules/RuleGraph.cs => Graph/FlowGraph.cs} (81%) rename src/Infrastructure/BotSharp.Abstraction/{Rules => Graph}/Models/FlowUnitSchema.cs (94%) diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/RuleGraph.cs b/src/Infrastructure/BotSharp.Abstraction/Graph/FlowGraph.cs similarity index 81% rename from src/Infrastructure/BotSharp.Abstraction/Rules/RuleGraph.cs rename to src/Infrastructure/BotSharp.Abstraction/Graph/FlowGraph.cs index e1be4cdba..cd4523e72 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/RuleGraph.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Graph/FlowGraph.cs @@ -1,26 +1,26 @@ -using BotSharp.Abstraction.Rules.Models; +using BotSharp.Abstraction.Graph.Models; namespace BotSharp.Abstraction.Rules; -public class RuleGraph +public class FlowGraph { private string _id = Guid.NewGuid().ToString(); - private List _nodes = []; - private List _edges = []; + private List _nodes = []; + private List _edges = []; - public RuleGraph() + public FlowGraph() { _id = Guid.NewGuid().ToString(); _nodes = []; _edges = []; } - public static RuleGraph Init() + public static FlowGraph Init() { - return new RuleGraph(); + return new FlowGraph(); } - public RuleNode? GetRootNode(string? name = null) + public FlowNode? GetRootNode(string? name = null) { if (!string.IsNullOrEmpty(name)) { @@ -30,7 +30,7 @@ public static RuleGraph Init() return _nodes.FirstOrDefault(x => x.Type.IsEqualTo("root") || x.Type.IsEqualTo("start")); } - public (RuleNode? Node, IEnumerable IncomingEdges, IEnumerable OutgoingEdges) GetNode(string id) + public (FlowNode? Node, IEnumerable IncomingEdges, IEnumerable OutgoingEdges) GetNode(string id) { var node = _nodes.FirstOrDefault(x => x.Id.IsEqualTo(id)); if (node == null) @@ -55,12 +55,12 @@ public string GetGraphId() return _id; } - public IEnumerable GetNodes(Func? filter = null) + public IEnumerable GetNodes(Func? filter = null) { return filter == null ? [.. _nodes] : [.. _nodes.Where(filter)]; } - public IEnumerable GetEdges(Func? filter = null) + public IEnumerable GetEdges(Func? filter = null) { return filter == null ? [.. _edges] : [.. _edges.Where(filter)]; } @@ -70,17 +70,17 @@ public void SetGraphId(string id) _id = id; } - public void SetNodes(IEnumerable nodes) + public void SetNodes(IEnumerable nodes) { _nodes = [.. nodes?.ToList() ?? []]; } - public void SetEdges(IEnumerable edges) + public void SetEdges(IEnumerable edges) { _edges = [.. edges?.ToList() ?? []]; } - public void AddNode(RuleNode node) + public void AddNode(FlowNode node) { var found = _nodes.Exists(x => x.Id.IsEqualTo(node.Id)); if (!found) @@ -89,7 +89,7 @@ public void AddNode(RuleNode node) } } - public void AddEdge(RuleNode from, RuleNode to, EdgeItemPayload payload) + public void AddEdge(FlowNode from, FlowNode to, EdgeItemPayload payload) { var sourceFound = _nodes.Exists(x => x.Id.IsEqualTo(from.Id)); var targetFound = _nodes.Exists(x => x.Id.IsEqualTo(to.Id)); @@ -107,7 +107,7 @@ public void AddEdge(RuleNode from, RuleNode to, EdgeItemPayload payload) if (!edgeFound) { - _edges.Add(new RuleEdge(from, to) + _edges.Add(new FlowEdge(from, to) { Id = payload.Id, Name = payload.Name, @@ -121,7 +121,7 @@ public void AddEdge(RuleNode from, RuleNode to, EdgeItemPayload payload) } } - public IEnumerable<(RuleNode, RuleEdge)> GetParentNodes(RuleNode node, bool ascending = false) + public IEnumerable<(FlowNode, FlowEdge)> GetParentNodes(FlowNode node, bool ascending = false) { var filtered = _edges.Where(e => e.To != null && e.To.Id.IsEqualTo(node.Id)); var ordered = ascending @@ -131,7 +131,7 @@ public void AddEdge(RuleNode from, RuleNode to, EdgeItemPayload payload) return ordered.Select(e => (e.From, e)).ToList(); } - public IEnumerable<(RuleNode, RuleEdge)> GetChildrenNodes(RuleNode node, bool ascending = false) + public IEnumerable<(FlowNode, FlowEdge)> GetChildrenNodes(FlowNode node, bool ascending = false) { var filtered = _edges.Where(e => e.From != null && e.From.Id.IsEqualTo(node.Id)); var ordered = ascending @@ -141,7 +141,7 @@ public void AddEdge(RuleNode from, RuleNode to, EdgeItemPayload payload) return ordered.Select(e => (e.To, e)).ToList(); } - public RuleGraphInfo GetGraphInfo() + public FlowGraphInfo GetGraphInfo() { return new() { @@ -157,9 +157,9 @@ public void Clear() _edges = []; } - public static RuleGraph FromGraphInfo(RuleGraphInfo graphInfo) + public static FlowGraph FromGraphInfo(FlowGraphInfo graphInfo) { - var graph = new RuleGraph(); + var graph = new FlowGraph(); graph.SetGraphId(graphInfo.GraphId.IfNullOrEmptyAs(Guid.NewGuid().ToString())!); graph.SetNodes(graphInfo.Nodes); graph.SetEdges(graphInfo.Edges); @@ -172,7 +172,7 @@ public override string ToString() } } -public class RuleNode : GraphItem +public class FlowNode : GraphItem { /// /// Node type: root, criteria, action, etc. @@ -200,22 +200,22 @@ public override string ToString() } } -public class RuleEdge : GraphItem +public class FlowEdge : GraphItem { /// /// Edge type: is_next, etc. /// public override string Type { get; set; } = "next"; - public RuleNode From { get; set; } - public RuleNode To { get; set; } + public FlowNode From { get; set; } + public FlowNode To { get; set; } - public RuleEdge() : base() + public FlowEdge() : base() { - + } - public RuleEdge(RuleNode from, RuleNode to) : base() + public FlowEdge(FlowNode from, FlowNode to) : base() { Id = Guid.NewGuid().ToString(); From = from; @@ -279,11 +279,11 @@ public class EdgeItemPayload : GraphItem } -public class RuleGraphInfo +public class FlowGraphInfo { [JsonPropertyName("graph_id")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string GraphId { get; set; } - public IEnumerable Nodes { get; set; } = []; - public IEnumerable Edges { get; set; } = []; + public IEnumerable Nodes { get; set; } = []; + public IEnumerable Edges { get; set; } = []; } \ No newline at end of file diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/FlowUnitSchema.cs b/src/Infrastructure/BotSharp.Abstraction/Graph/Models/FlowUnitSchema.cs similarity index 94% rename from src/Infrastructure/BotSharp.Abstraction/Rules/Models/FlowUnitSchema.cs rename to src/Infrastructure/BotSharp.Abstraction/Graph/Models/FlowUnitSchema.cs index a443519fd..797ba8bfc 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/FlowUnitSchema.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Graph/Models/FlowUnitSchema.cs @@ -1,6 +1,4 @@ -using System.Text.Json.Serialization; - -namespace BotSharp.Abstraction.Rules.Models; +namespace BotSharp.Abstraction.Graph.Models; /// /// Describes the input or output contract of a rule flow unit (action or condition). diff --git a/src/Infrastructure/BotSharp.Abstraction/Graph/Utils/GraphBuilder.cs b/src/Infrastructure/BotSharp.Abstraction/Graph/Utils/GraphBuilder.cs index 2988d5a42..7628eca72 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Graph/Utils/GraphBuilder.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Graph/Utils/GraphBuilder.cs @@ -7,9 +7,9 @@ namespace BotSharp.Abstraction.Graph.Utils; public static class GraphBuilder { - public static RuleGraph Build(GraphQueryResult result, IDictionary? data = null) + public static FlowGraph Build(GraphQueryResult result, IDictionary? data = null) { - var graph = RuleGraph.Init(); + var graph = FlowGraph.Init(); if (result.Values.IsNullOrEmpty()) { return graph; @@ -50,7 +50,7 @@ public static RuleGraph Build(GraphQueryResult result, IDictionary Task.CompletedTask; - Task AfterRuleConditionExecuted(Agent agent, RuleNode conditionNode, RuleEdge incomingEdge, IRuleTrigger trigger, RuleFlowContext context, RuleNodeResult result) => Task.CompletedTask; + Task BeforeRuleConditionExecuting(Agent agent, FlowNode conditionNode, FlowEdge incomingEdge, IRuleTrigger trigger, RuleFlowContext context) => Task.CompletedTask; + Task AfterRuleConditionExecuted(Agent agent, FlowNode conditionNode, FlowEdge incomingEdge, IRuleTrigger trigger, RuleFlowContext context, RuleNodeResult result) => Task.CompletedTask; - Task BeforeRuleActionExecuting(Agent agent, RuleNode actionNode, RuleEdge incomingEdge, IRuleTrigger trigger, RuleFlowContext context) => Task.CompletedTask; - Task AfterRuleActionExecuted(Agent agent, RuleNode actionNode, RuleEdge incomingEdge, IRuleTrigger trigger, RuleFlowContext context, RuleNodeResult result) => Task.CompletedTask; + Task BeforeRuleActionExecuting(Agent agent, FlowNode actionNode, FlowEdge incomingEdge, IRuleTrigger trigger, RuleFlowContext context) => Task.CompletedTask; + Task AfterRuleActionExecuted(Agent agent, FlowNode actionNode, FlowEdge incomingEdge, IRuleTrigger trigger, RuleFlowContext context, RuleNodeResult result) => Task.CompletedTask; } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs index d9958e549..16101a920 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Graph; using BotSharp.Abstraction.Rules.Models; namespace BotSharp.Abstraction.Rules; @@ -25,5 +26,5 @@ Task> Triggered(IRuleTrigger trigger, string text, IEnumerab /// /// /// - Task ExecuteGraphNode(RuleNode node, RuleGraph graph, string agentId, IRuleTrigger trigger, RuleNodeExecutionOptions options); + Task ExecuteGraphNode(FlowNode node, FlowGraph graph, string agentId, IRuleTrigger trigger, RuleNodeExecutionOptions options); } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlowUnit.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlowUnit.cs index fbccc3f45..73e1b8e25 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlowUnit.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlowUnit.cs @@ -1,4 +1,4 @@ -using BotSharp.Abstraction.Rules.Models; +using BotSharp.Abstraction.Graph.Models; namespace BotSharp.Abstraction.Rules; diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowContext.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowContext.cs index 4e9040efe..5e330e01e 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowContext.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowContext.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Graph; using System.Text.Json; namespace BotSharp.Abstraction.Rules.Models; @@ -11,8 +12,8 @@ public class RuleFlowContext public Dictionary Parameters { get; set; } = []; public IEnumerable PrevStepResults { get; set; } = []; public JsonSerializerOptions? JsonOptions { get; set; } - public RuleNode Node { get; set; } - public RuleEdge Edge { get; set; } - public RuleGraph Graph { get; set; } + public FlowNode Node { get; set; } + public FlowEdge Edge { get; set; } + public FlowGraph Graph { get; set; } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs index b4b6d5953..d5b30ddc9 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Rules/Models/RuleFlowStepResult.cs @@ -1,13 +1,15 @@ +using BotSharp.Abstraction.Graph; + namespace BotSharp.Abstraction.Rules.Models; public class RuleFlowStepResult : RuleNodeResult { - public RuleNode Node { get; set; } + public FlowNode Node { get; set; } /// - /// Create a RuleFlowStepResult from a RuleNodeResult and a RuleNode + /// Create a RuleFlowStepResult from a RuleNodeResult and a FlowNode /// - public static RuleFlowStepResult FromResult(RuleNodeResult result, RuleNode node) + public static RuleFlowStepResult FromResult(RuleNodeResult result, FlowNode node) { return new RuleFlowStepResult { diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatAction.cs index b6f3cff3e..a3d8b166d 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/ChatAction.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Graph.Models; + namespace BotSharp.Core.Rules.Actions; public class ChatAction : IRuleAction diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRequestAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRequestAction.cs index c42e2054f..dc7a060b0 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRequestAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRequestAction.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Graph.Models; using System.Net.Mime; using System.Text.Json; using System.Web; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Actions/ToolCallAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Actions/ToolCallAction.cs index 3e6dc647b..1cd2e62b2 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Actions/ToolCallAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Actions/ToolCallAction.cs @@ -1,4 +1,5 @@ using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Graph.Models; namespace BotSharp.Core.Rules.Actions; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Conditions/AllVisitedRuleCondition.cs b/src/Infrastructure/BotSharp.Core.Rules/Conditions/AllVisitedRuleCondition.cs index 1ba3650f9..2c77a9a99 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Conditions/AllVisitedRuleCondition.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Conditions/AllVisitedRuleCondition.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Graph.Models; + namespace BotSharp.Core.Rules.Conditions; public class AllVisitedRuleCondition : IRuleCondition diff --git a/src/Infrastructure/BotSharp.Core.Rules/Conditions/LogicGateCondition.cs b/src/Infrastructure/BotSharp.Core.Rules/Conditions/LogicGateCondition.cs index 364c67ff6..4429ac1a1 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Conditions/LogicGateCondition.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Conditions/LogicGateCondition.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Graph.Models; using BotSharp.Core.Rules.Models; namespace BotSharp.Core.Rules.Conditions; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Conditions/LoopingCondition.cs b/src/Infrastructure/BotSharp.Core.Rules/Conditions/LoopingCondition.cs index 4bd605378..2104662d3 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Conditions/LoopingCondition.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Conditions/LoopingCondition.cs @@ -1,3 +1,4 @@ +using BotSharp.Abstraction.Graph.Models; using System.Text.Json; namespace BotSharp.Core.Rules.Conditions; diff --git a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs index 6d4143c1c..536937952 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Graph.Models; + namespace BotSharp.Core.Rules.Engines; public class RuleEngine : IRuleEngine @@ -83,7 +85,7 @@ public async Task> Triggered(IRuleTrigger trigger, string te return newConversationIds; } - public async Task ExecuteGraphNode(RuleNode node, RuleGraph graph, string agentId, IRuleTrigger trigger, RuleNodeExecutionOptions options) + public async Task ExecuteGraphNode(FlowNode node, FlowGraph graph, string agentId, IRuleTrigger trigger, RuleNodeExecutionOptions options) { if (node == null || graph == null || options == null) { @@ -112,9 +114,9 @@ await ExecuteGraphNode( } #region Graph - private async Task LoadGraph(string name, Agent agent, IRuleTrigger trigger, RuleFlowOptions? options) + private async Task LoadGraph(string name, Agent agent, IRuleTrigger trigger, RuleFlowOptions? options) { - var flow = _services.GetServices>().FirstOrDefault(x => x.Name.IsEqualTo(name)); + var flow = _services.GetServices>().FirstOrDefault(x => x.Name.IsEqualTo(name)); if (flow == null) { return null; @@ -167,8 +169,8 @@ await ExecuteGraphNode( } private async Task ExecuteGraphNode( - RuleNode node, - RuleGraph graph, + FlowNode node, + FlowGraph graph, Agent agent, IRuleTrigger trigger, string text, @@ -191,8 +193,8 @@ private async Task ExecuteGraphNode( /// Config["traversal_algorithm"] value ("dfs" or "bfs"). /// private async Task ExecuteGraphTraversal( - RuleNode root, - RuleGraph graph, + FlowNode root, + FlowGraph graph, Agent agent, IRuleTrigger trigger, string text, @@ -207,9 +209,9 @@ private async Task ExecuteGraphTraversal( // Choose initial frontier based on the global option var useBfs = options?.Flow?.TraversalAlgorithm?.IsEqualTo("bfs") == true; - IFrontier<(RuleNode Node, RuleEdge Edge)> frontier = useBfs - ? new QueueFrontier<(RuleNode, RuleEdge)>() - : new StackFrontier<(RuleNode, RuleEdge)>(); + IFrontier<(FlowNode Node, FlowEdge Edge)> frontier = useBfs + ? new QueueFrontier<(FlowNode, FlowEdge)>() + : new StackFrontier<(FlowNode, FlowEdge)>(); EnqueueChildren(frontier, graph, root); @@ -308,9 +310,9 @@ private async Task ExecuteGraphTraversal( /// that differs from the current frontier type, swap to the requested one /// and drain all pending items into the new frontier. /// - private static IFrontier<(RuleNode, RuleEdge)> SwitchFrontier( - IFrontier<(RuleNode, RuleEdge)> current, - RuleNode? node) + 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"); @@ -321,27 +323,27 @@ private async Task ExecuteGraphTraversal( } var requireBfs = hint.Equals("bfs", StringComparison.OrdinalIgnoreCase); - var currentBfs = current is QueueFrontier<(RuleNode, RuleEdge)>; + var currentBfs = current is QueueFrontier<(FlowNode, FlowEdge)>; if (requireBfs == currentBfs) { return current; } - IFrontier<(RuleNode, RuleEdge)> next = requireBfs - ? new QueueFrontier<(RuleNode, RuleEdge)>() - : new StackFrontier<(RuleNode, RuleEdge)>(); + IFrontier<(FlowNode, FlowEdge)> next = requireBfs + ? new QueueFrontier<(FlowNode, FlowEdge)>() + : new StackFrontier<(FlowNode, FlowEdge)>(); current.DrainTo(next); return next; } private static void EnqueueChildren( - IFrontier<(RuleNode Node, RuleEdge Edge)> frontier, - RuleGraph graph, - RuleNode parent) + IFrontier<(FlowNode Node, FlowEdge Edge)> frontier, + FlowGraph graph, + FlowNode parent) { - var sortAscending = frontier is StackFrontier<(RuleNode, RuleEdge)>; + var sortAscending = frontier is StackFrontier<(FlowNode, FlowEdge)>; foreach (var child in graph.GetChildrenNodes(parent, sortAscending)) { frontier.Add(child); @@ -353,11 +355,11 @@ private static void EnqueueChildren( #region Schema Validation /// /// Reads "input_schema" and "output_schema" from each node's Config, - /// deserializes them into FlowUnitSchema, and sets them on the RuleNode. + /// 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(RuleGraph graph) + private void LoadConfigSchemas(FlowGraph graph) { var nodes = graph.GetNodes(); if (nodes == null) @@ -405,7 +407,7 @@ private void LoadConfigSchemas(RuleGraph graph) /// 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(RuleGraph graph) + private void ValidateGraphSchema(FlowGraph graph) { var edges = graph.GetEdges(); if (edges == null || !edges.Any()) @@ -490,7 +492,7 @@ private void ValidateGraphSchema(RuleGraph graph) /// /// Resolves the IRuleFlowUnit (action or condition) implementation for a given node. /// - private IRuleFlowUnit? ResolveFlowUnit(RuleNode node) + private IRuleFlowUnit? ResolveFlowUnit(FlowNode node) { if (node == null || string.IsNullOrEmpty(node.Name)) { @@ -528,9 +530,9 @@ private void ValidateGraphSchema(RuleGraph graph) #region Action private async Task ExecuteAction( - RuleNode node, - RuleEdge incomingEdge, - RuleGraph graph, + FlowNode node, + FlowEdge incomingEdge, + FlowGraph graph, Agent agent, IRuleTrigger trigger, RuleFlowContext context) @@ -578,7 +580,7 @@ private void ValidateGraphSchema(RuleGraph graph) } // Find the matching action - private IRuleAction? GetRuleAction(RuleNode node, Agent agent, IRuleTrigger trigger) + private IRuleAction? GetRuleAction(FlowNode node, Agent agent, IRuleTrigger trigger) { var actions = _services.GetServices() .Where(x => x.Name.IsEqualTo(node?.Name)) @@ -615,9 +617,9 @@ private void ValidateGraphSchema(RuleGraph graph) #region Condition private async Task ExecuteCondition( - RuleNode node, - RuleEdge incomingEdge, - RuleGraph graph, + FlowNode node, + FlowEdge incomingEdge, + FlowGraph graph, Agent agent, IRuleTrigger trigger, RuleFlowContext context) @@ -665,7 +667,7 @@ private void ValidateGraphSchema(RuleGraph graph) } // Find the matching condition - private IRuleCondition? GetRuleCondition(RuleNode node, Agent agent, IRuleTrigger trigger) + private IRuleCondition? GetRuleCondition(FlowNode node, Agent agent, IRuleTrigger trigger) { var conditions = _services.GetServices() .Where(x => x.Name.IsEqualTo(node?.Name)) diff --git a/src/Infrastructure/BotSharp.Core.Rules/Root/EndAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Root/EndAction.cs index 705f9cb24..95485cecc 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Root/EndAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Root/EndAction.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Graph.Models; + namespace BotSharp.Core.Rules.Root; /// diff --git a/src/Infrastructure/BotSharp.Core.Rules/Root/StartAction.cs b/src/Infrastructure/BotSharp.Core.Rules/Root/StartAction.cs index 680140303..c5bb68d53 100644 --- a/src/Infrastructure/BotSharp.Core.Rules/Root/StartAction.cs +++ b/src/Infrastructure/BotSharp.Core.Rules/Root/StartAction.cs @@ -1,3 +1,5 @@ +using BotSharp.Abstraction.Graph.Models; + namespace BotSharp.Core.Rules.Root; /// From 27b1dc2b81b3e02d023dad1111476cfbcf3b55be Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 28 May 2026 19:13:41 -0500 Subject: [PATCH 4/6] minor change --- .../Services/MembaseInstructionResolver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseInstructionResolver.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseInstructionResolver.cs index 61700372d..ff0c19964 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseInstructionResolver.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseInstructionResolver.cs @@ -29,9 +29,9 @@ public MembaseInstructionResolver( public async Task ResolveAsync(Agent agent, string instruction, IEnumerable args, IDictionary kwArgs) { - string? graphId = kwArgs?.TryGetValue("graphId", out string? gId) == true ? gId : null; + string? graphId = kwArgs?.TryGetValue("graph_id", out string? gId) == true ? gId : null; string? format = kwArgs?.TryGetValue("format", out string? f) == true ? f : null; - string? startNodeId = kwArgs?.TryGetValue("startNodeId", out string? s) == true ? s : null; + string? startNodeId = kwArgs?.TryGetValue("start_node_id", out string? s) == true ? s : null; int hop = kwArgs?.TryGetValue("hop", out var h) == true && h != null ? Convert.ToInt32(h) : 0; if (string.IsNullOrEmpty(startNodeId)) From 709400583f34073a58a35e975834273975861c7c Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 29 May 2026 10:16:29 -0500 Subject: [PATCH 5/6] check start node --- .../Services/MembaseInstructionResolver.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseInstructionResolver.cs b/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseInstructionResolver.cs index ff0c19964..6cde0807a 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseInstructionResolver.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Services/MembaseInstructionResolver.cs @@ -39,6 +39,7 @@ public async Task ResolveAsync(Agent agent, string instruction, IEnumera return instruction; } + var prevStartNodeId = _states.GetState("start_node_id"); _states.SetState("start_node_id", startNodeId); var graphDb = _services.GetServices().First(x => x.Provider.IsEqualTo(Provider)); @@ -74,7 +75,7 @@ RETURN DISTINCT startNode(rel) AS sourceNode, rel as edge, endNode(rel) AS targe else if (format.IsEqualTo("partial graph")) { var nextNodeId = _states.GetState("next_node_id"); - if (string.IsNullOrEmpty(nextNodeId)) + if (string.IsNullOrEmpty(nextNodeId) || !prevStartNodeId.IsEqualTo(startNodeId)) { nextNodeId = startNodeId; _states.SetState("next_node_id", nextNodeId); From 6c26b9e9acb88ed884d2840b33fa46180c0f7530 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Fri, 29 May 2026 13:52:44 -0500 Subject: [PATCH 6/6] add dictionary type --- .../Templating/RenderConfiguration.cs | 34 ++++++++++++++++++- .../Functions/UtilAdvanceWorkflowCursorFn.cs | 7 ---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/Infrastructure/BotSharp.Core/Templating/RenderConfiguration.cs b/src/Infrastructure/BotSharp.Core/Templating/RenderConfiguration.cs index 63527918a..27e252da1 100644 --- a/src/Infrastructure/BotSharp.Core/Templating/RenderConfiguration.cs +++ b/src/Infrastructure/BotSharp.Core/Templating/RenderConfiguration.cs @@ -72,7 +72,12 @@ public void RegisterType(Type type) { if (type == null || IsStringType(type)) return; - if (IsListType(type)) + if (TryGetDictionaryValueType(type, out var keyType, out var valueType)) + { + RegisterType(keyType); + RegisterType(valueType); + } + else if (IsListType(type)) { if (type.IsGenericType) { @@ -287,6 +292,33 @@ private static bool IsListType(Type type) return type.IsArray || interfaces.Any(x => x.Name == typeof(IEnumerable).Name); } + private static bool TryGetDictionaryValueType(Type type, out Type keyType, out Type valueType) + { + keyType = null!; + valueType = null!; + + var dictionaryType = type.IsGenericType && IsDictionaryType(type) + ? type + : type.GetTypeInfo().ImplementedInterfaces.FirstOrDefault(x => x.IsGenericType && IsDictionaryType(x)); + + if (dictionaryType == null) + { + return false; + } + + keyType = dictionaryType.GetGenericArguments()[0]; + valueType = dictionaryType.GetGenericArguments()[1]; + return true; + } + + private static bool IsDictionaryType(Type type) + { + var genericType = type.GetGenericTypeDefinition(); + return genericType == typeof(IDictionary<,>) + || genericType == typeof(IReadOnlyDictionary<,>) + || genericType == typeof(Dictionary<,>); + } + private static bool IsTrackToNextLevel(Type type) { return type.IsClass || type.IsInterface || type.IsAbstract; diff --git a/src/Plugins/BotSharp.Plugin.Membase/Functions/UtilAdvanceWorkflowCursorFn.cs b/src/Plugins/BotSharp.Plugin.Membase/Functions/UtilAdvanceWorkflowCursorFn.cs index 7115a5329..1a8071d74 100644 --- a/src/Plugins/BotSharp.Plugin.Membase/Functions/UtilAdvanceWorkflowCursorFn.cs +++ b/src/Plugins/BotSharp.Plugin.Membase/Functions/UtilAdvanceWorkflowCursorFn.cs @@ -29,13 +29,6 @@ public async Task Execute(RoleDialogModel message) { var args = JsonSerializer.Deserialize(message.FunctionArgs ?? "{}"); var nextNodeId = args?.NextNodeId; - - if (string.IsNullOrWhiteSpace(nextNodeId)) - { - nextNodeId = _states.GetState("start_node_id", ""); - _states.SetState("next_node_id", nextNodeId); - } - message.Content = $"The next node id is '{nextNodeId}'"; return true; }