agent-framework-declarative 1.0.0rc2__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.0rc2 → agent_framework_declarative-1.0.2}/PKG-INFO +5 -6
  2. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/README.md +2 -3
  3. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/__init__.py +1 -1
  4. agent_framework_declarative-1.0.2/agent_framework_declarative/_feature_usage.py +10 -0
  5. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_loader.py +13 -7
  6. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_models.py +72 -55
  7. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_declarative_base.py +11 -11
  8. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_declarative_builder.py +1 -1
  9. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_agents.py +226 -93
  10. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_basic.py +5 -3
  11. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_mcp.py +1 -1
  12. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_factory.py +3 -0
  13. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_http_handler.py +1 -1
  14. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_powerfx_functions.py +1 -1
  15. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_state.py +1 -1
  16. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/pyproject.toml +3 -3
  17. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/LICENSE +0 -0
  18. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/__init__.py +0 -0
  19. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_errors.py +0 -0
  20. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_control_flow.py +0 -0
  21. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_external_input.py +0 -0
  22. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_http.py +0 -0
  23. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_tools.py +0 -0
  24. {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_mcp_handler.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-framework-declarative
3
- Version: 1.0.0rc2
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
7
7
  Description-Content-Type: text/markdown
8
8
  Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Development Status :: 4 - Beta
9
+ Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.10
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Typing :: Typed
17
17
  License-File: LICENSE
18
- Requires-Dist: agent-framework-core>=1.9.0,<2
18
+ Requires-Dist: agent-framework-core>=1.13.0,<2
19
19
  Requires-Dist: httpx>=0.27,<1
20
20
  Requires-Dist: powerfx>=0.0.32,<0.0.35; python_version < '3.14'
21
21
  Requires-Dist: pyyaml>=6.0,<7.0
@@ -29,7 +29,7 @@ Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/pyth
29
29
  Please install this package via pip:
30
30
 
31
31
  ```bash
32
- pip install agent-framework-declarative --pre
32
+ pip install agent-framework-declarative
33
33
  ```
34
34
 
35
35
  ## Release stage
@@ -37,8 +37,7 @@ pip install agent-framework-declarative --pre
37
37
  This package ships at two different stability levels:
38
38
 
39
39
  - **Declarative workflows** (`WorkflowFactory`, executors, handlers, and the
40
- `_workflows` surface) are at **release-candidate** stability and may receive only
41
- minor refinements before GA.
40
+ `_workflows` surface) are **stable**.
42
41
  - **Declarative agents** (`AgentFactory` and the YAML agent loading/parsing path:
43
42
  `DeclarativeLoaderError`, `ProviderLookupError`, `ProviderTypeMapping`) are
44
43
  **experimental** and may change or be removed in future versions without notice.
@@ -3,7 +3,7 @@
3
3
  Please install this package via pip:
4
4
 
5
5
  ```bash
6
- pip install agent-framework-declarative --pre
6
+ pip install agent-framework-declarative
7
7
  ```
8
8
 
9
9
  ## Release stage
@@ -11,8 +11,7 @@ pip install agent-framework-declarative --pre
11
11
  This package ships at two different stability levels:
12
12
 
13
13
  - **Declarative workflows** (`WorkflowFactory`, executors, handlers, and the
14
- `_workflows` surface) are at **release-candidate** stability and may receive only
15
- minor refinements before GA.
14
+ `_workflows` surface) are **stable**.
16
15
  - **Declarative agents** (`AgentFactory` and the YAML agent loading/parsing path:
17
16
  `DeclarativeLoaderError`, `ProviderLookupError`, `ProviderTypeMapping`) are
18
17
  **experimental** and may change or be removed in future versions without notice.
@@ -5,7 +5,7 @@
5
5
  Release stage:
6
6
 
7
7
  * The declarative-workflows surface (``WorkflowFactory``, executors, handlers,
8
- etc.) is at release-candidate stability.
8
+ etc.) is stable.
9
9
  * The declarative-agents surface (``AgentFactory`` and the YAML agent
10
10
  loading/parsing path: ``DeclarativeLoaderError``, ``ProviderLookupError``,
11
11
  ``ProviderTypeMapping``) is *experimental* and may change or be removed in
