ibm-watsonx-orchestrate 1.3.0__py3-none-any.whl → 1.4.2__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 (54) hide show
  1. ibm_watsonx_orchestrate/__init__.py +1 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +2 -0
  3. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +9 -2
  4. ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +32 -0
  5. ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +42 -0
  6. ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +10 -1
  7. ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +4 -2
  8. ibm_watsonx_orchestrate/agent_builder/tools/types.py +2 -1
  9. ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +29 -0
  10. ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +271 -12
  11. ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +17 -2
  12. ibm_watsonx_orchestrate/cli/commands/models/env_file_model_provider_mapper.py +180 -0
  13. ibm_watsonx_orchestrate/cli/commands/models/models_command.py +194 -8
  14. ibm_watsonx_orchestrate/cli/commands/server/server_command.py +117 -48
  15. ibm_watsonx_orchestrate/cli/commands/server/types.py +105 -0
  16. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +55 -7
  17. ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +123 -42
  18. ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +22 -1
  19. ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +197 -12
  20. ibm_watsonx_orchestrate/client/agents/agent_client.py +4 -1
  21. ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +5 -1
  22. ibm_watsonx_orchestrate/client/agents/external_agent_client.py +5 -1
  23. ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +2 -6
  24. ibm_watsonx_orchestrate/client/base_api_client.py +5 -2
  25. ibm_watsonx_orchestrate/client/connections/connections_client.py +3 -9
  26. ibm_watsonx_orchestrate/client/model_policies/__init__.py +0 -0
  27. ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +47 -0
  28. ibm_watsonx_orchestrate/client/model_policies/types.py +36 -0
  29. ibm_watsonx_orchestrate/client/models/__init__.py +0 -0
  30. ibm_watsonx_orchestrate/client/models/models_client.py +46 -0
  31. ibm_watsonx_orchestrate/client/models/types.py +177 -0
  32. ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +15 -6
  33. ibm_watsonx_orchestrate/client/tools/tempus_client.py +40 -0
  34. ibm_watsonx_orchestrate/client/tools/tool_client.py +8 -0
  35. ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -13
  36. ibm_watsonx_orchestrate/docker/default.env +22 -12
  37. ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -1
  38. ibm_watsonx_orchestrate/experimental/flow_builder/__init__.py +0 -0
  39. ibm_watsonx_orchestrate/experimental/flow_builder/flows/__init__.py +41 -0
  40. ibm_watsonx_orchestrate/experimental/flow_builder/flows/constants.py +17 -0
  41. ibm_watsonx_orchestrate/experimental/flow_builder/flows/data_map.py +91 -0
  42. ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +143 -0
  43. ibm_watsonx_orchestrate/experimental/flow_builder/flows/events.py +72 -0
  44. ibm_watsonx_orchestrate/experimental/flow_builder/flows/flow.py +1288 -0
  45. ibm_watsonx_orchestrate/experimental/flow_builder/node.py +97 -0
  46. ibm_watsonx_orchestrate/experimental/flow_builder/resources/flow_status.openapi.yml +98 -0
  47. ibm_watsonx_orchestrate/experimental/flow_builder/types.py +492 -0
  48. ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +113 -0
  49. ibm_watsonx_orchestrate/utils/utils.py +5 -2
  50. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/METADATA +4 -1
  51. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/RECORD +54 -32
  52. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/WHEEL +0 -0
  53. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/entry_points.txt +0 -0
  54. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,97 @@
