8000 Merge pull request #180 from microsoft/axsuarez/error-handling-in-ada… · zigri2612/botbuilder-python@95843b2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 95843b2

Browse files
authored
Merge pull request microsoft#180 from microsoft/axsuarez/error-handling-in-adapter
On turn error on Bot Adapter
2 parents fe82333 + f27650a commit 95843b2

File tree

20 files changed

+370
-67
lines changed

20 files changed

+370
-67
lines changed

libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ async def _recognize_internal(
290290
telemetry_metrics: Dict[str, float],
291291
) -> RecognizerResult:
292292

293-
BotAssert.context_not_null(turn_context)
293+
BotAssert.context_not_none(turn_context)
294294

295295
if turn_context.activity.type != ActivityTypes.message:
296296
return None

libraries/botbuilder-core/botbuilder/core/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
# Licensed under the MIT License. See License.txt in the project root for
55
# license information.
66
# --------------------------------------------------------------------------
7+
from . import conversation_reference_extension
78

89
from .about import __version__
910
from .activity_handler import ActivityHandler
10-
from .assertions import BotAssert
11+
from .bot_assert import BotAssert
1112
from .bot_adapter import BotAdapter
1213
from .bot_framework_adapter import BotFrameworkAdapter, BotFrameworkAdapterSettings
1314
from .bot_state import BotState
@@ -35,6 +36,7 @@
3536
'calculate_change_hash',
3637
'CardFactory',
3738
'ConversationState',
39+
'conversation_reference_extension',
3840
'MemoryStorage',
3941
'MessageFactory',
4042
'Middleware',

libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import asyncio
55
import inspect
66
from datetime import datetime
7-
from typing import Coroutine, List
7+
from typing import Coroutine, List, Callable
88
from copy import copy
99
from ..bot_adapter import BotAdapter
1010
from ..turn_context import TurnContext
@@ -77,15 +77,15 @@ async def update_activity(self, context, activity: Activity):
7777
"""
7878
self.updated_activities.append(activity)
7979

80-
async def continue_conversation(self, reference, logic):
80+
async def continue_conversation(self, bot_id: str, reference: ConversationReference, callback: Callable):
8181
"""
82-
The `TestAdapter` doesn't implement `continueConversation()` and will return an error if it's
83-
called.
82+
The `TestAdapter` just calls parent implementation.
83+
:param bot_id
8484
:param reference:
85-
:param logic:
85+
:param callback:
8686
:return:
8787
"""
88-
raise NotImplementedError('TestAdapter.continue_conversation(): is not implemented.')
88+
await super().continue_conversation(bot_id, reference, callback)
8989

9090
async def receive_activity(self, activity):
9191
"""
@@ -110,7 +110,7 @@ async def receive_activity(self, activity):
110110

111111
# Create context object and run middleware
112112
context = TurnContext(self, request)
113-
return await self.run_middleware(context, self.logic)
113+
return await self.run_pipeline(context, self.logic)
114114

115115
async def send(self, user_says) -> object:
116116
"""

libraries/botbuilder-core/botbuilder/core/assertions.py

Lines changed: 0 additions & 40 deletions
This file was deleted.

libraries/botbuilder-core/botbuilder/core/bot_adapter.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
# Licensed under the MIT License.
33

44
from abc import ABC, abstractmethod
5-
from typing import List, Callable
5+
from typing import List, Callable, Awaitable
66
from botbuilder.schema import Activity, ConversationReference
77

8+
from . import conversation_reference_extension
9+
from .bot_assert import BotAssert
810
from .turn_context import TurnContext
911
from .middleware_set import MiddlewareSet
1012

1113

1214
class BotAdapter(ABC):
13-
def __init__(self):
15+
def __init__(self, on_turn_error: Callable[[TurnContext, Exception], Awaitable] = None):
1416
self._middleware = MiddlewareSet()
17+
self.on_turn_error = on_turn_error
1518

1619
@abstractmethod
1720
async def send_activities(self, context: TurnContext, activities: List[Activity]):
@@ -47,13 +50,41 @@ def use(self, middleware):
4750
:return:
4851
"""
4952
self._middleware.use(middleware)
53+
return self
54+
55+
async def continue_conversation(self, bot_id: str, reference: ConversationReference, callback: Callable):
56+
"""
57+
Sends a proactive message to a conversation. Call this method to proactively send a message to a conversation.
58+
Most _channels require a user to initiate a conversation with a bot before the bot can send activities
59+
to the user.
60+
:param bot_id: The application ID of the bot. This paramter is ignored in
61+
single tenant the Adpters (Console, Test, etc) but is critical to the BotFrameworkAdapter
62+
which is multi-tenant aware. </param>
63+
:param reference: A reference to the conversation to continue.</param>
64+
:param callback: The method to call for the resulting bot turn.</param>
65+
"""
66+
context = TurnContext(self, conversation_reference_extension.get_continuation_activity(reference))
67+
return await self.run_pipeline(context, callback)
5068