@@ -0,0 +1,10 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ from enum import IntEnum
4
+
5
+
6
+ class FeatureIndex(IntEnum):
7
+ """Declarative-owned feature-usage indexes."""
8
+
9
+ DECLARATIVE_AGENT = 75
10
+ DECLARATIVE_WORKFLOW = 76
@@ -15,13 +15,15 @@ from agent_framework import (
15
15
  from agent_framework import (
16
16
  FunctionTool as AFFunctionTool,
17
17
  )
18
- from agent_framework._feature_stage import ( # type: ignore[reportPrivateUsage]
18
+ from agent_framework._feature_stage import (
19
19
  ExperimentalFeature,
20
20
  experimental,
21
21
  )
22
+ from agent_framework._telemetry import mark_feature_used
22
23
  from agent_framework.exceptions import AgentException
23
24
  from dotenv import load_dotenv
24
25
 
26
+ from ._feature_usage import FeatureIndex
25
27
  from ._models import (
26
28
  AnonymousConnection,
27
29
  ApiKeyConnection,
@@ -42,9 +44,9 @@ from ._models import (
42
44
  )
43
45
 
44
46
  if sys.version_info >= (3, 11):
45
- from typing import TypedDict # type: ignore # pragma: no cover
47
+ from typing import TypedDict # pragma: no cover
46
48
  else:
47
- from typing_extensions import TypedDict # type: ignore # pragma: no cover
49
+ from typing_extensions import TypedDict # pragma: no cover
48
50
 
49
51
 
50
52
  @experimental(feature_id=ExperimentalFeature.DECLARATIVE_AGENTS)
@@ -471,13 +473,15 @@ class AgentFactory:
471
473
  if output_schema := prompt_agent.outputSchema:
472
474
  chat_options["response_format"] = output_schema.to_json_schema()
473
475
  # Step 3: Create the agent instance
474
- return Agent(
476
+ agent = Agent(
475
477
  client=client,
476
478
  name=prompt_agent.name,
477
479
  description=prompt_agent.description,
478
480
  instructions=prompt_agent.instructions,
479
481
  default_options=chat_options, # type: ignore[arg-type]
480
482
  )
483
+ mark_feature_used(FeatureIndex.DECLARATIVE_AGENT)
484
+ return agent
481
485
 
482
486
  async def create_agent_from_yaml_path_async(self, yaml_path: str | Path) -> Agent:
483
487
  """Async version: Create a Agent from a YAML file path.
@@ -582,13 +586,15 @@ class AgentFactory:
582
586
  chat_options["tools"] = tools
583
587
  if output_schema := prompt_agent.outputSchema:
584
588
  chat_options["response_format"] = output_schema.to_json_schema()
585
- return Agent(
589
+ agent = Agent(
586
590
  client=client,
587
591
  name=prompt_agent.name,
588
592
  description=prompt_agent.description,
589
593
  instructions=prompt_agent.instructions,
590
594
  default_options=chat_options, # type: ignore[arg-type]
591
595
  )
596
+ mark_feature_used(FeatureIndex.DECLARATIVE_AGENT)
597
+ return agent
592
598
 
593
599
  async def _create_agent_with_provider(self, prompt_agent: PromptAgent, mapping: ProviderTypeMapping) -> Agent:
594
600
  """Create an Agent through a provider object that exposes ``create_agent``.
@@ -708,7 +714,7 @@ class AgentFactory:
708
714
  module = __import__(module_name, fromlist=[class_name])
709
715
  agent_class = getattr(module, class_name)
710
716
  setup_dict[mapping["model_field"]] = prompt_agent.model.id
711
- return agent_class(**setup_dict) # type: ignore[no-any-return]
717
+ return agent_class(**setup_dict)
712
718
 
713
719
  def _parse_chat_options(self, model: Model | None) -> dict[str, Any]:
714
720
  """Parse ModelOptions into chat options dictionary."""
@@ -753,7 +759,7 @@ class AgentFactory:
753
759
  for binding in tool_resource.bindings:
754
760
  if binding.name and (func := self.bindings.get(binding.name)):
755
761
  break
756
- return AFFunctionTool( # type: ignore
762
+ return AFFunctionTool(
757
763
  name=tool_resource.name, # type: ignore
758
764
  description=tool_resource.description, # type: ignore
759
765
  input_model=tool_resource.parameters.to_json_schema() if tool_resource.parameters else None,
@@ -5,7 +5,7 @@ import logging
5
5
  import os
6
6
  from collections.abc import MutableMapping
7
7
  from contextvars import ContextVar
8
- from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, overload
8
+ from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, cast, overload
9
9
 
10
10
  from agent_framework._serialization import SerializationMixin
11
11
 
@@ -121,7 +121,7 @@ class Property(SerializationMixin):
121
121
  # Only dispatch if we're being called on the base Property class
122
122
  if cls is not Property:
123
123
  # We're being called on a subclass, use the normal from_dict
124
- return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies) # type: ignore[attr-defined, no-any-return]
124
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
125
125
 
126
126
  # The YAML spec uses 'type' for the data type, but Property stores it as 'kind'
127
127
  if "type" in value:
@@ -135,7 +135,7 @@ class Property(SerializationMixin):
135
135
  if kind == "object":
136
136
  return ObjectProperty.from_dict(value, dependencies=dependencies)
137
137
  # Default to Property for kind="property" or empty
138
- return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies) # type: ignore[attr-defined, no-any-return]
138
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
139
139
 
140
140
 
141
141
  class ArrayProperty(Property):
@@ -203,6 +203,52 @@ class ObjectProperty(Property):
203
203
  self.properties = converted_properties
204
204
 
205
205
 
206
+ def _normalize_nested_schemas(node: dict[str, Any]) -> None:
207
+ """Recursively convert a node's nested schemas to JSON Schema form.
208
+
209
+ Nested schemas (array ``items``, object ``properties``) keep the declarative
210
+ shape after serialization: ``kind`` instead of ``type``, empty ``enum``
211
+ placeholders, and object properties as a list of ``{"name": ..., ...}``
212
+ entries. OpenAI rejects schemas whose nested nodes lack a ``type`` key, so
213
+ apply the same conversion the top-level properties loop performs.
214
+ """
215
+ items = node.get("items")
216
+ if isinstance(items, dict):
217
+ _normalize_schema_node(cast("dict[str, Any]", items))
218
+ props = node.get("properties")
219
+ if not isinstance(props, list):
220
+ return
221
+ # Serialized PropertySchema shape: [{"name": ..., "kind": ..., ...}, ...].
222
+ # Validate every element BEFORE mutating any, so an unexpected shape
223
+ # leaves the node fully untouched rather than half-converted.
224
+ if not all(isinstance(prop, dict) and "name" in prop for prop in cast("list[Any]", props)):
225
+ return
226
+ new_props: dict[str, Any] = {}
227
+ required_fields: list[str] = []
228
+ for prop in cast("list[dict[str, Any]]", props):
229
+ prop_name = prop.pop("name")
230
+ if prop.pop("required", False):
231
+ required_fields.append(prop_name)
232
+ _normalize_schema_node(prop)
233
+ new_props[prop_name] = prop
234
+ node["properties"] = new_props
235
+ if required_fields:
236
+ node["required"] = required_fields
237
+
238
+
239
+ def _normalize_schema_node(node: dict[str, Any]) -> None:
240
+ """Rename ``kind`` -> ``type``, drop empty ``enum``, and recurse into children."""
241
+ if "kind" in node:
242
+ node["type"] = node.pop("kind")
243
+ if not node.get("enum"):
244
+ node.pop("enum", None)
245
+ if node.get("type") == "object":
246
+ # OpenAI strict structured outputs require additionalProperties: false on
247
+ # every object node; chat clients only inject it at the schema root.
248
+ node.setdefault("additionalProperties", False)
249
+ _normalize_nested_schemas(node)
250
+
251
+
206
252
  class PropertySchema(SerializationMixin):
207
253
  """Object representing a property schema."""
208
254
 
@@ -235,7 +281,7 @@ class PropertySchema(SerializationMixin):
235
281
  # Filter out 'kind', 'type', 'name', and 'description' fields that may appear in YAML
236
282
  # but aren't PropertySchema params
237
283
  kwargs = {k: v for k, v in value.items() if k not in ("type", "kind", "name", "description")}
238
- return SerializationMixin.from_dict.__func__(cls, kwargs, dependencies=dependencies) # type: ignore[attr-defined, no-any-return]
284
+ return SerializationMixin.from_dict.__func__(cls, kwargs, dependencies=dependencies)
239
285
 
240
286
  def to_json_schema(self) -> dict[str, Any]:
241
287
  """Get a schema out of this PropertySchema to create pydantic models."""
@@ -244,13 +290,10 @@ class PropertySchema(SerializationMixin):
244
290
  required_fields: list[str] = []
245
291
  for prop in json_schema.get("properties", []):
246
292
  prop_name = prop.pop("name")
247
- prop["type"] = prop.pop("kind", None)
248
293
  # Convert property-level 'required' boolean to a top-level 'required' array
249
294
  if prop.pop("required", False):
250
295
  required_fields.append(prop_name)
251
- # Remove empty enum arrays
252
- if not prop.get("enum"):
253
- prop.pop("enum", None)
296
+ _normalize_schema_node(prop)
254
297
  new_props[prop_name] = prop
255
298
  json_schema["type"] = "object"
256
299
  json_schema["properties"] = new_props
@@ -287,26 +330,18 @@ class Connection(SerializationMixin):
287
330
  # Only dispatch if we're being called on the base Connection class
288
331
  if cls is not Connection:
289
332
  # We're being called on a subclass, use the normal from_dict
290
- return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies) # type: ignore[attr-defined, no-any-return]
333
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
291
334
 
292
335
  kind = value.get("kind", "").lower()
293
336
  if kind == "reference":
294
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
295
- ReferenceConnection, value, dependencies=dependencies
296
- )
337
+ return SerializationMixin.from_dict.__func__(ReferenceConnection, value, dependencies=dependencies)
297
338
  if kind == "remote":
298
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
299
- RemoteConnection, value, dependencies=dependencies
300
- )
339
+ return SerializationMixin.from_dict.__func__(RemoteConnection, value, dependencies=dependencies)
301
340
  if kind in ("key", "apikey"):
302
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
303
- ApiKeyConnection, value, dependencies=dependencies
304
- )
341
+ return SerializationMixin.from_dict.__func__(ApiKeyConnection, value, dependencies=dependencies)
305
342
  if kind == "anonymous":
