uipath-core 0.1.3__tar.gz → 0.1.5__tar.gz

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.
Files changed (60) hide show
  1. {uipath_core-0.1.3 → uipath_core-0.1.5}/PKG-INFO +1 -1
  2. {uipath_core-0.1.3 → uipath_core-0.1.5}/pyproject.toml +1 -1
  3. uipath_core-0.1.5/src/uipath/core/guardrails/_deterministic_guardrails_service.py +142 -0
  4. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/guardrails/_evaluators.py +108 -30
  5. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/guardrails/guardrails.py +14 -4
  6. {uipath_core-0.1.3 → uipath_core-0.1.5}/tests/guardrails/test_deterministic_guardrails_service.py +446 -44
  7. {uipath_core-0.1.3 → uipath_core-0.1.5}/uv.lock +1 -1
  8. uipath_core-0.1.3/src/uipath/core/guardrails/_deterministic_guardrails_service.py +0 -79
  9. {uipath_core-0.1.3 → uipath_core-0.1.5}/.cursorrules +0 -0
  10. {uipath_core-0.1.3 → uipath_core-0.1.5}/.editorconfig +0 -0
  11. {uipath_core-0.1.3 → uipath_core-0.1.5}/.gitattributes +0 -0
  12. {uipath_core-0.1.3 → uipath_core-0.1.5}/.github/workflows/cd.yml +0 -0
  13. {uipath_core-0.1.3 → uipath_core-0.1.5}/.github/workflows/ci.yml +0 -0
  14. {uipath_core-0.1.3 → uipath_core-0.1.5}/.github/workflows/commitlint.yml +0 -0
  15. {uipath_core-0.1.3 → uipath_core-0.1.5}/.github/workflows/lint.yml +0 -0
  16. {uipath_core-0.1.3 → uipath_core-0.1.5}/.github/workflows/publish-dev.yml +0 -0
  17. {uipath_core-0.1.3 → uipath_core-0.1.5}/.github/workflows/test.yml +0 -0
  18. {uipath_core-0.1.3 → uipath_core-0.1.5}/.gitignore +0 -0
  19. {uipath_core-0.1.3 → uipath_core-0.1.5}/.pre-commit-config.yaml +0 -0
  20. {uipath_core-0.1.3 → uipath_core-0.1.5}/.python-version +0 -0
  21. {uipath_core-0.1.3 → uipath_core-0.1.5}/.vscode/extensions.json +0 -0
  22. {uipath_core-0.1.3 → uipath_core-0.1.5}/.vscode/launch.json +0 -0
  23. {uipath_core-0.1.3 → uipath_core-0.1.5}/.vscode/settings.json +0 -0
  24. {uipath_core-0.1.3 → uipath_core-0.1.5}/CONTRIBUTING.md +0 -0
  25. {uipath_core-0.1.3 → uipath_core-0.1.5}/LICENSE +0 -0
  26. {uipath_core-0.1.3 → uipath_core-0.1.5}/README.md +0 -0
  27. {uipath_core-0.1.3 → uipath_core-0.1.5}/justfile +0 -0
  28. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/__init__.py +0 -0
  29. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/chat/__init__.py +0 -0
  30. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/chat/async_stream.py +0 -0
  31. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/chat/citation.py +0 -0
  32. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/chat/content.py +0 -0
  33. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/chat/conversation.py +0 -0
  34. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/chat/error.py +0 -0
  35. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/chat/event.py +0 -0
  36. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/chat/exchange.py +0 -0
  37. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/chat/interrupt.py +0 -0
  38. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/chat/message.py +0 -0
  39. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/chat/meta.py +0 -0
  40. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/chat/tool.py +0 -0
  41. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/errors/__init__.py +0 -0
  42. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/errors/errors.py +0 -0
  43. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/guardrails/__init__.py +0 -0
  44. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/py.typed +0 -0
  45. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/tracing/__init__.py +0 -0
  46. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/tracing/_utils.py +0 -0
  47. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/tracing/decorators.py +0 -0
  48. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/tracing/exporters.py +0 -0
  49. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/tracing/processors.py +0 -0
  50. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/tracing/span_utils.py +0 -0
  51. {uipath_core-0.1.3 → uipath_core-0.1.5}/src/uipath/core/tracing/trace_manager.py +0 -0
  52. {uipath_core-0.1.3 → uipath_core-0.1.5}/tests/__init__.py +0 -0
  53. {uipath_core-0.1.3 → uipath_core-0.1.5}/tests/conftest.py +0 -0
  54. {uipath_core-0.1.3 → uipath_core-0.1.5}/tests/tracing/test_external_integration.py +0 -0
  55. {uipath_core-0.1.3 → uipath_core-0.1.5}/tests/tracing/test_serialization.py +0 -0
  56. {uipath_core-0.1.3 → uipath_core-0.1.5}/tests/tracing/test_span_nesting.py +0 -0
  57. {uipath_core-0.1.3 → uipath_core-0.1.5}/tests/tracing/test_span_registry.py +0 -0
  58. {uipath_core-0.1.3 → uipath_core-0.1.5}/tests/tracing/test_trace_manager.py +0 -0
  59. {uipath_core-0.1.3 → uipath_core-0.1.5}/tests/tracing/test_traced.py +0 -0
  60. {uipath_core-0.1.3 → uipath_core-0.1.5}/tests/tracing/test_tracing_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uipath-core
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: UiPath Core abstractions
5
5
  Project-URL: Homepage, https://uipath.com
