ibm-watsonx-orchestrate 1.3.0__py3-none-any.whl → 1.5.0b0__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 +2 -1
  2. ibm_watsonx_orchestrate/agent_builder/agents/types.py +2 -0
  3. ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +10 -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 +199 -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 +189 -0
  32. ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +20 -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/data_map.py +19 -0
  40. ibm_watsonx_orchestrate/experimental/flow_builder/flows/__init__.py +42 -0
  41. ibm_watsonx_orchestrate/experimental/flow_builder/flows/constants.py +19 -0
  42. ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +144 -0
  43. ibm_watsonx_orchestrate/experimental/flow_builder/flows/events.py +72 -0
  44. ibm_watsonx_orchestrate/experimental/flow_builder/flows/flow.py +1310 -0
  45. ibm_watsonx_orchestrate/experimental/flow_builder/node.py +116 -0
  46. ibm_watsonx_orchestrate/experimental/flow_builder/resources/flow_status.openapi.yml +66 -0
  47. ibm_watsonx_orchestrate/experimental/flow_builder/types.py +765 -0
  48. ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +115 -0
  49. ibm_watsonx_orchestrate/utils/utils.py +5 -2
  50. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/METADATA +4 -1
  51. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/RECORD +54 -32
  52. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/WHEEL +0 -0
  53. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/entry_points.txt +0 -0
  54. {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,765 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ import inspect
4
+ import logging
5
+ from typing import (
6
+ Any, Callable, Self, 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 = JsonSchemaObject.model_validate(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 hasattr(schema, 'model_extra') and schema.model_extra:
68
+ # for each extra fiels, add it to the model spec
69
+ for key, value in schema.model_extra.items():
70
+ model_spec[key] = value
71
+
72
+ if isinstance(schema, JsonSchemaObjectRef):
73
+ model_spec["$ref"] = schema.ref
74
+ return model_spec
75
+
76
+
77
+ def _to_json_from_input_schema(schema: Union[ToolRequestBody, SchemaRef]) -> dict[str, Any]:
78
+ model_spec = {}
79
+ if isinstance(schema, ToolRequestBody):
80
+ request_body = cast(ToolRequestBody, schema)
81
+ model_spec["type"] = request_body.type
82
+ if request_body.properties:
83
+ model_spec["properties"] = {}
84
+ for prop_name, prop_schema in request_body.properties.items():
85
+ model_spec["properties"][prop_name] = _to_json_from_json_schema(prop_schema)
86
+ model_spec["required"] = request_body.required
87
+ elif isinstance(schema, SchemaRef):
88
+ model_spec["$ref"] = schema.ref
89
+
90
+ return model_spec
91
+
92
+ def _to_json_from_output_schema(schema: Union[ToolResponseBody, SchemaRef]) -> dict[str, Any]:
93
+ model_spec = {}
94
+ if isinstance(schema, ToolResponseBody):
95
+ response_body = cast(ToolResponseBody, schema)
96
+ model_spec["type"] = response_body.type
97
+ if response_body.description:
98
+ model_spec["description"] = response_body.description
99
+ if response_body.properties:
100
+ model_spec["properties"] = {}
101
+ for prop_name, prop_schema in response_body.properties.items():
102
+ model_spec["properties"][prop_name] = _to_json_from_json_schema(prop_schema)
103
+ if response_body.items:
104
+ model_spec["items"] = _to_json_from_json_schema(response_body.items)
105
+ if response_body.uniqueItems:
106
+ model_spec["uniqueItems"] = response_body.uniqueItems
107
+ if response_body.anyOf:
108
+ model_spec["anyOf"] = [_to_json_from_json_schema(schema) for schema in response_body.anyOf]
109
+ if response_body.required and len(response_body.required) > 0:
110
+ model_spec["required"] = response_body.required
111
+ elif isinstance(schema, SchemaRef):
112
+ model_spec["$ref"] = schema.ref
113
+
114
+ return model_spec
115
+
116
+ class NodeSpec(BaseModel):
117
+ kind: Literal["node", "tool", "user", "agent", "flow", "start", "decisions", "prompt", "branch", "wait", "foreach", "loop", "userflow", "end"] = "node"
118
+ name: str
119
+ display_name: str | None = None
120
+ description: str | None = None
121
+ input_schema: ToolRequestBody | SchemaRef | None = None
122
+ output_schema: ToolResponseBody | SchemaRef | None = None
123
+ output_schema_object: JsonSchemaObject | SchemaRef | None = None
124
+
125
+ def __init__(self, **data):
126
+ super().__init__(**data)
127
+
128
+ if not self.name:
129
+ if self.display_name:
130
+ self.name = get_valid_name(self.display_name)
131
+ else:
132
+ raise ValueError("Either name or display_name must be specified.")
133
+
134
+ if not self.display_name:
135
+ if self.name:
136
+ self.display_name = self.name
137
+ else:
138
+ raise ValueError("Either name or display_name must be specified.")
139
+
140
+ # need to make sure name is valid
141
+ self.name = get_valid_name(self.name)
142
+
143
+ def to_json(self) -> dict[str, Any]:
144
+ '''Create a JSON object representing the data'''
145
+ model_spec = {}
146
+ model_spec["kind"] = self.kind
147
+ model_spec["name"] = self.name
148
+ if self.display_name:
149
+ model_spec["display_name"] = self.display_name
150
+ if self.description:
151
+ model_spec["description"] = self.description
152
+ if self.input_schema:
153
+ model_spec["input_schema"] = _to_json_from_input_schema(self.input_schema)
154
+ if self.output_schema:
155
+ if isinstance(self.output_schema, ToolResponseBody):
156
+ if self.output_schema.type != 'null':
157
+ model_spec["output_schema"] = _to_json_from_output_schema(self.output_schema)
158
+ else:
159
+ model_spec["output_schema"] = _to_json_from_output_schema(self.output_schema)
160
+
161
+ return model_spec
162
+
163
+ class StartNodeSpec(NodeSpec):
164
+ def __init__(self, **data):
165
+ super().__init__(**data)
166
+ self.kind = "start"
167
+
168
+ class EndNodeSpec(NodeSpec):
169
+ def __init__(self, **data):
170
+ super().__init__(**data)
171
+ self.kind = "end"
172
+
173
+ class ToolNodeSpec(NodeSpec):
174
+ tool: Union[str, ToolSpec] = Field(default = None, description="the tool to use")
175
+
176
+ def __init__(self, **data):
177
+ super().__init__(**data)
178
+ self.kind = "tool"
179
+
180
+ def to_json(self) -> dict[str, Any]:
181
+ model_spec = super().to_json()
182
+ if self.tool:
183
+ if isinstance(self.tool, ToolSpec):
184
+ model_spec["tool"] = self.tool.model_dump(exclude_defaults=True, exclude_none=True, exclude_unset=True)
185
+ else:
186
+ model_spec["tool"] = self.tool
187
+ return model_spec
188
+
189
+
190
+ class UserFieldValue(BaseModel):
191
+ text: str | None = None
192
+ value: str | None = None
193
+
194
+ def __init__(self, text: str | None = None, value: str | None = None):
195
+ super().__init__(text=text, value=value)
196
+ if self.value is None:
197
+ self.value = self.text
198
+
199
+ def to_json(self) -> dict[str, Any]:
200
+ model_spec = {}
201
+ if self.text:
202
+ model_spec["text"] = self.text
203
+ if self.value:
204
+ model_spec["value"] = self.value
205
+
206
+ return model_spec
207
+
208
+ class UserFieldOption(BaseModel):
209
+ label: str
210
+ values: list[UserFieldValue] | None = None
211
+
212
+ # create a constructor that will take a list and create UserFieldValue
213
+ def __init__(self, label: str, values=list[str]):
214
+ super().__init__(label=label)
215
+ self.values = []
216
+ for value in values:
217
+ item = UserFieldValue(text=value)
218
+ self.values.append(item)
219
+
220
+ def to_json(self) -> dict[str, Any]:
221
+ model_spec = {}
222
+ model_spec["label"] = self.label
223
+ if self.values and len(self.values) > 0:
224
+ model_spec["values"] = [value.to_json() for value in self.values]
225
+ return model_spec
226
+
227
+ class UserFieldKind(str, Enum):
228
+ Text: str = "text"
229
+ Date: str = "date"
230
+ DateTime: str = "datetime"
231
+ Time: str = "time"
232
+ Number: str = "number"
233
+ Document: str = "document"
234
+ Boolean: str = "boolean"
235
+ Object: str = "object"
236
+
237
+ def convert_python_type_to_kind(python_type: type) -> "UserFieldKind":
238
+ if inspect.isclass(python_type):
239
+ raise ValueError("Cannot convert class to kind")
240
+
241
+ if python_type == str:
242
+ return UserFieldKind.Text
243
+ elif python_type == int:
244
+ return UserFieldKind.Number
245
+ elif python_type == float:
246
+ return UserFieldKind.Number
247
+ elif python_type == bool:
248
+ return UserFieldKind.Boolean
249
+ elif python_type == list:
250
+ raise ValueError("Cannot convert list to kind")
251
+ elif python_type == dict:
252
+ raise ValueError("Cannot convert dict to kind")
253
+
254
+ return UserFieldKind.Text
255
+
256
+ def convert_kind_to_schema_property(kind: "UserFieldKind", name: str, description: str,
257
+ default: Any, option: UserFieldOption,
258
+ custom: dict[str, Any]) -> dict[str, Any]:
259
+ model_spec = {}
260
+ model_spec["title"] = name
261
+ model_spec["description"] = description
262
+ model_spec["default"] = default
263
+
264
+ model_spec["type"] = "string"
265
+ if kind == UserFieldKind.Date:
266
+ model_spec["format"] = "date"
267
+ elif kind == UserFieldKind.Time:
268
+ model_spec["format"] = "time"
269
+ elif kind == UserFieldKind.DateTime:
270
+ model_spec["format"] = "datetime"
271
+ elif kind == UserFieldKind.Number:
272
+ model_spec["format"] = "number"
273
+ elif kind == UserFieldKind.Boolean:
274
+ model_spec["type"] = "boolean"
275
+ elif kind == UserFieldKind.Document:
276
+ model_spec["format"] = "uri"
277
+ elif kind == UserFieldKind.Object:
278
+ raise ValueError("Object user fields are not supported.")
279
+
280
+ if option:
281
+ model_spec["enum"] = [value.text for value in option.values]
282
+
283
+ if custom:
284
+ for key, value in custom.items():
285
+ model_spec[key] = value
286
+ return model_spec
287
+
288
+
289
+ class UserField(BaseModel):
290
+ name: str
291
+ kind: UserFieldKind = UserFieldKind.Text
292
+ text: str | None = Field(default=None, description="A descriptive text that can be used to ask user about this field.")
293
+ display_name: str | None = None
294
+ description: str | None = None
295
+ default: Any | None = None
296
+ option: UserFieldOption | None = None
297
+ is_list: bool = False
298
+ custom: dict[str, Any] | None = None
299
+ widget: str | None = None
300
+
301
+ def to_json(self) -> dict[str, Any]:
302
+ model_spec = {}
303
+ if self.name:
304
+ model_spec["name"] = self.name
305
+ if self.kind:
306
+ model_spec["kind"] = self.kind.value
307
+ if self.text:
308
+ model_spec["text"] = self.text
309
+ if self.display_name:
310
+ model_spec["display_name"] = self.display_name
311
+ if self.description:
312
+ model_spec["description"] = self.description
313
+ if self.default:
314
+ model_spec["default"] = self.default
315
+ if self.is_list:
316
+ model_spec["is_list"] = self.is_list
317
+ if self.option:
318
+ model_spec["option"] = self.option.to_json()
319
+ if self.custom:
320
+ model_spec["custom"] = self.custom
321
+ if self.widget:
322
+ model_spec["widget"] = self.widget
323
+ return model_spec
324
+
325
+ class UserNodeSpec(NodeSpec):
326
+ owners: Sequence[str] | None = None
327
+ text: str | None = None
328
+ fields: list[UserField] | None = None
329
+
330
+ def __init__(self, **data):
331
+ super().__init__(**data)
332
+ self.fields = []
333
+ self.kind = "user"
334
+
335
+ def to_json(self) -> dict[str, Any]:
336
+ model_spec = super().to_json()
337
+ # remove input schema
338
+ # if "input_schema" in model_spec:
339
+ # raise ValueError("Input schema is not allowed for user node.")
340
+ # del model_spec["input_schema"]
341
+
342
+ if self.owners:
343
+ model_spec["owners"] = self.owners
344
+ if self.text:
345
+ model_spec["text"] = self.text
346
+ if self.fields and len(self.fields) > 0:
347
+ model_spec["fields"] = [field.to_json() for field in self.fields]
348
+
349
+ return model_spec
350
+
351
+ def field(self, name: str,
352
+ kind: UserFieldKind,
353
+ text: str | None = None,
354
+ display_name: str | None = None,
355
+ description: str | None = None,
356
+ default: Any | None = None,
357
+ option: list[str] | None = None, is_list: bool = False,
358
+ custom: dict[str, Any] | None = None,
359
+ widget: str | None = None):
360
+ userfield = UserField(name=name,
361
+ kind=kind,
362
+ text=text,
363
+ display_name=display_name,
364
+ description=description,
365
+ default=default,
366
+ option=option,
367
+ is_list=is_list,
368
+ custom=custom,
369
+ widget=widget)
370
+
371
+ # find the index of the field
372
+ i = 0
373
+ for field in self.fields:
374
+ if field.name == name:
375
+ break
376
+
377
+ if (len(self.fields) - 1) >= i:
378
+ self.fields[i] = userfield # replace
379
+ else:
380
+ self.fields.append(userfield) # append
381
+
382
+ def setup_fields(self):
383
+ # make sure fields are not there already
384
+ if hasattr(self, "fields") and len(self.fields) > 0:
385
+ raise ValueError("Fields are already defined.")
386
+
387
+ if self.output_schema:
388
+ if isinstance(self.output_schema, SchemaRef):
389
+ schema = dereference_refs(schema)
390
+ schema = self.output_schema
391
+
392
+ # get all the fields from JSON schema
393
+ if self.output_schema and isinstance(self.output_schema, ToolResponseBody):
394
+ self.fields = []
395
+ for prop_name, prop_schema in self.output_schema.properties.items():
396
+ self.fields.append(UserField(name=prop_name,
397
+ kind=UserFieldKind.convert_python_type_to_kind(prop_schema.type),
398
+ display_name=prop_schema.title,
399
+ description=prop_schema.description,
400
+ default=prop_schema.default,
401
+ option=self.setup_field_options(prop_schema.title, prop_schema.enum),
402
+ is_list=prop_schema.type == "array",
403
+ custom=prop_schema.model_extra))
404
+
405
+ def setup_field_options(self, name: str, enums: List[str]) -> UserFieldOption:
406
+ if enums:
407
+ option = UserFieldOption(label=name, values=enums)
408
+ return option
409
+ else:
410
+ return None
411
+
412
+
413
+
414
+ class AgentNodeSpec(ToolNodeSpec):
415
+ message: str | None = Field(default=None, description="The instructions for the task.")
416
+ guidelines: str | None = Field(default=None, description="The guidelines for the task.")
417
+ agent: str
418
+
419
+ def __init__(self, **data):
420
+ super().__init__(**data)
421
+ self.kind = "agent"
422
+
423
+ def to_json(self) -> dict[str, Any]:
424
+ model_spec = super().to_json()
425
+ if self.message:
426
+ model_spec["message"] = self.message
427
+ if self.guidelines:
428
+ model_spec["guidelines"] = self.guidelines
429
+ if self.agent:
430
+ model_spec["agent"] = self.agent
431
+ return model_spec
432
+
433
+ class PromptLLMParameters(BaseModel):
434
+ temperature: Optional[float] = None
435
+ min_new_tokens: Optional[int] = None
436
+ max_new_tokens: Optional[int] = None
437
+ top_k: Optional[int] = None
438
+ top_p: Optional[float] = None
439
+ stop_sequences: Optional[list[str]] = None
440
+
441
+ def to_json(self) -> dict[str, Any]:
442
+ model_spec = {}
443
+ if self.temperature:
444
+ model_spec["temperature"] = self.temperature
445
+ if self.min_new_tokens:
446
+ model_spec["min_new_tokens"] = self.min_new_tokens
447
+ if self.max_new_tokens:
448
+ model_spec["max_new_tokens"] = self.max_new_tokens
449
+ if self.top_k:
450
+ model_spec["top_k"] = self.top_k
451
+ if self.top_p:
452
+ model_spec["top_p"] = self.top_p
453
+ if self.stop_sequences:
454
+ model_spec["stop_sequences"] = self.stop_sequences
455
+ return model_spec
456
+
457
+
458
+ class PromptNodeSpec(NodeSpec):
459
+ system_prompt: str | list[str]
460
+ user_prompt: str | list[str]
461
+ llm: Optional[str]
462
+ llm_parameters: Optional[PromptLLMParameters]
463
+
464
+ def __init__(self, **kwargs):
465
+ super().__init__(**kwargs)
466
+ self.kind = "prompt"
467
+
468
+ def to_json(self) -> dict[str, Any]:
469
+ model_spec = super().to_json()
470
+ if self.system_prompt:
471
+ model_spec["system_prompt"] = self.system_prompt
472
+ if self.user_prompt:
473
+ model_spec["user_prompt"] = self.user_prompt
474
+ if self.llm:
475
+ model_spec["llm"] = self.llm
476
+ if self.llm_parameters:
477
+ model_spec["llm_parameters"] = self.llm_parameters.to_json()
478
+
479
+ return model_spec
480
+
481
+ class Expression(BaseModel):
482
+ '''An expression could return a boolean or a value'''
483
+ expression: str = Field(description="A python expression to be run by the flow engine")
484
+
485
+ def to_json(self) -> dict[str, Any]:
486
+ model_spec = {}
487
+ model_spec["expression"] = self.expression;
488
+ return model_spec
489
+
490
+ class MatchPolicy(Enum):
491
+
492
+ FIRST_MATCH = 1
493
+ ANY_MATCH = 2
494
+
495
+ class FlowControlNodeSpec(NodeSpec):
496
+ ...
497
+
498
+ class BranchNodeSpec(FlowControlNodeSpec):
499
+ '''
500
+ A node that evaluates an expression and executes one of its cases based on the result.
501
+
502
+ Parameters:
503
+ 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.
504
+ cases (dict[str | bool, str]): A dictionary of labels to node names. The keys can be strings or booleans.
505
+ match_policy (MatchPolicy): The policy to use when evaluating the expression.
506
+ '''
507
+ evaluator: Expression
508
+ cases: dict[str | bool, str] = Field(default = {},
509
+ description="A dictionary of labels to node names.")
510
+ match_policy: MatchPolicy = Field(default = MatchPolicy.FIRST_MATCH)
511
+
512
+ def __init__(self, **data):
513
+ super().__init__(**data)
514
+ self.kind = "branch"
515
+
516
+ def to_json(self) -> dict[str, Any]:
517
+ my_dict = super().to_json()
518
+
519
+ if self.evaluator:
520
+ my_dict["evaluator"] = self.evaluator.to_json()
521
+
522
+ my_dict["cases"] = self.cases
523
+ my_dict["match_policy"] = self.match_policy.name
524
+ return my_dict
525
+
526
+
527
+ class WaitPolicy(Enum):
528
+
529
+ ONE_OF = 1
530
+ ALL_OF = 2
531
+ MIN_OF = 3
532
+
533
+ class WaitNodeSpec(FlowControlNodeSpec):
534
+
535
+ nodes: List[str] = []
536
+ wait_policy: WaitPolicy = Field(default = WaitPolicy.ALL_OF)
537
+ minimum_nodes: int = 1 # only used when the policy is MIN_OF
538
+
539
+ def __init__(self, **data):
540
+ super().__init__(**data)
541
+ self.kind = "wait"
542
+
543
+ def to_json(self) -> dict[str, Any]:
544
+ my_dict = super().to_json()
545
+
546
+ my_dict["nodes"] = self.nodes
547
+ my_dict["wait_policy"] = self.wait_policy.name
548
+ if (self.wait_policy == WaitPolicy.MIN_OF):
549
+ my_dict["minimum_nodes"] = self.minimum_nodes
550
+
551
+ return my_dict
552
+
553
+ class FlowSpec(NodeSpec):
554
+
555
+
556
+ # who can initiate the flow
557
+ initiators: Sequence[str] = [ANY_USER]
558
+
559
+ def __init__(self, **kwargs):
560
+ super().__init__(**kwargs)
561
+ self.kind = "flow"
562
+
563
+ def to_json(self) -> dict[str, Any]:
564
+ model_spec = super().to_json()
565
+ if self.initiators:
566
+ model_spec["initiators"] = self.initiators
567
+
568
+ return model_spec
569
+
570
+ class LoopSpec(FlowSpec):
571
+
572
+ evaluator: Expression = Field(description="the condition to evaluate")
573
+
574
+ def __init__(self, **kwargs):
575
+ super().__init__(**kwargs)
576
+ self.kind = "loop"
577
+
578
+ def to_json(self) -> dict[str, Any]:
579
+ model_spec = super().to_json()
580
+ if self.evaluator:
581
+ model_spec["evaluator"] = self.evaluator.to_json()
582
+
583
+ return model_spec
584
+
585
+ class UserFlowSpec(FlowSpec):
586
+ owners: Sequence[str] = [ANY_USER]
587
+
588
+ def __init__(self, **kwargs):
589
+ super().__init__(**kwargs)
590
+ self.kind = "userflow"
591
+
592
+ def to_json(self) -> dict[str, Any]:
593
+ model_spec = super().to_json()
594
+ if self.initiators:
595
+ model_spec["owners"] = self.initiators
596
+
597
+ return model_spec
598
+
599
+ class ForeachPolicy(Enum):
600
+
601
+ SEQUENTIAL = 1
602
+ # support only SEQUENTIAL for now
603
+ # PARALLEL = 2
604
+
605
+ class ForeachSpec(FlowSpec):
606
+
607
+ item_schema: JsonSchemaObject | SchemaRef = Field(description="The schema of the items in the list")
608
+ foreach_policy: ForeachPolicy = Field(default=ForeachPolicy.SEQUENTIAL, description="The type of foreach loop")
609
+
610
+ def __init__(self, **kwargs):
611
+ super().__init__(**kwargs)
612
+ self.kind = "foreach"
613
+
614
+ def to_json(self) -> dict[str, Any]:
615
+ my_dict = super().to_json()
616
+
617
+ if isinstance(self.item_schema, JsonSchemaObject):
618
+ my_dict["item_schema"] = _to_json_from_json_schema(self.item_schema)
619
+ else:
620
+ my_dict["item_schema"] = self.item_schema.model_dump(exclude_defaults=True, exclude_none=True, exclude_unset=True)
621
+
622
+ my_dict["foreach_policy"] = self.foreach_policy.name
623
+ return my_dict
624
+
625
+ class TaskData(NamedTuple):
626
+
627
+ inputs: dict | None = None
628
+ outputs: dict | None = None
629
+
630
+ class TaskEventType(Enum):
631
+
632
+ ON_TASK_WAIT = "on_task_wait" # the task is waiting for inputs before proceeding
633
+ ON_TASK_START = "on_task_start"
634
+ ON_TASK_END = "on_task_end"
635
+ ON_TASK_STREAM = "on_task_stream"
636
+ ON_TASK_ERROR = "on_task_error"
637
+
638
+ class FlowContext(BaseModel):
639
+
640
+ name: str | None = None # name of the process or task
641
+ task_id: str | None = None # id of the task, this is at the task definition level
642
+ flow_id: str | None = None # id of the flow, this is at the flow definition level
643
+ instance_id: str | None = None
644
+ thread_id: str | None = None
645
+ parent_context: Any | None = None
646
+ child_context: List["FlowContext"] | None = None
647
+ metadata: dict = Field(default_factory=dict[str, Any])
648
+ data: dict = Field(default_factory=dict[str, Any])
649
+
650
+ def get(self, key: str) -> Any:
651
+
652
+ if key in self.data:
653
+ return self.data[key]
654
+
655
+ if self.parent_context:
656
+ pc = cast(FlowContext, self.parent_conetxt)
657
+ return pc.get(key)
658
+
659
+ class FlowEventType(Enum):
660
+
661
+ ON_FLOW_START = "on_flow_start"
662
+ ON_FLOW_END = "on_flow_end"
663
+ ON_FLOW_ERROR = "on_flow_error"
664
+
665
+
666
+ @dataclass
667
+ class FlowEvent:
668
+
669
+ kind: Union[FlowEventType, TaskEventType] # type of event
670
+ context: FlowContext
671
+ error: dict | None = None # error message if any
672
+
673
+
674
+ class Assignment(BaseModel):
675
+ '''
676
+ This class represents an assignment in the system. Specify an expression that
677
+ can be used to retrieve or set a value in the FlowContext
678
+
679
+ Attributes:
680
+ target (str): The target of the assignment. Always assume the context is the current Node. e.g. "name"
681
+ source (str): The source code of the assignment. This can be a simple variable name or a more python expression.
682
+ e.g. "node.input.name" or "=f'{node.output.name}_{node.output.id}'"
683
+
684
+ '''
685
+ target: str
686
+ source: str
687
+
688
+ def extract_node_spec(
689
+ fn: Callable | PythonTool,
690
+ name: Optional[str] = None,
691
+ description: Optional[str] = None) -> NodeSpec:
692
+ """Extract the task specification from a function. """
693
+ if isinstance(fn, PythonTool):
694
+ fn = cast(PythonTool, fn).fn
695
+
696
+ if fn.__doc__ is not None:
697
+ doc = docstring_parser.parse(fn.__doc__)
698
+ else:
699
+ doc = None
700
+
701
+ # Use the function docstring if no description is provided
702
+ _desc = description
703
+ if description is None and doc is not None:
704
+ _desc = doc.description
705
+
706
+ # Use the function name if no name is provided
707
+ _name = name or fn.__name__
708
+
709
+ # Create the input schema from the function
710
+ input_schema: type[BaseModel] = create_schema_from_function(_name, fn, parse_docstring=False)
711
+ input_schema_json = input_schema.model_json_schema()
712
+ input_schema_json = dereference_refs(input_schema_json)
713
+ # logger.info("Input schema: %s", input_schema_json)
714
+
715
+ # Convert the input schema to a JsonSchemaObject
716
+ input_schema_obj = JsonSchemaObject(**input_schema_json)
717
+
718
+ # Get the function signature
719
+ sig = inspect.signature(fn)
720
+
721
+ # Get the function return type
722
+ return_type = sig.return_annotation
723
+ output_schema = ToolResponseBody(type='null')
724
+ output_schema_obj = None
725
+
726
+ if not return_type or return_type == inspect._empty:
727
+ pass
728
+ elif inspect.isclass(return_type) and issubclass(return_type, BaseModel):
729
+ output_schema_json = return_type.model_json_schema()
730
+ output_schema_obj = JsonSchemaObject(**output_schema_json)
731
+ output_schema = ToolResponseBody(
732
+ type="object",
733
+ properties=output_schema_obj.properties or {},
734
+ required=output_schema_obj.required or []
735
+ )
736
+ elif isinstance(return_type, type):
737
+ schema_type = 'object'
738
+ if return_type == str:
739
+ schema_type = 'string'
740
+ elif return_type == int:
741
+ schema_type = 'integer'
742
+ elif return_type == float:
743
+ schema_type = 'number'
744
+ elif return_type == bool:
745
+ schema_type = 'boolean'
746
+ elif issubclass(return_type, list):
747
+ schema_type = 'array'
748
+ # TODO: inspect the list item type and use that as the item type
749
+ output_schema = ToolResponseBody(type=schema_type)
750
+
751
+ # Create the tool spec
752
+ spec = NodeSpec(
753
+ name=_name,
754
+ description=_desc,
755
+ input_schema=ToolRequestBody(
756
+ type=input_schema_obj.type,
757
+ properties=input_schema_obj.properties or {},
758
+ required=input_schema_obj.required or []
759
+ ),
760
+ output_schema=output_schema,
761
+ output_schema_object = output_schema_obj
762
+ )
763
+
764
+ # logger.info("Generated node spec: %s", spec)
765
+ return spec