306
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
307
- AnonymousConnection, value, dependencies=dependencies
308
- )
309
- return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies) # type: ignore[attr-defined, no-any-return]
343
+ return SerializationMixin.from_dict.__func__(AnonymousConnection, value, dependencies=dependencies)
344
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
310
345
 
311
346
 
312
347
  class ReferenceConnection(Connection):
@@ -525,13 +560,13 @@ class AgentDefinition(SerializationMixin):
525
560
  # Only dispatch if we're being called on the base AgentDefinition class
526
561
  if cls is not AgentDefinition:
527
562
  # We're being called on a subclass, use the normal from_dict
528
- return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies) # type: ignore[attr-defined, no-any-return]
563
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
529
564
 
530
565
  kind = value.get("kind", "")
531
566
  if kind == "Prompt" or kind == "Agent":
532
567
  return PromptAgent.from_dict(value, dependencies=dependencies)
533
568
  # Default to AgentDefinition
534
- return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies) # type: ignore[attr-defined, no-any-return]
569
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
535
570
 
536
571
 
537
572
  ToolT = TypeVar("ToolT", bound="Tool")
@@ -571,39 +606,25 @@ class Tool(SerializationMixin):
571
606
  # Only dispatch if we're being called on the base Tool class
572
607
  if cls is not Tool:
