Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dev support]: How to share TurnState within the app #2242

Open
donatas-xyz opened this issue Dec 19, 2024 · 3 comments
Open

[Dev support]: How to share TurnState within the app #2242

donatas-xyz opened this issue Dec 19, 2024 · 3 comments
Assignees
Labels
dev support Dev support tracking

Comments

@donatas-xyz
Copy link

donatas-xyz commented Dec 19, 2024

Question

I'm using AppStae class, which inherits from TurnState class. Everything works just fine, however, lately I was expanding the app to support document upload and search functionality and therefore I've refactored it to be more flexible using DI container. This way I can access various services easily from anywhere in the app.

However, same cannot be said about the AppState. As soon as I register it with builder.Services.AddScoped<AppState>();, whenever I try to populate property values in TempState, I get TurnState hasn't been loaded. Call LoadStateAsync() first... ---> Microsoft.Teams.AI.Exceptions.TeamsAIException: One or more errors occurred. (temp TurnState hasn't been loaded. Call LoadStateAsync() first.) --- End of inner exception stack trace --- at Microsoft.Teams.AI.AI.Planners.ActionPlanner1.ContinueTaskAsync(ITurnContext context, TState state, AI1 ai, CancellationToken cancellationToken) at Microsoft.Teams.AI.AI.Planners.ActionPlanner1.BeginTaskAsync(ITurnContext context, TState state, AI1 ai, CancellationToken cancellationToken) at Microsoft.Teams.AI.AI.AI1.RunAsync(ITurnContext turnContext, TState turnState, Nullable1 startTime, Int32 stepCount, CancellationToken cancellationToken) at Microsoft.Teams.AI.Application1._OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken) at Microsoft.Teams.AI.Application1.OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken) at Microsoft.Bot.Builder.MiddlewareSet.ReceiveActivityWithStatusAsync(ITurnContext turnContext, BotCallbackHandler callback, CancellationToken cancellationToken) at Microsoft.Bot.Builder.BotAdapter.RunPipelineAsync(ITurnContext turnContext, BotCallbackHandler callback, CancellationToken cancellationToken) error.

Code snippets

public class AppState : TurnState
{
    public AppState() : base()
    {
        ScopeDefaults[CONVERSATION_SCOPE] = new AppConversationState();
        ScopeDefaults[USER_SCOPE] = new AppUserState();
        ScopeDefaults[TEMP_SCOPE] = new AppTempState();
    }

    /// <summary>
    /// Stores all the conversation-related state.
    /// </summary>
    public new AppConversationState Conversation
    {
        get
        {
            TurnStateEntry scope = GetScope(CONVERSATION_SCOPE);

            if (scope == null)
            {
                throw new ArgumentException($"{CONVERSATION_SCOPE} TurnState hasn't been loaded. Call LoadStateAsync() first.");
            }

            return (AppConversationState)scope.Value!;
        }
        set
        {
            TurnStateEntry scope = GetScope(CONVERSATION_SCOPE);

            if (scope == null)
            {
                throw new ArgumentException($"{CONVERSATION_SCOPE} TurnState hasn't been loaded. Call LoadStateAsync() first.");
            }

            scope.Replace(value!);
        }
    }

    public new AppUserState User
    {
        get
        {
            TurnStateEntry scope = GetScope(USER_SCOPE);

            if (scope == null)
            {
                throw new ArgumentException($"{USER_SCOPE} TurnState hasn't been loaded. Call LoadStateAsync() first.");
            }

            return (AppUserState)scope.Value!;
        }
        set
        {
            TurnStateEntry scope = GetScope(USER_SCOPE);

            if (scope == null)
            {
                throw new ArgumentException($"{USER_SCOPE} TurnState hasn't been loaded. Call LoadStateAsync() first.");
            }

            scope.Replace(value!);
        }
    }

    public new AppTempState Temp
    {
        get
        {
            TurnStateEntry scope = GetScope(TEMP_SCOPE);

            if (scope == null)
            {
                throw new ArgumentException($"{TEMP_SCOPE} TurnState hasn't been loaded. Call LoadStateAsync() first.");
            }

            return (AppTempState)scope.Value!;
        }
        set
        {
            TurnStateEntry scope = GetScope(TEMP_SCOPE);

            if (scope == null)
            {
                throw new ArgumentException($"{TEMP_SCOPE} TurnState hasn't been loaded. Call LoadStateAsync() first.");
            }

            scope.Replace(value!);
        }
    }
}

