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,417 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Executor for the ``HttpRequestAction`` declarative action.
|
|
4
|
+
|
|
5
|
+
Mirrors the .NET ``HttpRequestExecutor``: dispatches an HTTP request through the
|
|
6
|
+
configured :class:`HttpRequestHandler`, parses the response body, and assigns
|
|
7
|
+
the parsed body and response headers to the declared state paths.
|
|
8
|
+
|
|
9
|
+
Security note: response bodies can echo secrets and may be very large. Diagnostic
|
|
10
|
+
messages produced for non-2xx responses truncate the body to 256 characters and
|
|
11
|
+
collapse CR/LF/TAB to spaces (parity with .NET ``FormatBodyForDiagnostics``).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
from agent_framework import (
|
|
23
|
+
Message,
|
|
24
|
+
WorkflowContext,
|
|
25
|
+
handler,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from ._declarative_base import (
|
|
29
|
+
ActionComplete,
|
|
30
|
+
DeclarativeActionExecutor,
|
|
31
|
+
DeclarativeWorkflowState,
|
|
32
|
+
)
|
|
33
|
+
from ._errors import DeclarativeActionError
|
|
34
|
+
from ._http_handler import HttpRequestHandler, HttpRequestInfo, HttpRequestResult
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"HTTP_ACTION_EXECUTORS",
|
|
38
|
+
"HttpRequestActionExecutor",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
_MAX_BODY_DIAGNOSTIC_LENGTH = 256
|
|
44
|
+
_BODY_TRUNCATION_SUFFIX = " \u2026 [truncated]"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Body discriminator aliases. Long forms match the .NET object-model type
|
|
48
|
+
# names so YAML produced by .NET round-trips. Short forms are the .NET YAML
|
|
49
|
+
# convention used in test fixtures.
|
|
50
|
+
_BODY_KIND_JSON = {"json", "JsonRequestContent"}
|
|
51
|
+
_BODY_KIND_RAW = {"raw", "RawRequestContent"}
|
|
52
|
+
_BODY_KIND_NONE = {"none", "NoRequestContent"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_path(action_def: Mapping[str, Any], key: str) -> str | None:
|
|
56
|
+
"""Extract a state path from ``response``/``responseHeaders`` field.
|
|
57
|
+
|
|
58
|
+
Supports two YAML shapes (matches .NET serialization round-trips):
|
|
59
|
+
|
|
60
|
+
- ``response: Local.MyVar`` (plain string).
|
|
61
|
+
- ``response: { path: Local.MyVar }`` (object form).
|
|
62
|
+
"""
|
|
63
|
+
value = action_def.get(key)
|
|
64
|
+
if isinstance(value, str):
|
|
65
|
+
return value or None
|
|
66
|
+
if isinstance(value, Mapping):
|
|
67
|
+
path = value.get("path") # type: ignore[reportUnknownMemberType, reportUnknownVariableType]
|
|
68
|
+
return path if isinstance(path, str) and path else None
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _format_body_for_diagnostics(body: str | None) -> str:
|
|
73
|
+
"""Truncate and sanitise a response body for inclusion in error messages.
|
|
74
|
+
|
|
75
|
+
Mirrors the .NET ``FormatBodyForDiagnostics`` helper:
|
|
76
|
+
|
|
77
|
+
- Empty/None -> empty string.
|
|
78
|
+
- Replaces CR/LF/TAB with spaces.
|
|
79
|
+
- Truncates to 256 chars with a unicode-ellipsis ``[truncated]`` suffix.
|
|
80
|
+
"""
|
|
81
|
+
if not body:
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
truncated = len(body) > _MAX_BODY_DIAGNOSTIC_LENGTH
|
|
85
|
+
head = body[:_MAX_BODY_DIAGNOSTIC_LENGTH] if truncated else body
|
|
86
|
+
sanitized = head.replace("\r", " ").replace("\n", " ").replace("\t", " ")
|
|
87
|
+
return sanitized + _BODY_TRUNCATION_SUFFIX if truncated else sanitized
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parse_response_body(body: str | None) -> Any:
|
|
91
|
+
"""Parse an HTTP response body the same way the .NET executor does.
|
|
92
|
+
|
|
93
|
+
JSON-first: if the body parses as JSON, the parsed value is returned. Other
|
|
94
|
+
bodies are returned as the raw string. Empty/None bodies return ``None``.
|
|
95
|
+
"""
|
|
96
|
+
if body is None or body == "":
|
|
97
|
+
return None
|
|
98
|
+
try:
|
|
99
|
+
return json.loads(body)
|
|
100
|
+
except json.JSONDecodeError:
|
|
101
|
+
return body
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _format_query_value(value: Any) -> str | None:
|
|
105
|
+
"""Format a query-parameter value for URL inclusion.
|
|
106
|
+
|
|
107
|
+
Mirrors .NET ``FormatQueryValue``: ``None`` is dropped, ``bool`` becomes
|
|
108
|
+
lower-case ``"true"``/``"false"``, numerics use invariant ``str()``, and
|
|
109
|
+
other values fall through to ``str()``.
|
|
110
|
+
"""
|
|
111
|
+
if value is None:
|
|
112
|
+
return None
|
|
113
|
+
if isinstance(value, bool):
|
|
114
|
+
return "true" if value else "false"
|
|
115
|
+
if isinstance(value, str):
|
|
116
|
+
return value
|
|
117
|
+
return str(value)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _get_messages_path(state: DeclarativeWorkflowState, conversation_id_expr: str | None) -> str | None:
|
|
121
|
+
"""Return the configured conversation messages path, if any.
|
|
122
|
+
|
|
123
|
+
Returns ``System.conversations.{evaluated_id}.messages`` when a
|
|
124
|
+
``conversation_id_expr`` is configured and evaluates to a non-empty value.
|
|
125
|
+
Returns ``None`` when no conversation id expression is configured or when
|
|
126
|
+
the expression evaluates to ``None`` or an empty string (matches .NET
|
|
127
|
+
``GetConversationId`` behaviour where empty becomes ``null`` and the
|
|
128
|
+
response is not appended).
|
|
129
|
+
"""
|
|
130
|
+
if not conversation_id_expr:
|
|
131
|
+
return None
|
|
132
|
+
evaluated = state.eval_if_expression(conversation_id_expr)
|
|
133
|
+
if evaluated is None or (isinstance(evaluated, str) and not evaluated):
|
|
134
|
+
return None
|
|
135
|
+
return f"System.conversations.{evaluated}.messages"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class HttpRequestActionExecutor(DeclarativeActionExecutor):
|
|
139
|
+
"""Executor for the ``HttpRequestAction`` declarative action.
|
|
140
|
+
|
|
141
|
+
Dispatches through the supplied :class:`HttpRequestHandler` and:
|
|
142
|
+
|
|
143
|
+
- Parses the response body (JSON-first, raw string fall-back).
|
|
144
|
+
- Assigns the parsed body to ``response`` path (if configured).
|
|
145
|
+
- Folds multi-value response headers (comma-joined) and assigns them to
|
|
146
|
+
``responseHeaders`` path (if configured).
|
|
147
|
+
- On 2xx with non-empty body and a configured ``conversationId``, appends
|
|
148
|
+
an Assistant :class:`agent_framework.Message` to
|
|
149
|
+
``System.conversations.{id}.messages``.
|
|
150
|
+
- On non-2xx, still publishes ``responseHeaders`` (diagnostic) and raises
|
|
151
|
+
:class:`DeclarativeActionError` with a status-coded message containing a
|
|
152
|
+
truncated/sanitised body preview.
|
|
153
|
+
|
|
154
|
+
Transport errors (``httpx.TimeoutException``, ``TimeoutError``,
|
|
155
|
+
``httpx.HTTPError``) become :class:`DeclarativeActionError`. ``CancelledError``
|
|
156
|
+
is intentionally NOT caught so that workflow cancellation propagates.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
action_def: dict[str, Any],
|
|
162
|
+
*,
|
|
163
|
+
id: str | None = None,
|
|
164
|
+
http_request_handler: HttpRequestHandler,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Create an HTTP request action executor.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
action_def: Parsed ``HttpRequestAction`` YAML dict.
|
|
170
|
+
id: Optional executor id (defaults to action id or generated).
|
|
171
|
+
http_request_handler: Handler used to dispatch HTTP requests.
|
|
172
|
+
Required: the builder enforces presence at workflow-build time.
|
|
173
|
+
"""
|
|
174
|
+
super().__init__(action_def, id=id)
|
|
175
|
+
self._http_request_handler = http_request_handler
|
|
176
|
+
|
|
177
|
+
@handler
|
|
178
|
+
async def handle_action(
|
|
179
|
+
self,
|
|
180
|
+
trigger: Any,
|
|
181
|
+
ctx: WorkflowContext[ActionComplete],
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Execute the HTTP request action."""
|
|
184
|
+
state = await self._ensure_state_initialized(ctx, trigger)
|
|
185
|
+
|
|
186
|
+
method = self._get_method(state)
|
|
187
|
+
url = self._get_url(state)
|
|
188
|
+
headers = self._get_headers(state)
|
|
189
|
+
query_parameters = self._get_query_parameters(state)
|
|
190
|
+
body, body_content_type = self._get_body(state)
|
|
191
|
+
timeout_ms = self._get_timeout_ms(state)
|
|
192
|
+
conversation_id_expr = self._action_def.get("conversationId")
|
|
193
|
+
connection_name = self._get_connection_name(state)
|
|
194
|
+
|
|
195
|
+
info = HttpRequestInfo(
|
|
196
|
+
method=method,
|
|
197
|
+
url=url,
|
|
198
|
+
headers=headers or {},
|
|
199
|
+
query_parameters=query_parameters or {},
|
|
200
|
+
body=body,
|
|
201
|
+
body_content_type=body_content_type,
|
|
202
|
+
timeout_ms=timeout_ms,
|
|
203
|
+
connection_name=connection_name,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
result = await self._http_request_handler.send(info)
|
|
208
|
+
except (httpx.TimeoutException, TimeoutError) as exc:
|
|
209
|
+
raise DeclarativeActionError(f"HTTP request to '{url}' timed out.") from exc
|
|
210
|
+
except DeclarativeActionError:
|
|
211
|
+
raise
|
|
212
|
+
except httpx.HTTPError as exc:
|
|
213
|
+
raise DeclarativeActionError(f"HTTP request to '{url}' failed: {type(exc).__name__}") from exc
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
# Custom HttpRequestHandler implementations may raise arbitrary
|
|
216
|
+
# exception types. Wrap them in DeclarativeActionError so workflow
|
|
217
|
+
# error handling stays uniform regardless of transport. Note that
|
|
218
|
+
# ``asyncio.CancelledError`` is a ``BaseException`` (not
|
|
219
|
+
# ``Exception``) and so still propagates unmodified, preserving
|
|
220
|
+
# workflow-cancellation semantics.
|
|
221
|
+
raise DeclarativeActionError(f"HTTP request to '{url}' failed: {type(exc).__name__}") from exc
|
|
222
|
+
|
|
223
|
+
if result.is_success_status_code:
|
|
224
|
+
self._assign_response(state, result)
|
|
225
|
+
self._assign_response_headers(state, result)
|
|
226
|
+
self._append_response_to_conversation(state, conversation_id_expr, result.body)
|
|
227
|
+
await ctx.send_message(ActionComplete())
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
# Non-success path: still publish headers diagnostically, then raise.
|
|
231
|
+
self._assign_response_headers(state, result)
|
|
232
|
+
body_preview = _format_body_for_diagnostics(result.body)
|
|
233
|
+
if body_preview:
|
|
234
|
+
message = f"HTTP request to '{url}' failed with status code {result.status_code}. Body: '{body_preview}'"
|
|
235
|
+
else:
|
|
236
|
+
message = f"HTTP request to '{url}' failed with status code {result.status_code}."
|
|
237
|
+
raise DeclarativeActionError(message)
|
|
238
|
+
|
|
239
|
+
# ----- Field resolution ----------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def _get_method(self, state: DeclarativeWorkflowState) -> str:
|
|
242
|
+
method = self._action_def.get("method")
|
|
243
|
+
evaluated = state.eval_if_expression(method) if method is not None else None
|
|
244
|
+
if not evaluated:
|
|
245
|
+
return "GET"
|
|
246
|
+
return str(evaluated).upper()
|
|
247
|
+
|
|
248
|
+
def _get_url(self, state: DeclarativeWorkflowState) -> str:
|
|
249
|
+
raw = self._action_def.get("url")
|
|
250
|
+
if raw is None:
|
|
251
|
+
raise ValueError("HttpRequestAction requires a 'url' field.")
|
|
252
|
+
evaluated = state.eval_if_expression(raw)
|
|
253
|
+
if not isinstance(evaluated, str) or not evaluated:
|
|
254
|
+
raise ValueError("HttpRequestAction 'url' evaluated to an empty value.")
|
|
255
|
+
return evaluated
|
|
256
|
+
|
|
257
|
+
def _get_headers(self, state: DeclarativeWorkflowState) -> dict[str, str] | None:
|
|
258
|
+
raw_headers = self._action_def.get("headers")
|
|
259
|
+
if not isinstance(raw_headers, Mapping) or not raw_headers:
|
|
260
|
+
return None
|
|
261
|
+
result: dict[str, str] = {}
|
|
262
|
+
for key, value in raw_headers.items(): # type: ignore[reportUnknownVariableType]
|
|
263
|
+
if not isinstance(key, str) or not key:
|
|
264
|
+
continue
|
|
265
|
+
evaluated = state.eval_if_expression(value)
|
|
266
|
+
if evaluated is None:
|
|
267
|
+
continue
|
|
268
|
+
text = str(evaluated)
|
|
269
|
+
if not text:
|
|
270
|
+
continue
|
|
271
|
+
result[key] = text
|
|
272
|
+
return result or None
|
|
273
|
+
|
|
274
|
+
def _get_query_parameters(self, state: DeclarativeWorkflowState) -> dict[str, str] | None:
|
|
275
|
+
raw_params = self._action_def.get("queryParameters")
|
|
276
|
+
if not isinstance(raw_params, Mapping) or not raw_params:
|
|
277
|
+
return None
|
|
278
|
+
result: dict[str, str] = {}
|
|
279
|
+
for key, value in raw_params.items(): # type: ignore[reportUnknownVariableType]
|
|
280
|
+
if not isinstance(key, str) or not key or value is None:
|
|
281
|
+
continue
|
|
282
|
+
evaluated = state.eval_if_expression(value)
|
|
283
|
+
formatted = _format_query_value(evaluated)
|
|
284
|
+
if formatted is not None:
|
|
285
|
+
result[key] = formatted
|
|
286
|
+
return result or None
|
|
287
|
+
|
|
288
|
+
def _get_body(self, state: DeclarativeWorkflowState) -> tuple[str | None, str | None]:
|
|
289
|
+
raw_body = self._action_def.get("body")
|
|
290
|
+
if raw_body is None:
|
|
291
|
+
return None, None
|
|
292
|
+
if not isinstance(raw_body, Mapping):
|
|
293
|
+
raise ValueError(
|
|
294
|
+
"HttpRequestAction 'body' must be a mapping with a 'kind' field (json, raw) or omitted entirely."
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
kind_value: Any = raw_body.get("kind") or raw_body.get("$kind") # type: ignore[reportUnknownMemberType]
|
|
298
|
+
if kind_value is None:
|
|
299
|
+
raise ValueError(
|
|
300
|
+
"HttpRequestAction 'body' is missing 'kind'. Use 'json', 'raw', or omit 'body' for no request body."
|
|
301
|
+
)
|
|
302
|
+
if not isinstance(kind_value, str):
|
|
303
|
+
raise ValueError(f"HttpRequestAction 'body.kind' must be a string, got {kind_value!r}.")
|
|
304
|
+
|
|
305
|
+
if kind_value in _BODY_KIND_NONE:
|
|
306
|
+
return None, None
|
|
307
|
+
|
|
308
|
+
if kind_value in _BODY_KIND_JSON:
|
|
309
|
+
content_expr: Any = raw_body.get("content") # type: ignore[reportUnknownMemberType]
|
|
310
|
+
if content_expr is None:
|
|
311
|
+
return None, None
|
|
312
|
+
evaluated = state.eval_if_expression(content_expr)
|
|
313
|
+
try:
|
|
314
|
+
body_text = json.dumps(evaluated, default=str)
|
|
315
|
+
except (TypeError, ValueError) as exc:
|
|
316
|
+
raise ValueError(f"HttpRequestAction 'body.content' could not be serialised as JSON: {exc}") from exc
|
|
317
|
+
return body_text, "application/json"
|
|
318
|
+
|
|
319
|
+
if kind_value in _BODY_KIND_RAW:
|
|
320
|
+
content_expr = raw_body.get("content") # type: ignore[reportUnknownMemberType]
|
|
321
|
+
content_type_expr: Any = raw_body.get("contentType") # type: ignore[reportUnknownMemberType]
|
|
322
|
+
content: str | None = None
|
|
323
|
+
if content_expr is not None:
|
|
324
|
+
evaluated = state.eval_if_expression(content_expr)
|
|
325
|
+
content = None if evaluated is None else str(evaluated)
|
|
326
|
+
content_type: str | None = None
|
|
327
|
+
if content_type_expr is not None:
|
|
328
|
+
ct_eval = state.eval_if_expression(content_type_expr)
|
|
329
|
+
ct_text = None if ct_eval is None else str(ct_eval)
|
|
330
|
+
content_type = ct_text or None
|
|
331
|
+
# Match .NET RawRequestContent semantics: when a raw body is sent
|
|
332
|
+
# without an explicit content type, default to text/plain so the
|
|
333
|
+
# request is interpretable by servers.
|
|
334
|
+
if content is not None and not content_type:
|
|
335
|
+
content_type = "text/plain"
|
|
336
|
+
return content, content_type
|
|
337
|
+
|
|
338
|
+
raise ValueError(
|
|
339
|
+
f"HttpRequestAction 'body.kind' has unsupported value '{kind_value}'. "
|
|
340
|
+
"Expected one of: json, raw, JsonRequestContent, RawRequestContent, "
|
|
341
|
+
"NoRequestContent."
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def _get_timeout_ms(self, state: DeclarativeWorkflowState) -> int | None:
|
|
345
|
+
raw = self._action_def.get("requestTimeoutInMilliseconds")
|
|
346
|
+
if raw is None:
|
|
347
|
+
return None
|
|
348
|
+
evaluated = state.eval_if_expression(raw)
|
|
349
|
+
if evaluated is None:
|
|
350
|
+
return None
|
|
351
|
+
try:
|
|
352
|
+
value = int(evaluated)
|
|
353
|
+
except (TypeError, ValueError):
|
|
354
|
+
logger.debug(
|
|
355
|
+
"HttpRequestAction: ignoring non-numeric requestTimeoutInMilliseconds=%r",
|
|
356
|
+
evaluated,
|
|
357
|
+
)
|
|
358
|
+
return None
|
|
359
|
+
return value if value > 0 else None
|
|
360
|
+
|
|
361
|
+
def _get_connection_name(self, state: DeclarativeWorkflowState) -> str | None:
|
|
362
|
+
connection = self._action_def.get("connection")
|
|
363
|
+
if not isinstance(connection, Mapping):
|
|
364
|
+
return None
|
|
365
|
+
name_expr: Any = connection.get("name") # type: ignore[reportUnknownMemberType]
|
|
366
|
+
if name_expr is None:
|
|
367
|
+
return None
|
|
368
|
+
evaluated = state.eval_if_expression(name_expr)
|
|
369
|
+
if evaluated is None:
|
|
370
|
+
return None
|
|
371
|
+
text = str(evaluated)
|
|
372
|
+
return text or None
|
|
373
|
+
|
|
374
|
+
# ----- Result handling -----------------------------------------------------
|
|
375
|
+
|
|
376
|
+
def _assign_response(self, state: DeclarativeWorkflowState, result: HttpRequestResult) -> None:
|
|
377
|
+
path = _get_path(self._action_def, "response")
|
|
378
|
+
if path is None:
|
|
379
|
+
return
|
|
380
|
+
state.set(path, _parse_response_body(result.body))
|
|
381
|
+
|
|
382
|
+
def _assign_response_headers(self, state: DeclarativeWorkflowState, result: HttpRequestResult) -> None:
|
|
383
|
+
path = _get_path(self._action_def, "responseHeaders")
|
|
384
|
+
if path is None:
|
|
385
|
+
return
|
|
386
|
+
if not result.headers:
|
|
387
|
+
state.set(path, None)
|
|
388
|
+
return
|
|
389
|
+
# Fold multi-value headers with commas (standard HTTP folding) only at
|
|
390
|
+
# assignment time. The raw multi-value dict on HttpRequestResult.headers
|
|
391
|
+
# is left untouched so callers/tests can inspect duplicates.
|
|
392
|
+
flattened: dict[str, str] = {}
|
|
393
|
+
for key, values in result.headers.items():
|
|
394
|
+
flattened[key] = ",".join(values)
|
|
395
|
+
state.set(path, flattened)
|
|
396
|
+
|
|
397
|
+
def _append_response_to_conversation(
|
|
398
|
+
self,
|
|
399
|
+
state: DeclarativeWorkflowState,
|
|
400
|
+
conversation_id_expr: str | None,
|
|
401
|
+
body: str,
|
|
402
|
+
) -> None:
|
|
403
|
+
if not body:
|
|
404
|
+
return
|
|
405
|
+
messages_path = _get_messages_path(state, conversation_id_expr)
|
|
406
|
+
if messages_path is None:
|
|
407
|
+
return
|
|
408
|
+
# Mirrors InvokeAzureAgentExecutor: rely on state.append to lazily
|
|
409
|
+
# create the conversation entry. Avoids re-parsing the id back out
|
|
410
|
+
# of the dotted path string.
|
|
411
|
+
message = Message(role="assistant", contents=[body])
|
|
412
|
+
state.append(messages_path, message)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
HTTP_ACTION_EXECUTORS: dict[str, type[DeclarativeActionExecutor]] = {
|
|
416
|
+
"HttpRequestAction": HttpRequestActionExecutor,
|
|
417
|
+
}
|