-
Notifications
You must be signed in to change notification settings - Fork 29
feat: add escalation tool #303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,10 +8,23 @@ | |
| from pydantic import BaseModel, Field | ||
|
|
||
|
|
||
| class AgentTerminationSource(StrEnum): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need more information here. AFAIK escalations states are submit and reject. IMO the model should be the one deciding to terminate. Otherwise, any potential cleanup may get skipped.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is the way low code currently works. without this we won t be able to reach parity |
||
| ESCALATION = "escalation" | ||
|
|
||
|
|
||
| class AgentTermination(BaseModel): | ||
| """Agent Graph Termination model.""" | ||
|
|
||
| source: AgentTerminationSource | ||
| title: str | ||
| detail: str = "" | ||
|
|
||
|
|
||
| class AgentGraphState(BaseModel): | ||
| """Agent Graph state for standard loop execution.""" | ||
|
|
||
| messages: Annotated[list[AnyMessage], add_messages] = [] | ||
| termination: AgentTermination | None = None | ||
|
|
||
|
|
||
| class AgentGraphNode(StrEnum): | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,119 @@ | ||||
| """Escalation tool creation for Action Center integration.""" | ||||
|
|
||||
| from __future__ import annotations | ||||
|
|
||||
| import logging | ||||
| from enum import Enum | ||||
| from typing import Annotated, Any | ||||
|
|
||||
| from langchain_core.messages import ToolMessage | ||||
| from langchain_core.tools import InjectedToolCallId, StructuredTool | ||||
| from langgraph.types import Command, interrupt | ||||
| from pydantic import BaseModel, create_model | ||||
| from uipath.agent.models.agent import ( | ||||
| AgentEscalationChannel, | ||||
| AgentEscalationRecipientType, | ||||
| AgentEscalationResourceConfig, | ||||
| ) | ||||
| from uipath.eval.mocks import mockable | ||||
| from uipath.platform.common import CreateEscalation | ||||
| from uipath.utils.dynamic_schema import jsonschema_to_pydantic | ||||
|
|
||||
| from ..react.types import AgentGraphNode, AgentTerminationSource | ||||
| from .utils import sanitize_tool_name | ||||
|
|
||||
| logger = logging.getLogger(__name__) | ||||
|
|
||||
|
|
||||
| class EscalationAction(str, Enum): | ||||
| """Actions that can be taken after an escalation completes.""" | ||||
|
|
||||
| CONTINUE = "continue" | ||||
| END = "end" | ||||
|
|
||||
|
|
||||
| def create_escalation_tool(resource: AgentEscalationResourceConfig) -> StructuredTool: | ||||
| """Uses interrupt() for Action Center human-in-the-loop.""" | ||||
|
|
||||
| tool_name: str = f"escalate_{sanitize_tool_name(resource.name)}" | ||||
| channel: AgentEscalationChannel = resource.channels[0] | ||||
| base_input_model: type[BaseModel] = jsonschema_to_pydantic(channel.input_schema) | ||||
| input_model = create_model( | ||||
| base_input_model.__name__, | ||||
| __base__=base_input_model, | ||||
| tool_call_id=(Annotated[str, InjectedToolCallId]), | ||||
| ) | ||||
| output_model: type[BaseModel] = jsonschema_to_pydantic(channel.output_schema) | ||||
|
|
||||
| # only works for UserEmail type as value is GUID for UserId or GroupId for other types | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be better to extend the api to accept recipient type and to update this: https://github.com/UiPath/uipath-python/blob/379b7b310dca36cd6bcf9889bdbc2c81a4d0966d/src/uipath/platform/action_center/_tasks_service.py#L125-L125 ? |
||||
| # but the API expects strings e.g: [email protected] | ||||
| # we need to do user resolution via Identity here | ||||
| assignee: str | None = ( | ||||
| channel.recipients[0].value | ||||
| if channel.recipients | ||||
| and channel.recipients[0].type == AgentEscalationRecipientType.USER_EMAIL | ||||
| else None | ||||
| ) | ||||
|
|
||||
| @mockable( | ||||
| name=resource.name, | ||||
| description=resource.description, | ||||
| input_schema=input_model.model_json_schema(), | ||||
| output_schema=output_model.model_json_schema(), | ||||
| ) | ||||
| async def escalation_tool_fn( | ||||
| tool_call_id: Annotated[str, InjectedToolCallId], **kwargs: Any | ||||
| ) -> Command[Any] | Any: | ||||
| task_title = channel.task_title or "Escalation Task" | ||||
| result = interrupt( | ||||
| CreateEscalation( | ||||
| title=task_title, | ||||
| data=kwargs, | ||||
| assignee=assignee, | ||||
| app_name=channel.properties.app_name, | ||||
| app_folder_path=channel.properties.folder_name, | ||||
| app_version=channel.properties.app_version, | ||||
| priority=channel.priority, | ||||
| labels=channel.labels, | ||||
| is_actionable_message_enabled=channel.properties.is_actionable_message_enabled, | ||||
| actionable_message_metadata=channel.properties.actionable_message_meta_data, | ||||
| ) | ||||
| ) | ||||
|
|
||||
| escalation_action = getattr(result, "action", None) | ||||
| escalation_output = getattr(result, "data", {}) | ||||
| validated_result = output_model.model_validate(escalation_output) | ||||
|
|
||||
| outcome = ( | ||||
| channel.outcome_mapping.get(escalation_action) | ||||
| if channel.outcome_mapping and escalation_action | ||||
| else None | ||||
| ) | ||||
| if outcome and outcome == EscalationAction.END: | ||||
| return Command( | ||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels like an anti-pattern. Tools should ideally not be aware of state.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above. the escalations can be configured to terminate the execution on reject
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, can we port over this logic to
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is already being converted to a ToolNode
|
||||
| update={ | ||||
| "messages": [ | ||||
| ToolMessage( | ||||
| content="Terminating the agent execution as configured in the escalation outcome", | ||||
| tool_call_id=tool_call_id, | ||||
| ) | ||||
| ], | ||||
| "termination": { | ||||
| "source": AgentTerminationSource.ESCALATION, | ||||
| "title": f"Agent run ended based on escalation outcome {outcome} with directive {escalation_action}", | ||||
| "detail": f"Escalation output: {validated_result.model_dump()}", | ||||
| }, | ||||
| }, | ||||
| goto=AgentGraphNode.TERMINATE, | ||||
| ) | ||||
|
|
||||
| return validated_result | ||||
|
|
||||
| tool = StructuredTool( | ||||
| name=tool_name, | ||||
| description=resource.description, | ||||
| args_schema=input_model, | ||||
| coroutine=escalation_tool_fn, | ||||
| ) | ||||
|
|
||||
| return tool | ||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we make these into
tools instead? That way, the LLM gets the tool definitions without additional change.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be better to leave the terminate node as a 'special' tool for separation of concerns.
