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,490 @@
1
+ """Schema extraction utilities for OpenAI Agents."""
2
+
3
+ import inspect
4
+ from typing import Any, get_args, get_origin, get_type_hints
5
+
6
+ from agents import Agent
7
+ from pydantic import BaseModel, TypeAdapter
8
+ from uipath.runtime.schema import (
9
+ UiPathRuntimeEdge,
10
+ UiPathRuntimeGraph,
11
+ UiPathRuntimeNode,
12
+ )
13
+
14
+
15
+ def _is_pydantic_model(type_hint: Any) -> bool:
16
+ """
17
+ Check if a type hint is a Pydantic BaseModel.
18
+
19
+ Args:
20
+ type_hint: A type hint from type annotations
21
+
22
+ Returns:
23
+ True if the type is a Pydantic model, False otherwise
24
+ """
25
+ try:
26
+ # Direct check
27
+ if inspect.isclass(type_hint) and issubclass(type_hint, BaseModel):
28
+ return True
29
+
30
+ # Handle generic types (e.g., Optional[Model])
31
+ origin = get_origin(type_hint)
32
+ if origin is not None:
33
+ args = get_args(type_hint)
34
+ for arg in args:
35
+ if inspect.isclass(arg) and issubclass(arg, BaseModel):
36
+ return True
37
+
38
+ except TypeError:
39
+ pass
40
+
41
+ return False
42
+
43
+
44
+ def _extract_schema_from_callable(callable_obj: Any) -> dict[str, Any] | None:
45
+ """
46
+ Extract input/output schemas from a callable's type annotations.
47
+
48
+ Args:
49
+ callable_obj: A callable object (function, async function, etc.)
50
+
51
+ Returns:
52
+ Dictionary with input and output schemas if type hints are found,
53
+ None otherwise
54
+ """
55
+ if not callable(callable_obj):
56
+ return None
57
+
58
+ try:
59
+ # Get type hints from the callable
60
+ type_hints = get_type_hints(callable_obj)
61
+
62
+ if not type_hints:
63
+ return None
64
+
65
+ # Get function signature to identify parameters
66
+ sig = inspect.signature(callable_obj)
67
+ params = list(sig.parameters.values())
68
+
69
+ # Find the first parameter (usually the input)
70
+ input_type = None
71
+
72
+ for param in params:
73
+ if param.name in ("self", "cls"):
74
+ continue
75
+ if param.name in type_hints:
76
+ input_type = type_hints[param.name]
77
+ break
78
+
79
+ # Get return type
80
+ return_type = type_hints.get("return")
81
+
82
+ schema: dict[str, Any] = {
83
+ "input": {"type": "object", "properties": {}, "required": []},
84
+ "output": {"type": "object", "properties": {}, "required": []},
85
+ }
86
+
87
+ # Extract input schema from Pydantic model
88
+ if input_type and _is_pydantic_model(input_type):
89
+ adapter = TypeAdapter(input_type)
90
+ input_schema = adapter.json_schema()
91
+ unpacked_input = _resolve_refs(input_schema)
92
+
93
+ schema["input"]["properties"] = _process_nullable_types(
94
+ unpacked_input.get("properties", {})
95
+ )
96
+ schema["input"]["required"] = unpacked_input.get("required", [])
97
+
98
+ # Add title and description if available
99
+ if "title" in unpacked_input:
100
+ schema["input"]["title"] = unpacked_input["title"]
101
+ if "description" in unpacked_input:
102
+ schema["input"]["description"] = unpacked_input["description"]
103
+
104
+ # Extract output schema from Pydantic model
105
+ if return_type and _is_pydantic_model(return_type):
106
+ adapter = TypeAdapter(return_type)
107
+ output_schema = adapter.json_schema()
108
+ unpacked_output = _resolve_refs(output_schema)
109
+
110
+ schema["output"]["properties"] = _process_nullable_types(
111
+ unpacked_output.get("properties", {})
112
+ )
113
+ schema["output"]["required"] = unpacked_output.get("required", [])
114
+
115
+ # Add title and description if available
116
+ if "title" in unpacked_output:
117
+ schema["output"]["title"] = unpacked_output["title"]
118
+ if "description" in unpacked_output:
119
+ schema["output"]["description"] = unpacked_output["description"]
120
+
121
+ # Only return schema if we found at least one Pydantic model
122
+ if schema["input"]["properties"] or schema["output"]["properties"]:
123
+ return schema
124
+
125
+ except Exception:
126
+ # If schema extraction fails, return None to fall back to default
127
+ pass
128
+
129
+ return None
130
+
131
+
132
+ def get_entrypoints_schema(
133
+ agent: Agent, loaded_object: Any | None = None
134
+ ) -> dict[str, Any]:
135
+ """
136
+ Extract input/output schema from an OpenAI Agent.
137
+
138
+ Prioritizes the agent's native output_type attribute (OpenAI Agents pattern),
139
+ with optional fallback to wrapper function type hints (UiPath pattern).
140
+
141
+ Args:
142
+ agent: An OpenAI Agent instance
143
+ loaded_object: Optional original loaded object (function/callable) with type annotations
144
+
145
+ Returns:
146
+ Dictionary with input and output schemas
147
+ """
148
+ schema = {
149
+ "input": {"type": "object", "properties": {}, "required": []},
150
+ "output": {"type": "object", "properties": {}, "required": []},
151
+ }
152
+
153
+ # Extract input schema - check agent's context type or use default messages
154
+ # For OpenAI Agents, input is typically messages (string or list of message objects)
155
+ schema["input"] = {
156
+ "type": "object",
157
+ "properties": {
158
+ "message": {
159
+ "anyOf": [
160
+ {"type": "string"},
161
+ {
162
+ "type": "array",
163
+ "items": {"type": "object"},
164
+ },
165
+ ],
166
+ "title": "Message",
167
+ "description": "User message(s) to send to the agent",
168
+ }
169
+ },
170
+ "required": ["message"],
171
+ }
172
+
173
+ # Extract output schema - PRIORITY 1: Agent's output_type (native OpenAI Agents pattern)
174
+ output_type = getattr(agent, "output_type", None)
175
+ output_extracted = False
176
+
177
+ # Unwrap AgentOutputSchema if present (OpenAI Agents SDK wrapper)
178
+ # Check for AgentOutputSchema by looking for 'schema' attribute on non-type instances
179
+ if (
180
+ output_type is not None
181
+ and hasattr(output_type, "schema")
182
+ and not isinstance(output_type, type)
183
+ ):
184
+ # This is an AgentOutputSchema wrapper instance, extract the actual model
185
+ output_type = output_type.schema
186
+
187
+ if output_type is not None and _is_pydantic_model(output_type):
188
+ try:
189
+ adapter = TypeAdapter(output_type)
190
+ output_schema = adapter.json_schema()
191
+
192
+ # Resolve references and handle nullable types
193
+ unpacked_output = _resolve_refs(output_schema)
194
+ schema["output"]["properties"] = _process_nullable_types(
195
+ unpacked_output.get("properties", {})
196
+ )
197
+ schema["output"]["required"] = unpacked_output.get("required", [])
198
+
199
+ # Add title and description if available
200
+ if "title" in unpacked_output:
201
+ schema["output"]["title"] = unpacked_output["title"]
202
+ if "description" in unpacked_output:
203
+ schema["output"]["description"] = unpacked_output["description"]
204
+
205
+ output_extracted = True
206
+ except Exception:
207
+ # Continue to fallback if extraction fails
208
+ pass
209
+
210
+ # Extract output schema - PRIORITY 2: Wrapper function type hints (UiPath pattern)
211
+ # This allows UiPath-specific patterns where agents are wrapped in typed functions
212
+ if not output_extracted and loaded_object is not None:
213
+ wrapper_schema = _extract_schema_from_callable(loaded_object)
214
+ if wrapper_schema is not None:
215
+ # Use the wrapper's output schema, but keep the default input (messages)
216
+ schema["output"] = wrapper_schema["output"]
217
+ output_extracted = True
218
+
219
+ # Fallback: Default output schema for agents without explicit output_type
220
+ if not output_extracted:
221
+ schema["output"] = {
222
+ "type": "object",
223
+ "properties": {
224
+ "result": {
225
+ "title": "Result",
226
+ "description": "The agent's response",
227
+ "anyOf": [
228
+ {"type": "string"},
229
+ {"type": "object"},
230
+ {
231
+ "type": "array",
232
+ "items": {"type": "object"},
233
+ },
234
+ ],
235
+ }
236
+ },
237
+ "required": ["result"],
238
+ }
239
+
240
+ return schema
241
+
242
+
243
+ def get_agent_schema(agent: Agent) -> UiPathRuntimeGraph:
244
+ """
245
+ Extract graph structure from an OpenAI Agent.
246
+
247
+ OpenAI Agents can delegate to other agents through handoffs,
248
+ creating a hierarchical agent structure.
249
+
250
+ Args:
251
+ agent: An OpenAI Agent instance
252
+
253
+ Returns:
254
+ UiPathRuntimeGraph with nodes and edges representing the agent structure
255
+ """
256
+ nodes: list[UiPathRuntimeNode] = []
257
+ edges: list[UiPathRuntimeEdge] = []
258
+
259
+ # Start node
260
+ nodes.append(
261
+ UiPathRuntimeNode(
262
+ id="__start__",
263
+ name="__start__",
264
+ type="__start__",
265
+ subgraph=None,
266
+ )
267
+ )
268
+
269
+ # Main agent node (always type "model" since it's an LLM)
270
+ agent_name = getattr(agent, "name", "agent")
271
+ nodes.append(
272
+ UiPathRuntimeNode(
273
+ id=agent_name,
274
+ name=agent_name,
275
+ type="model",
276
+ subgraph=None,
277
+ )
278
+ )
279
+
280
+ # Connect start to main agent
281
+ edges.append(
282
+ UiPathRuntimeEdge(
283
+ source="__start__",
284
+ target=agent_name,
285
+ label="input",
286
+ )
287
+ )
288
+
289
+ # Add tool nodes if tools are available
290
+ tools = getattr(agent, "tools", None) or []
291
+ if tools:
292
+ for tool in tools:
293
+ # Extract tool name - handle various tool types
294
+ tool_name = _get_tool_name(tool)
295
+ if tool_name:
296
+ nodes.append(
297
+ UiPathRuntimeNode(
298
+ id=tool_name,
299
+ name=tool_name,
300
+ type="tool",
301
+ subgraph=None,
302
+ )
303
+ )
304
+ # Bidirectional edges: agent calls tool, tool returns to agent
305
+ edges.append(
306
+ UiPathRuntimeEdge(
307
+ source=agent_name,
308
+ target=tool_name,
309
+ label="tool_call",
310
+ )
311
+ )
312
+ edges.append(
313
+ UiPathRuntimeEdge(
314
+ source=tool_name,
315
+ target=agent_name,
316
+ label="tool_result",
317
+ )
318
+ )
319
+
320
+ # Add handoff agents as nodes
321
+ handoffs = getattr(agent, "handoffs", None) or []
322
+ if handoffs:
323
+ for handoff_agent in handoffs:
324
+ handoff_name = getattr(handoff_agent, "name", None)
325
+ if handoff_name:
326
+ nodes.append(
327
+ UiPathRuntimeNode(
328
+ id=handoff_name,
329
+ name=handoff_name,
330
+ type="model",
331
+ subgraph=None, # Handoff agents are peers, not subgraphs
332
+ )
333
+ )
334
+ # Handoff edges
335
+ edges.append(
336
+ UiPathRuntimeEdge(
337
+ source=agent_name,
338
+ target=handoff_name,
339
+ label="handoff",
340
+ )
341
+ )
342
+ edges.append(
343
+ UiPathRuntimeEdge(
344
+ source=handoff_name,
345
+ target=agent_name,
346
+ label="handoff_complete",
347
+ )
348
+ )
349
+
350
+ # End node
351
+ nodes.append(
352
+ UiPathRuntimeNode(
353
+ id="__end__",
354
+ name="__end__",
355
+ type="__end__",
356
+ subgraph=None,
357
+ )
358
+ )
359
+
360
+ # Connect agent to end
361
+ edges.append(
362
+ UiPathRuntimeEdge(
363
+ source=agent_name,
364
+ target="__end__",
365
+ label="output",
366
+ )
367
+ )
368
+
369
+ return UiPathRuntimeGraph(nodes=nodes, edges=edges)
370
+
371
+
372
+ def _get_tool_name(tool: Any) -> str | None:
373
+ """
374
+ Extract the name of a tool from various tool types.
375
+
376
+ Args:
377
+ tool: A tool object (could be a function, class, or tool instance)
378
+
379
+ Returns:
380
+ The tool name as a string, or None if it cannot be determined
381
+ """
382
+ # Try common attributes for tool names
383
+ if hasattr(tool, "name"):
384
+ return str(tool.name)
385
+ if hasattr(tool, "__name__"):
386
+ return str(tool.__name__)
387
+ if hasattr(tool, "tool_name"):
388
+ return str(tool.tool_name)
389
+
390
+ # For class-based tools, try to get class name
391
+ if hasattr(tool, "__class__"):
392
+ class_name = tool.__class__.__name__
393
+ # Remove common suffixes like "Tool" for cleaner names
394
+ if class_name.endswith("Tool"):
395
+ return class_name[:-4].lower()
396
+ return class_name.lower()
397
+
398
+ return None
399
+
400
+
401
+ def _resolve_refs(
402
+ schema: dict[str, Any],
403
+ root: dict[str, Any] | None = None,
404
+ visited: set[str] | None = None,
405
+ ) -> dict[str, Any]:
406
+ """
407
+ Recursively resolves $ref references in a JSON schema.
408
+
409
+ Args:
410
+ schema: The schema dictionary to resolve
411
+ root: The root schema for reference resolution
412
+ visited: Set of visited references to detect circular dependencies
413
+
414
+ Returns:
415
+ Resolved schema dictionary
416
+ """
417
+ if root is None:
418
+ root = schema
419
+
420
+ if visited is None:
421
+ visited = set()
422
+
423
+ if isinstance(schema, dict):
424
+ if "$ref" in schema:
425
+ ref_path = schema["$ref"]
426
+
427
+ if ref_path in visited:
428
+ # Circular dependency detected
429
+ return {
430
+ "type": "object",
431
+ "description": f"Circular reference to {ref_path}",
432
+ }
433
+
434
+ visited.add(ref_path)
435
+
436
+ # Resolve the reference - handle both #/definitions/ and #/$defs/ formats
437
+ ref_parts = ref_path.lstrip("#/").split("/")
438
+ ref_schema = root
439
+ for part in ref_parts:
440
+ ref_schema = ref_schema.get(part, {})
441
+
442
+ result = _resolve_refs(ref_schema, root, visited)
443
+
444
+ # Remove from visited after resolution
445
+ visited.discard(ref_path)
446
+
447
+ return result
448
+
449
+ return {k: _resolve_refs(v, root, visited) for k, v in schema.items()}
450
+
451
+ elif isinstance(schema, list):
452
+ return [_resolve_refs(item, root, visited) for item in schema]
453
+
454
+ return schema
455
+
456
+
457
+ def _process_nullable_types(properties: dict[str, Any]) -> dict[str, Any]:
458
+ """
459
+ Process properties to handle nullable types correctly.
460
+
461
+ This matches the original implementation that adds "nullable": True
462
+ instead of simplifying the schema structure.
463
+
464
+ Args:
465
+ properties: The properties dictionary from a schema
466
+
467
+ Returns:
468
+ Processed properties with nullable types marked
469
+ """
470
+ result = {}
471
+ for name, prop in properties.items():
472
+ if "anyOf" in prop:
473
+ types = [item.get("type") for item in prop["anyOf"] if "type" in item]
474
+ if "null" in types:
475
+ non_null_types = [t for t in types if t != "null"]
476
+ if len(non_null_types) == 1:
477
+ result[name] = {"type": non_null_types[0], "nullable": True}
478
+ else:
479
+ result[name] = {"type": non_null_types, "nullable": True}
480
+ else:
481
+ result[name] = prop
482
+ else:
483
+ result[name] = prop
484
+ return result
485
+
486
+
487
+ __all__ = [
488
+ "get_entrypoints_schema",
489
+ "get_agent_schema",
490
+ ]