51-
async def run_middleware(self, context: TurnContext, callback: Callable=None):
69+
async def run_pipeline(self, context: TurnContext, callback: Callable[[TurnContext], Awaitable]= None):
5270
"""
5371
Called by the parent class to run the adapters middleware set and calls the passed in `callback()` handler at
5472
the end of the chain.
5573
:param context:
5674
:param callback:
5775
:return:
5876
"""
59-
return await self._middleware.receive_activity_with_status(context, callback)
77+
BotAssert.context_not_none(context)
78+
79+
if context.activity is not None:
80+
try:
81+
return await self._middleware.receive_activity_with_status(context, callback)
82+
except Exception as error:
83+
if self.on_turn_error is not None:
84+
await self.on_turn_error(context, error)
85+
else:
86+
raise error
87+
else:
88+
# callback to caller on proactive case
89+
if callback is not None:
90+
await callback(context)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from typing import List
5+
6+
from botbuilder.schema import Activity, ConversationReference
7+
from .middleware_set import Middleware
8+
from .turn_context import TurnContext
9+
10+
class BotAssert():
11+
12+
@staticmethod
13+
def activity_not_none(activity: Activity) -> None:
14+
"""
15+
Checks that an activity object is not None
16+
:param activity: The activity object
17+
"""
18+
if activity is None:
19+
raise TypeError(activity.__class__.__name__)
20+
21+
@staticmethod
22+
def context_not_none(turn_context: TurnContext) -> None:
23+
"""
24+
Checks that a context object is not None
25+
:param turn_context: The context object
26+
"""
27+
if turn_context is None:
28+
raise TypeError(turn_context.__class__.__name__)
29+
30+
@staticmethod
31+
def conversation_reference_not_none(reference: ConversationReference) -> None:
32+
"""
33+
Checks that a conversation reference object is not None
34+
:param reference: The conversation reference object
35+
"""
36+
if reference is None:
37+
raise TypeError(reference.__class__.__name__)
38+
39+
@staticmethod
40+
def activity_list_not_none(activities: List[Activity]) -> None:
41+
"""
42+
Checks that an activity list is not None
43+
:param activities: The activity list
44+
"""
45+
if activities is None:
46+
raise TypeError(activities.__class__.__name__)
47+
48+
@staticmethod
49+
def middleware_not_none(middleware: Middleware) -> None:
50+
"""
51+
Checks that a middleware object is not None
52+
:param middleware: The middleware object
53+
"""
54+
if middleware is None:
55+
raise TypeError(middleware.__class__.__name__)
56+
57+
@staticmethod
58+
def middleware_list_not_none(middleware: List[Middleware]) -> None:
59+
"""
60+
Checks that a middeware list is not None
61+
:param activities: The middleware list
62+
"""
63+
if middleware is None:
64+
raise TypeError(middleware.__class__.__name__)
65+
66+

libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Licensed under the MIT License.
33

