agent-framework-declarative 1.3.0__tar.gz → 1.4.0__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.3.0 → agent_framework_declarative-1.4.0}/PKG-INFO +3 -2
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_declarative_base.py +17 -80
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_executors_basic.py +2 -1
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_executors_mcp.py +3 -3
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_mcp_handler.py +24 -10
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_powerfx_functions.py +147 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_state.py +6 -6
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/pyproject.toml +3 -2
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/LICENSE +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/README.md +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/__init__.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_feature_usage.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_loader.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_models.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/__init__.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_declarative_builder.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_errors.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_executors_agents.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_executors_control_flow.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_executors_external_input.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_executors_http.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_executors_tools.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_factory.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_http_handler.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_powerfx_limits.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/_workflows/_state_path.py +0 -0
- {agent_framework_declarative-1.3.0 → agent_framework_declarative-1.4.0}/agent_framework_declarative/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-framework-declarative
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
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
|
|
@@ -15,8 +15,9 @@ 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.23.0,<2
|
|
19
19
|
Requires-Dist: httpx>=0.27,<0.29
|
|
20
|
+
Requires-Dist: httpx2>=2.0.0,<3
|
|
20
21
|
Requires-Dist: powerfx>=0.0.32,<0.0.35; python_version < '3.14'
|
|
21
22
|
Requires-Dist: pyyaml>=6.0,<7.0
|
|
22
23
|
Project-URL: homepage, https://aka.ms/agent-framework
|
|
@@ -46,6 +46,10 @@ from agent_framework import (
|
|
|
46
46
|
from agent_framework._workflows._state import State
|
|
47
47
|
|
|
48
48
|
from ._errors import DeclarativeWorkflowError
|
|
49
|
+
from ._powerfx_functions import (
|
|
50
|
+
_iter_powerfx_expression_indices, # pyright: ignore[reportPrivateUsage]
|
|
51
|
+
whole_call_args,
|
|
52
|
+
)
|
|
49
53
|
from ._powerfx_limits import _PowerFxStateBudget, _validate_powerfx_state # pyright: ignore[reportPrivateUsage]
|
|
50
54
|
from ._state_path import _is_safe_path_segment # pyright: ignore[reportPrivateUsage]
|
|
51
55
|
|
|
@@ -68,73 +72,6 @@ logger = logging.getLogger(__name__)
|
|
|
68
72
|
_ENV_REFERENCE_RE = re.compile(r"\bEnv\.([A-Za-z_][A-Za-z0-9_]*)")
|
|
69
73
|
|
|
70
74
|
|
|
71
|
-
def _skip_powerfx_opaque_token(formula: str, start: int) -> int:
|
|
72
|
-
"""Return the end of an ordinary quoted token or comment, or start if neither."""
|
|
73
|
-
quote = formula[start]
|
|
74
|
-
if quote in ('"', "'"):
|
|
75
|
-
pos = start + 1
|
|
76
|
-
while pos < len(formula):
|
|
77
|
-
if formula[pos] == quote:
|
|
78
|
-
if pos + 1 < len(formula) and formula[pos + 1] == quote:
|
|
79
|
-
pos += 2
|
|
80
|
-
continue
|
|
81
|
-
return pos + 1
|
|
82
|
-
pos += 1
|
|
83
|
-
return pos
|
|
84
|
-
if formula.startswith("//", start):
|
|
85
|
-
pos = start + 2
|
|
86
|
-
while pos < len(formula) and formula[pos] not in "\r\n":
|
|
87
|
-
pos += 1
|
|
88
|
-
return pos
|
|
89
|
-
if formula.startswith("/*", start):
|
|
90
|
-
end = formula.find("*/", start + 2)
|
|
91
|
-
return len(formula) if end == -1 else end + 2
|
|
92
|
-
return start
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _iter_powerfx_expression_indices(formula: str) -> Iterator[int]:
|
|
96
|
-
"""Yield code positions, excluding literals/comments but including interpolation expressions."""
|
|
97
|
-
# None denotes interpolated text; integers track record braces in expression sections.
|
|
98
|
-
scopes: list[int | None] = [0]
|
|
99
|
-
cursor = 0
|
|
100
|
-
while cursor < len(formula):
|
|
101
|
-
depth = scopes[-1]
|
|
102
|
-
char = formula[cursor]
|
|
103
|
-
if depth is None:
|
|
104
|
-
if formula.startswith(('""', "{{", "}}"), cursor):
|
|
105
|
-
cursor += 2
|
|
106
|
-
continue
|
|
107
|
-
if char == "{":
|
|
108
|
-
scopes.append(0)
|
|
109
|
-
elif char == '"':
|
|
110
|
-
scopes.pop()
|
|
111
|
-
cursor += 1
|
|
112
|
-
continue
|
|
113
|
-
|
|
114
|
-
if formula.startswith('$"', cursor):
|
|
115
|
-
scopes.append(None)
|
|
116
|
-
cursor += 2
|
|
117
|
-
continue
|
|
118
|
-
|
|
119
|
-
token_end = _skip_powerfx_opaque_token(formula, cursor)
|
|
120
|
-
if token_end != cursor:
|
|
121
|
-
cursor = token_end
|
|
122
|
-
continue
|
|
123
|
-
|
|
124
|
-
if char == "{":
|
|
125
|
-
scopes[-1] = depth + 1
|
|
126
|
-
elif char == "}":
|
|
127
|
-
if depth > 0:
|
|
128
|
-
scopes[-1] = depth - 1
|
|
129
|
-
elif len(scopes) > 1:
|
|
130
|
-
scopes.pop()
|
|
131
|
-
cursor += 1
|
|
132
|
-
continue
|
|
133
|
-
|
|
134
|
-
yield cursor
|
|
135
|
-
cursor += 1
|
|
136
|
-
|
|
137
|
-
|
|
138
75
|
@dataclass(frozen=True)
|
|
139
76
|
class DeclarativeEnvConfig:
|
|
140
77
|
"""Configuration that populates the PowerFx ``Env`` symbol for a workflow.
|
|
@@ -721,14 +658,14 @@ class DeclarativeWorkflowState:
|
|
|
721
658
|
|
|
722
659
|
Returns None if the formula is not a custom function call.
|
|
723
660
|
"""
|
|
724
|
-
|
|
661
|
+
formula = formula.strip()
|
|
725
662
|
|
|
726
663
|
# Concat/Concatenate - string concatenation
|
|
727
664
|
# In standard PowerFx, Concatenate is for strings, Concat is for tables.
|
|
728
665
|
# Copilot Studio uses Concat for strings, so we support both.
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
666
|
+
args_str = whole_call_args(formula, "Concat", "Concatenate")
|
|
667
|
+
# Empty arguments fall through so PowerFx reports the arity error.
|
|
668
|
+
if args_str:
|
|
732
669
|
# Parse comma-separated arguments (handling nested parentheses)
|
|
733
670
|
args = self._parse_function_args(args_str)
|
|
734
671
|
evaluated_args: list[str] = []
|
|
@@ -747,24 +684,24 @@ class DeclarativeWorkflowState:
|
|
|
747
684
|
return "".join(evaluated_args)
|
|
748
685
|
|
|
749
686
|
# UserMessage(expr) - creates a user message dict
|
|
750
|
-
|
|
751
|
-
if
|
|
752
|
-
inner_expr =
|
|
687
|
+
inner_args = whole_call_args(formula, "UserMessage")
|
|
688
|
+
if inner_args:
|
|
689
|
+
inner_expr = inner_args.strip()
|
|
753
690
|
# Evaluate the inner expression
|
|
754
691
|
text = self.eval(f"={inner_expr}")
|
|
755
692
|
return {"role": "user", "text": str(text) if text else ""}
|
|
756
693
|
|
|
757
694
|
# AgentMessage(expr) - creates an assistant message dict
|
|
758
|
-
|
|
759
|
-
if
|
|
760
|
-
inner_expr =
|
|
695
|
+
inner_args = whole_call_args(formula, "AgentMessage")
|
|
696
|
+
if inner_args:
|
|
697
|
+
inner_expr = inner_args.strip()
|
|
761
698
|
text = self.eval(f"={inner_expr}")
|
|
762
699
|
return {"role": "assistant", "text": str(text) if text else ""}
|
|
763
700
|
|
|
764
701
|
# MessageText(expr) - extracts text from the last message
|
|
765
|
-
|
|
766
|
-
if
|
|
767
|
-
inner_expr =
|
|
702
|
+
inner_args = whole_call_args(formula, "MessageText")
|
|
703
|
+
if inner_args:
|
|
704
|
+
inner_expr = inner_args.strip()
|
|
768
705
|
# Reuse the helper method for consistent text extraction
|
|
769
706
|
return self._eval_and_replace_message_text(inner_expr)
|
|
770
707
|
|
|
@@ -53,7 +53,8 @@ class SetValueExecutor(DeclarativeActionExecutor):
|
|
|
53
53
|
"""Handle the SetValue action."""
|
|
54
54
|
state = await self._ensure_state_initialized(ctx, trigger)
|
|
55
55
|
|
|
56
|
-
path
|
|
56
|
+
# The builder accepts 'variable' in place of 'path' for SetValue
|
|
57
|
+
path = self._action_def.get("path") or _get_variable_path(self._action_def)
|
|
57
58
|
value = self._action_def.get("value")
|
|
58
59
|
|
|
59
60
|
if path:
|
|
@@ -32,7 +32,7 @@ from collections.abc import Mapping
|
|
|
32
32
|
from dataclasses import dataclass, field
|
|
33
33
|
from typing import Any
|
|
34
34
|
|
|
35
|
-
import httpx
|
|
35
|
+
import httpx2 as httpx
|
|
36
36
|
from agent_framework import (
|
|
37
37
|
Content,
|
|
38
38
|
Message,
|
|
@@ -503,10 +503,10 @@ class InvokeMcpToolActionExecutor(DeclarativeActionExecutor):
|
|
|
503
503
|
)
|
|
504
504
|
except Exception as exc:
|
|
505
505
|
try:
|
|
506
|
-
from mcp.shared.exceptions import
|
|
506
|
+
from mcp.shared.exceptions import MCPError
|
|
507
507
|
except ImportError: # pragma: no cover - mcp is a hard dep
|
|
508
508
|
raise
|
|
509
|
-
if isinstance(exc,
|
|
509
|
+
if isinstance(exc, MCPError):
|
|
510
510
|
message = str(exc) or type(exc).__name__
|
|
511
511
|
return MCPToolResult(
|
|
512
512
|
outputs=[Content.from_text(f"Error: {message}")],
|
|
@@ -27,6 +27,7 @@ must trust the MCP server they invoke.
|
|
|
27
27
|
from __future__ import annotations
|
|
28
28
|
|
|
29
29
|
import asyncio
|
|
30
|
+
import contextlib
|
|
30
31
|
import hashlib
|
|
31
32
|
import json
|
|
32
33
|
import logging
|
|
@@ -36,7 +37,7 @@ from contextvars import ContextVar, Token
|
|
|
36
37
|
from dataclasses import dataclass, field
|
|
37
38
|
from typing import TYPE_CHECKING, Any, ClassVar, Protocol, cast, runtime_checkable
|
|
38
39
|
|
|
39
|
-
import httpx
|
|
40
|
+
import httpx2 as httpx
|
|
40
41
|
|
|
41
42
|
if TYPE_CHECKING:
|
|
42
43
|
from agent_framework import Content
|
|
@@ -345,10 +346,10 @@ class DefaultMCPToolHandler:
|
|
|
345
346
|
# Be defensive about MCP errors that may bubble up without being
|
|
346
347
|
# wrapped in ToolExecutionException by custom parsers.
|
|
347
348
|
try:
|
|
348
|
-
from mcp.shared.exceptions import
|
|
349
|
+
from mcp.shared.exceptions import MCPError
|
|
349
350
|
except ImportError: # pragma: no cover - mcp is a hard dep but stay defensive
|
|
350
351
|
raise
|
|
351
|
-
if isinstance(exc,
|
|
352
|
+
if isinstance(exc, MCPError):
|
|
352
353
|
message = str(exc) or type(exc).__name__
|
|
353
354
|
return MCPToolResult(
|
|
354
355
|
outputs=[Content.from_text(f"Error: {message}")],
|
|
@@ -369,7 +370,7 @@ class DefaultMCPToolHandler:
|
|
|
369
370
|
async def _invoke_list_tools(entry: _CacheEntry) -> MCPToolResult:
|
|
370
371
|
"""Handle the reserved :attr:`LIST_TOOLS_TOOL_NAME` invocation.
|
|
371
372
|
|
|
372
|
-
Pages through
|
|
373
|
+
Pages through the normalized MCP connection (mirroring the pagination loop
|
|
373
374
|
in :meth:`agent_framework.MCPTool.load_tools`) and serialises the
|
|
374
375
|
full catalog as a single ``TextContent`` containing JSON of shape
|
|
375
376
|
``{"tools": [{name, description, inputSchema, outputSchema}, ...]}``.
|
|
@@ -383,8 +384,16 @@ class DefaultMCPToolHandler:
|
|
|
383
384
|
"""
|
|
384
385
|
from agent_framework import Content
|
|
385
386
|
|
|
386
|
-
|
|
387
|
-
|
|
387
|
+
require_connection: Any = getattr(entry.tool, "_require_connection", None)
|
|
388
|
+
connection: Any | None = None
|
|
389
|
+
session: Any | None = None
|
|
390
|
+
if callable(require_connection):
|
|
391
|
+
with contextlib.suppress(RuntimeError):
|
|
392
|
+
connection = require_connection()
|
|
393
|
+
else:
|
|
394
|
+
session = getattr(entry.tool, "session", None)
|
|
395
|
+
|
|
396
|
+
if connection is None and session is None:
|
|
388
397
|
message = "MCP session is not connected; cannot list tools."
|
|
389
398
|
return MCPToolResult(
|
|
390
399
|
outputs=[Content.from_text(f"Error: {message}")],
|
|
@@ -398,9 +407,14 @@ class DefaultMCPToolHandler:
|
|
|
398
407
|
collected: list[Any] = []
|
|
399
408
|
params: mcp_types.PaginatedRequestParams | None = None
|
|
400
409
|
while True:
|
|
401
|
-
|
|
410
|
+
if connection is not None:
|
|
411
|
+
tool_list = await connection.list_tools_page(params)
|
|
412
|
+
elif session is not None:
|
|
413
|
+
tool_list = await session.list_tools(params=params)
|
|
414
|
+
else: # pragma: no cover - guarded before entering the loop
|
|
415
|
+
raise RuntimeError("MCP connection became unavailable while listing tools.")
|
|
402
416
|
collected.extend(tool_list.tools)
|
|
403
|
-
next_cursor = getattr(tool_list, "nextCursor", None)
|
|
417
|
+
next_cursor = getattr(tool_list, "next_cursor", getattr(tool_list, "nextCursor", None))
|
|
404
418
|
if not next_cursor:
|
|
405
419
|
break
|
|
406
420
|
params = mcp_types.PaginatedRequestParams(cursor=next_cursor)
|
|
@@ -410,8 +424,8 @@ class DefaultMCPToolHandler:
|
|
|
410
424
|
{
|
|
411
425
|
"name": tool.name,
|
|
412
426
|
"description": tool.description,
|
|
413
|
-
"inputSchema": tool
|
|
414
|
-
"outputSchema": tool
|
|
427
|
+
"inputSchema": getattr(tool, "input_schema", getattr(tool, "inputSchema", None)),
|
|
428
|
+
"outputSchema": getattr(tool, "output_schema", getattr(tool, "outputSchema", None)),
|
|
415
429
|
}
|
|
416
430
|
for tool in collected
|
|
417
431
|
],
|
|
@@ -12,9 +12,156 @@ or registered with the PowerFx engine when it is available.
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
from collections.abc import Iterator
|
|
15
16
|
from typing import Any, cast
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
def _skip_powerfx_opaque_token(formula: str, start: int) -> int:
|
|
20
|
+
"""Return the end of an ordinary quoted token or comment, or start if neither."""
|
|
21
|
+
quote = formula[start]
|
|
22
|
+
if quote in ('"', "'"):
|
|
23
|
+
pos = start + 1
|
|
24
|
+
while pos < len(formula):
|
|
25
|
+
if formula[pos] == quote:
|
|
26
|
+
if pos + 1 < len(formula) and formula[pos + 1] == quote:
|
|
27
|
+
pos += 2
|
|
28
|
+
continue
|
|
29
|
+
return pos + 1
|
|
30
|
+
pos += 1
|
|
31
|
+
return pos
|
|
32
|
+
if formula.startswith("//", start):
|
|
33
|
+
pos = start + 2
|
|
34
|
+
while pos < len(formula) and formula[pos] not in "\r\n":
|
|
35
|
+
pos += 1
|
|
36
|
+
return pos
|
|
37
|
+
if formula.startswith("/*", start):
|
|
38
|
+
end = formula.find("*/", start + 2)
|
|
39
|
+
# An unterminated /* is not a complete comment: leave it as code so a
|
|
40
|
+
# trailing one is not mistaken for trivia following a complete call.
|
|
41
|
+
return start if end == -1 else end + 2
|
|
42
|
+
return start
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _iter_powerfx_expression_indices(formula: str) -> Iterator[int]:
|
|
46
|
+
"""Yield code positions, excluding literals/comments but including interpolation expressions."""
|
|
47
|
+
# None denotes interpolated text; integers track record braces in expression sections.
|
|
48
|
+
scopes: list[int | None] = [0]
|
|
49
|
+
cursor = 0
|
|
50
|
+
while cursor < len(formula):
|
|
51
|
+
depth = scopes[-1]
|
|
52
|
+
char = formula[cursor]
|
|
53
|
+
if depth is None:
|
|
54
|
+
if formula.startswith(('""', "{{", "}}"), cursor):
|
|
55
|
+
cursor += 2
|
|
56
|
+
continue
|
|
57
|
+
if char == "{":
|
|
58
|
+
scopes.append(0)
|
|
59
|
+
elif char == '"':
|
|
60
|
+
scopes.pop()
|
|
61
|
+
cursor += 1
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
if formula.startswith('$"', cursor):
|
|
65
|
+
scopes.append(None)
|
|
66
|
+
cursor += 2
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
token_end = _skip_powerfx_opaque_token(formula, cursor)
|
|
70
|
+
if token_end != cursor:
|
|
71
|
+
cursor = token_end
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if char == "{":
|
|
75
|
+
scopes[-1] = depth + 1
|
|
76
|
+
elif char == "}":
|
|
77
|
+
if depth > 0:
|
|
78
|
+
scopes[-1] = depth - 1
|
|
79
|
+
elif len(scopes) > 1:
|
|
80
|
+
scopes.pop()
|
|
81
|
+
cursor += 1
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
yield cursor
|
|
85
|
+
cursor += 1
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _after_trailing_trivia(formula: str, start: int) -> int:
|
|
89
|
+
"""Return the index after trailing whitespace and complete comments.
|
|
90
|
+
|
|
91
|
+
Unlike the expression walker, this never skips a string literal: a quoted
|
|
92
|
+
token after the call's own closing paren is trailing code, not trivia, so it
|
|
93
|
+
stops the scan and leaves the formula to PowerFx.
|
|
94
|
+
"""
|
|
95
|
+
index = start
|
|
96
|
+
while index < len(formula):
|
|
97
|
+
if formula[index].isspace():
|
|
98
|
+
index += 1
|
|
99
|
+
continue
|
|
100
|
+
if formula.startswith(("//", "/*"), index):
|
|
101
|
+
token_end = _skip_powerfx_opaque_token(formula, index)
|
|
102
|
+
if token_end == index:
|
|
103
|
+
# Unterminated /* is not a complete comment, so real code follows.
|
|
104
|
+
break
|
|
105
|
+
index = token_end
|
|
106
|
+
continue
|
|
107
|
+
break
|
|
108
|
+
return index
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def whole_call_args(formula: str, *names: str) -> str | None:
|
|
112
|
+
"""Return the argument text when ``formula`` is exactly one ``Name(...)`` call.
|
|
113
|
+
|
|
114
|
+
A custom-function handler must not fire for a formula that merely *starts*
|
|
115
|
+
with its name: the closing parenthesis has to be the call's own and nothing
|
|
116
|
+
but trivia may follow it. Otherwise a trailing operator is swallowed into the
|
|
117
|
+
argument list and the formula is evaluated as something the author never
|
|
118
|
+
wrote, instead of being left to PowerFx.
|
|
119
|
+
|
|
120
|
+
Parentheses inside string literals and comments are data, not delimiters, so
|
|
121
|
+
the scan walks the formula with the shared PowerFx expression walker: it
|
|
122
|
+
skips those tokens whole and counts only parentheses that are real code,
|
|
123
|
+
including those inside an interpolated string's ``{...}`` expression sections.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
formula: The stripped formula to inspect.
|
|
127
|
+
*names: Candidate function names, matched case-sensitively.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The text between the call's parentheses - an empty string for an
|
|
131
|
+
argument-less call - or None when ``formula`` is not a single complete
|
|
132
|
+
call to one of ``names``. Callers that require an argument check the
|
|
133
|
+
result for emptiness so the call still falls through to PowerFx, which
|
|
134
|
+
reports the arity error.
|
|
135
|
+
"""
|
|
136
|
+
for name in names:
|
|
137
|
+
prefix = f"{name}("
|
|
138
|
+
if not formula.startswith(prefix):
|
|
139
|
+
continue
|
|
140
|
+
positions = _iter_powerfx_expression_indices(formula)
|
|
141
|
+
depth = 0
|
|
142
|
+
close_index = -1
|
|
143
|
+
for index in positions:
|
|
144
|
+
char = formula[index]
|
|
145
|
+
if char == "(":
|
|
146
|
+
depth += 1
|
|
147
|
+
elif char == ")":
|
|
148
|
+
depth -= 1
|
|
149
|
+
if depth == 0:
|
|
150
|
+
close_index = index
|
|
151
|
+
break
|
|
152
|
+
if close_index == -1:
|
|
153
|
+
# Unbalanced: the call never closes, so it is not a whole call.
|
|
154
|
+
return None
|
|
155
|
+
# Only whitespace and complete comments may follow the call's own closing
|
|
156
|
+
# paren. The suffix scan deliberately does not reuse the literal-skipping
|
|
157
|
+
# walker: a trailing string literal (or any other code) there means the
|
|
158
|
+
# formula is a larger expression the caller must not claim.
|
|
159
|
+
if _after_trailing_trivia(formula, close_index + 1) != len(formula):
|
|
160
|
+
return None
|
|
161
|
+
return formula[len(prefix) : close_index]
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
18
165
|
def message_text(messages: Any) -> str:
|
|
19
166
|
"""Extract text content from a message or list of messages.
|
|
20
167
|
|
|
@@ -441,7 +441,7 @@ class WorkflowState:
|
|
|
441
441
|
Returns:
|
|
442
442
|
The evaluated result
|
|
443
443
|
"""
|
|
444
|
-
from ._powerfx_functions import CUSTOM_FUNCTIONS
|
|
444
|
+
from ._powerfx_functions import CUSTOM_FUNCTIONS, whole_call_args
|
|
445
445
|
|
|
446
446
|
formula = formula.strip()
|
|
447
447
|
|
|
@@ -452,15 +452,15 @@ class WorkflowState:
|
|
|
452
452
|
return not bool(result)
|
|
453
453
|
|
|
454
454
|
# Handle Not() function
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
result = self._eval_simple(
|
|
455
|
+
not_args = whole_call_args(formula, "Not")
|
|
456
|
+
if not_args is not None:
|
|
457
|
+
result = self._eval_simple(not_args.strip())
|
|
458
458
|
return not bool(result)
|
|
459
459
|
|
|
460
460
|
# Handle function calls
|
|
461
461
|
for func_name, func in CUSTOM_FUNCTIONS.items():
|
|
462
|
-
|
|
463
|
-
|
|
462
|
+
args_str = whole_call_args(formula, func_name)
|
|
463
|
+
if args_str is not None:
|
|
464
464
|
# Simple argument parsing (doesn't handle nested calls well)
|
|
465
465
|
args = self._parse_function_args(args_str)
|
|
466
466
|
evaluated_args = [self._eval_simple(arg) if isinstance(arg, str) else arg for arg in args]
|
|
@@ -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.
|
|
7
|
+
version = "1.4.0"
|
|
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"
|
|
@@ -22,8 +22,9 @@ classifiers = [
|
|
|
22
22
|
"Typing :: Typed",
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
|
-
"agent-framework-core>=1.
|
|
25
|
+
"agent-framework-core>=1.23.0,<2",
|
|
26
26
|
"httpx>=0.27,<0.29",
|
|
27
|
+
"httpx2>=2.0.0,<3",
|
|
27
28
|
"powerfx>=0.0.32,<0.0.35; python_version < '3.14'",
|
|
28
29
|
"pyyaml>=6.0,<7.0",
|
|
29
30
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|