uipath 2.1.60__py3-none-any.whl → 2.1.61__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.
@@ -753,17 +753,24 @@ class UiPathRuntimeFactory(Generic[T, C]):
753
753
  span_processor.force_flush()
754
754
 
755
755
  async def execute_in_root_span(
756
- self, context: C, root_span: str = "root"
756
+ self,
757
+ context: C,
758
+ root_span: str = "root",
759
+ attributes: Optional[dict[str, str]] = None,
757
760
  ) -> Optional[UiPathRuntimeResult]:
758
761
  """Execute runtime with context."""
759
762
  async with self.from_context(context) as runtime:
760
763
  try:
761
764
  tracer: Tracer = trace.get_tracer("uipath-runtime")
765
+ span_attributes = {}
766
+ if context.execution_id:
767
+ span_attributes["execution.id"] = context.execution_id
768
+ if attributes:
769
+ span_attributes.update(attributes)
770
+
762
771
  with tracer.start_as_current_span(
763
772
  root_span,
764
- attributes={"execution.id": context.execution_id}
765
- if context.execution_id
766
- else {},
773
+ attributes=span_attributes,
767
774
  ):
768
775
  return await runtime.execute()
769
776
  finally:
@@ -1,35 +1,41 @@
1
- from typing import Optional, Tuple
1
+ from typing import Any, Optional, Tuple
2
2
 
3
- import httpx
4
-
5
- from ..._utils._ssl_context import get_httpx_client_kwargs
6
3
  from ._console import ConsoleLogger
7
4
 
8
5
  console = ConsoleLogger()
9
6
 
10
7
 
11
- def get_personal_workspace_info(
12
- base_url: str, token: str
13
- ) -> Tuple[Optional[str], Optional[str]]:
14
- user_url = f"{base_url}/orchestrator_/odata/Users/UiPath.Server.Configuration.OData.GetCurrentUserExtended?$expand=PersonalWorkspace"
8
+ async def get_personal_workspace_info_async() -> Tuple[Optional[str], Optional[str]]:
9
+ response = await _get_personal_workspace_info_internal_async()
10
+ feed_id = response.get("PersonalWorskpaceFeedId")
11
+ personal_workspace = response.get("PersonalWorkspace")
12
+
13
+ if not personal_workspace or not feed_id or "Id" not in personal_workspace:
14
+ return None, None
15
+
16
+ folder_id = personal_workspace.get("Id")
17
+ return feed_id, folder_id
18
+
19
+
20
+ async def get_personal_workspace_key_async() -> Optional[str]:
21
+ response = await _get_personal_workspace_info_internal_async()
22
+ personal_workspace = response.get("PersonalWorkspace")
23
+ if not personal_workspace or "Key" not in personal_workspace:
24
+ return None
25
+ return personal_workspace["Key"]
26
+
15
27
 
16
- with httpx.Client(**get_httpx_client_kwargs()) as client:
17
- user_response = client.get(
18
- user_url, headers={"Authorization": f"Bearer {token}"}
19
- )
28
+ async def _get_personal_workspace_info_internal_async() -> dict[str, Any]:
29
+ from ... import UiPath
20
30
 
21
- if user_response.status_code != 200:
22
- console.error(
23
- "Error: Failed to fetch user info. Please try reauthenticating."
24
- )
25
- return None, None
31
+ uipath = UiPath()
26
32
 
27
- user_data = user_response.json()
28
- feed_id = user_data.get("PersonalWorskpaceFeedId")
29
- personal_workspace = user_data.get("PersonalWorkspace")
33
+ response = await uipath.api_client.request_async(
34
+ method="GET",
35
+ url="/orchestrator_/odata/Users/UiPath.Server.Configuration.OData.GetCurrentUserExtended?$expand=PersonalWorkspace",
36
+ )
30
37
 
31
- if not personal_workspace or not feed_id or "Id" not in personal_workspace:
32
- return None, None
38
+ if response.status_code != 200:
39
+ console.error("Error: Failed to fetch user info. Please try reauthenticating.")
33
40
 
34
- folder_id = personal_workspace.get("Id")
35
- return feed_id, folder_id
41
+ return response.json()
uipath/_cli/cli_eval.py CHANGED
@@ -2,17 +2,24 @@
2
2
  import ast
3
3
  import asyncio
4
4
  import os
