Skip to content

Commit 8a0f0cc

Browse files
authored
Merge pull request #93 from g4-api/development
Development
2 parents 50b50d6 + fd8d04a commit 8a0f0cc

9 files changed

Lines changed: 445 additions & 64 deletions

File tree

‎src/G4.Services.Domain.V4/Extensions/LocalExtensions.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ public McpToolModel ConvertToTool()
221221
Description = description,
222222
QualifiedName = manifest.Key,
223223
Name = name,
224+
Namespace = manifest.Namespace,
224225
Metadata = new McpToolModel.ToolMetadataModel
225226
{
226227
Description = string.Join(Environment.NewLine, manifest.Summary),
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using Swashbuckle.AspNetCore.Annotations;
2+
3+
namespace G4.Services.Domain.V4.Models.Schema
4+
{
5+
/// <summary>
6+
/// Input schema for starting the execution of a collection of rules within an active G4 driver session.
7+
/// Requires a driver session identifier, the ordered rule definitions to execute, and an authorization token.
8+
/// </summary>
9+
[SwaggerSchema(description: "Schema for starting the execution of a collection of rules within an active G4 driver session.")]
10+
public class SendRulesInputSchema
11+
{
12+
/// <summary>
13+
/// Gets or sets the driver session identifier.
14+
/// Specifies the active session in which the rules should be executed.
15+
/// </summary>
16+
[SwaggerSchema(description: "The unique session ID associated with the current browser session. " +
17+
"This ID is used to retrieve the appropriate browser driver for interacting with " +
18+
"the session and performing automation tasks.")]
19+
public string DriverSession { get; set; }
20+
21+
/// <summary>
22+
/// Gets or sets the user intent or natural-language request
23+
/// that describes the purpose of the rules being sent.
24+
/// </summary>
25+
[SwaggerSchema(description: "The user intent or natural-language request that describes the purpose of the rules being sent.")]
26+
public IntentModel Intent { get; set; }
27+
28+
/// <summary>
29+
/// Gets or sets the ordered collection of rule definitions to execute.
30+
/// This may be a structured array representing the automation rule logic.
31+
/// </summary>
32+
[SwaggerSchema(description: "The ordered collection of G4 rules to be executed, including their parameters and configuration.")]
33+
public object Rules { get; set; }
34+
35+
/// <summary>
36+
/// Gets or sets the authorization token required by the G4 engine
37+
/// to validate and execute the rule request.
38+
/// </summary>
39+
[SwaggerSchema(description: "The G4 Authentication token used to authenticate the session initiation process. " +
40+
"This is required to authorize the session creation.")]
41+
public string Token { get; set; }
42+
}
43+
}

‎src/G4.Services.Domain.V4/Models/McpToolModel.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ public class McpToolModel
3636
/// </summary>
3737
public string Name { get; set; }
3838

39+
/// <summary>
40+
/// Gets or sets the namespace that owns the tool.
41+
/// </summary>
42+
public string Namespace { get; set; }
43+
3944
/// <summary>
4045
/// Gets or sets additional metadata about the tool.
4146
/// </summary>

‎src/G4.Services.Domain.V4/Repositories/IToolsRepository.cs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ public interface IToolsRepository
137137
/// <returns>An object representing the outcome of the rule execution. The exact structure of the result is defined by the G4 engine.</returns>
138138
object SendRule(SendRuleInputSchema schema);
139139

140+
/// <summary>
141+
/// Sends an ordered collection of rules for execution within an active G4 driver session.
142+
/// All rules are executed sequentially as a single job against the specified driver session,
143+
/// sharing the same page and session state.
144+
/// </summary>
145+
/// <param name="schema">The input schema that includes the driver session identifier, the ordered rule definitions to execute, and the authorization token required by the G4 engine.</param>
146+
/// <returns>An object representing the outcome of the rule collection execution, including the ordered per-rule output. The exact structure of the result is defined by the G4 engine.</returns>
147+
object SendRules(SendRulesInputSchema schema);
148+
140149
/// <summary>
141150
/// Starts a new driver session using the provided input schema.
142151
/// The session is created through the G4 engine using the driver configuration

‎src/G4.Services.Domain.V4/Repositories/McpRepository.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ static ToolOutputSchema FindTool(
9696
{
9797
response.Result = new
9898
{
99-
Tool = tool.ClientTool
99+
Tool = tool
100100
};
101101
}
102102

‎src/G4.Services.Domain.V4/Repositories/ToolsRepository.cs‎

Lines changed: 158 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public object CallTool(JsonElement parameters)
134134
{ Name: "g4.ConvertToRule" } => new { options.Rule },
135135

136136
// Built-in: Finds and returns relevant examples based on the provided intent and tool filters.
137-
{ Name: "g4.FindExamples" } => FindExamples(options),
137+
{ Name: "g4.FindExamples" } => FindExamples(options.Arguments),
138138

139139
// Built-in: Finds and returns metadata about a tool by its name.
140140
{ Name: "g4.FindTool" } => FindTool(options),
@@ -162,6 +162,9 @@ public object CallTool(JsonElement parameters)
162162
// Built-in: Retrieves the locator for a specific element on the page.
163163
{ Name: "g4.ResolveLocator" } => ResolveLocator(options),
164164

165+
// Built-in: Executes an ordered collection of rules as a single job in one driver session.
166+
{ Name: "g4.SendRules" } => SendRules(options),
167+
165168
// Built-in: Starts a new G4 browser automation session.
166169
{ Name: "g4.StartSession" } => StartSession(options),
167170

@@ -219,15 +222,6 @@ public object FindExamples(ExamplesQueryModel query)
219222
};
220223
}
221224

