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.
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/PKG-INFO +5 -6
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/README.md +2 -3
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/__init__.py +1 -1
- agent_framework_declarative-1.0.2/agent_framework_declarative/_feature_usage.py +10 -0
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_loader.py +13 -7
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_models.py +72 -55
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_declarative_base.py +11 -11
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_declarative_builder.py +1 -1
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_agents.py +226 -93
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_basic.py +5 -3
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_mcp.py +1 -1
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_factory.py +3 -0
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_http_handler.py +1 -1
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_powerfx_functions.py +1 -1
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_state.py +1 -1
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/pyproject.toml +3 -3
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/LICENSE +0 -0
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/__init__.py +0 -0
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_errors.py +0 -0
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_control_flow.py +0 -0
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_external_input.py +0 -0
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_http.py +0 -0
- {agent_framework_declarative-1.0.0rc2 → agent_framework_declarative-1.0.2}/agent_framework_declarative/_workflows/_executors_tools.py +0 -0
- {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.
|
|
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 ::
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
@@ -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 (
|
|
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 #
|
|
47
|
+
from typing import TypedDict # pragma: no cover
|
|
46
48
|
else:
|
|
47
|
-
from typing_extensions import TypedDict #
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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(
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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__(
|
|
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__(
|
|
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__(
|
|
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__(
|
|
307
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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__(
|
|
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__(
|
|
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__(
|
|
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__(
|
|
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__(
|
|
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__(
|
|
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__(
|
|
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)
|
|
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)
|
|
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__(
|
|
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__(
|
|
913
|
-
|
|
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
|
|
53
|
+
Engine = None
|
|
54
54
|
|
|
55
55
|
if sys.version_info >= (3, 11):
|
|
56
|
-
from typing import TypedDict #
|
|
56
|
+
from typing import TypedDict # pragma: no cover
|
|
57
57
|
else:
|
|
58
|
-
from typing_extensions import TypedDict #
|
|
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():
|
|
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):
|
|
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)
|
|
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)
|
|
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
|
|
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)
|
|
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)
|
|
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),
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
@@ -371,7 +504,7 @@ def _normalize_variable_path(variable: str) -> str:
|
|
|
371
504
|
|
|
372
505
|
|
|
373
506
|
class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
|
|
374
|
-
"""Executor that invokes
|
|
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")
|
|
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)
|
|
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)
|
|
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)
|
|
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,
|
|
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)
|
|
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)
|
|
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.
|
|
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 ::
|
|
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.
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|