research-agent-two / research /app_renderer.py
evalstate's picture
evalstate HF Staff
Isolate restored research runtime and version app renderer
9c370dc verified
Raw
History Blame Contribute Delete
2.49 kB
"""Versioned Prefab renderer resource for reliable host cache invalidation."""
from __future__ import annotations
import hashlib
from collections.abc import Sequence
from pathlib import Path
from fastmcp import FastMCP
from fastmcp.apps.config import UI_MIME_TYPE, AppConfig, ResourceCSP
from fastmcp.server.providers.addressing import hash_tool
from fastmcp.server.transforms import Transform
from fastmcp.tools.base import Tool
from prefab_ui.renderer import get_renderer_csp, get_renderer_html
class RendererUriTransform(Transform):
"""Point one UI tool at a content-addressed renderer resource."""
def __init__(self, tool_name: str, resource_uri: str) -> None:
self.tool_name = tool_name
self.resource_uri = resource_uri
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
return [
self._with_renderer(tool) if tool.name == self.tool_name else tool
for tool in tools
]
def _with_renderer(self, tool: Tool) -> Tool:
meta = dict(tool.meta or {})
ui = dict(meta.get("ui") or {})
ui["resourceUri"] = self.resource_uri
meta["ui"] = ui
return tool.model_copy(update={"meta": meta})
def install_versioned_renderer(
mcp: FastMCP,
*,
app_name: str,
tool_name: str,
build_id: str,
) -> str:
"""Register the Prefab renderer at a URI derived from its exact content."""
html = get_renderer_html().replace(
"</head>",
f'<meta name="research-app-build" content="{build_id}"></head>',
)
digest = hashlib.sha256(html.encode()).hexdigest()[:12]
tool_digest = hash_tool(app_name, tool_name)
resource_uri = f"ui://prefab/tool/{tool_digest}/renderer-{digest}.html"
csp = ResourceCSP(**(get_renderer_csp() or {}))
@mcp.resource(
resource_uri,
name="Versioned Prefab Renderer",
mime_type=UI_MIME_TYPE,
app=AppConfig(csp=csp),
)
def prefab_renderer() -> str:
return html
mcp.add_transform(RendererUriTransform(tool_name, resource_uri))
return digest
def app_build_id(home: Path) -> str:
"""Hash the app code and prompts that shape a rendered research view."""
digest = hashlib.sha256()
paths = sorted(home.glob("*.py")) + sorted((home / "agent-cards").glob("*.md"))
for path in paths:
digest.update(path.relative_to(home).as_posix().encode())
digest.update(path.read_bytes())
return digest.hexdigest()[:8]