uipath-core 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl

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.
@@ -40,7 +40,7 @@ class DeterministicGuardrailsService(BaseModel):
40
40
  if has_output_rule:
41
41
  return GuardrailValidationResult(
42
42
  result=GuardrailValidationResultType.PASSED,
43
- reason="Guardrail contains output-dependent rules that will be evaluated during post-execution",
43
+ reason="No rules to apply for input data.",
44
44
  )
45
45
  return self._evaluate_deterministic_guardrail(
46
46
  input_data=input_data,
@@ -66,7 +66,7 @@ class DeterministicGuardrailsService(BaseModel):
66
66
  if not has_output_rule:
67
67
  return GuardrailValidationResult(
68
68
  result=GuardrailValidationResultType.PASSED,
69
- reason="Guardrail contains only input-dependent rules that were evaluated during pre-execution",
69
+ reason="No rules to apply for output data.",
70
70
  )
71
71
 
72
72
  return self._evaluate_deterministic_guardrail(
@@ -117,7 +117,12 @@ class DeterministicGuardrailsService(BaseModel):
117
117
  output_data: dict[str, Any],
118
118
  guardrail: DeterministicGuardrail,
119
119
  ) -> GuardrailValidationResult:
120
- """Evaluate deterministic guardrail rules against input and output data."""
120
+ """Evaluate deterministic guardrail rules against input and output data.
121
+
122
+ Validation fails only if ALL guardrail rules are violated.
123
+ """
124
+ validated_conditions: list[str] = []
125
+
121
126
  for rule in guardrail.rules:
122
127
  if isinstance(rule, WordRule):
123
128
  passed, reason = evaluate_word_rule(rule, input_data, output_data)
@@ -132,14 +137,25 @@ class DeterministicGuardrailsService(BaseModel):
132
137
  result=GuardrailValidationResultType.VALIDATION_FAILED,
133
138
  reason=f"Unknown rule type: {type(rule)}",
134
139
  )
135
-
136
- if not passed:
140
+ validated_conditions.append(reason)
141
+ if passed:
137
142
  return GuardrailValidationResult(
138
- result=GuardrailValidationResultType.VALIDATION_FAILED,
139
- reason=reason or "Rule validation failed",
143
+ result=GuardrailValidationResultType.PASSED,
144
+ reason=reason,
140
145
  )
141
146
 
147
+ has_always_rule = any(
148
+ condition == "Always rule enforced" for condition in validated_conditions
149
+ )
150
+
151
+ validated_conditions_str = ", ".join(validated_conditions)
152
+ final_reason = (
153
+ "Always rule enforced"
154
+ if has_always_rule
155
+ else f"Data matched all guardrail conditions: [{validated_conditions_str}]"
156
+ )
157
+
142
158
  return GuardrailValidationResult(
143
- result=GuardrailValidationResultType.PASSED,
144
- reason="All deterministic guardrail rules passed",
159
+ result=GuardrailValidationResultType.VALIDATION_FAILED,
160
+ reason=final_reason,
145
161
  )
