private async Task <bool> EvaluateConditionAsync( Condition condition, LazyInput lazyInput, IContext context, CancellationToken cancellationToken) { string comparisonValue; switch (condition.Source) { case ValueSource.Input: comparisonValue = lazyInput.SerializedContent; break; case ValueSource.Context: comparisonValue = await context.GetVariableAsync(condition.Variable, cancellationToken); break; case ValueSource.Intent: comparisonValue = (await lazyInput.GetIntentAsync())?.Name; break; case ValueSource.Entity: comparisonValue = (await lazyInput.GetEntityValue(condition.Entity))?.Value; break; default: throw new ArgumentOutOfRangeException(); } switch (condition.Comparison.GetComparisonType()) { case ComparisonType.Unary: var unaryComparisonFunc = condition.Comparison.ToUnaryDelegate(); return(unaryComparisonFunc(comparisonValue)); case ComparisonType.Binary: var binaryComparisonFunc = condition.Comparison.ToBinaryDelegate(); switch (condition.Operator) { case ConditionOperator.Or: return(condition.Values.Any(v => binaryComparisonFunc(comparisonValue, v))); case ConditionOperator.And: return(condition.Values.All(v => binaryComparisonFunc(comparisonValue, v))); default: throw new ArgumentOutOfRangeException(); } default: throw new ArgumentOutOfRangeException(); } }
private async Task <State> ProcessOutputsAsync(LazyInput lazyInput, IContext context, Flow flow, State state, ICollection <OutputTrace> outputTraces, CancellationToken cancellationToken) { var outputs = state.Outputs; state = null; // If there's any output in the current state if (outputs != null) { // Evalute each output conditions foreach (var output in outputs.OrderBy(o => o.Order)) { var(outputTrace, outputStopwatch) = outputTraces != null ? (output.ToTrace(), Stopwatch.StartNew()) : (null, null); try { if (output.Conditions == null || await output.Conditions.EvaluateConditionsAsync(lazyInput, context, cancellationToken)) { state = flow.States.SingleOrDefault(s => s.Id == output.StateId); break; } } catch (Exception ex) { if (outputTrace != null) { outputTrace.Error = ex.ToString(); } throw new OutputProcessingException($"Failed to process output condition to state '{output.StateId}'", ex) { OutputStateId = output.StateId, OutputConditions = output.Conditions }; } finally { outputStopwatch?.Stop(); if (outputTrace != null && outputTraces != null && outputStopwatch != null) { outputTrace.ElapsedMilliseconds = outputStopwatch.ElapsedMilliseconds; outputTraces.Add(outputTrace); } } } } return(state); }
public IContext CreateContext(Identity userIdentity, Identity ownerIdentity, LazyInput input, Flow flow) { switch (_configuration.ContextType) { case nameof(StorageContext): return(new StorageContext(userIdentity, ownerIdentity, input, flow, _variableProviders.Value, _ownerCallerNameDocumentMap.Value)); default: return(new ExtensionContext(userIdentity, ownerIdentity, input, flow, _variableProviders.Value, _contextExtension.Value)); } }
public ExtensionContext( Identity user, Identity application, LazyInput input, Flow flow, IEnumerable <IVariableProvider> variableProviders, IContextExtension contextExtension) : base(user, application, input, flow, variableProviders) { _contextExtension = contextExtension; }
public StorageContext( Identity user, Identity application, LazyInput input, Flow flow, IEnumerable <IVariableProvider> variableProviders, IOwnerCallerNameDocumentMap ownerCallerNameDocumentMap) : base(user, application, input, flow, variableProviders) { _ownerCallerNameDocumentMap = ownerCallerNameDocumentMap; }
public Context( Identity user, LazyInput input, Flow flow, IContextExtension contextExtension, IEnumerable <IVariableProvider> variableProviders) { User = user ?? throw new ArgumentNullException(nameof(user)); Input = input ?? throw new ArgumentNullException(nameof(input)); Flow = flow ?? throw new ArgumentNullException(nameof(flow)); _contextExtension = contextExtension; _variableProviderDictionary = variableProviders.ToDictionary(v => v.Source, v => v); }
public ContextBase( Identity user, Identity application, LazyInput input, Flow flow, IEnumerable <IVariableProvider> variableProviders) { UserIdentity = user ?? throw new ArgumentNullException(nameof(user)); OwnerIdentity = application ?? throw new ArgumentNullException(nameof(application)); Input = input ?? throw new ArgumentNullException(nameof(input)); Flow = flow ?? throw new ArgumentNullException(nameof(flow)); InputContext = new Dictionary <string, object>(); _variableProviderDictionary = variableProviders.ToDictionary(v => v.Source, v => v); }
private static bool ValidateDocument(LazyInput lazyInput, InputValidation inputValidation) { switch (inputValidation.Rule) { case InputValidationRule.Text: return(lazyInput.Content is PlainText); case InputValidationRule.Number: return(decimal.TryParse(lazyInput.SerializedContent, out _)); case InputValidationRule.Date: return(DateTime.TryParse(lazyInput.SerializedContent, out _)); case InputValidationRule.Regex: return(Regex.IsMatch(lazyInput.SerializedContent, inputValidation.Regex)); case InputValidationRule.Type: return(lazyInput.Content.GetMediaType() == inputValidation.Type); default: throw new ArgumentOutOfRangeException(nameof(inputValidation)); } }
private static bool ValidateDocument(LazyInput lazyInput, InputValidation inputValidation) { switch (inputValidation.Rule) { case InputValidationRule.Text: return(lazyInput.Content is PlainText); case InputValidationRule.Number: return(decimal.TryParse(lazyInput.SerializedContent, out _)); case InputValidationRule.Date: return(DateTime.TryParseExact(lazyInput.SerializedContent, Constants.DateValidationFormats, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out _)); case InputValidationRule.Regex: return(Regex.IsMatch(lazyInput.SerializedContent, inputValidation.Regex, default, Constants.REGEX_TIMEOUT)); case InputValidationRule.Type: return(lazyInput.Content.GetMediaType() == inputValidation.Type); default: throw new ArgumentOutOfRangeException(nameof(inputValidation)); } }
private async Task <State> ProcessOutputsAsync(LazyInput lazyInput, IContext context, Flow flow, State state, CancellationToken cancellationToken) { var outputs = state.Outputs; state = null; // If there's any output in the current state if (outputs != null) { // Evalute each output conditions foreach (var output in outputs.OrderBy(o => o.Order)) { var isValidOutput = true; if (output.Conditions != null) { foreach (var outputCondition in output.Conditions) { isValidOutput = await EvaluateConditionAsync(outputCondition, lazyInput, context, cancellationToken); if (!isValidOutput) { break; } } } if (isValidOutput) { state = flow.States.SingleOrDefault(s => s.Id == output.StateId); break; } } } return(state); }
public IContext CreateContext(Identity user, LazyInput input, Flow flow) => new Context(user, input, flow, _contextExtension, _variableProviders);
public IContext CreateContext(Identity userIdentity, Identity ownerIdentity, LazyInput input, Flow flow) { return(new ExtensionContext(userIdentity, ownerIdentity, input, flow, _variableProviders.Value, _contextExtension.Value)); }
public async Task ProcessInputAsync(Message message, Flow flow, CancellationToken cancellationToken) { if (message == null) { throw new ArgumentNullException(nameof(message)); } if (message.From == null) { throw new ArgumentException("Message 'from' must be present", nameof(message)); } if (flow == null) { throw new ArgumentNullException(nameof(flow)); } message = _inputExpirationHandler.ValidateMessage(message); flow.Validate(); // Determine the user / owner pair var(userIdentity, ownerIdentity) = await _userOwnerResolver.GetUserOwnerIdentitiesAsync(message, flow.BuilderConfiguration, cancellationToken); // Input tracing infrastructure InputTrace inputTrace = null; TraceSettings traceSettings; if (message.Metadata != null && message.Metadata.Keys.Contains(TraceSettings.BUILDER_TRACE_TARGET)) { traceSettings = new TraceSettings(message.Metadata); } else { traceSettings = flow.TraceSettings; } if (traceSettings != null && traceSettings.Mode != TraceMode.Disabled) { inputTrace = new InputTrace { FlowId = flow.Id, User = userIdentity, Input = message.Content.ToString() }; } var inputStopwatch = inputTrace != null ? Stopwatch.StartNew() : null; var ownerContext = OwnerContext.Create(ownerIdentity); State state = default; try { // Create a cancellation token using (var cts = new CancellationTokenSource(_configuration.InputProcessingTimeout)) using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken)) { // Synchronize to avoid concurrency issues on multiple running instances var handle = await _namedSemaphore.WaitAsync($"{flow.Id}:{userIdentity}", _configuration.InputProcessingTimeout, linkedCts.Token); try { // Create the input evaluator var lazyInput = new LazyInput(message, userIdentity, flow.BuilderConfiguration, _documentSerializer, _envelopeSerializer, _artificialIntelligenceExtension, linkedCts.Token); // Load the user context var context = _contextProvider.CreateContext(userIdentity, ownerIdentity, lazyInput, flow); // Try restore a stored state var stateId = await _stateManager.GetStateIdAsync(context, linkedCts.Token); state = flow.States.FirstOrDefault(s => s.Id == stateId) ?? flow.States.Single(s => s.Root); // If current stateId of user is different of inputExpiration stop processing if (!_inputExpirationHandler.IsValidateState(state, message)) { return; } await _inputExpirationHandler.OnFlowPreProcessingAsync(state, message, _applicationNode, linkedCts.Token); // Calculate the number of state transitions var transitions = 0; // Create trace instances, if required var(stateTrace, stateStopwatch) = _traceManager.CreateStateTrace(inputTrace, state); // Process the global input actions if (flow.InputActions != null) { await ProcessActionsAsync(lazyInput, context, flow.InputActions, inputTrace?.InputActions, linkedCts.Token); } var stateWaitForInput = true; do { try { linkedCts.Token.ThrowIfCancellationRequested(); // Validate the input for the current state if (stateWaitForInput && state.Input?.Validation != null && !ValidateDocument(lazyInput, state.Input.Validation)) { if (state.Input.Validation.Error != null) { // Send the validation error message await _sender.SendMessageAsync(state.Input.Validation.Error, message.From, linkedCts.Token); } break; } // Set the input in the context if (!string.IsNullOrEmpty(state.Input?.Variable)) { await context.SetVariableAsync(state.Input.Variable, lazyInput.SerializedContent, linkedCts.Token); } // Prepare to leave the current state executing the output actions if (state.OutputActions != null) { await ProcessActionsAsync(lazyInput, context, state.OutputActions, stateTrace?.OutputActions, linkedCts.Token); } var previousStateId = state.Id; // Determine the next state state = await ProcessOutputsAsync(lazyInput, context, flow, state, stateTrace?.Outputs, linkedCts.Token); // Store the previous state await _stateManager.SetPreviousStateIdAsync(context, previousStateId, cancellationToken); // Create trace instances, if required (stateTrace, stateStopwatch) = _traceManager.CreateStateTrace(inputTrace, state, stateTrace, stateStopwatch); // Store the next state if (state != null) { await _stateManager.SetStateIdAsync(context, state.Id, linkedCts.Token); } else { await _stateManager.DeleteStateIdAsync(context, linkedCts.Token); } // Process the next state input actions if (state?.InputActions != null) { await ProcessActionsAsync(lazyInput, context, state.InputActions, stateTrace?.InputActions, linkedCts.Token); } // Check if the state transition limit has reached (to avoid loops in the flow) if (transitions++ >= _configuration.MaxTransitionsByInput) { throw new BuilderException($"Max state transitions of {_configuration.MaxTransitionsByInput} was reached"); } } catch (Exception ex) { if (stateTrace != null) { stateTrace.Error = ex.ToString(); } throw; } finally { // Continue processing if the next state do not expect the user input var inputConditionIsValid = state?.Input?.Conditions == null || await state.Input.Conditions.EvaluateConditionsAsync(lazyInput, context, cancellationToken); stateWaitForInput = state == null || (state.Input != null && !state.Input.Bypass && inputConditionIsValid); if (stateTrace?.Error != null || stateWaitForInput) { // Create a new trace if the next state waits for an input or the state without an input throws an error (stateTrace, stateStopwatch) = _traceManager.CreateStateTrace(inputTrace, state, stateTrace, stateStopwatch); } } } while (!stateWaitForInput); // Process the global output actions if (flow.OutputActions != null) { await ProcessActionsAsync(lazyInput, context, flow.OutputActions, inputTrace?.OutputActions, linkedCts.Token); } await _inputExpirationHandler.OnFlowProcessedAsync(state, message, _applicationNode, linkedCts.Token); } finally { await handle?.DisposeAsync(); } } } catch (Exception ex) { if (inputTrace != null) { inputTrace.Error = ex.ToString(); } var builderException = ex is BuilderException be ? be : new BuilderException($"Error processing input '{message.Content}' for user '{userIdentity}' in state '{state?.Id}'", ex); builderException.StateId = state?.Id; builderException.UserId = userIdentity; builderException.MessageId = message.Id; throw builderException; } finally { using (var cts = new CancellationTokenSource(_configuration.TraceTimeout)) { await _traceManager.ProcessTraceAsync(inputTrace, traceSettings, inputStopwatch, cts.Token); } ownerContext.Dispose(); } }
private async Task ProcessActionsAsync(LazyInput lazyInput, IContext context, Action[] actions, ICollection <ActionTrace> actionTraces, CancellationToken cancellationToken) { // Execute all state actions foreach (var stateAction in actions.OrderBy(a => a.Order)) { if (stateAction.Conditions != null && !await stateAction.Conditions.EvaluateConditionsAsync(lazyInput, context, cancellationToken)) { continue; } var action = _actionProvider.Get(stateAction.Type); // Trace infra var(actionTrace, actionStopwatch) = actionTraces != null ? (stateAction.ToTrace(), Stopwatch.StartNew()) : (null, null); if (actionTrace != null) { context.SetCurrentActionTrace(actionTrace); } // Configure the action timeout, that can be defined in action or flow level var executionTimeoutInSeconds = stateAction.Timeout ?? context.Flow?.BuilderConfiguration?.ActionExecutionTimeout; var executionTimeout = executionTimeoutInSeconds.HasValue ? TimeSpan.FromSeconds(executionTimeoutInSeconds.Value) : _configuration.DefaultActionExecutionTimeout; using (var cts = new CancellationTokenSource(executionTimeout)) using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken)) { try { var settings = stateAction.Settings; if (settings != null) { var settingsJson = settings.ToString(Formatting.None); settingsJson = await _variableReplacer.ReplaceAsync(settingsJson, context, cancellationToken); settings = JObject.Parse(settingsJson); } if (actionTrace != null) { actionTrace.ParsedSettings = settings; } using (LogContext.PushProperty(nameof(BuilderException.MessageId), lazyInput?.Message?.Id)) using (LogContext.PushProperty(nameof(Action.Settings), settings, true)) await action.ExecuteAsync(context, settings, linkedCts.Token); } catch (Exception ex) { if (actionTrace != null) { actionTrace.Error = ex.ToString(); } var message = ex is OperationCanceledException && cts.IsCancellationRequested ? $"The processing of the action '{stateAction.Type}' has timed out after {executionTimeout.TotalMilliseconds} ms" : $"The processing of the action '{stateAction.Type}' has failed"; var actionProcessingException = new ActionProcessingException(message, ex) { ActionType = stateAction.Type, ActionSettings = stateAction.Settings.ToObject <IDictionary <string, object> >() }; if (stateAction.ContinueOnError) { _logger.Warning(actionProcessingException, "Action '{ActionType}' has failed but was forgotten", stateAction.Type); } else { throw actionProcessingException; } } finally { actionStopwatch?.Stop(); if (actionTrace != null && actionTraces != null && actionStopwatch != null) { actionTrace.ElapsedMilliseconds = actionStopwatch.ElapsedMilliseconds; actionTraces.Add(actionTrace); } } } } }
public async Task ProcessInputAsync(Document input, Identity user, Flow flow, CancellationToken cancellationToken) { if (input == null) { throw new ArgumentNullException(nameof(input)); } if (user == null) { throw new ArgumentNullException(nameof(user)); } if (flow == null) { throw new ArgumentNullException(nameof(flow)); } flow.Validate(); // Create a cancellation token using (var cts = new CancellationTokenSource(_configuration.InputProcessingTimeout)) using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken)) { // Synchronize to avoid concurrency issues on multiple running instances var handle = await _namedSemaphore.WaitAsync($"{flow.Id}:{user}", _configuration.InputProcessingTimeout, linkedCts.Token); try { // Create the input evaluator var lazyInput = new LazyInput(input, flow.Configuration, _documentSerializer, _envelopeSerializer, _artificialIntelligenceExtension, linkedCts.Token); // Try restore a stored state var stateId = await _stateManager.GetStateIdAsync(flow.Id, user, linkedCts.Token); var state = flow.States.FirstOrDefault(s => s.Id == stateId) ?? flow.States.Single(s => s.Root); // Load the user context var context = _contextProvider.CreateContext(user, lazyInput, flow); // Calculate the number of state transitions var transitions = 0; do { linkedCts.Token.ThrowIfCancellationRequested(); // Validate the input for the current state if (state.Input != null && !state.Input.Bypass && state.Input.Validation != null && !ValidateDocument(lazyInput, state.Input.Validation)) { if (state.Input.Validation.Error != null) { // Send the validation error message await _sender.SendMessageAsync(state.Input.Validation.Error, user.ToNode(), linkedCts.Token); } break; } // Set the input in the context if (!string.IsNullOrEmpty(state.Input?.Variable)) { await context.SetVariableAsync(state.Input.Variable, lazyInput.SerializedContent, linkedCts.Token); } // Prepare to leave the current state executing the output actions if (state.OutputActions != null) { await ProcessActionsAsync(context, state.OutputActions, linkedCts.Token); } // Store the previous state and determine the next await _stateManager.SetPreviousStateIdAsync(flow.Id, context.User, state.Id, cancellationToken); state = await ProcessOutputsAsync(lazyInput, context, flow, state, linkedCts.Token); // Store the next state if (state != null) { await _stateManager.SetStateIdAsync(flow.Id, context.User, state.Id, linkedCts.Token); } else { await _stateManager.DeleteStateIdAsync(flow.Id, context.User, linkedCts.Token); } // Process the next state input actions if (state?.InputActions != null) { await ProcessActionsAsync(context, state.InputActions, linkedCts.Token); } // Check if the state transition limit has reached (to avoid loops in the flow) if (transitions++ >= _configuration.MaxTransitionsByInput) { throw new BuilderException("Max state transitions reached"); } // Continue processing if the next has do not expect the user input } while (state != null && (state.Input == null || state.Input.Bypass)); } finally { await handle.DisposeAsync(); } } }