codemie-sdk-python 0.1.685__py3-none-any.whl → 0.1.686__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.
- codemie_sdk/builders/__init__.py +0 -0
- codemie_sdk/builders/workflow/__init__.py +53 -0
- codemie_sdk/builders/workflow/builder.py +383 -0
- codemie_sdk/builders/workflow/definition.py +389 -0
- codemie_sdk/builders/workflow/publish.py +189 -0
- {codemie_sdk_python-0.1.685.dist-info → codemie_sdk_python-0.1.686.dist-info}/METADATA +313 -19
- {codemie_sdk_python-0.1.685.dist-info → codemie_sdk_python-0.1.686.dist-info}/RECORD +8 -3
- {codemie_sdk_python-0.1.685.dist-info → codemie_sdk_python-0.1.686.dist-info}/WHEEL +0 -0
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Copyright 2026 EPAM Systems, Inc. ("EPAM")
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Workflow builder module for CodeMie SDK.
|
|
16
|
+
|
|
17
|
+
``publish()`` is a bound method on :class:`WorkflowBuilder` (``builder.publish(client,
|
|
18
|
+
project, ...)``), not a free function — there is no standalone ``publish`` to import.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from ...models.workflow import WorkflowMode
|
|
22
|
+
from .builder import WorkflowBuilder
|
|
23
|
+
from .definition import (
|
|
24
|
+
CustomWorkflowNode,
|
|
25
|
+
WorkflowAssistant,
|
|
26
|
+
WorkflowAssistantTool,
|
|
27
|
+
WorkflowDefinition,
|
|
28
|
+
WorkflowMCPServer,
|
|
29
|
+
WorkflowNextState,
|
|
30
|
+
WorkflowRetryPolicy,
|
|
31
|
+
WorkflowState,
|
|
32
|
+
WorkflowStateCondition,
|
|
33
|
+
WorkflowStateSwitch,
|
|
34
|
+
WorkflowStateSwitchCondition,
|
|
35
|
+
WorkflowTool,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__: list[str] = [
|
|
39
|
+
"CustomWorkflowNode",
|
|
40
|
+
"WorkflowAssistant",
|
|
41
|
+
"WorkflowAssistantTool",
|
|
42
|
+
"WorkflowBuilder",
|
|
43
|
+
"WorkflowDefinition",
|
|
44
|
+
"WorkflowMCPServer",
|
|
45
|
+
"WorkflowMode",
|
|
46
|
+
"WorkflowNextState",
|
|
47
|
+
"WorkflowRetryPolicy",
|
|
48
|
+
"WorkflowState",
|
|
49
|
+
"WorkflowStateCondition",
|
|
50
|
+
"WorkflowStateSwitch",
|
|
51
|
+
"WorkflowStateSwitchCondition",
|
|
52
|
+
"WorkflowTool",
|
|
53
|
+
]
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# Copyright 2026 EPAM Systems, Inc. ("EPAM")
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Fluent, immutable builder for :class:`WorkflowDefinition`.
|
|
16
|
+
|
|
17
|
+
Every mutator method returns a brand-new :class:`WorkflowBuilder`; the
|
|
18
|
+
receiver is never mutated. Internal state lives in a private, frozen
|
|
19
|
+
dataclass (`_BuilderState`) that is copied via `dataclasses.replace` on
|
|
20
|
+
every mutation, so independent branches of the same builder chain never
|
|
21
|
+
observe each other's changes.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass, field, replace
|
|
25
|
+
from typing import TYPE_CHECKING, Any
|
|
26
|
+
|
|
27
|
+
from ...models.workflow import WorkflowMode
|
|
28
|
+
from .definition import (
|
|
29
|
+
CustomWorkflowNode,
|
|
30
|
+
WorkflowAssistant,
|
|
31
|
+
WorkflowDefinition,
|
|
32
|
+
WorkflowMCPServer,
|
|
33
|
+
WorkflowNextState,
|
|
34
|
+
WorkflowRetryPolicy,
|
|
35
|
+
WorkflowState,
|
|
36
|
+
WorkflowTool,
|
|
37
|
+
)
|
|
38
|
+
from .publish import _publish
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from ...client.client import CodeMieClient
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class _BuilderState:
|
|
46
|
+
"""Immutable snapshot of everything a :class:`WorkflowBuilder` holds."""
|
|
47
|
+
|
|
48
|
+
name: str
|
|
49
|
+
description: str | None = None
|
|
50
|
+
mode: WorkflowMode = WorkflowMode.SEQUENTIAL
|
|
51
|
+
shared: bool = False
|
|
52
|
+
icon_url: str | None = None
|
|
53
|
+
assistants: dict[str, WorkflowAssistant] = field(default_factory=dict)
|
|
54
|
+
tools: dict[str, WorkflowTool] = field(default_factory=dict)
|
|
55
|
+
custom_nodes: dict[str, CustomWorkflowNode] = field(default_factory=dict)
|
|
56
|
+
states: tuple[WorkflowState, ...] = ()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class WorkflowBuilder:
|
|
60
|
+
"""Fluent, immutable builder for a :class:`WorkflowDefinition`.
|
|
61
|
+
|
|
62
|
+
Every method returns a new :class:`WorkflowBuilder`; the instance a
|
|
63
|
+
method is called on is left completely untouched, so branching a
|
|
64
|
+
builder into two independent chains is always safe.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, name: str | None = None, *, _state: _BuilderState | None = None):
|
|
68
|
+
if _state is not None:
|
|
69
|
+
self._state = _state
|
|
70
|
+
else:
|
|
71
|
+
if not name:
|
|
72
|
+
raise ValueError("workflow name is required")
|
|
73
|
+
self._state = _BuilderState(name=name)
|
|
74
|
+
|
|
75
|
+
def _replaced(self, **changes: Any) -> "WorkflowBuilder":
|
|
76
|
+
"""Return a new builder wrapping a copy of this state with changes applied."""
|
|
77
|
+
return WorkflowBuilder(_state=replace(self._state, **changes))
|
|
78
|
+
|
|
79
|
+
def with_description(self, description: str) -> "WorkflowBuilder":
|
|
80
|
+
return self._replaced(description=description)
|
|
81
|
+
|
|
82
|
+
def with_mode(self, mode: WorkflowMode) -> "WorkflowBuilder":
|
|
83
|
+
return self._replaced(mode=mode)
|
|
84
|
+
|
|
85
|
+
def with_shared(self, shared: bool) -> "WorkflowBuilder":
|
|
86
|
+
return self._replaced(shared=shared)
|
|
87
|
+
|
|
88
|
+
def with_icon_url(self, icon_url: str) -> "WorkflowBuilder":
|
|
89
|
+
return self._replaced(icon_url=icon_url)
|
|
90
|
+
|
|
91
|
+
def add_assistant(
|
|
92
|
+
self,
|
|
93
|
+
id: str,
|
|
94
|
+
*,
|
|
95
|
+
system_prompt: str | None = None,
|
|
96
|
+
assistant_id: str | None = None,
|
|
97
|
+
**fields: Any,
|
|
98
|
+
) -> "WorkflowBuilder":
|
|
99
|
+
"""Register an inline assistant definition under the builder-local `id`.
|
|
100
|
+
|
|
101
|
+
Raises `ValueError` if neither `system_prompt` nor `assistant_id` is
|
|
102
|
+
given, mirroring the backend's `anyOf` requirement.
|
|
103
|
+
"""
|
|
104
|
+
if system_prompt is None and assistant_id is None:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"assistant {id!r} requires either system_prompt or assistant_id"
|
|
107
|
+
)
|
|
108
|
+
assistant = WorkflowAssistant(
|
|
109
|
+
id=id,
|
|
110
|
+
system_prompt=system_prompt,
|
|
111
|
+
assistant_id=assistant_id,
|
|
112
|
+
**fields,
|
|
113
|
+
)
|
|
114
|
+
return self._with_assistant(id, assistant)
|
|
115
|
+
|
|
116
|
+
def add_assistant_ref(self, id: str, assistant_name: str) -> "WorkflowBuilder":
|
|
117
|
+
"""Register a deliberately unresolved, name-only assistant reference."""
|
|
118
|
+
assistant = WorkflowAssistant(id=id, name=assistant_name)
|
|
119
|
+
return self._with_assistant(id, assistant)
|
|
120
|
+
|
|
121
|
+
def _with_assistant(
|
|
122
|
+
self, id: str, assistant: WorkflowAssistant
|
|
123
|
+
) -> "WorkflowBuilder":
|
|
124
|
+
if id in self._state.assistants:
|
|
125
|
+
raise ValueError(f"duplicate assistant id: {id!r}")
|
|
126
|
+
new_assistants = {**self._state.assistants, id: assistant}
|
|
127
|
+
return self._replaced(assistants=new_assistants)
|
|
128
|
+
|
|
129
|
+
def add_state(
|
|
130
|
+
self,
|
|
131
|
+
id: str,
|
|
132
|
+
assistant: str,
|
|
133
|
+
task: str,
|
|
134
|
+
next: WorkflowNextState,
|
|
135
|
+
*,
|
|
136
|
+
retry_policy: WorkflowRetryPolicy | None = None,
|
|
137
|
+
interrupt_before: bool = False,
|
|
138
|
+
**fields: Any,
|
|
139
|
+
) -> "WorkflowBuilder":
|
|
140
|
+
"""Append a state that runs the assistant registered under `assistant`.
|
|
141
|
+
|
|
142
|
+
Raises `ValueError` naming the id if `assistant` was not previously
|
|
143
|
+
registered via `add_assistant`/`add_assistant_ref` on this chain.
|
|
144
|
+
"""
|
|
145
|
+
if assistant not in self._state.assistants:
|
|
146
|
+
raise ValueError(f"unknown assistant id: {assistant!r}")
|
|
147
|
+
state = WorkflowState(
|
|
148
|
+
id=id,
|
|
149
|
+
assistant_id=assistant,
|
|
150
|
+
task=task,
|
|
151
|
+
next=next,
|
|
152
|
+
retry_policy=retry_policy,
|
|
153
|
+
interrupt_before=interrupt_before,
|
|
154
|
+
**fields,
|
|
155
|
+
)
|
|
156
|
+
return self._with_state(state)
|
|
157
|
+
|
|
158
|
+
def add_tool(
|
|
159
|
+
self,
|
|
160
|
+
id: str,
|
|
161
|
+
tool: str,
|
|
162
|
+
*,
|
|
163
|
+
tool_args: dict | None = None,
|
|
164
|
+
integration_alias: str | None = None,
|
|
165
|
+
mcp_server: WorkflowMCPServer | None = None,
|
|
166
|
+
**fields: Any,
|
|
167
|
+
) -> "WorkflowBuilder":
|
|
168
|
+
"""Declare a tool under `id` so tool states can run it.
|
|
169
|
+
|
|
170
|
+
`tool` is the tool's name; `id` is what `add_tool_state` refers to.
|
|
171
|
+
Set `mcp_server` for a tool that is served by an MCP server.
|
|
172
|
+
"""
|
|
173
|
+
declared = WorkflowTool(
|
|
174
|
+
id=id,
|
|
175
|
+
tool=tool,
|
|
176
|
+
tool_args=tool_args,
|
|
177
|
+
integration_alias=integration_alias,
|
|
178
|
+
mcp_server=mcp_server,
|
|
179
|
+
**fields,
|
|
180
|
+
)
|
|
181
|
+
if id in self._state.tools:
|
|
182
|
+
raise ValueError(f"duplicate tool id: {id!r}")
|
|
183
|
+
return self._replaced(tools={**self._state.tools, id: declared})
|
|
184
|
+
|
|
185
|
+
def add_custom_node(
|
|
186
|
+
self,
|
|
187
|
+
id: str,
|
|
188
|
+
custom_node_id: str | None = None,
|
|
189
|
+
*,
|
|
190
|
+
name: str | None = None,
|
|
191
|
+
model: str | None = None,
|
|
192
|
+
system_prompt: str | None = None,
|
|
193
|
+
config: dict | None = None,
|
|
194
|
+
) -> "WorkflowBuilder":
|
|
195
|
+
"""Declare a custom node under `id` so custom-node states can run it.
|
|
196
|
+
|
|
197
|
+
`custom_node_id` is the kind of node (for example
|
|
198
|
+
`"state_processor_node"`) and defaults to `id`, as on the platform.
|
|
199
|
+
"""
|
|
200
|
+
node = CustomWorkflowNode(
|
|
201
|
+
id=id,
|
|
202
|
+
custom_node_id=custom_node_id or id,
|
|
203
|
+
name=name,
|
|
204
|
+
model=model,
|
|
205
|
+
system_prompt=system_prompt,
|
|
206
|
+
config=config or {},
|
|
207
|
+
)
|
|
208
|
+
if id in self._state.custom_nodes:
|
|
209
|
+
raise ValueError(f"duplicate custom node id: {id!r}")
|
|
210
|
+
return self._replaced(custom_nodes={**self._state.custom_nodes, id: node})
|
|
211
|
+
|
|
212
|
+
def add_transform_node(
|
|
213
|
+
self,
|
|
214
|
+
id: str,
|
|
215
|
+
mappings: list[dict],
|
|
216
|
+
*,
|
|
217
|
+
name: str | None = None,
|
|
218
|
+
input_source: str | None = None,
|
|
219
|
+
input_key: str | None = None,
|
|
220
|
+
output_schema: dict | None = None,
|
|
221
|
+
default_output: dict | None = None,
|
|
222
|
+
on_error: str | None = None,
|
|
223
|
+
) -> "WorkflowBuilder":
|
|
224
|
+
"""Declare a transform node: a custom node that reshapes data without an LLM.
|
|
225
|
+
|
|
226
|
+
Each mapping is a dict with an `output_field` and a `type`
|
|
227
|
+
(`extract`, `template`, `condition`, `constant`, `script` or
|
|
228
|
+
`array_map`) plus that type's own keys. Only the options you pass
|
|
229
|
+
reach the node's config.
|
|
230
|
+
"""
|
|
231
|
+
if not mappings:
|
|
232
|
+
raise ValueError(f"transform node {id!r} needs at least one mapping")
|
|
233
|
+
if any("output_field" not in mapping for mapping in mappings):
|
|
234
|
+
raise ValueError(
|
|
235
|
+
f"every mapping of transform node {id!r} needs an 'output_field'"
|
|
236
|
+
)
|
|
237
|
+
options = {
|
|
238
|
+
"input_source": input_source,
|
|
239
|
+
"input_key": input_key,
|
|
240
|
+
"output_schema": output_schema,
|
|
241
|
+
"default_output": default_output,
|
|
242
|
+
"on_error": on_error,
|
|
243
|
+
}
|
|
244
|
+
config = {"mappings": list(mappings)}
|
|
245
|
+
config.update(
|
|
246
|
+
{key: value for key, value in options.items() if value is not None}
|
|
247
|
+
)
|
|
248
|
+
return self.add_custom_node(id, "transform_node", name=name, config=config)
|
|
249
|
+
|
|
250
|
+
def add_tool_state(
|
|
251
|
+
self,
|
|
252
|
+
id: str,
|
|
253
|
+
tool_id: str,
|
|
254
|
+
next: WorkflowNextState,
|
|
255
|
+
*,
|
|
256
|
+
tool_args: dict | None = None,
|
|
257
|
+
retry_policy: WorkflowRetryPolicy | None = None,
|
|
258
|
+
interrupt_before: bool = False,
|
|
259
|
+
**fields: Any,
|
|
260
|
+
) -> "WorkflowBuilder":
|
|
261
|
+
"""Append a state that runs the tool declared under `tool_id`.
|
|
262
|
+
|
|
263
|
+
Raises `ValueError` if `tool_id` was not declared with `add_tool`.
|
|
264
|
+
"""
|
|
265
|
+
if tool_id not in self._state.tools:
|
|
266
|
+
raise ValueError(f"unknown tool id: {tool_id!r}")
|
|
267
|
+
state = WorkflowState(
|
|
268
|
+
id=id,
|
|
269
|
+
tool_id=tool_id,
|
|
270
|
+
tool_args=tool_args,
|
|
271
|
+
next=next,
|
|
272
|
+
retry_policy=retry_policy,
|
|
273
|
+
interrupt_before=interrupt_before,
|
|
274
|
+
**fields,
|
|
275
|
+
)
|
|
276
|
+
return self._with_state(state)
|
|
277
|
+
|
|
278
|
+
def add_custom_node_state(
|
|
279
|
+
self,
|
|
280
|
+
id: str,
|
|
281
|
+
custom_node_id: str,
|
|
282
|
+
next: WorkflowNextState,
|
|
283
|
+
*,
|
|
284
|
+
task: str = "",
|
|
285
|
+
retry_policy: WorkflowRetryPolicy | None = None,
|
|
286
|
+
interrupt_before: bool = False,
|
|
287
|
+
**fields: Any,
|
|
288
|
+
) -> "WorkflowBuilder":
|
|
289
|
+
"""Append a state that runs the custom node declared under `custom_node_id`.
|
|
290
|
+
|
|
291
|
+
Works for transform nodes too. Raises `ValueError` if the node was not
|
|
292
|
+
declared with `add_custom_node` or `add_transform_node`.
|
|
293
|
+
"""
|
|
294
|
+
if custom_node_id not in self._state.custom_nodes:
|
|
295
|
+
raise ValueError(f"unknown custom node id: {custom_node_id!r}")
|
|
296
|
+
state = WorkflowState(
|
|
297
|
+
id=id,
|
|
298
|
+
custom_node_id=custom_node_id,
|
|
299
|
+
task=task,
|
|
300
|
+
next=next,
|
|
301
|
+
retry_policy=retry_policy,
|
|
302
|
+
interrupt_before=interrupt_before,
|
|
303
|
+
**fields,
|
|
304
|
+
)
|
|
305
|
+
return self._with_state(state)
|
|
306
|
+
|
|
307
|
+
def add_sub_workflow_state(
|
|
308
|
+
self,
|
|
309
|
+
id: str,
|
|
310
|
+
workflow_id: str,
|
|
311
|
+
next: WorkflowNextState,
|
|
312
|
+
*,
|
|
313
|
+
retry_policy: WorkflowRetryPolicy | None = None,
|
|
314
|
+
interrupt_before: bool = False,
|
|
315
|
+
**fields: Any,
|
|
316
|
+
) -> "WorkflowBuilder":
|
|
317
|
+
"""Append a state that runs another, already existing workflow."""
|
|
318
|
+
state = WorkflowState(
|
|
319
|
+
id=id,
|
|
320
|
+
workflow_id=workflow_id,
|
|
321
|
+
next=next,
|
|
322
|
+
retry_policy=retry_policy,
|
|
323
|
+
interrupt_before=interrupt_before,
|
|
324
|
+
**fields,
|
|
325
|
+
)
|
|
326
|
+
return self._with_state(state)
|
|
327
|
+
|
|
328
|
+
def _with_state(self, state: WorkflowState) -> "WorkflowBuilder":
|
|
329
|
+
if any(existing.id == state.id for existing in self._state.states):
|
|
330
|
+
raise ValueError(f"duplicate state id: {state.id!r}")
|
|
331
|
+
return self._replaced(states=self._state.states + (state,))
|
|
332
|
+
|
|
333
|
+
def render(self) -> WorkflowDefinition:
|
|
334
|
+
"""Build the final, immutable :class:`WorkflowDefinition`."""
|
|
335
|
+
return WorkflowDefinition(
|
|
336
|
+
name=self._state.name,
|
|
337
|
+
description=self._state.description,
|
|
338
|
+
mode=self._state.mode,
|
|
339
|
+
shared=self._state.shared,
|
|
340
|
+
icon_url=self._state.icon_url,
|
|
341
|
+
assistants=dict(self._state.assistants),
|
|
342
|
+
tools=list(self._state.tools.values()),
|
|
343
|
+
custom_nodes=list(self._state.custom_nodes.values()),
|
|
344
|
+
states=list(self._state.states),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def publish(
|
|
348
|
+
self,
|
|
349
|
+
client: "CodeMieClient",
|
|
350
|
+
project: str,
|
|
351
|
+
*,
|
|
352
|
+
workflow_id: str | None = None,
|
|
353
|
+
) -> dict:
|
|
354
|
+
"""Render this builder's own state and create or update it on the live platform.
|
|
355
|
+
|
|
356
|
+
Resolves every name-only assistant reference this builder holds
|
|
357
|
+
against the live platform (scoped to `project`), renders the
|
|
358
|
+
`yaml_config`, and dispatches to
|
|
359
|
+
`WorkflowService.create_workflow`/`.update` — both used completely
|
|
360
|
+
unmodified.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
client: An authenticated `CodeMieClient`.
|
|
364
|
+
project: Project the workflow belongs to; also scopes
|
|
365
|
+
assistant-name resolution for any unresolved
|
|
366
|
+
`WorkflowAssistant` reference.
|
|
367
|
+
workflow_id: When given, updates that existing workflow instead
|
|
368
|
+
of creating a new one.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Whatever `WorkflowService.create_workflow`/`.update` returns
|
|
372
|
+
today (currently the raw response `dict`, not a `Workflow`
|
|
373
|
+
model).
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
ValueError: An assistant name this builder holds matches zero or
|
|
377
|
+
more than one assistant in `project`.
|
|
378
|
+
Exception: Whatever `create_workflow`/`.update` raises
|
|
379
|
+
(currently an unwrapped `requests.HTTPError` on backend
|
|
380
|
+
rejection) propagates unchanged — never wrapped, reshaped,
|
|
381
|
+
or swallowed here.
|
|
382
|
+
"""
|
|
383
|
+
return _publish(client, self.render(), project, workflow_id=workflow_id)
|