diffusers-workflow 0.4.0__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.
- diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
- diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
- diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
- diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
- diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
- diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
- dw/__init__.py +440 -0
- dw/adapter_compatibility.py +226 -0
- dw/arguments.py +1231 -0
- dw/assessment_rules.py +159 -0
- dw/assets.py +130 -0
- dw/cache_blocks.json +16 -0
- dw/cache_blocks.py +146 -0
- dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
- dw/content_types.py +150 -0
- dw/dissolve_frame_errors.py +121 -0
- dw/docs/ACCELERATION.md +352 -0
- dw/docs/AGENT_LOOP.md +95 -0
- dw/docs/DEPENDENCIES.md +91 -0
- dw/docs/IP_ADAPTER.md +109 -0
- dw/docs/LORAS.md +131 -0
- dw/docs/MCP.md +517 -0
- dw/docs/PROMPT_WEIGHTING.md +78 -0
- dw/docs/QUANTIZATION.md +230 -0
- dw/docs/RECIPES_24GB.md +201 -0
- dw/docs/RELEASING.md +195 -0
- dw/docs/REMOTE.md +140 -0
- dw/docs/REPL_COMMANDS.md +121 -0
- dw/docs/REPL_WORKER_GUIDE.md +51 -0
- dw/docs/SECURITY.md +272 -0
- dw/docs/SECURITY_QUICKREF.md +112 -0
- dw/docs/SERVER.md +679 -0
- dw/docs/TASKS.md +1741 -0
- dw/docs/TESTING.md +71 -0
- dw/docs/WORKFLOW_GUIDE.md +2038 -0
- dw/docs/WORKSPACES.md +316 -0
- dw/download_watch.py +335 -0
- dw/elision.py +306 -0
- dw/events.py +275 -0
- dw/for_each.py +409 -0
- dw/host_memory.py +258 -0
- dw/host_memory_projection.py +230 -0
- dw/hub_cache.py +432 -0
- dw/introspection.py +1228 -0
- dw/kernel_availability.py +208 -0
- dw/locations.py +599 -0
- dw/log_setup.py +45 -0
- dw/loudness.py +82 -0
- dw/media_audio.py +217 -0
- dw/media_frames.py +367 -0
- dw/media_info.py +297 -0
- dw/pipeline_processors/chain.py +821 -0
- dw/pipeline_processors/config_objects.py +237 -0
- dw/pipeline_processors/pipeline.py +2297 -0
- dw/pipeline_processors/remote.py +46 -0
- dw/plan.py +920 -0
- dw/previous_results.py +411 -0
- dw/probe_paths.py +59 -0
- dw/prompt_schema.json +48 -0
- dw/prompt_weighting.py +378 -0
- dw/prompts.py +159 -0
- dw/realize.py +250 -0
- dw/reference_limits.py +215 -0
- dw/reference_names.py +125 -0
- dw/repl.py +338 -0
- dw/repl_commands.py +836 -0
- dw/repl_worker.py +159 -0
- dw/result.py +1720 -0
- dw/result_fps.py +82 -0
- dw/run.py +162 -0
- dw/runs.py +768 -0
- dw/scalar_result_validation.py +97 -0
- dw/schema.py +283 -0
- dw/security.py +1038 -0
- dw/select_validation.py +115 -0
- dw/serve.py +277 -0
- dw/server/__init__.py +2 -0
- dw/server/app.py +4586 -0
- dw/server/assess.py +132 -0
- dw/server/catalog_shape.py +487 -0
- dw/server/enhancers.py +129 -0
- dw/server/exports.py +480 -0
- dw/server/guides.py +257 -0
- dw/server/jobs.py +1561 -0
- dw/server/mcp_mount.py +95 -0
- dw/server/netinfo.py +124 -0
- dw/server/observed_cost.py +379 -0
- dw/server/sysinfo.py +71 -0
- dw/server/ui/assets/abap-08VXUWAP.js +1 -0
- dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
- dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
- dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
- dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
- dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
- dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
- dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
- dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
- dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
- dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
- dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
- dw/server/ui/assets/css-DIMkf-bt.js +3 -0
- dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
- dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
- dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
- dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
- dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
- dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
- dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
- dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
- dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
- dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
- dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
- dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
- dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
- dw/server/ui/assets/go-C-y9NEjX.js +1 -0
- dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
- dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
- dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
- dw/server/ui/assets/html-PhsdjHSr.js +1 -0
- dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
- dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
- dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
- dw/server/ui/assets/index-DgrYhQd9.js +43 -0
- dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
- dw/server/ui/assets/java-BEtHBSE6.js +1 -0
- dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
- dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
- dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
- dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
- dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
- dw/server/ui/assets/less-B9JPFI3C.js +2 -0
- dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
- dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
- dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
- dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
- dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
- dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
- dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
- dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
- dw/server/ui/assets/monaco--ixms01u.css +1 -0
- dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
- dw/server/ui/assets/msdax-DauUninz.js +1 -0
- dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
- dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
- dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
- dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
- dw/server/ui/assets/perl-oz_6vUea.js +1 -0
- dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
- dw/server/ui/assets/php-nr791fC2.js +1 -0
- dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
- dw/server/ui/assets/postiats-43DmfD33.js +1 -0
- dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
- dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
- dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
- dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
- dw/server/ui/assets/python-Bcn70HdC.js +1 -0
- dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
- dw/server/ui/assets/r-BwWrilGY.js +1 -0
- dw/server/ui/assets/razor-D1HmNnby.js +1 -0
- dw/server/ui/assets/redis-ClamHrr6.js +1 -0
- dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
- dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
- dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
- dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
- dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
- dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
- dw/server/ui/assets/scheme-BeGwcela.js +1 -0
- dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
- dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
- dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
- dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
- dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
- dw/server/ui/assets/sql-NEE52Syq.js +1 -0
- dw/server/ui/assets/st-DbInun42.js +1 -0
- dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
- dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
- dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
- dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
- dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
- dw/server/ui/assets/twig-De2hgUGE.js +1 -0
- dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
- dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
- dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
- dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
- dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
- dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
- dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
- dw/server/ui/index.html +17 -0
- dw/server/updater.py +192 -0
- dw/settings.py +98 -0
- dw/shot_span_preflight.py +116 -0
- dw/shots.py +359 -0
- dw/slice_preflight.py +148 -0
- dw/step.py +187 -0
- dw/step_cache.py +442 -0
- dw/subfolders.py +107 -0
- dw/task_domains.py +307 -0
- dw/tasks/assess.py +826 -0
- dw/tasks/audio_transcription.py +88 -0
- dw/tasks/audio_utils.py +1862 -0
- dw/tasks/background_remover.py +43 -0
- dw/tasks/borders.py +113 -0
- dw/tasks/compose_text.py +74 -0
- dw/tasks/concat_videos.py +300 -0
- dw/tasks/depth_estimator.py +54 -0
- dw/tasks/diffusion_upscale.py +109 -0
- dw/tasks/dissolve_videos.py +342 -0
- dw/tasks/format_messages.py +24 -0
- dw/tasks/gather.py +173 -0
- dw/tasks/grade.py +97 -0
- dw/tasks/image_to_text.py +43 -0
- dw/tasks/image_utils.py +764 -0
- dw/tasks/interpolate_frames.py +252 -0
- dw/tasks/judge.py +68 -0
- dw/tasks/model_cache.py +55 -0
- dw/tasks/pair_audio.py +268 -0
- dw/tasks/qr_code.py +19 -0
- dw/tasks/restore_faces.py +175 -0
- dw/tasks/rife_model.py +192 -0
- dw/tasks/segment.py +121 -0
- dw/tasks/select.py +111 -0
- dw/tasks/speech_generation.py +228 -0
- dw/tasks/stabilize.py +129 -0
- dw/tasks/task.py +920 -0
- dw/tasks/tensor_image.py +57 -0
- dw/tasks/text_generation.py +169 -0
- dw/tasks/text_sections.py +80 -0
- dw/tasks/upscale.py +203 -0
- dw/tasks/video_utils.py +624 -0
- dw/tasks/zoe_depth.py +71 -0
- dw/teacache.py +381 -0
- dw/teacache_models.json +99 -0
- dw/test.py +29 -0
- dw/type_helpers.py +231 -0
- dw/validate.py +68 -0
- dw/variable_constraints.py +444 -0
- dw/variables.py +443 -0
- dw/video_extensions.py +141 -0
- dw/vram_estimate.py +116 -0
- dw/worker.py +764 -0
- dw/workflow.py +2007 -0
- dw/workflow_schema.json +1346 -0
- dw/workflow_sources.py +383 -0
- dw/workflows/h3_context_ir.json +57 -0
- dw/workflows/test.json +31 -0
- dw/workspace.py +730 -0
- dw_mcp/__init__.py +6 -0
- dw_mcp/__main__.py +133 -0
- dw_mcp/assets.py +336 -0
- dw_mcp/authoring.py +114 -0
- dw_mcp/catalog.py +360 -0
- dw_mcp/client.py +486 -0
- dw_mcp/diagnose.py +371 -0
- dw_mcp/exports.py +84 -0
- dw_mcp/guides.py +35 -0
- dw_mcp/media.py +638 -0
- dw_mcp/models.py +97 -0
- dw_mcp/prompts.py +104 -0
- dw_mcp/server.py +1343 -0
- dw_mcp/workspaces.py +212 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""A `result` block on a step whose command returns a scalar, not an
|
|
2
|
+
artifact (#212).
|
|
3
|
+
|
|
4
|
+
`judge` returns a bare `float` - a natural fit for `result` to look like it
|
|
5
|
+
applies (the guide's Result Configuration section describes saving text,
|
|
6
|
+
and a score reads like one), but there is no file to write. `validate_workflow`
|
|
7
|
+
answered `valid: true` and the run generated its full fan-out before dying
|
|
8
|
+
deep inside `save_artifact` on `write() argument must be str, not float`,
|
|
9
|
+
naming neither the step nor the command. Checked here against the command's
|
|
10
|
+
own declared `returns` kind (`dw/tasks/task.py`'s `register_command`) rather
|
|
11
|
+
than a name match on `judge`, so a future scalar-returning task is covered
|
|
12
|
+
by declaring itself rather than by a second special case here.
|
|
13
|
+
|
|
14
|
+
A "json" command (the assessment probes, #387) answers a dict of
|
|
15
|
+
measurements, which `Result.save` writes whole only under
|
|
16
|
+
`application/json`; any other content type explodes the dict key by key
|
|
17
|
+
into files, or dies on a number. So a `result` on one is allowed and must
|
|
18
|
+
say `application/json`.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .for_each import MEMBER_SEPARATOR, render_path
|
|
22
|
+
from .tasks.task import task_command_info
|
|
23
|
+
|
|
24
|
+
RESULT_KEY = "result"
|
|
25
|
+
JSON_CONTENT_TYPE = "application/json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def scalar_result_errors(workflow_definition, source_indices=None):
|
|
29
|
+
"""Every `result` block a command's `returns` kind cannot save, as
|
|
30
|
+
[{path, message}].
|
|
31
|
+
|
|
32
|
+
The definition handed here has already been substituted and expanded,
|
|
33
|
+
so a step's `task.command` is literal. `source_indices`, when given, is
|
|
34
|
+
the source step index of each step - a `for_each` group turns one
|
|
35
|
+
written step into several, and the path an error carries has to be one
|
|
36
|
+
the author can find in the file they wrote; the member is named in the
|
|
37
|
+
message.
|
|
38
|
+
"""
|
|
39
|
+
steps = workflow_definition.get("steps")
|
|
40
|
+
if not isinstance(steps, list):
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
errors = []
|
|
44
|
+
for index, step in enumerate(steps):
|
|
45
|
+
if not isinstance(step, dict):
|
|
46
|
+
continue
|
|
47
|
+
result = step.get(RESULT_KEY)
|
|
48
|
+
if not isinstance(result, dict):
|
|
49
|
+
continue
|
|
50
|
+
task = step.get("task")
|
|
51
|
+
if not isinstance(task, dict):
|
|
52
|
+
continue
|
|
53
|
+
command = task.get("command")
|
|
54
|
+
if not isinstance(command, str):
|
|
55
|
+
continue
|
|
56
|
+
try:
|
|
57
|
+
info = task_command_info(command)
|
|
58
|
+
except ValueError:
|
|
59
|
+
continue
|
|
60
|
+
returns = info.get("returns")
|
|
61
|
+
if returns == "json":
|
|
62
|
+
if result.get("content_type") == JSON_CONTENT_TYPE:
|
|
63
|
+
continue
|
|
64
|
+
message = (
|
|
65
|
+
f"{command} answers a JSON document - 'result' must set "
|
|
66
|
+
f"content_type '{JSON_CONTENT_TYPE}', not "
|
|
67
|
+
f"{result.get('content_type')!r}"
|
|
68
|
+
)
|
|
69
|
+
elif returns == "scalar":
|
|
70
|
+
message = (
|
|
71
|
+
f"{command} returns a number, not an artifact - "
|
|
72
|
+
f"'result' cannot be saved"
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
source = (
|
|
78
|
+
source_indices[index]
|
|
79
|
+
if source_indices is not None and index < len(source_indices)
|
|
80
|
+
else index
|
|
81
|
+
)
|
|
82
|
+
name = step.get("name")
|
|
83
|
+
where = (
|
|
84
|
+
f" in member '{name}'"
|
|
85
|
+
if isinstance(name, str) and MEMBER_SEPARATOR in name
|
|
86
|
+
else ""
|
|
87
|
+
)
|
|
88
|
+
errors.append(
|
|
89
|
+
{
|
|
90
|
+
"path": render_path(("steps", source, RESULT_KEY)),
|
|
91
|
+
"message": f"{message}{where}",
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
return errors
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = ["scalar_result_errors"]
|
dw/schema.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from jsonschema import validate, ValidationError
|
|
4
|
+
from jsonschema.exceptions import best_match
|
|
5
|
+
from jsonschema.validators import validator_for
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def validate_data(data, schema):
|
|
9
|
+
try:
|
|
10
|
+
validate(instance=data, schema=schema)
|
|
11
|
+
return True, "Validation successful"
|
|
12
|
+
|
|
13
|
+
except ValidationError as ve:
|
|
14
|
+
path = json_path(ve.absolute_path)
|
|
15
|
+
location = f" at {path}" if path else ""
|
|
16
|
+
return False, f"Validation error{location}: {error_message(ve)}"
|
|
17
|
+
except json.JSONDecodeError as je:
|
|
18
|
+
return False, f"JSON parsing error: {str(je)}"
|
|
19
|
+
except Exception as e:
|
|
20
|
+
return False, f"Unexpected error: {str(e)}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# anyOf branches produce dozens of near-identical entries; this is more than
|
|
24
|
+
# an agent fixes in one pass, and enough that nothing real is hidden
|
|
25
|
+
MAX_VALIDATION_ERRORS = 25
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate_data_all(data, schema):
|
|
29
|
+
"""Every schema violation in `data`, as [{path, message}].
|
|
30
|
+
|
|
31
|
+
Sorted by path, deduplicated on (path, message), capped at
|
|
32
|
+
MAX_VALIDATION_ERRORS. Each top-level error is reduced with best_match
|
|
33
|
+
- the same descent into anyOf branches jsonschema.validate performs to
|
|
34
|
+
pick the one exception it raises - so a definition with a single
|
|
35
|
+
violation is reported exactly as validate_data reports it.
|
|
36
|
+
"""
|
|
37
|
+
validator = validator_for(schema)(schema)
|
|
38
|
+
seen = {}
|
|
39
|
+
for error in validator.iter_errors(data):
|
|
40
|
+
chosen = best_match([error])
|
|
41
|
+
key = (json_path(chosen.absolute_path), error_message(chosen))
|
|
42
|
+
seen.setdefault(key, None)
|
|
43
|
+
ordered = sorted(
|
|
44
|
+
_only_unknown_property(seen), key=lambda key: (key[0] or "", key[1])
|
|
45
|
+
)
|
|
46
|
+
return [
|
|
47
|
+
{"path": path, "message": message}
|
|
48
|
+
for path, message in ordered[:MAX_VALIDATION_ERRORS]
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _only_unknown_property(keys):
|
|
53
|
+
"""Drop the shape complaints about a key that is simply unknown.
|
|
54
|
+
|
|
55
|
+
A key refused by a typed `additionalProperties` fails that shape's own
|
|
56
|
+
'type' and 'required' checks too, so one mistyped 'trasformer' arrives
|
|
57
|
+
as three errors, two of which describe the component definition the
|
|
58
|
+
author never meant to write. Where a path has the unknown-property
|
|
59
|
+
message, it is the whole story (#123).
|
|
60
|
+
"""
|
|
61
|
+
unknown = {
|
|
62
|
+
path for path, message in keys if message.startswith('unknown property "')
|
|
63
|
+
}
|
|
64
|
+
return [
|
|
65
|
+
(path, message)
|
|
66
|
+
for path, message in keys
|
|
67
|
+
if path not in unknown or message.startswith('unknown property "')
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def error_message(error):
|
|
72
|
+
"""The message an agent can act on for one schema violation.
|
|
73
|
+
|
|
74
|
+
Only `additionalProperties` is rewritten. jsonschema says "Additional
|
|
75
|
+
properties are not allowed ('when' was unexpected)", which does not say
|
|
76
|
+
what the object *does* take - and the whole point of closing these
|
|
77
|
+
objects is that an invented key ('when', 'retry') or a mistyped real one
|
|
78
|
+
('relase_pipeline') is a silent no-op the engine never reads (#118). So
|
|
79
|
+
the message names the object, the key, and the full set that is read.
|
|
80
|
+
"""
|
|
81
|
+
if error.validator != "additionalProperties":
|
|
82
|
+
# An object that is closed except for one shape of key - the pipeline,
|
|
83
|
+
# where any other key may name a component - refuses an unknown key
|
|
84
|
+
# through that shape's own 'type'/'required' rather than through
|
|
85
|
+
# additionalProperties, and jsonschema then reports the shape rather
|
|
86
|
+
# than the key. The schema carries the sentence to say instead (#123)
|
|
87
|
+
explanation = (error.schema or {}).get("unknownPropertyMessage")
|
|
88
|
+
if explanation and error.absolute_path:
|
|
89
|
+
return f'unknown property "{error.absolute_path[-1]}" - {explanation}'
|
|
90
|
+
return error.message
|
|
91
|
+
|
|
92
|
+
properties = (error.schema or {}).get("properties") or {}
|
|
93
|
+
# An engine-injected key is legal but is not something an author writes,
|
|
94
|
+
# so listing it as one of the properties on offer only invites its use
|
|
95
|
+
allowed = sorted(
|
|
96
|
+
name
|
|
97
|
+
for name, subschema in properties.items()
|
|
98
|
+
if not str((subschema or {}).get("description", "")).startswith(
|
|
99
|
+
"Engine-injected"
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
instance = error.instance if isinstance(error.instance, dict) else {}
|
|
103
|
+
unknown = sorted(key for key in instance if key not in allowed)
|
|
104
|
+
name = instance.get("name")
|
|
105
|
+
named = f'"{name}": ' if isinstance(name, str) and name else ""
|
|
106
|
+
keys = ", ".join(f'"{key}"' for key in unknown) or "a property"
|
|
107
|
+
plural = "properties" if len(unknown) > 1 else "property"
|
|
108
|
+
return (
|
|
109
|
+
f"{named}unknown {plural} {keys} - the engine reads only "
|
|
110
|
+
f"{', '.join(allowed)}, so anything else would be silently ignored"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def format_validation_errors(errors):
|
|
115
|
+
"""The message a raised validation failure carries.
|
|
116
|
+
|
|
117
|
+
One error keeps the line every caller already shows -
|
|
118
|
+
'Validation error at <path>: <message>'. Several are listed one per
|
|
119
|
+
line under a single heading, so the text 'Validation error' still
|
|
120
|
+
appears once per failure (the CLI and the REPL count on that).
|
|
121
|
+
"""
|
|
122
|
+
if len(errors) == 1:
|
|
123
|
+
path, message = errors[0]["path"], errors[0]["message"]
|
|
124
|
+
location = f" at {path}" if path else ""
|
|
125
|
+
return f"Validation error{location}: {message}"
|
|
126
|
+
count = (
|
|
127
|
+
f"first {MAX_VALIDATION_ERRORS}"
|
|
128
|
+
if len(errors) >= MAX_VALIDATION_ERRORS
|
|
129
|
+
else str(len(errors))
|
|
130
|
+
)
|
|
131
|
+
lines = [f"Validation errors ({count}):"]
|
|
132
|
+
for error in errors:
|
|
133
|
+
lines.append(f" at {error['path'] or 'root'}: {error['message']}")
|
|
134
|
+
return "\n".join(lines)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def json_path(absolute_path):
|
|
138
|
+
"""Render a jsonschema ValidationError's absolute_path (a deque of dict
|
|
139
|
+
keys and list indices) as a dotted/bracket path, e.g.
|
|
140
|
+
steps[0].pipeline.arguments.prompt. None if the error is at the root."""
|
|
141
|
+
if not absolute_path:
|
|
142
|
+
return None
|
|
143
|
+
parts = []
|
|
144
|
+
for element in absolute_path:
|
|
145
|
+
if isinstance(element, int):
|
|
146
|
+
parts.append(f"[{element}]")
|
|
147
|
+
elif not parts:
|
|
148
|
+
parts.append(str(element))
|
|
149
|
+
else:
|
|
150
|
+
parts.append(f".{element}")
|
|
151
|
+
return "".join(parts)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def load_schema(schema_name):
|
|
155
|
+
file_spec = os.path.join(
|
|
156
|
+
os.path.dirname(os.path.abspath(__file__)), f"{schema_name}_schema.json"
|
|
157
|
+
)
|
|
158
|
+
with open(file_spec, "r") as file:
|
|
159
|
+
return json.load(file)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# The parts of the workflow schema that can be asked for on their own, and
|
|
163
|
+
# what each holds. Whole, the schema is ~36 KB - ~8.6k tokens in one call,
|
|
164
|
+
# spent by an agent that needed the shape of a `result` block (#101). A
|
|
165
|
+
# section answers that question at a tenth the size; the no-argument call
|
|
166
|
+
# is unchanged, so nothing that reads the whole schema is affected.
|
|
167
|
+
SCHEMA_SECTIONS = {
|
|
168
|
+
"variables": {
|
|
169
|
+
"properties": [
|
|
170
|
+
"id",
|
|
171
|
+
"description",
|
|
172
|
+
"summary",
|
|
173
|
+
"shape",
|
|
174
|
+
"traits",
|
|
175
|
+
"configures",
|
|
176
|
+
"cost",
|
|
177
|
+
"cost_drivers",
|
|
178
|
+
"variables",
|
|
179
|
+
"variable_constraints",
|
|
180
|
+
"seed",
|
|
181
|
+
],
|
|
182
|
+
"defs": ["variable_constraint"],
|
|
183
|
+
},
|
|
184
|
+
"steps": {"properties": ["steps"], "defs": ["step", "workflow_reference"]},
|
|
185
|
+
"pipelines": {
|
|
186
|
+
"properties": [],
|
|
187
|
+
"defs": ["pipeline", "pipeline_reference", "chain", "arguments", "image"],
|
|
188
|
+
},
|
|
189
|
+
"tasks": {"properties": [], "defs": ["task"]},
|
|
190
|
+
"result": {"properties": [], "defs": ["result"]},
|
|
191
|
+
"configuration": {
|
|
192
|
+
"properties": [],
|
|
193
|
+
"defs": [
|
|
194
|
+
"pipeline_configuration",
|
|
195
|
+
"pipeline_component",
|
|
196
|
+
"shared_components",
|
|
197
|
+
"reused_components",
|
|
198
|
+
"quantization_config",
|
|
199
|
+
"scheduler",
|
|
200
|
+
"lora",
|
|
201
|
+
"controlnet",
|
|
202
|
+
"ip_adapter",
|
|
203
|
+
"from_pretrained_arguments",
|
|
204
|
+
"group_offload",
|
|
205
|
+
"compile_config",
|
|
206
|
+
"enable_layerwise_casting",
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
DEFS_KEY = "$defs"
|
|
212
|
+
DEFS_REF_PREFIX = f"#/{DEFS_KEY}/"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class SchemaSectionError(LookupError):
|
|
216
|
+
"""A section name the schema has no part for. The message names the
|
|
217
|
+
ones it does, so a route can hand it back as a 404 detail."""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _referenced_defs(node, found):
|
|
221
|
+
"""Every '#/$defs/x' name reachable from a subtree, into `found`."""
|
|
222
|
+
if isinstance(node, dict):
|
|
223
|
+
ref = node.get("$ref")
|
|
224
|
+
if isinstance(ref, str) and ref.startswith(DEFS_REF_PREFIX):
|
|
225
|
+
found.add(ref[len(DEFS_REF_PREFIX) :])
|
|
226
|
+
for value in node.values():
|
|
227
|
+
_referenced_defs(value, found)
|
|
228
|
+
elif isinstance(node, list):
|
|
229
|
+
for value in node:
|
|
230
|
+
_referenced_defs(value, found)
|
|
231
|
+
return found
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def schema_section(schema, section):
|
|
235
|
+
"""One part of the workflow schema, plus where the rest of it is.
|
|
236
|
+
|
|
237
|
+
The fragment carries only the definitions this section owns; a `$ref`
|
|
238
|
+
to a definition another section owns is left standing and named in
|
|
239
|
+
`elsewhere` ({definition: section}), because expanding it transitively
|
|
240
|
+
would pull most of the schema back in and there would be no section
|
|
241
|
+
left to speak of. `sections` lists every section name.
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
SchemaSectionError: If `section` is not one of SCHEMA_SECTIONS.
|
|
245
|
+
"""
|
|
246
|
+
if section not in SCHEMA_SECTIONS:
|
|
247
|
+
raise SchemaSectionError(
|
|
248
|
+
f"No schema section '{section}'. The sections are: "
|
|
249
|
+
f"{', '.join(sorted(SCHEMA_SECTIONS))}."
|
|
250
|
+
)
|
|
251
|
+
wanted = SCHEMA_SECTIONS[section]
|
|
252
|
+
defs = schema.get(DEFS_KEY, {})
|
|
253
|
+
fragment = {
|
|
254
|
+
key: schema[key]
|
|
255
|
+
for key in ("$schema", "$id", "type", "description")
|
|
256
|
+
if key in schema
|
|
257
|
+
}
|
|
258
|
+
properties = {
|
|
259
|
+
name: schema["properties"][name]
|
|
260
|
+
for name in wanted["properties"]
|
|
261
|
+
if name in schema.get("properties", {})
|
|
262
|
+
}
|
|
263
|
+
if properties:
|
|
264
|
+
fragment["properties"] = properties
|
|
265
|
+
required = [name for name in schema.get("required", []) if name in properties]
|
|
266
|
+
if required:
|
|
267
|
+
fragment["required"] = required
|
|
268
|
+
held = {name: defs[name] for name in wanted["defs"] if name in defs}
|
|
269
|
+
if held:
|
|
270
|
+
fragment[DEFS_KEY] = held
|
|
271
|
+
owner = {
|
|
272
|
+
name: other for other, part in SCHEMA_SECTIONS.items() for name in part["defs"]
|
|
273
|
+
}
|
|
274
|
+
referenced = _referenced_defs(fragment, set())
|
|
275
|
+
elsewhere = {
|
|
276
|
+
name: owner[name] for name in sorted(referenced - set(held)) if name in owner
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
"section": section,
|
|
280
|
+
"sections": sorted(SCHEMA_SECTIONS),
|
|
281
|
+
"elsewhere": elsewhere,
|
|
282
|
+
"schema": fragment,
|
|
283
|
+
}
|