alright uhh

This commit is contained in:
lda
2026-05-18 01:40:50 +07:00 Verified
parent 9e7f91a223
commit d29688ec26
10 changed files with 241 additions and 5 deletions
+3
View File
@@ -17,6 +17,7 @@ from .models import (
)
from .store import FileWorkflowArtifactStore, WorkflowArtifactStore
from .validation import validate_deployment_dependencies
from .references import logical_ref_for_concrete_ref, normalize_plan_node_refs
__all__ = [
"AvailableCapability",
@@ -34,5 +35,7 @@ __all__ = [
"artifact_catalog_entry",
"artifact_node_name",
"create_workflow_artifact_from_plan",
"logical_ref_for_concrete_ref",
"normalize_plan_node_refs",
"validate_deployment_dependencies",
]
+12 -5
View File
@@ -5,6 +5,7 @@ from collections.abc import Mapping
from wf_core import ReducerRef, Workflow
from .models import ArtifactKind, JsonObject, RequiredCapability, WorkflowArtifact
from .references import normalize_plan_node_refs
def create_workflow_artifact_from_plan(
@@ -17,22 +18,28 @@ def create_workflow_artifact_from_plan(
kind: ArtifactKind = "workflow",
description: str | None = None,
required_capabilities: Mapping[str, RequiredCapability] | None = None,
source_bindings: Mapping[str, str] | None = None,
created_from_catalog_version: str | None = None,
) -> WorkflowArtifact:
"""Create an immutable artifact from a declarative workflow plan."""
_validate_workflow_plan(plan)
normalized_plan, node_requirements = normalize_plan_node_refs(
plan,
source_bindings or {},
)
_validate_workflow_plan(normalized_plan)
return WorkflowArtifact(
id=artifact_id,
version=version,
title=title,
kind=kind,
description=description,
input_schema=_required_object_field(plan, "input_schema"),
output_schema=_required_object_field(plan, "output_schema"),
input_schema=_required_object_field(normalized_plan, "input_schema"),
output_schema=_required_object_field(normalized_plan, "output_schema"),
outcomes=outcomes,
plan=plan,
plan=normalized_plan,
required_capabilities={
**_required_reducers_from_plan(plan),
**_required_reducers_from_plan(normalized_plan),
**node_requirements,
**dict(required_capabilities or {}),
},
created_from_catalog_version=created_from_catalog_version,
+73
View File
@@ -0,0 +1,73 @@
from __future__ import annotations
from collections.abc import Mapping
from copy import deepcopy
from .models import JsonObject, RequiredCapability
def normalize_plan_node_refs(
plan: JsonObject,
source_bindings: Mapping[str, str],
) -> tuple[JsonObject, dict[str, RequiredCapability]]:
"""Rewrite concrete node refs in a plan to deployment-bound logical refs.
`source_bindings` uses the same direction as `WorkflowDeployment.bindings`:
logical source -> concrete source. This lets artifact creation accept the
source mapping authors already need at deployment time while saving portable
plan refs such as `demo.echo_tool` instead of `demo.personal.echo_tool`.
"""
if not source_bindings:
return deepcopy(plan), {}
normalized = deepcopy(plan)
requirements: dict[str, RequiredCapability] = {}
nodes = normalized.get("nodes")
if not isinstance(nodes, list):
return normalized, requirements
for node in nodes:
if not isinstance(node, dict):
continue
raw_node_ref = node.get("node")
if not isinstance(raw_node_ref, str):
continue
replacement = logical_ref_for_concrete_ref(raw_node_ref, source_bindings)
if replacement is None:
continue
logical_ref, logical_source, capability_name, concrete_source = replacement
node["node"] = logical_ref
requirements[logical_ref] = RequiredCapability(
logical_source=logical_source,
capability_name=capability_name,
kind="node_spec",
observed_concrete_source=concrete_source,
)
return normalized, requirements
def logical_ref_for_concrete_ref(
concrete_ref: str,
source_bindings: Mapping[str, str],
) -> tuple[str, str, str, str] | None:
"""Return a logical ref for one concrete capability ref when bound.
The longest concrete source match wins so `demo.personal.pro.echo` can be
handled safely when both `demo.personal` and `demo.personal.pro` exist.
"""
matches = sorted(
source_bindings.items(),
key=lambda item: len(item[1]),
reverse=True,
)
for logical_source, concrete_source in matches:
prefix = f"{concrete_source}."
if not concrete_ref.startswith(prefix):
continue
capability_name = concrete_ref[len(prefix) :]
if not capability_name:
continue
logical_ref = f"{logical_source}.{capability_name}"
return logical_ref, logical_source, capability_name, concrete_source
return None