573
608
  # We're being called on a subclass, use the normal from_dict
574
- return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies) # type: ignore[attr-defined, no-any-return]
609
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
575
610
 
576
611
  kind = value.get("kind", "")
577
612
  if kind == "function":
578
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
579
- FunctionTool, value, dependencies=dependencies
580
- )
613
+ return SerializationMixin.from_dict.__func__(FunctionTool, value, dependencies=dependencies)
581
614
  if kind == "custom":
582
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
583
- CustomTool, value, dependencies=dependencies
584
- )
615
+ return SerializationMixin.from_dict.__func__(CustomTool, value, dependencies=dependencies)
585
616
  if kind == "web_search":
586
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
587
- WebSearchTool, value, dependencies=dependencies
588
- )
617
+ return SerializationMixin.from_dict.__func__(WebSearchTool, value, dependencies=dependencies)
589
618
  if kind == "file_search":
590
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
591
- FileSearchTool, value, dependencies=dependencies
592
- )
619
+ return SerializationMixin.from_dict.__func__(FileSearchTool, value, dependencies=dependencies)
593
620
  if kind == "mcp":
594
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
595
- McpTool, value, dependencies=dependencies
596
- )
621
+ return SerializationMixin.from_dict.__func__(McpTool, value, dependencies=dependencies)
597
622
  if kind == "openapi":
