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.
- ibm_watsonx_orchestrate/__init__.py +1 -1
- ibm_watsonx_orchestrate/agent_builder/agents/types.py +2 -0
- ibm_watsonx_orchestrate/agent_builder/knowledge_bases/types.py +9 -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 +194 -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 +177 -0
- ibm_watsonx_orchestrate/client/toolkit/toolkit_client.py +15 -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/flows/__init__.py +41 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/constants.py +17 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/data_map.py +91 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/decorators.py +143 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/events.py +72 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/flows/flow.py +1288 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/node.py +97 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/resources/flow_status.openapi.yml +98 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/types.py +492 -0
- ibm_watsonx_orchestrate/experimental/flow_builder/utils.py +113 -0
- ibm_watsonx_orchestrate/utils/utils.py +5 -2
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/METADATA +4 -1
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/RECORD +54 -32
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/WHEEL +0 -0
- {ibm_watsonx_orchestrate-1.3.0.dist-info → ibm_watsonx_orchestrate-1.4.2.dist-info}/entry_points.txt +0 -0
- {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
|