uipath-openai-agents 0.0.1__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.
@@ -0,0 +1,353 @@
1
+ """Factory for creating OpenAI Agents runtimes from openai_agents.json configuration."""
2
+
3
+ import asyncio
4
+ import os
5
+ from typing import Any
6
+
7
+ from agents import Agent
8
+ from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor
9
+ from uipath.core.tracing import UiPathSpanUtils
10
+ from uipath.runtime import (
11
+ UiPathRuntimeContext,
12
+ UiPathRuntimeProtocol,
13
+ )
14
+ from uipath.runtime.errors import UiPathErrorCategory
15
+
16
+ from uipath_openai_agents.runtime._telemetry import get_current_span_wrapper
17
+ from uipath_openai_agents.runtime.agent import OpenAiAgentLoader
18
+ from uipath_openai_agents.runtime.config import OpenAiAgentsConfig
19
+ from uipath_openai_agents.runtime.errors import (
20
+ UiPathOpenAIAgentsErrorCode,
21
+ UiPathOpenAIAgentsRuntimeError,
22
+ )
23
+ from uipath_openai_agents.runtime.runtime import UiPathOpenAIAgentRuntime
24
+ from uipath_openai_agents.runtime.storage import SqliteAgentStorage
25
+
26
+
27
+ class UiPathOpenAIAgentRuntimeFactory:
28
+ """Factory for creating OpenAI Agent runtimes from openai_agents.json configuration."""
29
+
30
+ def __init__(
31
+ self,
32
+ context: UiPathRuntimeContext,
33
+ ):
34
+ """
35
+ Initialize the factory.
36
+
37
+ Args:
38
+ context: UiPathRuntimeContext to use for runtime creation
39
+ """
40
+ self.context = context
41
+ self._config: OpenAiAgentsConfig | None = None
42
+
43
+ self._agent_cache: dict[str, Agent] = {}
44
+ self._agent_loaders: dict[str, OpenAiAgentLoader] = {}
45
+ self._agent_lock = asyncio.Lock()
46
+
47
+ self._storage: SqliteAgentStorage | None = None
48
+ self._storage_lock = asyncio.Lock()
49
+
50
+ self._setup_instrumentation()
51
+
52
+ def _setup_instrumentation(self) -> None:
53
+ """Setup tracing and instrumentation."""
54
+ OpenAIAgentsInstrumentor().instrument()
55
+ UiPathSpanUtils.register_current_span_provider(get_current_span_wrapper)
56
+
57
+ async def _get_or_create_storage(self) -> SqliteAgentStorage | None:
58
+ """Get or create the shared storage instance.
59
+
60
+ Returns:
61
+ Shared storage instance, or None if storage is disabled
62
+ """
63
+ async with self._storage_lock:
64
+ if self._storage is None:
65
+ storage_path = self._get_storage_path()
66
+ if storage_path:
67
+ self._storage = SqliteAgentStorage(storage_path)
68
+ await self._storage.setup()
69
+ return self._storage
70
+
71
+ def _remove_file_with_retry(self, path: str, max_attempts: int = 5) -> None:
72
+ """Remove file with retry logic for Windows file locking.
73
+
74
+ OpenAI SDK uses sync sqlite3 which doesn't immediately release file locks
75
+ on Windows. This retry mechanism gives the OS time to release the lock.
76
+
77
+ Args:
78
+ path: Path to file to remove
79
+ max_attempts: Maximum number of retry attempts (default: 5)
80
+
81
+ Raises:
82
+ OSError: If file cannot be removed after all retries
83
+ """
84
+ import time
85
+
86
+ for attempt in range(max_attempts):
87
+ try:
88
+ os.remove(path)
89
+ return # Success
90
+ except PermissionError:
91
+ if attempt == max_attempts - 1:
92
+ # Last attempt failed, re-raise
93
+ raise
94
+ # Exponential backoff: 0.1s, 0.2s, 0.4s, 0.8s
95
+ time.sleep(0.1 * (2**attempt))
96
+
97
+ def _get_storage_path(self) -> str | None:
98
+ """Get the storage path for agent state.
99
+
100
+ Returns:
101
+ Path to SQLite database for storage, or None if storage is disabled
102
+ """
103
+ if self.context.state_file_path is not None:
104
+ return self.context.state_file_path
105
+
106
+ if self.context.runtime_dir and self.context.state_file:
107
+ path = os.path.join(self.context.runtime_dir, self.context.state_file)
108
+ if (
109
+ not self.context.resume
110
+ and self.context.job_id is None
111
+ and not self.context.keep_state_file
112
+ ):
113
+ # If not resuming and no job id, delete the previous state file
114
+ if os.path.exists(path):
115
+ self._remove_file_with_retry(path)
116
+ os.makedirs(self.context.runtime_dir, exist_ok=True)
117
+ return path
118
+
119
+ default_path = os.path.join("__uipath", "state.db")
120
+ os.makedirs(os.path.dirname(default_path), exist_ok=True)
121
+ return default_path
122
+
123
+ def _get_storage_path_legacy(self, runtime_id: str) -> str | None:
124
+ """
125
+ Get the storage path for agent session state.
126
+
127
+ Args:
128
+ runtime_id: Unique identifier for the runtime instance
129
+
130
+ Returns:
131
+ Path to SQLite database for session storage, or None if storage is disabled
132
+ """
133
+ if self.context.runtime_dir and self.context.state_file:
134
+ # Use state file name pattern but with runtime_id
135
+ base_name = os.path.splitext(self.context.state_file)[0]
136
+ file_name = f"{base_name}_{runtime_id}.db"
137
+ path = os.path.join(self.context.runtime_dir, file_name)
138
+
139
+ if not self.context.resume and self.context.job_id is None:
140
+ # If not resuming and no job id, delete the previous state file
141
+ if os.path.exists(path):
142
+ self._remove_file_with_retry(path)
143
+
144
+ os.makedirs(self.context.runtime_dir, exist_ok=True)
145
+ return path
146
+
147
+ # Default storage path
148
+ default_dir = os.path.join("__uipath", "sessions")
149
+ os.makedirs(default_dir, exist_ok=True)
150
+ return os.path.join(default_dir, f"{runtime_id}.db")
151
+
152
+ def _load_config(self) -> OpenAiAgentsConfig:
153
+ """Load openai_agents.json configuration."""
154
+ if self._config is None:
155
+ self._config = OpenAiAgentsConfig()
156
+ return self._config
157
+
158
+ async def _load_agent(self, entrypoint: str) -> Agent:
159
+ """
160
+ Load an agent for the given entrypoint.
161
+
162
+ Args:
163
+ entrypoint: Name of the agent to load
164
+
165
+ Returns:
166
+ The loaded Agent
167
+
168
+ Raises:
169
+ UiPathOpenAIAgentRuntimeError: If agent cannot be loaded
170
+ """
171
+ config = self._load_config()
172
+ if not config.exists:
173
+ raise UiPathOpenAIAgentsRuntimeError(
174
+ UiPathOpenAIAgentsErrorCode.CONFIG_MISSING,
175
+ "Invalid configuration",
176
+ "Failed to load openai_agents.json configuration",
177
+ UiPathErrorCategory.DEPLOYMENT,
178
+ )
179
+
180
+ if entrypoint not in config.agents:
181
+ available = ", ".join(config.entrypoint)
182
+ raise UiPathOpenAIAgentsRuntimeError(
183
+ UiPathOpenAIAgentsErrorCode.AGENT_NOT_FOUND,
184
+ "Agent not found",
185
+ f"Agent '{entrypoint}' not found. Available: {available}",
186
+ UiPathErrorCategory.DEPLOYMENT,
187
+ )
188
+
189
+ path = config.agents[entrypoint]
190
+ agent_loader = OpenAiAgentLoader.from_path_string(entrypoint, path)
191
+
192
+ self._agent_loaders[entrypoint] = agent_loader
193
+
194
+ try:
195
+ return await agent_loader.load()
196
+ except UiPathOpenAIAgentsRuntimeError:
197
+ # Re-raise our own errors as-is
198
+ raise
199
+ except ImportError as e:
200
+ raise UiPathOpenAIAgentsRuntimeError(
201
+ UiPathOpenAIAgentsErrorCode.AGENT_IMPORT_ERROR,
202
+ "Agent import failed",
203
+ f"Failed to import agent '{entrypoint}': {str(e)}",
204
+ UiPathErrorCategory.USER,
205
+ ) from e
206
+ except TypeError as e:
207
+ raise UiPathOpenAIAgentsRuntimeError(
208
+ UiPathOpenAIAgentsErrorCode.AGENT_TYPE_ERROR,
209
+ "Invalid agent type",
210
+ f"Agent '{entrypoint}' is not a valid OpenAI Agent: {str(e)}",
211
+ UiPathErrorCategory.USER,
212
+ ) from e
213
+ except ValueError as e:
214
+ raise UiPathOpenAIAgentsRuntimeError(
215
+ UiPathOpenAIAgentsErrorCode.AGENT_VALUE_ERROR,
216
+ "Invalid agent value",
217
+ f"Invalid value in agent '{entrypoint}': {str(e)}",
218
+ UiPathErrorCategory.USER,
219
+ ) from e
220
+ except Exception as e:
221
+ raise UiPathOpenAIAgentsRuntimeError(
222
+ UiPathOpenAIAgentsErrorCode.AGENT_LOAD_ERROR,
223
+ "Failed to load agent",
224
+ f"Unexpected error loading agent '{entrypoint}': {str(e)}",
225
+ UiPathErrorCategory.USER,
226
+ ) from e
227
+
228
+ async def _resolve_agent(self, entrypoint: str) -> Agent:
229
+ """
230
+ Resolve an agent from configuration.
231
+ Results are cached for reuse across multiple runtime instances.
232
+
233
+ Args:
234
+ entrypoint: Name of the agent to resolve
235
+
236
+ Returns:
237
+ The loaded Agent ready for execution
238
+
239
+ Raises:
240
+ UiPathOpenAIAgentRuntimeError: If resolution fails
241
+ """
242
+ async with self._agent_lock:
243
+ if entrypoint in self._agent_cache:
244
+ return self._agent_cache[entrypoint]
245
+
246
+ loaded_agent = await self._load_agent(entrypoint)
247
+ self._agent_cache[entrypoint] = loaded_agent
248
+
249
+ return loaded_agent
250
+
251
+ def discover_entrypoints(self) -> list[str]:
252
+ """
253
+ Discover all agent entrypoints.
254
+
255
+ Returns:
256
+ List of agent names that can be used as entrypoints
257
+ """
258
+ config = self._load_config()
259
+ if not config.exists:
260
+ return []
261
+ return config.entrypoint
262
+
263
+ async def discover_runtimes(self) -> list[UiPathRuntimeProtocol]:
264
+ """
265
+ Discover runtime instances for all entrypoints.
266
+
267
+ Returns:
268
+ List of OpenAI Agent runtime instances, one per entrypoint
269
+ """
270
+ entrypoints = self.discover_entrypoints()
271
+
272
+ runtimes: list[UiPathRuntimeProtocol] = []
273
+ for entrypoint in entrypoints:
274
+ agent = await self._resolve_agent(entrypoint)
275
+
276
+ runtime = await self._create_runtime_instance(
277
+ agent=agent,
278
+ runtime_id=entrypoint,
279
+ entrypoint=entrypoint,
280
+ )
281
+ runtimes.append(runtime)
282
+
283
+ return runtimes
284
+
285
+ async def _create_runtime_instance(
286
+ self,
287
+ agent: Agent,
288
+ runtime_id: str,
289
+ entrypoint: str,
290
+ ) -> UiPathRuntimeProtocol:
291
+ """
292
+ Create a runtime instance from an agent.
293
+
294
+ Args:
295
+ agent: The OpenAI Agent
296
+ runtime_id: Unique identifier for the runtime instance
297
+ entrypoint: Agent entrypoint name
298
+
299
+ Returns:
300
+ Configured runtime instance
301
+ """
302
+ # Get shared storage instance
303
+ storage = await self._get_or_create_storage()
304
+ storage_path = storage.storage_path if storage else None
305
+
306
+ # Get the loaded object from the agent loader for schema inference
307
+ loaded_object = None
308
+ if entrypoint in self._agent_loaders:
309
+ loaded_object = self._agent_loaders[entrypoint].get_loaded_object()
310
+
311
+ return UiPathOpenAIAgentRuntime(
312
+ agent=agent,
313
+ runtime_id=runtime_id,
314
+ entrypoint=entrypoint,
315
+ storage_path=storage_path,
316
+ loaded_object=loaded_object,
317
+ storage=storage,
318
+ )
319
+
320
+ async def new_runtime(
321
+ self, entrypoint: str, runtime_id: str, **kwargs: Any
322
+ ) -> UiPathRuntimeProtocol:
323
+ """
324
+ Create a new OpenAI Agent runtime instance.
325
+
326
+ Args:
327
+ entrypoint: Agent name from openai_agents.json
328
+ runtime_id: Unique identifier for the runtime instance
329
+ **kwargs: Additional keyword arguments (unused)
330
+
331
+ Returns:
332
+ Configured runtime instance with agent
333
+ """
334
+ agent = await self._resolve_agent(entrypoint)
335
+
336
+ return await self._create_runtime_instance(
337
+ agent=agent,
338
+ runtime_id=runtime_id,
339
+ entrypoint=entrypoint,
340
+ )
341
+
342
+ async def dispose(self) -> None:
343
+ """Cleanup factory resources."""
344
+ for loader in self._agent_loaders.values():
345
+ await loader.cleanup()
346
+
347
+ self._agent_loaders.clear()
348
+ self._agent_cache.clear()
349
+
350
+ # Dispose shared storage
351
+ if self._storage:
352
+ await self._storage.dispose()
353
+ self._storage = None