microsoft-agents-hosting-fastapi 0.6.0.dev4__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/fastapi/__init__.py +23 -0
- microsoft_agents/hosting/fastapi/_start_agent_process.py +26 -0
- microsoft_agents/hosting/fastapi/agent_http_adapter.py +15 -0
- microsoft_agents/hosting/fastapi/app/__init__.py +14 -0
- microsoft_agents/hosting/fastapi/app/streaming/__init__.py +12 -0
- microsoft_agents/hosting/fastapi/app/streaming/citation.py +22 -0
- microsoft_agents/hosting/fastapi/app/streaming/citation_util.py +85 -0
- microsoft_agents/hosting/fastapi/app/streaming/streaming_response.py +392 -0
- microsoft_agents/hosting/fastapi/channel_service_route_table.py +205 -0
- microsoft_agents/hosting/fastapi/cloud_adapter.py +112 -0
- microsoft_agents/hosting/fastapi/jwt_authorization_middleware.py +77 -0
- microsoft_agents_hosting_fastapi-0.6.0.dev4.dist-info/METADATA +50 -0
- microsoft_agents_hosting_fastapi-0.6.0.dev4.dist-info/RECORD +16 -0
- microsoft_agents_hosting_fastapi-0.6.0.dev4.dist-info/WHEEL +5 -0
- microsoft_agents_hosting_fastapi-0.6.0.dev4.dist-info/licenses/LICENSE +21 -0
- microsoft_agents_hosting_fastapi-0.6.0.dev4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from ._start_agent_process import start_agent_process
|
|
2
|
+
from .agent_http_adapter import AgentHttpAdapter
|
|
3
|
+
from .channel_service_route_table import channel_service_route_table
|
|
4
|
+
from .cloud_adapter import CloudAdapter
|
|
5
|
+
from .jwt_authorization_middleware import (
|
|
6
|
+
JwtAuthorizationMiddleware,
|
|
7
|
+
)
|
|
8
|
+
from .app.streaming import (
|
|
9
|
+
Citation,
|
|
10
|
+
CitationUtil,
|
|
11
|
+
StreamingResponse,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"start_agent_process",
|
|
16
|
+
"AgentHttpAdapter",
|
|
17
|
+
"CloudAdapter",
|
|
18
|
+
"JwtAuthorizationMiddleware",
|
|
19
|
+
"channel_service_route_table",
|
|
20
|
+
"Citation",
|
|
21
|
+
"CitationUtil",
|
|
22
|
+
"StreamingResponse",
|
|
23
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from fastapi import Request, Response
|
|
3
|
+
from microsoft_agents.hosting.core.app import AgentApplication
|
|
4
|
+
from .cloud_adapter import CloudAdapter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def start_agent_process(
|
|
8
|
+
request: Request,
|
|
9
|
+
agent_application: AgentApplication,
|
|
10
|
+
adapter: CloudAdapter,
|
|
11
|
+
) -> Optional[Response]:
|
|
12
|
+
"""Starts the agent host with the provided adapter and agent application.
|
|
13
|
+
Args:
|
|
14
|
+
adapter (CloudAdapter): The adapter to use for the agent host.
|
|
15
|
+
agent_application (AgentApplication): The agent application to run.
|
|
16
|
+
"""
|
|
17
|
+
if not adapter:
|
|
18
|
+
raise TypeError("start_agent_process: adapter can't be None")
|
|
19
|
+
if not agent_application:
|
|
20
|
+
raise TypeError("start_agent_process: agent_application can't be None")
|
|
21
|
+
|
|
22
|
+
# Start the agent application with the provided adapter
|
|
23
|
+
return await adapter.process(
|
|
24
|
+
request,
|
|
25
|
+
agent_application,
|
|
26
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from typing import Optional, Protocol
|
|
6
|
+
|
|
7
|
+
from fastapi import Request, Response
|
|
8
|
+
|
|
9
|
+
from microsoft_agents.hosting.core import Agent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentHttpAdapter(Protocol):
|
|
13
|
+
@abstractmethod
|
|
14
|
+
async def process(self, request: Request, agent: Agent) -> Optional[Response]:
|
|
15
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from .streaming import (
|
|
5
|
+
Citation,
|
|
6
|
+
CitationUtil,
|
|
7
|
+
StreamingResponse,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Citation",
|
|
12
|
+
"CitationUtil",
|
|
13
|
+
"StreamingResponse",
|
|
14
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from .citation import Citation
|
|
5
|
+
from .citation_util import CitationUtil
|
|
6
|
+
from .streaming_response import StreamingResponse
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Citation",
|
|
10
|
+
"CitationUtil",
|
|
11
|
+
"StreamingResponse",
|
|
12
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Citation:
|
|
10
|
+
"""Citations returned by the model."""
|
|
11
|
+
|
|
12
|
+
content: str
|
|
13
|
+
"""The content of the citation."""
|
|
14
|
+
|
|
15
|
+
title: Optional[str] = None
|
|
16
|
+
"""The title of the citation."""
|
|
17
|
+
|
|
18
|
+
url: Optional[str] = None
|
|
19
|
+
"""The URL of the citation."""
|
|
20
|
+
|
|
21
|
+
filepath: Optional[str] = None
|
|
22
|
+
"""The filepath of the document."""
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from microsoft_agents.activity import ClientCitation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CitationUtil:
|
|
11
|
+
"""Utility functions for manipulating text and citations."""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def snippet(text: str, max_length: int) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Clips the text to a maximum length in case it exceeds the limit.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
text: The text to clip.
|
|
20
|
+
max_length: The maximum length of the text to return, cutting off the last whole word.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
The modified text
|
|
24
|
+
"""
|
|
25
|
+
if len(text) <= max_length:
|
|
26
|
+
return text
|
|
27
|
+
|
|
28
|
+
snippet = text[:max_length]
|
|
29
|
+
snippet = snippet[: min(len(snippet), snippet.rfind(" "))]
|
|
30
|
+
snippet += "..."
|
|
31
|
+
return snippet
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def format_citations_response(text: str) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Convert citation tags `[doc(s)n]` to `[n]` where n is a number.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
text: The text to format.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The formatted text.
|
|
43
|
+
"""
|
|
44
|
+
return re.sub(r"\[docs?(\d+)\]", r"[\1]", text, flags=re.IGNORECASE)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def get_used_citations(
|
|
48
|
+
text: str, citations: List[ClientCitation]
|
|
49
|
+
) -> Optional[List[ClientCitation]]:
|
|
50
|
+
"""
|
|
51
|
+
Get the citations used in the text. This will remove any citations that are
|
|
52
|
+
included in the citations array from the response but not referenced in the text.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
text: The text to search for citation references, i.e. [1], [2], etc.
|
|
56
|
+
citations: The list of citations to search for.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The list of citations used in the text.
|
|
60
|
+
"""
|
|
61
|
+
regex = re.compile(r"\[(\d+)\]", re.IGNORECASE)
|
|
62
|
+
matches = regex.findall(text)
|
|
63
|
+
|
|
64
|
+
if not matches:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
# Remove duplicates
|
|
68
|
+
filtered_matches = set(matches)
|
|
69
|
+
|
|
70
|
+
# Add citations
|
|
71
|
+
used_citations = []
|
|
72
|
+
for match in filtered_matches:
|
|
73
|
+
citation_ref = f"[{match}]"
|
|
74
|
+
found = next(
|
|
75
|
+
(
|
|
76
|
+
citation
|
|
77
|
+
for citation in citations
|
|
78
|
+
if f"[{citation.position}]" == citation_ref
|
|
79
|
+
),
|
|
80
|
+
None,
|
|
81
|
+
)
|
|
82
|
+
if found:
|
|
83
|
+
used_citations.append(found)
|
|
84
|
+
|
|
85
|
+
return used_citations if used_citations else None
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
from typing import List, Optional, Callable, Literal, TYPE_CHECKING
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from microsoft_agents.activity import (
|
|
10
|
+
Activity,
|
|
11
|
+
Entity,
|
|
12
|
+
Attachment,
|
|
13
|
+
Channels,
|
|
14
|
+
ClientCitation,
|
|
15
|
+
DeliveryModes,
|
|
16
|
+
SensitivityUsageInfo,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from microsoft_agents.hosting.core.turn_context import TurnContext
|
|
21
|
+
|
|
22
|
+
from .citation import Citation
|
|
23
|
+
from .citation_util import CitationUtil
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StreamingResponse:
|
|
29
|
+
"""
|
|
30
|
+
A helper class for streaming responses to the client.
|
|
31
|
+
|
|
32
|
+
This class is used to send a series of updates to the client in a single response.
|
|
33
|
+
The expected sequence of calls is:
|
|
34
|
+
|
|
35
|
+
`queue_informative_update()`, `queue_text_chunk()`, `queue_text_chunk()`, ..., `end_stream()`.
|
|
36
|
+
|
|
37
|
+
Once `end_stream()` is called, the stream is considered ended and no further updates can be sent.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, context: "TurnContext"):
|
|
41
|
+
"""
|
|
42
|
+
Creates a new StreamingResponse instance.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
context: Context for the current turn of conversation with the user.
|
|
46
|
+
"""
|
|
47
|
+
self._context = context
|
|
48
|
+
self._sequence_number = 1
|
|
49
|
+
self._stream_id: Optional[str] = None
|
|
50
|
+
self._message = ""
|
|
51
|
+
self._queue: List[Callable[[], Activity]] = []
|
|
52
|
+
self._queue_sync: Optional[asyncio.Task] = None
|
|
53
|
+
self._chunk_queued = False
|
|
54
|
+
self._ended = False
|
|
55
|
+
self._cancelled = False
|
|
56
|
+
self._is_streaming_channel = False
|
|
57
|
+
self._interval = 0.1
|
|
58
|
+
self._channel_id: Optional[str] = None
|
|
59
|
+
self._attachments: Optional[List[Attachment]] = None
|
|
60
|
+
self._citations: Optional[List[ClientCitation]] = None
|
|
61
|
+
self._sensitivity_label: Optional[SensitivityUsageInfo] = None
|
|
62
|
+
self._enable_feedback_loop = False
|
|
63
|
+
self._feedback_loop_type: Optional[Literal["default", "custom"]] = None
|
|
64
|
+
self._enable_generated_by_ai_label = False
|
|
65
|
+
|
|
66
|
+
# Set defaults based on channel
|
|
67
|
+
self._set_defaults(context)
|
|
68
|
+
|
|
69
|
+
def queue_informative_update(self, text: str) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Queues an informative update to be sent to the client.
|
|
72
|
+
|
|
73
|
+
Informative updates do not contain the message content that the user will
|
|
74
|
+
read but rather an indication that the agent is processing the request.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
text: The informative text to send to the client.
|
|
78
|
+
"""
|
|
79
|
+
if self._cancelled:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if self._ended:
|
|
83
|
+
raise RuntimeError("The stream has already ended.")
|
|
84
|
+
|
|
85
|
+
# Queue a typing activity
|
|
86
|
+
def create_activity():
|
|
87
|
+
activity = Activity(
|
|
88
|
+
type="typing",
|
|
89
|
+
text=text,
|
|
90
|
+
entities=[
|
|
91
|
+
Entity(
|
|
92
|
+
type="streaminfo",
|
|
93
|
+
stream_type="informative",
|
|
94
|
+
stream_sequence=self._sequence_number,
|
|
95
|
+
)
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
self._sequence_number += 1
|
|
99
|
+
return activity
|
|
100
|
+
|
|
101
|
+
self._queue_activity(create_activity)
|
|
102
|
+
|
|
103
|
+
def queue_text_chunk(
|
|
104
|
+
self, text: str, citations: Optional[List[Citation]] = None
|
|
105
|
+
) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Queues a chunk of partial message text to be sent to the client.
|
|
108
|
+
|
|
109
|
+
The text will be sent as quickly as possible to the client.
|
|
110
|
+
Chunks may be combined before delivery to the client.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
text: Partial text of the message to send.
|
|
114
|
+
citations: Citations to be included in the message.
|
|
115
|
+
"""
|
|
116
|
+
if self._cancelled:
|
|
117
|
+
return
|
|
118
|
+
if self._ended:
|
|
119
|
+
raise RuntimeError("The stream has already ended.")
|
|
120
|
+
|
|
121
|
+
# Update full message text
|
|
122
|
+
self._message += text
|
|
123
|
+
|
|
124
|
+
# If there are citations, modify the content so that the sources are numbers instead of [doc1], [doc2], etc.
|
|
125
|
+
self._message = CitationUtil.format_citations_response(self._message)
|
|
126
|
+
|
|
127
|
+
# Queue the next chunk
|
|
128
|
+
self._queue_next_chunk()
|
|
129
|
+
|
|
130
|
+
async def end_stream(self) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Ends the stream by sending the final message to the client.
|
|
133
|
+
"""
|
|
134
|
+
if self._ended:
|
|
135
|
+
raise RuntimeError("The stream has already ended.")
|
|
136
|
+
|
|
137
|
+
# Queue final message
|
|
138
|
+
self._ended = True
|
|
139
|
+
self._queue_next_chunk()
|
|
140
|
+
|
|
141
|
+
# Wait for the queue to drain
|
|
142
|
+
await self.wait_for_queue()
|
|
143
|
+
|
|
144
|
+
def set_attachments(self, attachments: List[Attachment]) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Sets the attachments to attach to the final chunk.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
attachments: List of attachments.
|
|
150
|
+
"""
|
|
151
|
+
self._attachments = attachments
|
|
152
|
+
|
|
153
|
+
def set_sensitivity_label(self, sensitivity_label: SensitivityUsageInfo) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Sets the sensitivity label to attach to the final chunk.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
sensitivity_label: The sensitivity label.
|
|
159
|
+
"""
|
|
160
|
+
self._sensitivity_label = sensitivity_label
|
|
161
|
+
|
|
162
|
+
def set_citations(self, citations: List[Citation]) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Sets the citations for the full message.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
citations: Citations to be included in the message.
|
|
168
|
+
"""
|
|
169
|
+
if citations:
|
|
170
|
+
if not self._citations:
|
|
171
|
+
self._citations = []
|
|
172
|
+
|
|
173
|
+
curr_pos = len(self._citations)
|
|
174
|
+
|
|
175
|
+
for citation in citations:
|
|
176
|
+
client_citation = ClientCitation(
|
|
177
|
+
type="Claim",
|
|
178
|
+
position=curr_pos + 1,
|
|
179
|
+
appearance={
|
|
180
|
+
"type": "DigitalDocument",
|
|
181
|
+
"name": citation.title or f"Document #{curr_pos + 1}",
|
|
182
|
+
"abstract": CitationUtil.snippet(citation.content, 477),
|
|
183
|
+
},
|
|
184
|
+
)
|
|
185
|
+
curr_pos += 1
|
|
186
|
+
self._citations.append(client_citation)
|
|
187
|
+
|
|
188
|
+
def set_feedback_loop(self, enable_feedback_loop: bool) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Sets the Feedback Loop in Teams that allows a user to
|
|
191
|
+
give thumbs up or down to a response.
|
|
192
|
+
Default is False.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
enable_feedback_loop: If true, the feedback loop is enabled.
|
|
196
|
+
"""
|
|
197
|
+
self._enable_feedback_loop = enable_feedback_loop
|
|
198
|
+
|
|
199
|
+
def set_feedback_loop_type(
|
|
200
|
+
self, feedback_loop_type: Literal["default", "custom"]
|
|
201
|
+
) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Sets the type of UI to use for the feedback loop.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
feedback_loop_type: The type of the feedback loop.
|
|
207
|
+
"""
|
|
208
|
+
self._feedback_loop_type = feedback_loop_type
|
|
209
|
+
|
|
210
|
+
def set_generated_by_ai_label(self, enable_generated_by_ai_label: bool) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Sets the Generated by AI label in Teams.
|
|
213
|
+
Default is False.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
enable_generated_by_ai_label: If true, the label is added.
|
|
217
|
+
"""
|
|
218
|
+
self._enable_generated_by_ai_label = enable_generated_by_ai_label
|
|
219
|
+
|
|
220
|
+
def get_message(self) -> str:
|
|
221
|
+
"""
|
|
222
|
+
Returns the most recently streamed message.
|
|
223
|
+
"""
|
|
224
|
+
return self._message
|
|
225
|
+
|
|
226
|
+
async def wait_for_queue(self) -> None:
|
|
227
|
+
"""
|
|
228
|
+
Waits for the outgoing activity queue to be empty.
|
|
229
|
+
"""
|
|
230
|
+
if self._queue_sync:
|
|
231
|
+
await self._queue_sync
|
|
232
|
+
|
|
233
|
+
def _set_defaults(self, context: "TurnContext"):
|
|
234
|
+
if context.activity.channel_id == Channels.ms_teams:
|
|
235
|
+
self._is_streaming_channel = True
|
|
236
|
+
self._interval = 1.0
|
|
237
|
+
elif context.activity.channel_id == Channels.direct_line:
|
|
238
|
+
self._is_streaming_channel = True
|
|
239
|
+
self._interval = 0.5
|
|
240
|
+
elif context.activity.delivery_mode == DeliveryModes.stream:
|
|
241
|
+
self._is_streaming_channel = True
|
|
242
|
+
self._interval = 0.1
|
|
243
|
+
|
|
244
|
+
self._channel_id = context.activity.channel_id
|
|
245
|
+
|
|
246
|
+
def _queue_next_chunk(self) -> None:
|
|
247
|
+
"""
|
|
248
|
+
Queues the next chunk of text to be sent to the client.
|
|
249
|
+
"""
|
|
250
|
+
# Are we already waiting to send a chunk?
|
|
251
|
+
if self._chunk_queued:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
# Queue a chunk of text to be sent
|
|
255
|
+
self._chunk_queued = True
|
|
256
|
+
|
|
257
|
+
def create_activity():
|
|
258
|
+
self._chunk_queued = False
|
|
259
|
+
if self._ended:
|
|
260
|
+
# Send final message
|
|
261
|
+
activity = Activity(
|
|
262
|
+
type="message",
|
|
263
|
+
text=self._message or "end stream response",
|
|
264
|
+
attachments=self._attachments or [],
|
|
265
|
+
entities=[
|
|
266
|
+
Entity(
|
|
267
|
+
type="streaminfo",
|
|
268
|
+
stream_type="final",
|
|
269
|
+
stream_sequence=self._sequence_number,
|
|
270
|
+
)
|
|
271
|
+
],
|
|
272
|
+
)
|
|
273
|
+
elif self._is_streaming_channel:
|
|
274
|
+
# Send typing activity
|
|
275
|
+
activity = Activity(
|
|
276
|
+
type="typing",
|
|
277
|
+
text=self._message,
|
|
278
|
+
entities=[
|
|
279
|
+
Entity(
|
|
280
|
+
type="streaminfo",
|
|
281
|
+
stream_type="streaming",
|
|
282
|
+
stream_sequence=self._sequence_number,
|
|
283
|
+
)
|
|
284
|
+
],
|
|
285
|
+
)
|
|
286
|
+
else:
|
|
287
|
+
return
|
|
288
|
+
self._sequence_number += 1
|
|
289
|
+
return activity
|
|
290
|
+
|
|
291
|
+
self._queue_activity(create_activity)
|
|
292
|
+
|
|
293
|
+
def _queue_activity(self, factory: Callable[[], Activity]) -> None:
|
|
294
|
+
"""
|
|
295
|
+
Queues an activity to be sent to the client.
|
|
296
|
+
"""
|
|
297
|
+
self._queue.append(factory)
|
|
298
|
+
|
|
299
|
+
# If there's no sync in progress, start one
|
|
300
|
+
if not self._queue_sync:
|
|
301
|
+
self._queue_sync = asyncio.create_task(self._drain_queue())
|
|
302
|
+
|
|
303
|
+
async def _drain_queue(self) -> None:
|
|
304
|
+
"""
|
|
305
|
+
Sends any queued activities to the client until the queue is empty.
|
|
306
|
+
"""
|
|
307
|
+
try:
|
|
308
|
+
logger.debug(f"Draining queue with {len(self._queue)} activities.")
|
|
309
|
+
while self._queue:
|
|
310
|
+
factory = self._queue.pop(0)
|
|
311
|
+
activity = factory()
|
|
312
|
+
if activity:
|
|
313
|
+
await self._send_activity(activity)
|
|
314
|
+
except Exception as err:
|
|
315
|
+
if (
|
|
316
|
+
"403" in str(err)
|
|
317
|
+
and self._context.activity.channel_id == Channels.ms_teams
|
|
318
|
+
):
|
|
319
|
+
logger.warning("Teams channel stopped the stream.")
|
|
320
|
+
self._cancelled = True
|
|
321
|
+
else:
|
|
322
|
+
logger.error(
|
|
323
|
+
f"Error occurred when sending activity while streaming: {type(err).__name__}"
|
|
324
|
+
)
|
|
325
|
+
raise
|
|
326
|
+
finally:
|
|
327
|
+
self._queue_sync = None
|
|
328
|
+
|
|
329
|
+
async def _send_activity(self, activity: Activity) -> None:
|
|
330
|
+
"""
|
|
331
|
+
Sends an activity to the client and saves the stream ID returned.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
activity: The activity to send.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
streaminfo_entity = None
|
|
338
|
+
|
|
339
|
+
if not activity.entities:
|
|
340
|
+
streaminfo_entity = Entity(type="streaminfo")
|
|
341
|
+
activity.entities = [streaminfo_entity]
|
|
342
|
+
else:
|
|
343
|
+
for entity in activity.entities:
|
|
344
|
+
if hasattr(entity, "type") and entity.type == "streaminfo":
|
|
345
|
+
streaminfo_entity = entity
|
|
346
|
+
break
|
|
347
|
+
|
|
348
|
+
if not streaminfo_entity:
|
|
349
|
+
# If no streaminfo entity exists, create one
|
|
350
|
+
streaminfo_entity = Entity(type="streaminfo")
|
|
351
|
+
activity.entities.append(streaminfo_entity)
|
|
352
|
+
|
|
353
|
+
# Set activity ID to the assigned stream ID
|
|
354
|
+
if self._stream_id:
|
|
355
|
+
activity.id = self._stream_id
|
|
356
|
+
streaminfo_entity.stream_id = self._stream_id
|
|
357
|
+
|
|
358
|
+
if self._citations and not self._ended:
|
|
359
|
+
# Filter out the citations unused in content.
|
|
360
|
+
curr_citations = CitationUtil.get_used_citations(
|
|
361
|
+
self._message, self._citations
|
|
362
|
+
)
|
|
363
|
+
if curr_citations:
|
|
364
|
+
activity.entities.append(
|
|
365
|
+
Entity(
|
|
366
|
+
type="https://schema.org/Message",
|
|
367
|
+
schema_type="Message",
|
|
368
|
+
context="https://schema.org",
|
|
369
|
+
id="",
|
|
370
|
+
citation=curr_citations,
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Add in Powered by AI feature flags
|
|
375
|
+
if self._ended:
|
|
376
|
+
if self._enable_feedback_loop and self._feedback_loop_type:
|
|
377
|
+
# Add feedback loop to streaminfo entity
|
|
378
|
+
streaminfo_entity.feedback_loop = {"type": self._feedback_loop_type}
|
|
379
|
+
else:
|
|
380
|
+
# Add feedback loop enabled to streaminfo entity
|
|
381
|
+
streaminfo_entity.feedback_loop_enabled = self._enable_feedback_loop
|
|
382
|
+
# Add in Generated by AI
|
|
383
|
+
if self._enable_generated_by_ai_label:
|
|
384
|
+
activity.add_ai_metadata(self._citations, self._sensitivity_label)
|
|
385
|
+
|
|
386
|
+
# Send activity
|
|
387
|
+
response = await self._context.send_activity(activity)
|
|
388
|
+
await asyncio.sleep(self._interval)
|
|
389
|
+
|
|
390
|
+
# Save assigned stream ID
|
|
391
|
+
if not self._stream_id and response:
|
|
392
|
+
self._stream_id = response.id
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
import json
|
|
4
|
+
from typing import List, Union, Type
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Request, Response, HTTPException, Depends
|
|
7
|
+
from fastapi.responses import JSONResponse
|
|
8
|
+
|
|
9
|
+
from microsoft_agents.activity import (
|
|
10
|
+
AgentsModel,
|
|
11
|
+
Activity,
|
|
12
|
+
AttachmentData,
|
|
13
|
+
ConversationParameters,
|
|
14
|
+
Transcript,
|
|
15
|
+
)
|
|
16
|
+
from microsoft_agents.hosting.core import ChannelApiHandlerProtocol
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def deserialize_from_body(
|
|
20
|
+
request: Request, target_model: Type[AgentsModel]
|
|
21
|
+
) -> AgentsModel:
|
|
22
|
+
content_type = request.headers.get("Content-Type", "")
|
|
23
|
+
if "application/json" in content_type:
|
|
24
|
+
body = await request.json()
|
|
25
|
+
else:
|
|
26
|
+
raise HTTPException(status_code=415, detail="Unsupported Media Type")
|
|
27
|
+
|
|
28
|
+
return target_model.model_validate(body)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_serialized_response(
|
|
32
|
+
model_or_list: Union[AgentsModel, List[AgentsModel]],
|
|
33
|
+
) -> JSONResponse:
|
|
34
|
+
if isinstance(model_or_list, AgentsModel):
|
|
35
|
+
json_obj = model_or_list.model_dump(
|
|
36
|
+
mode="json", exclude_unset=True, by_alias=True
|
|
37
|
+
)
|
|
38
|
+
else:
|
|
39
|
+
json_obj = [
|
|
40
|
+
model.model_dump(mode="json", exclude_unset=True, by_alias=True)
|
|
41
|
+
for model in model_or_list
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
return JSONResponse(content=json_obj)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def channel_service_route_table(
|
|
48
|
+
handler: ChannelApiHandlerProtocol, base_url: str = ""
|
|
49
|
+
) -> APIRouter:
|
|
50
|
+
router = APIRouter()
|
|
51
|
+
|
|
52
|
+
@router.post(base_url + "/v3/conversations/{conversation_id}/activities")
|
|
53
|
+
async def send_to_conversation(conversation_id: str, request: Request):
|
|
54
|
+
activity = await deserialize_from_body(request, Activity)
|
|
55
|
+
result = await handler.on_send_to_conversation(
|
|
56
|
+
getattr(request.state, "claims_identity", None),
|
|
57
|
+
conversation_id,
|
|
58
|
+
activity,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return get_serialized_response(result)
|
|
62
|
+
|
|
63
|
+
@router.post(
|
|
64
|
+
base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}"
|
|
65
|
+
)
|
|
66
|
+
async def reply_to_activity(
|
|
67
|
+
conversation_id: str, activity_id: str, request: Request
|
|
68
|
+
):
|
|
69
|
+
activity = await deserialize_from_body(request, Activity)
|
|
70
|
+
result = await handler.on_reply_to_activity(
|
|
71
|
+
getattr(request.state, "claims_identity", None),
|
|
72
|
+
conversation_id,
|
|
73
|
+
activity_id,
|
|
74
|
+
activity,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return get_serialized_response(result)
|
|
78
|
+
|
|
79
|
+
@router.put(
|
|
80
|
+
base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}"
|
|
81
|
+
)
|
|
82
|
+
async def update_activity(conversation_id: str, activity_id: str, request: Request):
|
|
83
|
+
activity = await deserialize_from_body(request, Activity)
|
|
84
|
+
result = await handler.on_update_activity(
|
|
85
|
+
getattr(request.state, "claims_identity", None),
|
|
86
|
+
conversation_id,
|
|
87
|
+
activity_id,
|
|
88
|
+
activity,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return get_serialized_response(result)
|
|
92
|
+
|
|
93
|
+
@router.delete(
|
|
94
|
+
base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}"
|
|
95
|
+
)
|
|
96
|
+
async def delete_activity(conversation_id: str, activity_id: str, request: Request):
|
|
97
|
+
await handler.on_delete_activity(
|
|
98
|
+
getattr(request.state, "claims_identity", None),
|
|
99
|
+
conversation_id,
|
|
100
|
+
activity_id,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return Response(status_code=200)
|
|
104
|
+
|
|
105
|
+
@router.get(
|
|
106
|
+
base_url
|
|
107
|
+
+ "/v3/conversations/{conversation_id}/activities/{activity_id}/members"
|
|
108
|
+
)
|
|
109
|
+
async def get_activity_members(
|
|
110
|
+
conversation_id: str, activity_id: str, request: Request
|
|
111
|
+
):
|
|
112
|
+
result = await handler.on_get_activity_members(
|
|
113
|
+
getattr(request.state, "claims_identity", None),
|
|
114
|
+
conversation_id,
|
|
115
|
+
activity_id,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return get_serialized_response(result)
|
|
119
|
+
|
|
120
|
+
@router.post(base_url + "/")
|
|
121
|
+
async def create_conversation(request: Request):
|
|
122
|
+
conversation_parameters = await deserialize_from_body(
|
|
123
|
+
request, ConversationParameters
|
|
124
|
+
)
|
|
125
|
+
result = await handler.on_create_conversation(
|
|
126
|
+
getattr(request.state, "claims_identity", None), conversation_parameters
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return get_serialized_response(result)
|
|
130
|
+
|
|
131
|
+
@router.get(base_url + "/")
|
|
132
|
+
async def get_conversation(request: Request):
|
|
133
|
+
# TODO: continuation token? conversation_id?
|
|
134
|
+
result = await handler.on_get_conversations(
|
|
135
|
+
getattr(request.state, "claims_identity", None), None
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return get_serialized_response(result)
|
|
139
|
+
|
|
140
|
+
@router.get(base_url + "/v3/conversations/{conversation_id}/members")
|
|
141
|
+
async def get_conversation_members(conversation_id: str, request: Request):
|
|
142
|
+
result = await handler.on_get_conversation_members(
|
|
143
|
+
getattr(request.state, "claims_identity", None),
|
|
144
|
+
conversation_id,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return get_serialized_response(result)
|
|
148
|
+
|
|
149
|
+
@router.get(base_url + "/v3/conversations/{conversation_id}/members/{member_id}")
|
|
150
|
+
async def get_conversation_member(
|
|
151
|
+
conversation_id: str, member_id: str, request: Request
|
|
152
|
+
):
|
|
153
|
+
result = await handler.on_get_conversation_member(
|
|
154
|
+
getattr(request.state, "claims_identity", None),
|
|
155
|
+
member_id,
|
|
156
|
+
conversation_id,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return get_serialized_response(result)
|
|
160
|
+
|
|
161
|
+
@router.get(base_url + "/v3/conversations/{conversation_id}/pagedmembers")
|
|
162
|
+
async def get_conversation_paged_members(conversation_id: str, request: Request):
|
|
163
|
+
# TODO: continuation token? page size?
|
|
164
|
+
result = await handler.on_get_conversation_paged_members(
|
|
165
|
+
getattr(request.state, "claims_identity", None),
|
|
166
|
+
conversation_id,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return get_serialized_response(result)
|
|
170
|
+
|
|
171
|
+
@router.delete(base_url + "/v3/conversations/{conversation_id}/members/{member_id}")
|
|
172
|
+
async def delete_conversation_member(
|
|
173
|
+
conversation_id: str, member_id: str, request: Request
|
|
174
|
+
):
|
|
175
|
+
result = await handler.on_delete_conversation_member(
|
|
176
|
+
getattr(request.state, "claims_identity", None),
|
|
177
|
+
conversation_id,
|
|
178
|
+
member_id,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return get_serialized_response(result)
|
|
182
|
+
|
|
183
|
+
@router.post(base_url + "/v3/conversations/{conversation_id}/activities/history")
|
|
184
|
+
async def send_conversation_history(conversation_id: str, request: Request):
|
|
185
|
+
transcript = await deserialize_from_body(request, Transcript)
|
|
186
|
+
result = await handler.on_send_conversation_history(
|
|
187
|
+
getattr(request.state, "claims_identity", None),
|
|
188
|
+
conversation_id,
|
|
189
|
+
transcript,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return get_serialized_response(result)
|
|
193
|
+
|
|
194
|
+
@router.post(base_url + "/v3/conversations/{conversation_id}/attachments")
|
|
195
|
+
async def upload_attachment(conversation_id: str, request: Request):
|
|
196
|
+
attachment_data = await deserialize_from_body(request, AttachmentData)
|
|
197
|
+
result = await handler.on_upload_attachment(
|
|
198
|
+
getattr(request.state, "claims_identity", None),
|
|
199
|
+
conversation_id,
|
|
200
|
+
attachment_data,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return get_serialized_response(result)
|
|
204
|
+
|
|
205
|
+
return router
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
from traceback import format_exc
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from fastapi import Request, Response, HTTPException
|
|
7
|
+
from fastapi.responses import JSONResponse
|
|
8
|
+
from microsoft_agents.hosting.core.authorization import (
|
|
9
|
+
ClaimsIdentity,
|
|
10
|
+
Connections,
|
|
11
|
+
)
|
|
12
|
+
from microsoft_agents.activity import (
|
|
13
|
+
Activity,
|
|
14
|
+
DeliveryModes,
|
|
15
|
+
)
|
|
16
|
+
from microsoft_agents.hosting.core import (
|
|
17
|
+
Agent,
|
|
18
|
+
ChannelServiceAdapter,
|
|
19
|
+
ChannelServiceClientFactoryBase,
|
|
20
|
+
MessageFactory,
|
|
21
|
+
RestChannelServiceClientFactory,
|
|
22
|
+
TurnContext,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from .agent_http_adapter import AgentHttpAdapter
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CloudAdapter(ChannelServiceAdapter, AgentHttpAdapter):
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
connection_manager: Connections = None,
|
|
33
|
+
channel_service_client_factory: ChannelServiceClientFactoryBase = None,
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Initializes a new instance of the CloudAdapter class.
|
|
37
|
+
|
|
38
|
+
:param channel_service_client_factory: The factory to use to create the channel service client.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
async def on_turn_error(context: TurnContext, error: Exception):
|
|
42
|
+
error_message = f"Exception caught : {error}"
|
|
43
|
+
print(format_exc())
|
|
44
|
+
|
|
45
|
+
await context.send_activity(MessageFactory.text(error_message))
|
|
46
|
+
|
|
47
|
+
# Send a trace activity
|
|
48
|
+
await context.send_trace_activity(
|
|
49
|
+
"OnTurnError Trace",
|
|
50
|
+
error_message,
|
|
51
|
+
"https://www.botframework.com/schemas/error",
|
|
52
|
+
"TurnError",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self.on_turn_error = on_turn_error
|
|
56
|
+
|
|
57
|
+
channel_service_client_factory = (
|
|
58
|
+
channel_service_client_factory
|
|
59
|
+
or RestChannelServiceClientFactory(connection_manager)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
super().__init__(channel_service_client_factory)
|
|
63
|
+
|
|
64
|
+
async def process(self, request: Request, agent: Agent) -> Optional[Response]:
|
|
65
|
+
if not request:
|
|
66
|
+
raise TypeError("CloudAdapter.process: request can't be None")
|
|
67
|
+
if not agent:
|
|
68
|
+
raise TypeError("CloudAdapter.process: agent can't be None")
|
|
69
|
+
|
|
70
|
+
if request.method == "POST":
|
|
71
|
+
# Deserialize the incoming Activity
|
|
72
|
+
content_type = request.headers.get("Content-Type", "")
|
|
73
|
+
if "application/json" in content_type:
|
|
74
|
+
body = await request.json()
|
|
75
|
+
else:
|
|
76
|
+
raise HTTPException(status_code=415, detail="Unsupported Media Type")
|
|
77
|
+
|
|
78
|
+
activity: Activity = Activity.model_validate(body)
|
|
79
|
+
|
|
80
|
+
# default to anonymous identity with no claims
|
|
81
|
+
claims_identity: ClaimsIdentity = getattr(
|
|
82
|
+
request.state, "claims_identity", ClaimsIdentity({}, False)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# A POST request must contain an Activity
|
|
86
|
+
if (
|
|
87
|
+
not activity.type
|
|
88
|
+
or not activity.conversation
|
|
89
|
+
or not activity.conversation.id
|
|
90
|
+
):
|
|
91
|
+
raise HTTPException(status_code=400, detail="Bad Request")
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
# Process the inbound activity with the agent
|
|
95
|
+
invoke_response = await self.process_activity(
|
|
96
|
+
claims_identity, activity, agent.on_turn
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
activity.type == "invoke"
|
|
101
|
+
or activity.delivery_mode == DeliveryModes.expect_replies
|
|
102
|
+
):
|
|
103
|
+
# Invoke and ExpectReplies cannot be performed async, the response must be written before the calling thread is released.
|
|
104
|
+
return JSONResponse(
|
|
105
|
+
content=invoke_response.body, status_code=invoke_response.status
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return Response(status_code=202)
|
|
109
|
+
except PermissionError:
|
|
110
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
111
|
+
else:
|
|
112
|
+
raise HTTPException(status_code=405, detail="Method Not Allowed")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from fastapi import Request
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
import logging
|
|
4
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
5
|
+
from microsoft_agents.hosting.core import (
|
|
6
|
+
AgentAuthConfiguration,
|
|
7
|
+
JwtTokenValidator,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JwtAuthorizationMiddleware:
|
|
15
|
+
"""Starlette-compatible ASGI middleware for JWT authorization.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
from fastapi import FastAPI
|
|
19
|
+
|
|
20
|
+
app = FastAPI()
|
|
21
|
+
app.add_middleware(JwtAuthorizationMiddleware)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, app: ASGIApp):
|
|
25
|
+
self.app = app
|
|
26
|
+
|
|
27
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
|
28
|
+
if scope["type"] == "lifespan":
|
|
29
|
+
await self.app(scope, receive, send)
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
app = scope.get("app")
|
|
33
|
+
state = getattr(app, "state", None) if app else None
|
|
34
|
+
auth_config: AgentAuthConfiguration = getattr(
|
|
35
|
+
state, "agent_configuration", None
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
request = Request(scope, receive=receive)
|
|
39
|
+
token_validator = JwtTokenValidator(auth_config)
|
|
40
|
+
auth_header = request.headers.get("Authorization")
|
|
41
|
+
|
|
42
|
+
if auth_header:
|
|
43
|
+
parts = auth_header.split(" ")
|
|
44
|
+
if len(parts) == 2 and parts[0].lower() == "bearer":
|
|
45
|
+
token = parts[1]
|
|
46
|
+
try:
|
|
47
|
+
claims = await token_validator.validate_token(token)
|
|
48
|
+
request.state.claims_identity = claims
|
|
49
|
+
except ValueError as e:
|
|
50
|
+
logger.warning("JWT validation error: %s", e)
|
|
51
|
+
response = JSONResponse(
|
|
52
|
+
{"error": "Invalid token or authentication failed."},
|
|
53
|
+
status_code=401,
|
|
54
|
+
)
|
|
55
|
+
await response(scope, receive, send)
|
|
56
|
+
return
|
|
57
|
+
else:
|
|
58
|
+
response = JSONResponse(
|
|
59
|
+
{"error": "Invalid authorization header format"},
|
|
60
|
+
status_code=401,
|
|
61
|
+
)
|
|
62
|
+
await response(scope, receive, send)
|
|
63
|
+
return
|
|
64
|
+
else:
|
|
65
|
+
if not auth_config or not auth_config.CLIENT_ID:
|
|
66
|
+
request.state.claims_identity = (
|
|
67
|
+
await token_validator.get_anonymous_claims()
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
response = JSONResponse(
|
|
71
|
+
{"error": "Authorization header not found"},
|
|
72
|
+
status_code=401,
|
|
73
|
+
)
|
|
74
|
+
await response(scope, receive, send)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
await self.app(scope, receive, send)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microsoft-agents-hosting-fastapi
|
|
3
|
+
Version: 0.6.0.dev4
|
|
4
|
+
Summary: Integration library for Microsoft Agents with FastAPI
|
|
5
|
+
Author: Microsoft Corporation
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/microsoft/Agents
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: microsoft-agents-hosting-core==0.6.0.dev4
|
|
14
|
+
Requires-Dist: fastapi>=0.104.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
Dynamic: requires-dist
|
|
17
|
+
|
|
18
|
+
# Microsoft Agents Hosting FastAPI
|
|
19
|
+
|
|
20
|
+
This library provides FastAPI integration for Microsoft Agents, enabling you to build conversational agents using the FastAPI web framework.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- FastAPI integration for Microsoft Agents
|
|
25
|
+
- JWT authorization middleware
|
|
26
|
+
- Channel service API endpoints
|
|
27
|
+
- Streaming response support
|
|
28
|
+
- Cloud adapter for processing agent activities
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install microsoft-agents-hosting-fastapi
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from fastapi import FastAPI, Request
|
|
40
|
+
from microsoft_agents.hosting.fastapi import start_agent_process, CloudAdapter
|
|
41
|
+
from microsoft_agents.hosting.core.app import AgentApplication
|
|
42
|
+
|
|
43
|
+
app = FastAPI()
|
|
44
|
+
adapter = CloudAdapter()
|
|
45
|
+
agent_app = AgentApplication()
|
|
46
|
+
|
|
47
|
+
@app.post("/api/messages")
|
|
48
|
+
async def messages(request: Request):
|
|
49
|
+
return await start_agent_process(request, agent_app, adapter)
|
|
50
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
microsoft_agents/hosting/fastapi/__init__.py,sha256=9IuCCWHY4FEPGAzv9uEyob0irIbMqAomjjiYKs95NPA,592
|
|
2
|
+
microsoft_agents/hosting/fastapi/_start_agent_process.py,sha256=g2CktwTO2Yxdxkg_Zzq73MDxLPCj1rb98cqPU7AD7Iw,905
|
|
3
|
+
microsoft_agents/hosting/fastapi/agent_http_adapter.py,sha256=40fi8bPXzXMGMAdeK1NDT34RSbKP--jLzBvRJ159-BQ,427
|
|
4
|
+
microsoft_agents/hosting/fastapi/channel_service_route_table.py,sha256=NHt5958_SpcT1O6tTSPuTtZX0R196IAcDUr5gWuTYZ4,7113
|
|
5
|
+
microsoft_agents/hosting/fastapi/cloud_adapter.py,sha256=87JtnT21rIsdF1wmJ8panUeWAJBshEvJcve05zoiZiE,3988
|
|
6
|
+
microsoft_agents/hosting/fastapi/jwt_authorization_middleware.py,sha256=YMLqoXrUcC5klaLiKT4uP_dF7Tz1KbsVJjL2PvWgkLQ,2600
|
|
7
|
+
microsoft_agents/hosting/fastapi/app/__init__.py,sha256=TioskqZet16twXOsI3X2snyLzmuyeKNtN2dySD1Xw7s,253
|
|
8
|
+
microsoft_agents/hosting/fastapi/app/streaming/__init__.py,sha256=G_VGmQ0m6TkHZsHjRV5HitaCOt2EBEjENIoBYabJMqM,292
|
|
9
|
+
microsoft_agents/hosting/fastapi/app/streaming/citation.py,sha256=ZGaMUOWxxoMplwRrkFsjnK7Z12V6rT5odE7qZCu-mP8,498
|
|
10
|
+
microsoft_agents/hosting/fastapi/app/streaming/citation_util.py,sha256=c95c3Y3genmFc0vSXppPaD1-ShFohAV1UABZnyJS_BQ,2478
|
|
11
|
+
microsoft_agents/hosting/fastapi/app/streaming/streaming_response.py,sha256=kcSjKr_VAuZVaiKUTfTJHU13Bx7GpkjtCdP9WOvLvRQ,13265
|
|
12
|
+
microsoft_agents_hosting_fastapi-0.6.0.dev4.dist-info/licenses/LICENSE,sha256=oDrK6gJRdwYynx5l4UtyDa2nX_D1WWkvionBYrCebek,1073
|
|
13
|
+
microsoft_agents_hosting_fastapi-0.6.0.dev4.dist-info/METADATA,sha256=-KIO1SKs6D98D80g54hgGTRAVa7nFFm_0XS2uI0c7ks,1431
|
|
14
|
+
microsoft_agents_hosting_fastapi-0.6.0.dev4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
15
|
+
microsoft_agents_hosting_fastapi-0.6.0.dev4.dist-info/top_level.txt,sha256=lWKcT4v6fTA_NgsuHdNvuMjSrkiBMXohn64ApY7Xi8A,17
|
|
16
|
+
microsoft_agents_hosting_fastapi-0.6.0.dev4.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
microsoft_agents
|