280 lines
9.7 KiB
Python
280 lines
9.7 KiB
Python
from dataclasses import dataclass, field
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from wf_authoring import NodeSpec
|
|
from wf_core import ReducerSpec
|
|
from wf_core.runtime.ops.merges import ReducerDefinition
|
|
from wf_platform.refs import CapabilityRef
|
|
|
|
# Source kind is source-origin metadata, not a provider interface:
|
|
# system=built-ins, connection=upstream/stateful providers, python=trusted local code.
|
|
SourceKind = Literal["system", "connection", "python"]
|
|
JsonObject = dict[str, Any]
|
|
SOURCE_PREVIEW_LIMIT = 3
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SourceVisibility:
|
|
planner: bool = False
|
|
client: bool = False
|
|
admin_dashboard: bool = True
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SourcePermissions:
|
|
safe_for_workflow: bool = False
|
|
calls_upstream: bool = False
|
|
mutates_config: bool = False
|
|
mutates_auth: bool = False
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SourcePolicy:
|
|
"""Runtime/deployment policy for one capability source.
|
|
|
|
Platform sources are process-provided built-ins such as `wf.std`. They are
|
|
not account-bound and should not require deployment source bindings.
|
|
"""
|
|
|
|
platform: bool = False
|
|
binding_required: bool = True
|
|
|
|
def __post_init__(self) -> None:
|
|
"""Reject contradictory deployment policy combinations."""
|
|
if self.platform and self.binding_required:
|
|
raise ValueError("platform sources cannot require bindings")
|
|
|
|
|
|
class SourceVisibilitySnapshot(BaseModel):
|
|
"""Serializable visibility flags for one source inventory snapshot."""
|
|
|
|
planner: bool = False
|
|
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 SourcePolicySnapshot(BaseModel):
|
|
"""Serializable source policy used by CLI/API inventory responses."""
|
|
|
|
platform: bool = False
|
|
binding_required: bool = True
|
|
|
|
|
|
class NodeSpecInventory(BaseModel):
|
|
"""Serializable public contract for one executable node spec."""
|
|
|
|
name: str
|
|
description: str | None = None
|
|
outcomes: tuple[str, ...]
|
|
input_schema: JsonObject
|
|
output_schema: JsonObject
|
|
is_async: bool
|
|
accepts_context: bool
|
|
|
|
|
|
class ReducerInventory(BaseModel):
|
|
"""Serializable public contract for one pure reducer."""
|
|
|
|
name: str
|
|
ref: CapabilityRef
|
|
description: str | None = None
|
|
config_schema: JsonObject
|
|
|
|
|
|
class SourceCapabilityInventory(BaseModel):
|
|
"""Serializable names owned by one source, grouped by capability kind."""
|
|
|
|
tools: tuple[str, ...] = ()
|
|
node_specs: tuple[str, ...] = ()
|
|
node_spec_details: tuple[NodeSpecInventory, ...] = ()
|
|
reducers: tuple[str, ...] = ()
|
|
reducer_details: tuple[ReducerInventory, ...] = ()
|
|
prompts: tuple[str, ...] = ()
|
|
resources: tuple[str, ...] = ()
|
|
|
|
|
|
class SourceCapabilityPreview(BaseModel):
|
|
"""Small sorted capability-name sample for compact source discovery."""
|
|
|
|
tools: tuple[str, ...] = ()
|
|
node_specs: tuple[str, ...] = ()
|
|
reducers: tuple[str, ...] = ()
|
|
prompts: tuple[str, ...] = ()
|
|
resources: tuple[str, ...] = ()
|
|
|
|
|
|
class SourceCapabilityHasMore(BaseModel):
|
|
"""Whether each compact source preview omitted owned capabilities."""
|
|
|
|
tools: bool = False
|
|
node_specs: bool = False
|
|
reducers: bool = False
|
|
prompts: bool = False
|
|
resources: bool = False
|
|
|
|
|
|
class SourceStatus(BaseModel):
|
|
"""Serializable source metadata without the full owned-name inventory."""
|
|
|
|
id: str
|
|
kind: SourceKind
|
|
enabled: bool
|
|
visibility: SourceVisibilitySnapshot
|
|
permissions: SourcePermissionsSnapshot
|
|
policy: SourcePolicySnapshot
|
|
description: str | None = None
|
|
tool_count: int
|
|
node_spec_count: int
|
|
reducer_count: int
|
|
prompt_count: int
|
|
resource_count: int
|
|
preview: SourceCapabilityPreview
|
|
has_more: SourceCapabilityHasMore
|
|
|
|
|
|
class SourceInventory(SourceStatus):
|
|
"""Serializable source snapshot with the capability names it owns."""
|
|
|
|
capabilities: SourceCapabilityInventory
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class CapabilityBuckets:
|
|
tools: dict[str, Any] = field(default_factory=dict)
|
|
node_specs: dict[str, NodeSpec[Any, Any]] = field(default_factory=dict)
|
|
reducers: dict[str, ReducerSpec] = field(default_factory=dict)
|
|
reducer_definitions: dict[str, ReducerDefinition] = field(default_factory=dict)
|
|
prompts: dict[str, Any] = field(default_factory=dict)
|
|
resources: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class CapabilitySource:
|
|
id: str
|
|
kind: SourceKind
|
|
capabilities: CapabilityBuckets = field(default_factory=CapabilityBuckets)
|
|
enabled: bool = True
|
|
visibility: SourceVisibility = field(default_factory=SourceVisibility)
|
|
permissions: SourcePermissions = field(default_factory=SourcePermissions)
|
|
policy: SourcePolicy = field(default_factory=SourcePolicy)
|
|
description: str | None = None
|
|
|
|
def as_status(self) -> SourceStatus:
|
|
"""Return serializable source metadata without owned capability names."""
|
|
return SourceStatus(
|
|
id=self.id,
|
|
kind=self.kind,
|
|
enabled=self.enabled,
|
|
visibility=SourceVisibilitySnapshot(
|
|
planner=self.visibility.planner,
|
|
client=self.visibility.client,
|
|
admin_dashboard=self.visibility.admin_dashboard,
|
|
),
|
|
permissions=SourcePermissionsSnapshot(
|
|
safe_for_workflow=self.permissions.safe_for_workflow,
|
|
calls_upstream=self.permissions.calls_upstream,
|
|
mutates_config=self.permissions.mutates_config,
|
|
mutates_auth=self.permissions.mutates_auth,
|
|
),
|
|
policy=SourcePolicySnapshot(
|
|
platform=self.policy.platform,
|
|
binding_required=self.policy.binding_required,
|
|
),
|
|
description=self.description,
|
|
tool_count=len(self.capabilities.tools),
|
|
node_spec_count=len(self.capabilities.node_specs),
|
|
reducer_count=len(self.capabilities.reducers),
|
|
prompt_count=len(self.capabilities.prompts),
|
|
resource_count=len(self.capabilities.resources),
|
|
preview=SourceCapabilityPreview(
|
|
tools=_preview_names(self.capabilities.tools, SOURCE_PREVIEW_LIMIT),
|
|
node_specs=_preview_names(
|
|
self.capabilities.node_specs,
|
|
SOURCE_PREVIEW_LIMIT,
|
|
),
|
|
reducers=_preview_names(
|
|
self.capabilities.reducers, SOURCE_PREVIEW_LIMIT
|
|
),
|
|
prompts=_preview_names(self.capabilities.prompts, SOURCE_PREVIEW_LIMIT),
|
|
resources=_preview_names(
|
|
self.capabilities.resources, SOURCE_PREVIEW_LIMIT
|
|
),
|
|
),
|
|
has_more=SourceCapabilityHasMore(
|
|
tools=_has_more(self.capabilities.tools, SOURCE_PREVIEW_LIMIT),
|
|
node_specs=_has_more(
|
|
self.capabilities.node_specs, SOURCE_PREVIEW_LIMIT
|
|
),
|
|
reducers=_has_more(self.capabilities.reducers, SOURCE_PREVIEW_LIMIT),
|
|
prompts=_has_more(self.capabilities.prompts, SOURCE_PREVIEW_LIMIT),
|
|
resources=_has_more(self.capabilities.resources, SOURCE_PREVIEW_LIMIT),
|
|
),
|
|
)
|
|
|
|
def as_inventory(self) -> SourceInventory:
|
|
"""Return a serializable source snapshot plus owned capability names."""
|
|
return SourceInventory(
|
|
**self.as_status().model_dump(),
|
|
capabilities=SourceCapabilityInventory(
|
|
tools=tuple(sorted(self.capabilities.tools)),
|
|
node_specs=tuple(sorted(self.capabilities.node_specs)),
|
|
node_spec_details=tuple(
|
|
_node_spec_inventory(spec)
|
|
for spec in sorted(
|
|
self.capabilities.node_specs.values(),
|
|
key=lambda spec: spec.name,
|
|
)
|
|
),
|
|
reducers=tuple(sorted(self.capabilities.reducers)),
|
|
reducer_details=tuple(
|
|
ReducerInventory(
|
|
name=reducer.name,
|
|
ref=CapabilityRef.parse(reducer.name),
|
|
description=reducer.description,
|
|
config_schema=reducer.config_schema,
|
|
)
|
|
for reducer in sorted(
|
|
self.capabilities.reducers.values(),
|
|
key=lambda reducer: reducer.name,
|
|
)
|
|
),
|
|
prompts=tuple(sorted(self.capabilities.prompts)),
|
|
resources=tuple(sorted(self.capabilities.resources)),
|
|
),
|
|
)
|
|
|
|
|
|
def _node_spec_inventory(spec: NodeSpec[Any, Any]) -> NodeSpecInventory:
|
|
"""Project one executable node spec into a serializable public contract."""
|
|
node_def = spec.to_node_def()
|
|
return NodeSpecInventory(
|
|
name=spec.name,
|
|
description=spec.description,
|
|
outcomes=tuple(node_def.outcomes),
|
|
input_schema=node_def.input_schema.model_dump(mode="json"),
|
|
output_schema=node_def.output_schema.model_dump(mode="json"),
|
|
is_async=spec.is_async,
|
|
accepts_context=spec.accepts_context,
|
|
)
|
|
|
|
|
|
def _preview_names(values: dict[str, Any], limit: int) -> tuple[str, ...]:
|
|
"""Return a tiny deterministic sample so list views stay inspectable."""
|
|
return tuple(sorted(values)[:limit])
|
|
|
|
|
|
def _has_more(values: dict[str, Any], limit: int) -> bool:
|
|
"""Return whether the compact preview omitted owned capability names."""
|
|
return len(values) > limit
|