44
import asyncio
5-
from typing import List, Callable
5+
from typing import List, Callable, Awaitable
66
from botbuilder.schema import (Activity, ChannelAccount,
77
ConversationAccount,
88
ConversationParameters, ConversationReference,
@@ -46,9 +46,9 @@ async def continue_conversation(self, reference: ConversationReference, logic):
4646
"""
4747
request = TurnContext.apply_conversation_reference(Activity(), reference, is_incoming=True)
4848
context = self.create_context(request)
49-
return await self.run_middleware(context, logic)
49+
return await self.run_pipeline(context, logic)
5050

51-
async def create_conversation(self, reference: ConversationReference, logic):
51+
async def create_conversation(self, reference: ConversationReference, logic: Callable[[TurnContext], Awaitable]=None):
5252
"""
5353
Starts a new conversation with a user. This is typically used to Direct Message (DM) a member
5454
of a group.
@@ -71,7 +71,7 @@ async def create_conversation(self, reference: ConversationReference, logic):
7171
request.service_url = resource_response.service_url
7272

7373
context = self.create_context(request)
74-
return await self.run_middleware(context, logic)
74+
return await self.run_pipeline(context, logic)
7575

7676
except Exception as e:
7777
raise e
@@ -92,7 +92,7 @@ async def process_activity(self, req, auth_header: str, logic: Callable):
9292
await self.authenticate_request(activity, auth_header)
9393
context = self.create_context(activity)
9494

95-
return await self.run_middleware(context, logic)
95+
return await self.run_pipeline(context, logic)
9696

9797
async def authenticate_request(self, request: Activity, auth_header: str):
9898
"""
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import uuid
4+
from botbuilder.schema import Activity, ActivityTypes, ConversationReference
5+
6+
def get_continuation_activity(reference: ConversationReference) -> Activity:
7+
return Activity(
8+
type= ActivityTypes.event,
9+
name= "ContinueConversation",
10+
id= str(uuid.uuid1()),
11+
channel_id= reference.channel_id,
12+
service_url= reference.service_url,
13+
conversation= reference.conversation,
14+
recipient= reference.bot,
15+
from_property= reference.user,
16+
relates_to= reference
17+
)

libraries/botbuilder-core/botbuilder/core/middleware_set.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33

44
from asyncio import iscoroutinefunction
55
from abc import ABC, abstractmethod
6+
from typing import Awaitable, Callable
67

78
from .turn_context import TurnContext
89

910

1011
class Middleware(ABC):
1112
@abstractmethod
12-
def on_process_request(self, context: TurnContext, next): pass
13+
def on_process_request(self, context: TurnContext, next: Callable): pass
1314

1415

1516
class AnonymousReceiveMiddleware(Middleware):
@@ -48,16 +49,16 @@ def use(self, *middleware: Middleware):
4849
async def receive_activity(self, context: TurnContext):
4950
await self.receive_activity_internal(context, None)
5051

51-
async def on_process_request(self, context, logic):
52+
async def on_process_request(self, context: TurnContext, logic: Callable[[TurnContext], Awaitable]):
5253
await self.receive_activity_internal(context, None)
5354
await logic()
5455

55-
async def receive_activity_with_status(self, context: TurnContext, callback):
56+
async def receive_activity_with_status(self, context: TurnContext, callback: Callable[[TurnContext], Awaitable]):
5657
return await self.receive_activity_internal(context, callback)
5758

58-
async def receive_activity_internal(self, context, callback, next_middleware_index=0):
59+
async def receive_activity_internal(self, context: TurnContext, callback: Callable[[TurnContext], Awaitable], next_middleware_index: int = 0):
5960
if next_middleware_index == len(self._middleware):
60-
if callback:
61+
if callback is not None:
6162
return await callback(context)
6263
else:
6364
return None

libraries/botbuilder-core/botbuilder/core/turn_context.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
ConversationReference,
1111
ResourceResponse
1212
)
13-
from .assertions import BotAssert
1413

1514

1615
class TurnContext(object):
@@ -148,7 +147,9 @@ async def callback(context: 'TurnContext', output):
148147
context._responded = True
149148
return responses
150149

151-
await self._emit(self._on_send_activities, output, callback(self, output))
150+
result = await self._emit(self._on_send_activities, output, callback(self, output))
151+
152+
return result[0] if result is not None and len(result) > 0 else ResourceResponse()
152153

153154
async def update_activity(self, activity: Activity):
154155
"""

0 commit comments

Comments
 (0)
0