Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion surfsense_local/backend/modules/artifacts/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
FormatRead,
StudioJobCreate,
)
from modules.artifacts.service import create_artifact_job, list_formats
from modules.artifacts.service import create_artifact_job, list_formats, retry_artifact
from modules.documents.models import Document, DocumentType
from modules.workspaces.dependencies import WorkspaceDep
from shared.config import get_storage_settings
Expand Down Expand Up @@ -102,6 +102,16 @@ def read_artifact_file(artifact: ArtifactDep, role: ArtifactFileRole) -> FileRes
)


@router.post(
"/artifacts/{artifact_id}/retry",
response_model=ArtifactRead,
summary="Requeue a failed artifact",
)
def retry_studio_artifact(artifact: ArtifactDep, session: SessionDep) -> ArtifactRead:
artifact = retry_artifact(session, artifact)
return ArtifactRead.of(artifact)


@router.delete(
"/artifacts/{artifact_id}",
status_code=status.HTTP_204_NO_CONTENT,
Expand Down
22 changes: 22 additions & 0 deletions surfsense_local/backend/modules/artifacts/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,28 @@ def create_artifact_job(
return artifact


def retry_artifact(session: Session, artifact: Artifact) -> Artifact:
"""Requeue a failed artifact's generation, in place.

Mirrors retry_document: the same artifact_metadata (sources, prompt,
options) that created the artifact is still there, so this just resets
the backing document and re-enqueues the same job — no new row.
"""
document = artifact.document
if document.status is not DocumentStatus.FAILED:
raise HTTPException(
status.HTTP_409_CONFLICT, "only a failed artifact can be retried"
)

document.status = DocumentStatus.PENDING
document.error_message = None
session.commit()

studio_job(artifact.id)
logger.info("studio: retrying artifact %s", artifact.id)
return artifact


def _resolve_sources(
session: Session, workspace: Workspace, document_ids: list[int]
) -> list[Document]:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import pytest
from httpx import AsyncClient
from sqlalchemy import Engine
from sqlalchemy import Engine, text

from modules.documents.models import Document, DocumentStatus, DocumentType
from modules.llm.models import ModelRole, SelectedModel
from shared.db import create_session_factory
from shared.queue import huey

pytestmark = pytest.mark.integration

Expand Down Expand Up @@ -144,6 +145,51 @@ async def test_a_job_waits_for_a_source_to_index(
assert response.status_code == 409


async def test_a_failed_artifact_can_be_retried(
client: AsyncClient, engine: Engine, workspace_id: int, choose_model: None
) -> None:
"""Mirrors document retry: reset the backing document and requeue in place."""
source_id = make_ready_source(engine, workspace_id)
created = await client.post(
f"/workspaces/{workspace_id}/studio/jobs",
json={"format": "summary", "document_ids": [source_id]},
)
artifact_id = created.json()["id"]
document_id = created.json()["document_id"]
with engine.begin() as connection:
connection.execute(
text(
"UPDATE documents SET status = 'failed', "
"error_message = 'model server down' WHERE id = :id"
),
{"id": document_id},
)
huey.flush()

response = await client.post(f"/artifacts/{artifact_id}/retry")

assert response.status_code == 200
assert response.json()["status"] == "pending"
assert response.json()["error_message"] is None
assert [job.args for job in huey.pending()] == [(artifact_id,)]


async def test_only_a_failed_artifact_is_retried(
client: AsyncClient, engine: Engine, workspace_id: int, choose_model: None
) -> None:
"""Requeueing one already pending or processing would run it twice."""
source_id = make_ready_source(engine, workspace_id)
created = await client.post(
f"/workspaces/{workspace_id}/studio/jobs",
json={"format": "summary", "document_ids": [source_id]},
)
artifact_id = created.json()["id"]

response = await client.post(f"/artifacts/{artifact_id}/retry")

assert response.status_code == 409


async def test_an_artifact_can_be_deleted(
client: AsyncClient, engine: Engine, workspace_id: int, choose_model: None
) -> None:
Expand Down
5 changes: 4 additions & 1 deletion surfsense_local/frontend/src/components/theme-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ export function ThemeProvider({
return
}

if (event.metaKey || event.ctrlKey || event.altKey) {
// Cmd/Ctrl+Shift+D, so a bare "d" while typing never flips the theme.
if (!(event.metaKey || event.ctrlKey) || !event.shiftKey || event.altKey) {
return
}

Expand All @@ -171,6 +172,8 @@ export function ThemeProvider({
return
}

event.preventDefault()

setThemeState((currentTheme) => {
const nextTheme =
currentTheme === "dark"
Expand Down
5 changes: 5 additions & 0 deletions surfsense_local/frontend/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,17 @@ const buttonVariants = cva(
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xl: "h-10 gap-2 px-3 text-base has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5 [&_svg:not([class*='size-'])]:size-4.5",
"2xl":
"h-11 gap-2 px-3.5 text-lg has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3 [&_svg:not([class*='size-'])]:size-5",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
"icon-xl": "size-10 [&_svg:not([class*='size-'])]:size-4.5",
"icon-2xl": "size-11 [&_svg:not([class*='size-'])]:size-5",
},
},
defaultVariants: {
Expand Down
Loading
Loading