-
Notifications
You must be signed in to change notification settings - Fork 28
feat: HitlSchema types and dynamic QuickForm dispatch for guardrail escalations #1737
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
Draft
dushyant-uipath
wants to merge
1
commit into
main
Choose a base branch
from
feat/guardrail-dynamic-hitl-schema
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+539
−16
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
packages/uipath-platform/src/uipath/platform/hitl/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| """HITL form schema types and generator. | ||
|
|
||
| Provides Python-native representations of the HITL schema format and a utility | ||
| to generate schemas from Pydantic models. The generated schema can be attached | ||
| to :class:`~uipath.platform.common.CreateTask` or | ||
| :class:`~uipath.platform.common.CreateEscalation` so the runtime creates a | ||
| QuickForm task with an inline schema instead of requiring a pre-deployed | ||
| Action App. | ||
|
|
||
| Example:: | ||
|
|
||
| from uipath.platform.hitl import HitlSchema, pydantic_to_hitl_schema | ||
| from pydantic import BaseModel, Field | ||
|
|
||
| class ReviewInputs(BaseModel): | ||
| flagged_content: str = Field(title="Flagged Content") | ||
| reason: str = Field(title="Reason") | ||
|
|
||
| class ReviewOutputs(BaseModel): | ||
| decision: str = Field(default="", title="Decision") | ||
| notes: str = Field(default="", title="Notes") | ||
|
|
||
| schema = pydantic_to_hitl_schema( | ||
| input_model=ReviewInputs, | ||
| output_model=ReviewOutputs, | ||
| outcomes=["Approve", "Reject"], | ||
| title="Content Review", | ||
| ) | ||
| """ | ||
|
|
||
| from .models import ( | ||
| HitlFieldDirection, | ||
| HitlFieldType, | ||
| HitlSchema, | ||
| HitlSchemaField, | ||
| HitlSchemaOutcome, | ||
| ) | ||
| from .schema_gen import pydantic_to_hitl_schema | ||
|
|
||
| __all__ = [ | ||
| "HitlFieldDirection", | ||
| "HitlFieldType", | ||
| "HitlSchema", | ||
| "HitlSchemaField", | ||
| "HitlSchemaOutcome", | ||
| "pydantic_to_hitl_schema", | ||
| ] |
76 changes: 76 additions & 0 deletions
76
packages/uipath-platform/src/uipath/platform/hitl/models.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| """Python representation of the HITL form schema format. | ||
|
|
||
| Mirrors the TypeScript ``HitlSchema`` interface from ``@uipath/hitl-schema-types`` | ||
| and is compatible with the QuickForm task endpoint | ||
| (``GenericTasks/CreateTask``, ``type=6``). | ||
| """ | ||
|
|
||
| from enum import Enum | ||
| from typing import Any | ||
|
|
||
| from pydantic import BaseModel, Field | ||
|
|
||
|
|
||
| class HitlFieldType(str, Enum): | ||
| """Types supported by HITL form fields.""" | ||
|
|
||
| STRING = "string" | ||
| NUMBER = "number" | ||
| INTEGER = "integer" | ||
| FLOAT = "float" | ||
| DOUBLE = "double" | ||
| BOOLEAN = "boolean" | ||
| DATE = "date" | ||
| DATETIME = "datetime" | ||
| FILE = "file" | ||
| OBJECT = "object" | ||
| ARRAY = "array" | ||
|
|
||
|
|
||
| class HitlFieldDirection(str, Enum): | ||
| """Direction of a HITL form field. | ||
|
|
||
| - ``INPUT`` — read-only display, pre-populated from task data. | ||
| - ``OUTPUT`` — editable by the human reviewer, written to a variable on submit. | ||
| - ``IN_OUT`` — both pre-populated and editable. | ||
| """ | ||
|
|
||
| INPUT = "input" | ||
| OUTPUT = "output" | ||
| IN_OUT = "inOut" | ||
|
|
||
|
|
||
| class HitlSchemaField(BaseModel): | ||
| """A single field in a HITL form schema.""" | ||
|
|
||
| id: str | ||
| label: str | None = None | ||
| type: HitlFieldType = HitlFieldType.STRING | ||
| direction: HitlFieldDirection = HitlFieldDirection.INPUT | ||
| required: bool | None = None | ||
|
|
||
|
|
||
| class HitlSchemaOutcome(BaseModel): | ||
| """An outcome button in a HITL form schema (e.g. Approve / Reject).""" | ||
|
|
||
| id: str | ||
| label: str | None = None | ||
|
|
||
|
|
||
| class HitlSchema(BaseModel): | ||
| """A dynamic HITL form schema generated from Python type annotations. | ||
|
|
||
| Attach to :class:`~uipath.platform.common.CreateTask` or | ||
| :class:`~uipath.platform.common.CreateEscalation` so the runtime creates a | ||
| schema-driven QuickForm task instead of an action-app task. No pre-deployed | ||
| Action App is required when a schema is provided. | ||
| """ | ||
|
|
||
| id: str | None = None | ||
| title: str | None = None | ||
| fields: list[HitlSchemaField] = Field(default_factory=list) | ||
| outcomes: list[HitlSchemaOutcome] = Field(default_factory=list) | ||
|
|
||
| def to_wire_format(self) -> dict[str, Any]: | ||
| """Serialise to the dict expected by the QuickForm task API.""" | ||
| return self.model_dump(mode="json", exclude_none=True) | ||
143 changes: 143 additions & 0 deletions
143
packages/uipath-platform/src/uipath/platform/hitl/schema_gen.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| """Generate a :class:`HitlSchema` from Python type annotations. | ||
|
|
||
| The primary entry point is :func:`pydantic_to_hitl_schema`, which converts | ||
| Pydantic ``BaseModel`` classes into the :class:`HitlSchema` format consumed by | ||
| Action Center's QuickForm task endpoint. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import inspect | ||
| import re | ||
| from types import UnionType | ||
| from typing import Any, Union, get_args, get_origin | ||
|
|
||
| from pydantic import BaseModel | ||
|
|
||
| from .models import ( | ||
| HitlFieldDirection, | ||
| HitlFieldType, | ||
| HitlSchema, | ||
| HitlSchemaField, | ||
| HitlSchemaOutcome, | ||
| ) | ||
|
|
||
|
|
||
| def _annotation_to_hitl_type(annotation: Any) -> HitlFieldType: | ||
|
Check failure on line 26 in packages/uipath-platform/src/uipath/platform/hitl/schema_gen.py
|
||
| """Map a Python type annotation to the closest :class:`HitlFieldType`.""" | ||
| if annotation is None or annotation is inspect.Parameter.empty: | ||
| return HitlFieldType.STRING | ||
|
|
||
| origin = get_origin(annotation) | ||
| args = get_args(annotation) | ||
|
|
||
| # Handle Optional[T] / T | None (both typing.Union and PEP 604 UnionType) | ||
| if origin is Union or isinstance(annotation, UnionType): | ||
| non_none = [a for a in args if a is not type(None)] | ||
| if non_none: | ||
| return _annotation_to_hitl_type(non_none[0]) | ||
| return HitlFieldType.STRING | ||
|
|
||
| if origin in (list, tuple): | ||
| return HitlFieldType.ARRAY | ||
| if origin is dict: | ||
| return HitlFieldType.OBJECT | ||
|
|
||
| if not inspect.isclass(annotation): | ||
| return HitlFieldType.STRING | ||
|
|
||
| # Check bool before int — bool is a subclass of int | ||
| if issubclass(annotation, bool): | ||
| return HitlFieldType.BOOLEAN | ||
| if issubclass(annotation, int): | ||
| return HitlFieldType.INTEGER | ||
| if issubclass(annotation, float): | ||
| return HitlFieldType.NUMBER | ||
| if issubclass(annotation, str): | ||
| return HitlFieldType.STRING | ||
| if issubclass(annotation, (list, tuple)): | ||
| return HitlFieldType.ARRAY | ||
| if issubclass(annotation, dict): | ||
| return HitlFieldType.OBJECT | ||
| if issubclass(annotation, BaseModel): | ||
| return HitlFieldType.OBJECT | ||
|
|
||
| return HitlFieldType.STRING | ||
|
|
||
|
|
||
| def _label_from_name(name: str) -> str: | ||
| """Produce a human-readable label from a snake_case or PascalCase name.""" | ||
| # PascalCase → insert space before each capital that follows a lowercase letter | ||
| spaced = re.sub(r"(?<=[a-z])(?=[A-Z])", " ", name) | ||
| # Underscores → spaces | ||
| spaced = spaced.replace("_", " ") | ||
| return spaced.title() | ||
|
|
||
|
|
||
| def pydantic_to_hitl_schema( | ||
| *, | ||
| input_model: type[BaseModel] | None = None, | ||
| output_model: type[BaseModel] | None = None, | ||
| outcomes: list[str] | None = None, | ||
| title: str | None = None, | ||
| ) -> HitlSchema: | ||
| """Generate a :class:`HitlSchema` from Pydantic input and output models. | ||
|
|
||
| Fields from *input_model* become ``direction="input"`` fields (read-only, | ||
| pre-populated from task data). Fields from *output_model* become | ||
| ``direction="output"`` fields (editable by the human reviewer). | ||
|
|
||
| The field ``id`` is taken from ``field_info.alias`` when one is set, | ||
| otherwise from the Python field name. Set an alias on a Pydantic field | ||
| to control the id that Action Center uses (useful when the form contract | ||
| requires PascalCase names but Python convention is snake_case). | ||
|
|
||
| Args: | ||
| input_model: Pydantic model for data shown to the reviewer. | ||
| output_model: Pydantic model for data the reviewer fills in. | ||
| outcomes: Outcome button labels. Defaults to ``["Approve", "Reject"]``. | ||
| title: Human-readable title for the form. | ||
|
|
||
| Returns: | ||
| A :class:`HitlSchema` ready to attach to | ||
| :class:`~uipath.platform.common.CreateTask`. | ||
| """ | ||
| if outcomes is None: | ||
| outcomes = ["Approve", "Reject"] | ||
|
|
||
| fields: list[HitlSchemaField] = [] | ||
|
|
||
| if input_model is not None: | ||
| for name, field_info in input_model.model_fields.items(): | ||
| field_id = field_info.alias or name | ||
| fields.append( | ||
| HitlSchemaField( | ||
| id=str(field_id), | ||
| label=field_info.title or _label_from_name(str(field_id)), | ||
| type=_annotation_to_hitl_type(field_info.annotation), | ||
| direction=HitlFieldDirection.INPUT, | ||
| required=True if field_info.is_required() else None, | ||
| ) | ||
| ) | ||
|
|
||
| if output_model is not None: | ||
| for name, field_info in output_model.model_fields.items(): | ||
| field_id = field_info.alias or name | ||
| fields.append( | ||
| HitlSchemaField( | ||
| id=str(field_id), | ||
| label=field_info.title or _label_from_name(str(field_id)), | ||
| type=_annotation_to_hitl_type(field_info.annotation), | ||
| direction=HitlFieldDirection.OUTPUT, | ||
| required=None, | ||
| ) | ||
| ) | ||
|
|
||
| return HitlSchema( | ||
| title=title, | ||
| fields=fields, | ||
| outcomes=[ | ||
| HitlSchemaOutcome(id=outcome.lower(), label=outcome) | ||
| for outcome in outcomes | ||
| ], | ||
| ) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Can we have this case?
Generarly at pre target execution, the input field is editable.
At post target execution, we show both fields (input and output), but only output is editable. Doesn't make sense to edit input since it is allready consumed by llm/tool.