agent-framework-declarative 1.0.1__tar.gz → 1.0.2__tar.gz
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-1.0.1 → agent_framework_declarative-1.0.2}/PKG-INFO +1 -1
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_agents.py +224 -91
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_basic.py +3 -1
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/pyproject.toml +1 -1
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/LICENSE +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/README.md +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/__init__.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_feature_usage.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_loader.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_models.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/__init__.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_declarative_base.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_declarative_builder.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_errors.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_control_flow.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_external_input.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_http.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_mcp.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_tools.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_factory.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_http_handler.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_mcp_handler.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_powerfx_functions.py +0 -0
- {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_state.py +0 -0
|
@@ -16,6 +16,7 @@ import contextlib
|
|
|
16
16
|
import json
|
|
17
17
|
import logging
|
|
18
18
|
import uuid
|
|
19
|
+
from collections.abc import Iterator
|
|
19
20
|
from dataclasses import dataclass, field
|
|
20
21
|
from typing import Any, cast
|
|
21
22
|
|
|
@@ -36,6 +37,218 @@ from ._declarative_base import (
|
|
|
36
37
|
|
|
37
38
|
logger = logging.getLogger(__name__)
|
|
38
39
|
|
|
40
|
+
_CODE_FENCE = "```"
|
|
41
|
+
_JSON_CODE_FENCE_QUALIFIER = "json"
|
|
42
|
+
_MAX_JSON_DECODE_BUDGET_MULTIPLIER = 4
|
|
43
|
+
_NO_JSON = object()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _iter_fenced_blocks(text: str, *, require_json_qualifier: bool) -> Iterator[str]:
|
|
47
|
+
"""Yield non-overlapping fenced blocks in source order."""
|
|
48
|
+
search_start = 0
|
|
49
|
+
while True:
|
|
50
|
+
opening_index = text.find(_CODE_FENCE, search_start)
|
|
51
|
+
if opening_index < 0:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
content_start = opening_index + len(_CODE_FENCE)
|
|
55
|
+
if require_json_qualifier:
|
|
56
|
+
if not text.startswith(_JSON_CODE_FENCE_QUALIFIER, content_start):
|
|
57
|
+
search_start = content_start
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
qualifier_end = content_start + len(_JSON_CODE_FENCE_QUALIFIER)
|
|
61
|
+
if (
|
|
62
|
+
qualifier_end < len(text)
|
|
63
|
+
and not text[qualifier_end].isspace()
|
|
64
|
+
and text[qualifier_end] not in "{["
|
|
65
|
+
and not text.startswith(_CODE_FENCE, qualifier_end)
|
|
66
|
+
):
|
|
67
|
+
search_start = content_start
|
|
68
|
+
continue
|
|
69
|
+
content_start = qualifier_end
|
|
70
|
+
|
|
71
|
+
while content_start < len(text) and text[content_start].isspace():
|
|
72
|
+
content_start += 1
|
|
73
|
+
|
|
74
|
+
closing_index = text.find(_CODE_FENCE, content_start)
|
|
75
|
+
if closing_index < 0:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
yield text[content_start:closing_index].strip()
|
|
79
|
+
search_start = closing_index + len(_CODE_FENCE)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _index_escaped_quotes(text: str) -> bytearray:
|
|
83
|
+
"""Index quote characters preceded by an odd-length backslash run."""
|
|
84
|
+
escaped_quotes = bytearray(len(text))
|
|
85
|
+
backslash_count = 0
|
|
86
|
+
|
|
87
|
+
for index, char in enumerate(text):
|
|
88
|
+
if char == "\\":
|
|
89
|
+
backslash_count += 1
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
if char == '"' and backslash_count % 2 == 1:
|
|
93
|
+
escaped_quotes[index] = 1
|
|
94
|
+
backslash_count = 0
|
|
95
|
+
|
|
96
|
+
return escaped_quotes
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _index_json_candidates_forward(text: str, escaped_quotes: bytearray) -> set[tuple[int, int]]:
|
|
100
|
+
"""Index JSON candidate ranges from left to right."""
|
|
101
|
+
candidates: set[tuple[int, int]] = set()
|
|
102
|
+
object_openings: list[int] = []
|
|
103
|
+
array_openings: list[int] = []
|
|
104
|
+
in_string = False
|
|
105
|
+
|
|
106
|
+
for index, char in enumerate(text):
|
|
107
|
+
if not object_openings and not array_openings:
|
|
108
|
+
if char in "{[":
|
|
109
|
+
(object_openings if char == "{" else array_openings).append(index)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
if char == '"' and not escaped_quotes[index]:
|
|
113
|
+
in_string = not in_string
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
if in_string:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
if char in "{[":
|
|
120
|
+
(object_openings if char == "{" else array_openings).append(index)
|
|
121
|
+
elif char == "}" and object_openings:
|
|
122
|
+
candidates.add((object_openings.pop(), index))
|
|
123
|
+
elif char == "]" and array_openings:
|
|
124
|
+
candidates.add((array_openings.pop(), index))
|
|
125
|
+
|
|
126
|
+
return candidates
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _index_json_candidates_reverse(text: str, escaped_quotes: bytearray) -> set[tuple[int, int]]:
|
|
130
|
+
"""Index JSON candidate ranges from right to left."""
|
|
131
|
+
candidates: set[tuple[int, int]] = set()
|
|
132
|
+
object_closings: list[int] = []
|
|
133
|
+
array_closings: list[int] = []
|
|
134
|
+
in_string = False
|
|
135
|
+
|
|
136
|
+
for index in range(len(text) - 1, -1, -1):
|
|
137
|
+
char = text[index]
|
|
138
|
+
if not object_closings and not array_closings:
|
|
139
|
+
if char in "}]":
|
|
140
|
+
(object_closings if char == "}" else array_closings).append(index)
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
if char == '"' and not escaped_quotes[index]:
|
|
144
|
+
in_string = not in_string
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if in_string:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
if char in "}]":
|
|
151
|
+
(object_closings if char == "}" else array_closings).append(index)
|
|
152
|
+
elif char == "{" and object_closings:
|
|
153
|
+
candidates.add((index, object_closings.pop()))
|
|
154
|
+
elif char == "[" and array_closings:
|
|
155
|
+
candidates.add((index, array_closings.pop()))
|
|
156
|
+
|
|
157
|
+
return candidates
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _find_last_decodable_json(text: str) -> Any:
|
|
161
|
+
"""Find the last decodable JSON object or array within text."""
|
|
162
|
+
escaped_quotes = _index_escaped_quotes(text)
|
|
163
|
+
candidates = _index_json_candidates_forward(text, escaped_quotes)
|
|
164
|
+
candidates.update(_index_json_candidates_reverse(text, escaped_quotes))
|
|
165
|
+
|
|
166
|
+
candidate_groups: list[tuple[int, int, list[tuple[int, int]]]] = []
|
|
167
|
+
for candidate in sorted(candidates):
|
|
168
|
+
json_start, json_end = candidate
|
|
169
|
+
if not candidate_groups or json_start > candidate_groups[-1][1]:
|
|
170
|
+
candidate_groups.append((json_start, json_end, [candidate]))
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
group_start, group_end, group_candidates = candidate_groups[-1]
|
|
174
|
+
group_candidates.append(candidate)
|
|
175
|
+
candidate_groups[-1] = (group_start, max(group_end, json_end), group_candidates)
|
|
176
|
+
|
|
177
|
+
for group_start, group_end, group_candidates in reversed(candidate_groups):
|
|
178
|
+
group_span = group_end - group_start + 1
|
|
179
|
+
primary_decode_budget = group_span * (_MAX_JSON_DECODE_BUDGET_MULTIPLIER // 2)
|
|
180
|
+
recovery_decode_budget = group_span * (
|
|
181
|
+
_MAX_JSON_DECODE_BUDGET_MULTIPLIER - (_MAX_JSON_DECODE_BUDGET_MULTIPLIER // 2)
|
|
182
|
+
)
|
|
183
|
+
attempted_candidates: set[tuple[int, int]] = set()
|
|
184
|
+
last_json: Any = _NO_JSON
|
|
185
|
+
consumed_end = -1
|
|
186
|
+
candidate_index = 0
|
|
187
|
+
|
|
188
|
+
while candidate_index < len(group_candidates) and primary_decode_budget > 0:
|
|
189
|
+
json_start, json_end = group_candidates[candidate_index]
|
|
190
|
+
candidate_index += 1
|
|
191
|
+
if json_start <= consumed_end:
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
candidate_length = json_end - json_start + 1
|
|
195
|
+
if candidate_length > primary_decode_budget:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
primary_decode_budget -= candidate_length
|
|
199
|
+
attempted_candidates.add((json_start, json_end))
|
|
200
|
+
try:
|
|
201
|
+
last_json = json.loads(text[json_start : json_end + 1])
|
|
202
|
+
except json.JSONDecodeError:
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
consumed_end = json_end
|
|
206
|
+
while candidate_index < len(group_candidates) and group_candidates[candidate_index][0] <= consumed_end:
|
|
207
|
+
candidate_index += 1
|
|
208
|
+
|
|
209
|
+
recovery_candidates = sorted(
|
|
210
|
+
group_candidates,
|
|
211
|
+
key=lambda candidate: (candidate[1] - candidate[0], -candidate[0]),
|
|
212
|
+
)
|
|
213
|
+
recovered_json: Any = _NO_JSON
|
|
214
|
+
recovered_range: tuple[int, int] | None = None
|
|
215
|
+
for json_start, json_end in recovery_candidates:
|
|
216
|
+
if recovery_decode_budget == 0:
|
|
217
|
+
break
|
|
218
|
+
if (json_start, json_end) in attempted_candidates or json_start <= consumed_end:
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
candidate_length = json_end - json_start + 1
|
|
222
|
+
if candidate_length > recovery_decode_budget:
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
recovery_decode_budget -= candidate_length
|
|
226
|
+
try:
|
|
227
|
+
candidate_json = json.loads(text[json_start : json_end + 1])
|
|
228
|
+
except json.JSONDecodeError:
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
candidate_contains_recovered = (
|
|
232
|
+
recovered_range is not None and json_start <= recovered_range[0] and json_end >= recovered_range[1]
|
|
233
|
+
)
|
|
234
|
+
recovered_contains_candidate = (
|
|
235
|
+
recovered_range is not None and recovered_range[0] <= json_start and recovered_range[1] >= json_end
|
|
236
|
+
)
|
|
237
|
+
if (
|
|
238
|
+
recovered_range is None
|
|
239
|
+
or candidate_contains_recovered
|
|
240
|
+
or (not recovered_contains_candidate and json_start > recovered_range[0])
|
|
241
|
+
):
|
|
242
|
+
recovered_json = candidate_json
|
|
243
|
+
recovered_range = (json_start, json_end)
|
|
244
|
+
|
|
245
|
+
if recovered_json is not _NO_JSON:
|
|
246
|
+
return recovered_json
|
|
247
|
+
if last_json is not _NO_JSON:
|
|
248
|
+
return last_json
|
|
249
|
+
|
|
250
|
+
raise json.JSONDecodeError("No valid JSON found in response", text, 0)
|
|
251
|
+
|
|
39
252
|
|
|
40
253
|
def _extract_json_from_response(text: str) -> Any:
|
|
41
254
|
r"""Extract and parse JSON from an agent response.
|
|
@@ -58,13 +271,11 @@ def _extract_json_from_response(text: str) -> Any:
|
|
|
58
271
|
text: The raw text response from an agent
|
|
59
272
|
|
|
60
273
|
Returns:
|
|
61
|
-
Parsed JSON
|
|
274
|
+
Parsed JSON, or None if the response is empty.
|
|
62
275
|
|
|
63
276
|
Raises:
|
|
64
277
|
json.JSONDecodeError: If no valid JSON can be extracted
|
|
65
278
|
"""
|
|
66
|
-
import re
|
|
67
|
-
|
|
68
279
|
if not text:
|
|
69
280
|
return None
|
|
70
281
|
|
|
@@ -79,96 +290,18 @@ def _extract_json_from_response(text: str) -> Any:
|
|
|
79
290
|
except json.JSONDecodeError:
|
|
80
291
|
pass
|
|
81
292
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
matches = list(re.finditer(pattern, text, re.DOTALL))
|
|
90
|
-
if matches:
|
|
91
|
-
# Try the last match first (most likely to be the final result)
|
|
92
|
-
for match in reversed(matches):
|
|
93
|
-
try:
|
|
94
|
-
return json.loads(match.group(1).strip())
|
|
95
|
-
except json.JSONDecodeError:
|
|
96
|
-
continue
|
|
97
|
-
|
|
98
|
-
# Find ALL JSON objects {...} or arrays [...] in the text and return the last valid one
|
|
99
|
-
# This handles cases where agents stream multiple JSON objects (partial, then final)
|
|
100
|
-
all_json_objects: list[Any] = []
|
|
101
|
-
|
|
102
|
-
pos = 0
|
|
103
|
-
while pos < len(text):
|
|
104
|
-
# Find next { or [
|
|
105
|
-
json_start = -1
|
|
106
|
-
bracket_char = None
|
|
107
|
-
for i in range(pos, len(text)):
|
|
108
|
-
if text[i] == "{":
|
|
109
|
-
json_start = i
|
|
110
|
-
bracket_char = "{"
|
|
111
|
-
break
|
|
112
|
-
if text[i] == "[":
|
|
113
|
-
json_start = i
|
|
114
|
-
bracket_char = "["
|
|
115
|
-
break
|
|
116
|
-
|
|
117
|
-
if json_start < 0:
|
|
118
|
-
break # No more JSON objects
|
|
119
|
-
|
|
120
|
-
# Find matching closing bracket
|
|
121
|
-
open_bracket = bracket_char
|
|
122
|
-
close_bracket = "}" if open_bracket == "{" else "]"
|
|
123
|
-
depth = 0
|
|
124
|
-
in_string = False
|
|
125
|
-
escape_next = False
|
|
126
|
-
found_end = False
|
|
127
|
-
|
|
128
|
-
for i in range(json_start, len(text)):
|
|
129
|
-
char = text[i]
|
|
130
|
-
|
|
131
|
-
if escape_next:
|
|
132
|
-
escape_next = False
|
|
133
|
-
continue
|
|
134
|
-
|
|
135
|
-
if char == "\\":
|
|
136
|
-
escape_next = True
|
|
137
|
-
continue
|
|
138
|
-
|
|
139
|
-
if char == '"' and not escape_next:
|
|
140
|
-
in_string = not in_string
|
|
141
|
-
continue
|
|
142
|
-
|
|
143
|
-
if in_string:
|
|
293
|
+
# Exactly-qualified JSON fences take precedence over plain fences.
|
|
294
|
+
for require_json_qualifier in (True, False):
|
|
295
|
+
last_fenced_json: Any = _NO_JSON
|
|
296
|
+
for block in _iter_fenced_blocks(text, require_json_qualifier=require_json_qualifier):
|
|
297
|
+
try:
|
|
298
|
+
last_fenced_json = json.loads(block)
|
|
299
|
+
except json.JSONDecodeError:
|
|
144
300
|
continue
|
|
301
|
+
if last_fenced_json is not _NO_JSON:
|
|
302
|
+
return last_fenced_json
|
|
145
303
|
|
|
146
|
-
|
|
147
|
-
depth += 1
|
|
148
|
-
elif char == close_bracket:
|
|
149
|
-
depth -= 1
|
|
150
|
-
if depth == 0:
|
|
151
|
-
# Found the end
|
|
152
|
-
potential_json = text[json_start : i + 1]
|
|
153
|
-
try:
|
|
154
|
-
parsed = json.loads(potential_json)
|
|
155
|
-
all_json_objects.append(parsed)
|
|
156
|
-
except json.JSONDecodeError:
|
|
157
|
-
pass
|
|
158
|
-
pos = i + 1
|
|
159
|
-
found_end = True
|
|
160
|
-
break
|
|
161
|
-
|
|
162
|
-
if not found_end:
|
|
163
|
-
# Malformed JSON, move past the start character
|
|
164
|
-
pos = json_start + 1
|
|
165
|
-
|
|
166
|
-
# Return the last valid JSON object (most likely to be the final/complete result)
|
|
167
|
-
if all_json_objects:
|
|
168
|
-
return all_json_objects[-1]
|
|
169
|
-
|
|
170
|
-
# Unable to extract JSON
|
|
171
|
-
raise json.JSONDecodeError("No valid JSON found in response", text, 0)
|
|
304
|
+
return _find_last_decodable_json(text)
|
|
172
305
|
|
|
173
306
|
|
|
174
307
|
def _validate_conversation_history(messages: list[Message], agent_name: str) -> None:
|
|
@@ -361,7 +361,9 @@ class EditTableV2Executor(DeclarativeActionExecutor):
|
|
|
361
361
|
|
|
362
362
|
table_path = self._action_def.get("table") or _get_variable_path(self._action_def, "variable")
|
|
363
363
|
operation = self._action_def.get("operation", "add").lower()
|
|
364
|
-
item = self._action_def.get("item")
|
|
364
|
+
item = self._action_def.get("item")
|
|
365
|
+
if item is None:
|
|
366
|
+
item = self._action_def.get("value")
|
|
365
367
|
key_field = self._action_def.get("key")
|
|
366
368
|
index = self._action_def.get("index")
|
|
367
369
|
|
|
@@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework."
|
|
|
4
4
|
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.2"
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
9
|
urls.homepage = "https://aka.ms/agent-framework"
|
|
10
10
|
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|