feat(studio): expose partial capability states

parent fadd63bb
......@@ -206,6 +206,22 @@ def merge_capabilities(
return StudioCapabilityMergeResult(capabilities=capabilities, partial_capabilities=[])
def derive_aggregate_capabilities(capability_sets: Iterable[Optional[Iterable[str]]]) -> StudioCapabilityMergeResult:
normalized_sets = [normalize_capabilities(capabilities) for capabilities in capability_sets if capabilities]
if not normalized_sets:
return StudioCapabilityMergeResult(capabilities=[], partial_capabilities=[])
aggregate = list(normalized_sets[0])
partial: List[str] = []
for capability_set in normalized_sets[1:]:
merged = merge_capabilities(aggregate, capability_set, support_mode="intersection")
aggregate = merged.capabilities
partial = _dedupe([*partial, *merged.partial_capabilities, *[capability for capability in capability_set if capability not in aggregate]])
partial = [capability for capability in partial if capability not in aggregate]
return StudioCapabilityMergeResult(capabilities=aggregate, partial_capabilities=partial)
def build_catalog_entry(
scope: str,
owner_id: Optional[int],
......@@ -220,7 +236,14 @@ def build_catalog_entry(
metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
metadata = metadata or {}
effective_capabilities = metadata.get("studio_capabilities") or capabilities
explicit_capabilities = metadata.get("studio_capabilities") or capabilities
aggregate_capabilities = metadata.get("aggregate_capabilities")
effective_capabilities = aggregate_capabilities if not explicit_capabilities else explicit_capabilities
partial_capabilities = []
if explicit_capabilities:
partial_capabilities = []
elif aggregate_capabilities:
partial_capabilities = normalize_capabilities(metadata.get("aggregate_partial_capabilities"))
return {
"id": f"provider/{source_id}/{target_id}",
"kind": kind,
......@@ -231,6 +254,7 @@ def build_catalog_entry(
"label": label,
"description": description,
"capabilities": normalize_capabilities(effective_capabilities),
"partial_capabilities": partial_capabilities,
"availability_state": availability_state,
"availability_reason": availability_reason,
"metadata": metadata,
......@@ -387,6 +411,9 @@ def _build_rotation_entries(scope: str, owner_id: Optional[int], rotations: Dict
entries: List[Dict[str, Any]] = []
for rotation_id, rotation_config in rotations.items():
config_data = rotation_config if isinstance(rotation_config, dict) else rotation_config.model_dump()
aggregate_capabilities = derive_aggregate_capabilities(
provider.get("capabilities") for provider in (config_data.get("providers") or [])
)
entries.append(
_build_named_catalog_entry(
kind="rotation",
......@@ -400,6 +427,9 @@ def _build_rotation_entries(scope: str, owner_id: Optional[int], rotations: Dict
metadata={
"provider_count": len(config_data.get("providers") or []),
"context_length": config_data.get("context_length"),
"aggregate_capabilities": aggregate_capabilities.capabilities,
"aggregate_partial_capabilities": aggregate_capabilities.partial_capabilities,
"aggregate_capability_source": "derived",
},
)
)
......@@ -411,6 +441,9 @@ def _build_autoselect_entries(scope: str, owner_id: Optional[int], autoselects:
for autoselect_id, autoselect_config in autoselects.items():
config_data = autoselect_config if isinstance(autoselect_config, dict) else autoselect_config.model_dump()
available_models = config_data.get("available_models") or []
aggregate_capabilities = derive_aggregate_capabilities(
model.get("capabilities") if isinstance(model, dict) else None for model in available_models
)
entries.append(
_build_named_catalog_entry(
kind="autoselect",
......@@ -425,6 +458,9 @@ def _build_autoselect_entries(scope: str, owner_id: Optional[int], autoselects:
"available_model_count": len(available_models),
"fallback": config_data.get("fallback"),
"selection_model": config_data.get("selection_model"),
"aggregate_capabilities": aggregate_capabilities.capabilities,
"aggregate_partial_capabilities": aggregate_capabilities.partial_capabilities,
"aggregate_capability_source": "derived",
},
)
)
......
(function () {
var bootstrapEl = document.getElementById("studio-bootstrap");
var diagnosticsEl = document.getElementById("studio-diagnostics");
if (!bootstrapEl || !diagnosticsEl) {
var targetsEl = document.getElementById("studio-targets");
if (!bootstrapEl || !diagnosticsEl || !targetsEl) {
return;
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function renderCapabilityList(capabilities, className) {
if (!Array.isArray(capabilities) || capabilities.length === 0) {
return "";
}
return '<div class="' + className + '">' + capabilities.map(function (capability) {
return '<span class="studio-chip">' + escapeHtml(capability.replace(/_/g, " ")) + "</span>";
}).join("") + "</div>";
}
function renderTarget(target) {
var capabilities = Array.isArray(target.capabilities) ? target.capabilities : [];
var partialCapabilities = Array.isArray(target.partial_capabilities) ? target.partial_capabilities : [];
var stateLabel = partialCapabilities.length > 0 ? "Partial support" : "Ready";
return [
'<article class="studio-target-card" data-kind="' + escapeHtml(target.kind || "unknown") + '">',
' <div class="studio-target-card__header">',
' <strong>' + escapeHtml(target.label || target.id || "Unnamed target") + '</strong>',
' <span class="studio-chip">' + escapeHtml(stateLabel) + '</span>',
" </div>",
target.description ? ' <p class="studio-copy">' + escapeHtml(target.description) + '</p>' : "",
renderCapabilityList(capabilities, "studio-target-card__capabilities"),
partialCapabilities.length > 0 ? ' <p class="studio-copy">Partial: ' + escapeHtml(partialCapabilities.join(", ").replace(/_/g, " ")) + '</p>' : "",
"</article>",
].join("");
}
var payload = {};
try {
payload = JSON.parse(bootstrapEl.textContent || "{}");
......@@ -14,11 +52,25 @@
return;
}
var targets = Array.isArray(payload.targets) ? payload.targets.length : 0;
diagnosticsEl.dataset.state = targets > 0 ? "ready" : "empty";
if (targets > 0) {
diagnosticsEl.textContent = "Studio bootstrap payload loaded.";
} else if (!diagnosticsEl.textContent.trim()) {
var targets = Array.isArray(payload.targets)
? payload.targets
: Array.isArray(payload.entries)
? payload.entries
: [];
diagnosticsEl.dataset.state = targets.length > 0 ? "ready" : "empty";
if (targets.length > 0) {
targetsEl.innerHTML = targets.map(renderTarget).join("");
var partialCount = targets.filter(function (target) {
return Array.isArray(target.partial_capabilities) && target.partial_capabilities.length > 0;
}).length;
diagnosticsEl.textContent = partialCount > 0
? partialCount + " Studio targets have partial capability support."
: "Studio bootstrap payload loaded.";
} else {
targetsEl.textContent = targetsEl.dataset.emptyMessage || "No Studio targets available.";
}
if (!targets.length && !diagnosticsEl.textContent.trim()) {
diagnosticsEl.textContent = diagnosticsEl.dataset.emptyMessage || "No diagnostics yet.";
}
})();
......@@ -26,20 +26,18 @@
<h3 data-i18n="studio.catalog_title">Catalog</h3>
<span class="studio-chip" data-i18n="studio.catalog_scope">Dashboard shell</span>
</header>
<p class="studio-copy" data-i18n="studio.catalog_description">This page reserves the full Studio workspace and navigation entry while backend catalog data is still under construction.</p>
<div class="studio-placeholder-list" aria-hidden="true">
<div class="studio-placeholder-item"></div>
<div class="studio-placeholder-item"></div>
<div class="studio-placeholder-item"></div>
<p class="studio-copy" data-i18n="studio.catalog_description">Review provider, rotation, and autoselect capability coverage from one dashboard-native workspace.</p>
<div class="studio-targets" id="studio-targets" data-empty-message="No Studio targets available.">
<span data-i18n="studio.catalog_loading">Loading Studio targets.</span>
</div>
</section>
<section class="studio-panel studio-panel-emphasis">
<header class="studio-panel-header">
<h3 data-i18n="studio.workspace_title">Workspace</h3>
<span class="studio-chip" data-i18n="studio.workspace_mode">UI shell only</span>
<span class="studio-chip" data-i18n="studio.workspace_mode">Capability coverage</span>
</header>
<p class="studio-copy" data-i18n="studio.workspace_description">Frontend hooks, theme mapping, and bootstrap wiring are in place for the upcoming catalog endpoints.</p>
<p class="studio-copy" data-i18n="studio.workspace_description">Partial capability states call out what rotations and autoselects only support on some underlying targets.</p>
<div class="studio-diagnostics" id="studio-diagnostics" data-empty-message="No diagnostics yet.">
<span data-i18n="studio.diagnostics_empty">No diagnostics yet.</span>
</div>
......
......@@ -176,3 +176,61 @@ def test_build_catalog_entry_prefers_persisted_studio_capabilities_over_legacy_c
)
assert entry["capabilities"] == ["audio_input", "transcription"]
def test_build_catalog_entry_uses_aggregate_capabilities_for_rotation_without_explicit_capabilities():
entry = build_catalog_entry(
scope="admin",
owner_id=None,
kind="rotation",
source_id="creative-rotation",
target_id="creative-rotation",
label="Creative Rotation",
description=None,
capabilities=None,
availability_state="ready",
availability_reason=None,
metadata={
"aggregate_capabilities": ["chat", "vision"],
"aggregate_partial_capabilities": ["image_generation"],
"aggregate_capability_source": "derived",
},
)
assert entry["capabilities"] == ["chat", "vision"]
assert entry["partial_capabilities"] == ["image_generation"]
assert entry["metadata"]["aggregate_capability_source"] == "derived"
def test_build_catalog_entry_preserves_explicit_capabilities_when_aggregate_capabilities_exist():
entry = build_catalog_entry(
scope="admin",
owner_id=None,
kind="autoselect",
source_id="balanced-autoselect",
target_id="balanced-autoselect",
label="Balanced Autoselect",
description=None,
capabilities=["chat"],
availability_state="ready",
availability_reason=None,
metadata={
"aggregate_capabilities": ["chat", "vision"],
"aggregate_partial_capabilities": ["image_generation"],
"aggregate_capability_source": "derived",
},
)
assert entry["capabilities"] == ["chat"]
assert entry["partial_capabilities"] == []
def test_merge_capabilities_reports_partial_support_for_rotation_intersection():
merged = merge_capabilities(
base_capabilities=["chat", "vision", "image_generation"],
override_capabilities=["chat", "vision", "tool_use"],
support_mode="intersection",
)
assert merged.capabilities == ["chat", "vision"]
assert merged.partial_capabilities == ["image_generation"]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment