Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
from contextlib import nullcontext
from copy import copy
from functools import partial
from typing_extensions import deprecated
Comment thread
rodrigobr-msft marked this conversation as resolved.

import re
from typing import (
Any,
Awaitable,
Callable,
Generic,
Optional,
TypeVar,
cast,
overload,
Expand Down Expand Up @@ -74,22 +74,22 @@ class AgentApplication(Agent, Generic[StateT]):
typing: TypingIndicator

_options: ApplicationOptions
_adapter: Optional[ChannelServiceAdapter] = None
_adapter: ChannelServiceAdapter | None = None
_auth: Authorization
_proactive: Optional[Proactive] = None
_proactive: Proactive | None = None
_internal_before_turn: list[Callable[[TurnContext, StateT], Awaitable[bool]]]
_internal_after_turn: list[Callable[[TurnContext, StateT], Awaitable[bool]]]
_route_list: _RouteList[StateT]
_error: Optional[Callable[[TurnContext, Exception], Awaitable[None]]] = None
_turn_state_factory: Optional[Callable[[TurnContext], StateT]] = None
_error: Callable[[TurnContext, Exception], Awaitable[None]] | None = None
_turn_state_factory: Callable[[], StateT] | None = None
_connection_manager: Connections

def __init__(
self,
options: Optional[ApplicationOptions] = None,
options: ApplicationOptions | None = None,
*,
connection_manager: Optional[Connections] = None,
authorization: Optional[Authorization] = None,
connection_manager: Connections | None = None,
authorization: Authorization | None = None,
**kwargs,
) -> None:
"""
Expand Down Expand Up @@ -131,6 +131,7 @@ def __init__(
raise ApplicationError("""
The `ApplicationOptions.storage` property is required and was not configured.
""")
self._storage = self._options.storage

if options.long_running_messages and (
not options.adapter or not options.bot_app_id
Expand All @@ -150,13 +151,13 @@ def __init__(
self._turn_state_factory = (
options.turn_state_factory
or kwargs.get("turn_state_factory", None)
or partial(TurnState.with_storage, self._options.storage)
or partial(TurnState.with_storage, self._storage)
)
Comment thread
rodrigobr-msft marked this conversation as resolved.

if options.proactive:
proactive_opts = copy(options.proactive)
if not proactive_opts.storage:
proactive_opts.storage = self._options.storage
proactive_opts.storage = self._storage
self._proactive = Proactive(self, proactive_opts)

# TODO: decide how to initialize the Authorization (params vs options vs kwargs)
Expand Down Expand Up @@ -186,7 +187,7 @@ def __init__(
if key not in ["storage", "connection_manager", "handlers"]
}
self._auth = Authorization(
storage=self._options.storage,
storage=self._storage,
connection_manager=connection_manager,
auth_handlers=options.authorization_handlers,
**auth_options,
Expand Down Expand Up @@ -793,7 +794,7 @@ async def on_error(context: TurnContext, err: Exception):

return func

def turn_state_factory(self, func: Callable[[TurnContext], Awaitable[StateT]]):
def set_turn_state_factory(self, func: Callable[[], StateT]):
"""
Custom Turn State Factory
"""
Expand Down Expand Up @@ -873,6 +874,9 @@ def _remove_mentions(self, context: TurnContext):
context.activity.text = context.remove_recipient_mention(context.activity)

@staticmethod
@deprecated(
"Use `load_configuration_from_env` from `microsoft_agents.activity` instead."
)
def parse_env_vars_configuration(vars: dict[str, Any]) -> dict:
Comment thread
rodrigobr-msft marked this conversation as resolved.
"""
Parses environment variables and returns a dictionary with the relevant configuration.
Expand Down Expand Up @@ -908,12 +912,12 @@ async def _initialize_state(self, context: TurnContext) -> StateT:
turn_state = self._turn_state_factory()
else:
logger.debug("Using default turn state factory")
turn_state = TurnState.with_storage(self._options.storage)
turn_state = TurnState.with_storage(self._storage)

turn_state = cast(StateT, turn_state)

logger.debug("Loading turn state from storage")
await turn_state.load(context, self._options.storage)
await turn_state.load(context, self._storage)
turn_state.temp.input = context.activity.text
return turn_state

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ def __init__(
if auth_handler:
self._handler = auth_handler
else:
if not auth_handler_settings:
raise ValueError(
"auth_handler_settings must be provided if auth_handler is None."
)
self._handler = AuthHandler._from_settings(auth_handler_settings)

self._id = auth_handler_id or self._handler.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from __future__ import annotations
import logging
from typing import Optional
from typing import cast

from microsoft_agents.activity import (
Activity,
Expand All @@ -24,6 +24,8 @@
from microsoft_agents.activity.token_exchange_invoke_response import (
TokenExchangeInvokeResponse,
)

from microsoft_agents.hosting.core.authorization import ClaimsIdentity
from microsoft_agents.hosting.core._oauth._flow_state import _FlowErrorTag
from microsoft_agents.hosting.core.card_factory import CardFactory
from microsoft_agents.hosting.core.message_factory import MessageFactory
Expand Down Expand Up @@ -65,9 +67,14 @@ async def _load_flow(
context and the specified auth handler.
:rtype: tuple[OAuthFlow, FlowStorageClient]
"""
user_token_client: UserTokenClient = context.turn_state.get(
context.adapter.USER_TOKEN_CLIENT_KEY
user_token_client = cast(
UserTokenClient | None,
context.turn_state.get(context.adapter.USER_TOKEN_CLIENT_KEY),
)
if not user_token_client:
raise ValueError(
"UserTokenClient is required in TurnState for OAuth flow handling."
)

if (
not context.activity.channel_id
Expand All @@ -79,14 +86,21 @@ async def _load_flow(
channel_id = context.activity.channel_id
user_id = context.activity.from_property.id

ms_app_id = context.turn_state.get(context.adapter.AGENT_IDENTITY_KEY).claims[
"aud"
]
identity = cast(
ClaimsIdentity | None,
context.turn_state.get(context.adapter.AGENT_IDENTITY_KEY),
)
if not identity or "aud" not in identity.claims:
raise ValueError(
"ClaimsIdentity with 'aud' claim is required in TurnState for OAuth flow handling."
)

ms_app_id = identity.claims["aud"]

# try to load existing state
flow_storage_client = _FlowStorageClient(channel_id, user_id, self._storage)
logger.info("Loading OAuth flow state from storage")
flow_state: _FlowState = await flow_storage_client.read(self._id)
flow_state: _FlowState | None = await flow_storage_client.read(self._id)
if not flow_state:
logger.info("No existing flow state found, creating new flow state")
flow_state = _FlowState(
Expand All @@ -105,8 +119,8 @@ async def _handle_obo(
self,
context: TurnContext,
input_token_response: TokenResponse,
exchange_connection: Optional[str] = None,
exchange_scopes: Optional[list[str]] = None,
exchange_connection: str | None = None,
exchange_scopes: list[str] | None = None,
) -> TokenResponse:
"""
Exchanges a token for another token with different scopes.
Expand Down Expand Up @@ -244,8 +258,8 @@ async def _handle_flow_response(
async def _sign_in(
self,
context: TurnContext,
exchange_connection: Optional[str] = None,
exchange_scopes: Optional[list[str]] = None,
exchange_connection: str | None = None,
exchange_scopes: list[str] | None = None,
) -> _SignInResponse:
"""Begins or continues an OAuth flow.

Expand Down Expand Up @@ -296,17 +310,17 @@ async def _sign_in(
async def get_refreshed_token(
self,
context: TurnContext,
exchange_connection: Optional[str] = None,
exchange_scopes: Optional[list[str]] = None,
exchange_connection: str | None = None,
exchange_scopes: list[str] | None = None,
) -> TokenResponse:
"""Attempts to get a refreshed token for the user with the given scopes

:param context: The turn context for the current turn of conversation.
:type context: TurnContext
:param exchange_connection: Optional name of the connection to use for token exchange. If None, default connection will be used.
:type exchange_connection: Optional[str], Optional
:type exchange_connection: str | None, Optional
:param exchange_scopes: Optional list of scopes to request during token exchange. If None, default scopes will be used.
:type exchange_scopes: Optional[list[str]], Optional
:type exchange_scopes: list[str] | None, Optional
"""
flow, _ = await self._load_flow(context)
input_token_response = await flow.get_user_token()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,19 @@ async def get_agentic_instance_token(self, context: TurnContext) -> TokenRespons
)
agentic_instance_id = context.activity.get_agentic_instance_id()
assert agentic_instance_id

tenant_id = context.activity.get_agentic_tenant_id()
if not tenant_id:
logger.error(
"Unable to retrieve agentic instance token: missing agentic tenant Id. Agentic Instance ID: %s",
agentic_instance_id,
)
raise ValueError(
f"Unable to retrieve agentic instance token: missing agentic tenant Id. Agentic Instance ID: {agentic_instance_id}"
)
Comment thread
rodrigobr-msft marked this conversation as resolved.

instance_token, _ = await connection.get_agentic_instance_token(
context.activity.get_agentic_tenant_id(), agentic_instance_id
tenant_id, agentic_instance_id
)
return (
TokenResponse(token=instance_token) if instance_token else TokenResponse()
Expand Down Expand Up @@ -131,8 +142,18 @@ async def get_agentic_user_token(
f"Unable to retrieve agentic user token: missing agentic User Id or agentic instance Id. agentic_user_id: {agentic_user_id}, Agentic Instance ID: {agentic_instance_id}"
)

tenant_id = context.activity.get_agentic_tenant_id()
if not tenant_id:
logger.error(
"Unable to retrieve agentic user token: missing agentic tenant Id. Agentic Instance ID: %s",
agentic_instance_id,
)
raise ValueError(
f"Unable to retrieve agentic user token: missing agentic tenant Id. Agentic Instance ID: {agentic_instance_id}"
)
Comment thread
rodrigobr-msft marked this conversation as resolved.

token = await connection.get_agentic_user_token(
context.activity.get_agentic_tenant_id(),
tenant_id,
agentic_instance_id,
agentic_user_id,
scopes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ async def _handle_obo(
scopes=scopes,
user_assertion=input_token_response.token,
)
return TokenResponse(token=token) if token else None
return TokenResponse(token=token) if token else TokenResponse()

def _create_token_response(self, context: TurnContext) -> TokenResponse:
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from __future__ import annotations

import uuid
import asyncio
import logging
from typing import Optional, Callable, Literal, cast
from typing import Optional, Callable, Literal, cast, TYPE_CHECKING

from microsoft_agents.activity import (
Activity,
Expand All @@ -24,6 +26,9 @@
from .citation import Citation
from .citation_util import CitationUtil

if TYPE_CHECKING:
from microsoft_agents.hosting.core.turn_context import TurnContext

logger = logging.getLogger(__name__)


Expand All @@ -39,7 +44,7 @@ class StreamingResponse:
Once `end_stream()` is called, the stream is considered ended and no further updates can be sent.
"""

def __init__(self, context: "TurnContext"):
def __init__(self, context: TurnContext):
Comment thread
rodrigobr-msft marked this conversation as resolved.
"""
Creates a new StreamingResponse instance.

Expand Down Expand Up @@ -267,7 +272,7 @@ async def wait_for_queue(self) -> None:
if self._queue_sync:
await self._queue_sync

def _set_defaults(self, context: "TurnContext"):
def _set_defaults(self, context: TurnContext):
Comment thread
rodrigobr-msft marked this conversation as resolved.

channel = (
context.activity.channel_id.channel if context.activity.channel_id else None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ async def test_get_refreshed_token_handles_obo_failure(
context, "some_connection", ["scope1"]
)

# Should return None when OBO fails
assert token_response is None
# Should return an empty TokenResponse when OBO fails
assert token_response == TokenResponse()

@pytest.mark.asyncio
async def test_sign_out_is_noop(
Expand Down
Loading