|
7 | 7 | from langchain_core.messages.tool import ToolCall, ToolMessage |
8 | 8 | from langchain_core.tools import BaseTool, InjectedToolCallId |
9 | 9 | from langchain_core.tools import tool as langchain_tool |
10 | | -from uipath.core.chat import ( |
11 | | - UiPathConversationToolCallConfirmationValue, |
12 | | -) |
| 10 | +from uipath.core.chat import UiPathConversationToolCallConfirmationEvent |
13 | 11 |
|
14 | 12 | from uipath_langchain._utils.durable_interrupt import durable_interrupt |
15 | 13 |
|
16 | 14 | CANCELLED_MESSAGE = "Cancelled by user" |
| 15 | +ARGS_MODIFIED_MESSAGE = "User has modified the tool arguments" |
17 | 16 |
|
18 | 17 | CONVERSATIONAL_APPROVED_TOOL_ARGS = "conversational_approved_tool_args" |
19 | 18 | REQUIRE_CONVERSATIONAL_CONFIRMATION = "require_conversational_confirmation" |
20 | 19 |
|
21 | 20 |
|
| 21 | +def _wrap_with_args_modified_meta(result: Any, approved_args: dict[str, Any]) -> str: |
| 22 | + """Wrap a tool result with metadata indicating the user modified the args.""" |
| 23 | + try: |
| 24 | + result_value = json.loads(result) if isinstance(result, str) else result |
| 25 | + except (json.JSONDecodeError, TypeError): |
| 26 | + result_value = result |
| 27 | + return json.dumps( |
| 28 | + { |
| 29 | + "meta": { |
| 30 | + "message": ARGS_MODIFIED_MESSAGE, |
| 31 | + "executed_args": approved_args, |
| 32 | + }, |
| 33 | + "result": result_value, |
| 34 | + } |
| 35 | + ) |
| 36 | + |
| 37 | + |
| 38 | +def get_confirmation_schema(tool: Any) -> dict[str, Any] | None: |
| 39 | + """Return the JSON input schema if this tool requires confirmation, else None.""" |
| 40 | + metadata = getattr(tool, "metadata", None) or {} |
| 41 | + if not metadata.get(REQUIRE_CONVERSATIONAL_CONFIRMATION): |
| 42 | + return None |
| 43 | + tool_call_schema = getattr(tool, "tool_call_schema", None) |
| 44 | + return tool_call_schema.model_json_schema() if tool_call_schema is not None else {} |
| 45 | + |
| 46 | + |
22 | 47 | class ConfirmationResult(NamedTuple): |
23 | 48 | """Result of a tool confirmation check.""" |
24 | 49 |
|
@@ -47,20 +72,8 @@ def annotate_result(self, output: dict[str, Any] | Any) -> None: |
47 | 72 | msg.response_metadata[CONVERSATIONAL_APPROVED_TOOL_ARGS] = ( |
48 | 73 | self.approved_args |
49 | 74 | ) |
50 | | - if self.args_modified: |
51 | | - try: |
52 | | - result_value = json.loads(msg.content) |
53 | | - except (json.JSONDecodeError, TypeError): |
54 | | - result_value = msg.content |
55 | | - msg.content = json.dumps( |
56 | | - { |
57 | | - "meta": { |
58 | | - "args_modified_by_user": True, |
59 | | - "executed_args": self.approved_args, |
60 | | - }, |
61 | | - "result": result_value, |
62 | | - } |
63 | | - ) |
| 75 | + if self.args_modified and self.approved_args is not None: |
| 76 | + msg.content = _wrap_with_args_modified_meta(msg.content, self.approved_args) |
64 | 77 |
|
65 | 78 |
|
66 | 79 | def _patch_span_input(approved_args: dict[str, Any]) -> None: |
@@ -113,39 +126,24 @@ def request_approval( |
113 | 126 | """ |
114 | 127 | tool_call_id: str = tool_args.pop("tool_call_id") |
115 | 128 |
|
116 | | - input_schema: dict[str, Any] = {} |
117 | | - tool_call_schema = getattr( |
118 | | - tool, "tool_call_schema", None |
119 | | - ) # doesn't include InjectedToolCallId (tool id from claude/oai/etc.) |
120 | | - if tool_call_schema is not None: |
121 | | - input_schema = tool_call_schema.model_json_schema() |
122 | | - |
123 | 129 | @durable_interrupt |
124 | 130 | def ask_confirmation(): |
125 | | - return UiPathConversationToolCallConfirmationValue( |
126 | | - tool_call_id=tool_call_id, |
127 | | - tool_name=tool.name, |
128 | | - input_schema=input_schema, |
129 | | - input_value=tool_args, |
130 | | - ) |
| 131 | + return { |
| 132 | + "tool_call_id": tool_call_id, |
| 133 | + "tool_name": tool.name, |
| 134 | + "input": tool_args, |
| 135 | + } |
131 | 136 |
|
132 | 137 | response = ask_confirmation() |
133 | 138 |
|
134 | | - # The resume payload from CAS has shape: |
135 | | - # {"type": "uipath_cas_tool_call_confirmation", |
136 | | - # "value": {"approved": bool, "input": <edited args | None>}} |
137 | 139 | if not isinstance(response, dict): |
138 | 140 | return tool_args |
139 | 141 |
|
140 | | - confirmation = response.get("value", response) |
141 | | - if not confirmation.get("approved", True): |
| 142 | + confirmation = UiPathConversationToolCallConfirmationEvent.model_validate(response) |
| 143 | + if not confirmation.approved: |
142 | 144 | return None |
143 | 145 |
|
144 | | - return ( |
145 | | - confirmation.get("input") |
146 | | - if confirmation.get("input") is not None |
147 | | - else tool_args |
148 | | - ) |
| 146 | + return confirmation.input if confirmation.input is not None else tool_args |
149 | 147 |
|
150 | 148 |
|
151 | 149 | # for conversational low code agents |
@@ -200,8 +198,15 @@ def wrapper(**tool_args: Any) -> Any: |
200 | 198 | if approved_args is None: |
201 | 199 | return json.dumps({"meta": CANCELLED_MESSAGE}) |
202 | 200 |
|
| 201 | + args_modified = approved_args != tool_args |
| 202 | + |
203 | 203 | _patch_span_input(approved_args) |
204 | | - return fn(**approved_args) |
| 204 | + result = fn(**approved_args) |
| 205 | + |
| 206 | + if args_modified: |
| 207 | + return _wrap_with_args_modified_meta(result, approved_args) |
| 208 | + |
| 209 | + return result |
205 | 210 |
|
206 | 211 | # rewrite the signature: e.g. (query: str) -> (query: str, *, tool_call_id: str) |
207 | 212 | original_sig = inspect.signature(fn) |
@@ -234,6 +239,10 @@ def wrapper(**tool_args: Any) -> Any: |
234 | 239 | return_direct=return_direct, |
235 | 240 | ) |
236 | 241 |
|
| 242 | + if result.metadata is None: |
| 243 | + result.metadata = {} |
| 244 | + result.metadata[REQUIRE_CONVERSATIONAL_CONFIRMATION] = True |
| 245 | + |
237 | 246 | _created_tool.append(result) |
238 | 247 | return result |
239 | 248 |
|
|
0 commit comments