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.
- uipath_openai_agents/__init__.py +7 -0
- uipath_openai_agents/_cli/__init__.py +1 -0
- uipath_openai_agents/_cli/_templates/AGENTS.md.template +21 -0
- uipath_openai_agents/_cli/_templates/main.py.template +28 -0
- uipath_openai_agents/_cli/_templates/openai_agents.json.template +5 -0
- uipath_openai_agents/_cli/cli_new.py +81 -0
- uipath_openai_agents/chat/__init__.py +5 -0
- uipath_openai_agents/chat/openai.py +242 -0
- uipath_openai_agents/chat/supported_models.py +78 -0
- uipath_openai_agents/middlewares.py +8 -0
- uipath_openai_agents/py.typed +0 -0
- uipath_openai_agents/runtime/__init__.py +40 -0
- uipath_openai_agents/runtime/_serialize.py +51 -0
- uipath_openai_agents/runtime/_sqlite.py +190 -0
- uipath_openai_agents/runtime/_telemetry.py +32 -0
- uipath_openai_agents/runtime/agent.py +201 -0
- uipath_openai_agents/runtime/config.py +55 -0
- uipath_openai_agents/runtime/errors.py +48 -0
- uipath_openai_agents/runtime/factory.py +353 -0
- uipath_openai_agents/runtime/runtime.py +532 -0
- uipath_openai_agents/runtime/schema.py +490 -0
- uipath_openai_agents/runtime/storage.py +357 -0
- uipath_openai_agents-0.0.1.dist-info/METADATA +53 -0
- uipath_openai_agents-0.0.1.dist-info/RECORD +26 -0
- uipath_openai_agents-0.0.1.dist-info/WHEEL +4 -0
- uipath_openai_agents-0.0.1.dist-info/entry_points.txt +5 -0
|
@@ -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
|