280 lines
11 KiB
C#
280 lines
11 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace AI.Client.SCL
|
|
{
|
|
/// <summary>
|
|
/// Represents the outcome of an SCL command execution.
|
|
/// </summary>
|
|
public class SclResult
|
|
{
|
|
/// <summary>
|
|
/// 0 for Success, any non-zero value for Error.
|
|
/// </summary>
|
|
public int StatusCode { get; set; }
|
|
|
|
/// <summary>
|
|
/// The output data or error message to return to the AI.
|
|
/// </summary>
|
|
public string Data { get; set; }
|
|
|
|
/// <summary>
|
|
/// Flag indicating if the response should be omitted entirely to save tokens.
|
|
/// </summary>
|
|
public bool OmitResponse { get; set; }
|
|
|
|
/// <summary>
|
|
/// Creates a successful result.
|
|
/// </summary>
|
|
public static SclResult Success(string data = "Success")
|
|
{
|
|
return new SclResult { StatusCode = 0, Data = data, OmitResponse = false };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an error result.
|
|
/// </summary>
|
|
public static SclResult Error(string errorMessage, int statusCode = 1)
|
|
{
|
|
return new SclResult { StatusCode = statusCode, Data = errorMessage, OmitResponse = false };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a result that instructs the processor to NOT send a response back to the AI.
|
|
/// </summary>
|
|
public static SclResult NoResponse()
|
|
{
|
|
return new SclResult { OmitResponse = true };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The core engine for parsing and executing Synapse Command Language (SCL) instructions.
|
|
/// </summary>
|
|
public class SclProcessor
|
|
{
|
|
// Thread-safe dictionary to hold registered commands and their execution logic.
|
|
private readonly ConcurrentDictionary<string, Func<string[], Task<SclResult>>> _commandHandlers;
|
|
|
|
public SclProcessor()
|
|
{
|
|
_commandHandlers = new ConcurrentDictionary<string, Func<string[], Task<SclResult>>>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a new command that the AI can invoke.
|
|
/// </summary>
|
|
/// <param name="commandId">The alphanumeric identifier for the command (e.g., "ls", "mouse_move").</param>
|
|
/// <param name="handler">An asynchronous function that takes an array of string parameters and returns an SclResult.</param>
|
|
public void RegisterCommand(string commandId, Func<string[], Task<SclResult>> handler)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(commandId))
|
|
throw new ArgumentException("Command ID cannot be null or whitespace.", nameof(commandId));
|
|
|
|
if (commandId.Contains(" ") || commandId.Contains("[") || commandId.Contains("]") || commandId.Contains("|") || commandId.Contains("~") || commandId.Contains("^"))
|
|
throw new ArgumentException("Command ID contains invalid characters.", nameof(commandId));
|
|
|
|
_commandHandlers[commandId] = handler ?? throw new ArgumentNullException(nameof(handler));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a previously registered command.
|
|
/// </summary>
|
|
public bool UnregisterCommand(string commandId)
|
|
{
|
|
return _commandHandlers.TryRemove(commandId, out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the AI's text output, executes any found SCL commands, and returns the formatted SCL result string.
|
|
/// </summary>
|
|
/// <param name="aiText">The raw text output from the AI.</param>
|
|
/// <returns>A string containing the concatenated ^ result blocks to be sent back to the AI.</returns>
|
|
public async Task<string> ProcessTextAsync(string aiText)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(aiText))
|
|
return string.Empty;
|
|
|
|
var parsedCommands = ParseCommands(aiText);
|
|
if (parsedCommands.Count == 0)
|
|
return string.Empty;
|
|
|
|
StringBuilder resultBuilder = new StringBuilder();
|
|
|
|
foreach (var cmd in parsedCommands)
|
|
{
|
|
SclResult executionResult;
|
|
|
|
if (_commandHandlers.TryGetValue(cmd.CommandId, out var handler))
|
|
{
|
|
try
|
|
{
|
|
// Execute the registered handler
|
|
executionResult = await handler(cmd.Parameters);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Catch unhandled exceptions in the client code to prevent crashing the processor
|
|
executionResult = SclResult.Error($"Internal Execution Exception: {ex.Message}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
executionResult = SclResult.Error($"Unknown command: {cmd.CommandId}");
|
|
}
|
|
|
|
// If the command requested no response (e.g. background GUI app), skip appending it
|
|
if (executionResult.OmitResponse)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Format the result back into SCL syntax: ^commandId[statusCode|escapedData]
|
|
string escapedData = EscapeForScl(executionResult.Data ?? string.Empty);
|
|
resultBuilder.Append($"^{cmd.CommandId}[{executionResult.StatusCode}|{escapedData}] ");
|
|
}
|
|
|
|
return resultBuilder.ToString().TrimEnd();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal struct to hold parsed command data.
|
|
/// </summary>
|
|
private struct ParsedCommand
|
|
{
|
|
public string CommandId { get; }
|
|
public string[] Parameters { get; }
|
|
|
|
public ParsedCommand(string commandId, string[] parameters)
|
|
{
|
|
CommandId = commandId;
|
|
Parameters = parameters;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A robust character-stepping state machine that extracts SCL commands while respecting escape characters, quotes, and nested brackets.
|
|
/// </summary>
|
|
private List<ParsedCommand> ParseCommands(string input)
|
|
{
|
|
var commands = new List<ParsedCommand>();
|
|
|
|
for (int i = 0; i < input.Length; i++)
|
|
{
|
|
// Look for the Action Trigger
|
|
if (input[i] == '~')
|
|
{
|
|
int bracketIdx = input.IndexOf('[', i);
|
|
if (bracketIdx == -1) continue; // Malformed, no opening bracket found
|
|
|
|
string commandId = input.Substring(i + 1, bracketIdx - i - 1).Trim();
|
|
if (string.IsNullOrEmpty(commandId)) continue; // Malformed, no command ID
|
|
|
|
List<string> parameters = new List<string>();
|
|
StringBuilder currentParam = new StringBuilder();
|
|
|
|
bool isEscaped = false;
|
|
bool isClosed = false;
|
|
int bracketDepth = 1; // We just passed the first '['
|
|
bool inDoubleQuotes = false;
|
|
bool inSingleQuotes = false;
|
|
|
|
int j = bracketIdx + 1;
|
|
for (; j < input.Length; j++)
|
|
{
|
|
char c = input[j];
|
|
|
|
if (isEscaped)
|
|
{
|
|
// Only consume the escape character if it's escaping an SCL control char.
|
|
// Otherwise, preserve the backslash (e.g., for Windows file paths like C:\Temp).
|
|
if (c == '[' || c == ']' || c == '|' || c == '\\')
|
|
{
|
|
currentParam.Append(c);
|
|
}
|
|
else
|
|
{
|
|
currentParam.Append('\\');
|
|
currentParam.Append(c);
|
|
}
|
|
isEscaped = false;
|
|
continue;
|
|
}
|
|
|
|
if (c == '\\')
|
|
{
|
|
isEscaped = true;
|
|
continue;
|
|
}
|
|
|
|
// Toggle quote states
|
|
if (c == '"' && !inSingleQuotes) inDoubleQuotes = !inDoubleQuotes;
|
|
if (c == '\'' && !inDoubleQuotes) inSingleQuotes = !inSingleQuotes;
|
|
|
|
bool inQuotes = inDoubleQuotes || inSingleQuotes;
|
|
|
|
if (!inQuotes && c == '[')
|
|
{
|
|
bracketDepth++;
|
|
currentParam.Append(c);
|
|
}
|
|
else if (!inQuotes && c == ']')
|
|
{
|
|
bracketDepth--;
|
|
if (bracketDepth == 0)
|
|
{
|
|
parameters.Add(currentParam.ToString());
|
|
isClosed = true;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
currentParam.Append(c);
|
|
}
|
|
}
|
|
else if (!inQuotes && bracketDepth == 1 && c == '|')
|
|
{
|
|
parameters.Add(currentParam.ToString());
|
|
currentParam.Clear();
|
|
}
|
|
else
|
|
{
|
|
currentParam.Append(c);
|
|
}
|
|
}
|
|
|
|
// Only register the command if the parameter block was properly closed
|
|
if (isClosed)
|
|
{
|
|
commands.Add(new ParsedCommand(commandId, parameters.ToArray()));
|
|
i = j; // Advance the outer loop past this parsed command
|
|
}
|
|
}
|
|
}
|
|
|
|
return commands;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Escapes reserved SCL characters in the output data to ensure the AI parses the result correctly.
|
|
/// </summary>
|
|
private string EscapeForScl(string input)
|
|
{
|
|
if (string.IsNullOrEmpty(input)) return input;
|
|
|
|
StringBuilder sb = new StringBuilder(input.Length + 10);
|
|
foreach (char c in input)
|
|
{
|
|
if (c == '\\' || c == '|' || c == ']' || c == '[')
|
|
{
|
|
sb.Append('\\');
|
|
}
|
|
sb.Append(c);
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
} |