feat(scheduler,rotation,envelope,api,ui): pgqueuer jobs and deferred injects, master rotation with handout, vault envelope, jobs page
This commit is contained in:
@@ -60,6 +60,7 @@ if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
from beaver_gateway.core.conversations import Conversations
|
||||
from beaver_gateway.core.scheduler import Scheduler
|
||||
from beaver_gateway.frontends.base import GatewayRuntime
|
||||
|
||||
_log = logging.getLogger("beaver_gateway.frontends.api")
|
||||
@@ -525,19 +526,48 @@ def build_app(runtime: GatewayRuntime, *, memory_root: Path | None = None) -> Fa
|
||||
await require_token(request, runtime, scope=SCOPE)
|
||||
raw = request.query_params.get("conversation")
|
||||
conv = await conv_of(raw) if raw else None
|
||||
return {
|
||||
"schedules": [
|
||||
{
|
||||
"id": s.id,
|
||||
"conversation_row": s.conversation_id,
|
||||
"execute_at": _iso(s.execute_at),
|
||||
"text": s.text,
|
||||
"created_at": _iso(s.created_at),
|
||||
"delivered_at": _iso(s.delivered_at),
|
||||
}
|
||||
for s in await conversations.schedules(conv)
|
||||
]
|
||||
}
|
||||
return {"schedules": await conversations.schedules(conv)}
|
||||
|
||||
def scheduler_of() -> Scheduler:
|
||||
if runtime.scheduler is None:
|
||||
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, "no scheduler")
|
||||
return cast("Scheduler", runtime.scheduler)
|
||||
|
||||
@app.get("/jobs")
|
||||
async def jobs(request: Request) -> dict[str, Any]:
|
||||
await require_token(request, runtime, scope=SCOPE)
|
||||
return await scheduler_of().snapshot()
|
||||
|
||||
@app.post("/jobs/{name}/run", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def run_job(name: str, request: Request) -> dict[str, Any]:
|
||||
await require_token(request, runtime, scope=SCOPE)
|
||||
scheduler = scheduler_of()
|
||||
job = scheduler.job(name)
|
||||
if job is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, f"no job {name!r}")
|
||||
return {"job": await scheduler.trigger(job, await body_of(request))}
|
||||
|
||||
@app.delete("/jobs/queue/{job_id}")
|
||||
async def cancel_job(job_id: int, request: Request) -> dict[str, Any]:
|
||||
await require_token(request, runtime, scope=SCOPE)
|
||||
if not await scheduler_of().cancel(job_id):
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, f"no queued job {job_id}")
|
||||
return {"cancelled": job_id}
|
||||
|
||||
@app.post(
|
||||
"/conversations/{public_id}/schedule", status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
async def post_schedule(public_id: str, request: Request) -> dict[str, Any]:
|
||||
await require_token(request, runtime, scope=SCOPE)
|
||||
conv = await conv_of(public_id)
|
||||
data = await body_of(request)
|
||||
try:
|
||||
job_id, when = await conversations.schedule(
|
||||
conv, text_of(data, "at"), text_of(data, "text")
|
||||
)
|
||||
except (RuntimeError, ValueError) as exc:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc
|
||||
return {"job": job_id, "execute_at": _iso(when)}
|
||||
|
||||
@app.get("/usage")
|
||||
async def usage(request: Request) -> dict[str, Any]:
|
||||
|
||||
@@ -89,6 +89,7 @@ class GatewayRuntime:
|
||||
conversations: Any = None
|
||||
bus: Any = None
|
||||
pool: Any = None
|
||||
scheduler: Any = None
|
||||
# External origin the reverse proxy puts in front of the gateway
|
||||
# (``Gateway.public_url``); ``None`` means "derive from the request".
|
||||
public_url: str | None = None
|
||||
@@ -134,3 +135,7 @@ class Frontend(ABC):
|
||||
|
||||
async def materialize(self, conv: Conversation) -> ConversationBinding | None: # noqa: ARG002
|
||||
return None
|
||||
|
||||
async def mark_closed(self, conv: Conversation) -> bool: # noqa: ARG002
|
||||
"""Show in the window that the conversation is over (a renamed topic)."""
|
||||
return False
|
||||
|
||||
@@ -15,19 +15,22 @@ from starlette.responses import JSONResponse, RedirectResponse
|
||||
from starlette.routing import Mount, Route
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Iterable, Mapping
|
||||
|
||||
from starlette.requests import Request
|
||||
from starlette.types import ASGIApp
|
||||
|
||||
from beaver_gateway.frontends.base import Frontend
|
||||
|
||||
__all__ = ["build_root_app"]
|
||||
|
||||
|
||||
def build_root_app(frontends: Iterable[Frontend]) -> Starlette:
|
||||
def build_root_app(
|
||||
frontends: Iterable[Frontend], *, extra: Mapping[str, ASGIApp] | None = None
|
||||
) -> Starlette:
|
||||
mounted = [fe for fe in frontends if fe.path and fe.app() is not None]
|
||||
landing = next((fe for fe in mounted if fe.landing), None)
|
||||
paths = [fe.path for fe in mounted]
|
||||
paths = [*(fe.path for fe in mounted), *(extra or {})]
|
||||
|
||||
async def healthz(_request: Request) -> JSONResponse:
|
||||
return JSONResponse({"status": "ok", "frontends": paths})
|
||||
@@ -46,4 +49,6 @@ def build_root_app(frontends: Iterable[Frontend]) -> Starlette:
|
||||
assert app is not None # noqa: S101 - filtered above; narrows for ty
|
||||
assert fe.path is not None # noqa: S101
|
||||
routes.append(Mount(fe.path, app=app, name=fe.name or fe.path.strip("/")))
|
||||
for path, app in (extra or {}).items():
|
||||
routes.append(Mount(path, app=app, name=path.strip("/")))
|
||||
return Starlette(routes=routes)
|
||||
|
||||
@@ -203,12 +203,11 @@ class TelegramFrontend(Frontend):
|
||||
conv, frontend=FRONTEND, external_id=self._ext(topic.message_thread_id)
|
||||
)
|
||||
|
||||
async def mark_topic(self, conv: Conversation, prefix: str = "✅ ") -> bool:
|
||||
"""Rotation hook for M3.
|
||||
async def mark_closed(self, conv: Conversation) -> bool:
|
||||
return await self.mark_topic(conv)
|
||||
|
||||
``closeForumTopic`` does not exist in private chats; the state of a
|
||||
merged or closed branch lives in its name.
|
||||
"""
|
||||
async def mark_topic(self, conv: Conversation, prefix: str = "✅ ") -> bool:
|
||||
"""Rename the topic; closeForumTopic does not exist in private chats."""
|
||||
target = await self._target_of(conv)
|
||||
if target is None or target[1] is None:
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user