haystack-experimental 0.17.0__py3-none-any.whl → 0.19.0__py3-none-any.whl
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.
- haystack_experimental/components/agents/agent.py +75 -143
- haystack_experimental/components/agents/human_in_the_loop/__init__.py +2 -15
- haystack_experimental/components/agents/human_in_the_loop/breakpoint.py +1 -2
- haystack_experimental/components/agents/human_in_the_loop/strategies.py +4 -548
- haystack_experimental/dataclasses/breakpoints.py +3 -4
- {haystack_experimental-0.17.0.dist-info → haystack_experimental-0.19.0.dist-info}/METADATA +24 -27
- {haystack_experimental-0.17.0.dist-info → haystack_experimental-0.19.0.dist-info}/RECORD +10 -14
- haystack_experimental/components/agents/human_in_the_loop/dataclasses.py +0 -72
- haystack_experimental/components/agents/human_in_the_loop/policies.py +0 -78
- haystack_experimental/components/agents/human_in_the_loop/types.py +0 -124
- haystack_experimental/components/agents/human_in_the_loop/user_interfaces.py +0 -209
- {haystack_experimental-0.17.0.dist-info → haystack_experimental-0.19.0.dist-info}/WHEEL +0 -0
- {haystack_experimental-0.17.0.dist-info → haystack_experimental-0.19.0.dist-info}/licenses/LICENSE +0 -0
- {haystack_experimental-0.17.0.dist-info → haystack_experimental-0.19.0.dist-info}/licenses/LICENSE-MIT.txt +0 -0
|
@@ -2,26 +2,12 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
+
from typing import Any
|
|
7
6
|
|
|
8
|
-
from haystack.
|
|
9
|
-
from haystack.
|
|
10
|
-
from haystack.core.serialization import default_from_dict, default_to_dict, import_class_by_name
|
|
11
|
-
from haystack.dataclasses import ChatMessage, StreamingCallbackT
|
|
12
|
-
from haystack.tools import Tool
|
|
13
|
-
|
|
14
|
-
from haystack_experimental.components.agents.human_in_the_loop import (
|
|
15
|
-
ConfirmationPolicy,
|
|
16
|
-
ConfirmationStrategy,
|
|
17
|
-
ConfirmationUI,
|
|
18
|
-
HITLBreakpointException,
|
|
19
|
-
ToolExecutionDecision,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
if TYPE_CHECKING:
|
|
23
|
-
from haystack_experimental.components.agents.agent import _ExecutionContext
|
|
7
|
+
from haystack.core.serialization import default_from_dict, default_to_dict
|
|
8
|
+
from haystack.human_in_the_loop.dataclasses import ToolExecutionDecision
|
|
24
9
|
|
|
10
|
+
from haystack_experimental.components.agents.human_in_the_loop import HITLBreakpointException
|
|
25
11
|
|
|
26
12
|
_REJECTION_FEEDBACK_TEMPLATE = "Tool execution for '{tool_name}' was rejected by the user."
|
|
27
13
|
_MODIFICATION_FEEDBACK_TEMPLATE = (
|
|
@@ -29,163 +15,6 @@ _MODIFICATION_FEEDBACK_TEMPLATE = (
|
|
|
29
15
|
)
|
|
30
16
|
|
|
31
17
|
|
|
32
|
-
class BlockingConfirmationStrategy:
|
|
33
|
-
"""
|
|
34
|
-
Confirmation strategy that blocks execution to gather user feedback.
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
def __init__(self, confirmation_policy: ConfirmationPolicy, confirmation_ui: ConfirmationUI) -> None:
|
|
38
|
-
"""
|
|
39
|
-
Initialize the BlockingConfirmationStrategy with a confirmation policy and UI.
|
|
40
|
-
|
|
41
|
-
:param confirmation_policy:
|
|
42
|
-
The confirmation policy to determine when to ask for user confirmation.
|
|
43
|
-
:param confirmation_ui:
|
|
44
|
-
The user interface to interact with the user for confirmation.
|
|
45
|
-
"""
|
|
46
|
-
self.confirmation_policy = confirmation_policy
|
|
47
|
-
self.confirmation_ui = confirmation_ui
|
|
48
|
-
|
|
49
|
-
def run(
|
|
50
|
-
self,
|
|
51
|
-
*,
|
|
52
|
-
tool_name: str,
|
|
53
|
-
tool_description: str,
|
|
54
|
-
tool_params: dict[str, Any],
|
|
55
|
-
tool_call_id: str | None = None,
|
|
56
|
-
confirmation_strategy_context: dict[str, Any] | None = None,
|
|
57
|
-
) -> ToolExecutionDecision:
|
|
58
|
-
"""
|
|
59
|
-
Run the human-in-the-loop strategy for a given tool and its parameters.
|
|
60
|
-
|
|
61
|
-
:param tool_name:
|
|
62
|
-
The name of the tool to be executed.
|
|
63
|
-
:param tool_description:
|
|
64
|
-
The description of the tool.
|
|
65
|
-
:param tool_params:
|
|
66
|
-
The parameters to be passed to the tool.
|
|
67
|
-
:param tool_call_id:
|
|
68
|
-
Optional unique identifier for the tool call. This can be used to track and correlate the decision with a
|
|
69
|
-
specific tool invocation.
|
|
70
|
-
:param confirmation_strategy_context:
|
|
71
|
-
Optional dictionary for passing request-scoped resources. Useful in web/server environments
|
|
72
|
-
to provide per-request objects (e.g., WebSocket connections, async queues, Redis pub/sub clients)
|
|
73
|
-
that strategies can use for non-blocking user interaction.
|
|
74
|
-
|
|
75
|
-
:returns:
|
|
76
|
-
A ToolExecutionDecision indicating whether to execute the tool with the given parameters, or a
|
|
77
|
-
feedback message if rejected.
|
|
78
|
-
"""
|
|
79
|
-
# Check if we should ask based on policy
|
|
80
|
-
if not self.confirmation_policy.should_ask(
|
|
81
|
-
tool_name=tool_name, tool_description=tool_description, tool_params=tool_params
|
|
82
|
-
):
|
|
83
|
-
return ToolExecutionDecision(
|
|
84
|
-
tool_name=tool_name, execute=True, tool_call_id=tool_call_id, final_tool_params=tool_params
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# Get user confirmation through UI
|
|
88
|
-
confirmation_ui_result = self.confirmation_ui.get_user_confirmation(tool_name, tool_description, tool_params)
|
|
89
|
-
|
|
90
|
-
# Pass back the result to the policy for any learning/updating
|
|
91
|
-
self.confirmation_policy.update_after_confirmation(
|
|
92
|
-
tool_name, tool_description, tool_params, confirmation_ui_result
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
# Process the confirmation result
|
|
96
|
-
final_args = {}
|
|
97
|
-
if confirmation_ui_result.action == "reject":
|
|
98
|
-
explanation_text = _REJECTION_FEEDBACK_TEMPLATE.format(tool_name=tool_name)
|
|
99
|
-
if confirmation_ui_result.feedback:
|
|
100
|
-
explanation_text += f" With feedback: {confirmation_ui_result.feedback}"
|
|
101
|
-
return ToolExecutionDecision(
|
|
102
|
-
tool_name=tool_name, execute=False, tool_call_id=tool_call_id, feedback=explanation_text
|
|
103
|
-
)
|
|
104
|
-
elif confirmation_ui_result.action == "modify" and confirmation_ui_result.new_tool_params:
|
|
105
|
-
# Update the tool call params with the new params
|
|
106
|
-
final_args.update(confirmation_ui_result.new_tool_params)
|
|
107
|
-
explanation_text = _MODIFICATION_FEEDBACK_TEMPLATE.format(tool_name=tool_name, final_tool_params=final_args)
|
|
108
|
-
if confirmation_ui_result.feedback:
|
|
109
|
-
explanation_text += f" With feedback: {confirmation_ui_result.feedback}"
|
|
110
|
-
return ToolExecutionDecision(
|
|
111
|
-
tool_name=tool_name,
|
|
112
|
-
tool_call_id=tool_call_id,
|
|
113
|
-
execute=True,
|
|
114
|
-
feedback=explanation_text,
|
|
115
|
-
final_tool_params=final_args,
|
|
116
|
-
)
|
|
117
|
-
else: # action == "confirm"
|
|
118
|
-
return ToolExecutionDecision(
|
|
119
|
-
tool_name=tool_name, execute=True, tool_call_id=tool_call_id, final_tool_params=tool_params
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
async def run_async(
|
|
123
|
-
self,
|
|
124
|
-
*,
|
|
125
|
-
tool_name: str,
|
|
126
|
-
tool_description: str,
|
|
127
|
-
tool_params: dict[str, Any],
|
|
128
|
-
tool_call_id: str | None = None,
|
|
129
|
-
confirmation_strategy_context: dict[str, Any] | None = None,
|
|
130
|
-
) -> ToolExecutionDecision:
|
|
131
|
-
"""
|
|
132
|
-
Async version of run. Calls the sync run() method by default.
|
|
133
|
-
|
|
134
|
-
:param tool_name:
|
|
135
|
-
The name of the tool to be executed.
|
|
136
|
-
:param tool_description:
|
|
137
|
-
The description of the tool.
|
|
138
|
-
:param tool_params:
|
|
139
|
-
The parameters to be passed to the tool.
|
|
140
|
-
:param tool_call_id:
|
|
141
|
-
Optional unique identifier for the tool call.
|
|
142
|
-
:param confirmation_strategy_context:
|
|
143
|
-
Optional dictionary for passing request-scoped resources.
|
|
144
|
-
|
|
145
|
-
:returns:
|
|
146
|
-
A ToolExecutionDecision indicating whether to execute the tool with the given parameters.
|
|
147
|
-
"""
|
|
148
|
-
return self.run(
|
|
149
|
-
tool_name=tool_name,
|
|
150
|
-
tool_description=tool_description,
|
|
151
|
-
tool_params=tool_params,
|
|
152
|
-
tool_call_id=tool_call_id,
|
|
153
|
-
confirmation_strategy_context=confirmation_strategy_context,
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
def to_dict(self) -> dict[str, Any]:
|
|
157
|
-
"""
|
|
158
|
-
Serializes the BlockingConfirmationStrategy to a dictionary.
|
|
159
|
-
|
|
160
|
-
:returns:
|
|
161
|
-
Dictionary with serialized data.
|
|
162
|
-
"""
|
|
163
|
-
return default_to_dict(
|
|
164
|
-
self, confirmation_policy=self.confirmation_policy.to_dict(), confirmation_ui=self.confirmation_ui.to_dict()
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
@classmethod
|
|
168
|
-
def from_dict(cls, data: dict[str, Any]) -> "BlockingConfirmationStrategy":
|
|
169
|
-
"""
|
|
170
|
-
Deserializes the BlockingConfirmationStrategy from a dictionary.
|
|
171
|
-
|
|
172
|
-
:param data:
|
|
173
|
-
Dictionary to deserialize from.
|
|
174
|
-
|
|
175
|
-
:returns:
|
|
176
|
-
Deserialized BlockingConfirmationStrategy.
|
|
177
|
-
"""
|
|
178
|
-
policy_data = data["init_parameters"]["confirmation_policy"]
|
|
179
|
-
policy_class = import_class_by_name(policy_data["type"])
|
|
180
|
-
if not hasattr(policy_class, "from_dict"):
|
|
181
|
-
raise ValueError(f"Class {policy_class} does not implement from_dict method.")
|
|
182
|
-
ui_data = data["init_parameters"]["confirmation_ui"]
|
|
183
|
-
ui_class = import_class_by_name(ui_data["type"])
|
|
184
|
-
if not hasattr(ui_class, "from_dict"):
|
|
185
|
-
raise ValueError(f"Class {ui_class} does not implement from_dict method.")
|
|
186
|
-
return cls(confirmation_policy=policy_class.from_dict(policy_data), confirmation_ui=ui_class.from_dict(ui_data))
|
|
187
|
-
|
|
188
|
-
|
|
189
18
|
class BreakpointConfirmationStrategy:
|
|
190
19
|
"""
|
|
191
20
|
Confirmation strategy that raises a tool breakpoint exception to pause execution and gather user feedback.
|
|
@@ -297,376 +126,3 @@ class BreakpointConfirmationStrategy:
|
|
|
297
126
|
Deserialized BreakpointConfirmationStrategy.
|
|
298
127
|
"""
|
|
299
128
|
return default_from_dict(cls, data)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def _prepare_tool_args(
|
|
303
|
-
*,
|
|
304
|
-
tool: Tool,
|
|
305
|
-
tool_call_arguments: dict[str, Any],
|
|
306
|
-
state: State,
|
|
307
|
-
streaming_callback: StreamingCallbackT | None = None,
|
|
308
|
-
enable_streaming_passthrough: bool = False,
|
|
309
|
-
) -> dict[str, Any]:
|
|
310
|
-
"""
|
|
311
|
-
Prepare the final arguments for a tool by injecting state inputs and optionally a streaming callback.
|
|
312
|
-
|
|
313
|
-
:param tool:
|
|
314
|
-
The tool instance to prepare arguments for.
|
|
315
|
-
:param tool_call_arguments:
|
|
316
|
-
The initial arguments provided for the tool call.
|
|
317
|
-
:param state:
|
|
318
|
-
The current state containing inputs to be injected into the tool arguments.
|
|
319
|
-
:param streaming_callback:
|
|
320
|
-
Optional streaming callback to be injected if enabled and applicable.
|
|
321
|
-
:param enable_streaming_passthrough:
|
|
322
|
-
Flag indicating whether to inject the streaming callback into the tool arguments.
|
|
323
|
-
|
|
324
|
-
:returns:
|
|
325
|
-
A dictionary of final arguments ready for tool invocation.
|
|
326
|
-
"""
|
|
327
|
-
# Combine user + state inputs
|
|
328
|
-
final_args = ToolInvoker._inject_state_args(tool, tool_call_arguments.copy(), state)
|
|
329
|
-
# Check whether to inject streaming_callback
|
|
330
|
-
if (
|
|
331
|
-
enable_streaming_passthrough
|
|
332
|
-
and streaming_callback is not None
|
|
333
|
-
and "streaming_callback" not in final_args
|
|
334
|
-
and "streaming_callback" in ToolInvoker._get_func_params(tool)
|
|
335
|
-
):
|
|
336
|
-
final_args["streaming_callback"] = streaming_callback
|
|
337
|
-
return final_args
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def _process_confirmation_strategies(
|
|
341
|
-
*,
|
|
342
|
-
confirmation_strategies: dict[str, ConfirmationStrategy],
|
|
343
|
-
messages_with_tool_calls: list[ChatMessage],
|
|
344
|
-
execution_context: "_ExecutionContext",
|
|
345
|
-
) -> tuple[list[ChatMessage], list[ChatMessage]]:
|
|
346
|
-
"""
|
|
347
|
-
Run the confirmation strategies and return modified tool call messages and updated chat history.
|
|
348
|
-
|
|
349
|
-
:param confirmation_strategies: Mapping of tool names to their corresponding confirmation strategies
|
|
350
|
-
:param messages_with_tool_calls: Chat messages containing tool calls
|
|
351
|
-
:param execution_context: The current execution context of the agent
|
|
352
|
-
:returns:
|
|
353
|
-
Tuple of modified messages with confirmed tool calls and updated chat history
|
|
354
|
-
"""
|
|
355
|
-
# Run confirmation strategies and get tool execution decisions
|
|
356
|
-
teds = _run_confirmation_strategies(
|
|
357
|
-
confirmation_strategies=confirmation_strategies,
|
|
358
|
-
messages_with_tool_calls=messages_with_tool_calls,
|
|
359
|
-
execution_context=execution_context,
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
# Apply tool execution decisions to messages_with_tool_calls
|
|
363
|
-
rejection_messages, modified_tool_call_messages = _apply_tool_execution_decisions(
|
|
364
|
-
tool_call_messages=messages_with_tool_calls,
|
|
365
|
-
tool_execution_decisions=teds,
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
# Update the chat history with rejection messages and new tool call messages
|
|
369
|
-
new_chat_history = _update_chat_history(
|
|
370
|
-
chat_history=execution_context.state.get("messages"),
|
|
371
|
-
rejection_messages=rejection_messages,
|
|
372
|
-
tool_call_and_explanation_messages=modified_tool_call_messages,
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
return modified_tool_call_messages, new_chat_history
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
async def _process_confirmation_strategies_async(
|
|
379
|
-
*,
|
|
380
|
-
confirmation_strategies: dict[str, ConfirmationStrategy],
|
|
381
|
-
messages_with_tool_calls: list[ChatMessage],
|
|
382
|
-
execution_context: "_ExecutionContext",
|
|
383
|
-
) -> tuple[list[ChatMessage], list[ChatMessage]]:
|
|
384
|
-
"""
|
|
385
|
-
Async version of _process_confirmation_strategies.
|
|
386
|
-
|
|
387
|
-
Run the confirmation strategies and return modified tool call messages and updated chat history.
|
|
388
|
-
|
|
389
|
-
:param confirmation_strategies: Mapping of tool names to their corresponding confirmation strategies
|
|
390
|
-
:param messages_with_tool_calls: Chat messages containing tool calls
|
|
391
|
-
:param execution_context: The current execution context of the agent
|
|
392
|
-
:returns:
|
|
393
|
-
Tuple of modified messages with confirmed tool calls and updated chat history
|
|
394
|
-
"""
|
|
395
|
-
# Run confirmation strategies and get tool execution decisions (async version)
|
|
396
|
-
teds = await _run_confirmation_strategies_async(
|
|
397
|
-
confirmation_strategies=confirmation_strategies,
|
|
398
|
-
messages_with_tool_calls=messages_with_tool_calls,
|
|
399
|
-
execution_context=execution_context,
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
# Apply tool execution decisions to messages_with_tool_calls
|
|
403
|
-
rejection_messages, modified_tool_call_messages = _apply_tool_execution_decisions(
|
|
404
|
-
tool_call_messages=messages_with_tool_calls,
|
|
405
|
-
tool_execution_decisions=teds,
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
# Update the chat history with rejection messages and new tool call messages
|
|
409
|
-
new_chat_history = _update_chat_history(
|
|
410
|
-
chat_history=execution_context.state.get("messages"),
|
|
411
|
-
rejection_messages=rejection_messages,
|
|
412
|
-
tool_call_and_explanation_messages=modified_tool_call_messages,
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
return modified_tool_call_messages, new_chat_history
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
def _run_confirmation_strategies(
|
|
419
|
-
confirmation_strategies: dict[str, ConfirmationStrategy],
|
|
420
|
-
messages_with_tool_calls: list[ChatMessage],
|
|
421
|
-
execution_context: "_ExecutionContext",
|
|
422
|
-
) -> list[ToolExecutionDecision]:
|
|
423
|
-
"""
|
|
424
|
-
Run confirmation strategies for tool calls in the provided chat messages.
|
|
425
|
-
|
|
426
|
-
:param confirmation_strategies: Mapping of tool names to their corresponding confirmation strategies
|
|
427
|
-
:param messages_with_tool_calls: Messages containing tool calls to process
|
|
428
|
-
:param execution_context: The current execution context containing state and inputs
|
|
429
|
-
:returns:
|
|
430
|
-
A list of ToolExecutionDecision objects representing the decisions made for each tool call.
|
|
431
|
-
"""
|
|
432
|
-
state = execution_context.state
|
|
433
|
-
tools_with_names = {tool.name: tool for tool in execution_context.tool_invoker_inputs["tools"]}
|
|
434
|
-
existing_teds = execution_context.tool_execution_decisions if execution_context.tool_execution_decisions else []
|
|
435
|
-
existing_teds_by_name = {ted.tool_name: ted for ted in existing_teds if ted.tool_name}
|
|
436
|
-
existing_teds_by_id = {ted.tool_call_id: ted for ted in existing_teds if ted.tool_call_id}
|
|
437
|
-
|
|
438
|
-
teds = []
|
|
439
|
-
for message in messages_with_tool_calls:
|
|
440
|
-
if not message.tool_calls:
|
|
441
|
-
continue
|
|
442
|
-
|
|
443
|
-
for tool_call in message.tool_calls:
|
|
444
|
-
tool_name = tool_call.tool_name
|
|
445
|
-
tool_to_invoke = tools_with_names[tool_name]
|
|
446
|
-
|
|
447
|
-
# Prepare final tool args
|
|
448
|
-
final_args = _prepare_tool_args(
|
|
449
|
-
tool=tool_to_invoke,
|
|
450
|
-
tool_call_arguments=tool_call.arguments,
|
|
451
|
-
state=state,
|
|
452
|
-
streaming_callback=execution_context.tool_invoker_inputs.get("streaming_callback"),
|
|
453
|
-
enable_streaming_passthrough=execution_context.tool_invoker_inputs.get(
|
|
454
|
-
"enable_streaming_passthrough", False
|
|
455
|
-
),
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
# Get tool execution decisions from confirmation strategies
|
|
459
|
-
# If no confirmation strategy is defined for this tool, proceed with execution
|
|
460
|
-
if tool_name not in confirmation_strategies:
|
|
461
|
-
teds.append(
|
|
462
|
-
ToolExecutionDecision(
|
|
463
|
-
tool_call_id=tool_call.id,
|
|
464
|
-
tool_name=tool_name,
|
|
465
|
-
execute=True,
|
|
466
|
-
final_tool_params=final_args,
|
|
467
|
-
)
|
|
468
|
-
)
|
|
469
|
-
continue
|
|
470
|
-
|
|
471
|
-
# Check if there's already a decision for this tool call in the execution context
|
|
472
|
-
ted = existing_teds_by_id.get(tool_call.id or "") or existing_teds_by_name.get(tool_name)
|
|
473
|
-
|
|
474
|
-
# If not, run the confirmation strategy
|
|
475
|
-
if not ted:
|
|
476
|
-
ted = confirmation_strategies[tool_name].run(
|
|
477
|
-
tool_name=tool_name,
|
|
478
|
-
tool_description=tool_to_invoke.description,
|
|
479
|
-
tool_params=final_args,
|
|
480
|
-
tool_call_id=tool_call.id,
|
|
481
|
-
confirmation_strategy_context=execution_context.confirmation_strategy_context,
|
|
482
|
-
)
|
|
483
|
-
teds.append(ted)
|
|
484
|
-
|
|
485
|
-
return teds
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
async def _run_confirmation_strategies_async(
|
|
489
|
-
confirmation_strategies: dict[str, ConfirmationStrategy],
|
|
490
|
-
messages_with_tool_calls: list[ChatMessage],
|
|
491
|
-
execution_context: "_ExecutionContext",
|
|
492
|
-
) -> list[ToolExecutionDecision]:
|
|
493
|
-
"""
|
|
494
|
-
Async version of _run_confirmation_strategies.
|
|
495
|
-
|
|
496
|
-
Run confirmation strategies for tool calls in the provided chat messages.
|
|
497
|
-
|
|
498
|
-
:param confirmation_strategies: Mapping of tool names to their corresponding confirmation strategies
|
|
499
|
-
:param messages_with_tool_calls: Messages containing tool calls to process
|
|
500
|
-
:param execution_context: The current execution context containing state and inputs
|
|
501
|
-
:returns:
|
|
502
|
-
A list of ToolExecutionDecision objects representing the decisions made for each tool call.
|
|
503
|
-
"""
|
|
504
|
-
state = execution_context.state
|
|
505
|
-
tools_with_names = {tool.name: tool for tool in execution_context.tool_invoker_inputs["tools"]}
|
|
506
|
-
existing_teds = execution_context.tool_execution_decisions if execution_context.tool_execution_decisions else []
|
|
507
|
-
existing_teds_by_name = {ted.tool_name: ted for ted in existing_teds if ted.tool_name}
|
|
508
|
-
existing_teds_by_id = {ted.tool_call_id: ted for ted in existing_teds if ted.tool_call_id}
|
|
509
|
-
|
|
510
|
-
teds = []
|
|
511
|
-
for message in messages_with_tool_calls:
|
|
512
|
-
if not message.tool_calls:
|
|
513
|
-
continue
|
|
514
|
-
|
|
515
|
-
for tool_call in message.tool_calls:
|
|
516
|
-
tool_name = tool_call.tool_name
|
|
517
|
-
tool_to_invoke = tools_with_names[tool_name]
|
|
518
|
-
|
|
519
|
-
# Prepare final tool args
|
|
520
|
-
final_args = _prepare_tool_args(
|
|
521
|
-
tool=tool_to_invoke,
|
|
522
|
-
tool_call_arguments=tool_call.arguments,
|
|
523
|
-
state=state,
|
|
524
|
-
streaming_callback=execution_context.tool_invoker_inputs.get("streaming_callback"),
|
|
525
|
-
enable_streaming_passthrough=execution_context.tool_invoker_inputs.get(
|
|
526
|
-
"enable_streaming_passthrough", False
|
|
527
|
-
),
|
|
528
|
-
)
|
|
529
|
-
|
|
530
|
-
# Get tool execution decisions from confirmation strategies
|
|
531
|
-
# If no confirmation strategy is defined for this tool, proceed with execution
|
|
532
|
-
if tool_name not in confirmation_strategies:
|
|
533
|
-
teds.append(
|
|
534
|
-
ToolExecutionDecision(
|
|
535
|
-
tool_call_id=tool_call.id,
|
|
536
|
-
tool_name=tool_name,
|
|
537
|
-
execute=True,
|
|
538
|
-
final_tool_params=final_args,
|
|
539
|
-
)
|
|
540
|
-
)
|
|
541
|
-
continue
|
|
542
|
-
|
|
543
|
-
# Check if there's already a decision for this tool call in the execution context
|
|
544
|
-
ted = existing_teds_by_id.get(tool_call.id or "") or existing_teds_by_name.get(tool_name)
|
|
545
|
-
|
|
546
|
-
# If not, run the confirmation strategy (async version)
|
|
547
|
-
if not ted:
|
|
548
|
-
strategy = confirmation_strategies[tool_name]
|
|
549
|
-
# Use run_async if available, otherwise fall back to sync run
|
|
550
|
-
if hasattr(strategy, "run_async"):
|
|
551
|
-
ted = await strategy.run_async(
|
|
552
|
-
tool_name=tool_name,
|
|
553
|
-
tool_description=tool_to_invoke.description,
|
|
554
|
-
tool_params=final_args,
|
|
555
|
-
tool_call_id=tool_call.id,
|
|
556
|
-
confirmation_strategy_context=execution_context.confirmation_strategy_context,
|
|
557
|
-
)
|
|
558
|
-
else:
|
|
559
|
-
ted = strategy.run(
|
|
560
|
-
tool_name=tool_name,
|
|
561
|
-
tool_description=tool_to_invoke.description,
|
|
562
|
-
tool_params=final_args,
|
|
563
|
-
tool_call_id=tool_call.id,
|
|
564
|
-
confirmation_strategy_context=execution_context.confirmation_strategy_context,
|
|
565
|
-
)
|
|
566
|
-
teds.append(ted)
|
|
567
|
-
|
|
568
|
-
return teds
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
def _apply_tool_execution_decisions(
|
|
572
|
-
tool_call_messages: list[ChatMessage], tool_execution_decisions: list[ToolExecutionDecision]
|
|
573
|
-
) -> tuple[list[ChatMessage], list[ChatMessage]]:
|
|
574
|
-
"""
|
|
575
|
-
Apply the tool execution decisions to the tool call messages.
|
|
576
|
-
|
|
577
|
-
:param tool_call_messages: The tool call messages to apply the decisions to.
|
|
578
|
-
:param tool_execution_decisions: The tool execution decisions to apply.
|
|
579
|
-
:returns:
|
|
580
|
-
A tuple containing:
|
|
581
|
-
- A list of rejection messages for rejected tool calls. These are pairs of tool call and tool call result
|
|
582
|
-
messages.
|
|
583
|
-
- A list of tool call messages for confirmed or modified tool calls. If tool parameters were modified,
|
|
584
|
-
a user message explaining the modification is included before the tool call message.
|
|
585
|
-
"""
|
|
586
|
-
decision_by_id = {d.tool_call_id: d for d in tool_execution_decisions if d.tool_call_id}
|
|
587
|
-
decision_by_name = {d.tool_name: d for d in tool_execution_decisions if d.tool_name}
|
|
588
|
-
|
|
589
|
-
def make_assistant_message(chat_message, tool_calls):
|
|
590
|
-
return ChatMessage.from_assistant(
|
|
591
|
-
text=chat_message.text,
|
|
592
|
-
meta=chat_message.meta,
|
|
593
|
-
name=chat_message.name,
|
|
594
|
-
tool_calls=tool_calls,
|
|
595
|
-
reasoning=chat_message.reasoning,
|
|
596
|
-
)
|
|
597
|
-
|
|
598
|
-
new_tool_call_messages = []
|
|
599
|
-
rejection_messages = []
|
|
600
|
-
|
|
601
|
-
for chat_msg in tool_call_messages:
|
|
602
|
-
new_tool_calls = []
|
|
603
|
-
for tc in chat_msg.tool_calls or []:
|
|
604
|
-
ted = decision_by_id.get(tc.id or "") or decision_by_name.get(tc.tool_name)
|
|
605
|
-
if not ted:
|
|
606
|
-
# This shouldn't happen, if so something went wrong in _run_confirmation_strategies
|
|
607
|
-
continue
|
|
608
|
-
|
|
609
|
-
if not ted.execute:
|
|
610
|
-
# rejected tool call
|
|
611
|
-
tool_result_text = ted.feedback or _REJECTION_FEEDBACK_TEMPLATE.format(tool_name=tc.tool_name)
|
|
612
|
-
rejection_messages.extend(
|
|
613
|
-
[
|
|
614
|
-
make_assistant_message(chat_msg, [tc]),
|
|
615
|
-
ChatMessage.from_tool(tool_result=tool_result_text, origin=tc, error=True),
|
|
616
|
-
]
|
|
617
|
-
)
|
|
618
|
-
continue
|
|
619
|
-
|
|
620
|
-
# Covers confirm and modify cases
|
|
621
|
-
final_args = ted.final_tool_params or {}
|
|
622
|
-
if tc.arguments != final_args:
|
|
623
|
-
# In the modify case we add a user message explaining the modification otherwise the LLM won't know
|
|
624
|
-
# why the tool parameters changed and will likely just try and call the tool again with the
|
|
625
|
-
# original parameters.
|
|
626
|
-
user_text = ted.feedback or _MODIFICATION_FEEDBACK_TEMPLATE.format(
|
|
627
|
-
tool_name=tc.tool_name, final_tool_params=final_args
|
|
628
|
-
)
|
|
629
|
-
new_tool_call_messages.append(ChatMessage.from_user(text=user_text))
|
|
630
|
-
new_tool_calls.append(replace(tc, arguments=final_args))
|
|
631
|
-
|
|
632
|
-
# Only add the tool call message if there are any tool calls left (i.e. not all were rejected)
|
|
633
|
-
if new_tool_calls:
|
|
634
|
-
new_tool_call_messages.append(make_assistant_message(chat_msg, new_tool_calls))
|
|
635
|
-
|
|
636
|
-
return rejection_messages, new_tool_call_messages
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
def _update_chat_history(
|
|
640
|
-
chat_history: list[ChatMessage],
|
|
641
|
-
rejection_messages: list[ChatMessage],
|
|
642
|
-
tool_call_and_explanation_messages: list[ChatMessage],
|
|
643
|
-
) -> list[ChatMessage]:
|
|
644
|
-
"""
|
|
645
|
-
Update the chat history to include rejection messages and tool call messages at the appropriate positions.
|
|
646
|
-
|
|
647
|
-
Steps:
|
|
648
|
-
1. Identify the last user message and the last tool message in the current chat history.
|
|
649
|
-
2. Determine the insertion point as the maximum index of these two messages.
|
|
650
|
-
3. Create a new chat history that includes:
|
|
651
|
-
- All messages up to the insertion point.
|
|
652
|
-
- Any rejection messages (pairs of tool call and tool call result messages).
|
|
653
|
-
- Any tool call messages for confirmed or modified tool calls, including user messages explaining modifications.
|
|
654
|
-
|
|
655
|
-
:param chat_history: The current chat history.
|
|
656
|
-
:param rejection_messages: Chat messages to add for rejected tool calls (pairs of tool call and tool call result
|
|
657
|
-
messages).
|
|
658
|
-
:param tool_call_and_explanation_messages: Tool call messages for confirmed or modified tool calls, which may
|
|
659
|
-
include user messages explaining modifications.
|
|
660
|
-
:returns:
|
|
661
|
-
The updated chat history.
|
|
662
|
-
"""
|
|
663
|
-
user_indices = [i for i, message in enumerate(chat_history) if message.is_from("user")]
|
|
664
|
-
tool_indices = [i for i, message in enumerate(chat_history) if message.is_from("tool")]
|
|
665
|
-
|
|
666
|
-
last_user_idx = max(user_indices) if user_indices else -1
|
|
667
|
-
last_tool_idx = max(tool_indices) if tool_indices else -1
|
|
668
|
-
|
|
669
|
-
insertion_point = max(last_user_idx, last_tool_idx)
|
|
670
|
-
|
|
671
|
-
new_chat_history = chat_history[: insertion_point + 1] + rejection_messages + tool_call_and_explanation_messages
|
|
672
|
-
return new_chat_history
|
|
@@ -6,10 +6,9 @@ from dataclasses import dataclass
|
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
from haystack.dataclasses
|
|
10
|
-
from haystack.dataclasses
|
|
11
|
-
|
|
12
|
-
from haystack_experimental.components.agents.human_in_the_loop.dataclasses import ToolExecutionDecision
|
|
9
|
+
from haystack.dataclasses import AgentBreakpoint
|
|
10
|
+
from haystack.dataclasses import AgentSnapshot as HaystackAgentSnapshot
|
|
11
|
+
from haystack.human_in_the_loop.dataclasses import ToolExecutionDecision
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
@dataclass
|