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,532 @@
1
+ """Runtime class for executing OpenAI Agents within the UiPath framework."""
2
+
3
+ import json
4
+ import os
5
+ from typing import Any, AsyncGenerator
6
+ from uuid import uuid4
7
+
8
+ from agents import (
9
+ Agent,
10
+ Runner,
11
+ SQLiteSession,
12
+ )
13
+ from uipath.runtime import (
14
+ UiPathExecuteOptions,
15
+ UiPathRuntimeResult,
16
+ UiPathRuntimeStatus,
17
+ UiPathStreamOptions,
18
+ )
19
+ from uipath.runtime.errors import UiPathErrorCategory, UiPathErrorCode
20
+ from uipath.runtime.events import (
21
+ UiPathRuntimeEvent,
22
+ UiPathRuntimeMessageEvent,
23
+ UiPathRuntimeStateEvent,
24
+ )
25
+ from uipath.runtime.schema import UiPathRuntimeSchema
26
+
27
+ from ._serialize import serialize_output
28
+ from .errors import UiPathOpenAIAgentsErrorCode, UiPathOpenAIAgentsRuntimeError
29
+ from .schema import get_agent_schema, get_entrypoints_schema
30
+ from .storage import SqliteAgentStorage
31
+
32
+
33
+ class UiPathOpenAIAgentRuntime:
34
+ """
35
+ A runtime class for executing OpenAI Agents within the UiPath framework.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ agent: Agent,
41
+ runtime_id: str | None = None,
42
+ entrypoint: str | None = None,
43
+ storage_path: str | None = None,
44
+ loaded_object: Any | None = None,
45
+ storage: SqliteAgentStorage | None = None,
46
+ ):
47
+ """
48
+ Initialize the runtime.
49
+
50
+ Args:
51
+ agent: The OpenAI Agent to execute
52
+ runtime_id: Unique identifier for this runtime instance
53
+ entrypoint: Optional entrypoint name (for schema generation)
54
+ storage_path: Path to SQLite database for session persistence
55
+ loaded_object: Original loaded object (for schema inference)
56
+ storage: Optional storage instance for state persistence
57
+ """
58
+ self.agent: Agent = agent
59
+ self.runtime_id: str = runtime_id or "default"
60
+ self.entrypoint: str | None = entrypoint
61
+ self.storage_path: str | None = storage_path
62
+ self.loaded_object: Any | None = loaded_object
63
+ self.storage: SqliteAgentStorage | None = storage
64
+
65
+ # Configure OpenAI Agents SDK to use Responses API
66
+ # UiPath supports both APIs via X-UiPath-LlmGateway-ApiFlavor header
67
+ # Using responses API for enhanced agent capabilities (conversation state, reasoning)
68
+ from agents import set_default_openai_api
69
+
70
+ set_default_openai_api("responses")
71
+
72
+ # Inject UiPath OpenAI client if UiPath credentials are available
73
+ self._setup_uipath_client()
74
+
75
+ def _setup_uipath_client(self) -> None:
76
+ """Set up UiPath OpenAI client for agents to use UiPath gateway.
77
+
78
+ This injects the UiPath OpenAI client into the OpenAI Agents SDK
79
+ so all agents use the UiPath LLM Gateway instead of direct OpenAI.
80
+
81
+ The model is automatically extracted from the agent's `model` parameter.
82
+ If not specified in Agent(), the SDK uses agents.models.get_default_model().
83
+
84
+ If UiPath credentials are not available, falls back to default OpenAI client.
85
+ """
86
+ try:
87
+ # Import here to avoid circular dependency
88
+ from uipath_openai_agents.chat import UiPathChatOpenAI
89
+
90
+ # Check if UiPath credentials are available
91
+ org_id = os.getenv("UIPATH_ORGANIZATION_ID")
92
+ tenant_id = os.getenv("UIPATH_TENANT_ID")
93
+ token = os.getenv("UIPATH_ACCESS_TOKEN")
94
+ uipath_url = os.getenv("UIPATH_URL")
95
+
96
+ if org_id and tenant_id and token and uipath_url:
97
+ # Extract model from agent definition
98
+ from agents.models import get_default_model
99
+
100
+ from uipath_openai_agents.chat.supported_models import OpenAIModels
101
+
102
+ if hasattr(self.agent, "model") and self.agent.model:
103
+ model_name = str(self.agent.model)
104
+ else:
105
+ model_name = get_default_model()
106
+
107
+ # Normalize generic model names to UiPath-specific versions
108
+ model_name = OpenAIModels.normalize_model_name(model_name)
109
+
110
+ # Update agent's model to normalized version so SDK sends correct model in body
111
+ self.agent.model = model_name
112
+
113
+ # Create UiPath OpenAI client
114
+ uipath_client = UiPathChatOpenAI(
115
+ token=token,
116
+ org_id=org_id,
117
+ tenant_id=tenant_id,
118
+ model_name=model_name,
119
+ )
120
+
121
+ # Inject into OpenAI Agents SDK
122
+ # This makes all agents use UiPath gateway
123
+ from agents.models import _openai_shared
124
+
125
+ _openai_shared.set_default_openai_client(uipath_client.async_client)
126
+
127
+ except ImportError:
128
+ # UiPath chat module not available, skip injection
129
+ pass
130
+ except Exception:
131
+ # If injection fails, fall back to default OpenAI client
132
+ # Agents will use OPENAI_API_KEY if set
133
+ pass
134
+
135
+ async def execute(
136
+ self,
137
+ input: dict[str, Any] | None = None,
138
+ options: UiPathExecuteOptions | None = None,
139
+ ) -> UiPathRuntimeResult:
140
+ """
141
+ Execute the agent with the provided input and configuration.
142
+
143
+ Args:
144
+ input: Input dictionary containing the message for the agent
145
+ options: Execution options
146
+
147
+ Returns:
148
+ UiPathRuntimeResult with the agent's output
149
+
150
+ Raises:
151
+ UiPathOpenAIAgentRuntimeError: If execution fails
152
+ """
153
+ try:
154
+ result: UiPathRuntimeResult | None = None
155
+ async for event in self._run_agent(input, options, stream_events=False):
156
+ if isinstance(event, UiPathRuntimeResult):
157
+ result = event
158
+
159
+ if result is None:
160
+ raise RuntimeError("Agent completed without returning a result")
161
+
162
+ return result
163
+
164
+ except Exception as e:
165
+ raise self._create_runtime_error(e) from e
166
+
167
+ async def stream(
168
+ self,
169
+ input: dict[str, Any] | None = None,
170
+ options: UiPathStreamOptions | None = None,
171
+ ) -> AsyncGenerator[UiPathRuntimeEvent, None]:
172
+ """
173
+ Stream agent execution events in real-time.
174
+
175
+ Args:
176
+ input: Input dictionary containing the message for the agent
177
+ options: Stream options
178
+
179
+ Yields:
180
+ UiPathRuntimeEvent instances during execution,
181
+ then the final UiPathRuntimeResult
182
+
183
+ Raises:
184
+ UiPathOpenAIAgentRuntimeError: If execution fails
185
+ """
186
+ try:
187
+ async for event in self._run_agent(input, options, stream_events=True):
188
+ yield event
189
+ except Exception as e:
190
+ raise self._create_runtime_error(e) from e
191
+
192
+ async def _run_agent(
193
+ self,
194
+ input: dict[str, Any] | None,
195
+ options: UiPathExecuteOptions | UiPathStreamOptions | None,
196
+ stream_events: bool,
197
+ ) -> AsyncGenerator[UiPathRuntimeEvent | UiPathRuntimeResult, None]:
198
+ """
199
+ Core agent execution logic used by both execute() and stream().
200
+
201
+ Args:
202
+ input: Input dictionary
203
+ options: Execution/stream options
204
+ stream_events: Whether to stream events during execution
205
+
206
+ Yields:
207
+ Runtime events if stream_events=True, then final result
208
+ """
209
+ agent_input = self._prepare_agent_input(input)
210
+ is_resuming = bool(options and options.resume)
211
+
212
+ # Create session for state persistence (local to this run)
213
+ # SQLiteSession automatically loads existing data from the database when created
214
+ session: SQLiteSession | None = None
215
+ if self.storage_path:
216
+ session = SQLiteSession(self.runtime_id, self.storage_path)
217
+
218
+ # Run the agent with streaming if events requested
219
+ try:
220
+ if stream_events:
221
+ # Use streaming for events
222
+ async for event_or_result in self._run_agent_streamed(
223
+ agent_input, options, stream_events, session
224
+ ):
225
+ yield event_or_result
226
+ else:
227
+ # Use non-streaming for simple execution
228
+ result = await Runner.run(
229
+ starting_agent=self.agent,
230
+ input=agent_input,
231
+ session=session,
232
+ )
233
+ yield self._create_success_result(result.final_output)
234
+
235
+ except Exception:
236
+ # Clean up session on error
237
+ if session and self.storage_path and not is_resuming:
238
+ # Delete incomplete session
239
+ try:
240
+ import os
241
+
242
+ if os.path.exists(self.storage_path):
243
+ os.remove(self.storage_path)
244
+ except Exception:
245
+ pass # Best effort cleanup
246
+ raise
247
+ finally:
248
+ # Always close session after run completes with proper WAL checkpoint
249
+ if session:
250
+ self._close_session_with_checkpoint(session)
251
+
252
+ async def _run_agent_streamed(
253
+ self,
254
+ agent_input: str | list[Any],
255
+ options: UiPathExecuteOptions | UiPathStreamOptions | None,
256
+ stream_events: bool,
257
+ session: SQLiteSession | None,
258
+ ) -> AsyncGenerator[UiPathRuntimeEvent | UiPathRuntimeResult, None]:
259
+ """
260
+ Run agent using streaming API to enable event streaming.
261
+
262
+ Args:
263
+ agent_input: Prepared agent input (string or list of messages)
264
+ options: Execution/stream options
265
+ stream_events: Whether to yield streaming events to caller
266
+
267
+ Yields:
268
+ Runtime events if stream_events=True, then final result
269
+ """
270
+
271
+ # Use Runner.run_streamed() for streaming events (returns RunResultStreaming directly)
272
+ result = Runner.run_streamed(
273
+ starting_agent=self.agent,
274
+ input=agent_input,
275
+ session=session,
276
+ )
277
+
278
+ # Stream events from the agent
279
+ async for event in result.stream_events():
280
+ # Emit the event to caller if streaming is enabled
281
+ if stream_events:
282
+ runtime_event = self._convert_stream_event_to_runtime_event(event)
283
+ if runtime_event:
284
+ yield runtime_event
285
+
286
+ # Stream complete - yield final result
287
+ yield self._create_success_result(result.final_output)
288
+
289
+ def _convert_stream_event_to_runtime_event(
290
+ self,
291
+ event: Any,
292
+ ) -> UiPathRuntimeEvent | None:
293
+ """
294
+ Convert OpenAI streaming event to UiPath runtime event.
295
+
296
+ Args:
297
+ event: Streaming event from Runner.run_streamed()
298
+
299
+ Returns:
300
+ UiPathRuntimeEvent or None if event should be filtered
301
+ """
302
+
303
+ event_type = getattr(event, "type", None)
304
+ event_name = getattr(event, "name", None)
305
+
306
+ # Handle run item events (messages, tool calls, etc.)
307
+ if event_type == "run_item_stream_event":
308
+ event_item = getattr(event, "item", None)
309
+ if event_item:
310
+ # Determine if this is a message or state event
311
+ if event_name in ["message_output_created", "reasoning_item_created"]:
312
+ return UiPathRuntimeMessageEvent(
313
+ payload=serialize_output(event_item),
314
+ metadata={"event_name": event_name},
315
+ )
316
+ else:
317
+ return UiPathRuntimeStateEvent(
318
+ payload=serialize_output(event_item),
319
+ metadata={"event_name": event_name},
320
+ )
321
+
322
+ # Handle agent updated events
323
+ if event_type == "agent_updated_stream_event":
324
+ new_agent = getattr(event, "new_agent", None)
325
+ if new_agent:
326
+ return UiPathRuntimeStateEvent(
327
+ payload={"agent_name": getattr(new_agent, "name", "unknown")},
328
+ metadata={"event_type": "agent_updated"},
329
+ )
330
+
331
+ # Filter out raw response events (too granular)
332
+ return None
333
+
334
+ def _prepare_agent_input(self, input: dict[str, Any] | None) -> str | list[Any]:
335
+ """
336
+ Prepare agent input from UiPath input dictionary.
337
+
338
+ Supports two input formats:
339
+ - {"message": "text"} → returns string for Runner.run()
340
+ - {"messages": [...]} → returns list of message dicts for Runner.run()
341
+
342
+ Note: When using sessions, string input is preferred as it doesn't
343
+ require a session_input_callback.
344
+
345
+ Args:
346
+ input: Input dictionary from UiPath
347
+
348
+ Returns:
349
+ String or list for Runner.run() input parameter
350
+
351
+ Raises:
352
+ ValueError: If input doesn't contain "message" or "messages" field
353
+ """
354
+ if not input:
355
+ raise ValueError(
356
+ "Input is required. Provide either 'message' (string) or 'messages' (list of message dicts)"
357
+ )
358
+
359
+ # Check for "messages" field (list of message dicts)
360
+ if "messages" in input:
361
+ messages = input["messages"]
362
+ # Ensure it's a list
363
+ if isinstance(messages, list):
364
+ return messages
365
+ else:
366
+ raise ValueError(
367
+ "'messages' field must be a list of message dictionaries"
368
+ )
369
+
370
+ # Check for "message" field (simple string)
371
+ if "message" in input:
372
+ message = input["message"]
373
+ # Return as string (OpenAI Agents SDK handles string → message conversion)
374
+ return str(message)
375
+
376
+ # No valid field found
377
+ raise ValueError(
378
+ "Input must contain either 'message' (string) or 'messages' (list of message dicts). "
379
+ f"Got keys: {list(input.keys())}"
380
+ )
381
+
382
+ def _serialize_message(self, message: Any) -> dict[str, Any]:
383
+ """
384
+ Serialize an agent message for event streaming.
385
+
386
+ Args:
387
+ message: Message object from the agent
388
+
389
+ Returns:
390
+ Dictionary representation of the message
391
+ """
392
+ serialized = serialize_output(message)
393
+
394
+ # Ensure the result is a dictionary
395
+ if isinstance(serialized, dict):
396
+ return serialized
397
+
398
+ # Fallback to wrapping in a content field
399
+ return {"content": serialized}
400
+
401
+ def _create_success_result(self, output: Any) -> UiPathRuntimeResult:
402
+ """
403
+ Create result for successful completion.
404
+
405
+ Args:
406
+ output: The agent's output
407
+
408
+ Returns:
409
+ UiPathRuntimeResult with serialized output
410
+ """
411
+ # Serialize output
412
+ serialized_output = self._serialize_output(output)
413
+
414
+ # Ensure output is a dictionary
415
+ if not isinstance(serialized_output, dict):
416
+ serialized_output = {"result": serialized_output}
417
+
418
+ return UiPathRuntimeResult(
419
+ output=serialized_output,
420
+ status=UiPathRuntimeStatus.SUCCESSFUL,
421
+ )
422
+
423
+ def _serialize_output(self, output: Any) -> Any:
424
+ """
425
+ Serialize agent output to a JSON-compatible format.
426
+
427
+ Args:
428
+ output: Output from the agent
429
+
430
+ Returns:
431
+ JSON-compatible representation
432
+ """
433
+ return serialize_output(output)
434
+
435
+ def _create_runtime_error(self, e: Exception) -> UiPathOpenAIAgentsRuntimeError:
436
+ """
437
+ Handle execution errors and create appropriate runtime error.
438
+
439
+ Args:
440
+ e: The exception that occurred
441
+
442
+ Returns:
443
+ UiPathOpenAIAgentsRuntimeError with appropriate error code
444
+ """
445
+ if isinstance(e, UiPathOpenAIAgentsRuntimeError):
446
+ return e
447
+
448
+ detail = f"Error: {str(e)}"
449
+
450
+ if isinstance(e, json.JSONDecodeError):
451
+ return UiPathOpenAIAgentsRuntimeError(
452
+ UiPathErrorCode.INPUT_INVALID_JSON,
453
+ "Invalid JSON input",
454
+ detail,
455
+ UiPathErrorCategory.USER,
456
+ )
457
+
458
+ if isinstance(e, TimeoutError):
459
+ return UiPathOpenAIAgentsRuntimeError(
460
+ UiPathOpenAIAgentsErrorCode.TIMEOUT_ERROR,
461
+ "Agent execution timed out",
462
+ detail,
463
+ UiPathErrorCategory.USER,
464
+ )
465
+
466
+ return UiPathOpenAIAgentsRuntimeError(
467
+ UiPathOpenAIAgentsErrorCode.AGENT_EXECUTION_FAILURE,
468
+ "Agent execution failed",
469
+ detail,
470
+ UiPathErrorCategory.USER,
471
+ )
472
+
473
+ async def get_schema(self) -> UiPathRuntimeSchema:
474
+ """
475
+ Get schema for this OpenAI Agent runtime.
476
+
477
+ Returns:
478
+ UiPathRuntimeSchema with input/output schemas and graph structure
479
+ """
480
+ entrypoints_schema = get_entrypoints_schema(self.agent, self.loaded_object)
481
+
482
+ return UiPathRuntimeSchema(
483
+ filePath=self.entrypoint,
484
+ uniqueId=str(uuid4()),
485
+ type="agent",
486
+ input=entrypoints_schema.get("input", {}),
487
+ output=entrypoints_schema.get("output", {}),
488
+ graph=get_agent_schema(self.agent),
489
+ )
490
+
491
+ def _close_session_with_checkpoint(self, session: SQLiteSession) -> None:
492
+ """Close SQLite session with WAL checkpoint to release file locks.
493
+
494
+ OpenAI SDK uses sync sqlite3 which doesn't release file locks on Windows
495
+ without explicit WAL checkpoint. This is especially important for cleanup.
496
+
497
+ Args:
498
+ session: The SQLiteSession to close
499
+ """
500
+ try:
501
+ # Get the underlying connection
502
+ conn = session._get_connection()
503
+
504
+ # Commit any pending transactions
505
+ try:
506
+ conn.commit()
507
+ except Exception:
508
+ pass # Best effort
509
+
510
+ # Force WAL checkpoint to release shared memory files
511
+ # This is especially important on Windows
512
+ try:
513
+ conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
514
+ conn.commit()
515
+ except Exception:
516
+ pass # Best effort
517
+
518
+ except Exception:
519
+ pass # Best effort cleanup
520
+
521
+ finally:
522
+ # Always call the session's close method
523
+ try:
524
+ session.close()
525
+ except Exception:
526
+ pass # Best effort
527
+
528
+ async def dispose(self) -> None:
529
+ """Cleanup runtime resources."""
530
+ # Sessions are closed immediately after each run in _run_agent()
531
+ # Storage is shared across runtimes and managed by the factory
532
+ pass