6
6
  Project-URL: Repository, https://github.com/UiPath/uipath-core-python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uipath-core"
3
- version = "0.1.3"
3
+ version = "0.1.5"
4
4
  description = "UiPath Core abstractions"
5
5
  readme = { file = "README.md", content-type = "text/markdown" }
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,142 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from ..tracing.decorators import traced
6
+ from ._evaluators import (
7
+ evaluate_boolean_rule,
8
+ evaluate_number_rule,
9
+ evaluate_universal_rule,
10
+ evaluate_word_rule,
11
+ )
12
+ from .guardrails import (
13
+ AllFieldsSelector,
14
+ ApplyTo,
15
+ BooleanRule,
16
+ DeterministicGuardrail,
17
+ FieldSource,
18
+ GuardrailValidationResult,
19
+ NumberRule,
20
+ SpecificFieldsSelector,
21
+ UniversalRule,
22
+ WordRule,
23
+ )
24
+
25
+
26
+ class DeterministicGuardrailsService(BaseModel):
27
+ @traced("evaluate_pre_deterministic_guardrail", run_type="uipath")
28
+ def evaluate_pre_deterministic_guardrail(
29
+ self,
30
+ input_data: dict[str, Any],
31
+ guardrail: DeterministicGuardrail,
32
+ ) -> GuardrailValidationResult:
33
+ """Evaluate deterministic guardrail rules against input data (pre-execution)."""
34
+ # Check if guardrail contains any output-dependent rules
35
+ has_output_rule = self._has_output_dependent_rule(guardrail, [ApplyTo.OUTPUT])
36
+
37
+ # If guardrail has output-dependent rules, skip evaluation in pre-execution
38
+ # Output rules will be evaluated during post-execution
39
+ if has_output_rule:
40
+ return GuardrailValidationResult(
41
+ validation_passed=True,
42
+ reason="Guardrail contains output-dependent rules that will be evaluated during post-execution",
43
+ )
44
+ return self._evaluate_deterministic_guardrail(
45
+ input_data=input_data,
46
+ output_data={},
47
+ guardrail=guardrail,
48
+ )
49
+
50
+ @traced("evaluate_post_deterministic_guardrails", run_type="uipath")
51
+ def evaluate_post_deterministic_guardrail(
52
+ self,
53
+ input_data: dict[str, Any],
54
+ output_data: dict[str, Any],
55
+ guardrail: DeterministicGuardrail,
56
+ ) -> GuardrailValidationResult:
57
+ """Evaluate deterministic guardrail rules against input and output data."""
58
+ # Check if guardrail contains any output-dependent rules
59
+ has_output_rule = self._has_output_dependent_rule(
60
+ guardrail, [ApplyTo.OUTPUT, ApplyTo.INPUT_AND_OUTPUT]
61
+ )
62
+
63
+ # If guardrail has no output-dependent rules, skip post-execution evaluation
64
+ # Only input rules exist and they should have been evaluated during pre-execution
65
+ if not has_output_rule:
66
+ return GuardrailValidationResult(
67
+ validation_passed=True,
68
+ reason="Guardrail contains only input-dependent rules that were evaluated during pre-execution",
69
+ )
70
+
71
+ return self._evaluate_deterministic_guardrail(
72
+ input_data=input_data,
73
+ output_data=output_data,
74
+ guardrail=guardrail,
75
+ )
76
+
77
+ @staticmethod
78
+ def _has_output_dependent_rule(
79
+ guardrail: DeterministicGuardrail,
80
+ universal_rules_apply_to_values: list[ApplyTo],
81
+ ) -> bool:
82
+ """Check if at least one rule EXCLUSIVELY requires output data.
83
+
84
+ Args:
85
+ guardrail: The guardrail to check
86
+ universal_rules_apply_to_values: List of ApplyTo values to consider as output-dependent for UniversalRules.
87
+
88
+ Returns:
89
+ True if at least one rule exclusively depends on output data, False otherwise.
90
+ """
91
+ for rule in guardrail.rules:
92
+ # UniversalRule: only return True if it applies to values in universal_rules_apply_to_values
93
+ if isinstance(rule, UniversalRule):
94
+ if rule.apply_to in universal_rules_apply_to_values:
95
+ return True
96
+ # Rules with field_selector
97
+ elif isinstance(rule, (WordRule, NumberRule, BooleanRule)):
98
+ field_selector = rule.field_selector
99
+ # AllFieldsSelector applies to both input and output, not exclusively output
100
+ # SpecificFieldsSelector: only return True if at least one field has OUTPUT source
101
+ if isinstance(field_selector, SpecificFieldsSelector):
102
+ if field_selector.fields and any(
103
+ field.source == FieldSource.OUTPUT
104
+ for field in field_selector.fields
105
+ ):
106
+ return True
107
+ elif isinstance(field_selector, AllFieldsSelector):
108
+ if FieldSource.OUTPUT in field_selector.sources:
109
+ return True
110
+
111
+ return False
112
+
113
+ @staticmethod
114
+ def _evaluate_deterministic_guardrail(
115
+ input_data: dict[str, Any],
116
+ output_data: dict[str, Any],
117
+ guardrail: DeterministicGuardrail,
118
+ ) -> GuardrailValidationResult:
119
+ """Evaluate deterministic guardrail rules against input and output data."""
120
+ for rule in guardrail.rules:
121
+ if isinstance(rule, WordRule):
122
+ passed, reason = evaluate_word_rule(rule, input_data, output_data)
123
+ elif isinstance(rule, NumberRule):
124
+ passed, reason = evaluate_number_rule(rule, input_data, output_data)
125
+ elif isinstance(rule, BooleanRule):
126
+ passed, reason = evaluate_boolean_rule(rule, input_data, output_data)
127
+ elif isinstance(rule, UniversalRule):
128
+ passed, reason = evaluate_universal_rule(rule, output_data)
129
+ else:
130
+ return GuardrailValidationResult(
131
+ validation_passed=False,
132
+ reason=f"Unknown rule type: {type(rule)}",
133
+ )
134
+
135
+ if not passed:
136
+ return GuardrailValidationResult(
137
+ validation_passed=False, reason=reason or "Rule validation failed"
138
+ )
139
+
140
+ return GuardrailValidationResult(
141
+ validation_passed=True, reason="All deterministic guardrail rules passed"
142
+ )
@@ -4,8 +4,9 @@ This module provides functions for evaluating different types of guardrail rules
4
4
  against input and output data.
