less dicts more models

This commit is contained in:
lda
2026-05-18 02:19:35 +07:00 Verified
parent 9900b01965
commit cb8d43a158
5 changed files with 154 additions and 36 deletions
+3
View File
@@ -191,6 +191,9 @@ The code now has the first capability-source layer in place.
- `wf_platform.CapabilitySource` owns source metadata, visibility, permissions, and - `wf_platform.CapabilitySource` owns source metadata, visibility, permissions, and
capability buckets. capability buckets.
- `CapabilitySource` is the mutable runtime registry object; typed
`SourceStatus` and `SourceInventory` snapshots are the serializable domain
projections used at boundaries such as `list_sources()`.
- `WfMcpService.capability_sources` is the canonical in-memory registry. - `WfMcpService.capability_sources` is the canonical in-memory registry.
- Planner node lookup reads `CapabilitySource.capabilities.node_specs` - Planner node lookup reads `CapabilitySource.capabilities.node_specs`
directly; the old `SpecSource` compatibility layer has been removed. directly; the old `SpecSource` compatibility layer has been removed.
+1 -1
View File
@@ -213,7 +213,7 @@ class WfMcpService:
def list_sources(self) -> list[dict[str, Any]]: def list_sources(self) -> list[dict[str, Any]]:
"""Return every capability source with the names it currently owns.""" """Return every capability source with the names it currently owns."""
return [ return [
source.as_inventory() source.as_inventory().model_dump(mode="json")
for source in sorted( for source in sorted(
self.capability_sources.values(), self.capability_sources.values(),
key=lambda source: source.id, key=lambda source: source.id,
+10
View File
@@ -1,15 +1,25 @@
from .sources import ( from .sources import (
CapabilityBuckets, CapabilityBuckets,
CapabilitySource, CapabilitySource,
SourceCapabilityInventory,
SourceInventory,
SourceKind, SourceKind,
SourcePermissions, SourcePermissions,
SourcePermissionsSnapshot,
SourceStatus,
SourceVisibility, SourceVisibility,
SourceVisibilitySnapshot,
) )
__all__ = [ __all__ = [
"CapabilityBuckets", "CapabilityBuckets",
"CapabilitySource", "CapabilitySource",
"SourceCapabilityInventory",
"SourceInventory",
"SourceKind", "SourceKind",
"SourcePermissions", "SourcePermissions",
"SourcePermissionsSnapshot",
"SourceStatus",
"SourceVisibility", "SourceVisibility",
"SourceVisibilitySnapshot",
] ]
+87 -35
View File
@@ -1,6 +1,8 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Literal from typing import Any, Literal
from pydantic import BaseModel
from wf_authoring import NodeSpec from wf_authoring import NodeSpec
from wf_core import ReducerSpec from wf_core import ReducerSpec
from wf_core.runtime.ops.merges import ReducerDefinition from wf_core.runtime.ops.merges import ReducerDefinition
@@ -23,6 +25,55 @@ class SourcePermissions:
mutates_auth: bool = False mutates_auth: bool = False
class SourceVisibilitySnapshot(BaseModel):
"""Serializable visibility flags for one source inventory snapshot."""
planner: bool = False
mcp_client: bool = False
admin_dashboard: bool = True
class SourcePermissionsSnapshot(BaseModel):
"""Serializable permission flags for one source inventory snapshot."""
safe_for_workflow: bool = False
calls_upstream: bool = False
mutates_config: bool = False
mutates_auth: bool = False
class SourceCapabilityInventory(BaseModel):
"""Serializable names owned by one source, grouped by capability kind."""
tools: tuple[str, ...] = ()
node_specs: tuple[str, ...] = ()
reducers: tuple[str, ...] = ()
prompts: tuple[str, ...] = ()
resources: tuple[str, ...] = ()
class SourceStatus(BaseModel):
"""Serializable source metadata without the full owned-name inventory."""
id: str
kind: SourceKind
enabled: bool
visibility: SourceVisibilitySnapshot
permissions: SourcePermissionsSnapshot
description: str | None = None
tool_count: int
node_spec_count: int
reducer_count: int
prompt_count: int
resource_count: int
class SourceInventory(SourceStatus):
"""Serializable source snapshot with the capability names it owns."""
capabilities: SourceCapabilityInventory
@dataclass(slots=True) @dataclass(slots=True)
class CapabilityBuckets: class CapabilityBuckets:
tools: dict[str, Any] = field(default_factory=dict) tools: dict[str, Any] = field(default_factory=dict)
@@ -43,39 +94,40 @@ class CapabilitySource:
permissions: SourcePermissions = field(default_factory=SourcePermissions) permissions: SourcePermissions = field(default_factory=SourcePermissions)
description: str | None = None description: str | None = None
def as_status(self) -> dict[str, Any]: def as_status(self) -> SourceStatus:
return { """Return serializable source metadata without owned capability names."""
"id": self.id, return SourceStatus(
"kind": self.kind, id=self.id,
"enabled": self.enabled, kind=self.kind,
"visibility": { enabled=self.enabled,
"planner": self.visibility.planner, visibility=SourceVisibilitySnapshot(
"mcp_client": self.visibility.mcp_client, planner=self.visibility.planner,
"admin_dashboard": self.visibility.admin_dashboard, mcp_client=self.visibility.mcp_client,
}, admin_dashboard=self.visibility.admin_dashboard,
"permissions": { ),
"safe_for_workflow": self.permissions.safe_for_workflow, permissions=SourcePermissionsSnapshot(
"calls_upstream": self.permissions.calls_upstream, safe_for_workflow=self.permissions.safe_for_workflow,
"mutates_config": self.permissions.mutates_config, calls_upstream=self.permissions.calls_upstream,
"mutates_auth": self.permissions.mutates_auth, mutates_config=self.permissions.mutates_config,
}, mutates_auth=self.permissions.mutates_auth,
"description": self.description, ),
"tool_count": len(self.capabilities.tools), description=self.description,
"node_spec_count": len(self.capabilities.node_specs), tool_count=len(self.capabilities.tools),
"reducer_count": len(self.capabilities.reducers), node_spec_count=len(self.capabilities.node_specs),
"prompt_count": len(self.capabilities.prompts), reducer_count=len(self.capabilities.reducers),
"resource_count": len(self.capabilities.resources), prompt_count=len(self.capabilities.prompts),
} resource_count=len(self.capabilities.resources),
)
def as_inventory(self) -> dict[str, Any]: def as_inventory(self) -> SourceInventory:
"""Return source metadata plus the capability names it owns.""" """Return a serializable source snapshot plus owned capability names."""
return { return SourceInventory(
**self.as_status(), **self.as_status().model_dump(),
"capabilities": { capabilities=SourceCapabilityInventory(
"tools": sorted(self.capabilities.tools), tools=tuple(sorted(self.capabilities.tools)),
"node_specs": sorted(self.capabilities.node_specs), node_specs=tuple(sorted(self.capabilities.node_specs)),
"reducers": sorted(self.capabilities.reducers), reducers=tuple(sorted(self.capabilities.reducers)),
"prompts": sorted(self.capabilities.prompts), prompts=tuple(sorted(self.capabilities.prompts)),
"resources": sorted(self.capabilities.resources), resources=tuple(sorted(self.capabilities.resources)),
}, ),
} )
+53
View File
@@ -0,0 +1,53 @@
from __future__ import annotations
from wf_platform import (
CapabilityBuckets,
CapabilitySource,
SourceInventory,
SourcePermissions,
SourceStatus,
SourceVisibility,
)
def test_capability_source_projects_typed_status() -> None:
source = CapabilitySource(
id="wf.std",
kind="system",
capabilities=CapabilityBuckets(
tools={"wf.std.inspect": object()},
resources={"wf.std.manual": object()},
),
visibility=SourceVisibility(planner=True, mcp_client=True),
permissions=SourcePermissions(safe_for_workflow=True),
description="Workflow standard library.",
)
status = source.as_status()
assert isinstance(status, SourceStatus)
assert status.id == "wf.std"
assert status.visibility.planner is True
assert status.permissions.safe_for_workflow is True
assert status.tool_count == 1
assert status.resource_count == 1
def test_capability_source_projects_typed_inventory() -> None:
source = CapabilitySource(
id="wf.std",
kind="system",
capabilities=CapabilityBuckets(
tools={"wf.std.inspect": object()},
resources={"wf.std.manual": object()},
),
)
inventory = source.as_inventory()
assert isinstance(inventory, SourceInventory)
assert inventory.capabilities.tools == ("wf.std.inspect",)
assert inventory.capabilities.resources == ("wf.std.manual",)
assert inventory.model_dump(mode="json")["capabilities"]["tools"] == [
"wf.std.inspect"
]