Skip to content

Commit eb19af4

Browse files
authored
Python: fix(ag-ui): Execute tools with approval_mode, fix shared state, code cleanup (microsoft#3079)
* fix(ag-ui): execute tools after approval in human-in-the-loop flow * Fix shared state bug * Bug fix finalized * Refactoring to clean up code * Code cleanup * More fixes * More code cleanup * Add version detection in __init__.py to ruff ignore list
1 parent 77d6312 commit eb19af4

21 files changed

Lines changed: 2444 additions & 637 deletions

python/packages/ag-ui/agent_framework_ag_ui/_confirmation_strategies.py

Lines changed: 122 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,61 @@
1111

1212

1313
class ConfirmationStrategy(ABC):
14-
"""Strategy for generating confirmation messages during human-in-the-loop flows."""
14+
"""Strategy for generating confirmation messages during human-in-the-loop flows.
1515
16+
Subclasses must define the message properties. The methods use those properties
17+
by default, but can be overridden for complete customization.
18+
"""
19+
20+
@property
21+
@abstractmethod
22+
def approval_header(self) -> str:
23+
"""Header for approval accepted message. Must be overridden."""
24+
...
25+
26+
@property
27+
@abstractmethod
28+
def approval_footer(self) -> str:
29+
"""Footer for approval accepted message. Must be overridden."""
30+
...
31+
32+
@property
33+
@abstractmethod
34+
def rejection_message(self) -> str:
35+
"""Message when user rejects. Must be overridden."""
36+
...
37+
38+
@property
1639
@abstractmethod
40+
def state_confirmed_message(self) -> str:
41+
"""Message when state is confirmed. Must be overridden."""
42+
...
43+
44+
@property
45+
@abstractmethod
46+
def state_rejected_message(self) -> str:
47+
"""Message when state is rejected. Must be overridden."""
48+
...
49+
1750
def on_approval_accepted(self, steps: list[dict[str, Any]]) -> str:
1851
"""Generate message when user approves function execution.
1952
53+
Default implementation uses header/footer properties.
54+
Override for complete customization.
55+
2056
Args:
2157
steps: List of approved steps with 'description', 'status', etc.
2258
2359
Returns:
2460
Message to display to user
2561
"""
26-
...
62+
enabled_steps = [s for s in steps if s.get("status") == "enabled"]
63+
message_parts = [self.approval_header.format(count=len(enabled_steps))]
64+
for i, step in enumerate(enabled_steps, 1):
65+
message_parts.append(f"{i}. {step['description']}\n")
66+
message_parts.append(self.approval_footer)
67+
return "".join(message_parts)
2768

28-
@abstractmethod
2969
def on_approval_rejected(self, steps: list[dict[str, Any]]) -> str:
3070
"""Generate message when user rejects function execution.
3171
@@ -35,141 +75,143 @@ def on_approval_rejected(self, steps: list[dict[str, Any]]) -> str:
3575
Returns:
3676
Message to display to user
3777
"""
38-
...
78+
return self.rejection_message
3979

40-
@abstractmethod
4180
def on_state_confirmed(self) -> str:
4281
"""Generate message when user confirms predictive state changes.
4382
4483
Returns:
4584
Message to display to user
4685
"""
47-
...
86+
return self.state_confirmed_message
4887

49-
@abstractmethod
5088
def on_state_rejected(self) -> str:
5189
"""Generate message when user rejects predictive state changes.
5290
5391
Returns:
5492
Message to display to user
5593
"""
56-
...
94+
return self.state_rejected_message
5795

5896

5997
class DefaultConfirmationStrategy(ConfirmationStrategy):
60-
"""Generic confirmation messages suitable for most agents.
61-
62-
This preserves the original behavior from v1.
63-
"""
98+
"""Generic confirmation messages suitable for most agents."""
6499

65-
def on_approval_accepted(self, steps: list[dict[str, Any]]) -> str:
66-
"""Generate generic approval message with step list."""
67-
enabled_steps = [s for s in steps if s.get("status") == "enabled"]
68-
69-
message_parts = [f"Executing {len(enabled_steps)} approved steps:\n\n"]
70-
71-
for i, step in enumerate(enabled_steps, 1):
72-
message_parts.append(f"{i}. {step['description']}\n")
73-
74-
message_parts.append("\nAll steps completed successfully!")
100+
@property
101+
def approval_header(self) -> str:
102+
return "Executing {count} approved steps:\n\n"
75103

76-
return "".join(message_parts)
104+
@property
105+
def approval_footer(self) -> str:
106+
return "\nAll steps completed successfully!"
77107

78-
def on_approval_rejected(self, steps: list[dict[str, Any]]) -> str:
79-
"""Generate generic rejection message."""
108+
@property
109+
def rejection_message(self) -> str:
80110
return "No problem! What would you like me to change about the plan?"
81111

82-
def on_state_confirmed(self) -> str:
83-
"""Generate generic state confirmation message."""
112+
@property
113+
def state_confirmed_message(self) -> str:
84114
return "Changes confirmed and applied successfully!"
85115

86-
def on_state_rejected(self) -> str:
87-
"""Generate generic state rejection message."""
116+
@property
117+
def state_rejected_message(self) -> str:
88118
return "No problem! What would you like me to change?"
89119

90120

91121
class TaskPlannerConfirmationStrategy(ConfirmationStrategy):
92122
"""Domain-specific confirmation messages for task planning agents."""
93123

94-
def on_approval_accepted(self, steps: list[dict[str, Any]]) -> str:
95-
"""Generate task-specific approval message."""
96-
enabled_steps = [s for s in steps if s.get("status") == "enabled"]
97-
98-
message_parts = ["Executing your requested tasks:\n\n"]
99-
100-
for i, step in enumerate(enabled_steps, 1):
101-
message_parts.append(f"{i}. {step['description']}\n")
124+
@property
125+
def approval_header(self) -> str:
126+
return "Executing your requested tasks:\n\n"
102127

103-
message_parts.append("\nAll tasks completed successfully!")
128+
@property
129+
def approval_footer(self) -> str:
130+
return "\nAll tasks completed successfully!"
104131

105-
return "".join(message_parts)
106-
107-
def on_approval_rejected(self, steps: list[dict[str, Any]]) -> str:
108-
"""Generate task-specific rejection message."""
132+
@property
133+
def rejection_message(self) -> str:
109134
return "No problem! Let me revise the plan. What would you like me to change?"
110135

111-
def on_state_confirmed(self) -> str:
112-
"""Task planners typically don't use state confirmation."""
136+
@property
137+
def state_confirmed_message(self) -> str:
113138
return "Tasks confirmed and ready to execute!"
114139

115-
def on_state_rejected(self) -> str:
116-
"""Task planners typically don't use state confirmation."""
140+
@property
141+
def state_rejected_message(self) -> str:
117142
return "No problem! How should I adjust the task list?"
118143

119144

120145
class RecipeConfirmationStrategy(ConfirmationStrategy):
121146
"""Domain-specific confirmation messages for recipe agents."""
122147

123-
def on_approval_accepted(self, steps: list[dict[str, Any]]) -> str:
124-
"""Generate recipe-specific approval message."""
125-
enabled_steps = [s for s in steps if s.get("status") == "enabled"]
126-
127-
message_parts = ["Updating your recipe:\n\n"]
148+
@property
149+
def approval_header(self) -> str:
150+
return "Updating your recipe:\n\n"
128151

129-
for i, step in enumerate(enabled_steps, 1):
130-
message_parts.append(f"{i}. {step['description']}\n")
131-
132-
message_parts.append("\nRecipe updated successfully!")
133-
134-
return "".join(message_parts)
152+
@property
153+
def approval_footer(self) -> str:
154+
return "\nRecipe updated successfully!"
135155

136-
def on_approval_rejected(self, steps: list[dict[str, Any]]) -> str:
137-
"""Generate recipe-specific rejection message."""
156+
@property
157+
def rejection_message(self) -> str:
138158
return "No problem! What ingredients or steps should I change?"
139159

140-
def on_state_confirmed(self) -> str:
141-
"""Generate recipe-specific state confirmation message."""
160+
@property
161+
def state_confirmed_message(self) -> str:
142162
return "Recipe changes applied successfully!"
143163

144-
def on_state_rejected(self) -> str:
145-
"""Generate recipe-specific state rejection message."""
164+
@property
165+
def state_rejected_message(self) -> str:
146166
return "No problem! What would you like me to adjust in the recipe?"
147167

148168

149169
class DocumentWriterConfirmationStrategy(ConfirmationStrategy):
150170
"""Domain-specific confirmation messages for document writing agents."""
151171

152-
def on_approval_accepted(self, steps: list[dict[str, Any]]) -> str:
153-
"""Generate document-specific approval message."""
154-
enabled_steps = [s for s in steps if s.get("status") == "enabled"]
155-
156-
message_parts = ["Applying your edits:\n\n"]
157-
158-
for i, step in enumerate(enabled_steps, 1):
159-
message_parts.append(f"{i}. {step['description']}\n")
160-
161-
message_parts.append("\nDocument updated successfully!")
172+
@property
173+
def approval_header(self) -> str:
174+
return "Applying your edits:\n\n"
162175

163-
return "".join(message_parts)
176+
@property
177+
def approval_footer(self) -> str:
178+
return "\nDocument updated successfully!"
164179

165-
def on_approval_rejected(self, steps: list[dict[str, Any]]) -> str:
166-
"""Generate document-specific rejection message."""
180+
@property
181+
def rejection_message(self) -> str:
167182
return "No problem! Which changes should I keep or modify?"
168183

169-
def on_state_confirmed(self) -> str:
170-
"""Generate document-specific state confirmation message."""
184+
@property
185+
def state_confirmed_message(self) -> str:
171186
return "Document edits applied!"
172187

173-
def on_state_rejected(self) -> str:
174-
"""Generate document-specific state rejection message."""
188+
@property
189+
def state_rejected_message(self) -> str:
175190
return "No problem! What should I change about the document?"
191+
192+
193+
def apply_confirmation_strategy(
194+
strategy: ConfirmationStrategy | None,
195+
accepted: bool,
196+
steps: list[dict[str, Any]],
197+
) -> str:
198+
"""Apply a confirmation strategy to generate a message.
199+
200+
This helper consolidates the pattern used in multiple orchestrators.
201+
202+
Args:
203+
strategy: Strategy to use, or None for default
204+
accepted: Whether the user approved
205+
steps: List of steps (may be empty for state confirmations)
206+
207+
Returns:
208+
Generated message string
209+
"""
210+
if strategy is None:
211+
strategy = DefaultConfirmationStrategy()
212+
213+
if not steps:
214+
# State confirmation (no steps)
215+
return strategy.on_state_confirmed() if accepted else strategy.on_state_rejected()
216+
# Step-based approval
217+
return strategy.on_approval_accepted(steps) if accepted else strategy.on_approval_rejected(steps)

0 commit comments

Comments
 (0)