codemie-sdk-python 0.1.685__py3-none-any.whl → 0.1.687__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.
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)