read_resource to also read capability sources

This commit is contained in:
lda
2026-05-19 17:41:59 +07:00 Verified
parent d22f3e64c8
commit 8a345883d1
5 changed files with 115 additions and 7 deletions
+1 -4
View File
@@ -74,10 +74,7 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
).entry ).entry
if isinstance(step, DraftChooseStep): if isinstance(step, DraftChooseStep):
return builder.choose( return builder.choose(
*[ *[(clause.if_, clause.then) for clause in step.choose.clauses],
(clause.if_, clause.then)
for clause in step.choose.clauses
],
id=step_id, id=step_id,
default=step.choose.default, default=step.choose.default,
).entry ).entry
+8 -2
View File
@@ -101,7 +101,10 @@ def register_service_admin_tools(
@server.tool( @server.tool(
name=name("read_resource"), name=name("read_resource"),
title="Read Resource", title="Read Resource",
description="Read a broker-catalog resource by qualified name.", description=(
"Read a local docs or broker-catalog resource by qualified name, "
"for example wf.docs.workflow_capabilities."
),
) )
async def read_resource(qualified_name: str) -> dict[str, Any]: async def read_resource(qualified_name: str) -> dict[str, Any]:
return await handlers.read_broker_resource(qualified_name) return await handlers.read_broker_resource(qualified_name)
@@ -109,7 +112,10 @@ def register_service_admin_tools(
@server.tool( @server.tool(
name=name("render_prompt"), name=name("render_prompt"),
title="Render Prompt", title="Render Prompt",
description="Render a broker-catalog prompt by qualified name.", description=(
"Render a local docs or broker-catalog prompt by qualified name, "
"for example wf.docs.workflow_authoring_guide."
),
) )
async def render_prompt( async def render_prompt(
qualified_name: str, qualified_name: str,
+68
View File
@@ -23,6 +23,8 @@ from wf_core import NodeUse, Workflow, execute_workflow_async
from wf_platform import ( from wf_platform import (
CapabilityBuckets, CapabilityBuckets,
CapabilitySource, CapabilitySource,
DocumentationPrompt,
DocumentationResource,
SourcePermissions, SourcePermissions,
SourceVisibility, SourceVisibility,
page_items, page_items,
@@ -363,6 +365,25 @@ class WfMcpService:
return entry return entry
async def read_resource(self, qualified_name: str) -> dict[str, Any]: async def read_resource(self, qualified_name: str) -> dict[str, Any]:
local_resource = self._local_documentation_resource(qualified_name)
if local_resource is not None:
self._record_event(
make_event(
"resource_read_completed",
capability_id=qualified_name,
payload={"uri": local_resource.uri, "source": "local"},
)
)
return {
"contents": [
{
"uri": local_resource.uri,
"mimeType": local_resource.mime_type,
"text": local_resource.text,
}
]
}
resource = self.get_resource(qualified_name) resource = self.get_resource(qualified_name)
connection = self.connections.get(resource.connection_id) connection = self.connections.get(resource.connection_id)
adapter = require_adapter(connection, self.adapters) adapter = require_adapter(connection, self.adapters)
@@ -484,6 +505,31 @@ class WfMcpService:
*, *,
arguments: dict[str, str] | None = None, arguments: dict[str, str] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
local_prompt = self._local_documentation_prompt(qualified_name)
if local_prompt is not None:
self._record_event(
make_event(
"prompt_get_completed",
capability_id=qualified_name,
payload={
"argument_keys": sorted((arguments or {}).keys()),
"source": "local",
},
)
)
return {
"description": local_prompt.description,
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": local_prompt.text,
},
}
],
}
prompt = self.get_prompt(qualified_name) prompt = self.get_prompt(qualified_name)
connection = self.connections.get(prompt.connection_id) connection = self.connections.get(prompt.connection_id)
adapter = require_adapter(connection, self.adapters) adapter = require_adapter(connection, self.adapters)
@@ -512,6 +558,28 @@ class WfMcpService:
) )
return result return result
def _local_documentation_resource(
self,
qualified_name: str,
) -> DocumentationResource | None:
"""Return a local docs resource from capability sources by qualified name."""
for source in self.capability_sources.values():
resource = source.capabilities.resources.get(qualified_name)
if isinstance(resource, DocumentationResource):
return resource
return None
def _local_documentation_prompt(
self,
qualified_name: str,
) -> DocumentationPrompt | None:
"""Return a local docs prompt from capability sources by qualified name."""
for source in self.capability_sources.values():
prompt = source.capabilities.prompts.get(qualified_name)
if isinstance(prompt, DocumentationPrompt):
return prompt
return None
async def refresh_connection_catalog( async def refresh_connection_catalog(
self, self,
connection_id: str, connection_id: str,
+1 -1
View File
@@ -1,5 +1,5 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Literal from typing import Any, Literal, dataclass_transform
from pydantic import BaseModel from pydantic import BaseModel
+37
View File
@@ -450,6 +450,43 @@ def test_server_exposes_platform_documentation_prompts() -> None:
asyncio.run(run_proxy()) asyncio.run(run_proxy())
def test_admin_tools_can_read_local_documentation_source() -> None:
config = BrokerConfig(
store_root=local_temp_root() / "unified_admin_docs_store",
connections=[],
)
async def run_proxy() -> None:
client = create_server_client(
config,
resources_as_tools=True,
prompts_as_tools=True,
safe_tool_names=True,
)
async with client:
resource = await client.call_tool(
"wf_admin_read_resource",
{"qualified_name": "wf.docs.workflow_capabilities"},
)
prompt = await client.call_tool(
"wf_admin_render_prompt",
{"qualified_name": "wf.docs.workflow_authoring_guide"},
)
resource_payload = _structured(resource)
prompt_payload = _structured(prompt)
assert resource_payload["contents"][0]["uri"] == (
"wf://docs/workflow-capabilities"
)
assert "# Workflow Capabilities" in resource_payload["contents"][0]["text"]
assert (
"wf://docs/workflow-capabilities"
in (prompt_payload["messages"][0]["content"]["text"])
)
asyncio.run(run_proxy())
def test_server_reload_syncs_service_connection_source_enabled_state() -> None: def test_server_reload_syncs_service_connection_source_enabled_state() -> None:
tmp_path = local_temp_root() / "unified_reload_service_source_store" tmp_path = local_temp_root() / "unified_reload_service_source_store"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)