agent-framework-declarative 1.0.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.
- agent_framework_declarative/__init__.py +71 -0
- agent_framework_declarative/_loader.py +868 -0
- agent_framework_declarative/_models.py +1154 -0
- agent_framework_declarative/_workflows/__init__.py +167 -0
- agent_framework_declarative/_workflows/_declarative_base.py +1226 -0
- agent_framework_declarative/_workflows/_declarative_builder.py +1057 -0
- agent_framework_declarative/_workflows/_errors.py +38 -0
- agent_framework_declarative/_workflows/_executors_agents.py +1025 -0
- agent_framework_declarative/_workflows/_executors_basic.py +574 -0
- agent_framework_declarative/_workflows/_executors_control_flow.py +461 -0
- agent_framework_declarative/_workflows/_executors_external_input.py +243 -0
- agent_framework_declarative/_workflows/_executors_http.py +417 -0
- agent_framework_declarative/_workflows/_executors_mcp.py +549 -0
- agent_framework_declarative/_workflows/_executors_tools.py +660 -0
- agent_framework_declarative/_workflows/_factory.py +808 -0
- agent_framework_declarative/_workflows/_http_handler.py +237 -0
- agent_framework_declarative/_workflows/_mcp_handler.py +581 -0
- agent_framework_declarative/_workflows/_powerfx_functions.py +498 -0
- agent_framework_declarative/_workflows/_state.py +650 -0
- agent_framework_declarative-1.0.0.dist-info/METADATA +49 -0
- agent_framework_declarative-1.0.0.dist-info/RECORD +23 -0
- agent_framework_declarative-1.0.0.dist-info/WHEEL +4 -0
- agent_framework_declarative-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""HTTP request handler abstraction for declarative workflows.
|
|
4
|
+
|
|
5
|
+
Mirrors the .NET ``IHttpRequestHandler`` / ``DefaultHttpRequestHandler`` pair from
|
|
6
|
+
``Microsoft.Agents.AI.Workflows.Declarative``. Provides:
|
|
7
|
+
|
|
8
|
+
- :class:`HttpRequestInfo` — request input data passed from the executor.
|
|
9
|
+
- :class:`HttpRequestResult` — response data returned to the executor.
|
|
10
|
+
- :class:`HttpRequestHandler` — :class:`typing.Protocol` callers implement to plug
|
|
11
|
+
in custom transports (e.g. with allowlisting, mTLS, retries, etc.).
|
|
12
|
+
- :class:`DefaultHttpRequestHandler` — production-grade default backed by
|
|
13
|
+
``httpx.AsyncClient``.
|
|
14
|
+
|
|
15
|
+
Security note: :class:`DefaultHttpRequestHandler` performs **no** URL filtering
|
|
16
|
+
or SSRF protection. Production deployments should supply a custom handler that
|
|
17
|
+
enforces an allowlist or DNS-rebinding-resistant policy. This split mirrors the
|
|
18
|
+
.NET design.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any, Protocol, runtime_checkable
|
|
27
|
+
|
|
28
|
+
import httpx
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"DefaultHttpRequestHandler",
|
|
32
|
+
"HttpRequestHandler",
|
|
33
|
+
"HttpRequestInfo",
|
|
34
|
+
"HttpRequestResult",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class HttpRequestInfo:
|
|
40
|
+
"""Description of an HTTP request to be dispatched by a :class:`HttpRequestHandler`.
|
|
41
|
+
|
|
42
|
+
Mirrors the .NET ``HttpRequestInfo`` record. Field semantics:
|
|
43
|
+
|
|
44
|
+
- ``method``: HTTP method (``GET``, ``POST``, etc.). Already upper-cased by the executor.
|
|
45
|
+
- ``url``: Absolute URL. Already evaluated from the YAML expression.
|
|
46
|
+
- ``headers``: Single-value header map (case-insensitive keys per HTTP semantics
|
|
47
|
+
but stored as authored). Empty values are skipped by the executor.
|
|
48
|
+
- ``query_parameters``: String key/value pairs appended to the URL.
|
|
49
|
+
- ``body``: Request body bytes/text, or ``None`` for no body.
|
|
50
|
+
- ``body_content_type``: Content type to send (e.g. ``application/json``).
|
|
51
|
+
Ignored when ``body`` is ``None``.
|
|
52
|
+
- ``timeout_ms``: Per-request timeout in milliseconds. ``None`` => use the
|
|
53
|
+
handler's default.
|
|
54
|
+
- ``connection_name``: Optional Foundry connection name for handlers that
|
|
55
|
+
resolve auth/credentials by connection.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
method: str
|
|
59
|
+
url: str
|
|
60
|
+
headers: dict[str, str] = field(default_factory=dict) # type: ignore[reportUnknownVariableType]
|
|
61
|
+
query_parameters: dict[str, str] = field(default_factory=dict) # type: ignore[reportUnknownVariableType]
|
|
62
|
+
body: str | None = None
|
|
63
|
+
body_content_type: str | None = None
|
|
64
|
+
timeout_ms: int | None = None
|
|
65
|
+
connection_name: str | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class HttpRequestResult:
|
|
70
|
+
"""Response returned by a :class:`HttpRequestHandler`.
|
|
71
|
+
|
|
72
|
+
Mirrors the .NET ``HttpRequestResult`` record. ``headers`` preserves
|
|
73
|
+
multi-value response headers (e.g. multiple ``Set-Cookie`` headers) as a
|
|
74
|
+
``dict[str, list[str]]``. The executor folds duplicates into a single
|
|
75
|
+
comma-joined string only at the point it assigns ``responseHeaders`` to
|
|
76
|
+
workflow state.
|
|
77
|
+
|
|
78
|
+
Header keys are normalized to lowercase so that lookups are consistent
|
|
79
|
+
regardless of the server's transmitted casing (HTTP headers are
|
|
80
|
+
case-insensitive per RFC 7230 §3.2). Custom :class:`HttpRequestHandler`
|
|
81
|
+
implementations should follow the same convention.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
status_code: int
|
|
85
|
+
is_success_status_code: bool
|
|
86
|
+
body: str
|
|
87
|
+
headers: dict[str, list[str]] = field(default_factory=dict) # type: ignore[reportUnknownVariableType]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@runtime_checkable
|
|
91
|
+
class HttpRequestHandler(Protocol):
|
|
92
|
+
"""Protocol for HTTP request handlers used by ``HttpRequestAction``.
|
|
93
|
+
|
|
94
|
+
Implementations must be safe to call concurrently from multiple workflow
|
|
95
|
+
runs. Implementations are responsible for any URL allowlisting, SSRF
|
|
96
|
+
guards, retry policies, auth resolution, and other policies that the
|
|
97
|
+
workflow author wants applied.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
async def send(self, info: HttpRequestInfo) -> HttpRequestResult:
|
|
101
|
+
"""Dispatch ``info`` and return the response result.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
info: Description of the request to send.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The response. Implementations should NOT raise on non-2xx status
|
|
108
|
+
codes; instead, set ``is_success_status_code`` accordingly. They
|
|
109
|
+
SHOULD raise on transport-level failures (connection refused,
|
|
110
|
+
DNS errors, timeouts).
|
|
111
|
+
"""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
ClientProvider = Callable[[HttpRequestInfo], Awaitable["httpx.AsyncClient | None"]]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DefaultHttpRequestHandler:
|
|
119
|
+
"""Default :class:`HttpRequestHandler` backed by :class:`httpx.AsyncClient`.
|
|
120
|
+
|
|
121
|
+
Construction modes:
|
|
122
|
+
|
|
123
|
+
1. ``DefaultHttpRequestHandler()`` — owns an internal client created lazily
|
|
124
|
+
on first ``send()``. Closed by :meth:`aclose`.
|
|
125
|
+
2. ``DefaultHttpRequestHandler(client=existing)`` — caller-owned client.
|
|
126
|
+
Not closed by :meth:`aclose`.
|
|
127
|
+
3. ``DefaultHttpRequestHandler(client_provider=cb)`` — per-request client
|
|
128
|
+
lookup (parity with .NET's ``httpClientProvider`` callback). The
|
|
129
|
+
provider may return ``None`` to fall back to the owned/default client.
|
|
130
|
+
|
|
131
|
+
.. warning::
|
|
132
|
+
|
|
133
|
+
This handler performs **no** URL filtering or SSRF protection. Wrap or
|
|
134
|
+
replace it with a custom handler in production.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(
|
|
138
|
+
self,
|
|
139
|
+
*,
|
|
140
|
+
client: httpx.AsyncClient | None = None,
|
|
141
|
+
client_provider: ClientProvider | None = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
self._owned_client: httpx.AsyncClient | None = None
|
|
144
|
+
self._caller_client = client
|
|
145
|
+
self._client_provider = client_provider
|
|
146
|
+
# Guards lazy creation of ``_owned_client`` against concurrent first
|
|
147
|
+
# ``send()`` calls leaking duplicate clients.
|
|
148
|
+
self._owned_client_lock = asyncio.Lock()
|
|
149
|
+
|
|
150
|
+
async def send(self, info: HttpRequestInfo) -> HttpRequestResult:
|
|
151
|
+
"""Dispatch the request and return the parsed result."""
|
|
152
|
+
if not info.url:
|
|
153
|
+
raise ValueError("HttpRequestInfo.url must be a non-empty string.")
|
|
154
|
+
if not info.method:
|
|
155
|
+
raise ValueError("HttpRequestInfo.method must be a non-empty string.")
|
|
156
|
+
|
|
157
|
+
client = await self._resolve_client(info)
|
|
158
|
+
|
|
159
|
+
timeout: httpx.Timeout | object
|
|
160
|
+
if info.timeout_ms is not None and info.timeout_ms > 0:
|
|
161
|
+
timeout = httpx.Timeout(info.timeout_ms / 1000.0)
|
|
162
|
+
else:
|
|
163
|
+
timeout = httpx.USE_CLIENT_DEFAULT
|
|
164
|
+
|
|
165
|
+
headers = dict(info.headers)
|
|
166
|
+
content: bytes | str | None = None
|
|
167
|
+
if info.body is not None:
|
|
168
|
+
content = info.body
|
|
169
|
+
if not _has_header(headers, "content-type"):
|
|
170
|
+
# Match .NET DefaultHttpRequestHandler: when a body is sent
|
|
171
|
+
# without an explicit content type, default to ``text/plain``
|
|
172
|
+
# so the request is interpretable by servers and direct
|
|
173
|
+
# callers (not just the YAML executor) get sensible defaults.
|
|
174
|
+
headers["Content-Type"] = info.body_content_type or "text/plain"
|
|
175
|
+
|
|
176
|
+
params: Mapping[str, str] | None = info.query_parameters or None
|
|
177
|
+
|
|
178
|
+
response = await client.request(
|
|
179
|
+
method=info.method,
|
|
180
|
+
url=info.url,
|
|
181
|
+
params=params,
|
|
182
|
+
headers=headers or None,
|
|
183
|
+
content=content,
|
|
184
|
+
timeout=timeout,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Preserve multi-value headers (e.g. multiple Set-Cookie) as list[str].
|
|
188
|
+
# Normalize names to lowercase so lookups are consistent and case
|
|
189
|
+
# variations from the transport do not create duplicate logical keys
|
|
190
|
+
# (HTTP headers are case-insensitive per RFC 7230 §3.2).
|
|
191
|
+
result_headers: dict[str, list[str]] = {}
|
|
192
|
+
for key, value in response.headers.multi_items():
|
|
193
|
+
result_headers.setdefault(key.lower(), []).append(value)
|
|
194
|
+
|
|
195
|
+
body_text = response.text
|
|
196
|
+
|
|
197
|
+
return HttpRequestResult(
|
|
198
|
+
status_code=response.status_code,
|
|
199
|
+
is_success_status_code=200 <= response.status_code < 300,
|
|
200
|
+
body=body_text,
|
|
201
|
+
headers=result_headers,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
async def aclose(self) -> None:
|
|
205
|
+
"""Release the owned client, if any. Caller-owned clients are NOT closed."""
|
|
206
|
+
if self._owned_client is not None:
|
|
207
|
+
await self._owned_client.aclose()
|
|
208
|
+
self._owned_client = None
|
|
209
|
+
|
|
210
|
+
async def _resolve_client(self, info: HttpRequestInfo) -> httpx.AsyncClient:
|
|
211
|
+
"""Pick a client for this request: provider → caller → lazily-owned."""
|
|
212
|
+
if self._client_provider is not None:
|
|
213
|
+
provided = await self._client_provider(info)
|
|
214
|
+
if provided is not None:
|
|
215
|
+
return provided
|
|
216
|
+
if self._caller_client is not None:
|
|
217
|
+
return self._caller_client
|
|
218
|
+
if self._owned_client is None:
|
|
219
|
+
# Double-checked locking under asyncio.Lock so concurrent first
|
|
220
|
+
# callers don't each create a fresh httpx.AsyncClient and orphan
|
|
221
|
+
# one of them.
|
|
222
|
+
async with self._owned_client_lock:
|
|
223
|
+
if self._owned_client is None:
|
|
224
|
+
self._owned_client = httpx.AsyncClient()
|
|
225
|
+
return self._owned_client
|
|
226
|
+
|
|
227
|
+
async def __aenter__(self) -> DefaultHttpRequestHandler:
|
|
228
|
+
return self
|
|
229
|
+
|
|
230
|
+
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
231
|
+
await self.aclose()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _has_header(headers: Mapping[str, str], name: str) -> bool:
|
|
235
|
+
"""Case-insensitive header presence check."""
|
|
236
|
+
needle = name.lower()
|
|
237
|
+
return any(key.lower() == needle for key in headers)
|