microsoft-agents-hosting-core 0.6.1__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- microsoft_agents/hosting/core/__init__.py +24 -1
- microsoft_agents/hosting/core/app/agent_application.py +12 -24
- microsoft_agents/hosting/core/app/streaming/__init__.py +14 -0
- microsoft_agents/hosting/core/app/streaming/citation.py +22 -0
- microsoft_agents/hosting/core/app/streaming/citation_util.py +85 -0
- microsoft_agents/hosting/core/app/streaming/streaming_response.py +411 -0
- microsoft_agents/hosting/core/authorization/claims_identity.py +14 -0
- microsoft_agents/hosting/core/channel_service_adapter.py +28 -14
- microsoft_agents/hosting/core/connector/client/connector_client.py +0 -1
- microsoft_agents/hosting/core/connector/client/user_token_client.py +0 -1
- microsoft_agents/hosting/core/http/__init__.py +17 -0
- microsoft_agents/hosting/core/http/_channel_service_routes.py +202 -0
- microsoft_agents/hosting/core/http/_http_adapter_base.py +136 -0
- microsoft_agents/hosting/core/http/_http_request_protocol.py +36 -0
- microsoft_agents/hosting/core/http/_http_response.py +56 -0
- microsoft_agents/hosting/core/rest_channel_service_client_factory.py +2 -2
- microsoft_agents/hosting/core/storage/memory_storage.py +0 -1
- microsoft_agents/hosting/core/storage/storage.py +0 -1
- microsoft_agents/hosting/core/storage/transcript_logger.py +0 -1
- {microsoft_agents_hosting_core-0.6.1.dist-info → microsoft_agents_hosting_core-0.7.0.dist-info}/METADATA +21 -3
- {microsoft_agents_hosting_core-0.6.1.dist-info → microsoft_agents_hosting_core-0.7.0.dist-info}/RECORD +24 -15
- {microsoft_agents_hosting_core-0.6.1.dist-info → microsoft_agents_hosting_core-0.7.0.dist-info}/WHEEL +0 -0
- {microsoft_agents_hosting_core-0.6.1.dist-info → microsoft_agents_hosting_core-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {microsoft_agents_hosting_core-0.6.1.dist-info → microsoft_agents_hosting_core-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -341,6 +341,19 @@ class ChannelServiceAdapter(ChannelAdapter, ABC):
|
|
|
341
341
|
await connector_client.close()
|
|
342
342
|
await user_token_client.close()
|
|
343
343
|
|
|
344
|
+
def _resolve_if_connector_client_is_needed(self, activity: Activity) -> bool:
|
|
345
|
+
"""Determine if a connector client is needed based on the activity's delivery mode and service URL.
|
|
346
|
+
|
|
347
|
+
:param activity: The activity to evaluate.
|
|
348
|
+
:type activity: :class:`microsoft_agents.activity.Activity`
|
|
349
|
+
"""
|
|
350
|
+
if activity.delivery_mode in [
|
|
351
|
+
DeliveryModes.expect_replies,
|
|
352
|
+
DeliveryModes.stream,
|
|
353
|
+
]:
|
|
354
|
+
return False
|
|
355
|
+
return True
|
|
356
|
+
|
|
344
357
|
async def process_activity(
|
|
345
358
|
self,
|
|
346
359
|
claims_identity: ClaimsIdentity,
|
|
@@ -368,16 +381,14 @@ class ChannelServiceAdapter(ChannelAdapter, ABC):
|
|
|
368
381
|
If the task completes successfully, then an :class:`microsoft_agents.activity.InvokeResponse` is returned;
|
|
369
382
|
otherwise, `None` is returned.
|
|
370
383
|
"""
|
|
371
|
-
scopes: list[str] =
|
|
384
|
+
scopes: list[str] = claims_identity.get_token_scope()
|
|
372
385
|
outgoing_audience: str = None
|
|
373
386
|
|
|
374
387
|
if claims_identity.is_agent_claim():
|
|
375
388
|
outgoing_audience = claims_identity.get_token_audience()
|
|
376
|
-
scopes = [f"{claims_identity.get_outgoing_app_id()}/.default"]
|
|
377
389
|
activity.caller_id = f"{CallerIdConstants.agent_to_agent_prefix}{claims_identity.get_outgoing_app_id()}"
|
|
378
390
|
else:
|
|
379
391
|
outgoing_audience = AuthenticationConstants.AGENTS_SDK_SCOPE
|
|
380
|
-
scopes = [f"{AuthenticationConstants.AGENTS_SDK_SCOPE}/.default"]
|
|
381
392
|
|
|
382
393
|
use_anonymous_auth_callback = False
|
|
383
394
|
if (
|
|
@@ -403,21 +414,24 @@ class ChannelServiceAdapter(ChannelAdapter, ABC):
|
|
|
403
414
|
context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client
|
|
404
415
|
|
|
405
416
|
# Create the connector client to use for outbound requests.
|
|
406
|
-
connector_client: ConnectorClient =
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
417
|
+
connector_client: Optional[ConnectorClient] = None
|
|
418
|
+
if self._resolve_if_connector_client_is_needed(activity):
|
|
419
|
+
connector_client = (
|
|
420
|
+
await self._channel_service_client_factory.create_connector_client(
|
|
421
|
+
context,
|
|
422
|
+
claims_identity,
|
|
423
|
+
activity.service_url,
|
|
424
|
+
outgoing_audience,
|
|
425
|
+
scopes,
|
|
426
|
+
use_anonymous_auth_callback,
|
|
427
|
+
)
|
|
414
428
|
)
|
|
415
|
-
|
|
416
|
-
context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client
|
|
429
|
+
context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client
|
|
417
430
|
|
|
418
431
|
await self.run_pipeline(context, callback)
|
|
419
432
|
|
|
420
|
-
|
|
433
|
+
if connector_client:
|
|
434
|
+
await connector_client.close()
|
|
421
435
|
await user_token_client.close()
|
|
422
436
|
|
|
423
437
|
# If there are any results they will have been left on the TurnContext.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""HTTP abstractions for framework-agnostic adapter implementations."""
|
|
5
|
+
|
|
6
|
+
from ._http_request_protocol import HttpRequestProtocol
|
|
7
|
+
from ._http_response import HttpResponse, HttpResponseFactory
|
|
8
|
+
from ._http_adapter_base import HttpAdapterBase
|
|
9
|
+
from ._channel_service_routes import ChannelServiceRoutes
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"HttpRequestProtocol",
|
|
13
|
+
"HttpResponse",
|
|
14
|
+
"HttpResponseFactory",
|
|
15
|
+
"HttpAdapterBase",
|
|
16
|
+
"ChannelServiceRoutes",
|
|
17
|
+
]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""Channel service route definitions (framework-agnostic logic)."""
|
|
5
|
+
|
|
6
|
+
from typing import Type, List, Union
|
|
7
|
+
|
|
8
|
+
from microsoft_agents.activity import (
|
|
9
|
+
AgentsModel,
|
|
10
|
+
Activity,
|
|
11
|
+
AttachmentData,
|
|
12
|
+
ConversationParameters,
|
|
13
|
+
Transcript,
|
|
14
|
+
)
|
|
15
|
+
from microsoft_agents.hosting.core import ChannelApiHandlerProtocol
|
|
16
|
+
|
|
17
|
+
from ._http_request_protocol import HttpRequestProtocol
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ChannelServiceRoutes:
|
|
21
|
+
"""Defines the Channel Service API routes and their handlers.
|
|
22
|
+
|
|
23
|
+
This class provides framework-agnostic route logic that can be
|
|
24
|
+
adapted to different web frameworks (aiohttp, FastAPI, etc.).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, handler: ChannelApiHandlerProtocol, base_url: str = ""):
|
|
28
|
+
"""Initialize channel service routes.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
handler: The handler that implements the Channel API protocol.
|
|
32
|
+
base_url: Optional base URL prefix for all routes.
|
|
33
|
+
"""
|
|
34
|
+
self.handler = handler
|
|
35
|
+
self.base_url = base_url
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
async def deserialize_from_body(
|
|
39
|
+
request: HttpRequestProtocol, target_model: Type[AgentsModel]
|
|
40
|
+
) -> AgentsModel:
|
|
41
|
+
"""Deserialize request body to target model."""
|
|
42
|
+
content_type = request.headers.get("Content-Type", "")
|
|
43
|
+
if "application/json" not in content_type:
|
|
44
|
+
raise ValueError("Content-Type must be application/json")
|
|
45
|
+
|
|
46
|
+
body = await request.json()
|
|
47
|
+
return target_model.model_validate(body)
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def serialize_model(model_or_list: Union[AgentsModel, List[AgentsModel]]) -> dict:
|
|
51
|
+
"""Serialize model or list of models to JSON-compatible dict."""
|
|
52
|
+
if isinstance(model_or_list, AgentsModel):
|
|
53
|
+
return model_or_list.model_dump(
|
|
54
|
+
mode="json", exclude_unset=True, by_alias=True
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
return [
|
|
58
|
+
model.model_dump(mode="json", exclude_unset=True, by_alias=True)
|
|
59
|
+
for model in model_or_list
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# Route handler methods
|
|
63
|
+
async def send_to_conversation(self, request: HttpRequestProtocol) -> dict:
|
|
64
|
+
"""Handle POST /v3/conversations/{conversation_id}/activities."""
|
|
65
|
+
activity = await self.deserialize_from_body(request, Activity)
|
|
66
|
+
conversation_id = request.get_path_param("conversation_id")
|
|
67
|
+
result = await self.handler.on_send_to_conversation(
|
|
68
|
+
request.get_claims_identity(),
|
|
69
|
+
conversation_id,
|
|
70
|
+
activity,
|
|
71
|
+
)
|
|
72
|
+
return self.serialize_model(result)
|
|
73
|
+
|
|
74
|
+
async def reply_to_activity(self, request: HttpRequestProtocol) -> dict:
|
|
75
|
+
"""Handle POST /v3/conversations/{conversation_id}/activities/{activity_id}."""
|
|
76
|
+
activity = await self.deserialize_from_body(request, Activity)
|
|
77
|
+
conversation_id = request.get_path_param("conversation_id")
|
|
78
|
+
activity_id = request.get_path_param("activity_id")
|
|
79
|
+
result = await self.handler.on_reply_to_activity(
|
|
80
|
+
request.get_claims_identity(),
|
|
81
|
+
conversation_id,
|
|
82
|
+
activity_id,
|
|
83
|
+
activity,
|
|
84
|
+
)
|
|
85
|
+
return self.serialize_model(result)
|
|
86
|
+
|
|
87
|
+
async def update_activity(self, request: HttpRequestProtocol) -> dict:
|
|
88
|
+
"""Handle PUT /v3/conversations/{conversation_id}/activities/{activity_id}."""
|
|
89
|
+
activity = await self.deserialize_from_body(request, Activity)
|
|
90
|
+
conversation_id = request.get_path_param("conversation_id")
|
|
91
|
+
activity_id = request.get_path_param("activity_id")
|
|
92
|
+
result = await self.handler.on_update_activity(
|
|
93
|
+
request.get_claims_identity(),
|
|
94
|
+
conversation_id,
|
|
95
|
+
activity_id,
|
|
96
|
+
activity,
|
|
97
|
+
)
|
|
98
|
+
return self.serialize_model(result)
|
|
99
|
+
|
|
100
|
+
async def delete_activity(self, request: HttpRequestProtocol) -> None:
|
|
101
|
+
"""Handle DELETE /v3/conversations/{conversation_id}/activities/{activity_id}."""
|
|
102
|
+
conversation_id = request.get_path_param("conversation_id")
|
|
103
|
+
activity_id = request.get_path_param("activity_id")
|
|
104
|
+
await self.handler.on_delete_activity(
|
|
105
|
+
request.get_claims_identity(),
|
|
106
|
+
conversation_id,
|
|
107
|
+
activity_id,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
async def get_activity_members(self, request: HttpRequestProtocol) -> dict:
|
|
111
|
+
"""Handle GET /v3/conversations/{conversation_id}/activities/{activity_id}/members."""
|
|
112
|
+
conversation_id = request.get_path_param("conversation_id")
|
|
113
|
+
activity_id = request.get_path_param("activity_id")
|
|
114
|
+
result = await self.handler.on_get_activity_members(
|
|
115
|
+
request.get_claims_identity(),
|
|
116
|
+
conversation_id,
|
|
117
|
+
activity_id,
|
|
118
|
+
)
|
|
119
|
+
return self.serialize_model(result)
|
|
120
|
+
|
|
121
|
+
async def create_conversation(self, request: HttpRequestProtocol) -> dict:
|
|
122
|
+
"""Handle POST /."""
|
|
123
|
+
conversation_parameters = await self.deserialize_from_body(
|
|
124
|
+
request, ConversationParameters
|
|
125
|
+
)
|
|
126
|
+
result = await self.handler.on_create_conversation(
|
|
127
|
+
request.get_claims_identity(), conversation_parameters
|
|
128
|
+
)
|
|
129
|
+
return self.serialize_model(result)
|
|
130
|
+
|
|
131
|
+
async def get_conversations(self, request: HttpRequestProtocol) -> dict:
|
|
132
|
+
"""Handle GET /."""
|
|
133
|
+
# TODO: continuation token? conversation_id?
|
|
134
|
+
result = await self.handler.on_get_conversations(
|
|
135
|
+
request.get_claims_identity(), None
|
|
136
|
+
)
|
|
137
|
+
return self.serialize_model(result)
|
|
138
|
+
|
|
139
|
+
async def get_conversation_members(self, request: HttpRequestProtocol) -> dict:
|
|
140
|
+
"""Handle GET /v3/conversations/{conversation_id}/members."""
|
|
141
|
+
conversation_id = request.get_path_param("conversation_id")
|
|
142
|
+
result = await self.handler.on_get_conversation_members(
|
|
143
|
+
request.get_claims_identity(),
|
|
144
|
+
conversation_id,
|
|
145
|
+
)
|
|
146
|
+
return self.serialize_model(result)
|
|
147
|
+
|
|
148
|
+
async def get_conversation_member(self, request: HttpRequestProtocol) -> dict:
|
|
149
|
+
"""Handle GET /v3/conversations/{conversation_id}/members/{member_id}."""
|
|
150
|
+
conversation_id = request.get_path_param("conversation_id")
|
|
151
|
+
member_id = request.get_path_param("member_id")
|
|
152
|
+
result = await self.handler.on_get_conversation_member(
|
|
153
|
+
request.get_claims_identity(),
|
|
154
|
+
member_id,
|
|
155
|
+
conversation_id,
|
|
156
|
+
)
|
|
157
|
+
return self.serialize_model(result)
|
|
158
|
+
|
|
159
|
+
async def get_conversation_paged_members(
|
|
160
|
+
self, request: HttpRequestProtocol
|
|
161
|
+
) -> dict:
|
|
162
|
+
"""Handle GET /v3/conversations/{conversation_id}/pagedmembers."""
|
|
163
|
+
conversation_id = request.get_path_param("conversation_id")
|
|
164
|
+
# TODO: continuation token? page size?
|
|
165
|
+
result = await self.handler.on_get_conversation_paged_members(
|
|
166
|
+
request.get_claims_identity(),
|
|
167
|
+
conversation_id,
|
|
168
|
+
)
|
|
169
|
+
return self.serialize_model(result)
|
|
170
|
+
|
|
171
|
+
async def delete_conversation_member(self, request: HttpRequestProtocol) -> dict:
|
|
172
|
+
"""Handle DELETE /v3/conversations/{conversation_id}/members/{member_id}."""
|
|
173
|
+
conversation_id = request.get_path_param("conversation_id")
|
|
174
|
+
member_id = request.get_path_param("member_id")
|
|
175
|
+
result = await self.handler.on_delete_conversation_member(
|
|
176
|
+
request.get_claims_identity(),
|
|
177
|
+
conversation_id,
|
|
178
|
+
member_id,
|
|
179
|
+
)
|
|
180
|
+
return self.serialize_model(result)
|
|
181
|
+
|
|
182
|
+
async def send_conversation_history(self, request: HttpRequestProtocol) -> dict:
|
|
183
|
+
"""Handle POST /v3/conversations/{conversation_id}/activities/history."""
|
|
184
|
+
conversation_id = request.get_path_param("conversation_id")
|
|
185
|
+
transcript = await self.deserialize_from_body(request, Transcript)
|
|
186
|
+
result = await self.handler.on_send_conversation_history(
|
|
187
|
+
request.get_claims_identity(),
|
|
188
|
+
conversation_id,
|
|
189
|
+
transcript,
|
|
190
|
+
)
|
|
191
|
+
return self.serialize_model(result)
|
|
192
|
+
|
|
193
|
+
async def upload_attachment(self, request: HttpRequestProtocol) -> dict:
|
|
194
|
+
"""Handle POST /v3/conversations/{conversation_id}/attachments."""
|
|
195
|
+
conversation_id = request.get_path_param("conversation_id")
|
|
196
|
+
attachment_data = await self.deserialize_from_body(request, AttachmentData)
|
|
197
|
+
result = await self.handler.on_upload_attachment(
|
|
198
|
+
request.get_claims_identity(),
|
|
199
|
+
conversation_id,
|
|
200
|
+
attachment_data,
|
|
201
|
+
)
|
|
202
|
+
return self.serialize_model(result)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""Base HTTP adapter with shared processing logic."""
|
|
5
|
+
|
|
6
|
+
from abc import ABC
|
|
7
|
+
from traceback import format_exc
|
|
8
|
+
|
|
9
|
+
from microsoft_agents.activity import Activity, DeliveryModes
|
|
10
|
+
from microsoft_agents.hosting.core.authorization import ClaimsIdentity, Connections
|
|
11
|
+
from microsoft_agents.hosting.core import (
|
|
12
|
+
Agent,
|
|
13
|
+
ChannelServiceAdapter,
|
|
14
|
+
ChannelServiceClientFactoryBase,
|
|
15
|
+
MessageFactory,
|
|
16
|
+
RestChannelServiceClientFactory,
|
|
17
|
+
TurnContext,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ._http_request_protocol import HttpRequestProtocol
|
|
21
|
+
from ._http_response import HttpResponse, HttpResponseFactory
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HttpAdapterBase(ChannelServiceAdapter, ABC):
|
|
25
|
+
"""Base adapter for HTTP-based agent hosting with shared processing logic.
|
|
26
|
+
|
|
27
|
+
This class contains all the common logic for processing HTTP requests
|
|
28
|
+
and can be subclassed by framework-specific adapters (aiohttp, FastAPI, etc).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
connection_manager: Connections = None,
|
|
35
|
+
channel_service_client_factory: ChannelServiceClientFactoryBase = None,
|
|
36
|
+
):
|
|
37
|
+
"""Initialize the HTTP adapter.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
connection_manager: Optional connection manager for OAuth.
|
|
41
|
+
channel_service_client_factory: Factory for creating channel service clients.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
async def on_turn_error(context: TurnContext, error: Exception):
|
|
45
|
+
error_message = f"Exception caught : {error}"
|
|
46
|
+
print(format_exc())
|
|
47
|
+
|
|
48
|
+
await context.send_activity(MessageFactory.text(error_message))
|
|
49
|
+
|
|
50
|
+
# Send a trace activity
|
|
51
|
+
await context.send_trace_activity(
|
|
52
|
+
"OnTurnError Trace",
|
|
53
|
+
error_message,
|
|
54
|
+
"https://www.botframework.com/schemas/error",
|
|
55
|
+
"TurnError",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.on_turn_error = on_turn_error
|
|
59
|
+
|
|
60
|
+
channel_service_client_factory = (
|
|
61
|
+
channel_service_client_factory
|
|
62
|
+
or RestChannelServiceClientFactory(connection_manager)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
super().__init__(channel_service_client_factory)
|
|
66
|
+
|
|
67
|
+
async def process_request(
|
|
68
|
+
self, request: HttpRequestProtocol, agent: Agent
|
|
69
|
+
) -> HttpResponse:
|
|
70
|
+
"""Process an incoming HTTP request.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
request: The HTTP request to process.
|
|
74
|
+
agent: The agent to handle the request.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
HttpResponse with the result.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
TypeError: If request or agent is None.
|
|
81
|
+
"""
|
|
82
|
+
if not request:
|
|
83
|
+
raise TypeError("HttpAdapterBase.process_request: request can't be None")
|
|
84
|
+
if not agent:
|
|
85
|
+
raise TypeError("HttpAdapterBase.process_request: agent can't be None")
|
|
86
|
+
|
|
87
|
+
if request.method != "POST":
|
|
88
|
+
return HttpResponseFactory.method_not_allowed()
|
|
89
|
+
|
|
90
|
+
# Deserialize the incoming Activity
|
|
91
|
+
content_type = request.headers.get("Content-Type", "")
|
|
92
|
+
if "application/json" not in content_type:
|
|
93
|
+
return HttpResponseFactory.unsupported_media_type()
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
body = await request.json()
|
|
97
|
+
except Exception:
|
|
98
|
+
return HttpResponseFactory.bad_request("Invalid JSON")
|
|
99
|
+
|
|
100
|
+
activity: Activity = Activity.model_validate(body)
|
|
101
|
+
|
|
102
|
+
# Get claims identity (default to anonymous if not set by middleware)
|
|
103
|
+
claims_identity: ClaimsIdentity = (
|
|
104
|
+
request.get_claims_identity() or ClaimsIdentity({}, False)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Validate required activity fields
|
|
108
|
+
if (
|
|
109
|
+
not activity.type
|
|
110
|
+
or not activity.conversation
|
|
111
|
+
or not activity.conversation.id
|
|
112
|
+
):
|
|
113
|
+
return HttpResponseFactory.bad_request(
|
|
114
|
+
"Activity must have type and conversation.id"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# Process the inbound activity with the agent
|
|
119
|
+
invoke_response = await self.process_activity(
|
|
120
|
+
claims_identity, activity, agent.on_turn
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Check if we need to return a synchronous response
|
|
124
|
+
if (
|
|
125
|
+
activity.type == "invoke"
|
|
126
|
+
or activity.delivery_mode == DeliveryModes.expect_replies
|
|
127
|
+
):
|
|
128
|
+
# Invoke and ExpectReplies cannot be performed async
|
|
129
|
+
return HttpResponseFactory.json(
|
|
130
|
+
invoke_response.body, invoke_response.status
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return HttpResponseFactory.accepted()
|
|
134
|
+
|
|
135
|
+
except PermissionError:
|
|
136
|
+
return HttpResponseFactory.unauthorized()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""Protocol for abstracting HTTP request objects across frameworks."""
|
|
5
|
+
|
|
6
|
+
from typing import Protocol, Dict, Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HttpRequestProtocol(Protocol):
|
|
10
|
+
"""Protocol for HTTP requests that adapters must implement.
|
|
11
|
+
|
|
12
|
+
This protocol defines the interface that framework-specific request
|
|
13
|
+
adapters must implement to work with the shared HTTP adapter logic.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def method(self) -> str:
|
|
18
|
+
"""HTTP method (GET, POST, etc.)."""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def headers(self) -> Dict[str, str]:
|
|
23
|
+
"""Request headers."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
async def json(self) -> Dict[str, Any]:
|
|
27
|
+
"""Parse request body as JSON."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
def get_claims_identity(self) -> Optional[Any]:
|
|
31
|
+
"""Get claims identity attached by auth middleware."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def get_path_param(self, name: str) -> str:
|
|
35
|
+
"""Get path parameter by name."""
|
|
36
|
+
...
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""HTTP response abstraction."""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Optional, Dict
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class HttpResponse:
|
|
12
|
+
"""Framework-agnostic HTTP response."""
|
|
13
|
+
|
|
14
|
+
status_code: int
|
|
15
|
+
body: Optional[Any] = None
|
|
16
|
+
headers: Optional[Dict[str, str]] = None
|
|
17
|
+
content_type: Optional[str] = "application/json"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HttpResponseFactory:
|
|
21
|
+
"""Factory for creating HTTP responses."""
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def ok(body: Any = None) -> HttpResponse:
|
|
25
|
+
"""Create 200 OK response."""
|
|
26
|
+
return HttpResponse(status_code=200, body=body)
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def accepted() -> HttpResponse:
|
|
30
|
+
"""Create 202 Accepted response."""
|
|
31
|
+
return HttpResponse(status_code=202)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def json(body: Any, status_code: int = 200) -> HttpResponse:
|
|
35
|
+
"""Create JSON response."""
|
|
36
|
+
return HttpResponse(status_code=status_code, body=body)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def bad_request(message: str = "Bad Request") -> HttpResponse:
|
|
40
|
+
"""Create 400 Bad Request response."""
|
|
41
|
+
return HttpResponse(status_code=400, body={"error": message})
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def unauthorized(message: str = "Unauthorized") -> HttpResponse:
|
|
45
|
+
"""Create 401 Unauthorized response."""
|
|
46
|
+
return HttpResponse(status_code=401, body={"error": message})
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def method_not_allowed(message: str = "Method Not Allowed") -> HttpResponse:
|
|
50
|
+
"""Create 405 Method Not Allowed response."""
|
|
51
|
+
return HttpResponse(status_code=405, body={"error": message})
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def unsupported_media_type(message: str = "Unsupported Media Type") -> HttpResponse:
|
|
55
|
+
"""Create 415 Unsupported Media Type response."""
|
|
56
|
+
return HttpResponse(status_code=415, body={"error": message})
|
|
@@ -113,7 +113,7 @@ class RestChannelServiceClientFactory(ChannelServiceClientFactoryBase):
|
|
|
113
113
|
)
|
|
114
114
|
|
|
115
115
|
token = await token_provider.get_access_token(
|
|
116
|
-
audience, scopes or
|
|
116
|
+
audience, scopes or claims_identity.get_token_scope()
|
|
117
117
|
)
|
|
118
118
|
|
|
119
119
|
return TeamsConnectorClient(
|
|
@@ -142,7 +142,7 @@ class RestChannelServiceClientFactory(ChannelServiceClientFactoryBase):
|
|
|
142
142
|
if context.activity.is_agentic_request():
|
|
143
143
|
token = await self._get_agentic_token(context, self._token_service_endpoint)
|
|
144
144
|
else:
|
|
145
|
-
scopes =
|
|
145
|
+
scopes = claims_identity.get_token_scope()
|
|
146
146
|
|
|
147
147
|
token_provider = self._connection_manager.get_token_provider(
|
|
148
148
|
claims_identity, self._token_service_endpoint
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microsoft-agents-hosting-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Core library for Microsoft Agents
|
|
5
5
|
Author: Microsoft Corporation
|
|
6
6
|
License-Expression: MIT
|
|
@@ -15,7 +15,7 @@ Classifier: Operating System :: OS Independent
|
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
|
-
Requires-Dist: microsoft-agents-activity==0.
|
|
18
|
+
Requires-Dist: microsoft-agents-activity==0.7.0
|
|
19
19
|
Requires-Dist: pyjwt>=2.10.1
|
|
20
20
|
Requires-Dist: isodate>=0.6.1
|
|
21
21
|
Requires-Dist: azure-core>=1.30.0
|
|
@@ -41,11 +41,29 @@ This library is part of the **Microsoft 365 Agents SDK for Python** - a comprehe
|
|
|
41
41
|
<th style="width:20%">Date</th>
|
|
42
42
|
<th style="width:60%">Release Notes</th>
|
|
43
43
|
</tr>
|
|
44
|
+
<tr>
|
|
45
|
+
<td>0.6.1</td>
|
|
46
|
+
<td>2025-12-01</td>
|
|
47
|
+
<td>
|
|
48
|
+
<a href="https://github.com/microsoft/Agents-for-python/blob/main/changelog.md#microsoft-365-agents-sdk-for-python---release-notes-v061">
|
|
49
|
+
0.6.1 Release Notes
|
|
50
|
+
</a>
|
|
51
|
+
</td>
|
|
52
|
+
</tr>
|
|
53
|
+
<tr>
|
|
54
|
+
<td>0.6.0</td>
|
|
55
|
+
<td>2025-11-18</td>
|
|
56
|
+
<td>
|
|
57
|
+
<a href="https://github.com/microsoft/Agents-for-python/blob/main/changelog.md#microsoft-365-agents-sdk-for-python---release-notes-v060">
|
|
58
|
+
0.6.0 Release Notes
|
|
59
|
+
</a>
|
|
60
|
+
</td>
|
|
61
|
+
</tr>
|
|
44
62
|
<tr>
|
|
45
63
|
<td>0.5.0</td>
|
|
46
64
|
<td>2025-10-22</td>
|
|
47
65
|
<td>
|
|
48
|
-
<a href="https://github.com/microsoft/Agents-for-python/blob/main/changelog.md">
|
|
66
|
+
<a href="https://github.com/microsoft/Agents-for-python/blob/main/changelog.md#microsoft-365-agents-sdk-for-python---release-notes-v050">
|
|
49
67
|
0.5.0 Release Notes
|
|
50
68
|
</a>
|
|
51
69
|
</td>
|