222-
// Unwraps the raw "arguments" payload from an MCP tool-call invocation and delegates
223-
// to the public, strongly-typed FindExamples(JsonElement) overload. This gives the
224-
// g4.FindExamples switch arm in CallTool the same "Xxx(options)" call shape as every
225-
// other built-in tool handler, instead of reaching into options.Arguments directly.
226-
private object FindExamples(InvokeOptions options)
227-
{
228-
return FindExamples(options.Arguments);
229-
}
230-
231225
/// <inheritdoc />
232226
public McpToolModel FindTool(string intent, string toolName)
233227
{
@@ -488,6 +482,42 @@ public object SendRule(SendRuleInputSchema schema)
488482
return SendRule(options);
489483
}
490484

485+
/// <inheritdoc />
486+
public object SendRules(SendRulesInputSchema schema)
487+
{
488+
// Wrap the incoming rule collection in an anonymous arguments object so it can be
489+
// serialized and converted through the standard rule conversion pipeline.
490+
var arguments = new
491+
{
492+
schema.Rules
493+
};
494+
495+
// Serialize the arguments payload using the shared JSON settings.
496+
var json = JsonSerializer.Serialize(value: arguments, AppSettings.JsonOptions);
497+
498+
// Parse the serialized payload into a JsonElement so it can be passed
499+
// into the rule conversion routine.
500+
var jsonElement = JsonDocument.Parse(json).RootElement;
501+
502+
// Build the invocation options using the driver session, token,
503+
// tool catalog, and shared runtime services required by the G4 engine.
504+
var options = new InvokeOptions
505+
{
506+
Arguments = jsonElement,
507+
Buffer = _buffer,
508+
DriverSession = schema.DriverSession,
509+
G4Client = _client,
510+
HttpClient = _httpClient,
511+
Intent = schema.Intent,
512+
Sessions = _sessions,
513+
Token = schema.Token,
514+
Tools = _tools
515+
};
516+
517+
// Delegate the rule collection execution to the G4 engine and return its result.
518+
return SendRules(options);
519+
}
520+
491521
/// <inheritdoc />
492522
public void SyncTools()
493523
{
@@ -538,6 +568,18 @@ private static ActionRuleModel ConvertToRule(
538568
? ruleOut
539569
: JsonDocument.Parse("{}").RootElement;
540570

571+
// Delegate the actual conversion to the shared single-rule routine.
572+
return ConvertRuleData(ruleData, intent, tools);
573+
}
574+
575+
// Converts a single rule-data JSON object (tool name, parameters, and properties)
576+
// into a G4 action rule model, resolving the plugin name from the tool registry and
577+
// attaching the supplied intent to the rule's capabilities.
578+
private static ActionRuleModel ConvertRuleData(
579+
JsonElement ruleData,
580+
IntentModel intent,
581+
ConcurrentDictionary<string, McpToolModel> tools)
582+
{
541583
// Retrieve the "name" of the tool to be invoked. Defaults to null if not provided.
542584
var includes = new[] { "tool_name", "toolName", "name", "tool" };
543585
var toolName = includes
@@ -553,9 +595,6 @@ private static ActionRuleModel ConvertToRule(
553595
? parametersOut
554596
: JsonDocument.Parse("{}").RootElement;
555597

556-
// Format the parameters into a command-line style string (e.g., "--param1:value1 --param2:value2").
557-
var parametersCli = FormatParameters(parameters);
558-
559598
// Get the properties from the ruleData, defaulting to a JSON object with the plugin name if not provided.
560599
var properties = ruleData.TryGetProperty("toolProperties", out var propertiesOut)
561600
? propertiesOut.GetRawText()
@@ -573,10 +612,15 @@ private static ActionRuleModel ConvertToRule(
573612
// Set the PluginName property of the rule to the retrieved plugin name
574613
rule.PluginName = pluginName;
575614

576-
// Set the Argument property of the rule to the formatted parameters if provided
577-
rule.Argument = string.IsNullOrEmpty(parametersCli) || parametersCli.Equals("{{$ }}")
578-
? rule.Argument
579-
: parametersCli;
615+
// Preserve an explicitly supplied G4 expression and synthesize one only as a fallback.
616+
if (string.IsNullOrWhiteSpace(rule.Argument))
617+
{
618+
var parametersCli = FormatParameters(parameters);
619+
if (!string.IsNullOrEmpty(parametersCli) && !parametersCli.Equals("{{$ }}"))
620+
{
621+
rule.Argument = parametersCli;
622+
}
623+
}
580624

581625
// Return the created rule
582626
return rule;
@@ -590,23 +634,20 @@ static string FormatParameters(JsonElement parameters)
590634
return string.Empty;
591635
}
592636

593-
// Build a case-insensitive dictionary of name → textual value.
594-
// $"{i.Value}" calls JsonElement.ToString() (see boolean casing note above).
595-
var parametersObject = parameters
637+
// Expand arrays into repeated switches, as required by the G4 parameter schema.
638+
var parametersCollection = parameters
596639
.EnumerateObject()
597-
.ToDictionary(i => i.Name, i => $"{i.Value}", StringComparer.OrdinalIgnoreCase);
640+
.SelectMany(parameter => parameter.Value.ValueKind == JsonValueKind.Array
641+
? parameter.Value
642+
.EnumerateArray()
643+
.Select(value => new KeyValuePair<string, string>(parameter.Name, $"{value}"))
644+
: [new KeyValuePair<string, string>(parameter.Name, $"{parameter.Value}")]);
598645

599-
// Round-trip through System.Text.Json to apply G4JsonOptions policies (e.g., snake_case keys).
600-
var json = JsonSerializer.Serialize(value: parametersObject, options: AppSettings.JsonOptions);
601-
var parametersCollection = JsonSerializer.Deserialize<Dictionary<string, string>>(
602-
json,
603-
options: AppSettings.JsonOptions)!;
604-
605-
// Convert keys to PascalCase and render as --Key[:Value] (omit :Value when empty).
646+
// Render each scalar value as --Key[:Value] and each array item as its own switch.
606647
var parametersExpression = parametersCollection
607-
.Select(i =>
608-
$"--{i.Key}" +
609-
(string.IsNullOrEmpty(i.Value) ? "" : $":{i.Value}"))
648+
.Select(parameter =>
649+
$"--{parameter.Key}" +
650+
(string.IsNullOrEmpty(parameter.Value) ? "" : $":{parameter.Value}"))
610651
.ToArray();
611652

612653
// Wrap with G4 template delimiters.
@@ -726,6 +767,7 @@ static McpToolModel[] GetSystemTools()
726767
ClientTool = clientTool,
727768
QualifiedName = $"system.{clientTool.Name}",
728769
Name = clientTool.Name,
770+
Namespace = "G4.System",
729771
Description = clientTool.Description,
730772
Metadata = new()
731773
{
@@ -776,10 +818,10 @@ private static object FindTool(InvokeOptions options)
776818
? tool
777819
: options.Tools.Values.FirstOrDefault(i => i.QualifiedName.Equals(toolName, StringComparison.OrdinalIgnoreCase));
778820

779-
// Return the matched tool when a tool name was provided.
780-
// Return null when the caller did not supply any tool name.
781-
return !string.IsNullOrEmpty(toolName)
782-
? new { Tool = tool.ClientTool }
821+
// Return the complete authoritative tool model when it was found.
822+
// Return null when the caller did not supply a valid tool name.
823+
return !string.IsNullOrEmpty(toolName) && tool != null
824+
? new { Tool = tool }
783825
: null;
784826
}
785827

@@ -1154,6 +1196,87 @@ private static object SendRule(InvokeOptions options)
11541196
};
11551197
}
11561198

1199+
// Executes an ordered collection of rules as a single job within one driver session,
1200+
// and retrieves the response, including the driver session and the ordered per-rule output.
1201+
private static object SendRules(InvokeOptions options)
1202+
{
1203+
// Read the "rules" argument and reject the request when it is missing or not an array,
1204+
// mirroring the tool's required "rules" argument as declared in its input schema.
1205+
if (!options.Arguments.TryGetProperty("rules", out var rulesElement) ||
1206+
rulesElement.ValueKind != JsonValueKind.Array)
1207+
{
1208+
throw new ArgumentException("The rules argument is required and must be a non-empty array.");
1209+
}
1210+
1211+
// Convert every rule-data entry into an executable G4 action rule, preserving order.
1212+
var rules = rulesElement
1213+
.EnumerateArray()
1214+
.Select(ruleData => ConvertRuleData(ruleData, options.Intent, options.Tools))
1215+
.ToArray();
1216+
1217+
// Reject an empty collection so the engine is not invoked with a job that has no rules.
1218+
if (rules.Length == 0)
1219+
{
1220+
throw new ArgumentException("The rules argument is required and must be a non-empty array.");
1221+
}
1222+
1223+
// Create a single automation model whose only job carries every converted rule in order.
1224+
var automation = NewAutomation(options.DriverSession, options.Token, rules);
1225+
1226+
// Invoke the automation process using the client and retrieve the response.
1227+
var response = options.G4Client.Automation.Invoke(automation);
1228+
1229+
// Extract the session object from the response, ensuring we track the latest session.
1230+
var session = response.Values.Last().Sessions.Last();
1231+
1232+
// Add the session to the sessions dictionary to keep track of the active session.
1233+
options.Sessions[session.Key] = session.Value;
1234+
1235+
// Add one property-backed response entry per rule so later MCP serialization preserves the buffer values.
1236+
var buffer = options.Buffer.GetValueOrDefault(key: session.Key, defaultValue: null);
1237+
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
1238+
for (var i = 0; i < rules.Length; i++)
1239+
{
1240+
buffer?.Add(new BufferResponseModel.BufferItem
1241+
{
1242+
Timestamp = timestamp + i,
1243+
Rule = rules[i]
1244+
});
1245+
}
1246+
1247+
// Retrieve the complete set of plugin responses for the executed rule collection.
1248+
var ruleResponse = session
1249+
.Value
1250+
.ResponseTree
1251+
.Stages.Last()
1252+
.Jobs.Last()
1253+
.Plugins;
1254+
1255+
// Get the current buffer for the session, defaulting to an empty list if none exists.
1256+
var bufferOut = buffer ?? [];
1257+
1258+
// If the last rule closes the browser, clean up the session and buffer.
1259+
if (rules[^1].PluginName.Equals("CloseBrowser", StringComparison.OrdinalIgnoreCase))
1260+
{
1261+
// Remove the session from the sessions dictionary when closing the browser.
1262+
options.Sessions.TryRemove(session.Key, out _);
1263+
1264+
// Clear the buffer associated with the session key.
1265+
options.Buffer.TryRemove(session.Key, out bufferOut);
1266+
}
1267+
1268+
// Return an anonymous object containing the session key and the ordered rule response values.
1269+
return new
1270+
{
1271+
DriverSession = session.Key,
1272+
Value = new
1273+
{
1274+
Buffer = bufferOut.OrderBy(i => i.Timestamp).Select(i => i.Rule),
1275+
Response = ruleResponse
1276+
}
1277+
};
1278+
}
1279+
11571280
// Starts a new G4 session by invoking the automation process with the provided parameters.
11581281
// The session information is returned, and the session is added to the provided dictionary of active sessions.
11591282
private static object StartSession(InvokeOptions options)
@@ -1174,8 +1297,6 @@ private static object StartSession(InvokeOptions options)
11741297
// These parameters are required for the automation process to know which browser and driver binaries to use.
11751298
automation.DriverParameters = driverParameters;
11761299

1177-
var j = JsonSerializer.Serialize(automation, AppSettings.JsonOptions);
1178-
11791300
// Invoke the automation process and get the response.
11801301
var response = options.G4Client.Automation.Invoke(automation);
11811302

0 commit comments

Comments
 (0)