Spaces:
Paused
Paused
File size: 8,111 Bytes
ddd660b cfea6f8 ddd660b cfea6f8 ddd660b cfea6f8 ddd660b cfea6f8 ddd660b cfea6f8 ddd660b cfea6f8 ddd660b cfea6f8 ddd660b cfea6f8 ddd660b cfea6f8 ddd660b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | """Harness app wrapper for per-user research bucket instructions."""
from __future__ import annotations
import asyncio
from contextlib import asynccontextmanager
from dataclasses import replace
from typing import TYPE_CHECKING, Any
from fast_agent import AgentRequest, AppOpenRequest, HarnessAppContext
from mcp.types import TextContent
try:
from .app_jobs import current_research_job
from .app_observability import capture_markdown_report
from .archive_provisioning import ensure_archive_space
from .research_workspace import (
ResearchWorkspace,
current_research_workspace,
ensure_workspace,
)
except ImportError: # loaded as top-level module from the fast-agent home
from research.app_jobs import current_research_job
from research.app_observability import capture_markdown_report
from research.archive_provisioning import ensure_archive_space
from research.research_workspace import (
ResearchWorkspace,
current_research_workspace,
ensure_workspace,
)
if TYPE_CHECKING:
from collections.abc import AsyncIterator, Mapping
class ResearchHarnessApp:
"""Intercept opened harness sessions and wrap invocations."""
def __init__(self, context: HarnessAppContext) -> None:
self._default_app = context.default_app
@asynccontextmanager
async def open(self, request: AppOpenRequest | None = None) -> AsyncIterator[Any]:
resolved = request or AppOpenRequest()
async with self._default_app.open(resolved) as session:
yield ResearchHarnessSession(session, resolved.metadata)
class ResearchHarnessSession:
"""Per-open-session wrapper that injects bucket instructions per request."""
def __init__(self, session: Any, open_metadata: Mapping[str, object]) -> None:
self._session = session
self._open_metadata = dict(open_metadata)
@property
def agent_app(self) -> Any:
return self._session.agent_app
@property
def env(self) -> Any:
return self._session.env
async def invoke(self, request: AgentRequest) -> Any:
workspace = await asyncio.to_thread(
ensure_workspace,
auth=request.auth,
request_metadata={
**request.metadata,
"request_session_id": request.session_id,
},
open_metadata=self._open_metadata,
)
workspace = await self._with_archive_space(workspace)
forwarded = self._with_bucket_instructions(request, workspace)
workspace_token = current_research_workspace.set(workspace)
try:
if workspace.bearer_token is None:
response = await self._session.invoke(forwarded)
else:
from fast_agent.mcp.auth.context import request_bearer_token
auth_token = request_bearer_token.set(workspace.bearer_token)
try:
response = await self._session.invoke(forwarded)
finally:
request_bearer_token.reset(auth_token)
if job := current_research_job.get():
await capture_markdown_report(job)
return response
finally:
current_research_workspace.reset(workspace_token)
async def _with_archive_space(
self,
workspace: ResearchWorkspace,
) -> ResearchWorkspace:
try:
archive = await asyncio.to_thread(
ensure_archive_space,
username=workspace.username,
bucket_id=workspace.bucket_id,
token=workspace.bearer_token,
)
except Exception as exc:
return replace(
workspace,
archive_status="error",
archive_error=f"{type(exc).__name__}: {exc}",
)
return replace(
workspace,
archive_space_id=archive.space_id,
archive_space_url=archive.space_url,
archive_app_url=archive.app_url,
archive_status=archive.status,
archive_template_version=archive.template_version,
archive_installed_version=archive.installed_version,
)
def _with_bucket_instructions(
self, request: AgentRequest, workspace: ResearchWorkspace
) -> AgentRequest:
instructions = "\n".join(
[
"Verified research workspace for this request:",
f"- Hugging Face user: `{workspace.username}`",
f"- Bucket: `{workspace.bucket_id}`",
f"- Root: `{workspace.root}`",
f"- Scratch/workings: `{workspace.scratch}`",
f"- Final user-facing outputs: `{workspace.output}`",
*(
[
f"- Report archive Space: `{workspace.archive_space_id}`",
f"- Report archive: {workspace.archive_space_url}",
f"- Archive app: {workspace.archive_app_url}",
f"- Archive status: `{workspace.archive_status}`",
]
if workspace.archive_space_id
else []
),
"The workspace was verified before this prompt was sent.",
f"Hugging Face MCP authentication is verified for `{workspace.username}`.",
"The same caller bearer token is forwarded to Hugging Face MCP tool calls.",
"If authentication status must be reported, call `hf__hf_whoami`; do not infer it from cached server instructions.",
"Write the final Markdown report to the bucket-relative path `output/report.md` unless the user requests another filename.",
"That path is inside the verified Hugging Face bucket session, not the server's local filesystem.",
"Use Hugging Face filesystem tools for bucket files. Never create `output/`, `scratch/`, or report artifacts in the local working directory.",
"When you report a Hugging Face bucket artifact to the user, include both the `hf://` path and the accessible HTTPS URL.",
"Convert `hf://buckets/<owner>/<bucket>/<path>` to `https://huggingface.co/buckets/<owner>/<bucket>/tree/<path>`.",
f"Default report URL: `https://huggingface.co/buckets/{workspace.bucket_id}/tree/{workspace.session_id}/output/report.md`",
]
)
return replace(
request,
message=_prepend_text(request.message, instructions),
metadata={
**request.metadata,
"research_username": workspace.username,
"research_session_id": workspace.session_id,
"research_bucket_id": workspace.bucket_id,
"research_bucket_root": workspace.root,
"research_scratch": workspace.scratch,
"research_output": workspace.output,
"research_marker_paths": list(workspace.marker_paths),
"research_archive_space_id": workspace.archive_space_id,
"research_archive_space_url": workspace.archive_space_url,
"research_archive_app_url": workspace.archive_app_url,
"research_archive_status": workspace.archive_status,
"research_archive_template_version": (
workspace.archive_template_version
),
"research_archive_installed_version": (
workspace.archive_installed_version
),
"research_archive_error": workspace.archive_error,
},
)
def create_app(context: HarnessAppContext) -> ResearchHarnessApp:
return ResearchHarnessApp(context)
def _prepend_text(message: Any, text: str) -> Any:
content = list(message.content)
content.insert(
0,
TextContent(
type="text",
text=f"{text}\n\nUser request follows.",
),
)
return message.model_copy(update={"content": content})
|