598
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
599
- OpenApiTool, value, dependencies=dependencies
600
- )
623
+ return SerializationMixin.from_dict.__func__(OpenApiTool, value, dependencies=dependencies)
601
624
  if kind == "code_interpreter":
602
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
603
- CodeInterpreterTool, value, dependencies=dependencies
604
- )
625
+ return SerializationMixin.from_dict.__func__(CodeInterpreterTool, value, dependencies=dependencies)
605
626
  # Default to base Tool class
606
- return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies) # type: ignore[attr-defined, no-any-return]
627
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
607
628
 
608
629
 
609
630
  class FunctionTool(Tool):
@@ -901,18 +922,14 @@ class Resource(SerializationMixin):
901
922
  # Only dispatch if we're being called on the base Resource class
902
923
  if cls is not Resource:
903
924
  # We're being called on a subclass, use the normal from_dict
904
- return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies) # type: ignore[attr-defined, no-any-return]
925
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
905
926
 
906
927
  kind = value.get("kind", "")
907
928
  if kind == "model":
908
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
909
- ModelResource, value, dependencies=dependencies
910
- )
929
+ return SerializationMixin.from_dict.__func__(ModelResource, value, dependencies=dependencies)
911
930
  if kind == "tool":
912
- return SerializationMixin.from_dict.__func__( # type: ignore[attr-defined, no-any-return]
913
- ToolResource, value, dependencies=dependencies
914
- )
915
- return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies) # type: ignore[attr-defined, no-any-return]
931
+ return SerializationMixin.from_dict.__func__(ToolResource, value, dependencies=dependencies)
932
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
916
933
 
917
934
 
918
935
  class ModelResource(Resource):
@@ -50,12 +50,12 @@ try:
50
50
  except (ImportError, RuntimeError):
51
51
  # ImportError: powerfx package not installed
52
52
  # RuntimeError: .NET runtime not available or misconfigured
53
- Engine = None # type: ignore[assignment, misc]
53
+ Engine = None
54
54
 
55
55
  if sys.version_info >= (3, 11):
56
- from typing import TypedDict # type: ignore # pragma: no cover
56
+ from typing import TypedDict # pragma: no cover
57
57
  else:
58
- from typing_extensions import TypedDict # type: ignore # pragma: no cover
58
+ from typing_extensions import TypedDict # pragma: no cover
59
59
 
60
60
 
61
61
  logger = logging.getLogger(__name__)
@@ -147,11 +147,11 @@ def discover_env_references(node: Any) -> set[str]:
147
147
  names.update(_ENV_REFERENCE_RE.findall(value))
148
148
  return
149
149
  if isinstance(value, Mapping):
150
- for inner in cast(Mapping[Any, Any], value).values(): # type: ignore[redundant-cast]
150
+ for inner in cast(Mapping[Any, Any], value).values():
151
151
  visit(inner)
152
152
  return
153
153
  if isinstance(value, list):
154
- for item in cast(list[Any], value): # type: ignore[redundant-cast]
154
+ for item in cast(list[Any], value):
155
155
  visit(item)
156
156
 
157
157
  visit(node)
@@ -243,7 +243,7 @@ def _make_powerfx_safe(value: Any) -> Any:
243
243
  return {str(k): _make_powerfx_safe(v) for k, v in value_dict.items()}
244
244
 
245
245
  if isinstance(value, list):