1
+ import json
2
+ from typing import Any, cast
3
+ import uuid
4
+
5
+ import yaml
6
+ from pydantic import BaseModel, SerializeAsAny
7
+
8
+ from ibm_watsonx_orchestrate.agent_builder.tools.types import JsonSchemaObject, ToolResponseBody
9
+ from .utils import get_valid_name
10
+
11
+ from .types import EndNodeSpec, JsonSchemaObjectRef, NodeSpec, AgentNodeSpec, StartNodeSpec, ToolNodeSpec
12
+ from .flows.data_map import DataMap
13
+
14
+ class Node(BaseModel):
15
+ spec: SerializeAsAny[NodeSpec]
16
+ input_map: DataMap | None = None
17
+
18
+ def __call__(self, **kwargs):
19
+ pass
20
+
21
+ def dump_spec(self, file: str) -> None:
22
+ dumped = self.spec.model_dump(mode='json',
23
+ exclude_unset=True, exclude_none=True, by_alias=True)
24
+ with open(file, 'w', encoding="utf-8") as f:
25
+ if file.endswith('.yaml') or file.endswith('.yml'):
26
+ yaml.dump(dumped, f)
27
+ elif file.endswith('.json'):
28
+ json.dump(dumped, f, indent=2)
29
+ else:
30
+ raise ValueError('file must end in .json, .yaml, or .yml')
31
+
32
+ def dumps_spec(self) -> str:
33
+ dumped = self.spec.model_dump(mode='json',
34
+ exclude_unset=True, exclude_none=True, by_alias=True)
35
+ return json.dumps(dumped, indent=2)
36
+
37
+ def __repr__(self):
38
+ return f"Node(name='{self.spec.name}', description='{self.spec.description}')"
39
+
40
+ def to_json(self) -> dict[str, Any]:
41
+ model_spec = {}
42
+ model_spec["spec"] = self.spec.to_json()
43
+ if self.input_map is not None:
44
+ model_spec['input_map'] = self.input_map.to_json()
45
+ if hasattr(self, "output_map") and self.output_map is not None:
46
+ model_spec["output_map"] = self.output_map.to_json()
47
+
48
+ return model_spec
49
+
50
+ class StartNode(Node):
51
+ def __repr__(self):
52
+ return f"StartNode(name='{self.spec.name}', description='{self.spec.description}')"
53
+
54
+ def get_spec(self) -> StartNodeSpec:
55
+
56
+ return cast(StartNodeSpec, self.spec)
57
+
58
+ class EndNode(Node):
59
+ def __repr__(self):
60
+ return f"EndNode(name='{self.spec.name}', description='{self.spec.description}')"
61
+
62
+ def get_spec(self) -> EndNodeSpec:
63
+
64
+ return cast(EndNodeSpec, self.spec)
65
+
66
+ class ToolNode(Node):
67
+ def __repr__(self):
68
+ return f"ToolNode(name='{self.spec.name}', description='{self.spec.description}')"
69
+
70
+ def get_spec(self) -> ToolNodeSpec:
71
+
72
+ return cast(ToolNodeSpec, self.spec)
73
+
74
+ class UserNode(Node):
75
+ def __repr__(self):
76
+ return f"UserNode(name='{self.spec.name}', description='{self.spec.description}')"
77
+
78
+ def get_spec(self) -> NodeSpec:
79
+
80
+ return cast(NodeSpec, self.spec)
81
+
82
+ class AgentNode(Node):
83
+ def __repr__(self):
84
+ return f"AgentNode(name='{self.spec.name}', description='{self.spec.description}')"
85
+
86
+ def get_spec(self) -> AgentNodeSpec:
87
+
88
+ return cast(AgentNodeSpec, self.spec)
89
+
90
+ class NodeInstance(BaseModel):
91
+ node: Node
92
+ id: str # unique id of this task instance
93
+ flow: Any # the flow this task belongs to
94
+
95
+ def __init__(self, **kwargs): # type: ignore
96
+ super().__init__(**kwargs)
97
+ self.id = uuid.uuid4().hex
@@ -0,0 +1,98 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: watsonx Orchestrate Flow Status API
4
+ version: '0.1'
5
+ description: watsonx Orchestrate Flow Status API
6
+ security:
7
+ - IBM-WO-JWT: []
8
+ servers:
9
+ - url: http://wxo-tempus-runtime:9044
10
+ components:
11
+ securitySchemes:
12
+ IBM-WO-JWT:
13
+ type: http
14
+ scheme: bearer
15
+ bearerFormat: IBM-Watsonx-Orchestrate-JWT
16
+ schemas:
17
+ APIError:
18
+ type: object
19
+ properties:
20
+ data:
21
+ type: object
22
+ properties:
23
+ message:
24
+ type: string
25
+ additionalProperties: true
26
+ required:
27
+ - message
28
+ required:
29
+ - data
30
+ paths:
31
+ /v1/flows:
32
+ get:
33
+ description: Get flows status based on flow instance id.
34
+ tags:
35
+ - Flow
36
+ operationId: get_flow_status
37
+ security:
38
+ - IBM-WO-JWT: []
39
+ responses:
40
+ '200':
41
+ description: Return the current flow status based on the flow instance id.
42
+ content:
43
+ application/json:
44
+ schema:
45
+ type: array
46
+ items:
47
+ type: object
48
+ '400':
49
+ description: Bad input
50
+ content:
51
+ application/json:
52
+ schema:
53
+ $ref: '#/components/schemas/APIError'
54
+ '500':
55
+ description: Internal server error
56
+ content:
57
+ application/json:
58
+ schema:
59
+ $ref: '#/components/schemas/APIError'
60
+ parameters:
61
+ - in: query
62
+ name: flow_id
63
+ required: false
64
+ schema:
65
+ type: string
66
+ - in: query
67
+ name: version
68
+ required: false
69
+ schema:
70
+ type: string
71
+ - in: query
72
+ name: state
73
+ required: false
74
+ schema:
75
+ type: string
76
+ enum:
77
+ - completed
78
+ - in_progress
79
+ - interrupted
80
+ - failed
81
+ - in: query
82
+ name: instance_id
83
+ required: false
84
+ schema:
85
+ type: string
86
+ - in: query
87
+ name: page
88
+ required: false
89
+ schema:
90
+ type: number
91
+ default: 1
92
+ - in: query
93
+ name: page_size
94
+ required: false
95
+ schema:
96
+ type: number
97
+ default: 20
98
+ tags: []
@@ -0,0 +1,492 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ import inspect
4
+ import logging
5
+ from typing import (
6
+ Any, Callable, cast, Literal, List, NamedTuple, Optional, Sequence, Union
7
+ )
8
+
9
+ import docstring_parser
10
+ from munch import Munch
11
+ from pydantic import BaseModel, Field
12
+
13
+ from langchain_core.tools.base import create_schema_from_function
14
+ from langchain_core.utils.json_schema import dereference_refs
15
+
16
+ from ibm_watsonx_orchestrate.agent_builder.tools import PythonTool
17
+ from ibm_watsonx_orchestrate.experimental.flow_builder.flows.constants import ANY_USER
18
+ from ibm_watsonx_orchestrate.agent_builder.tools.types import (
19
+ ToolSpec, ToolRequestBody, ToolResponseBody, JsonSchemaObject
20
+ )
21
+ from .utils import get_valid_name
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ class JsonSchemaObjectRef(JsonSchemaObject):
26
+ ref: str=Field(description="The id of the schema to be used.", serialization_alias="$ref")
27
+
28
+ class SchemaRef(BaseModel):
29
+
30
+ ref: str = Field(description="The id of the schema to be used.", serialization_alias="$ref")
31
+
32
+ def _assign_attribute(obj, attr_name, schema):
33
+ if hasattr(schema, attr_name) and (getattr(schema, attr_name) is not None):
34
+ obj[attr_name] = getattr(schema, attr_name)
35
+
36
+ def _to_json_from_json_schema(schema: JsonSchemaObject) -> dict[str, Any]:
37
+ model_spec = {}
38
+ if isinstance(schema, dict):
39
+ schema = Munch(schema)
40
+ _assign_attribute(model_spec, "type", schema)
41
+ _assign_attribute(model_spec, "title", schema)
42
+ _assign_attribute(model_spec, "description", schema)
43
+ _assign_attribute(model_spec, "required", schema)
44
+
45
+ if hasattr(schema, "properties") and (schema.properties is not None):
46
+ model_spec["properties"] = {}
47
+ for prop_name, prop_schema in schema.properties.items():
48
+ model_spec["properties"][prop_name] = _to_json_from_json_schema(prop_schema)
49
+ if hasattr(schema, "items") and (schema.items is not None):
50
+ model_spec["items"] = _to_json_from_json_schema(schema.items)
51
+
52
+ _assign_attribute(model_spec, "default", schema)
53
+ _assign_attribute(model_spec, "enum", schema)
54
+ _assign_attribute(model_spec, "minimum", schema)
55
+ _assign_attribute(model_spec, "maximum", schema)
56
+ _assign_attribute(model_spec, "minLength", schema)
57
+ _assign_attribute(model_spec, "maxLength", schema)
58
+ _assign_attribute(model_spec, "format", schema)
59
+ _assign_attribute(model_spec, "pattern", schema)
60
+
61
+ if hasattr(schema, "anyOf") and getattr(schema, "anyOf") is not None:
62
+ model_spec["anyOf"] = [_to_json_from_json_schema(schema) for schema in schema.anyOf]
63
+
64
+ _assign_attribute(model_spec, "in_field", schema)
65
+ _assign_attribute(model_spec, "aliasName", schema)
66
+
67
+ if isinstance(schema, JsonSchemaObjectRef):
68
+ model_spec["$ref"] = schema.ref
69
+ return model_spec
70
+
71
+
72
+ def _to_json_from_input_schema(schema: Union[ToolRequestBody, SchemaRef]) -> dict[str, Any]:
73
+ model_spec = {}
74
+ if isinstance(schema, ToolRequestBody):
75
+ request_body = cast(ToolRequestBody, schema)
76
+ model_spec["type"] = request_body.type
77
+ if request_body.properties:
78
+ model_spec["properties"] = {}
79
+ for prop_name, prop_schema in request_body.properties.items():
80
+ model_spec["properties"][prop_name] = _to_json_from_json_schema(prop_schema)
81
+ model_spec["required"] = request_body.required
82
+ elif isinstance(schema, SchemaRef):
83
+ model_spec["$ref"] = schema.ref
84
+
85
+ return model_spec
86
+
87
+ def _to_json_from_output_schema(schema: Union[ToolResponseBody, SchemaRef]) -> dict[str, Any]:
88
+ model_spec = {}
89
+ if isinstance(schema, ToolResponseBody):
90
+ response_body = cast(ToolResponseBody, schema)
91
+ model_spec["type"] = response_body.type
92
+ if response_body.description:
93
+ model_spec["description"] = response_body.description
94
+ if response_body.properties:
95
+ model_spec["properties"] = {}
96
+ for prop_name, prop_schema in response_body.properties.items():
97
+ model_spec["properties"][prop_name] = _to_json_from_json_schema(prop_schema)
98
+ if response_body.items:
99
+ model_spec["items"] = _to_json_from_json_schema(response_body.items)
100
+ if response_body.uniqueItems:
101
+ model_spec["uniqueItems"] = response_body.uniqueItems
102
+ if response_body.anyOf:
103
+ model_spec["anyOf"] = [_to_json_from_json_schema(schema) for schema in response_body.anyOf]
104
+ if response_body.required and len(response_body.required) > 0:
105
+ model_spec["required"] = response_body.required
106
+ elif isinstance(schema, SchemaRef):
107
+ model_spec["$ref"] = schema.ref
108
+
109
+ return model_spec
110
+
111
+ class NodeSpec(BaseModel):
112
+
113
+ kind: Literal["node", "tool", "user", "script", "agent", "flow", "start", "decisions", "prompt", "branch", "wait", "foreach", "loop", "end"] = "node"
114
+ name: str
115
+ display_name: str | None = None
116
+ description: str | None = None
117
+ input_schema: ToolRequestBody | SchemaRef | None = None
118
+ output_schema: ToolResponseBody | SchemaRef | None = None
119
+ output_schema_object: JsonSchemaObject | SchemaRef | None = None
120
+
121
+ def __init__(self, **data):
122
+ super().__init__(**data)
123
+
124
+ if not self.name:
125
+ if self.display_name:
126
+ self.name = get_valid_name(self.display_name)
127
+ else:
128
+ raise ValueError("Either name or display_name must be specified.")
129
+
130
+ if not self.display_name:
131
+ if self.name:
132
+ self.display_name = self.name
133
+ else:
134
+ raise ValueError("Either name or display_name must be specified.")
135
+
136
+ # need to make sure name is valid
137
+ self.name = get_valid_name(self.name)
138
+
139
+ def to_json(self) -> dict[str, Any]:
140
+ '''Create a JSON object representing the data'''
141
+ model_spec = {}
142
+ model_spec["kind"] = self.kind
143
+ model_spec["name"] = self.name
144
+ if self.display_name:
145
+ model_spec["display_name"] = self.display_name
146
+ if self.description:
147
+ model_spec["description"] = self.description
148
+ if self.input_schema:
149
+ model_spec["input_schema"] = _to_json_from_input_schema(self.input_schema)
150
+ if self.output_schema:
151
+ if isinstance(self.output_schema, ToolResponseBody):
152
+ if self.output_schema.type != 'null':
153
+ model_spec["output_schema"] = _to_json_from_output_schema(self.output_schema)
154
+ else:
155
+ model_spec["output_schema"] = _to_json_from_output_schema(self.output_schema)
156
+
157
+ return model_spec
158
+
159
+ class StartNodeSpec(NodeSpec):
160
+ def __init__(self, **data):
161
+ super().__init__(**data)
162
+ self.kind = "start"
163
+
164
+ class EndNodeSpec(NodeSpec):
165
+ def __init__(self, **data):
166
+ super().__init__(**data)
167
+ self.kind = "end"
168
+
169
+ class ToolNodeSpec(NodeSpec):
170
+
171
+ tool: Union[str, ToolSpec] = Field(default = None, description="the tool to use")
172
+
173
+ def __init__(self, **data):
174
+ super().__init__(**data)
175
+ self.kind = "tool"
176
+
177
+ def to_json(self) -> dict[str, Any]:
178
+ model_spec = super().to_json()
179
+ if self.tool:
180
+ if isinstance(self.tool, ToolSpec):
181
+ model_spec["tool"] = self.tool.model_dump(exclude_defaults=True, exclude_none=True, exclude_unset=True)
182
+ else:
183
+ model_spec["tool"] = self.tool
184
+ return model_spec
185
+
186
+ class UserNodeSpec(NodeSpec):
187
+
188
+ owners: Sequence[str] = ANY_USER
189
+
190
+ def __init__(self, **data):
191
+ super().__init__(**data)
192
+ self.kind = "user"
193
+
194
+ def to_json(self) -> dict[str, Any]:
195
+ model_spec = super().to_json()
196
+ if self.owners:
197
+ model_spec["owners"] = self.owners
198
+ return model_spec
199
+
200
+ class AgentNodeSpec(ToolNodeSpec):
201
+
202
+ message: str | None = Field(default=None, description="The instructions for the task.")
203
+ guidelines: str | None = Field(default=None, description="The guidelines for the task.")
204
+ agent: str
205
+
206
+ def __init__(self, **data):
207
+ super().__init__(**data)
208
+ self.kind = "agent"
209
+
210
+ def to_json(self) -> dict[str, Any]:
211
+ model_spec = super().to_json()
212
+ if self.message:
213
+ model_spec["message"] = self.message
214
+ if self.guidelines:
215
+ model_spec["guidelines"] = self.guidelines
216
+ if self.agent:
217
+ model_spec["agent"] = self.agent
218
+ return model_spec
219
+
220
+ class Expression(BaseModel):
221
+ '''An expression could return a boolean or a value'''
222
+ expression: str = Field(description="A python expression to be run by the flow engine")
223
+
224
+ def to_json(self) -> dict[str, Any]:
225
+ model_spec = {}
226
+ model_spec["expression"] = self.expression;
227
+ return model_spec
228
+
229
+ class MatchPolicy(Enum):
230
+
231
+ FIRST_MATCH = 1
232
+ ANY_MATCH = 2
233
+
234
+ class FlowControlNodeSpec(NodeSpec):
235
+ ...
236
+
237
+ class BranchNodeSpec(FlowControlNodeSpec):
238
+ '''
239
+ A node that evaluates an expression and executes one of its cases based on the result.
240
+
241
+ Parameters:
242
+ evaluator (Expression): An expression that will be evaluated to determine which case to execute. The result can be a boolean, a label (string) or a list of labels.
243
+ cases (dict[str | bool, str]): A dictionary of labels to node names. The keys can be strings or booleans.
244
+ match_policy (MatchPolicy): The policy to use when evaluating the expression.
245
+ '''
246
+ evaluator: Expression
247
+ cases: dict[str | bool, str] = Field(default = {},
248
+ description="A dictionary of labels to node names.")
249
+ match_policy: MatchPolicy = Field(default = MatchPolicy.FIRST_MATCH)
250
+
251
+ def __init__(self, **data):
252
+ super().__init__(**data)
253
+ self.kind = "branch"
254
+
255
+ def to_json(self) -> dict[str, Any]:
256
+ my_dict = super().to_json()
257
+
258
+ if self.evaluator:
259
+ my_dict["evaluator"] = self.evaluator.to_json()
260
+
261
+ my_dict["cases"] = self.cases
262
+ my_dict["match_policy"] = self.match_policy.name
263
+ return my_dict
264
+
265
+
266
+ class WaitPolicy(Enum):
267
+
268
+ ONE_OF = 1
269
+ ALL_OF = 2
270
+ MIN_OF = 3
271
+
272
+ class WaitNodeSpec(FlowControlNodeSpec):
273
+
274
+ nodes: List[str] = []
275
+ wait_policy: WaitPolicy = Field(default = WaitPolicy.ALL_OF)
276
+ minimum_nodes: int = 1 # only used when the policy is MIN_OF
277
+
278
+ def __init__(self, **data):
279
+ super().__init__(**data)
280
+ self.kind = "wait"
281
+
282
+ def to_json(self) -> dict[str, Any]:
283
+ my_dict = super().to_json()
284
+
285
+ my_dict["nodes"] = self.nodes
286
+ my_dict["wait_policy"] = self.wait_policy.name
287
+ if (self.wait_policy == WaitPolicy.MIN_OF):
288
+ my_dict["minimum_nodes"] = self.minimum_nodes
289
+
290
+ return my_dict
291
+
292
+ class FlowSpec(NodeSpec):
293
+
294
+
295
+ # who can initiate the flow
296
+ initiators: Sequence[str] = ANY_USER
297
+
298
+ def __init__(self, **kwargs):
299
+ super().__init__(**kwargs)
300
+ self.kind = "flow"
301
+
302
+ def to_json(self) -> dict[str, Any]:
303
+ model_spec = super().to_json()
304
+ if self.initiators:
305
+ model_spec["initiators"] = self.initiators
306
+
307
+ return model_spec
308
+
309
+ class LoopSpec(FlowSpec):
310
+
311
+ evaluator: Expression = Field(description="the condition to evaluate")
312
+
313
+ def __init__(self, **kwargs):
314
+ super().__init__(**kwargs)
315
+ self.kind = "loop"
316
+
317
+ def to_json(self) -> dict[str, Any]:
318
+ model_spec = super().to_json()
319
+ if self.evaluator:
320
+ model_spec["evaluator"] = self.evaluator.to_json()
321
+
322
+ return model_spec
323
+
324
+ class ForeachPolicy(Enum):
325
+
326
+ SEQUENTIAL = 1
327
+ # support only SEQUENTIAL for now
328
+ # PARALLEL = 2
329
+
330
+ class ForeachSpec(FlowSpec):
331
+
332
+ item_schema: JsonSchemaObject | SchemaRef = Field(description="The schema of the items in the list")
333
+ foreach_policy: ForeachPolicy = Field(default=ForeachPolicy.SEQUENTIAL, description="The type of foreach loop")
334
+
335
+ def __init__(self, **kwargs):
336
+ super().__init__(**kwargs)
337
+ self.kind = "foreach"
338
+
339
+ def to_json(self) -> dict[str, Any]:
340
+ my_dict = super().to_json()
341
+
342
+ if isinstance(self.item_schema, JsonSchemaObject):
343
+ my_dict["item_schema"] = _to_json_from_json_schema(self.item_schema)
344
+ else:
345
+ my_dict["item_schema"] = self.item_schema.model_dump(exclude_defaults=True, exclude_none=True, exclude_unset=True)
346
+
347
+ my_dict["foreach_policy"] = self.foreach_policy.name
348
+ return my_dict
349
+
350
+ class TaskData(NamedTuple):
351
+
352
+ inputs: dict | None = None
353
+ outputs: dict | None = None
354
+
355
+ class TaskEventType(Enum):
356
+
357
+ ON_TASK_WAIT = "on_task_wait" # the task is waiting for inputs before proceeding
358
+ ON_TASK_START = "on_task_start"
359
+ ON_TASK_END = "on_task_end"
360
+ ON_TASK_STREAM = "on_task_stream"
361
+ ON_TASK_ERROR = "on_task_error"
362
+
363
+ class FlowContext(BaseModel):
364
+
365
+ name: str | None = None # name of the process or task
366
+ task_id: str | None = None # id of the task, this is at the task definition level
367
+ flow_id: str | None = None # id of the flow, this is at the flow definition level
368
+ instance_id: str | None = None
369
+ thread_id: str | None = None
370
+ instance_id: str | None = None
371
+ thread_id: str | None = None
372
+ parent_context: Any | None = None
373
+ child_context: List["FlowContext"] | None = None
374
+ metadata: dict = Field(default_factory=dict[str, Any])
375
+ data: dict = Field(default_factory=dict[str, Any])
376
+
377
+ def get(self, key: str) -> Any:
378
+
379
+ if key in self.data:
380
+ return self.data[key]
381
+
382
+ if self.parent_context:
383
+ pc = cast(FlowContext, self.parent_conetxt)
384
+ return pc.get(key)
385
+
386
+ class FlowEventType(Enum):
387
+
388
+ ON_FLOW_START = "on_flow_start"
389
+ ON_FLOW_END = "on_flow_end"
390
+ ON_FLOW_ERROR = "on_flow_error"
391
+
392
+
393
+ @dataclass
394
+ class FlowEvent:
395
+
396
+ kind: Union[FlowEventType, TaskEventType] # type of event
397
+ context: FlowContext
398
+ error: dict | None = None # error message if any
399
+
400
+
401
+ class Assignment(BaseModel):
402
+ '''
403
+ This class represents an assignment in the system. Specify an expression that
404
+ can be used to retrieve or set a value in the FlowContext
405
+
406
+ Attributes:
407
+ target (str): The target of the assignment. Always assume the context is the current Node. e.g. "name"
408
+ source (str): The source code of the assignment. This can be a simple variable name or a more python expression.
409
+ e.g. "node.input.name" or "=f'{node.output.name}_{node.output.id}'"
410
+
411
+ '''
412
+ target: str
413
+ source: str
414
+
415
+ def extract_node_spec(
416
+ fn: Callable | PythonTool,
417
+ name: Optional[str] = None,
418
+ description: Optional[str] = None) -> NodeSpec:
419
+ """Extract the task specification from a function. """
420
+ if isinstance(fn, PythonTool):
421
+ fn = cast(PythonTool, fn).fn
422
+
423
+ if fn.__doc__ is not None:
424
+ doc = docstring_parser.parse(fn.__doc__)
425
+ else:
426
+ doc = None
427
+
428
+ # Use the function docstring if no description is provided
429
+ _desc = description
430
+ if description is None and doc is not None:
431
+ _desc = doc.description
432
+
433
+ # Use the function name if no name is provided
434
+ _name = name or fn.__name__
435
+
436
+ # Create the input schema from the function
437
+ input_schema: type[BaseModel] = create_schema_from_function(_name, fn, parse_docstring=False)
438
+ input_schema_json = input_schema.model_json_schema()
439
+ input_schema_json = dereference_refs(input_schema_json)
440
+ # logger.info("Input schema: %s", input_schema_json)
441
+
442
+ # Convert the input schema to a JsonSchemaObject
443
+ input_schema_obj = JsonSchemaObject(**input_schema_json)
444
+
445
+ # Get the function signature
446
+ sig = inspect.signature(fn)
447
+
448
+ # Get the function return type
449
+ return_type = sig.return_annotation
450
+ output_schema = ToolResponseBody(type='null')
451
+ output_schema_obj = None
452
+
453
+ if not return_type or return_type == inspect._empty:
454
+ pass
455
+ elif isinstance(return_type, type) and issubclass(return_type, BaseModel):
456
+ output_schema_json = return_type.model_json_schema()
457
+ output_schema_obj = JsonSchemaObject(**output_schema_json)
458
+ output_schema = ToolResponseBody(
459
+ type="object",
460
+ properties=output_schema_obj.properties or {},
461
+ required=output_schema_obj.required or []
462
+ )
463
+ elif isinstance(return_type, type):
464
+ schema_type = 'object'
465
+ if return_type == str:
466
+ schema_type = 'string'
467
+ elif return_type == int:
468
+ schema_type = 'integer'
469
+ elif return_type == float:
470
+ schema_type = 'number'
471
+ elif return_type == bool:
472
+ schema_type = 'boolean'
473
+ elif issubclass(return_type, list):
474
+ schema_type = 'array'
475
+ # TODO: inspect the list item type and use that as the item type
476
+ output_schema = ToolResponseBody(type=schema_type)
477
+
478
+ # Create the tool spec
479
+ spec = NodeSpec(
480
+ name=_name,
481
+ description=_desc,
482
+ input_schema=ToolRequestBody(
483
+ type=input_schema_obj.type,
484
+ properties=input_schema_obj.properties or {},
485
+ required=input_schema_obj.required or []
486
+ ),
487
+ output_schema=output_schema,
488
+ output_schema_object = output_schema_obj
489
+ )
490
+
491
+ # logger.info("Generated node spec: %s", spec)
492
+ return spec