5
+ import uuid
5
6
  from typing import List, Optional
6
7
 
7
8
  import click
8
9
 
9
- from uipath._cli._evals._runtime import UiPathEvalContext, UiPathEvalRuntime
10
+ from uipath._cli._evals._progress_reporter import StudioWebProgressReporter
11
+ from uipath._cli._evals._runtime import (
12
+ UiPathEvalContext,
13
+ UiPathEvalRuntime,
14
+ )
10
15
  from uipath._cli._runtime._contracts import (
11
16
  UiPathRuntimeContext,
12
17
  UiPathRuntimeFactory,
13
18
  )
14
19
  from uipath._cli._runtime._runtime import UiPathScriptRuntime
20
+ from uipath._cli._utils._folders import get_personal_workspace_key_async
15
21
  from uipath._cli.middlewares import Middlewares
22
+ from uipath._events._event_bus import EventBus
16
23
  from uipath.eval._helpers import auto_discover_entrypoint
17
24
  from uipath.tracing import LlmOpsHttpExporter
18
25
 
@@ -72,6 +79,11 @@ def eval(
72
79
  workers: Number of parallel workers for running evaluations
73
80
  no_report: Do not report the evaluation results
74
81
  """
82
+ if not no_report and not os.getenv("UIPATH_FOLDER_KEY"):
83
+ os.environ["UIPATH_FOLDER_KEY"] = asyncio.run(
84
+ get_personal_workspace_key_async()
85
+ )
86
+
75
87
  result = Middlewares.next(
76
88
  "eval",
77
89
  entrypoint,
@@ -86,14 +98,23 @@ def eval(
86
98
  console.error(result.error_message)
87
99
 
88
100
  if result.should_continue:
101
+ event_bus = EventBus()
102
+
103
+ if not no_report:
104
+ progress_reporter = StudioWebProgressReporter()
105
+ asyncio.run(progress_reporter.subscribe_to_eval_runtime_events(event_bus))
89
106
 
90
107
  def generate_runtime_context(**context_kwargs) -> UiPathRuntimeContext:
91
108
  runtime_context = UiPathRuntimeContext.with_defaults(**context_kwargs)
92
109
  runtime_context.entrypoint = runtime_entrypoint
93
110
  return runtime_context
94
111
 
112
+ runtime_entrypoint = entrypoint or auto_discover_entrypoint()
113
+
95
114
  eval_context = UiPathEvalContext.with_defaults(
96
- execution_output_file=output_file
115
+ execution_output_file=output_file,
116
+ entrypoint=runtime_entrypoint,
117
+ execution_id=str(uuid.uuid4()),
97
118
  )
98
119
 
99
120
  eval_context.no_report = no_report
@@ -101,22 +122,23 @@ def eval(
101
122
  eval_context.eval_set = eval_set or EvalHelpers.auto_discover_eval_set()
102
123
  eval_context.eval_ids = eval_ids
103
124
 
104
- runtime_entrypoint = entrypoint or auto_discover_entrypoint()
105
-
106
125
  try:
107
126
  runtime_factory = UiPathRuntimeFactory(
108
127
  UiPathScriptRuntime,
109
128
  UiPathRuntimeContext,
110
129
  context_generator=generate_runtime_context,
111
130
  )
112
- if not no_report or eval_context.job_id:
131
+ if eval_context.job_id:
113
132
  runtime_factory.add_span_exporter(LlmOpsHttpExporter())
114
133
 
115
134
  async def execute():
116
135
  async with UiPathEvalRuntime.from_eval_context(
117
- factory=runtime_factory, context=eval_context
136
+ factory=runtime_factory,
137
+ context=eval_context,
138
+ event_bus=event_bus,
118
139
  ) as eval_runtime:
119
140
  await eval_runtime.execute()
141
+ await event_bus.wait_for_all(timeout=10)
120
142
 
121
143
  asyncio.run(execute())
122
144
  except Exception as e:
uipath/_cli/cli_invoke.py CHANGED
@@ -1,4 +1,5 @@
1
1
  # type: ignore
2
+ import asyncio
2
3
  import logging
3
4
  import os
4
5
  from typing import Optional
@@ -16,7 +17,7 @@ except ImportError:
16
17
  from .._utils._ssl_context import get_httpx_client_kwargs
17
18
  from ..telemetry import track
18
19
  from ._utils._common import get_env_vars
19
- from ._utils._folders import get_personal_workspace_info
20
+ from ._utils._folders import get_personal_workspace_info_async
20
21
  from ._utils._processes import get_release_info
21
22
  from .middlewares import Middlewares
22
23
 
@@ -65,7 +66,9 @@ def invoke(
65
66
  [base_url, token] = get_env_vars()
66
67
 
67
68
  url = f"{base_url}/orchestrator_/odata/Jobs/UiPath.Server.Configuration.OData.StartJobs"
68
- _, personal_workspace_folder_id = get_personal_workspace_info(base_url, token)
69
+ _, personal_workspace_folder_id = asyncio.run(
70
+ get_personal_workspace_info_async()
71
+ )
69
72
  project_name, project_version = _read_project_details()
70
73
  if not personal_workspace_folder_id:
71
74
  console.error(
@@ -1,4 +1,5 @@
1
1
  # type: ignore
2
+ import asyncio
2
3
  import json
3
4
  import os
4
5
 
@@ -9,7 +10,7 @@ from .._utils._ssl_context import get_httpx_client_kwargs
9
10
  from ..telemetry import track
10
11
  from ._utils._common import get_env_vars
11
12
  from ._utils._console import ConsoleLogger
12
- from ._utils._folders import get_personal_workspace_info
13
+ from ._utils._folders import get_personal_workspace_info_async
13
14
  from ._utils._processes import get_release_info
14
15
 
15
16
  console = ConsoleLogger()
@@ -106,8 +107,8 @@ def publish(feed):
106
107
 
107
108
  if feed and feed != "tenant":
108
109
  # Check user personal workspace
109
- personal_workspace_feed_id, personal_workspace_folder_id = (
110
- get_personal_workspace_info(base_url, token)
110
+ personal_workspace_feed_id, personal_workspace_folder_id = asyncio.run(
111
+ get_personal_workspace_info_async()
111
112
  )
112
113
  if feed == "personal" or feed == personal_workspace_feed_id:
113
114
  is_personal_workspace = True
File without changes
@@ -0,0 +1,157 @@
1
+ """Event bus implementation for evaluation events."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Any, Callable, Dict, List, Optional, Set, TypeVar
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ class EventBus:
13
+ """Event bus for publishing and subscribing to events."""
14
+
15
+ def __init__(self) -> None:
16
+ """Initialize a new EventBus instance."""
17
+ self._subscribers: Dict[str, List[Callable[[Any], Any]]] = {}
18
+ self._running_tasks: Set[asyncio.Task[Any]] = set()
19
+
20
+ def subscribe(self, topic: str, handler: Callable[[Any], Any]) -> None:
21
+ """Subscribe a handler method/function to a topic.
22
+
23
+ Args:
24
+ topic: The topic name to subscribe to.
25
+ handler: The async handler method/function that will handle events for this topic.
26
+ """
27
+ if topic not in self._subscribers:
28
+ self._subscribers[topic] = []
29
+ self._subscribers[topic].append(handler)
30
+ logger.debug(f"Handler registered for topic: {topic}")
31
+
32
+ def unsubscribe(self, topic: str, handler: Callable[[Any], Any]) -> None:
33
+ """Unsubscribe a handler from a topic.
34
+
35
+ Args:
36
+ topic: The topic name to unsubscribe from.
37
+ handler: The handler to remove.
38
+ """
39
+ if topic in self._subscribers:
40
+ try:
41
+ self._subscribers[topic].remove(handler)
42
+ if not self._subscribers[topic]:
43
+ del self._subscribers[topic]
44
+ logger.debug(f"Handler unregistered from topic: {topic}")
45
+ except ValueError:
46
+ logger.warning(f"Handler not found for topic: {topic}")
47
+
48
+ def _cleanup_completed_tasks(self) -> None:
49
+ completed_tasks = {task for task in self._running_tasks if task.done()}
50
+ self._running_tasks -= completed_tasks
51
+
52
+ async def publish(
53
+ self, topic: str, payload: T, wait_for_completion: bool = True
54
+ ) -> None:
55
+ """Publish an event to all handlers of a topic.
56
+
57
+ Args:
58
+ topic: The topic name to publish to.
59
+ payload: The event payload to publish.
60
+ wait_for_completion: Whether to wait for the event to be processed.
61
+ """
62
+ if topic not in self._subscribers:
63
+ logger.debug(f"No handlers for topic: {topic}")
64
+ return
65
+
66
+ self._cleanup_completed_tasks()
67
+
68
+ tasks = []
69
+ for subscriber in self._subscribers[topic]:
70
+ try:
71
+ task = asyncio.create_task(subscriber(payload))
72
+ tasks.append(task)
73
+ self._running_tasks.add(task)
74
+ except Exception as e:
75
+ logger.error(f"Error creating task for subscriber {subscriber}: {e}")
76
+
77
+ if tasks and wait_for_completion:
78
+ try:
79
+ await asyncio.gather(*tasks, return_exceptions=True)
80
+ except Exception as e:
81
+ logger.error(f"Error during event processing for topic {topic}: {e}")
82
+ finally:
83
+ # Clean up the tasks we just waited for
84
+ for task in tasks:
85
+ self._running_tasks.discard(task)
86
+
87
+ def get_running_tasks_count(self) -> int:
88
+ """Get the number of currently running subscriber tasks.
89
+
90
+ Returns:
91
+ Number of running tasks.
92
+ """
93
+ self._cleanup_completed_tasks()
94
+ return len(self._running_tasks)
95
+
96
+ async def wait_for_all(self, timeout: Optional[float] = None) -> None:
97
+ """Wait for all currently running subscriber tasks to complete.
98
+
99
+ Args:
100
+ timeout: Maximum time to wait in seconds. If None, waits indefinitely.
101
+ """
102
+ self._cleanup_completed_tasks()
103
+
104
+ if not self._running_tasks:
105
+ logger.debug("No running tasks to wait for")
106
+ return
107
+
108
+ logger.debug(
109
+ f"Waiting for {len(self._running_tasks)} EventBus tasks to complete..."
110
+ )
111
+
112
+ try:
113
+ tasks_to_wait = list(self._running_tasks)
114
+
115
+ if timeout:
116
+ await asyncio.wait_for(
117
+ asyncio.gather(*tasks_to_wait, return_exceptions=True),
118
+ timeout=timeout,
119
+ )
120
+ else:
121
+ await asyncio.gather(*tasks_to_wait, return_exceptions=True)
122
+
123
+ logger.debug("All EventBus tasks completed")
124
+
125
+ except asyncio.TimeoutError:
126
+ logger.warning(f"Timeout waiting for EventBus tasks after {timeout}s")
127
+ for task in tasks_to_wait:
128
+ if not task.done():
129
+ task.cancel()
130
+ except Exception as e:
131
+ logger.error(f"Error waiting for EventBus tasks: {e}")
132
+ finally:
133
+ self._cleanup_completed_tasks()
134
+
135
+ def get_subscribers_count(self, topic: str) -> int:
136
+ """Get the number of subscribers for a topic.
137
+
138
+ Args:
139
+ topic: The topic name.
140
+
141
+ Returns:
142
+ Number of handlers for the topic.
143
+ """
144
+ return len(self._subscribers.get(topic, []))
145
+
146
+ def clear_subscribers(self, topic: Optional[str] = None) -> None:
147
+ """Clear subscribers for a topic or all topics.
148
+
149
+ Args:
150
+ topic: The topic to clear. If None, clears all topics.
151
+ """
152
+ if topic is None:
153
+ self._subscribers.clear()
154
+ logger.debug("All handlers cleared")
155
+ elif topic in self._subscribers:
156
+ del self._subscribers[topic]
157
+ logger.debug(f"Handlers cleared for topic: {topic}")
@@ -0,0 +1,53 @@
1
+ import enum
2
+ from typing import Any, List, Union
3
+
4
+ from opentelemetry.sdk.trace import ReadableSpan
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ from uipath._cli._evals._models._evaluation_set import EvaluationItem
8
+ from uipath.eval.models import EvalItemResult
9
+
10
+
11
+ class EvaluationEvents(str, enum.Enum):
12
+ CREATE_EVAL_SET_RUN = "create_eval_set_run"
13
+ CREATE_EVAL_RUN = "create_eval_run"
14
+ UPDATE_EVAL_SET_RUN = "update_eval_set_run"
15
+ UPDATE_EVAL_RUN = "update_eval_run"
16
+
17
+
18
+ class EvalSetRunCreatedEvent(BaseModel):
19
+ execution_id: str
20
+ entrypoint: str
21
+ eval_set_id: str
22
+ no_of_evals: int
23
+ evaluators: List[Any]
24
+
25
+
26
+ class EvalRunCreatedEvent(BaseModel):
27
+ execution_id: str
28
+ eval_item: EvaluationItem
29
+
30
+
31
+ class EvalRunUpdatedEvent(BaseModel):
32
+ model_config = ConfigDict(arbitrary_types_allowed=True)
33
+
34
+ execution_id: str
35
+ eval_item: EvaluationItem
36
+ eval_results: List[EvalItemResult]
37
+ success: bool
38
+ agent_output: Any
39
+ agent_execution_time: float
40
+ spans: List[ReadableSpan]
41
+
42
+
43
+ class EvalSetRunUpdatedEvent(BaseModel):
44
+ execution_id: str
45
+ evaluator_scores: dict[str, float]
46
+
47
+
48
+ ProgressEvent = Union[
49
+ EvalSetRunCreatedEvent,
50
+ EvalRunCreatedEvent,
51
+ EvalRunUpdatedEvent,
52
+ EvalSetRunUpdatedEvent,
53
+ ]
@@ -70,7 +70,7 @@ EvaluationResult = Annotated[
70
70
  class EvalItemResult(BaseModel):
71
71
  """Result of a single evaluation item."""
72
72
 
73
- evaluator_name: str
73
+ evaluator_id: str
74
74
  result: EvaluationResult
75
75
 
76
76
 
@@ -1,91 +1,95 @@
1
- import json
2
- import logging
3
- import os
4
- import time
5
- from typing import Any, Dict, Sequence
6
-
7
- import httpx
8
- from opentelemetry.sdk.trace import ReadableSpan
9
- from opentelemetry.sdk.trace.export import (
10
- SpanExporter,
11
- SpanExportResult,
12
- )
13
-
14
- from uipath._utils._ssl_context import get_httpx_client_kwargs
15
-
16
- from ._utils import _SpanUtils
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
-
21
- class LlmOpsHttpExporter(SpanExporter):
22
- """An OpenTelemetry span exporter that sends spans to UiPath LLM Ops."""
23
-
24
- def __init__(self, **client_kwargs):
25
- """Initialize the exporter with the base URL and authentication token."""
26
- super().__init__(**client_kwargs)
27
- self.base_url = self._get_base_url()
28
- self.auth_token = os.environ.get("UIPATH_ACCESS_TOKEN")
29
- self.headers = {
30
- "Content-Type": "application/json",
31
- "Authorization": f"Bearer {self.auth_token}",
32
- }
33
-
34
- client_kwargs = get_httpx_client_kwargs()
35
-
36
- self.http_client = httpx.Client(**client_kwargs, headers=self.headers)
37
-
38
- def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
39
- """Export spans to UiPath LLM Ops."""
40
- logger.debug(
41
- f"Exporting {len(spans)} spans to {self.base_url}/llmopstenant_/api/Traces/spans"
42
- )
43
-
44
- span_list = [
45
- _SpanUtils.otel_span_to_uipath_span(span).to_dict() for span in spans
46
- ]
47
- url = self._build_url(span_list)
48
-
49
- logger.debug("Payload: %s", json.dumps(span_list))
50
-
51
- return self._send_with_retries(url, span_list)
52
-
53
- def force_flush(self, timeout_millis: int = 30000) -> bool:
54
- """Force flush the exporter."""
55
- return True
56
-
57
- def _build_url(self, span_list: list[Dict[str, Any]]) -> str:
58
- """Construct the URL for the API request."""
59
- trace_id = str(span_list[0]["TraceId"])
60
- return f"{self.base_url}/llmopstenant_/api/Traces/spans?traceId={trace_id}&source=Robots"
61
-
62
- def _send_with_retries(
63
- self, url: str, payload: list[Dict[str, Any]], max_retries: int = 4
64
- ) -> SpanExportResult:
65
- """Send the HTTP request with retry logic."""
66
- for attempt in range(max_retries):
67
- try:
68
- response = self.http_client.post(url, json=payload)
69
- if response.status_code == 200:
70
- return SpanExportResult.SUCCESS
71
- else:
72
- logger.warning(
73
- f"Attempt {attempt + 1} failed with status code {response.status_code}: {response.text}"
74
- )
75
- except Exception as e:
76
- logger.error(f"Attempt {attempt + 1} failed with exception: {e}")
77
-
78
- if attempt < max_retries - 1:
79
- time.sleep(1.5**attempt) # Exponential backoff
80
-
81
- return SpanExportResult.FAILURE
82
-
83
- def _get_base_url(self) -> str:
84
- uipath_url = (
85
- os.environ.get("UIPATH_URL")
86
- or "https://cloud.uipath.com/dummyOrg/dummyTennant/"
87
- )
88
-
89
- uipath_url = uipath_url.rstrip("/")
90
-
91
- return uipath_url
1
+ import json
2
+ import logging
3
+ import os
4
+ import time
5
+ from typing import Any, Dict, Optional, Sequence
6
+
7
+ import httpx
8
+ from opentelemetry.sdk.trace import ReadableSpan
9
+ from opentelemetry.sdk.trace.export import (
10
+ SpanExporter,
11
+ SpanExportResult,
12
+ )
13
+
14
+ from uipath._utils._ssl_context import get_httpx_client_kwargs
15
+
16
+ from ._utils import _SpanUtils
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class LlmOpsHttpExporter(SpanExporter):
22
+ """An OpenTelemetry span exporter that sends spans to UiPath LLM Ops."""
23
+
24
+ def __init__(self, trace_id: Optional[str] = None, **client_kwargs):
25
+ """Initialize the exporter with the base URL and authentication token."""
26
+ super().__init__(**client_kwargs)
27
+ self.base_url = self._get_base_url()
28
+ self.auth_token = os.environ.get("UIPATH_ACCESS_TOKEN")
29
+ self.headers = {
30
+ "Content-Type": "application/json",
31
+ "Authorization": f"Bearer {self.auth_token}",
32
+ }
33
+
34
+ client_kwargs = get_httpx_client_kwargs()
35
+
36
+ self.http_client = httpx.Client(**client_kwargs, headers=self.headers)
37
+ self.trace_id = trace_id
38
+
39
+ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
40
+ """Export spans to UiPath LLM Ops."""
41
+ logger.debug(
42
+ f"Exporting {len(spans)} spans to {self.base_url}/llmopstenant_/api/Traces/spans"
43
+ )
44
+
45
+ span_list = [
46
+ _SpanUtils.otel_span_to_uipath_span(
47
+ span, custom_trace_id=self.trace_id
48
+ ).to_dict()
49
+ for span in spans
50
+ ]
51
+ url = self._build_url(span_list)
52
+
53
+ logger.debug("Payload: %s", json.dumps(span_list))
54
+
55
+ return self._send_with_retries(url, span_list)
56
+
57
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
58
+ """Force flush the exporter."""
59
+ return True
60
+
61
+ def _build_url(self, span_list: list[Dict[str, Any]]) -> str:
62
+ """Construct the URL for the API request."""
63
+ trace_id = str(span_list[0]["TraceId"])
64
+ return f"{self.base_url}/llmopstenant_/api/Traces/spans?traceId={trace_id}&source=Robots"
65
+
66
+ def _send_with_retries(
67
+ self, url: str, payload: list[Dict[str, Any]], max_retries: int = 4
68
+ ) -> SpanExportResult:
69
+ """Send the HTTP request with retry logic."""
70
+ for attempt in range(max_retries):
71
+ try:
72
+ response = self.http_client.post(url, json=payload)
73
+ if response.status_code == 200:
74
+ return SpanExportResult.SUCCESS
75
+ else:
76
+ logger.warning(
77
+ f"Attempt {attempt + 1} failed with status code {response.status_code}: {response.text}"
78
+ )
79
+ except Exception as e:
80
+ logger.error(f"Attempt {attempt + 1} failed with exception: {e}")
81
+
82
+ if attempt < max_retries - 1:
83
+ time.sleep(1.5**attempt) # Exponential backoff
84
+
85
+ return SpanExportResult.FAILURE
86
+
87
+ def _get_base_url(self) -> str:
88
+ uipath_url = (
89
+ os.environ.get("UIPATH_URL")
90
+ or "https://cloud.uipath.com/dummyOrg/dummyTennant/"
91
+ )
92
+
93
+ uipath_url = uipath_url.rstrip("/")
94
+
95
+ return uipath_url