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.
Files changed (260) hide show
  1. diffusers_workflow-0.4.0.dist-info/METADATA +318 -0
  2. diffusers_workflow-0.4.0.dist-info/RECORD +260 -0
  3. diffusers_workflow-0.4.0.dist-info/WHEEL +5 -0
  4. diffusers_workflow-0.4.0.dist-info/entry_points.txt +7 -0
  5. diffusers_workflow-0.4.0.dist-info/licenses/LICENSE +201 -0
  6. diffusers_workflow-0.4.0.dist-info/top_level.txt +2 -0
  7. dw/__init__.py +440 -0
  8. dw/adapter_compatibility.py +226 -0
  9. dw/arguments.py +1231 -0
  10. dw/assessment_rules.py +159 -0
  11. dw/assets.py +130 -0
  12. dw/cache_blocks.json +16 -0
  13. dw/cache_blocks.py +146 -0
  14. dw/community_pipelines/pipeline_flux_rf_inversion.py +1184 -0
  15. dw/content_types.py +150 -0
  16. dw/dissolve_frame_errors.py +121 -0
  17. dw/docs/ACCELERATION.md +352 -0
  18. dw/docs/AGENT_LOOP.md +95 -0
  19. dw/docs/DEPENDENCIES.md +91 -0
  20. dw/docs/IP_ADAPTER.md +109 -0
  21. dw/docs/LORAS.md +131 -0
  22. dw/docs/MCP.md +517 -0
  23. dw/docs/PROMPT_WEIGHTING.md +78 -0
  24. dw/docs/QUANTIZATION.md +230 -0
  25. dw/docs/RECIPES_24GB.md +201 -0
  26. dw/docs/RELEASING.md +195 -0
  27. dw/docs/REMOTE.md +140 -0
  28. dw/docs/REPL_COMMANDS.md +121 -0
  29. dw/docs/REPL_WORKER_GUIDE.md +51 -0
  30. dw/docs/SECURITY.md +272 -0
  31. dw/docs/SECURITY_QUICKREF.md +112 -0
  32. dw/docs/SERVER.md +679 -0
  33. dw/docs/TASKS.md +1741 -0
  34. dw/docs/TESTING.md +71 -0
  35. dw/docs/WORKFLOW_GUIDE.md +2038 -0
  36. dw/docs/WORKSPACES.md +316 -0
  37. dw/download_watch.py +335 -0
  38. dw/elision.py +306 -0
  39. dw/events.py +275 -0
  40. dw/for_each.py +409 -0
  41. dw/host_memory.py +258 -0
  42. dw/host_memory_projection.py +230 -0
  43. dw/hub_cache.py +432 -0
  44. dw/introspection.py +1228 -0
  45. dw/kernel_availability.py +208 -0
  46. dw/locations.py +599 -0
  47. dw/log_setup.py +45 -0
  48. dw/loudness.py +82 -0
  49. dw/media_audio.py +217 -0
  50. dw/media_frames.py +367 -0
  51. dw/media_info.py +297 -0
  52. dw/pipeline_processors/chain.py +821 -0
  53. dw/pipeline_processors/config_objects.py +237 -0
  54. dw/pipeline_processors/pipeline.py +2297 -0
  55. dw/pipeline_processors/remote.py +46 -0
  56. dw/plan.py +920 -0
  57. dw/previous_results.py +411 -0
  58. dw/probe_paths.py +59 -0
  59. dw/prompt_schema.json +48 -0
  60. dw/prompt_weighting.py +378 -0
  61. dw/prompts.py +159 -0
  62. dw/realize.py +250 -0
  63. dw/reference_limits.py +215 -0
  64. dw/reference_names.py +125 -0
  65. dw/repl.py +338 -0
  66. dw/repl_commands.py +836 -0
  67. dw/repl_worker.py +159 -0
  68. dw/result.py +1720 -0
  69. dw/result_fps.py +82 -0
  70. dw/run.py +162 -0
  71. dw/runs.py +768 -0
  72. dw/scalar_result_validation.py +97 -0
  73. dw/schema.py +283 -0
  74. dw/security.py +1038 -0
  75. dw/select_validation.py +115 -0
  76. dw/serve.py +277 -0
  77. dw/server/__init__.py +2 -0
  78. dw/server/app.py +4586 -0
  79. dw/server/assess.py +132 -0
  80. dw/server/catalog_shape.py +487 -0
  81. dw/server/enhancers.py +129 -0
  82. dw/server/exports.py +480 -0
  83. dw/server/guides.py +257 -0
  84. dw/server/jobs.py +1561 -0
  85. dw/server/mcp_mount.py +95 -0
  86. dw/server/netinfo.py +124 -0
  87. dw/server/observed_cost.py +379 -0
  88. dw/server/sysinfo.py +71 -0
  89. dw/server/ui/assets/abap-08VXUWAP.js +1 -0
  90. dw/server/ui/assets/apex-BWPQTe0t.js +1 -0
  91. dw/server/ui/assets/azcli-Bc_sGQ0U.js +1 -0
  92. dw/server/ui/assets/bat-i0X4ZdIN.js +1 -0
  93. dw/server/ui/assets/bicep-B5-_aFwp.js +2 -0
  94. dw/server/ui/assets/cameligo-DMUM7wLl.js +1 -0
  95. dw/server/ui/assets/clojure-Cm7r79vr.js +1 -0
  96. dw/server/ui/assets/codicon-Brq4_Ui5.ttf +0 -0
  97. dw/server/ui/assets/coffee-Ba7i2nA0.js +1 -0
  98. dw/server/ui/assets/cpp-C7h46wYY.js +1 -0
  99. dw/server/ui/assets/csharp-BKxtCVv1.js +1 -0
  100. dw/server/ui/assets/csp-bTuwJoIa.js +1 -0
  101. dw/server/ui/assets/css-DIMkf-bt.js +3 -0
  102. dw/server/ui/assets/css.worker-B3ciXF_0.js +93 -0
  103. dw/server/ui/assets/cssMode-CPznxfY8.js +1 -0
  104. dw/server/ui/assets/cypher-CVaqCwHa.js +1 -0
  105. dw/server/ui/assets/dart-onAF5SnQ.js +1 -0
  106. dw/server/ui/assets/dockerfile-DZFCIeNp.js +1 -0
  107. dw/server/ui/assets/ecl-D05T4iGw.js +1 -0
  108. dw/server/ui/assets/editor-jjEx9u7D.css +1 -0
  109. dw/server/ui/assets/editor.api-CpWcotrd.js +847 -0
  110. dw/server/ui/assets/editor.worker-q-txB4vs.js +30 -0
  111. dw/server/ui/assets/elixir-6RTg0lbw.js +1 -0
  112. dw/server/ui/assets/flow9-C5_-GSwl.js +1 -0
  113. dw/server/ui/assets/freemarker2-CXtRM8N4.js +3 -0
  114. dw/server/ui/assets/fsharp-C8Ef5oNN.js +1 -0
  115. dw/server/ui/assets/go-C-y9NEjX.js +1 -0
  116. dw/server/ui/assets/graphql-fmXr3nnJ.js +1 -0
  117. dw/server/ui/assets/handlebars-N7x-6NMY.js +1 -0
  118. dw/server/ui/assets/hcl-CpzslTdj.js +1 -0
  119. dw/server/ui/assets/html-PhsdjHSr.js +1 -0
  120. dw/server/ui/assets/html.worker-C93Ht9o9.js +506 -0
  121. dw/server/ui/assets/htmlMode-Dgj0SEok.js +1 -0
  122. dw/server/ui/assets/index-3Vw6WAPW.css +1 -0
  123. dw/server/ui/assets/index-DgrYhQd9.js +43 -0
  124. dw/server/ui/assets/ini-sBoK_t0W.js +1 -0
  125. dw/server/ui/assets/java-BEtHBSE6.js +1 -0
  126. dw/server/ui/assets/javascript-BJqN9Qhv.js +1 -0
  127. dw/server/ui/assets/json.worker-B2V3pomh.js +62 -0
  128. dw/server/ui/assets/jsonMode-DbM4SWSv.js +7 -0
  129. dw/server/ui/assets/julia-Bri6UV-V.js +1 -0
  130. dw/server/ui/assets/kotlin-BOotOW0E.js +1 -0
  131. dw/server/ui/assets/less-B9JPFI3C.js +2 -0
  132. dw/server/ui/assets/lexon-CfSJPG6W.js +1 -0
  133. dw/server/ui/assets/liquid-BWr8lEc4.js +1 -0
  134. dw/server/ui/assets/lspLanguageFeatures-C1iGuDyZ.js +4 -0
  135. dw/server/ui/assets/lua-CsQS60Ue.js +1 -0
  136. dw/server/ui/assets/m3-D-oSqn_W.js +1 -0
  137. dw/server/ui/assets/markdown-Cimd5fb3.js +1 -0
  138. dw/server/ui/assets/mdx-DAdMi_0p.js +1 -0
  139. dw/server/ui/assets/mips-CIPQ_RoX.js +1 -0
  140. dw/server/ui/assets/monaco--ixms01u.css +1 -0
  141. dw/server/ui/assets/monaco-BGCeEqaw.js +56 -0
  142. dw/server/ui/assets/msdax-DauUninz.js +1 -0
  143. dw/server/ui/assets/mysql-SOo6toE5.js +1 -0
  144. dw/server/ui/assets/objective-c-FvmIjYaQ.js +1 -0
  145. dw/server/ui/assets/pascal-DrH0SRf2.js +1 -0
  146. dw/server/ui/assets/pascaligo-D-ptJ9y-.js +1 -0
  147. dw/server/ui/assets/perl-oz_6vUea.js +1 -0
  148. dw/server/ui/assets/pgsql-DTj74zXo.js +1 -0
  149. dw/server/ui/assets/php-nr791fC2.js +1 -0
  150. dw/server/ui/assets/pla-CopQ2nXW.js +1 -0
  151. dw/server/ui/assets/postiats-43DmfD33.js +1 -0
  152. dw/server/ui/assets/powerquery-D3hlyOfw.js +1 -0
  153. dw/server/ui/assets/powershell-DmHpPYUd.js +1 -0
  154. dw/server/ui/assets/protobuf-C531GsRP.js +2 -0
  155. dw/server/ui/assets/pug-Z5eAx3Zn.js +1 -0
  156. dw/server/ui/assets/python-Bcn70HdC.js +1 -0
  157. dw/server/ui/assets/qsharp-DkqhCAOL.js +1 -0
  158. dw/server/ui/assets/r-BwWrilGY.js +1 -0
  159. dw/server/ui/assets/razor-D1HmNnby.js +1 -0
  160. dw/server/ui/assets/redis-ClamHrr6.js +1 -0
  161. dw/server/ui/assets/redshift-DT7zqm-g.js +1 -0
  162. dw/server/ui/assets/restructuredtext-BYgofb2h.js +1 -0
  163. dw/server/ui/assets/ruby-DezsRK8O.js +1 -0
  164. dw/server/ui/assets/rust-DdL9SqIa.js +1 -0
  165. dw/server/ui/assets/sb-CcwsVR0C.js +1 -0
  166. dw/server/ui/assets/scala-DHpiXF5c.js +1 -0
  167. dw/server/ui/assets/scheme-BeGwcela.js +1 -0
  168. dw/server/ui/assets/scss-gp-XZpBa.js +3 -0
  169. dw/server/ui/assets/shell-CC2rA5mh.js +1 -0
  170. dw/server/ui/assets/solidity-BEEn4gHE.js +1 -0
  171. dw/server/ui/assets/sophia-CRfGWb83.js +1 -0
  172. dw/server/ui/assets/sparql-D_Lu-MrJ.js +1 -0
  173. dw/server/ui/assets/sql-NEE52Syq.js +1 -0
  174. dw/server/ui/assets/st-DbInun42.js +1 -0
  175. dw/server/ui/assets/swift-Bxkupp3x.js +1 -0
  176. dw/server/ui/assets/systemverilog-Bz4Y3fRF.js +1 -0
  177. dw/server/ui/assets/tcl-DISqw1ZD.js +1 -0
  178. dw/server/ui/assets/ts.worker-D7T1-Ig5.js +67738 -0
  179. dw/server/ui/assets/tsMode-D6u0XmOW.js +11 -0
  180. dw/server/ui/assets/twig-De2hgUGE.js +1 -0
  181. dw/server/ui/assets/typescript-BU6v-LMV.js +1 -0
  182. dw/server/ui/assets/typespec-B8J7ngcE.js +1 -0
  183. dw/server/ui/assets/vb-DV3o63ZY.js +1 -0
  184. dw/server/ui/assets/wgsl-DpFanUEy.js +298 -0
  185. dw/server/ui/assets/workers-Cn7cTUKr.js +1 -0
  186. dw/server/ui/assets/xml--0LP2Lwk.js +1 -0
  187. dw/server/ui/assets/yaml-mpBg9jnt.js +1 -0
  188. dw/server/ui/index.html +17 -0
  189. dw/server/updater.py +192 -0
  190. dw/settings.py +98 -0
  191. dw/shot_span_preflight.py +116 -0
  192. dw/shots.py +359 -0
  193. dw/slice_preflight.py +148 -0
  194. dw/step.py +187 -0
  195. dw/step_cache.py +442 -0
  196. dw/subfolders.py +107 -0
  197. dw/task_domains.py +307 -0
  198. dw/tasks/assess.py +826 -0
  199. dw/tasks/audio_transcription.py +88 -0
  200. dw/tasks/audio_utils.py +1862 -0
  201. dw/tasks/background_remover.py +43 -0
  202. dw/tasks/borders.py +113 -0
  203. dw/tasks/compose_text.py +74 -0
  204. dw/tasks/concat_videos.py +300 -0
  205. dw/tasks/depth_estimator.py +54 -0
  206. dw/tasks/diffusion_upscale.py +109 -0
  207. dw/tasks/dissolve_videos.py +342 -0
  208. dw/tasks/format_messages.py +24 -0
  209. dw/tasks/gather.py +173 -0
  210. dw/tasks/grade.py +97 -0
  211. dw/tasks/image_to_text.py +43 -0
  212. dw/tasks/image_utils.py +764 -0
  213. dw/tasks/interpolate_frames.py +252 -0
  214. dw/tasks/judge.py +68 -0
  215. dw/tasks/model_cache.py +55 -0
  216. dw/tasks/pair_audio.py +268 -0
  217. dw/tasks/qr_code.py +19 -0
  218. dw/tasks/restore_faces.py +175 -0
  219. dw/tasks/rife_model.py +192 -0
  220. dw/tasks/segment.py +121 -0
  221. dw/tasks/select.py +111 -0
  222. dw/tasks/speech_generation.py +228 -0
  223. dw/tasks/stabilize.py +129 -0
  224. dw/tasks/task.py +920 -0
  225. dw/tasks/tensor_image.py +57 -0
  226. dw/tasks/text_generation.py +169 -0
  227. dw/tasks/text_sections.py +80 -0
  228. dw/tasks/upscale.py +203 -0
  229. dw/tasks/video_utils.py +624 -0
  230. dw/tasks/zoe_depth.py +71 -0
  231. dw/teacache.py +381 -0
  232. dw/teacache_models.json +99 -0
  233. dw/test.py +29 -0
  234. dw/type_helpers.py +231 -0
  235. dw/validate.py +68 -0
  236. dw/variable_constraints.py +444 -0
  237. dw/variables.py +443 -0
  238. dw/video_extensions.py +141 -0
  239. dw/vram_estimate.py +116 -0
  240. dw/worker.py +764 -0
  241. dw/workflow.py +2007 -0
  242. dw/workflow_schema.json +1346 -0
  243. dw/workflow_sources.py +383 -0
  244. dw/workflows/h3_context_ir.json +57 -0
  245. dw/workflows/test.json +31 -0
  246. dw/workspace.py +730 -0
  247. dw_mcp/__init__.py +6 -0
  248. dw_mcp/__main__.py +133 -0
  249. dw_mcp/assets.py +336 -0
  250. dw_mcp/authoring.py +114 -0
  251. dw_mcp/catalog.py +360 -0
  252. dw_mcp/client.py +486 -0
  253. dw_mcp/diagnose.py +371 -0
  254. dw_mcp/exports.py +84 -0
  255. dw_mcp/guides.py +35 -0
  256. dw_mcp/media.py +638 -0
  257. dw_mcp/models.py +97 -0
  258. dw_mcp/prompts.py +104 -0
  259. dw_mcp/server.py +1343 -0
  260. 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
+ }