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.
Files changed (24) hide show
  1. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/PKG-INFO +1 -1
  2. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_agents.py +224 -91
  3. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_basic.py +3 -1
  4. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/pyproject.toml +1 -1
  5. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/LICENSE +0 -0
  6. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/README.md +0 -0
  7. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/__init__.py +0 -0
  8. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_feature_usage.py +0 -0
  9. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_loader.py +0 -0
  10. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_models.py +0 -0
  11. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/__init__.py +0 -0
  12. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_declarative_base.py +0 -0
  13. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_declarative_builder.py +0 -0
  14. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_errors.py +0 -0
  15. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_control_flow.py +0 -0
  16. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_external_input.py +0 -0
  17. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_http.py +0 -0
  18. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_mcp.py +0 -0
  19. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_tools.py +0 -0
  20. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_factory.py +0 -0
  21. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_http_handler.py +0 -0
  22. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_mcp_handler.py +0 -0
  23. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_powerfx_functions.py +0 -0
  24. {agent_framework_declarative-1.0.1 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-framework-declarative
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: Declarative specification support for Microsoft Agent Framework.
5
5
  Author-email: Microsoft <af-support@microsoft.com>
6
6
  Requires-Python: >=3.10
@@ -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 as a Python dict/list, or None if parsing fails
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
- # Try extracting from markdown code blocks: ```json ... ``` or ``` ... ```
83
- # Use the last code block if there are multiple
84
- code_block_patterns = [
85
- r"```json\s*\n?(.*?)\n?```", # ```json ... ```
86
- r"```\s*\n?(.*?)\n?```", # ``` ... ```
87
- ]
88
- for pattern in code_block_patterns:
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
- if char == open_bracket:
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") or self._action_def.get("value")
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.1"
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"