ibm-watsonx-orchestrate 1.4.2__py3-none-any.whl → 1.5.0b1__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.
Files changed (40) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +10 -1
  3. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +14 -0
  4. ibm_watsonx_orchestrate/agent_builder/model_policies/__init__.py +1 -0
  5. ibm_watsonx_orchestrate/{client → agent_builder}/model_policies/types.py +7 -8
  6. ibm_watsonx_orchestrate/agent_builder/models/__init__.py +1 -0
  7. ibm_watsonx_orchestrate/{client → agent_builder}/models/types.py +57 -9
  8. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +46 -3
  9. ibm_watsonx_orchestrate/agent_builder/tools/types.py +47 -1
  10. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +17 -0
  11. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +86 -39
  12. ibm_watsonx_orchestrate/cli/commands/models/model_provider_mapper.py +191 -0
  13. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +140 -258
  14. ibm_watsonx_orchestrate/cli/commands/models/models_controller.py +437 -0
  15. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +2 -1
  16. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +1 -1
  17. ibm_watsonx_orchestrate/client/connections/__init__.py +2 -1
  18. ibm_watsonx_orchestrate/client/connections/utils.py +30 -0
  19. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +23 -4
  20. ibm_watsonx_orchestrate/client/models/models_client.py +23 -3
  21. ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +13 -8
  22. ibm_watsonx_orchestrate/client/tools/tool_client.py +2 -1
  23. ibm_watsonx_orchestrate/docker/compose-lite.yml +2 -0
  24. ibm_watsonx_orchestrate/docker/default.env +10 -11
  25. ibm_watsonx_orchestrate/experimental/flow_builder/data_map.py +19 -0
  26. ibm_watsonx_orchestrate/experimental/flow_builder/flows/__init__.py +4 -3
  27. ibm_watsonx_orchestrate/experimental/flow_builder/flows/constants.py +3 -1
  28. ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +3 -2
  29. ibm_watsonx_orchestrate/experimental/flow_builder/flows/flow.py +245 -223
  30. ibm_watsonx_orchestrate/experimental/flow_builder/node.py +34 -15
  31. ibm_watsonx_orchestrate/experimental/flow_builder/resources/flow_status.openapi.yml +7 -39
  32. ibm_watsonx_orchestrate/experimental/flow_builder/types.py +285 -12
  33. ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +3 -1
  34. {ibm_watsonx_orchestrate-1.4.2.dist-info → ibm_watsonx_orchestrate-1.5.0b1.dist-info}/METADATA +1 -1
  35. {ibm_watsonx_orchestrate-1.4.2.dist-info → ibm_watsonx_orchestrate-1.5.0b1.dist-info}/RECORD +38 -35
  36. ibm_watsonx_orchestrate/cli/commands/models/env_file_model_provider_mapper.py +0 -180
  37. ibm_watsonx_orchestrate/experimental/flow_builder/flows/data_map.py +0 -91
  38. {ibm_watsonx_orchestrate-1.4.2.dist-info → ibm_watsonx_orchestrate-1.5.0b1.dist-info}/WHEEL +0 -0
  39. {ibm_watsonx_orchestrate-1.4.2.dist-info → ibm_watsonx_orchestrate-1.5.0b1.dist-info}/entry_points.txt +0 -0
  40. {ibm_watsonx_orchestrate-1.4.2.dist-info → ibm_watsonx_orchestrate-1.5.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -6,37 +6,36 @@ the Flow model.
6
6
  import asyncio
7
7
  from datetime import datetime
8
8
  from enum import Enum
9
+ import inspect
9
10
  from typing import (
10
11
  Any, AsyncIterator, Callable, cast, List, Sequence, Union, Tuple
11
12
  )
12
13
  import json
13
14
  import logging
14
- import time
15
15
  import copy
16
16
  import uuid
17
17
  import pytz
18
18
 
19
19
  from typing_extensions import Self
20
- from pydantic import BaseModel, Field, PrivateAttr, SerializeAsAny
20
+ from pydantic import BaseModel, Field, SerializeAsAny
21
21
  import yaml
22
- from munch import Munch
23
22
  from ibm_watsonx_orchestrate.agent_builder.tools.python_tool import PythonTool
24
23
  from ibm_watsonx_orchestrate.client.tools.tempus_client import TempusClient
25
24
  from ibm_watsonx_orchestrate.client.utils import instantiate_client
26
25
  from ..types import (
27
- EndNodeSpec, Expression, ForeachPolicy, ForeachSpec, LoopSpec, BranchNodeSpec, MatchPolicy,
28
- StartNodeSpec, ToolSpec, JsonSchemaObject, ToolRequestBody, ToolResponseBody, WaitPolicy
26
+ EndNodeSpec, Expression, ForeachPolicy, ForeachSpec, LoopSpec, BranchNodeSpec, MatchPolicy, PromptLLMParameters, PromptNodeSpec,
27
+ StartNodeSpec, ToolSpec, JsonSchemaObject, ToolRequestBody, ToolResponseBody, UserFieldKind, UserFieldOption, UserFlowSpec, UserNodeSpec, WaitPolicy
29
28
  )
30
- from .constants import START, END, ANY_USER
29
+ from .constants import CURRENT_USER, START, END, ANY_USER
31
30
  from ..node import (
32
- EndNode, Node, StartNode, UserNode, AgentNode, DataMap, ToolNode
31
+ EndNode, Node, PromptNode, StartNode, UserNode, AgentNode, DataMap, ToolNode
33
32
  )
34
33
  from ..types import (
35
34
  AgentNodeSpec, extract_node_spec, FlowContext, FlowEventType, FlowEvent, FlowSpec,
36
35
  NodeSpec, TaskEventType, ToolNodeSpec, SchemaRef, JsonSchemaObjectRef, _to_json_from_json_schema
37
36
  )
38
37
 
39
- from .data_map import Assignment, AssignmentDataMap, AssignmentDataMapSpec
38
+ from ..data_map import DataMap
40
39
  from ..utils import _get_json_schema_obj, get_valid_name, import_flow_model
41
40
 
42
41
  from .events import StreamConsumer
@@ -60,7 +59,6 @@ class FlowEdge(BaseModel):
60
59
  start: str
61
60
  end: str
62
61
 
63
-
64
62
  class Flow(Node):
65
63
  '''Flow represents a flow that will be run by wxO Flow engine.'''
66
64
  output_map: DataMap | None = None
@@ -71,6 +69,7 @@ class Flow(Node):
71
69
  validated: bool = False
72
70
  metadata: dict[str, str] = {}
73
71
  parent: Any = None
72
+ _sequence_id: int = 0 # internal-id
74
73
 
75
74
  def __init__(self, **kwargs):
76
75
  super().__init__(**kwargs)
@@ -83,6 +82,10 @@ class Flow(Node):
83
82
  return self.parent._find_topmost_flow()
84
83
  return self
85
84
 
85
+ def _next_sequence_id(self) -> int:
86
+ self._sequence_id += 1
87
+ return self._sequence_id
88
+
86
89
  def _add_schema(self, schema: JsonSchemaObject, title: str = None) -> JsonSchemaObject:
87
90
  '''
88
91
  Adds a schema to the dictionary of schemas. If a schema with the same name already exists, it returns the existing schema. Otherwise, it creates a deep copy of the schema, adds it to the dictionary, and returns the new schema.
@@ -107,7 +110,7 @@ class Flow(Node):
107
110
  if schema:
108
111
  if isinstance(schema, dict):
109
112
  # recast schema to support direct access
110
- schema = Munch(schema)
113
+ schema = JsonSchemaObject.model_validate(schema)
111
114
  # we should only add schema when it is a complex object
112
115
  if schema.type != "object" and schema.type != "array":
113
116
  return schema
@@ -134,7 +137,7 @@ class Flow(Node):
134
137
  schema_ref = self._add_schema_ref(value.items, value.items.title)
135
138
  new_schema.properties[key].items = JsonSchemaObjectRef(title=value.title,
136
139
  ref = f"{schema_ref.ref}")
137
- elif value.model_extra and value.model_extra["$ref"]:
140
+ elif value.model_extra and hasattr(value.model_extra, "$ref"):
138
141
  # there is already a reference, remove $/defs/ from the initial ref
139
142
  ref_value = value.model_extra["$ref"]
140
143
  schema_ref = f"#/schemas/{ref_value[8:]}"
@@ -254,159 +257,18 @@ class Flow(Node):
254
257
 
255
258
  input_schema: type[BaseModel] | None = None,
256
259
  output_schema: type[BaseModel] | None = None,
257
- input_map: List[Assignment] = None
258
- ) -> Node:
260
+ input_map: DataMap = None
261
+ ) -> ToolNode:
259
262
  '''create a tool node in the flow'''
260
263
  if tool is None:
261
264
  raise ValueError("tool must be provided")
262
265
 
263
- if isinstance(tool, str):
264
- return self._node(
265
- name=name if name is not None and name != "" else tool,
266
- tool=tool,
267
- display_name=display_name,
268
- description=description,
269
- input_schema=input_schema,
270
- output_schema=output_schema,
271
- input_map=input_map)
272
- elif isinstance(tool, PythonTool):
273
- return self._node(
274
- node=tool,
275
- name=name if name is not None and name != "" else tool.fn.__name__,
276
- display_name=display_name,
277
- description=description,
278
- input_schema=input_schema,
279
- output_schema=output_schema,
280
- input_map=input_map)
281
- else:
282
- raise ValueError(f"tool is not a string or a callable: {tool}")
283
-
284
- def agent(
285
- self,
286
- name: str | None = None,
287
- display_name: str | None = None,
288
- description: str | None = None,
289
- agent: str | None = None,
290
- message: str | None = None,
291
- guidelines: str | None = None,
292
- input_schema: type[BaseModel] | None = None,
293
- output_schema: type[BaseModel] | None = None,
294
- input_map: List[Assignment] = None
295
- ) -> Node:
296
- '''create an agent node in the flow'''
297
- return self._node(
298
- name=name,
299
- display_name=display_name,
300
- description=description,
301
- agent=agent,
302
- message=message,
303
- guidelines=guidelines,
304
- input_schema=input_schema,
305
- output_schema=output_schema,
306
- input_map=input_map
307
- )
308
-
309
- def _node(
310
- self,
311
- node: Union[Node, Callable] = None,
312
- name: str = None,
313
- display_name: str | None = None,
314
- description: str | None = None,
315
- owners: Sequence[str] | None = None,
316
- input_schema: type[BaseModel] | None = None,
317
- output_schema: type[BaseModel] | None = None,
318
- agent: str | None = None,
319
- tool: str | None = None,
320
- message: str | None = None,
321
- guidelines: str | None = None,
322
- input_map: Callable | List[Assignment] = None,
323
- output_map: Callable | List[Assignment] = None,
324
- ) -> Node:
325
-
326
- self._check_compiled()
327
-
328
- if owners is None:
329
- owners = []
330
-
331
- if node is not None:
332
- if not isinstance(node, Node):
333
- if callable(node):
334
- user_spec = getattr(node, "__user_spec__", None)
335
- # script_spec = getattr(node, "__script_spec__", None)
336
- tool_spec = getattr(node, "__tool_spec__", None)
337
- if user_spec:
338
- node = UserNode(spec = user_spec)
339
- # elif script_spec:
340
- # node = ScriptNode(spec = script_spec)
341
- elif tool_spec:
342
- node = self._create_node_from_tool_fn(node)
343
- else:
344
- raise ValueError(
345
- "Only functions with @user, @tool or @script decorator can be added.")
346
- elif isinstance(node, Node):
347
- if node.spec.name in self.nodes:
348
- raise ValueError(f"Node `{id}` already present.")
349
-
350
- if node.spec.name == END or node.spec.name == START:
351
- raise ValueError(f"Node `{id}` is reserved.")
352
- else:
353
- raise ValueError(
354
- "A valid node or function must be specified for the node parameter.")
355
-
356
- # setup input and output map
357
- if input_map:
358
- node.input_map = self._get_data_map(input_map)
359
- if output_map:
360
- node.output_map = self._get_data_map(output_map)
361
-
362
- # add the node to the list of node
363
- node = self._add_node(node)
364
- return node
365
-
366
- if name is not None:
367
- if agent is not None:
368
- node = self._create_agent_node(
369
- name, agent, display_name, message, description, input_schema, output_schema, guidelines)
370
- elif tool is not None:
371
- node = self._create_tool_node(
372
- name, tool, display_name, description, input_schema, output_schema)
373
- else:
374
- node = self._create_user_node(
375
- name, display_name, description, owners, input_schema, output_schema)
376
-
377
- # setup input and output map
378
- if input_map:
379
- node.input_map = self._get_data_map(input_map)
380
- if output_map:
381
- node.output_map = self._get_data_map(output_map)
382
-
383
- # add the node to the list of node
384
- node = self._add_node(node)
385
- return node
386
-
387
- raise ValueError("Either a node or a name must be specified.")
388
-
389
- def _add_node(self, node: Node) -> Node:
390
- # make a copy
391
- new_node = copy.copy(node)
392
-
393
- self._refactor_node_to_schemaref(new_node)
394
-
395
- self.nodes[node.spec.name] = new_node
396
- return new_node
397
-
398
-
399
- def _create_tool_node(self, name: str, tool: str,
400
- display_name: str|None=None,
401
- description: str|None=None,
402
- input_schema: type[BaseModel]|None=None,
403
- output_schema: type[BaseModel]|None=None) -> Node:
404
-
405
- # create input spec
406
- input_schema_obj = _get_json_schema_obj(parameter_name = "input", type_def = input_schema)
407
- output_schema_obj = _get_json_schema_obj("output", output_schema)
266
+ if isinstance(tool, str):
267
+ name = name if name is not None and name != "" else tool
268
+ input_schema_obj = _get_json_schema_obj(parameter_name = "input", type_def = input_schema)
269
+ output_schema_obj = _get_json_schema_obj("output", output_schema)
408
270
 
409
- toolnode_spec = ToolNodeSpec(type = "tool",
271
+ toolnode_spec = ToolNodeSpec(type = "tool",
410
272
  name = name,
411
273
  display_name = display_name,
412
274
  description = description,
@@ -423,28 +285,62 @@ class Flow(Node):
423
285
  output_schema_object = output_schema_obj,
424
286
  tool = tool)
425
287
 
426
- return ToolNode(spec=toolnode_spec)
288
+ node = ToolNode(spec=toolnode_spec)
289
+ elif isinstance(tool, PythonTool):
290
+ if callable(tool):
291
+ tool_spec = getattr(tool, "__tool_spec__", None)
292
+ if tool_spec:
293
+ node = self._create_node_from_tool_fn(tool)
294
+ else:
295
+ raise ValueError("Only functions with @tool decorator can be added.")
296
+ else:
297
+ raise ValueError(f"tool is not a string or Callable: {tool}")
298
+
299
+ # setup input and output map
300
+ if input_map:
301
+ node.input_map = self._get_data_map(input_map)
427
302
 
428
- def _create_user_node(self, name: str,
429
- display_name: str|None=None,
430
- description: str|None=None,
431
- owners: Sequence[str]|None=[ANY_USER],
432
- input_schema: type[BaseModel]|None=None,
433
- output_schema: type[BaseModel]|None=None) -> Node:
434
- # create input spec
303
+ node = self._add_node(node)
304
+ return cast(ToolNode, node)
305
+
306
+
307
+ def _add_node(self, node: Node) -> Node:
308
+ self._check_compiled()
309
+
310
+ if node.spec.name in self.nodes:
311
+ raise ValueError(f"Node `{id}` already present.")
312
+
313
+ # make a copy
314
+ new_node = copy.copy(node)
315
+
316
+ self._refactor_node_to_schemaref(new_node)
317
+
318
+ self.nodes[node.spec.name] = new_node
319
+ return new_node
320
+
321
+ def agent(self,
322
+ name: str,
323
+ agent: str,
324
+ display_name: str|None=None,
325
+ message: str | None = "Follow the agent instructions.",
326
+ description: str | None = None,
327
+ input_schema: type[BaseModel]|None = None,
328
+ output_schema: type[BaseModel]|None=None,
329
+ guidelines: str|None=None,
330
+ input_map: DataMap = None) -> AgentNode:
331
+
332
+ # create input spec
435
333
  input_schema_obj = _get_json_schema_obj(parameter_name = "input", type_def = input_schema)
436
334
  output_schema_obj = _get_json_schema_obj("output", output_schema)
437
335
 
438
- # identify owner
439
- if not owners:
440
- owners = [ANY_USER]
441
-
442
336
  # Create the tool spec
443
- task_spec = NodeSpec(
337
+ task_spec = AgentNodeSpec(
444
338
  name=name,
445
339
  display_name=display_name,
446
340
  description=description,
447
- owners=owners,
341
+ agent=agent,
342
+ message=message,
343
+ guidelines=guidelines,
448
344
  input_schema=ToolRequestBody(
449
345
  type=input_schema_obj.type,
450
346
  properties=input_schema_obj.properties,
@@ -455,31 +351,46 @@ class Flow(Node):
455
351
  properties=output_schema_obj.properties,
456
352
  required=output_schema_obj.required
457
353
  ),
458
- tool=[],
459
354
  output_schema_object = output_schema_obj
460
355
  )
461
356
 
462
- return UserNode(spec = task_spec)
463
-
464
- def _create_agent_node(self, name: str, agent: str, display_name: str|None=None,
465
- message: str | None = "Follow the agent instructions.",
466
- description: str | None = None,
467
- input_schema: type[BaseModel]|None = None,
468
- output_schema: type[BaseModel]|None=None,
469
- guidelines: str|None=None) -> Node:
470
-
357
+ node = AgentNode(spec=task_spec)
358
+ # setup input map
359
+ if input_map:
360
+ node.input_map = self._get_data_map(input_map)
361
+
362
+ # add the node to the list of node
363
+ node = self._add_node(node)
364
+ return cast(AgentNode, node)
365
+
366
+ def prompt(self,
367
+ name: str,
368
+ display_name: str|None=None,
369
+ system_prompt: str | list[str] | None = None,
370
+ user_prompt: str | list[str] | None = None,
371
+ llm: str | None = None,
372
+ llm_parameters: PromptLLMParameters | None = None,
373
+ description: str | None = None,
374
+ input_schema: type[BaseModel]|None = None,
375
+ output_schema: type[BaseModel]|None=None,
376
+ input_map: DataMap = None) -> PromptNode:
377
+
378
+ if name is None:
379
+ raise ValueError("name must be provided.")
380
+
471
381
  # create input spec
472
382
  input_schema_obj = _get_json_schema_obj(parameter_name = "input", type_def = input_schema)
473
383
  output_schema_obj = _get_json_schema_obj("output", output_schema)
474
384
 
475
385
  # Create the tool spec
476
- task_spec = AgentNodeSpec(
386
+ task_spec = PromptNodeSpec(
477
387
  name=name,
478
- display_name=display_name,
388
+ display_name=display_name if display_name is not None else name,
479
389
  description=description,
480
- agent=agent,
481
- message=message,
482
- guidelines=guidelines,
390
+ system_prompt=system_prompt,
391
+ user_prompt=user_prompt,
392
+ llm=llm,
393
+ llm_parameters=llm_parameters,
483
394
  input_schema=ToolRequestBody(
484
395
  type=input_schema_obj.type,
485
396
  properties=input_schema_obj.properties,
@@ -493,7 +404,14 @@ class Flow(Node):
493
404
  output_schema_object = output_schema_obj
494
405
  )
495
406
 
496
- return AgentNode(spec=task_spec)
407
+ node = PromptNode(spec=task_spec)
408
+ # setup input map
409
+ if input_map:
410
+ node.input_map = self._get_data_map(input_map)
411
+
412
+ # add the node to the list of node
413
+ node = self._add_node(node)
414
+ return cast(PromptNode, node)
497
415
 
498
416
  def node_exists(self, node: Union[str, Node]):
499
417
 
@@ -587,9 +505,9 @@ class Flow(Node):
587
505
  elif isinstance(evaluator, str):
588
506
  e = Expression(expression=evaluator)
589
507
 
590
- spec = BranchNodeSpec(name = "branch_" + uuid.uuid4().hex, evaluator=e)
508
+ spec = BranchNodeSpec(name = "branch_" + str(self._next_sequence_id()), evaluator=e)
591
509
  branch_node = Branch(spec = spec, containing_flow=self)
592
- return cast(Branch, self._node(branch_node))
510
+ return cast(Branch, self._add_node(branch_node))
593
511
 
594
512
  def wait_for(self, *args) -> "Wait":
595
513
  '''Wait for all incoming nodes to complete.'''
@@ -608,7 +526,7 @@ class Flow(Node):
608
526
 
609
527
  def foreach(self, item_schema: type[BaseModel],
610
528
  input_schema: type[BaseModel] |None=None,
611
- output_schema: type[BaseModel] |None=None) -> "Flow": # return an Foreach object
529
+ output_schema: type[BaseModel] |None=None) -> "Foreach": # return an Foreach object
612
530
  '''TODO: Docstrings'''
613
531
 
614
532
  output_schema_obj = _get_json_schema_obj("output", output_schema)
@@ -625,7 +543,7 @@ class Flow(Node):
625
543
  },
626
544
  required = ["items"])
627
545
 
628
- spec = ForeachSpec(name = "foreach_" + uuid.uuid4().hex,
546
+ spec = ForeachSpec(name = "foreach_" + str(self._next_sequence_id()),
629
547
  input_schema=ToolRequestBody(
630
548
  type=input_schema_obj.type,
631
549
  properties=input_schema_obj.properties,
@@ -638,15 +556,14 @@ class Flow(Node):
638
556
  ) if output_schema_obj is not None else None,
639
557
  item_schema = foreach_item_schema)
640
558
  foreach_obj = Foreach(spec = spec, parent = self)
641
- foreach_node = self._node(foreach_obj)
559
+ foreach_node = self._add_node(foreach_obj)
642
560
  self._add_schema(foreach_item_schema)
643
561
 
644
562
  return cast(Flow, foreach_node)
645
563
 
646
564
  def loop(self, evaluator: Union[Callable, Expression],
647
565
  input_schema: type[BaseModel]|None=None,
648
- output_schema: type[BaseModel]|None=None) -> "Flow": # return a WhileLoop object
649
- '''TODO: Docstrings'''
566
+ output_schema: type[BaseModel]|None=None) -> "Loop": # return a WhileLoop object
650
567
  e = evaluator
651
568
  input_schema_obj = _get_json_schema_obj("input", input_schema)
652
569
  output_schema_obj = _get_json_schema_obj("output", output_schema)
@@ -661,7 +578,7 @@ class Flow(Node):
661
578
  elif isinstance(evaluator, str):
662
579
  e = Expression(expression=evaluator)
663
580
 
664
- loop_spec = LoopSpec(name = "loop_" + uuid.uuid4().hex,
581
+ loop_spec = LoopSpec(name = "loop_" + str(self._next_sequence_id()),
665
582
  evaluator = e,
666
583
  input_schema=ToolRequestBody(
667
584
  type=input_schema_obj.type,
@@ -674,8 +591,35 @@ class Flow(Node):
674
591
  required=output_schema_obj.required
675
592
  ) if output_schema_obj is not None else None)
676
593
  while_loop = Loop(spec = loop_spec, parent = self)
677
- while_node = self._node(while_loop)
678
- return while_node
594
+ while_node = self._add_node(while_loop)
595
+ return cast(Loop, while_node)
596
+
597
+ def userflow(self,
598
+ owners: Sequence[str] = [],
599
+ input_schema: type[BaseModel] |None=None,
600
+ output_schema: type[BaseModel] |None=None) -> "UserFlow": # return a UserFlow object
601
+
602
+ logger.warning("userflow is NOT working yet.")
603
+
604
+ output_schema_obj = _get_json_schema_obj("output", output_schema)
605
+ input_schema_obj = _get_json_schema_obj("input", input_schema)
606
+
607
+ spec = UserFlowSpec(name = "userflow_" + str(self._next_sequence_id()),
608
+ input_schema=ToolRequestBody(
609
+ type=input_schema_obj.type,
610
+ properties=input_schema_obj.properties,
611
+ required=input_schema_obj.required,
612
+ ) if input_schema_obj is not None else None,
613
+ output_schema=ToolResponseBody(
614
+ type=output_schema_obj.type,
615
+ properties=output_schema_obj.properties,
616
+ required=output_schema_obj.required
617
+ ) if output_schema_obj is not None else None,
618
+ owners = owners)
619
+ userflow_obj = UserFlow(spec = spec, parent = self)
620
+ userflow_node = self._add_node(userflow_obj)
621
+
622
+ return cast(UserFlow, userflow_node)
679
623
 
680
624
  def validate_model(self) -> bool:
681
625
  ''' Validate the model. '''
@@ -787,25 +731,8 @@ class Flow(Node):
787
731
  node_id = node
788
732
  return node_id
789
733
 
790
- def _get_data_map(self, map_fn: Callable | List[Assignment]) -> DataMap:
791
- if map_fn:
792
- if isinstance(map_fn, Callable):
793
- raise ValueError("Datamap with function is not supported yet.")
794
- # map_spec = getattr(map_fn, "__map_spec__", None)
795
- # if not map_spec:
796
- # raise ValueError(
797
- # "Only functions with @map decorator can be used to map between nodes.")
798
- # map_spec_copy = copy.deepcopy(map_spec)
799
- # self.refactor_datamap_spec_to_schemaref(map_spec_copy)
800
- # data_map = FnDataMap(spec=map_spec_copy)
801
- # return data_map
802
- elif isinstance(map_fn, list):
803
- data_map = AssignmentDataMap(spec=AssignmentDataMapSpec(
804
- name="assignment",
805
- maps=map_fn))
806
- return data_map
807
- return None
808
-
734
+ def _get_data_map(self, map: DataMap) -> DataMap:
735
+ return map
809
736
 
810
737
  class FlowRunStatus(str, Enum):
811
738
  NOT_STARTED = "not_started"
@@ -831,7 +758,6 @@ class FlowRun(BaseModel):
831
758
  "arbitrary_types_allowed": True
832
759
  }
833
760
 
834
-
835
761
  async def _arun_events(self, input_data:dict=None, filters: Sequence[Union[FlowEventType, TaskEventType]]=None) -> AsyncIterator[FlowEvent]:
836
762
 
837
763
  if self.status is not FlowRunStatus.NOT_STARTED:
@@ -1176,10 +1102,6 @@ class Loop(Flow):
1176
1102
  def __init__(self, **kwargs):
1177
1103
  super().__init__(**kwargs)
1178
1104
 
1179
- # refactor item schema
1180
- if isinstance(self.spec.evaluator, ScriptNodeSpec):
1181
- self._refactor_spec_to_schemaref(self.spec.evaluator)
1182
-
1183
1105
  def to_json(self) -> dict[str, Any]:
1184
1106
  my_dict = super().to_json()
1185
1107
 
@@ -1286,3 +1208,103 @@ class FlowValidator(BaseModel):
1286
1208
  bool: True if there are no errors, False otherwise.
1287
1209
  '''
1288
1210
  return not any(m.kind == FlowValidationKind.ERROR for m in messages)
1211
+
1212
+ class UserFlow(Flow):
1213
+ '''
1214
+ A flow that represents a series of user nodes.
1215
+ A user flow can include other nodes, but not another User Flows.
1216
+ '''
1217
+
1218
+ def __repr__(self):
1219
+ return f"UserFlow(name='{self.spec.name}', description='{self.spec.description}')"
1220
+
1221
+ def get_spec(self) -> NodeSpec:
1222
+ return cast(UserFlowSpec, self.spec)
1223
+
1224
+ def to_json(self) -> dict[str, Any]:
1225
+ my_dict = super().to_json()
1226
+
1227
+ return my_dict
1228
+
1229
+ def field(self,
1230
+ name: str,
1231
+ kind: UserFieldKind = UserFieldKind.Text,
1232
+ display_name: str | None = None,
1233
+ description: str | None = None,
1234
+ owners: list[str] = [],
1235
+ default: Any | None = None,
1236
+ text: str = None,
1237
+ option: UserFieldOption | None = None,
1238
+ input_map: DataMap = None,
1239
+ custom: dict[str, Any] = {}) -> UserNode:
1240
+ '''create a node in the flow'''
1241
+ # create a json schema object based on the single field
1242
+ if not name:
1243
+ raise AssertionError("name cannot be empty")
1244
+
1245
+ schema_obj = JsonSchemaObject(type="object",
1246
+ title=name,
1247
+ description=description)
1248
+
1249
+ schema_obj.properties = {}
1250
+ schema_obj.properties[name] = UserFieldKind.convert_kind_to_schema_property(kind, name, description, default, option, custom)
1251
+
1252
+ return self.user(name,
1253
+ display_name=display_name,
1254
+ description=description,
1255
+ owners=owners,
1256
+ text=text,
1257
+ output_schema=schema_obj,
1258
+ input_map=input_map)
1259
+
1260
+ def user(
1261
+ self,
1262
+ name: str | None = None,
1263
+ display_name: str | None = None,
1264
+ description: str | None = None,
1265
+ owners: list[str] = [],
1266
+ text: str | None = None,
1267
+ output_schema: type[BaseModel] | JsonSchemaObject| None = None,
1268
+ input_map: DataMap = None,
1269
+ ) -> UserNode:
1270
+ '''create a user node in the flow'''
1271
+
1272
+ output_schema_obj = output_schema
1273
+ if inspect.isclass(output_schema):
1274
+ # create input spec
1275
+ output_schema_obj = _get_json_schema_obj(parameter_name = "output", type_def = output_schema)
1276
+ # input and output is always the same in an user node
1277
+ output_schema_obj = output_schema_obj
1278
+
1279
+ # identify owner
1280
+ if not owners:
1281
+ owners = [ANY_USER]
1282
+
1283
+ # Create the tool spec
1284
+ task_spec = UserNodeSpec(
1285
+ name=name,
1286
+ display_name=display_name,
1287
+ description=description,
1288
+ owners=owners,
1289
+ input_schema=None,
1290
+ output_schema=ToolResponseBody(
1291
+ type=output_schema_obj.type,
1292
+ properties=output_schema_obj.properties,
1293
+ required=output_schema_obj.required
1294
+ ),
1295
+ text=text,
1296
+ output_schema_object = output_schema_obj
1297
+ )
1298
+
1299
+ task_spec.setup_fields()
1300
+
1301
+ node = UserNode(spec = task_spec)
1302
+
1303
+ # setup input map
1304
+ if input_map:
1305
+ node.input_map = self._get_data_map(input_map)
1306
+
1307
+ node = self._add_node(node)
1308
+ return cast(UserNode, node)
1309
+
1310
+