// This class adds custom properties to the turn state which will be accessible in the activity handler methods.
public class AppConversationState : Record
{
    private const string _greetedKey = "greeted";

    public bool Greeted
    {
        get => Get<bool>(_greetedKey);
        set => Set(_greetedKey, value);
    }
}

public class AppUserState : Record
{

}

public class AppTempState : TempState
{
    private const string _documentTextKey = "documentText";

    public string? DocumentText
    {
        get => Get<string?>(_documentTextKey);
        set => Set(_documentTextKey, value);
    }

    private const string _propertyAddressKey = "propertyAddress";

    public string? PropertyAddress
    {
        get => Get<string?>(_propertyAddressKey);
        set => Set(_propertyAddressKey, value);
    }
}

It looks like authentication logic is not able to write into _turnState.Temp.AuthTokens["graph"]; and my AuthService cannot then pick it up:

    public class AuthService : IAuthService
    {
        private readonly IServiceScopeFactory _serviceScopeFactory;

        public AuthService(IServiceScopeFactory serviceScopeFactory)
        {
            _serviceScopeFactory = serviceScopeFactory;
        }

        public string GetGraphToken()
        {
            using (var scope = _serviceScopeFactory.CreateScope())
            {
                var _turnState = scope.ServiceProvider.GetRequiredService<AppState>();

                ArgumentNullException.ThrowIfNull(_turnState);

                string graphToken = string.Empty;

                if (LocalConfig.LocalSettings.Environment != "DEBUG")
                {
                    graphToken = _turnState.Temp.AuthTokens["graph"];

                    if (string.IsNullOrEmpty(graphToken))
                    {
                        throw new Exception("No auth token found in state. Authentication failed.");
                    }
                }
                else
                {
                    // FOR DEBUGGING ONLY
                    graphToken = "";
                }

                return graphToken;
            }
        }
    }

However, if I pass to action handlers - at that point I can save values into TempState just fine. What is the correct way of passing AppState object around? Unfortunately, due to firewall restrictions, authentication is one area which I cannot properly debug locally.

Auth logic I am using:

IConfidentialClientApplication msal = sp.GetService<IConfidentialClientApplication>()!;
string signInLink = $"https://{config.BOT_DOMAIN}/auth-start.html";

AuthenticationOptions<AppState> authOptions = new();
if (LocalConfig.LocalSettings.Environment != "DEBUG")
{
    authOptions.AddAuthentication("graph", new TeamsSsoSettings(config.AAD_APP_SCOPES!.Split(","), signInLink, msal));
}
@donatas-xyz donatas-xyz added the dev support Dev support tracking label Dec 19, 2024
@SubbaReddi SubbaReddi self-assigned this Dec 24, 2024
@donatas-xyz
Copy link
Author

Hi @SubbaReddi,

Apologies for being impatient, but I've tried dozens of ideas and spent days on this issue, and I still can't make AppState to be ready for the authentication logic of the app. Everything else works, if I simply hard-code the Graph token (i.e. from then on AppState properties can be written to and read from as usual), but authentication logic doesn't seem to be able to write into appState.Temp.AuthTokens["graph"] after it retrieves the token, because the AppState is not yet loaded. Just to remind you - it only happens if I register AppState as a service via builder.Services.AddScoped<AppState>(); or builder.Services.AddScoped<IAppState, AppState>();.

And I have no idea how would I get it loaded via LoadStateAsync() this early.

Thank you!

@SubbaReddi
Copy link
Contributor

@donatas-xyz : As appState is a scoped object here, I see it as an expected behavior.

@donatas-xyz
Copy link
Author

@SubbaReddi thank you. At first , I tried using it as a singleton as well, but that made no difference in its behaviour. I can try it again, if you believe that's where the issue is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dev support Dev support tracking
Projects
None yet
Development

No branches or pull requests

2 participants