コード例 #1
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();
            }
        }
コード例 #2
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();
                    }
                }
        }