11import functools
22import inspect
33from inspect import Parameter
4- from typing import Annotated , Any , Callable
4+ from typing import Annotated , Any , Callable , NamedTuple
55
6+ from langchain_core .messages .tool import ToolCall , ToolMessage
67from langchain_core .tools import BaseTool , InjectedToolCallId
78from langchain_core .tools import tool as langchain_tool
89from langgraph .types import interrupt
910from uipath .core .chat import (
1011 UiPathConversationToolCallConfirmationValue ,
1112)
1213
13- _CANCELLED_MESSAGE = "Cancelled by user"
14+ CANCELLED_MESSAGE = "Cancelled by user"
15+ ARGS_MODIFIED_MESSAGE = "Tool arguments were modified by the user"
16+
17+ CONVERSATIONAL_APPROVED_TOOL_ARGS = "conversational_approved_tool_args"
18+ REQUIRE_CONVERSATIONAL_CONFIRMATION = "require_conversational_confirmation"
19+
20+
21+ class ConfirmationResult (NamedTuple ):
22+ """Result of a tool confirmation check."""
23+
24+ cancelled : ToolMessage | None # ToolMessage if cancelled, None if approved
25+ args_modified : bool
26+ approved_args : dict [str , Any ] | None = None
27+
28+ def annotate_result (self , output : dict [str , Any ] | Any ) -> None :
29+ """Apply confirmation metadata to a tool result message."""
30+ msg = None
31+ if isinstance (output , dict ):
32+ messages = output .get ("messages" )
33+ if messages :
34+ msg = messages [0 ]
35+ if msg is None :
36+ return
37+ if self .approved_args is not None :
38+ msg .response_metadata [CONVERSATIONAL_APPROVED_TOOL_ARGS ] = (
39+ self .approved_args
40+ )
41+ if self .args_modified :
42+ msg .content = (
43+ f'{{"meta": "{ ARGS_MODIFIED_MESSAGE } ", "result": { msg .content } }}'
44+ )
1445
1546
1647def _patch_span_input (approved_args : dict [str , Any ]) -> None :
@@ -53,7 +84,7 @@ def _patch_span_input(approved_args: dict[str, Any]) -> None:
5384 pass
5485
5586
56- def _request_approval (
87+ def request_approval (
5788 tool_args : dict [str , Any ],
5889 tool : BaseTool ,
5990) -> dict [str , Any ] | None :
@@ -89,7 +120,39 @@ def _request_approval(
89120 if not confirmation .get ("approved" , True ):
90121 return None
91122
92- return confirmation .get ("input" ) or tool_args
123+ return (
124+ confirmation .get ("input" )
125+ if confirmation .get ("input" ) is not None
126+ else tool_args
127+ )
128+
129+
130+ def check_tool_confirmation (
131+ call : ToolCall , tool : BaseTool
132+ ) -> ConfirmationResult | None :
133+ if not (tool .metadata and tool .metadata .get (REQUIRE_CONVERSATIONAL_CONFIRMATION )):
134+ return None
135+
136+ original_args = call ["args" ]
137+ approved_args = request_approval (
138+ {** original_args , "tool_call_id" : call ["id" ]}, tool
139+ )
140+ if approved_args is None :
141+ cancelled_msg = ToolMessage (
142+ content = CANCELLED_MESSAGE ,
143+ name = call ["name" ],
144+ tool_call_id = call ["id" ],
145+ )
146+ cancelled_msg .response_metadata [CONVERSATIONAL_APPROVED_TOOL_ARGS ] = (
147+ original_args
148+ )
149+ return ConfirmationResult (cancelled = cancelled_msg , args_modified = False )
150+ call ["args" ] = approved_args
151+ return ConfirmationResult (
152+ cancelled = None ,
153+ args_modified = approved_args != original_args ,
154+ approved_args = approved_args ,
155+ )
93156
94157
95158def requires_approval (
@@ -107,9 +170,9 @@ def decorator(fn: Callable[..., Any]) -> BaseTool:
107170 # wrap the tool/function
108171 @functools .wraps (fn )
109172 def wrapper (** tool_args : Any ) -> Any :
110- approved_args = _request_approval (tool_args , _created_tool [0 ])
173+ approved_args = request_approval (tool_args , _created_tool [0 ])
111174 if approved_args is None :
112- return _CANCELLED_MESSAGE
175+ return { "meta" : CANCELLED_MESSAGE }
113176 _patch_span_input (approved_args )
114177 return fn (** approved_args )
115178
0 commit comments