uipath-openai-agents 0.0.2__tar.gz → 0.0.3__tar.gz

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.
Files changed (105) hide show
  1. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/PKG-INFO +1 -1
  2. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/pyproject.toml +1 -1
  3. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/runtime/schema.py +113 -60
  4. uipath_openai_agents-0.0.3/tests/test_multi_layer_agents.py +372 -0
  5. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/uv.lock +1 -1
  6. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/.gitignore +0 -0
  7. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/.python-version +0 -0
  8. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/CONTRIBUTING.md +0 -0
  9. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/README.md +0 -0
  10. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/docs/quick_start.md +0 -0
  11. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/.agent/CLI_REFERENCE.md +0 -0
  12. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/.agent/REQUIRED_STRUCTURE.md +0 -0
  13. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/.agent/SDK_REFERENCE.md +0 -0
  14. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/.env.example +0 -0
  15. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/AGENTS.md +0 -0
  16. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/CLAUDE.md +0 -0
  17. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/README.md +0 -0
  18. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/agent.mermaid +0 -0
  19. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/bindings.json +0 -0
  20. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/input.json +0 -0
  21. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/main.py +0 -0
  22. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/openai_agents.json +0 -0
  23. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/pyproject.toml +0 -0
  24. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/uipath.json +0 -0
  25. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/agent-as-tools/uv.lock +0 -0
  26. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/.agent/CLI_REFERENCE.md +0 -0
  27. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/.agent/REQUIRED_STRUCTURE.md +0 -0
  28. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/.agent/SDK_REFERENCE.md +0 -0
  29. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/.claude/commands/eval.md +0 -0
  30. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/.claude/commands/new-agent.md +0 -0
  31. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/.env.example +0 -0
  32. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/AGENTS.md +0 -0
  33. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/CLAUDE.md +0 -0
  34. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/agent.mermaid +0 -0
  35. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/bindings.json +0 -0
  36. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/entry-points.json +0 -0
  37. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/input.json +0 -0
  38. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/main.py +0 -0
  39. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/openai_agents.json +0 -0
  40. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/pyproject.toml +0 -0
  41. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/uipath.json +0 -0
  42. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/rag-assistant/uv.lock +0 -0
  43. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/.agent/CLI_REFERENCE.md +0 -0
  44. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/.agent/REQUIRED_STRUCTURE.md +0 -0
  45. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/.agent/SDK_REFERENCE.md +0 -0
  46. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/AGENTS.md +0 -0
  47. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/CLAUDE.md +0 -0
  48. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/README.md +0 -0
  49. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/agent.mermaid +0 -0
  50. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/bindings.json +0 -0
  51. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/entry-points.json +0 -0
  52. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/input.json +0 -0
  53. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/main.py +0 -0
  54. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/openai_agents.json +0 -0
  55. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/pyproject.toml +0 -0
  56. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/uipath.json +0 -0
  57. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/samples/triage-agent/uv.lock +0 -0
  58. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/__init__.py +0 -0
  59. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/_cli/__init__.py +0 -0
  60. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/_cli/_templates/AGENTS.md.template +0 -0
  61. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/_cli/_templates/main.py.template +0 -0
  62. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/_cli/_templates/openai_agents.json.template +0 -0
  63. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/_cli/cli_new.py +0 -0
  64. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/chat/__init__.py +0 -0
  65. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/chat/openai.py +0 -0
  66. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/chat/supported_models.py +0 -0
  67. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/middlewares.py +0 -0
  68. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/py.typed +0 -0
  69. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/runtime/__init__.py +0 -0
  70. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/runtime/_serialize.py +0 -0
  71. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/runtime/agent.py +0 -0
  72. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/runtime/config.py +0 -0
  73. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/runtime/errors.py +0 -0
  74. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/runtime/factory.py +0 -0
  75. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/src/uipath_openai_agents/runtime/runtime.py +0 -0
  76. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/common/__init__.py +0 -0
  77. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/common/trace_assert.py +0 -0
  78. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/common/validate_output.sh +0 -0
  79. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/init-flow/README.md +0 -0
  80. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/init-flow/expected_traces.json +0 -0
  81. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/init-flow/input.json +0 -0
  82. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/init-flow/pyproject.toml +0 -0
  83. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/init-flow/run.sh +0 -0
  84. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/init-flow/src/assert.py +0 -0
  85. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/triage-agent/input.json +0 -0
  86. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/triage-agent/openai_agents.json +0 -0
  87. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/triage-agent/pyproject.toml +0 -0
  88. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/triage-agent/run.sh +0 -0
  89. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/triage-agent/src/assert.py +0 -0
  90. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/testcases/triage-agent/src/main.py +0 -0
  91. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/__init__.py +0 -0
  92. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/cli/__init__.py +0 -0
  93. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/cli/conftest.py +0 -0
  94. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/cli/test_init.py +0 -0
  95. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/cli/test_run.py +0 -0
  96. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/conftest.py +0 -0
  97. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/demo_schema_inference.py +0 -0
  98. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/mocks/openai_agents.json +0 -0
  99. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/mocks/simple_agent_basic.py +0 -0
  100. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/mocks/simple_agent_translation.py +0 -0
  101. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/test_agent_as_tools_schema.py +0 -0
  102. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/test_integration.py +0 -0
  103. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/test_placeholder.py +0 -0
  104. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/test_schema_inference.py +0 -0
  105. {uipath_openai_agents-0.0.2 → uipath_openai_agents-0.0.3}/tests/test_serialization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uipath-openai-agents
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: UiPath OpenAI Agents SDK
5
5
  Project-URL: Homepage, https://uipath.com
