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.
- ibm_watsonx_orchestrate/__init__.py +2 -1
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +2 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +10 -2
- ibm_watsonx_orchestrate/agent_builder/toolkits/base_toolkit.py +32 -0
- ibm_watsonx_orchestrate/agent_builder/toolkits/types.py +42 -0
- ibm_watsonx_orchestrate/agent_builder/tools/openapi_tool.py +10 -1
- ibm_watsonx_orchestrate/agent_builder/tools/python_tool.py +4 -2
- ibm_watsonx_orchestrate/agent_builder/tools/types.py +2 -1
- ibm_watsonx_orchestrate/cli/commands/agents/agents_command.py +29 -0
- ibm_watsonx_orchestrate/cli/commands/agents/agents_controller.py +271 -12
- ibm_watsonx_orchestrate/cli/commands/knowledge_bases/knowledge_bases_controller.py +17 -2
- ibm_watsonx_orchestrate/cli/commands/models/env_file_model_provider_mapper.py +180 -0
- ibm_watsonx_orchestrate/cli/commands/models/models_command.py +199 -8
- ibm_watsonx_orchestrate/cli/commands/server/server_command.py +117 -48
- ibm_watsonx_orchestrate/cli/commands/server/types.py +105 -0
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_command.py +55 -7
- ibm_watsonx_orchestrate/cli/commands/toolkit/toolkit_controller.py +123 -42
- ibm_watsonx_orchestrate/cli/commands/tools/tools_command.py +22 -1
- ibm_watsonx_orchestrate/cli/commands/tools/tools_controller.py +197 -12
- ibm_watsonx_orchestrate/client/agents/agent_client.py +4 -1
- ibm_watsonx_orchestrate/client/agents/assistant_agent_client.py +5 -1
- ibm_watsonx_orchestrate/client/agents/external_agent_client.py +5 -1
- ibm_watsonx_orchestrate/client/analytics/llm/analytics_llm_client.py +2 -6
- ibm_watsonx_orchestrate/client/base_api_client.py +5 -2
- ibm_watsonx_orchestrate/client/connections/connections_client.py +3 -9
- ibm_watsonx_orchestrate/client/model_policies/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/model_policies/model_policies_client.py +47 -0
- ibm_watsonx_orchestrate/client/model_policies/types.py +36 -0
- ibm_watsonx_orchestrate/client/models/__init__.py +0 -0
- ibm_watsonx_orchestrate/client/models/models_client.py +46 -0
- ibm_watsonx_orchestrate/client/models/types.py +189 -0
- ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +20 -6
- ibm_watsonx_orchestrate/client/tools/tempus_client.py +40 -0
- ibm_watsonx_orchestrate/client/tools/tool_client.py +8 -0
- ibm_watsonx_orchestrate/docker/compose-lite.yml +68 -13
- ibm_watsonx_orchestrate/docker/default.env +22 -12
- ibm_watsonx_orchestrate/docker/tempus/common-config.yaml +1 -1
- ibm_watsonx_orchestrate/experimental/flow_builder/__init__.py +0 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/data_map.py +19 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/__init__.py +42 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/constants.py +19 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +144 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/events.py +72 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/flow.py +1310 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/node.py +116 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/resources/flow_status.openapi.yml +66 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/types.py +765 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +115 -0
- ibm_watsonx_orchestrate/utils/utils.py +5 -2
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/METADATA +4 -1
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/RECORD +54 -32
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.5.0b0.dist-info}/entry_points.txt +0 -0
- {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
|