feat: add wf source resource refs

This commit is contained in:
lda
2026-06-13 23:04:28 +07:00 Verified
parent 97b0f5b052
commit 4983033b7b
24 changed files with 682 additions and 48 deletions
+31
View File
@@ -0,0 +1,31 @@
from __future__ import annotations
import pytest
from wf_api.platform_context import SourceBindingPlatformContext
def test_platform_context_resolves_logical_source() -> None:
context = SourceBindingPlatformContext(
source_bindings={"drive": "drive.personal"},
read_resource_handler=None,
)
assert context.resolve_source("drive") == "drive.personal"
def test_platform_context_uses_identity_for_platform_sources() -> None:
context = SourceBindingPlatformContext(
source_bindings={},
platform_sources={"wf.source"},
read_resource_handler=None,
)
assert context.resolve_source("wf.source") == "wf.source"
def test_platform_context_rejects_unbound_source() -> None:
context = SourceBindingPlatformContext(source_bindings={}, read_resource_handler=None)
with pytest.raises(KeyError, match="unbound logical source"):
context.resolve_source("drive")
@@ -0,0 +1,98 @@
"""Regression: platform sources resolve reducers without deployment bindings."""
from __future__ import annotations
from unittest.mock import MagicMock
from wf_api.runtime_dependencies import resolve_runtime_dependencies
from wf_artifacts import RequiredCapability, WorkflowArtifact
from wf_core.runtime.ops.merges import ReducerDefinition, ReducerSpec, replace_reducer
from wf_platform import (
CapabilityBuckets,
CapabilitySource,
SourcePolicy,
SourceVisibility,
)
def _platform_source_with_reducer() -> CapabilitySource:
spec = ReducerSpec(name="wf.std.replace", description="Replace value.")
definition = ReducerDefinition(spec=spec, fn=replace_reducer)
return CapabilitySource(
id="wf.std",
kind="system",
capabilities=CapabilityBuckets(
reducers={"wf.std.replace": spec},
reducer_definitions={"wf.std.replace": definition},
),
visibility=SourceVisibility(planner=True),
policy=SourcePolicy(platform=True, binding_required=False),
)
def _non_platform_unbound_source_with_reducer() -> CapabilitySource:
spec = ReducerSpec(name="custom.replace", description="Replace value.")
definition = ReducerDefinition(spec=spec, fn=replace_reducer)
return CapabilitySource(
id="custom",
kind="system",
capabilities=CapabilityBuckets(
reducers={"custom.replace": spec},
reducer_definitions={"custom.replace": definition},
),
visibility=SourceVisibility(planner=True),
policy=SourcePolicy(platform=False, binding_required=False),
)
def _make_artifact_with_reducer(capability_name: str) -> WorkflowArtifact:
source, name = capability_name.rsplit(".", 1)
artifact = MagicMock(spec=WorkflowArtifact)
artifact.required_capability_map.return_value = {
capability_name: RequiredCapability(
ref=f"{source}.{name}",
kind="reducer",
),
}
return artifact
def test_platform_source_reducer_resolves_with_empty_bindings() -> None:
artifact = _make_artifact_with_reducer("wf.std.replace")
reducers = resolve_runtime_dependencies(
artifact=artifact,
deployment=None,
sources={"wf.std": _platform_source_with_reducer()},
plan_node_names=[],
).reducers
assert "wf.std.replace" in reducers
assert reducers["wf.std.replace"].spec.name == "wf.std.replace"
def test_platform_source_reducer_resolves_with_no_matching_binding() -> None:
from wf_artifacts import WorkflowDeployment
artifact = _make_artifact_with_reducer("wf.std.replace")
deployment = MagicMock(spec=WorkflowDeployment)
deployment.binding_map.return_value = {"external_source": "demo.personal"}
reducers = resolve_runtime_dependencies(
artifact=artifact,
deployment=deployment,
sources={"wf.std": _platform_source_with_reducer()},
plan_node_names=[],
).reducers
assert "wf.std.replace" in reducers
def test_non_platform_unbound_reducer_does_not_resolve_without_binding() -> None:
artifact = _make_artifact_with_reducer("custom.replace")
reducers = resolve_runtime_dependencies(
artifact=artifact,
deployment=None,
sources={"custom": _non_platform_unbound_source_with_reducer()},
plan_node_names=[],
).reducers
assert reducers == {}
+47
View File
@@ -0,0 +1,47 @@
from __future__ import annotations
import pytest
from wf_api.platform_context import SourceBindingPlatformContext
from wf_api.source_helpers import read_resource
from wf_api.source_refs import SourceResourceRef
from wf_core import RuntimeContext
async def test_read_resource_resolves_logical_source_and_bounds_text() -> None:
calls: list[tuple[str, str, int]] = []
async def handler(source_id: str, uri: str, max_chars: int):
calls.append((source_id, uri, max_chars))
return {
"contents": [
{
"type": "text",
"text": "abcdefghijklmnopqrstuvwxyz",
"mimeType": "text/plain",
}
]
}
platform = SourceBindingPlatformContext(
source_bindings={"drive": "drive.personal"},
read_resource_handler=handler,
)
result = await read_resource(
SourceResourceRef(logical_source="drive", uri="gdrive://file/abc"),
RuntimeContext(current_node_id="read", platform=platform),
max_chars=5,
)
assert calls == [("drive.personal", "gdrive://file/abc", 5)]
assert result.truncated is True
assert result.text == "abcde"
async def test_read_resource_requires_platform_context() -> None:
with pytest.raises(RuntimeError, match="platform context"):
await read_resource(
SourceResourceRef(logical_source="drive", uri="gdrive://file/abc"),
RuntimeContext(current_node_id="read"),
)
+24
View File
@@ -0,0 +1,24 @@
from __future__ import annotations
import pytest
from wf_api.source_refs import SourceResourceRef
def test_source_resource_ref_requires_logical_source_and_uri() -> None:
ref = SourceResourceRef(
logical_source="drive",
uri="gdrive://file/abc",
mime_type="application/pdf",
name="Report.pdf",
)
assert ref.kind == "source_resource_ref"
assert ref.logical_source == "drive"
assert ref.uri == "gdrive://file/abc"
assert ref.model_dump(mode="json")["name"] == "Report.pdf"
def test_source_resource_ref_rejects_empty_logical_source() -> None:
with pytest.raises(ValueError):
SourceResourceRef(logical_source="", uri="gdrive://file/abc")
@@ -265,3 +265,40 @@ async def test_content_access_uses_stateful_runtime_for_upstream_content() -> No
assert prompt["messages"][0]["content"]["text"] == "stateful prompt"
assert runtime.resources == ["demo://docs/welcome"]
assert runtime.prompts == ["prompt.summarize"]
async def test_read_resource_by_source_uri_reads_upstream() -> None:
service = WfMcpService(
store=FileStore(local_temp_root() / "content_source_uri")
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_adapter("demo", FakeAdapter())
await service.refresh_connection_catalog("demo.personal")
result = await service.content_access.read_resource_by_source_uri(
source_id="demo.personal",
uri="demo://docs/welcome",
max_chars=4000,
)
assert result["contents"][0]["text"] == "Welcome from the fake adapter resource."
async def test_read_resource_by_source_uri_rejects_unknown_resource() -> None:
service = WfMcpService(
store=FileStore(local_temp_root() / "content_source_uri_unknown")
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_adapter("demo", FakeAdapter())
await service.refresh_connection_catalog("demo.personal")
with pytest.raises(KeyError, match="unknown resource"):
await service.content_access.read_resource_by_source_uri(
source_id="demo.personal",
uri="demo://nonexistent",
max_chars=4000,
)
+92 -2
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from wf_artifacts import WorkflowDeployment
from wf_core import END, NodeUse, RunStatus
from wf_mcp.broker import WfMcpService
from wf_mcp.broker.service.source_catalog import SourceCatalogService
@@ -9,7 +10,7 @@ from wf_mcp.models import ConnectionConfig
from wf_mcp.storage import FileStore
from wf_platform import CapabilityBuckets, CapabilitySource, SourceVisibility
from ..test_support import echo_tool, local_temp_root
from ..test_support import FakeAdapter, echo_tool, local_temp_root, output_binding
from .conftest import raw_plan, single_echo_plan
@@ -70,6 +71,7 @@ def test_wfmcpservice_constructs_workflow_runtime_with_source_catalog() -> None:
assert service.workflow_runtime.source_catalog is service.source_catalog
assert service.workflow_runtime.artifact_store is service.artifact_store
assert service.workflow_runtime.read_resource_handler is not None
def test_wfmcpservice_compile_plan_delegates_to_workflow_runtime() -> None:
@@ -98,7 +100,7 @@ def test_workflow_runtime_service_prepares_node_registry_and_reducers() -> None:
emit_event=lambda event: None,
)
workflow, registry, reducers, prepared_subgraphs = runtime.prepare_workflow_runtime(
workflow, registry, reducers, prepared_subgraphs, platform_context = runtime.prepare_workflow_runtime(
single_echo_plan("runtime_prepare", "demo.personal.echo_tool"),
deployment=None,
artifact=None,
@@ -108,6 +110,27 @@ def test_workflow_runtime_service_prepares_node_registry_and_reducers() -> None:
assert "demo.personal.echo_tool" in registry
assert isinstance(reducers, dict)
assert prepared_subgraphs == {}
assert platform_context.source_bindings == {}
assert isinstance(platform_context.platform_sources, set)
assert platform_context.read_resource_handler is None
def test_wfmcpservice_prepares_platform_context_with_resource_handler() -> None:
service = WfMcpService(
store=FileStore(local_temp_root() / "runtime_platform_context")
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_specs("demo.personal", echo_tool)
*_runtime, platform_context = service.workflow_runtime.prepare_workflow_runtime(
single_echo_plan("runtime_platform_context", "demo.personal.echo_tool"),
deployment=None,
artifact=None,
)
assert platform_context.read_resource_handler is not None
async def test_workflow_runtime_service_runs_plan_and_emits_events() -> None:
@@ -131,6 +154,73 @@ async def test_workflow_runtime_service_runs_plan_and_emits_events() -> None:
assert events[1].payload["status"] == "completed"
async def test_wf_source_read_resource_runs_through_platform_context() -> None:
service = WfMcpService(
store=FileStore(local_temp_root() / "runtime_source_resource")
)
service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal")
)
service.register_adapter("demo", FakeAdapter())
await service.refresh_connection_catalog("demo.personal")
run = await service.workflow_runtime.run_workflow_from_plan(
raw_plan(
name="runtime_source_resource",
input_schema={"type": "object", "properties": {}},
state_schema={
"type": "object",
"properties": {"text": {"type": "string"}},
},
output_schema={
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
start="read",
nodes=[
{
"id": "read",
"type": "node",
"node": "wf.source.read_resource",
"input": [
{
"value": {
"kind": "source_resource_ref",
"logical_source": "drive",
"uri": "demo://docs/welcome",
},
"target": {"root": "local", "parts": ["ref"]},
},
{
"value": 7,
"target": {"root": "local", "parts": ["max_chars"]},
},
],
"output": [output_binding("text", "state.text")],
}
],
edges=[{"from": "read", "outcome": "ok", "to": END}],
output=[
{
"path": {"root": "state", "parts": ["text"]},
"target": {"root": "local", "parts": ["text"]},
}
],
),
{},
deployment=WorkflowDeployment(
id="runtime_source_resource.default",
artifact_id="runtime_source_resource",
artifact_version=1,
bindings={"drive": "demo.personal"},
),
)
assert run.status == RunStatus.COMPLETED
assert run.output == {"text": "Welcome"}
async def test_workflow_runtime_service_emits_failed_event_for_failed_run() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "runtime_failed_event"))
@@ -193,3 +193,13 @@ def test_local_static_builtins_are_platform_sources(tmp_path) -> None:
assert wf_std.policy.platform is True
assert wf_std.policy.binding_required is False
def test_local_static_server_exposes_wf_source_platform_source(tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path)
source = server.context.specs.capability_sources["wf.source"]
assert source.policy.platform is True
assert source.policy.binding_required is False
assert "wf.source.read_resource" in source.capabilities.node_specs