Skip to content

Commit 9e3fc04

Browse files
authored
Merge pull request SciSharp#1338 from iceljc/features/refine-rule-graph
refine rule graph, add input/output schema, graph traversal switch, g…
2 parents f17c490 + ee8af1d commit 9e3fc04

22 files changed

Lines changed: 1080 additions & 154 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
namespace BotSharp.Abstraction.Rules.Constants;
2+
3+
public static class RuleConstant
4+
{
5+
public const int MAX_GRAPH_RECURSION = 1000;
6+
7+
public const string INPUT_SCHEMA_KEY = "input_schema";
8+
public const string OUTPUT_SCHEMA_KEY = "output_schema";
9+
10+
public static IEnumerable<string> CONDITION_NODE_TYPES = new List<string>
11+
{
12+
"condition",
13+
"criteria"
14+
};
15+
16+
public static IEnumerable<string> ACTION_NODE_TYPES = new List<string>
17+
{
18+
"action"
19+
};
20+
21+
public static IEnumerable<string> ROOT_NODE_TYPES = new List<string>
22+
{
23+
"root",
24+
"start"
25+
};
26+
27+
public static IEnumerable<string> END_NODE_TYPES = new List<string>
28+
{
29+
"end",
30+
"terminal"
31+
};
32+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
namespace BotSharp.Core.Rules.Engines;
2+
3+
/// <summary>
4+
/// Abstraction over the data structure that drives graph traversal order.
5+
/// Stack → DFS, Queue → BFS. Swap the frontier mid-traversal to switch strategy.
6+
/// </summary>
7+
public interface IFrontier<T>
8+
{
9+
void Add(T item);
10+
T Remove();
11+
int Count { get; }
12+
13+
/// <summary>
14+
/// Drain every remaining item into <paramref name="other"/>, preserving order.
15+
/// </summary>
16+
void DrainTo(IFrontier<T> other);
17+
}
18+
19+
/// <summary>
20+
/// LIFO frontier – produces depth-first traversal.
21+
/// </summary>
22+
public sealed class StackFrontier<T> : IFrontier<T>
23+
{
24+
private readonly Stack<T> _stack = new();
25+
26+
public int Count => _stack.Count;
27+
28+
public void Add(T item) => _stack.Push(item);
29+
30+
public T Remove() => _stack.Pop();
31+
32+
public void DrainTo(IFrontier<T> other)
33+
{
34+
// Reverse so the item that was on top is added first and
35+
// therefore ends up at the same "priority" position in the target.
36+
var items = _stack.ToList();
37+
items.Reverse();
38+
_stack.Clear();
39+
foreach (var item in items)
40+
{
41+
other.Add(item);
42+
}
43+
}
44+
}
45+
46+
/// <summary>
47+
/// FIFO frontier – produces breadth-first traversal.
48+
/// </summary>
49+
public sealed class QueueFrontier<T> : IFrontier<T>
50+
{
51+
private readonly Queue<T> _queue = new();
52+
53+
public int Count => _queue.Count;
54+
55+
public void Add(T item) => _queue.Enqueue(item);
56+
57+
public T Remove() => _queue.Dequeue();
58+
59+
public void DrainTo(IFrontier<T> other)
60+
{
61+
while (_queue.Count > 0)
62+
{
63+
other.Add(_queue.Dequeue());
64+
}
65+
}
66+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace BotSharp.Abstraction.Rules;
2+
3+
public interface IRuleEnd : IRuleFlowUnit
4+
{
5+
}

src/Infrastructure/BotSharp.Abstraction/Rules/IRuleFlowUnit.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,16 @@ public interface IRuleFlowUnit
1818
/// The trigger names
1919
/// </summary>
2020
IEnumerable<string>? Triggers => null;
21+
22+
/// <summary>
23+
/// Schema describing the expected input parameters.
24+
/// Used for validating that upstream nodes produce the required fields.
25+
/// </summary>
26+
FlowUnitSchema? InputSchema => null;
27+
28+
/// <summary>
29+
/// Schema describing the output this unit produces.
30+
/// Used for validating that downstream nodes receive the expected fields.
31+
/// </summary>
32+
FlowUnitSchema? OutputSchema => null;
2133
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
namespace BotSharp.Abstraction.Rules;
2+
3+
public interface IRuleRoot : IRuleFlowUnit
4+
{
5+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace BotSharp.Abstraction.Rules.Models;
4+
5+
/// <summary>
6+
/// Describes the input or output contract of a rule flow unit (action or condition).
7+
/// Follows a JSON Schema-like structure with "properties" and "required" fields.
8+
/// </summary>
9+
public class FlowUnitSchema
10+
{
11+
/// <summary>
12+
/// Property definitions keyed by parameter name.
13+
/// </summary>
14+
[JsonPropertyName("properties")]
15+
public Dictionary<string, FlowUnitSchemaProperty> Properties { get; set; } = [];
16+
17+
/// <summary>
18+
/// List of required property names.
19+
/// </summary>
20+
[JsonPropertyName("required")]
21+
public List<string> Required { get; set; } = [];
22+
23+
public FlowUnitSchema() { }
24+
25+
public FlowUnitSchema(
26+
Dictionary<string, FlowUnitSchemaProperty> properties,
27+
List<string>? required = null)
28+
{
29+
Properties = properties;
30+
Required = required ?? [];
31+
}
32+
}
33+
34+
/// <summary>
35+
/// Describes a single property in a FlowUnitSchema.
36+
/// </summary>
37+
public class FlowUnitSchemaProperty
38+
{
39+
/// <summary>
40+
/// JSON type: "string", "number", "boolean", "object", "array"
41+
/// </summary>
42+
[JsonPropertyName("type")]
43+
public string Type { get; set; } = "string";
44+
45+
/// <summary>
46+
/// A brief explanation of the property's purpose.
47+
/// </summary>
48+
[JsonPropertyName("description")]
49+
public string? Description { get; set; }
50+
51+
public FlowUnitSchemaProperty() { }
52+
53+
public FlowUnitSchemaProperty(string type, string? description = null)
54+
{
55+
Type = type;
56+
Description = description;
57+
}
58+
}

src/Infrastructure/BotSharp.Abstraction/Rules/Options/RuleFlowOptions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ public class RuleFlowOptions
2020
[JsonPropertyName("traversal_algorithm")]
2121
public string TraversalAlgorithm { get; set; } = "dfs";
2222

23+
/// <summary>
24+
/// Whether to skip validation when loading the graph
25+
/// </summary>
26+
[JsonPropertyName("skip_validation")]
27+
public bool SkipValidation { get; set; }
28+
29+
/// <summary>
30+
/// Maximum number of nodes to visit
31+
/// </summary>
32+
[JsonPropertyName("max_recursion")]
33+
public int? MaxRecursion { get; set; }
34+
2335
/// <summary>
2436
/// Additional custom parameters, e.g., root_node_name, max_recursion
2537
/// </summary>

src/Infrastructure/BotSharp.Abstraction/Rules/RuleGraph.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using BotSharp.Abstraction.Rules.Models;
2+
13
namespace BotSharp.Abstraction.Rules;
24

35
public class RuleGraph
@@ -173,6 +175,20 @@ public class RuleNode : GraphItem
173175
/// </summary>
174176
public override string Type { get; set; } = "action";
175177

178+
/// <summary>
179+
/// Input schema loaded from node config. Overrides the code-defined schema on IRuleFlowUnit.
180+
/// </summary>
181+
[JsonPropertyName("input_schema")]
182+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
183+
public FlowUnitSchema? InputSchema { get; set; }
184+
185+
/// <summary>
186+
/// Output schema loaded from node config. Overrides the code-defined schema on IRuleFlowUnit.
187+
/// </summary>
188+
[JsonPropertyName("output_schema")]
189+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
190+
public FlowUnitSchema? OutputSchema { get; set; }
191+
176192
public override string ToString()
177193
{
178194
return $"Node ({Id}): {Name} ({Type} => {Description})";

src/Infrastructure/BotSharp.Core.Rules/Actions/ChatAction.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace BotSharp.Core.Rules.Actions;
22

3-
public sealed class ChatAction : IRuleAction
3+
public class ChatAction : IRuleAction
44
{
55
private readonly IServiceProvider _services;
66
private readonly ILogger<ChatAction> _logger;
@@ -15,6 +15,17 @@ public ChatAction(
1515

1616
public string Name => "send_message_to_agent";
1717

18+
public FlowUnitSchema? InputSchema => new();
19+
20+
public FlowUnitSchema? OutputSchema => new(
21+
properties: new()
22+
{
23+
["agent_id"] = new("string", "The agent ID"),
24+
["conversation_id"] = new("string", "The created conversation ID")
25+
},
26+
required: ["agent_id", "conversation_id"]
27+
);
28+
1829
public async Task<RuleNodeResult> ExecuteAsync(
1930
Agent agent,
2031
IRuleTrigger trigger,

src/Infrastructure/BotSharp.Core.Rules/Actions/HttpRequestAction.cs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace BotSharp.Core.Rules.Actions;
66

7-
public sealed class HttpRequestAction : IRuleAction
7+
public class HttpRequestAction : IRuleAction
88
{
99
private readonly IServiceProvider _services;
1010
private readonly ILogger<HttpRequestAction> _logger;
@@ -22,11 +22,26 @@ public HttpRequestAction(
2222

2323
public string Name => "http_request";
2424

25-
// Default configuration example:
26-
// {
27-
// "http_url": "https://dummy.example.com/api/v1/employees",
28-
// "http_method": "GET"
29-
// }
25+
public FlowUnitSchema? InputSchema => new(
26+
properties: new()
27+
{
28+
["http_url"] = new("string", "The HTTP URL to request"),
29+
["http_method"] = new("string", "HTTP method: GET, POST, PUT, DELETE, PATCH"),
30+
["http_query_params"] = new("array", "Query parameters as key-value pairs"),
31+
["http_request_headers"] = new("array", "Request headers as key-value pairs"),
32+
["http_request_body"] = new("string", "Request body (JSON string)")
33+
},
34+
required: ["http_url", "http_method"]
35+
);
36+
37+
public FlowUnitSchema? OutputSchema => new(
38+
properties: new()
39+
{
40+
["http_response"] = new("string", "The HTTP response body"),
41+
["http_response_headers"] = new("string", "The HTTP response headers as JSON")
42+
},
43+
required: ["http_response", "http_response_headers"]
44+
);
3045

3146
public async Task<RuleNodeResult> ExecuteAsync(
3247
Agent agent,

0 commit comments

Comments
 (0)