246
- value_list = cast(list[Any], value) # type: ignore[redundant-cast]
246
+ value_list = cast(list[Any], value)
247
247
  return [_make_powerfx_safe(item) for item in value_list]
248
248
 
249
249
  # Try to convert objects with __dict__ or dataclass-style attributes
@@ -581,10 +581,10 @@ class DeclarativeWorkflowState:
581
581
 
582
582
  original_culture = cast(Any, CultureInfo.CurrentCulture) # pyright: ignore[reportUnknownMemberType]
583
583
  try:
584
- CultureInfo.CurrentCulture = CultureInfo(_POWERFX_EVAL_LOCALE) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
584
+ CultureInfo.CurrentCulture = CultureInfo(_POWERFX_EVAL_LOCALE)
585
585
  return engine.eval(formula, symbols=symbols, locale=_POWERFX_EVAL_LOCALE)
586
586
  finally:
587
- CultureInfo.CurrentCulture = original_culture # pyright: ignore[reportUnknownMemberType]
587
+ CultureInfo.CurrentCulture = original_culture
588
588
  except ValueError as e:
589
589
  error_msg = str(e)
590
590
  # Handle undefined variable errors gracefully by returning None
@@ -780,7 +780,7 @@ class DeclarativeWorkflowState:
780
780
  """
781
781
  messages: Any = self.eval(f"={inner_expr}")
782
782
  if isinstance(messages, list) and messages:
783
- message_list = cast(list[Any], messages) # type: ignore[redundant-cast]
783
+ message_list = cast(list[Any], messages)
784
784
  last_msg: Any = message_list[-1]
785
785
  if isinstance(last_msg, dict):
786
786
  last_msg_dict = cast(dict[str, Any], last_msg)
@@ -791,7 +791,7 @@ class DeclarativeWorkflowState:
791
791
  # Message.text concatenates text from all TextContent items
792
792
  contents_obj = last_msg_dict.get("contents", [])
793
793
  if isinstance(contents_obj, list):
794
- contents = cast(list[Any], contents_obj) # type: ignore[redundant-cast]
794
+ contents = cast(list[Any], contents_obj)
795
795
  text_parts: list[str] = []
796
796
  for content in contents:
797
797
  if isinstance(content, dict):
@@ -1215,7 +1215,7 @@ class DeclarativeActionExecutor(Executor):
1215
1215
  state.set("System.LastMessageText", trigger)
1216
1216
  elif not isinstance(
1217
1217
  trigger,
1218
- (ActionTrigger, ActionComplete, ConditionResult, LoopIterationResult, LoopControl), # pyright: ignore[reportUnknownArgumentType]
1218
+ (ActionTrigger, ActionComplete, ConditionResult, LoopIterationResult, LoopControl),
1219
1219
  ):
1220
1220
  # Any other type - convert to string like .NET's DefaultTransform
1221
1221
  input_str = str(cast(Any, trigger))
@@ -433,7 +433,7 @@ class DeclarativeWorkflowBuilder:
433
433
 
434
434
  # Store the chain for later reference
435
435
  if first_executor is not None:
436
- first_executor._chain_executors = executors_in_chain # type: ignore[attr-defined]
436
+ first_executor._chain_executors = executors_in_chain
437
437
 
438
438
  return first_executor
439
439
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  """Agent invocation executors for declarative workflows.
4
4
 
5
- These executors handle invoking Azure AI Foundry agents and other AI agents,
5
+ These executors handle invoking Microsoft Foundry agents and other AI agents,
6
6
  supporting both streaming responses and human-in-loop patterns.
7
7
 
8
8
  Aligned with .NET's InvokeAzureAgentExecutor behavior including:
@@ -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:
@@ -371,7 +504,7 @@ def _normalize_variable_path(variable: str) -> str:
371
504
 
372
505
 
373
506
  class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
374
- """Executor that invokes an Azure AI Foundry agent.
507
+ """Executor that invokes a Microsoft Foundry agent.
375
508
 
376
509
  This executor supports both Python-style and .NET-style YAML schemas:
377
510
 
@@ -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
 
@@ -543,12 +545,12 @@ class ParseValueExecutor(DeclarativeActionExecutor):
543
545
  if value is None:
544
546
  return []
545
547
  if isinstance(value, list):