6
6
  Project-URL: Repository, https://github.com/UiPath/uipath-llamaindex-python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uipath-openai-agents"
3
- version = "0.0.2"
3
+ version = "0.0.3"
4
4
  description = "UiPath OpenAI Agents SDK"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -143,8 +143,9 @@ def get_agent_schema(agent: Agent) -> UiPathRuntimeGraph:
143
143
  """
144
144
  Extract graph structure from an OpenAI Agent.
145
145
 
146
- OpenAI Agents can delegate to other agents through handoffs,
147
- creating a hierarchical agent structure.
146
+ OpenAI Agents are represented as simple nodes. Regular tools are aggregated
147
+ into a single tools node per agent with metadata. Agent-tools and handoff
148
+ agents are represented as separate agent nodes.
148
149
 
149
150
  Args:
150
151
  agent: An OpenAI Agent instance
@@ -154,109 +155,161 @@ def get_agent_schema(agent: Agent) -> UiPathRuntimeGraph:
154
155
  """
155
156
  nodes: list[UiPathRuntimeNode] = []
156
157
  edges: list[UiPathRuntimeEdge] = []
157
-
158
- # Start node
159
- nodes.append(
160
- UiPathRuntimeNode(
161
- id="__start__",
162
- name="__start__",
163
- type="__start__",
164
- subgraph=None,
165
- )
166
- )
167
-
168
- # Main agent node (always type "model" since it's an LLM)
169
- agent_name = getattr(agent, "name", "agent")
170
- nodes.append(
171
- UiPathRuntimeNode(
172
- id=agent_name,
173
- name=agent_name,
174
- type="model",
175
- subgraph=None,
158
+ visited: set[str] = set() # Track visited agents to avoid circular references
159
+
160
+ def _add_agent_and_tools(current_agent: Agent) -> None:
161
+ """Recursively add agent, its tools, and nested agents to the graph."""
162
+ agent_name = getattr(current_agent, "name", "agent")
163
+
164
+ # Prevent circular references using agent name
165
+ if agent_name in visited:
166
+ return
167
+ visited.add(agent_name)
168
+
169
+ # Add agent node (first visit always adds the node)
170
+ nodes.append(
171
+ UiPathRuntimeNode(
172
+ id=agent_name,
173
+ name=agent_name,
174
+ type="node",
175
+ subgraph=None,
176
+ metadata=None,
177
+ )
176
178
  )
177
- )
178
179
 
179
- # Connect start to main agent
180
- edges.append(
181
- UiPathRuntimeEdge(
182
- source="__start__",
183
- target=agent_name,
184
- label="input",
185
- )
186
- )
180
+ # Process tools - separate agent-tools from regular tools
181
+ tools = getattr(current_agent, "tools", None) or []
182
+ agent_tools: list[Agent] = []
183
+ regular_tools: list[Any] = []
187
184
 
188
- # Add tool nodes if tools are available
189
- tools = getattr(agent, "tools", None) or []
190
- if tools:
191
185
  for tool in tools:
192
- # Extract tool name - handle various tool types
193
- tool_name = _get_tool_name(tool)
194
- if tool_name:
186
+ if isinstance(tool, Agent):
187
+ agent_tools.append(tool)
188
+ else:
189
+ regular_tools.append(tool)
190
+
191
+ # Process agent-tools (agents used as tools)
192
+ for tool_agent in agent_tools:
193
+ tool_agent_name = getattr(tool_agent, "name", _get_tool_name(tool_agent))
194
+ if tool_agent_name and tool_agent_name not in visited:
195
+ # Recursively process agent-tool
196
+ _add_agent_and_tools(tool_agent)
197
+
198
+ # Add edges for agent-tool
199
+ edges.append(
200
+ UiPathRuntimeEdge(
201
+ source=agent_name,
202
+ target=tool_agent_name,
203
+ label="tool_call",
204
+ )
205
+ )
206
+ edges.append(
207
+ UiPathRuntimeEdge(
208
+ source=tool_agent_name,
209
+ target=agent_name,
210
+ label="tool_result",
211
+ )
212
+ )
213
+
214
+ # Process regular tools - aggregate into single tools node
215
+ if regular_tools:
216
+ tool_names = [_get_tool_name(tool) for tool in regular_tools]
217
+ tool_names = [name for name in tool_names if name] # Filter out None values
218
+
219
+ if tool_names:
220
+ # Create a single tools node for this agent
221
+ tools_node_id = f"{agent_name}_tools"
195
222
  nodes.append(
196
223
  UiPathRuntimeNode(
197
- id=tool_name,
198
- name=tool_name,
224
+ id=tools_node_id,
225
+ name="tools",
199
226
  type="tool",
200
227
  subgraph=None,
228
+ metadata={
229
+ "tool_names": tool_names,
230
+ "tool_count": len(tool_names),
231
+ },
201
232
  )
202
233
  )
203
- # Bidirectional edges: agent calls tool, tool returns to agent
234
+
235
+ # Add bidirectional edges for tools node
204
236
  edges.append(
205
237
  UiPathRuntimeEdge(
206
238
  source=agent_name,
207
- target=tool_name,
208
- label="tool_call",
239
+ target=tools_node_id,
240
+ label=None,
209
241
  )
210
242
  )
211
243
  edges.append(
212
244
  UiPathRuntimeEdge(
213
- source=tool_name,
245
+ source=tools_node_id,
214
246
  target=agent_name,
215
- label="tool_result",
247
+ label=None,
216
248
  )
217
249
  )
218
250
 
219
- # Add handoff agents as nodes
220
- handoffs = getattr(agent, "handoffs", None) or []
221
- if handoffs:
251
+ # Process handoff agents
252
+ handoffs = getattr(current_agent, "handoffs", None) or []
222
253
  for handoff_agent in handoffs:
223
254
  handoff_name = getattr(handoff_agent, "name", None)
224
- if handoff_name:
225
- nodes.append(
226
- UiPathRuntimeNode(
227
- id=handoff_name,
228
- name=handoff_name,
229
- type="model",
230
- subgraph=None, # Handoff agents are peers, not subgraphs
231
- )
232
- )
233
- # Handoff edges
255
+ if handoff_name and handoff_name not in visited:
256
+ # Recursively process handoff agent
257
+ _add_agent_and_tools(handoff_agent)
258
+
259
+ # Add handoff edges without labels
234
260
  edges.append(
235
261
  UiPathRuntimeEdge(
236
262
  source=agent_name,
237
263
  target=handoff_name,
238
- label="handoff",
264
+ label=None,
239
265
  )
240
266
  )
241
267
  edges.append(
242
268
  UiPathRuntimeEdge(
243
269
  source=handoff_name,
244
270
  target=agent_name,
245
- label="handoff_complete",
271
+ label=None,
246
272
  )
247
273
  )
248
274
 
249
- # End node
275
+ # Add __start__ node
276
+ nodes.append(
277
+ UiPathRuntimeNode(
278
+ id="__start__",
279
+ name="__start__",
280
+ type="__start__",
281
+ subgraph=None,
282
+ metadata=None,
283
+ )
284
+ )
285
+
286
+ # Recursively build graph starting from main agent
287
+ _add_agent_and_tools(agent)
288
+
289
+ # Get the main agent name
290
+ agent_name = getattr(agent, "name", "agent")
291
+
292
+ # Add __end__ node
250
293
  nodes.append(
251
294
  UiPathRuntimeNode(
252
295
  id="__end__",
253
296
  name="__end__",
254
297
  type="__end__",
255
298
  subgraph=None,
299
+ metadata=None,
300
+ )
301
+ )
302
+
303
+ # Connect start to main agent
304
+ edges.append(
305
+ UiPathRuntimeEdge(
306
+ source="__start__",
307
+ target=agent_name,
308
+ label="input",
256
309
  )
257
310
  )
258
311
 
259
- # Connect agent to end
312
+ # Connect main agent to end
260
313
  edges.append(
261
314
  UiPathRuntimeEdge(
262
315
  source=agent_name,
@@ -0,0 +1,372 @@
1
+ """Tests for multi-layer OpenAI agents with handoffs and tools."""
2
+
3
+ import os
4
+
5
+ from agents import Agent, function_tool
6
+
7
+ # Set up mock environment variables
8
+ os.environ.setdefault("UIPATH_URL", "https://mock.uipath.com")
9
+ os.environ.setdefault("UIPATH_ORGANIZATION_ID", "mock-org-id")
10
+ os.environ.setdefault("UIPATH_TENANT_ID", "mock-tenant-id")
11
+ os.environ.setdefault("UIPATH_ACCESS_TOKEN", "mock-token")
12
+
13
+ from uipath_openai_agents.runtime.schema import ( # noqa: E402
14
+ get_agent_schema,
15
+ get_entrypoints_schema,
16
+ )
17
+
18
+ # ============= TOOLS =============
19
+
20
+
21
+ @function_tool
22
+ async def check_employee_benefits(employee_id: str) -> str:
23
+ """Check employee benefits information.
24
+
25
+ Args:
26
+ employee_id: The employee ID to look up
27
+
28
+ Returns:
29
+ Employee benefits information
30
+ """
31
+ return f"Employee {employee_id} benefits: Health Insurance, 401k, 20 days PTO"
32
+
33
+
34
+ @function_tool
35
+ async def submit_leave_request(employee_id: str, leave_type: str, days: int) -> str:
36
+ """Submit a leave request for an employee.
37
+
38
+ Args:
39
+ employee_id: The employee ID
40
+ leave_type: Type of leave (vacation, sick, personal)
41
+ days: Number of days requested
42
+
43
+ Returns:
44
+ Leave request confirmation
45
+ """
46
+ return f"Leave request submitted for employee {employee_id}: {days} days of {leave_type} leave"
47
+
48
+
49
+ @function_tool
50
+ async def get_salary_info(employee_id: str) -> str:
51
+ """Get salary information for an employee.
52
+
53
+ Args:
54
+ employee_id: The employee ID
55
+
56
+ Returns:
57
+ Salary information
58
+ """
59
+ return f"Employee {employee_id} salary information: $85,000 annual"
60
+
61
+
62
+ @function_tool
63
+ async def create_purchase_order(item: str, quantity: int, vendor: str) -> str:
64
+ """Create a purchase order for items.
65
+
66
+ Args:
67
+ item: Item description
68
+ quantity: Quantity to order
69
+ vendor: Vendor name
70
+
71
+ Returns:
72
+ Purchase order confirmation
73
+ """
74
+ return f"Purchase Order created: {item} x{quantity} from {vendor}"
75
+
76
+
77
+ @function_tool
78
+ async def check_budget_availability(department: str, amount: float) -> str:
79
+ """Check if budget is available for a department.
80
+
81
+ Args:
82
+ department: Department name
83
+ amount: Amount to check
84
+
85
+ Returns:
86
+ Budget availability status
87
+ """
88
+ return f"Budget check for {department}: ${amount:,.2f} - APPROVED"
89
+
90
+
91
+ @function_tool
92
+ async def track_order_status(po_number: str) -> str:
93
+ """Track the status of a purchase order.
94
+
95
+ Args:
96
+ po_number: Purchase order number
97
+
98
+ Returns:
99
+ Order status information
100
+ """
101
+ return f"Order Status for {po_number}: In Transit"
102
+
103
+
104
+ @function_tool
105
+ async def get_company_policy(policy_type: str) -> str:
106
+ """Get company policy information.
107
+
108
+ Args:
109
+ policy_type: Type of policy (remote_work, expense, code_of_conduct, etc.)
110
+
111
+ Returns:
112
+ Policy information
113
+ """
114
+ return f"Policy information for {policy_type}"
115
+
116
+
117
+ @function_tool
118
+ async def check_compliance_status(policy_area: str) -> str:
119
+ """Check compliance status for a policy area.
120
+
121
+ Args:
122
+ policy_area: Area to check compliance (data_security, safety, training, etc.)
123
+
124
+ Returns:
125
+ Compliance status
126
+ """
127
+ return f"Compliance Status for {policy_area}: COMPLIANT"
128
+
129
+
130
+ # ============= SPECIALIZED AGENTS =============
131
+
132
+
133
+ def create_multi_layer_agent():
134
+ """Create a multi-layer agent structure with handoffs and tools."""
135
+ # HR Agent - Handles human resources queries
136
+ hr_agent = Agent(
137
+ name="hr_agent",
138
+ instructions="You are an HR specialist assistant handling benefits, leave, and salary inquiries.",
139
+ model="gpt-4o-mini",
140
+ tools=[check_employee_benefits, submit_leave_request, get_salary_info],
141
+ )
142
+
143
+ # Procurement Agent - Handles purchasing and procurement
144
+ procurement_agent = Agent(
145
+ name="procurement_agent",
146
+ instructions="You are a procurement specialist handling purchase orders, budgets, and order tracking.",
147
+ model="gpt-4o-mini",
148
+ tools=[create_purchase_order, check_budget_availability, track_order_status],
149
+ )
150
+
151
+ # Policy Agent - Handles company policies and compliance
152
+ policy_agent = Agent(
153
+ name="policy_agent",
154
+ instructions="You are a policy and compliance specialist providing policy information.",
155
+ model="gpt-4o-mini",
156
+ tools=[get_company_policy, check_compliance_status],
157
+ )
158
+
159
+ # Orchestrator Agent (Main Entry Point) - Routes to specialized agents
160
+ orchestrator_agent = Agent(
161
+ name="orchestrator_agent",
162
+ instructions="You route employee requests to the appropriate department specialist.",
163
+ model="gpt-4o-mini",
164
+ handoffs=[hr_agent, procurement_agent, policy_agent],
165
+ )
166
+
167
+ return orchestrator_agent
168
+
169
+
170
+ # ============= TESTS =============
171
+
172
+
173
+ def test_multi_layer_agent_graph_nodes():
174
+ """Test that all agents and aggregated tools nodes are represented in the graph."""
175
+ agent = create_multi_layer_agent()
176
+ graph = get_agent_schema(agent)
177
+
178
+ # Get all node IDs
179
+ node_ids = {node.id for node in graph.nodes}
180
+
181
+ # Verify control nodes
182
+ assert "__start__" in node_ids
183
+ assert "__end__" in node_ids
184
+
185
+ # Verify all agents are present
186
+ assert "orchestrator_agent" in node_ids
187
+ assert "hr_agent" in node_ids
188
+ assert "procurement_agent" in node_ids
189
+ assert "policy_agent" in node_ids
190
+
191
+ # Verify aggregated tools nodes (one per agent with tools)
192
+ assert "hr_agent_tools" in node_ids
193
+ assert "procurement_agent_tools" in node_ids
194
+ assert "policy_agent_tools" in node_ids
195
+
196
+ # Total: 2 control + 4 agents + 3 tools nodes = 9 nodes
197
+ assert len(graph.nodes) == 9
198
+
199
+
200
+ def test_multi_layer_agent_node_types():
201
+ """Test that nodes have correct types."""
202
+ agent = create_multi_layer_agent()
203
+ graph = get_agent_schema(agent)
204
+
205
+ # Create a mapping of node ID to type
206
+ node_types = {node.id: node.type for node in graph.nodes}
207
+
208
+ # Verify control node types
209
+ assert node_types["__start__"] == "__start__"
210
+ assert node_types["__end__"] == "__end__"
211
+
212
+ # Verify agent nodes are of type "node"
213
+ assert node_types["orchestrator_agent"] == "node"
214
+ assert node_types["hr_agent"] == "node"
215
+ assert node_types["procurement_agent"] == "node"
216
+ assert node_types["policy_agent"] == "node"
217
+
218
+ # Verify aggregated tools nodes are of type "tool"
219
+ assert node_types["hr_agent_tools"] == "tool"
220
+ assert node_types["procurement_agent_tools"] == "tool"
221
+ assert node_types["policy_agent_tools"] == "tool"
222
+
223
+
224
+ def test_multi_layer_agent_handoff_edges():
225
+ """Test that handoff edges are correctly created between orchestrator and specialized agents without labels."""
226
+ agent = create_multi_layer_agent()
227
+ graph = get_agent_schema(agent)
228
+
229
+ # Get all edges
230
+ edges = [(edge.source, edge.target, edge.label) for edge in graph.edges]
231
+
232
+ # Verify bidirectional handoff edges without labels
233
+ assert ("orchestrator_agent", "hr_agent", None) in edges
234
+ assert ("hr_agent", "orchestrator_agent", None) in edges
235
+
236
+ assert ("orchestrator_agent", "procurement_agent", None) in edges
237
+ assert ("procurement_agent", "orchestrator_agent", None) in edges
238
+
239
+ assert ("orchestrator_agent", "policy_agent", None) in edges
240
+ assert ("policy_agent", "orchestrator_agent", None) in edges
241
+
242
+
243
+ def test_multi_layer_agent_tool_edges():
244
+ """Test that bidirectional tool edges exist for aggregated tools nodes without labels."""
245
+ agent = create_multi_layer_agent()
246
+ graph = get_agent_schema(agent)
247
+
248
+ # Get all edges
249
+ edges = [(edge.source, edge.target, edge.label) for edge in graph.edges]
250
+
251
+ # Verify bidirectional edges to/from aggregated tools nodes without labels
252
+ assert ("hr_agent", "hr_agent_tools", None) in edges
253
+ assert ("hr_agent_tools", "hr_agent", None) in edges
254
+
255
+ assert ("procurement_agent", "procurement_agent_tools", None) in edges
256
+ assert ("procurement_agent_tools", "procurement_agent", None) in edges
257
+
258
+ assert ("policy_agent", "policy_agent_tools", None) in edges
259
+ assert ("policy_agent_tools", "policy_agent", None) in edges
260
+
261
+
262
+ def test_multi_layer_agent_control_edges():
263
+ """Test that control flow edges (start/end) are correctly created."""
264
+ agent = create_multi_layer_agent()
265
+ graph = get_agent_schema(agent)
266
+
267
+ # Get all edges
268
+ edges = [(edge.source, edge.target, edge.label) for edge in graph.edges]
269
+
270
+ # Verify start edge to orchestrator
271
+ assert ("__start__", "orchestrator_agent", "input") in edges
272
+
273
+ # Verify end edge from orchestrator
274
+ assert ("orchestrator_agent", "__end__", "output") in edges
275
+
276
+
277
+ def test_multi_layer_agent_no_circular_references():
278
+ """Test that the graph doesn't create circular references for the same agent."""
279
+ agent = create_multi_layer_agent()
280
+ graph = get_agent_schema(agent)
281
+
282
+ # Count occurrences of each agent in nodes
283
+ node_counts: dict[str, int] = {}
284
+ for node in graph.nodes:
285
+ node_counts[node.id] = node_counts.get(node.id, 0) + 1
286
+
287
+ # Each agent should appear exactly once
288
+ assert node_counts["orchestrator_agent"] == 1
289
+ assert node_counts["hr_agent"] == 1
290
+ assert node_counts["procurement_agent"] == 1
291
+ assert node_counts["policy_agent"] == 1
292
+
293
+
294
+ def test_multi_layer_agent_entrypoints_schema():
295
+ """Test that entrypoints schema is correctly extracted."""
296
+ agent = create_multi_layer_agent()
297
+ schema = get_entrypoints_schema(agent)
298
+
299
+ # Verify input schema (default messages format)
300
+ assert "input" in schema
301
+ assert "properties" in schema["input"]
302
+ assert "messages" in schema["input"]["properties"]
303
+ assert "required" in schema["input"]
304
+ assert "messages" in schema["input"]["required"]
305
+
306
+ # Verify output schema (default result format since no output_type specified)
307
+ assert "output" in schema
308
+ assert "properties" in schema["output"]
309
+ assert "result" in schema["output"]["properties"]
310
+ assert "required" in schema["output"]
311
+ assert "result" in schema["output"]["required"]
312
+
313
+
314
+ def test_multi_layer_agent_edge_count():
315
+ """Test that the total number of edges is correct."""
316
+ agent = create_multi_layer_agent()
317
+ graph = get_agent_schema(agent)
318
+
319
+ # Count expected edges:
320
+ # - 2 control edges (start -> orchestrator, orchestrator -> end)
321
+ # - 6 handoff edges (3 agents * 2 bidirectional edges each)
322
+ # - 6 tool edges (3 tools nodes * 2 bidirectional edges each)
323
+ # Total: 2 + 6 + 6 = 14 edges
324
+ assert len(graph.edges) == 14
325
+
326
+
327
+ def test_multi_layer_agent_tools_metadata():
328
+ """Test that tools nodes have correct metadata with tool_names and tool_count."""
329
+ agent = create_multi_layer_agent()
330
+ graph = get_agent_schema(agent)
331
+
332
+ # Create a mapping of node ID to metadata
333
+ node_metadata = {node.id: node.metadata for node in graph.nodes}
334
+
335
+ # Verify HR agent tools metadata
336
+ hr_tools_metadata = node_metadata["hr_agent_tools"]
337
+ assert hr_tools_metadata is not None
338
+ assert "tool_names" in hr_tools_metadata
339
+ assert "tool_count" in hr_tools_metadata
340
+ assert hr_tools_metadata["tool_count"] == 3
341
+ assert "check_employee_benefits" in hr_tools_metadata["tool_names"]
342
+ assert "submit_leave_request" in hr_tools_metadata["tool_names"]
343
+ assert "get_salary_info" in hr_tools_metadata["tool_names"]
344
+
345
+ # Verify procurement agent tools metadata
346
+ procurement_tools_metadata = node_metadata["procurement_agent_tools"]
347
+ assert procurement_tools_metadata is not None
348
+ assert "tool_names" in procurement_tools_metadata
349
+ assert "tool_count" in procurement_tools_metadata
350
+ assert procurement_tools_metadata["tool_count"] == 3
351
+ assert "create_purchase_order" in procurement_tools_metadata["tool_names"]
352
+ assert "check_budget_availability" in procurement_tools_metadata["tool_names"]
353
+ assert "track_order_status" in procurement_tools_metadata["tool_names"]
354
+
355
+ # Verify policy agent tools metadata
356
+ policy_tools_metadata = node_metadata["policy_agent_tools"]
357
+ assert policy_tools_metadata is not None
358
+ assert "tool_names" in policy_tools_metadata
359
+ assert "tool_count" in policy_tools_metadata
360
+ assert policy_tools_metadata["tool_count"] == 2
361
+ assert "get_company_policy" in policy_tools_metadata["tool_names"]
362
+ assert "check_compliance_status" in policy_tools_metadata["tool_names"]
363
+
364
+
365
+ def test_multi_layer_agent_no_subgraphs():
366
+ """Test that OpenAI agents are represented as flat nodes without subgraphs."""
367
+ agent = create_multi_layer_agent()
368
+ graph = get_agent_schema(agent)
369
+
370
+ # Verify all nodes have None subgraph (flat structure)
371
+ for node in graph.nodes:
372
+ assert node.subgraph is None
@@ -2318,7 +2318,7 @@ wheels = [
2318
2318
 
2319
2319
  [[package]]
2320
2320
  name = "uipath-openai-agents"
2321
- version = "0.0.2"
2321
+ version = "0.0.3"
2322
2322
  source = { editable = "." }
2323
2323
  dependencies = [
2324
2324
  { name = "aiosqlite" },