@@ -159,24 +159,44 @@ def get_fields_from_selector(
159
159
  return fields
160
160
 
161
161
 
162
- def format_guardrail_error_message(
162
+ def format_guardrail_passed_validation_result_message(
163
163
  field_ref: FieldReference,
164
- operator: str,
165
- expected_value: str | None = None,
164
+ operator: str | None,
165
+ rule_description: str | None,
166
166
  ) -> str:
167
- """Format a guardrail error message following the standard pattern."""
167
+ """Format a guardrail validation result message following the standard pattern."""
168
168
  source = "Input" if field_ref.source == FieldSource.INPUT else "Output"
169
- message = f"{source} data didn't match the guardrail condition: [{field_ref.path}] comparing function [{operator}]"
170
- if expected_value and expected_value.strip():
171
- message += f" [{expected_value.strip()}]"
172
- return message
169
+
170
+ if rule_description:
171
+ return (
172
+ f"{source} data didn't match the guardrail condition for field "
173
+ f"[{field_ref.path}]: {rule_description}"
174
+ )
175
+
176
+ return (
177
+ f"{source} data didn't match the guardrail condition: "
178
+ f"[{field_ref.path}] comparing function [{operator}]"
179
+ )
180
+
181
+
182
+ def get_validated_conditions_description(
183
+ field_path: str,
184
+ operator: str | None,
185
+ rule_description: str | None,
186
+ ) -> str:
187
+ if rule_description:
188
+ return rule_description
189
+
190
+ return f"[{field_path}] comparing function [{operator}]"
173
191
 
174
192
 
175
193
  def evaluate_word_rule(
176
194
  rule: WordRule, input_data: dict[str, Any], output_data: dict[str, Any]
177
- ) -> tuple[bool, str | None]:
195
+ ) -> tuple[bool, str]:
178
196
  """Evaluate a word rule against input and output data."""
179
197
  fields = get_fields_from_selector(rule.field_selector, input_data, output_data)
198
+ operator = _humanize_guardrail_func(rule.detects_violation) or "violation check"
199
+ field_paths = ", ".join({field_ref.path for _, field_ref in fields})
180
200
 
181
201
  for field_value, field_ref in fields:
182
202
  if field_value is None:
@@ -197,22 +217,28 @@ def evaluate_word_rule(
197
217
  # If function raises an exception, treat as failure
198
218
  violation_detected = True
199
219
 
200
- if violation_detected:
201
- operator = (
202
- _humanize_guardrail_func(rule.detects_violation) or "violation check"
220
+ if not violation_detected:
221
+ reason = format_guardrail_passed_validation_result_message(
222
+ field_ref=field_ref,
223
+ operator=operator,
224
+ rule_description=rule.rule_description,
203
225
  )
204
- reason = format_guardrail_error_message(field_ref, operator, None)
205
- return False, reason
226
+ return True, reason
206
227
 
207
- return True, "All word rule validations passed"
228
+ return False, get_validated_conditions_description(
229
+ field_path=field_paths,
230
+ operator=operator,
231
+ rule_description=rule.rule_description,
232
+ )
208
233
 
209
234
 
210
235
  def evaluate_number_rule(
211
236
  rule: NumberRule, input_data: dict[str, Any], output_data: dict[str, Any]
212
- ) -> tuple[bool, str | None]:
237
+ ) -> tuple[bool, str]:
213
238
  """Evaluate a number rule against input and output data."""
214
239
  fields = get_fields_from_selector(rule.field_selector, input_data, output_data)
215
-
240
+ operator = _humanize_guardrail_func(rule.detects_violation) or "violation check"
241
+ field_paths = ", ".join({field_ref.path for _, field_ref in fields})
216
242
  for field_value, field_ref in fields:
217
243
  if field_value is None:
218
244
  continue
@@ -233,24 +259,30 @@ def evaluate_number_rule(
233
259
  # If function raises an exception, treat as failure
234
260
  violation_detected = True
235
261
 
236
- if violation_detected:
237
- operator = (
238
- _humanize_guardrail_func(rule.detects_violation) or "violation check"
262
+ if not violation_detected:
263
+ reason = format_guardrail_passed_validation_result_message(
264
+ field_ref=field_ref,
265
+ operator=operator,
266
+ rule_description=rule.rule_description,
239
267
  )
240
- reason = format_guardrail_error_message(field_ref, operator, None)
241
- return False, reason
268
+ return True, reason
242
269
 
243
- return True, "All number rule validations passed"
270
+ return False, get_validated_conditions_description(
271
+ field_path=field_paths,
272
+ operator=operator,
273
+ rule_description=rule.rule_description,
274
+ )
244
275
 
245
276
 
246
277
  def evaluate_boolean_rule(
247
278
  rule: BooleanRule,
248
279
  input_data: dict[str, Any],
249
280
  output_data: dict[str, Any],
250
- ) -> tuple[bool, str | None]:
281
+ ) -> tuple[bool, str]:
251
282
  """Evaluate a boolean rule against input and output data."""
252
283
  fields = get_fields_from_selector(rule.field_selector, input_data, output_data)
253
-
284
+ operator = _humanize_guardrail_func(rule.detects_violation) or "violation check"
285
+ field_paths = ", ".join({field_ref.path for _, field_ref in fields})
254
286
  for field_value, field_ref in fields:
255
287
  if field_value is None:
256
288
  continue
@@ -270,20 +302,25 @@ def evaluate_boolean_rule(
270
302
  # If function raises an exception, treat as failure
271
303
  violation_detected = True
272
304
 
273
- if violation_detected:
274
- operator = (
275
- _humanize_guardrail_func(rule.detects_violation) or "violation check"
305
+ if not violation_detected:
306
+ reason = format_guardrail_passed_validation_result_message(
307
+ field_ref=field_ref,
308
+ operator=operator,
309
+ rule_description=rule.rule_description,
276
310
  )
277
- reason = format_guardrail_error_message(field_ref, operator, None)
278
- return False, reason
311
+ return True, reason
279
312
 
280
- return True, "All boolean rule validations passed"
313
+ return False, get_validated_conditions_description(
314
+ field_path=field_paths,
315
+ operator=operator,
316
+ rule_description=rule.rule_description,
317
+ )
281
318
 
282
319
 
283
320
  def evaluate_universal_rule(
284
321
  rule: UniversalRule,
285
322
  output_data: dict[str, Any],
286
- ) -> tuple[bool, str | None]:
323
+ ) -> tuple[bool, str]:
287
324
  """Evaluate a universal rule against input and output data.
288
325
 
289
326
  Universal rules trigger based on the apply_to scope and execution phase:
@@ -302,18 +339,18 @@ def evaluate_universal_rule(
302
339
  if rule.apply_to == ApplyTo.INPUT:
303
340
  # INPUT: triggers in pre-execution, does not trigger in post-execution
304
341
  if is_pre_execution:
305
- return False, "Universal rule validation triggered (pre-execution, input)"
342
+ return False, "Always rule enforced"
306
343
  else:
307
- return True, "Universal rule validation passed (post-execution, input)"
344
+ return True, "No rules to apply for output data"
308
345
  elif rule.apply_to == ApplyTo.OUTPUT:
309
346
  # OUTPUT: does not trigger in pre-execution, triggers in post-execution
310
347
  if is_pre_execution:
311
- return True, "Universal rule validation passed (pre-execution, output)"
348
+ return True, "No rules to apply for input data"
312
349
  else:
313
- return False, "Universal rule validation triggered (post-execution, output)"
350
+ return False, "Always rule enforced"
314
351
  elif rule.apply_to == ApplyTo.INPUT_AND_OUTPUT:
315
352
  # INPUT_AND_OUTPUT: triggers in both phases
316
- return False, "Universal rule validation triggered (input and output)"
353
+ return False, "Always rule enforced"
317
354
  else:
318
355
  return False, f"Unknown apply_to value: {rule.apply_to}"
319
356
 
@@ -102,6 +102,11 @@ class WordRule(BaseModel):
102
102
 
103
103
  rule_type: Literal["word"] = Field(alias="$ruleType")
104
104
  field_selector: FieldSelector = Field(alias="fieldSelector")
105
+ rule_description: str | None = Field(
106
+ default=None,
107
+ exclude=True,
108
+ description="Human-friendly description of the rule condition.",
109
+ )
105
110
  detects_violation: Callable[[str], bool] = Field(
106
111
  exclude=True,
107
112
  description="Function that returns True if the string violates the rule (validation should fail).",
@@ -124,6 +129,11 @@ class NumberRule(BaseModel):
124
129
 
125
130
  rule_type: Literal["number"] = Field(alias="$ruleType")
126
131
  field_selector: FieldSelector = Field(alias="fieldSelector")
132
+ rule_description: str | None = Field(
133
+ default=None,
134
+ exclude=True,
135
+ description="Human-friendly description of the rule condition.",
136
+ )
127
137
  detects_violation: Callable[[float], bool] = Field(
128
138
  exclude=True,
129
139
  description="Function that returns True if the number violates the rule (validation should fail).",
@@ -137,6 +147,11 @@ class BooleanRule(BaseModel):
137
147
 
138
148
  rule_type: Literal["boolean"] = Field(alias="$ruleType")
139
149
  field_selector: FieldSelector = Field(alias="fieldSelector")
150
+ rule_description: str | None = Field(
151
+ default=None,
152
+ exclude=True,
153
+ description="Human-friendly description of the rule condition.",
154
+ )
140
155
  detects_violation: Callable[[bool], bool] = Field(
141
156
  exclude=True,
142
157
  description="Function that returns True if the boolean violates the rule (validation should fail).",
@@ -9,7 +9,7 @@ from typing import Any, Callable, Optional
9
9
  from opentelemetry import context as context_api
10
10
  from opentelemetry import trace
11
11
  from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY
12
- from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags
12
+ from opentelemetry.trace import SpanContext, TraceFlags
13
13
  from opentelemetry.trace.status import StatusCode
14
14
 
15
15
  from uipath.core.tracing._utils import (
@@ -17,7 +17,11 @@ from uipath.core.tracing._utils import (
17
17
  set_span_input_attributes,
18
18
  set_span_output_attributes,
19
19
  )
20
- from uipath.core.tracing.span_utils import UiPathSpanUtils
20
+ from uipath.core.tracing.span_utils import (
21
+ ParentedNonRecordingSpan,
22
+ UiPathSpanUtils,
23
+ _span_registry,
24
+ )
21
25
 
22
26
  logger = logging.getLogger(__name__)
23
27
 
@@ -50,7 +54,10 @@ def _opentelemetry_traced(
50
54
  trace_name = name or func.__name__
51
55
 
52
56
  def get_span():
57
+ ctx = UiPathSpanUtils.get_parent_context()
53
58
  if not recording:
59
+ parent_context = trace.get_current_span(ctx).get_span_context()
60
+
54
61
  # Create a valid but non-sampled trace context
55
62
  # Generate a valid trace ID (not INVALID)
56
63
  trace_id = random.getrandbits(128)
@@ -62,20 +69,24 @@ def _opentelemetry_traced(
62
69
  is_remote=False,
63
70
  trace_flags=TraceFlags(0x00), # NOT sampled
64
71
  )
65
- non_recording = NonRecordingSpan(non_sampled_context)
72
+ non_recording = ParentedNonRecordingSpan(
73
+ non_sampled_context, parent=parent_context
74
+ )
66
75
 
67
76
  # Make it active so children see it
68
77
  span_cm = trace.use_span(non_recording)
69
78
  span_cm.__enter__()
70
- return span_cm, non_recording
71
79
 
72
- # Normal recording span
73
- ctx = UiPathSpanUtils.get_parent_context()
74
- span_cm = trace.get_tracer(__name__).start_as_current_span(
75
- trace_name, context=ctx
76
- )
77
- span = span_cm.__enter__()
78
- return span_cm, span
80
+ _span_registry.register_span(non_recording)
81
+
82
+ return span_cm, non_recording
83
+ else:
84
+ # Normal recording span
85
+ span_cm = trace.get_tracer(__name__).start_as_current_span(
86
+ trace_name, context=ctx
87
+ )
88
+ span = span_cm.__enter__()
89
+ return span_cm, span
79
90
 
80
91
  # --------- Sync wrapper ---------
81
92
  @wraps(func)
@@ -4,11 +4,33 @@ import logging
4
4
  from typing import Callable, Optional
5
5
 
6
6
  from opentelemetry import context, trace
7
- from opentelemetry.trace import Span, set_span_in_context
7
+ from opentelemetry.trace import NonRecordingSpan, Span, set_span_in_context
8
8
 
9
9
  logger = logging.getLogger(__name__)
10
10
 
11
11
 
12
+ class ParentedNonRecordingSpan(NonRecordingSpan):
13
+ """Non-recording span with explicit parent tracking.
14
+
15
+ Extends NonRecordingSpan to include a parent attribute, allowing the SpanRegistry
16
+ to properly track parent-child relationships for non-recording spans.
17
+ This is necessary because NonRecordingSpan instances created directly don't have
18
+ their parent automatically set like normal recording spans do.
19
+ """
20
+
21
+ def __init__(
22
+ self, context: trace.SpanContext, parent: Optional[trace.SpanContext] = None
23
+ ):
24
+ """Initialize a parented non-recording span.
25
+
26
+ Args:
27
+ context: The SpanContext for this span
28
+ parent: Optional parent SpanContext
29
+ """
30
+ super().__init__(context)
31
+ self.parent = parent
32
+
33
+
12
34
  class SpanRegistry:
13
35
  """Registry to track all spans and their parent relationships."""
14
36
 
@@ -309,4 +331,4 @@ class UiPathSpanUtils:
309
331
  return UiPathSpanUtils._current_span_ancestors_provider
310
332
 
311
333
 
312
- __all__ = ["UiPathSpanUtils"]
334
+ __all__ = ["ParentedNonRecordingSpan", "UiPathSpanUtils"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uipath-core
3
- Version: 0.1.7
3
+ Version: 0.1.9
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
@@ -15,17 +15,17 @@ uipath/core/chat/tool.py,sha256=6e5pyX3hOWM5fIzr_fdG49Mbzz6XzJD3nsmha-yGa2k,2308
15
15
  uipath/core/errors/__init__.py,sha256=gjxdLibZ0fjwgzPuLJY04P8dIX9rbSM2wQ97jP34ucE,278
16
16
  uipath/core/errors/errors.py,sha256=5LajjuTfNW82ju07wT5mD3tXk0S-Ju7OqJqQpPN0F6g,486
17
17
  uipath/core/guardrails/__init__.py,sha256=hUCmD4y5te2iy01YnJlBuf2RWvqxmsNzoyOamXLXf2E,1028
18
- uipath/core/guardrails/_deterministic_guardrails_service.py,sha256=uX8f2HSGZ1bf5QFM9fnz0CvglxI3UIqY4OhHrKzemaU,5967
19
- uipath/core/guardrails/_evaluators.py,sha256=ovmVm-8iB8Pm9arjG7mHM9-GIRkrG3V6oHRPHKxZVO0,14601
20
- uipath/core/guardrails/guardrails.py,sha256=DraeFkoDKVDnc0EKdFYYP29lQu7U2hneTsj1dxveHU4,4935
18
+ uipath/core/guardrails/_deterministic_guardrails_service.py,sha256=61ROXYmX3rjBfFUp7E83fm4Lk7h1DlJeukpZm7uzVqQ,6355
19
+ uipath/core/guardrails/_evaluators.py,sha256=10tIRUufxoy9MkZPb-ytjsCSCIfIqQSOYXyEl4G7PSw,15649
20
+ uipath/core/guardrails/guardrails.py,sha256=sTxsNHilEX908aF-WtQWVOQ4dCCwMgAepDebH35ngvo,5430
21
21
  uipath/core/tracing/__init__.py,sha256=1XNLYZ4J76XkRrizGO486mS6yxzVXUbrldpvxTyJe3E,483
22
22
  uipath/core/tracing/_utils.py,sha256=FiCFGOFa4czruhlSF87Q5Q4jX9KKPHZiw8k14K7W5v4,6636
23
- uipath/core/tracing/decorators.py,sha256=nE57gAb5Ul6c3ep2Nkkqr40SO_eiCoJL8_4VQ0AiHjY,11643
23
+ uipath/core/tracing/decorators.py,sha256=JDNULkUu-Ufg8pJslG4i6Q2pmqaGNDS8NFRPgFs29Dw,11937
24
24
  uipath/core/tracing/exporters.py,sha256=FClouEEQfk3F8J7G_NFoarDJM3R0-gA5jUxA5xRHx5s,1562
25
25
  uipath/core/tracing/processors.py,sha256=R_652rtjPmfpUtaXoIcmfZrRZylVXFRNwjOmJUUxOQw,1408
26
- uipath/core/tracing/span_utils.py,sha256=WYBrd6ZbawAs7r1Js-Zvo9_8GzkD9LhHNOls00bK_xI,12235
26
+ uipath/core/tracing/span_utils.py,sha256=LZXNdnI0-fhKe49CLPsvMJIfh9zdzk8rK4g4YN5RfDU,13064
27
27
  uipath/core/tracing/trace_manager.py,sha256=51rscJcepkTK4bWoCZdE-DFc9wt2F-aSuFBaSXmkHl0,3130
28
- uipath_core-0.1.7.dist-info/METADATA,sha256=vpXcfigfRmjzvhsyAm8fFs8dEu7BAJ6yAmv_ltKxhzY,938
29
- uipath_core-0.1.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
30
- uipath_core-0.1.7.dist-info/licenses/LICENSE,sha256=-KBavWXepyDjimmzH5fVAsi-6jNVpIKFc2kZs0Ri4ng,1058
31
- uipath_core-0.1.7.dist-info/RECORD,,
28
+ uipath_core-0.1.9.dist-info/METADATA,sha256=wLk7EKaDR01IqA7RvbhRzw13dw2DRX6twknAm793_OY,938
29
+ uipath_core-0.1.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
30
+ uipath_core-0.1.9.dist-info/licenses/LICENSE,sha256=-KBavWXepyDjimmzH5fVAsi-6jNVpIKFc2kZs0Ri4ng,1058
31
+ uipath_core-0.1.9.dist-info/RECORD,,