546
- return cast(list[Any], value) # type: ignore[redundant-cast]
548
+ return cast(list[Any], value)
547
549
  if isinstance(value, str):
548
550
  try:
549
551
  parsed = json.loads(value)
550
552
  if isinstance(parsed, list):
551
- return cast(list[Any], parsed) # type: ignore[redundant-cast]
553
+ return cast(list[Any], parsed)
552
554
  return [parsed]
553
555
  except json.JSONDecodeError:
554
556
  return [value]
@@ -135,7 +135,7 @@ def _format_outputs_for_send(parsed_results: list[Any]) -> str:
135
135
  if not parsed_results:
136
136
  return ""
137
137
  if all(isinstance(item, str) for item in parsed_results):
138
- return "\n".join(parsed_results) # type: ignore[arg-type]
138
+ return "\n".join(parsed_results)
139
139
  if len(parsed_results) == 1:
140
140
  return json.dumps(parsed_results[0], ensure_ascii=False)
141
141
  return json.dumps(parsed_results, ensure_ascii=False)
@@ -24,7 +24,9 @@ from agent_framework import (
24
24
  SupportsAgentRun,
25
25
  Workflow,
26
26
  )
27
+ from agent_framework._telemetry import mark_feature_used
27
28
 
29
+ from .._feature_usage import FeatureIndex
28
30
  from .._loader import AgentFactory
29
31
  from ._declarative_base import DeclarativeEnvConfig, discover_env_references
30
32
  from ._declarative_builder import DeclarativeWorkflowBuilder
@@ -471,6 +473,7 @@ class WorkflowFactory:
471
473
  len(graph_builder._executors), # type: ignore[reportPrivateUsage]
472
474
  )
473
475
 
476
+ mark_feature_used(FeatureIndex.DECLARATIVE_WORKFLOW)
474
477
  return workflow
475
478
 
476
479
  def _normalize_workflow_def(self, workflow_def: dict[str, Any]) -> dict[str, Any]:
@@ -181,7 +181,7 @@ class DefaultHttpRequestHandler:
181
181
  params=params,
182
182
  headers=headers or None,
183
183
  content=content,
184
- timeout=timeout, # type: ignore[arg-type]
184
+ timeout=timeout,
185
185
  )
186
186
 
187
187
  # Preserve multi-value headers (e.g. multiple Set-Cookie) as list[str].
@@ -52,7 +52,7 @@ def message_text(messages: Any) -> str:
52
52
  if isinstance(messages, list):
53
53
  # List of messages - concatenate all text
54
54
  texts: list[str] = []
55
- message_list = cast(list[Any], messages) # type: ignore[redundant-cast]
55
+ message_list = cast(list[Any], messages)
56
56
  for msg in message_list:
57
57
  if isinstance(msg, str):
58
58
  texts.append(msg)
@@ -284,7 +284,7 @@ class WorkflowState:
284
284
  if existing is None:
285
285
  self.set(path, [value])
286
286
  elif isinstance(existing, list):
287
- existing_list = cast(list[Any], existing) # type: ignore[redundant-cast]
287
+ existing_list = cast(list[Any], existing)
288
288
  existing_list.append(value)
289
289
  self.set(path, existing_list)
290
290
  else:
@@ -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.0rc2"
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"
@@ -12,7 +12,7 @@ urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=ta
12
12
  urls.issues = "https://github.com/microsoft/agent-framework/issues"
13
13
  classifiers = [
14
14
  "License :: OSI Approved :: MIT License",
15
- "Development Status :: 4 - Beta",
15
+ "Development Status :: 5 - Production/Stable",
16
16
  "Intended Audience :: Developers",
17
17
  "Programming Language :: Python :: 3",
18
18
  "Programming Language :: Python :: 3.10",
@@ -22,7 +22,7 @@ classifiers = [
22
22
  "Typing :: Typed",
23
23
  ]
24
24
  dependencies = [
25
- "agent-framework-core>=1.9.0,<2",
25
+ "agent-framework-core>=1.13.0,<2",
26
26
  "httpx>=0.27,<1",
27
27
  "powerfx>=0.0.32,<0.0.35; python_version < '3.14'",
28
28
  "pyyaml>=6.0,<7.0",