Ejemplo n.º 1
0
        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();
            }
        }
Ejemplo n.º 2
0
        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);
        }
Ejemplo n.º 3
0
        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;
 }
Ejemplo n.º 5
0
 public StorageContext(
     Identity user,
     Identity application,
     LazyInput input,
     Flow flow,
     IEnumerable <IVariableProvider> variableProviders,
     IOwnerCallerNameDocumentMap ownerCallerNameDocumentMap)
     : base(user, application, input, flow, variableProviders)
 {
     _ownerCallerNameDocumentMap = ownerCallerNameDocumentMap;
 }
Ejemplo n.º 6
0
        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);
        }
Ejemplo n.º 7
0
 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);
 }
Ejemplo n.º 8
0
        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));
            }
        }
Ejemplo n.º 9
0
        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));
            }
        }
Ejemplo n.º 10
0
        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);
        }
Ejemplo n.º 11
0
 public IContext CreateContext(Identity user, LazyInput input, Flow flow) => new Context(user, input, flow, _contextExtension, _variableProviders);
Ejemplo n.º 12
0
 public IContext CreateContext(Identity userIdentity, Identity ownerIdentity, LazyInput input, Flow flow)
 {
     return(new ExtensionContext(userIdentity, ownerIdentity, input, flow, _variableProviders.Value, _contextExtension.Value));
 }
Ejemplo n.º 13
0
        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();
            }
        }
Ejemplo n.º 14
0
        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);
                            }
                        }
                    }
            }
        }
Ejemplo n.º 15
0
        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();
                    }
                }
        }