5
5
  """
6
6
 
7
+ import inspect
7
8
  from enum import IntEnum
8
- from typing import Any
9
+ from typing import Any, Callable
9
10
 
10
11
  from .guardrails import (
11
12
  AllFieldsSelector,
@@ -119,23 +120,25 @@ def get_fields_from_selector(
119
120
  fields: list[tuple[Any, FieldReference]] = []
120
121
 
121
122
  if isinstance(field_selector, AllFieldsSelector):
122
- # For "all" selector, we need to collect all fields from both input and output
123
+ # For "all" selector, we need to collect all fields from the specified sources
123
124
  # This is a simplified implementation - in practice, you might want to
124
125
  # recursively collect all nested fields
125
- for key, value in input_data.items():
126
- fields.append(
127
- (
128
- value,
129
- FieldReference(path=key, source=FieldSource.INPUT),
126
+ if FieldSource.INPUT in field_selector.sources:
127
+ for key, value in input_data.items():
128
+ fields.append(
129
+ (
130
+ value,
131
+ FieldReference(path=key, source=FieldSource.INPUT),
132
+ )
130
133
  )
131
- )
132
- for key, value in output_data.items():
133
- fields.append(
134
- (
135
- value,
136
- FieldReference(path=key, source=FieldSource.OUTPUT),
134
+ if FieldSource.OUTPUT in field_selector.sources:
135
+ for key, value in output_data.items():
136
+ fields.append(
137
+ (
138
+ value,
139
+ FieldReference(path=key, source=FieldSource.OUTPUT),
140
+ )
137
141
  )
138
- )
139
142
  elif isinstance(field_selector, SpecificFieldsSelector):
140
143
  # For specific fields, extract values based on field references
141
144
  for field_ref in field_selector.fields:
@@ -163,7 +166,7 @@ def format_guardrail_error_message(
163
166
  ) -> str:
164
167
  """Format a guardrail error message following the standard pattern."""
165
168
  source = "Input" if field_ref.source == FieldSource.INPUT else "Output"
166
- message = f"{source} data didn't match the guardrail condition: [{field_ref.path}] {operator}"
169
+ message = f"{source} data didn't match the guardrail condition: [{field_ref.path}] comparing function [{operator}]"
167
170
  if expected_value and expected_value.strip():
168
171
  message += f" [{expected_value.strip()}]"
169
172
  return message
@@ -187,16 +190,18 @@ def evaluate_word_rule(
187
190
  field_str = field_value
188
191
 
189
192
  # Use the custom function to evaluate the rule
193
+ # If detects_violation returns True, it means the rule was violated (validation fails)
190
194
  try:
191
- passed = rule.func(field_str)
195
+ violation_detected = rule.detects_violation(field_str)
192
196
  except Exception:
193
197
  # If function raises an exception, treat as failure
194
- passed = False
198
+ violation_detected = True
195
199
 
196
- if not passed:
197
- reason = format_guardrail_error_message(
198
- field_ref, "comparing function", None
200
+ if violation_detected:
201
+ operator = (
202
+ _humanize_guardrail_func(rule.detects_violation) or "violation check"
199
203
  )
204
+ reason = format_guardrail_error_message(field_ref, operator, None)
200
205
  return False, reason
201
206
 
202
207
  return True, "All word rule validations passed"
@@ -221,16 +226,18 @@ def evaluate_number_rule(
221
226
  field_num = float(field_value)
222
227
 
223
228
  # Use the custom function to evaluate the rule
229
+ # If detects_violation returns True, it means the rule was violated (validation fails)
224
230
  try:
225
- passed = rule.func(field_num)
231
+ violation_detected = rule.detects_violation(field_num)
226
232
  except Exception:
227
233
  # If function raises an exception, treat as failure
228
- passed = False
234
+ violation_detected = True
229
235
 
230
- if not passed:
231
- reason = format_guardrail_error_message(
232
- field_ref, "comparing function", None
236
+ if violation_detected:
237
+ operator = (
238
+ _humanize_guardrail_func(rule.detects_violation) or "violation check"
233
239
  )
240
+ reason = format_guardrail_error_message(field_ref, operator, None)
234
241
  return False, reason
235
242
 
236
243
  return True, "All number rule validations passed"
@@ -256,16 +263,18 @@ def evaluate_boolean_rule(
256
263
  field_bool = field_value
257
264
 
258
265
  # Use the custom function to evaluate the rule
266
+ # If detects_violation returns True, it means the rule was violated (validation fails)
259
267
  try:
260
- passed = rule.func(field_bool)
268
+ violation_detected = rule.detects_violation(field_bool)
261
269
  except Exception:
262
270
  # If function raises an exception, treat as failure
263
- passed = False
271
+ violation_detected = True
264
272
 
265
- if not passed:
266
- reason = format_guardrail_error_message(
267
- field_ref, "comparing function", None
273
+ if violation_detected:
274
+ operator = (
275
+ _humanize_guardrail_func(rule.detects_violation) or "violation check"
268
276
  )
277
+ reason = format_guardrail_error_message(field_ref, operator, None)
269
278
  return False, reason
270
279
 
271
280
  return True, "All boolean rule validations passed"
@@ -307,3 +316,72 @@ def evaluate_universal_rule(
307
316
  return False, "Universal rule validation triggered (input and output)"
308
317
  else:
309
318
  return False, f"Unknown apply_to value: {rule.apply_to}"
319
+
320
+
321
+ def _humanize_guardrail_func(func: Callable[..., Any] | str | None) -> str | None:
322
+ """Build a user-friendly description of a guardrail predicate.
323
+
324
+ Deterministic guardrails store Python callables (often lambdas) to evaluate
325
+ conditions. For diagnostics, it's useful to include a readable hint about the
326
+ predicate that failed.
327
+
328
+ Args:
329
+ func: A Python callable used as a predicate, or a pre-rendered string
330
+ description (for example, ``"s:str -> bool: contains 'test'"``).
331
+
332
+ Returns:
333
+ A human-readable description, or ``None`` if one cannot be produced.
334
+ """
335
+ if func is None:
336
+ return None
337
+
338
+ if isinstance(func, str):
339
+ rendered = func.strip()
340
+ return rendered or None
341
+
342
+ name = getattr(func, "__name__", None)
343
+ if name and name != "<lambda>":
344
+ return name
345
+
346
+ # Best-effort extraction for lambdas / callables.
347
+ try:
348
+ sig = str(inspect.signature(func))
349
+ except (TypeError, ValueError):
350
+ sig = ""
351
+
352
+ try:
353
+ source_lines = inspect.getsourcelines(func)
354
+ source = "".join(source_lines[0]).strip()
355
+ # Collapse whitespace to keep the message compact.
356
+ source = " ".join(source.split())
357
+
358
+ # Remove "detects_violation=lambda" prefix if present
359
+ # Pattern: "detects_violation=lambda s: condition" -> "condition"
360
+ if "detects_violation=lambda" in source:
361
+ # Find the lambda part
362
+ lambda_start = source.find("detects_violation=lambda")
363
+ if lambda_start != -1:
364
+ # Get everything after "detects_violation=lambda"
365
+ lambda_part = source[
366
+ lambda_start + len("detects_violation=lambda") :
367
+ ].strip()
368
+ # Find the colon that separates param from body
369
+ colon_idx = lambda_part.find(":")
370
+ if colon_idx != -1:
371
+ # Extract just the body (condition)
372
+ body = lambda_part[colon_idx + 1 :].strip()
373
+ # Remove trailing comma if present
374
+ body = body.rstrip(",").strip()
375
+ source = body
376
+ except (OSError, TypeError):
377
+ source = ""
378
+
379
+ if source and sig:
380
+ return f"{sig}: {source}"
381
+ if source:
382
+ return source
383
+ if sig:
384
+ return sig
385
+
386
+ rendered = repr(func).strip()
387
+ return rendered or None
@@ -59,6 +59,7 @@ class AllFieldsSelector(BaseModel):
59
59
  """All fields selector."""
60
60
 
61
61
  selector_type: Literal["all"] = Field(alias="$selectorType")
62
+ sources: list[FieldSource]
62
63
 
63
64
  model_config = ConfigDict(populate_by_name=True, extra="allow")
64
65
 
@@ -92,7 +93,10 @@ class WordRule(BaseModel):
92
93
 
93
94
  rule_type: Literal["word"] = Field(alias="$ruleType")
94
95
  field_selector: FieldSelector = Field(alias="fieldSelector")
95
- func: Callable[[str], bool] = Field(exclude=True)
96
+ detects_violation: Callable[[str], bool] = Field(
97
+ exclude=True,
98
+ description="Function that returns True if the string violates the rule (validation should fail).",
99
+ )
96
100
 
97
101
  model_config = ConfigDict(populate_by_name=True, extra="allow")
98
102
 
@@ -111,7 +115,10 @@ class NumberRule(BaseModel):
111
115
 
112
116
  rule_type: Literal["number"] = Field(alias="$ruleType")
113
117
  field_selector: FieldSelector = Field(alias="fieldSelector")
114
- func: Callable[[float], bool] = Field(exclude=True)
118
+ detects_violation: Callable[[float], bool] = Field(
119
+ exclude=True,
120
+ description="Function that returns True if the number violates the rule (validation should fail).",
121
+ )
115
122
 
116
123
  model_config = ConfigDict(populate_by_name=True, extra="allow")
117
124
 
@@ -121,7 +128,10 @@ class BooleanRule(BaseModel):
121
128
 
122
129
  rule_type: Literal["boolean"] = Field(alias="$ruleType")
123
130
  field_selector: FieldSelector = Field(alias="fieldSelector")
124
- func: Callable[[bool], bool] = Field(exclude=True)
131
+ detects_violation: Callable[[bool], bool] = Field(
132
+ exclude=True,
133
+ description="Function that returns True if the boolean violates the rule (validation should fail).",
134
+ )
125
135
 
126
136
  model_config = ConfigDict(populate_by_name=True, extra="allow")
127
137
 
@@ -164,7 +174,7 @@ class BaseGuardrail(BaseModel):
164
174
  class DeterministicGuardrail(BaseGuardrail):
165
175
  """Deterministic guardrail model."""
166
176
 
167
- guardrail_type: Literal["custom"] = Field(alias="custom")
177
+ guardrail_type: Literal["custom"] = Field(alias="$guardrailType")
168
178
  rules: list[Rule]
169
179
 
170
180
  model_config = ConfigDict(populate_by_name=True, extra="allow")