agent-framework-orchestrations 1.3.0__tar.gz → 1.3.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_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/PKG-INFO +4 -2
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_concurrent.py +10 -1
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_group_chat.py +15 -3
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_handoff.py +20 -3
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_magentic.py +40 -11
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_orchestration_request_info.py +18 -0
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_sequential.py +6 -1
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/pyproject.toml +3 -2
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/LICENSE +0 -0
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/README.md +0 -0
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/__init__.py +0 -0
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_base_group_chat_orchestrator.py +0 -0
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_feature_usage.py +0 -0
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_orchestration_state.py +0 -0
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_orchestrator_helpers.py +0 -0
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_participant_output_config.py +0 -0
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_workflow_builder.py +0 -0
- {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: agent-framework-orchestrations
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: Orchestration patterns for Microsoft Agent Framework. Includes SequentialBuilder, ConcurrentBuilder, HandoffBuilder, GroupChatBuilder, and MagenticBuilder.
|
|
5
5
|
Author-email: Microsoft <af-support@microsoft.com>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
17
18
|
Classifier: Typing :: Typed
|
|
18
19
|
License-File: LICENSE
|
|
19
20
|
Requires-Dist: agent-framework-core>=1.19.0,<2
|
|
@@ -21,6 +22,7 @@ Project-URL: homepage, https://aka.ms/agent-framework
|
|
|
21
22
|
Project-URL: issues, https://github.com/microsoft/agent-framework/issues
|
|
22
23
|
Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
|
|
23
24
|
Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
|
|
25
|
+
Import-Name: agent_framework_orchestrations
|
|
24
26
|
|
|
25
27
|
# Agent Framework Orchestrations
|
|
26
28
|
|
|
@@ -18,7 +18,7 @@ from agent_framework._workflows._workflow_context import WorkflowContext
|
|
|
18
18
|
from typing_extensions import Never
|
|
19
19
|
|
|
20
20
|
from ._feature_usage import FeatureIndex
|
|
21
|
-
from ._orchestration_request_info import AgentApprovalExecutor
|
|
21
|
+
from ._orchestration_request_info import AgentApprovalExecutor, validate_request_info_filter
|
|
22
22
|
from ._participant_output_config import (
|
|
23
23
|
UNSET,
|
|
24
24
|
_coalesce_output_from, # pyright: ignore[reportPrivateUsage]
|
|
@@ -172,6 +172,10 @@ class _CallbackAggregator(Executor):
|
|
|
172
172
|
else:
|
|
173
173
|
ret = await asyncio.to_thread(self._callback, results)
|
|
174
174
|
|
|
175
|
+
# Async callable objects and wrappers returning a coroutine are not coroutine functions
|
|
176
|
+
if inspect.isawaitable(ret):
|
|
177
|
+
ret = await ret
|
|
178
|
+
|
|
175
179
|
# If the callback returned a value, finalize the workflow with it
|
|
176
180
|
if ret is not None:
|
|
177
181
|
await ctx.yield_output(ret)
|
|
@@ -382,6 +386,11 @@ class ConcurrentBuilder:
|
|
|
382
386
|
else:
|
|
383
387
|
raise TypeError(f"Participants must be SupportsAgentRun or Executor instances. Got {type(p).__name__}.")
|
|
384
388
|
|
|
389
|
+
if self._request_info_enabled:
|
|
390
|
+
validate_request_info_filter(
|
|
391
|
+
self._request_info_filter,
|
|
392
|
+
[resolve_agent_id(p) for p in participants if not isinstance(p, Executor)],
|
|
393
|
+
)
|
|
385
394
|
return executors
|
|
386
395
|
|
|
387
396
|
def build(self) -> Workflow:
|
|
@@ -51,7 +51,7 @@ from ._base_group_chat_orchestrator import (
|
|
|
51
51
|
TerminationCondition,
|
|
52
52
|
)
|
|
53
53
|
from ._feature_usage import FeatureIndex
|
|
54
|
-
from ._orchestration_request_info import AgentApprovalExecutor
|
|
54
|
+
from ._orchestration_request_info import AgentApprovalExecutor, validate_request_info_filter
|
|
55
55
|
from ._orchestrator_helpers import clean_conversation_for_handoff, extract_markdown_fence_bodies
|
|
56
56
|
from ._participant_output_config import (
|
|
57
57
|
UNSET,
|
|
@@ -519,8 +519,15 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator):
|
|
|
519
519
|
# Parse and validate the structured output
|
|
520
520
|
agent_orchestration_output = self._parse_agent_output(agent_response)
|
|
521
521
|
|
|
522
|
-
if not agent_orchestration_output.terminate
|
|
523
|
-
|
|
522
|
+
if not agent_orchestration_output.terminate:
|
|
523
|
+
next_speaker = agent_orchestration_output.next_speaker
|
|
524
|
+
if not next_speaker:
|
|
525
|
+
raise ValueError("next_speaker must be provided if not terminating the conversation.")
|
|
526
|
+
if next_speaker not in self._participant_registry.participants:
|
|
527
|
+
raise ValueError(
|
|
528
|
+
f"Orchestrator agent selected unknown participant '{next_speaker}'. "
|
|
529
|
+
f"Valid participants: {', '.join(self._participant_registry.participants)}."
|
|
530
|
+
)
|
|
524
531
|
|
|
525
532
|
return agent_orchestration_output
|
|
526
533
|
|
|
@@ -1023,6 +1030,11 @@ class GroupChatBuilder:
|
|
|
1023
1030
|
f"Participants must be SupportsAgentRun or Executor instances. Got {type(participant).__name__}."
|
|
1024
1031
|
)
|
|
1025
1032
|
|
|
1033
|
+
if self._request_info_enabled:
|
|
1034
|
+
validate_request_info_filter(
|
|
1035
|
+
self._request_info_filter,
|
|
1036
|
+
[resolve_agent_id(p) for p in participants if not isinstance(p, Executor)],
|
|
1037
|
+
)
|
|
1026
1038
|
return executors
|
|
1027
1039
|
|
|
1028
1040
|
def build(self) -> Workflow:
|
|
@@ -32,6 +32,7 @@ Key properties:
|
|
|
32
32
|
import inspect
|
|
33
33
|
import json
|
|
34
34
|
import logging
|
|
35
|
+
import re
|
|
35
36
|
import sys
|
|
36
37
|
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
37
38
|
from copy import deepcopy
|
|
@@ -122,9 +123,17 @@ class HandoffConfiguration:
|
|
|
122
123
|
return hash(self.target_id)
|
|
123
124
|
|
|
124
125
|
|
|
126
|
+
_INVALID_TOOL_NAME_CHARACTERS = re.compile(r"[^a-zA-Z0-9_-]")
|
|
127
|
+
_MAX_TOOL_NAME_LENGTH = 64
|
|
128
|
+
|
|
129
|
+
|
|
125
130
|
def get_handoff_tool_name(target_id: str) -> str:
|
|
126
|
-
"""Get the standardized handoff tool name for a given target agent ID.
|
|
127
|
-
|
|
131
|
+
"""Get the standardized handoff tool name for a given target agent ID.
|
|
132
|
+
|
|
133
|
+
Characters that model providers reject in tool names, such as spaces, are replaced with underscores,
|
|
134
|
+
and the name is cut to the 64 characters that providers allow.
|
|
135
|
+
"""
|
|
136
|
+
return f"handoff_to_{_INVALID_TOOL_NAME_CHARACTERS.sub('_', target_id)}"[:_MAX_TOOL_NAME_LENGTH]
|
|
128
137
|
|
|
129
138
|
|
|
130
139
|
HANDOFF_FUNCTION_RESULT_KEY = "handoff_to"
|
|
@@ -266,6 +275,9 @@ class HandoffAgentExecutor(AgentExecutor):
|
|
|
266
275
|
autonomous_mode_prompt: Prompt to provide to the agent when continuing in autonomous mode.
|
|
267
276
|
This will guide the agent in the absence of user input.
|
|
268
277
|
autonomous_mode_turn_limit: Maximum number of autonomous turns before requesting user input.
|
|
278
|
+
``0`` returns control to the user as soon as the agent responds
|
|
279
|
+
without a handoff; the default limit is only used when this is
|
|
280
|
+
left as ``None``.
|
|
269
281
|
"""
|
|
270
282
|
self._auto_handoff_middleware = _AutoHandoffMiddleware(handoffs)
|
|
271
283
|
cloned_agent = self._prepare_agent_with_handoffs(agent, handoffs, self._auto_handoff_middleware)
|
|
@@ -279,7 +291,9 @@ class HandoffAgentExecutor(AgentExecutor):
|
|
|
279
291
|
# Autonomous mode members
|
|
280
292
|
self._autonomous_mode = autonomous_mode
|
|
281
293
|
self._autonomous_mode_prompt = autonomous_mode_prompt or _AUTONOMOUS_MODE_DEFAULT_PROMPT
|
|
282
|
-
self._autonomous_mode_turn_limit =
|
|
294
|
+
self._autonomous_mode_turn_limit = (
|
|
295
|
+
_DEFAULT_AUTONOMOUS_TURN_LIMIT if autonomous_mode_turn_limit is None else autonomous_mode_turn_limit
|
|
296
|
+
)
|
|
283
297
|
self._autonomous_mode_turns = 0
|
|
284
298
|
|
|
285
299
|
def _prepare_agent_with_handoffs(
|
|
@@ -372,6 +386,7 @@ class HandoffAgentExecutor(AgentExecutor):
|
|
|
372
386
|
f"Handoff tool name '{handoff_tool.name}' conflicts with existing tool."
|
|
373
387
|
"Please rename the existing tool or modify the target agent ID to avoid conflicts."
|
|
374
388
|
)
|
|
389
|
+
existing_names.add(handoff_tool.name)
|
|
375
390
|
new_tools.append(handoff_tool)
|
|
376
391
|
|
|
377
392
|
if new_tools:
|
|
@@ -928,6 +943,8 @@ class HandoffBuilder:
|
|
|
928
943
|
in autonomous mode. If not provided, a default prompt will be used.
|
|
929
944
|
turn_limits: Optional mapping of agent identifiers/factory names to maximum number of autonomous turns
|
|
930
945
|
before returning control to the user. If not provided, a default turn limit will be used.
|
|
946
|
+
A limit of ``0`` disables autonomous continuation for that agent instead of falling back
|
|
947
|
+
to the default.
|
|
931
948
|
"""
|
|
932
949
|
self._autonomous_mode = True
|
|
933
950
|
self._autonomous_mode_prompts = prompts or {}
|
|
@@ -427,8 +427,20 @@ def _extract_json(text: str) -> dict[str, Any]:
|
|
|
427
427
|
raise ValueError("No JSON object found.")
|
|
428
428
|
depth = 0
|
|
429
429
|
end = None
|
|
430
|
+
in_string = False
|
|
431
|
+
escaped = False
|
|
430
432
|
for i, ch in enumerate(text[start:], start=start):
|
|
431
|
-
|
|
433
|
+
# Braces inside string values, such as code in an instruction, do not count.
|
|
434
|
+
if in_string:
|
|
435
|
+
if escaped:
|
|
436
|
+
escaped = False
|
|
437
|
+
elif ch == "\\":
|
|
438
|
+
escaped = True
|
|
439
|
+
elif ch == '"':
|
|
440
|
+
in_string = False
|
|
441
|
+
elif ch == '"':
|
|
442
|
+
in_string = True
|
|
443
|
+
elif ch == "{":
|
|
432
444
|
depth += 1
|
|
433
445
|
elif ch == "}":
|
|
434
446
|
depth -= 1
|
|
@@ -517,7 +529,7 @@ class MagenticManagerBase(ABC):
|
|
|
517
529
|
|
|
518
530
|
|
|
519
531
|
class StandardMagenticManager(MagenticManagerBase):
|
|
520
|
-
"""Standard Magentic manager that performs real LLM calls via
|
|
532
|
+
"""Standard Magentic manager that performs real LLM calls via an Agent.
|
|
521
533
|
|
|
522
534
|
The manager constructs prompts that mirror the original Magentic One orchestration:
|
|
523
535
|
- Facts gathering
|
|
@@ -724,21 +736,37 @@ class StandardMagenticManager(MagenticManagerBase):
|
|
|
724
736
|
# Include full context to help the model decide current stage, with small retry loop
|
|
725
737
|
attempts = 0
|
|
726
738
|
last_error: Exception | None = None
|
|
739
|
+
unknown_speaker_ledger: MagenticProgressLedger | None = None
|
|
727
740
|
while attempts < self.progress_ledger_retry_count:
|
|
728
741
|
raw = await self._complete([*magentic_context.chat_history, user_message])
|
|
729
742
|
try:
|
|
730
743
|
ledger_dict = _extract_json(raw.text)
|
|
731
|
-
|
|
744
|
+
ledger = _coerce_model(MagenticProgressLedger, ledger_dict)
|
|
732
745
|
except Exception as ex:
|
|
733
746
|
last_error = ex
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
747
|
+
unknown_speaker_ledger = None
|
|
748
|
+
else:
|
|
749
|
+
next_speaker = ledger.next_speaker.answer
|
|
750
|
+
if (
|
|
751
|
+
ledger.is_request_satisfied.answer
|
|
752
|
+
or not isinstance(next_speaker, str)
|
|
753
|
+
or next_speaker in agent_names
|
|
754
|
+
):
|
|
755
|
+
return ledger
|
|
756
|
+
# Retry an unknown name, but keep the ledger so running out of attempts ends the run
|
|
757
|
+
# through the orchestrator's invalid speaker handling instead of another reset.
|
|
758
|
+
unknown_speaker_ledger = ledger
|
|
759
|
+
last_error = ValueError(f"Unknown next speaker '{next_speaker}'. Valid names: {names_csv}.")
|
|
760
|
+
attempts += 1
|
|
761
|
+
logger.warning(
|
|
762
|
+
f"Progress ledger attempt failed ({attempts}/{self.progress_ledger_retry_count}): {last_error}"
|
|
763
|
+
)
|
|
764
|
+
if attempts < self.progress_ledger_retry_count:
|
|
765
|
+
# brief backoff before next try
|
|
766
|
+
await asyncio.sleep(0.25 * attempts)
|
|
741
767
|
|
|
768
|
+
if unknown_speaker_ledger is not None:
|
|
769
|
+
return unknown_speaker_ledger
|
|
742
770
|
raise RuntimeError(
|
|
743
771
|
f"Progress ledger parse failed after {self.progress_ledger_retry_count} attempt(s): {last_error}"
|
|
744
772
|
)
|
|
@@ -1374,8 +1402,9 @@ class MagenticAgentExecutor(AgentExecutor):
|
|
|
1374
1402
|
# Request into related
|
|
1375
1403
|
self._pending_agent_requests.clear()
|
|
1376
1404
|
self._pending_responses_to_agent.clear()
|
|
1405
|
+
self._pending_request_order.clear()
|
|
1377
1406
|
# Reset sessions
|
|
1378
|
-
self.
|
|
1407
|
+
self._session = self._agent.create_session()
|
|
1379
1408
|
|
|
1380
1409
|
|
|
1381
1410
|
# endregion Magentic Agent Executor
|
|
@@ -41,6 +41,24 @@ def resolve_request_info_filter(agents: list[str | SupportsAgentRun] | None) ->
|
|
|
41
41
|
return result
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def validate_request_info_filter(request_info_filter: set[str] | None, agent_ids: list[str]) -> None:
|
|
45
|
+
"""Raise if the request info filter names an agent that is not a participant.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
request_info_filter: Agent IDs passed to ``with_request_info(agents=...)``. Empty or None means all agents.
|
|
49
|
+
agent_ids: IDs of the agent participants in the orchestration.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ValueError: If a name in the filter does not match any agent participant.
|
|
53
|
+
"""
|
|
54
|
+
unknown = sorted((request_info_filter or set()) - set(agent_ids))
|
|
55
|
+
if unknown:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"with_request_info() names agents that are not agent participants: "
|
|
58
|
+
f"{', '.join(repr(name) for name in unknown)}. Agent participants: {', '.join(agent_ids)}."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
44
62
|
@dataclass
|
|
45
63
|
class AgentRequestInfoResponse:
|
|
46
64
|
"""Response containing additional information requested from users for agents.
|
|
@@ -32,7 +32,7 @@ from agent_framework._workflows._workflow import Workflow
|
|
|
32
32
|
from agent_framework._workflows._workflow_context import WorkflowContext
|
|
33
33
|
|
|
34
34
|
from ._feature_usage import FeatureIndex
|
|
35
|
-
from ._orchestration_request_info import AgentApprovalExecutor
|
|
35
|
+
from ._orchestration_request_info import AgentApprovalExecutor, validate_request_info_filter
|
|
36
36
|
from ._participant_output_config import (
|
|
37
37
|
UNSET,
|
|
38
38
|
_coalesce_output_from, # pyright: ignore[reportPrivateUsage]
|
|
@@ -230,6 +230,11 @@ class SequentialBuilder:
|
|
|
230
230
|
else:
|
|
231
231
|
raise TypeError(f"Participants must be SupportsAgentRun or Executor instances. Got {type(p).__name__}.")
|
|
232
232
|
|
|
233
|
+
if self._request_info_enabled:
|
|
234
|
+
validate_request_info_filter(
|
|
235
|
+
self._request_info_filter,
|
|
236
|
+
[resolve_agent_id(p) for p in participants if not isinstance(p, Executor)],
|
|
237
|
+
)
|
|
233
238
|
return executors
|
|
234
239
|
|
|
235
240
|
def build(self) -> Workflow:
|
{agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/pyproject.toml
RENAMED
|
@@ -4,7 +4,7 @@ description = "Orchestration patterns for Microsoft Agent Framework. Includes Se
|
|
|
4
4
|
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.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"
|
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
|
20
20
|
"Programming Language :: Python :: 3.12",
|
|
21
21
|
"Programming Language :: Python :: 3.13",
|
|
22
22
|
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Programming Language :: Python :: 3.15",
|
|
23
24
|
"Typing :: Typed",
|
|
24
25
|
]
|
|
25
26
|
dependencies = [
|
|
@@ -92,5 +93,5 @@ help = "Run the default unit test suite for this package."
|
|
|
92
93
|
cmd = 'pytest -m "not integration" --cov=agent_framework_orchestrations --cov-report=term-missing:skip-covered -n auto --dist worksteal tests'
|
|
93
94
|
|
|
94
95
|
[build-system]
|
|
95
|
-
requires = ["flit-core
|
|
96
|
+
requires = ["flit-core>=3.11,<5.0"]
|
|
96
97
|
build-backend = "flit_core.buildapi"
|
|
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
|