From 94e834134f7269260095b1c7d1763bcf4fbd582b Mon Sep 17 00:00:00 2001
From: "DESKTOP-RTLN3BA\\$punk"
Date: Thu, 28 May 2026 19:21:29 -0700
Subject: [PATCH] chore: linting
---
.../versions/144_add_automation_tables.py | 4 +-
.../main_agent/tools/automation/prompt.py | 1 -
.../task_tool.py | 4 +-
.../builtins/deliverables/tools/podcast.py | 4 +-
.../deliverables/tools/video_presentation.py | 7 +-
.../app/agents/new_chat/tools/podcast.py | 8 +-
.../new_chat/tools/video_presentation.py | 4 +-
.../app/automations/actions/__init__.py | 2 +-
.../actions/agent_task/__init__.py | 2 +-
.../automations/actions/agent_task/invoke.py | 1 -
.../app/automations/persistence/models/run.py | 2 +-
.../app/automations/runtime/executor.py | 11 +-
.../app/automations/runtime/step.py | 21 ++-
.../schemas/definition/execution.py | 4 +-
.../schemas/definition/plan_step.py | 4 +-
.../schemas/definition/trigger_spec.py | 4 +-
.../app/automations/services/automation.py | 42 +++--
.../app/automations/services/run.py | 12 +-
.../app/automations/services/trigger.py | 14 +-
.../app/automations/tasks/execute_run.py | 2 +-
.../app/automations/tasks/schedule_tick.py | 4 +-
.../app/automations/triggers/__init__.py | 2 +-
.../automations/triggers/schedule/__init__.py | 2 +-
.../app/automations/triggers/schedule/cron.py | 6 +-
.../automations/triggers/schedule/params.py | 4 +-
surfsense_backend/app/db.py | 1 -
surfsense_backend/app/routes/__init__.py | 3 +-
.../streaming/context/deepagents_todos.py | 4 +-
.../chat/streaming/flows/new_chat/auto_pin.py | 8 +-
.../streaming/flows/new_chat/input_state.py | 4 +-
.../flows/new_chat/llm_capability.py | 4 +-
.../streaming/flows/new_chat/orchestrator.py | 27 ++--
.../flows/new_chat/runtime_context.py | 4 +-
.../streaming/flows/new_chat/title_gen.py | 8 +-
.../flows/resume_chat/orchestrator.py | 37 ++---
.../flows/resume_chat/resume_routing.py | 4 +-
.../flows/shared/assistant_finalize.py | 4 +-
.../streaming/flows/shared/finally_cleanup.py | 4 +-
.../streaming/flows/shared/premium_quota.py | 10 +-
.../tasks/chat/streaming/flows/shared/span.py | 3 +-
.../streaming/flows/shared/terminal_error.py | 3 +-
.../actions/agent_task/test_finalize.py | 6 +-
.../unit/automations/runtime/test_retries.py | 4 +-
.../schemas/definition/test_envelope.py | 4 +-
.../templating/test_environment.py | 4 +-
.../unit/automations/test_definition_types.py | 2 +-
.../tests/unit/automations/test_stores.py | 8 +-
.../triggers/schedule/test_cron.py | 8 +-
.../test_parallel_refactor_parity.py | 20 ++-
surfsense_web/app/(home)/free/page.tsx | 10 +-
surfsense_web/app/(home)/privacy/page.tsx | 98 ++++++------
surfsense_web/app/api/zero/query/route.ts | 2 +-
.../edit/automation-edit-content.tsx | 5 +-
.../edit/components/automation-edit-form.tsx | 5 +-
.../new-chat/[[...chat_id]]/page.tsx | 11 +-
.../[search_space_id]/team/team-content.tsx | 2 +-
.../atoms/members/members-query.atoms.ts | 8 +-
.../assistant-ui/assistant-message.tsx | 2 +-
.../components/obsidian-connect-form.tsx | 3 +-
.../components/circleback-config.tsx | 2 +-
.../views/connector-edit-view.tsx | 1 +
.../views/connector-accounts-list-view.tsx | 1 +
.../assistant-ui/inline-mention-editor.tsx | 20 ++-
.../components/assistant-ui/thread.tsx | 89 ++++++-----
.../components/assistant-ui/user-message.tsx | 8 +-
.../components/editor-panel/editor-panel.tsx | 1 +
.../components/free-chat/anonymous-chat.tsx | 26 ++--
.../layout/ui/icon-rail/SearchSpaceAvatar.tsx | 6 +-
.../layout/ui/sidebar/DocumentsSidebar.tsx | 2 +-
.../layout/ui/tabs/DocumentTabContent.tsx | 1 +
.../new-chat/composer-suggestion-popup.tsx | 5 +-
.../new-chat/document-mention-picker.tsx | 144 ++++++++++--------
.../components/new-chat/prompt-picker.tsx | 4 +-
.../settings/general-settings-manager.tsx | 2 +-
.../settings/prompt-config-manager.tsx | 8 +-
.../components/tool-ui/generate-podcast.tsx | 9 +-
.../generate-video-presentation.tsx | 2 +-
.../hooks/use-search-source-connectors.ts | 12 +-
surfsense_web/lib/auth-utils.ts | 1 +
surfsense_web/lib/posthog/events.ts | 2 +-
80 files changed, 443 insertions(+), 404 deletions(-)
diff --git a/surfsense_backend/alembic/versions/144_add_automation_tables.py b/surfsense_backend/alembic/versions/144_add_automation_tables.py
index 8d836095d..39f927417 100644
--- a/surfsense_backend/alembic/versions/144_add_automation_tables.py
+++ b/surfsense_backend/alembic/versions/144_add_automation_tables.py
@@ -98,9 +98,7 @@ def upgrade() -> None:
op.execute(
"CREATE INDEX ix_automation_triggers_automation_id ON automation_triggers(automation_id);"
)
- op.execute(
- "CREATE INDEX ix_automation_triggers_type ON automation_triggers(type);"
- )
+ op.execute("CREATE INDEX ix_automation_triggers_type ON automation_triggers(type);")
op.execute(
"CREATE INDEX ix_automation_triggers_enabled ON automation_triggers(enabled);"
)
diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/tools/automation/prompt.py b/surfsense_backend/app/agents/multi_agent_chat/main_agent/tools/automation/prompt.py
index 45870e768..09854aa2e 100644
--- a/surfsense_backend/app/agents/multi_agent_chat/main_agent/tools/automation/prompt.py
+++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/tools/automation/prompt.py
@@ -28,7 +28,6 @@ from __future__ import annotations
from datetime import UTC, datetime
-
_HEADER = """\
You are the SurfSense automation drafter. Convert the user intent below
into a SINGLE JSON object matching the AutomationCreate schema. Output
diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/main_agent/checkpointed_subagent_middleware/task_tool.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/main_agent/checkpointed_subagent_middleware/task_tool.py
index 91a0be506..eaed9a55f 100644
--- a/surfsense_backend/app/agents/multi_agent_chat/middleware/main_agent/checkpointed_subagent_middleware/task_tool.py
+++ b/surfsense_backend/app/agents/multi_agent_chat/middleware/main_agent/checkpointed_subagent_middleware/task_tool.py
@@ -404,9 +404,7 @@ def build_task_tool_with_parent_config(
continue
messages = payload.get("messages") or []
last_text = _safe_message_text(messages[-1]).rstrip() if messages else ""
- message_blocks.append(
- f"[task {task_index}] {last_text or ''}"
- )
+ message_blocks.append(f"[task {task_index}] {last_text or ''}")
try:
child_trace = _build_tool_trace(messages)
except Exception:
diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/tools/podcast.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/tools/podcast.py
index 84617d38b..298257799 100644
--- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/tools/podcast.py
+++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/tools/podcast.py
@@ -117,9 +117,7 @@ def create_generate_podcast_tool(
"podcast_id": podcast_id,
"title": podcast_title,
"file_location": file_location,
- "message": (
- "Podcast generated and saved to your podcast panel."
- ),
+ "message": ("Podcast generated and saved to your podcast panel."),
}
return with_receipt(
payload=payload,
diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/tools/video_presentation.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/tools/video_presentation.py
index 8c52293de..5407c8834 100644
--- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/tools/video_presentation.py
+++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/tools/video_presentation.py
@@ -126,8 +126,7 @@ def create_generate_video_presentation_tool(
elapsed,
)
err = (
- "Background worker reported FAILED status for this "
- "video presentation."
+ "Background worker reported FAILED status for this video presentation."
)
payload = {
"status": VideoPresentationStatus.FAILED.value,
@@ -151,9 +150,7 @@ def create_generate_video_presentation_tool(
except Exception as e:
error_message = str(e)
- logger.exception(
- "[generate_video_presentation] Error: %s", error_message
- )
+ logger.exception("[generate_video_presentation] Error: %s", error_message)
payload = {
"status": VideoPresentationStatus.FAILED.value,
"error": error_message,
diff --git a/surfsense_backend/app/agents/new_chat/tools/podcast.py b/surfsense_backend/app/agents/new_chat/tools/podcast.py
index 36aecfe49..83ac98768 100644
--- a/surfsense_backend/app/agents/new_chat/tools/podcast.py
+++ b/surfsense_backend/app/agents/new_chat/tools/podcast.py
@@ -131,9 +131,7 @@ def create_generate_podcast_tool(
"podcast_id": podcast_id,
"title": podcast_title,
"file_location": file_location,
- "message": (
- "Podcast generated and saved to your podcast panel."
- ),
+ "message": ("Podcast generated and saved to your podcast panel."),
}
# Only other terminal state is FAILED.
@@ -146,9 +144,7 @@ def create_generate_podcast_tool(
"status": PodcastStatus.FAILED.value,
"podcast_id": podcast_id,
"title": podcast_title,
- "error": (
- "Background worker reported FAILED status for this podcast."
- ),
+ "error": ("Background worker reported FAILED status for this podcast."),
}
except Exception as e:
diff --git a/surfsense_backend/app/agents/new_chat/tools/video_presentation.py b/surfsense_backend/app/agents/new_chat/tools/video_presentation.py
index 4bf13b28e..34f5183ca 100644
--- a/surfsense_backend/app/agents/new_chat/tools/video_presentation.py
+++ b/surfsense_backend/app/agents/new_chat/tools/video_presentation.py
@@ -127,9 +127,7 @@ def create_generate_video_presentation_tool(
except Exception as e:
error_message = str(e)
- logger.exception(
- "[generate_video_presentation] Error: %s", error_message
- )
+ logger.exception("[generate_video_presentation] Error: %s", error_message)
return {
"status": VideoPresentationStatus.FAILED.value,
"error": error_message,
diff --git a/surfsense_backend/app/automations/actions/__init__.py b/surfsense_backend/app/automations/actions/__init__.py
index 9ef091cb3..72669532f 100644
--- a/surfsense_backend/app/automations/actions/__init__.py
+++ b/surfsense_backend/app/automations/actions/__init__.py
@@ -21,4 +21,4 @@ __all__ = [
]
# Built-in actions self-register at import time.
-from . import agent_task # noqa: E402, F401
+from . import agent_task # noqa: F401
diff --git a/surfsense_backend/app/automations/actions/agent_task/__init__.py b/surfsense_backend/app/automations/actions/agent_task/__init__.py
index 308812211..3a42a2815 100644
--- a/surfsense_backend/app/automations/actions/agent_task/__init__.py
+++ b/surfsense_backend/app/automations/actions/agent_task/__init__.py
@@ -12,4 +12,4 @@ from .params import AgentTaskActionParams
__all__ = ["AgentTaskActionParams", "build_handler"]
# Side-effect: register on the actions store.
-from . import definition # noqa: E402, F401
+from . import definition # noqa: F401
diff --git a/surfsense_backend/app/automations/actions/agent_task/invoke.py b/surfsense_backend/app/automations/actions/agent_task/invoke.py
index a37e9beed..6cc92b232 100644
--- a/surfsense_backend/app/automations/actions/agent_task/invoke.py
+++ b/surfsense_backend/app/automations/actions/agent_task/invoke.py
@@ -13,7 +13,6 @@ from app.agents.multi_agent_chat import create_multi_agent_chat_deep_agent
from app.db import ChatVisibility, async_session_maker
from ..types import ActionContext
-
from .auto_decide import build_auto_decisions
from .dependencies import build_dependencies
from .finalize import extract_final_assistant_message
diff --git a/surfsense_backend/app/automations/persistence/models/run.py b/surfsense_backend/app/automations/persistence/models/run.py
index 262e4c2bf..471b2df77 100644
--- a/surfsense_backend/app/automations/persistence/models/run.py
+++ b/surfsense_backend/app/automations/persistence/models/run.py
@@ -50,7 +50,7 @@ class AutomationRun(BaseModel, TimestampMixin):
definition_snapshot = Column(JSONB, nullable=False)
# merged & validated inputs the run was dispatched with
- # (trigger.static_inputs ∪ producer runtime data, static wins on collision)
+ # (trigger.static_inputs union producer runtime data, static wins on collision)
inputs = Column(JSONB, nullable=False, server_default="{}")
# one entry per executed step; agent_task entries carry their own
# `agent_session_id` inside their entry
diff --git a/surfsense_backend/app/automations/runtime/executor.py b/surfsense_backend/app/automations/runtime/executor.py
index b8a377e5b..6a33ab314 100644
--- a/surfsense_backend/app/automations/runtime/executor.py
+++ b/surfsense_backend/app/automations/runtime/executor.py
@@ -6,9 +6,9 @@ from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
+from app.automations.actions.types import ActionContext
from app.automations.persistence.enums.run_status import RunStatus
from app.automations.persistence.models.run import AutomationRun
-from app.automations.actions.types import ActionContext
from app.automations.schemas.definition.envelope import AutomationDefinition
from app.automations.schemas.definition.plan_step import PlanStep
from app.automations.templating import build_run_context
@@ -32,7 +32,10 @@ async def execute_run(session: AsyncSession, run_id: int) -> None:
await repository.mark_failed(
session,
run,
- {"message": f"definition_snapshot invalid: {exc}", "type": type(exc).__name__},
+ {
+ "message": f"definition_snapshot invalid: {exc}",
+ "type": type(exc).__name__,
+ },
)
await session.commit()
return
@@ -92,7 +95,9 @@ async def _run_on_failure(
await session.commit()
-def _build_template_ctx(run: AutomationRun, step_outputs: dict[str, Any]) -> dict[str, Any]:
+def _build_template_ctx(
+ run: AutomationRun, step_outputs: dict[str, Any]
+) -> dict[str, Any]:
automation = run.automation
trigger = run.trigger
return build_run_context(
diff --git a/surfsense_backend/app/automations/runtime/step.py b/surfsense_backend/app/automations/runtime/step.py
index ac18b5e1f..6e7c9c671 100644
--- a/surfsense_backend/app/automations/runtime/step.py
+++ b/surfsense_backend/app/automations/runtime/step.py
@@ -30,14 +30,18 @@ async def execute_step(
try:
should_run = evaluate_predicate(step.when, template_context)
except Exception as exc:
- return _result(step, "failed", started_at, attempts=0, error=_error(exc, "when"))
+ return _result(
+ step, "failed", started_at, attempts=0, error=_error(exc, "when")
+ )
if not should_run:
return _result(step, "skipped", started_at, attempts=0)
try:
resolved_params = render_value(step.params, template_context)
except Exception as exc:
- return _result(step, "failed", started_at, attempts=0, error=_error(exc, "render"))
+ return _result(
+ step, "failed", started_at, attempts=0, error=_error(exc, "render")
+ )
action = get_action(step.action)
if action is None:
@@ -46,12 +50,17 @@ async def execute_step(
"failed",
started_at,
attempts=0,
- error={"message": f"action not registered: {step.action}", "type": "ActionNotFound"},
+ error={
+ "message": f"action not registered: {step.action}",
+ "type": "ActionNotFound",
+ },
)
handler = action.build_handler(action_context)
- max_retries = step.max_retries if step.max_retries is not None else default_max_retries
+ max_retries = (
+ step.max_retries if step.max_retries is not None else default_max_retries
+ )
timeout = step.timeout_seconds or default_timeout_seconds
try:
@@ -62,7 +71,9 @@ async def execute_step(
timeout=timeout,
)
except Exception as exc:
- return _result(step, "failed", started_at, attempts=max_retries + 1, error=_error(exc))
+ return _result(
+ step, "failed", started_at, attempts=max_retries + 1, error=_error(exc)
+ )
return _result(step, "succeeded", started_at, attempts=attempts, result=result)
diff --git a/surfsense_backend/app/automations/schemas/definition/execution.py b/surfsense_backend/app/automations/schemas/definition/execution.py
index 61861f8d8..bdbad62f8 100644
--- a/surfsense_backend/app/automations/schemas/definition/execution.py
+++ b/surfsense_backend/app/automations/schemas/definition/execution.py
@@ -12,7 +12,9 @@ from .plan_step import PlanStep
class Execution(BaseModel):
model_config = ConfigDict(extra="forbid")
- timeout_seconds: int = Field(default=600, gt=0, description="Wall-clock cap for the run.")
+ timeout_seconds: int = Field(
+ default=600, gt=0, description="Wall-clock cap for the run."
+ )
max_retries: int = Field(default=2, ge=0, description="Per-step retry budget.")
retry_backoff: Literal["exponential", "linear", "none"] = "exponential"
concurrency: Literal["drop_if_running", "queue", "always"] = "drop_if_running"
diff --git a/surfsense_backend/app/automations/schemas/definition/plan_step.py b/surfsense_backend/app/automations/schemas/definition/plan_step.py
index 5d16f1f3e..0d3bb9dfc 100644
--- a/surfsense_backend/app/automations/schemas/definition/plan_step.py
+++ b/surfsense_backend/app/automations/schemas/definition/plan_step.py
@@ -11,7 +11,9 @@ class PlanStep(BaseModel):
model_config = ConfigDict(extra="forbid")
step_id: str = Field(..., min_length=1, description="Unique within the plan.")
- action: str = Field(..., min_length=1, description="Action type; resolved via registry.")
+ action: str = Field(
+ ..., min_length=1, description="Action type; resolved via registry."
+ )
when: str | None = Field(
default=None,
description="Optional predicate; step is skipped when falsy.",
diff --git a/surfsense_backend/app/automations/schemas/definition/trigger_spec.py b/surfsense_backend/app/automations/schemas/definition/trigger_spec.py
index a359a2f63..e6a995bbf 100644
--- a/surfsense_backend/app/automations/schemas/definition/trigger_spec.py
+++ b/surfsense_backend/app/automations/schemas/definition/trigger_spec.py
@@ -10,7 +10,9 @@ from pydantic import BaseModel, ConfigDict, Field
class TriggerSpec(BaseModel):
model_config = ConfigDict(extra="forbid")
- type: str = Field(..., min_length=1, description="Trigger type; resolved via registry.")
+ type: str = Field(
+ ..., min_length=1, description="Trigger type; resolved via registry."
+ )
params: dict[str, Any] = Field(
default_factory=dict,
description="Type-specific params; validated against the trigger's schema.",
diff --git a/surfsense_backend/app/automations/services/automation.py b/surfsense_backend/app/automations/services/automation.py
index 9140da3b5..0d2937e0e 100644
--- a/surfsense_backend/app/automations/services/automation.py
+++ b/surfsense_backend/app/automations/services/automation.py
@@ -10,14 +10,14 @@ from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
+from app.automations.persistence.enums.trigger_type import TriggerType
+from app.automations.persistence.models.automation import Automation
+from app.automations.persistence.models.trigger import AutomationTrigger
from app.automations.schemas.api import (
AutomationCreate,
AutomationUpdate,
TriggerCreate,
)
-from app.automations.persistence.enums.trigger_type import TriggerType
-from app.automations.persistence.models.automation import Automation
-from app.automations.persistence.models.trigger import AutomationTrigger
from app.automations.triggers import get_trigger
from app.automations.triggers.schedule import compute_next_fire_at
from app.db import Permission, User, get_async_session
@@ -34,7 +34,9 @@ class AutomationService:
async def create(self, payload: AutomationCreate) -> Automation:
"""Create an automation and its initial triggers in one transaction."""
- await self._authorize(payload.search_space_id, Permission.AUTOMATIONS_CREATE.value)
+ await self._authorize(
+ payload.search_space_id, Permission.AUTOMATIONS_CREATE.value
+ )
automation = Automation(
search_space_id=payload.search_space_id,
@@ -67,22 +69,32 @@ class AutomationService:
)
rows = (
- await self.session.execute(
- base.order_by(Automation.created_at.desc()).limit(limit).offset(offset)
+ (
+ await self.session.execute(
+ base.order_by(Automation.created_at.desc())
+ .limit(limit)
+ .offset(offset)
+ )
)
- ).scalars().all()
+ .scalars()
+ .all()
+ )
return list(rows), int(total or 0)
async def get(self, automation_id: int) -> Automation:
"""Get an automation with its triggers loaded."""
automation = await self._get_with_triggers_or_raise(automation_id)
- await self._authorize(automation.search_space_id, Permission.AUTOMATIONS_READ.value)
+ await self._authorize(
+ automation.search_space_id, Permission.AUTOMATIONS_READ.value
+ )
return automation
async def update(self, automation_id: int, patch: AutomationUpdate) -> Automation:
"""Patch fields. Bumps ``version`` when ``definition`` changes."""
automation = await self._get_with_triggers_or_raise(automation_id)
- await self._authorize(automation.search_space_id, Permission.AUTOMATIONS_UPDATE.value)
+ await self._authorize(
+ automation.search_space_id, Permission.AUTOMATIONS_UPDATE.value
+ )
data = patch.model_dump(exclude_unset=True)
@@ -93,7 +105,9 @@ class AutomationService:
if "status" in data:
automation.status = data["status"]
if "definition" in data:
- automation.definition = patch.definition.model_dump(mode="json", by_alias=True)
+ automation.definition = patch.definition.model_dump(
+ mode="json", by_alias=True
+ )
automation.version += 1
await self.session.commit()
@@ -102,7 +116,9 @@ class AutomationService:
async def delete(self, automation_id: int) -> None:
"""Delete an automation; FK cascades remove triggers and runs."""
automation = await self._get_or_raise(automation_id)
- await self._authorize(automation.search_space_id, Permission.AUTOMATIONS_DELETE.value)
+ await self._authorize(
+ automation.search_space_id, Permission.AUTOMATIONS_DELETE.value
+ )
await self.session.delete(automation)
await self.session.commit()
@@ -141,7 +157,9 @@ def _build_trigger(spec: TriggerCreate) -> AutomationTrigger:
"""Validate trigger params via its registered Pydantic model and build the ORM row."""
definition = get_trigger(spec.type.value)
if definition is None:
- raise HTTPException(status_code=422, detail=f"unknown trigger type {spec.type.value!r}")
+ raise HTTPException(
+ status_code=422, detail=f"unknown trigger type {spec.type.value!r}"
+ )
try:
validated = definition.params_model.model_validate(spec.params)
diff --git a/surfsense_backend/app/automations/services/run.py b/surfsense_backend/app/automations/services/run.py
index ac9970241..3ef80416f 100644
--- a/surfsense_backend/app/automations/services/run.py
+++ b/surfsense_backend/app/automations/services/run.py
@@ -36,10 +36,16 @@ class RunService:
)
rows = (
- await self.session.execute(
- base.order_by(AutomationRun.created_at.desc()).limit(limit).offset(offset)
+ (
+ await self.session.execute(
+ base.order_by(AutomationRun.created_at.desc())
+ .limit(limit)
+ .offset(offset)
+ )
)
- ).scalars().all()
+ .scalars()
+ .all()
+ )
return list(rows), int(total or 0)
async def get(self, *, automation_id: int, run_id: int) -> AutomationRun:
diff --git a/surfsense_backend/app/automations/services/trigger.py b/surfsense_backend/app/automations/services/trigger.py
index c76cc0740..29ac84557 100644
--- a/surfsense_backend/app/automations/services/trigger.py
+++ b/surfsense_backend/app/automations/services/trigger.py
@@ -8,10 +8,10 @@ from fastapi import Depends, HTTPException
from pydantic import ValidationError
from sqlalchemy.ext.asyncio import AsyncSession
-from app.automations.schemas.api import TriggerCreate, TriggerUpdate
from app.automations.persistence.enums.trigger_type import TriggerType
from app.automations.persistence.models.automation import Automation
from app.automations.persistence.models.trigger import AutomationTrigger
+from app.automations.schemas.api import TriggerCreate, TriggerUpdate
from app.automations.triggers import get_trigger
from app.automations.triggers.schedule import compute_next_fire_at
from app.db import Permission, User, get_async_session
@@ -40,7 +40,9 @@ class TriggerService:
params=validated_params,
static_inputs=payload.static_inputs,
enabled=payload.enabled,
- next_fire_at=_initial_next_fire(payload.type, validated_params, payload.enabled),
+ next_fire_at=_initial_next_fire(
+ payload.type, validated_params, payload.enabled
+ ),
)
self.session.add(trigger)
await self.session.commit()
@@ -54,7 +56,9 @@ class TriggerService:
trigger_id: int,
patch: TriggerUpdate,
) -> AutomationTrigger:
- await self._authorize_automation(automation_id, Permission.AUTOMATIONS_UPDATE.value)
+ await self._authorize_automation(
+ automation_id, Permission.AUTOMATIONS_UPDATE.value
+ )
trigger = await self._get_trigger_or_raise(automation_id, trigger_id)
data = patch.model_dump(exclude_unset=True)
@@ -80,7 +84,9 @@ class TriggerService:
return trigger
async def remove(self, *, automation_id: int, trigger_id: int) -> None:
- await self._authorize_automation(automation_id, Permission.AUTOMATIONS_UPDATE.value)
+ await self._authorize_automation(
+ automation_id, Permission.AUTOMATIONS_UPDATE.value
+ )
trigger = await self._get_trigger_or_raise(automation_id, trigger_id)
await self.session.delete(trigger)
await self.session.commit()
diff --git a/surfsense_backend/app/automations/tasks/execute_run.py b/surfsense_backend/app/automations/tasks/execute_run.py
index 5fc84698b..ed448515d 100644
--- a/surfsense_backend/app/automations/tasks/execute_run.py
+++ b/surfsense_backend/app/automations/tasks/execute_run.py
@@ -17,7 +17,7 @@ TASK_NAME = "automation_run_execute"
@celery_app.task(name=TASK_NAME, bind=True)
-def automation_run_execute(self, run_id: int) -> None: # noqa: ARG001 — Celery bind
+def automation_run_execute(self, run_id: int) -> None:
"""Execute one ``AutomationRun``. Idempotent: terminal runs no-op."""
return run_async_celery_task(lambda: _impl(run_id))
diff --git a/surfsense_backend/app/automations/tasks/schedule_tick.py b/surfsense_backend/app/automations/tasks/schedule_tick.py
index 385bd7242..90fff66fc 100644
--- a/surfsense_backend/app/automations/tasks/schedule_tick.py
+++ b/surfsense_backend/app/automations/tasks/schedule_tick.py
@@ -103,9 +103,7 @@ async def _self_heal_null_next_fire(session: AsyncSession, *, now: datetime) ->
await session.commit()
-async def _claim_due_triggers(
- session: AsyncSession, *, now: datetime
-) -> list[_Claim]:
+async def _claim_due_triggers(session: AsyncSession, *, now: datetime) -> list[_Claim]:
"""Lock and advance due rows; return per-trigger fire context."""
stmt = (
select(AutomationTrigger)
diff --git a/surfsense_backend/app/automations/triggers/__init__.py b/surfsense_backend/app/automations/triggers/__init__.py
index d7abb6b5d..f630ebf6f 100644
--- a/surfsense_backend/app/automations/triggers/__init__.py
+++ b/surfsense_backend/app/automations/triggers/__init__.py
@@ -17,4 +17,4 @@ __all__ = [
]
# Built-in triggers self-register at import time.
-from . import schedule # noqa: E402, F401
+from . import schedule # noqa: F401
diff --git a/surfsense_backend/app/automations/triggers/schedule/__init__.py b/surfsense_backend/app/automations/triggers/schedule/__init__.py
index 5587692b9..92f478aac 100644
--- a/surfsense_backend/app/automations/triggers/schedule/__init__.py
+++ b/surfsense_backend/app/automations/triggers/schedule/__init__.py
@@ -15,4 +15,4 @@ __all__ = [
]
# Side-effect: register on the triggers store.
-from . import definition # noqa: E402, F401
+from . import definition # noqa: F401
diff --git a/surfsense_backend/app/automations/triggers/schedule/cron.py b/surfsense_backend/app/automations/triggers/schedule/cron.py
index 7155bab33..a8401e4a3 100644
--- a/surfsense_backend/app/automations/triggers/schedule/cron.py
+++ b/surfsense_backend/app/automations/triggers/schedule/cron.py
@@ -32,6 +32,10 @@ def compute_next_fire_at(cron: str, timezone: str, *, after: datetime) -> dateti
given timezone before evaluation so DST and IANA rules apply correctly.
"""
tz = ZoneInfo(timezone)
- base = after.astimezone(tz) if after.tzinfo else after.replace(tzinfo=UTC).astimezone(tz)
+ base = (
+ after.astimezone(tz)
+ if after.tzinfo
+ else after.replace(tzinfo=UTC).astimezone(tz)
+ )
nxt: datetime = croniter(cron, base).get_next(datetime)
return nxt.astimezone(UTC)
diff --git a/surfsense_backend/app/automations/triggers/schedule/params.py b/surfsense_backend/app/automations/triggers/schedule/params.py
index 21da84f68..f3945a1b8 100644
--- a/surfsense_backend/app/automations/triggers/schedule/params.py
+++ b/surfsense_backend/app/automations/triggers/schedule/params.py
@@ -10,7 +10,9 @@ from .cron import InvalidCronError, validate_cron
class ScheduleTriggerParams(BaseModel):
model_config = ConfigDict(extra="forbid")
- cron: str = Field(..., description="Five-field cron expression.", examples=["0 9 * * 1-5"])
+ cron: str = Field(
+ ..., description="Five-field cron expression.", examples=["0 9 * * 1-5"]
+ )
timezone: str = Field(..., description="IANA timezone.", examples=["Africa/Kigali"])
@model_validator(mode="after")
diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py
index ac880ded5..fe2e53268 100644
--- a/surfsense_backend/app/db.py
+++ b/surfsense_backend/app/db.py
@@ -2605,7 +2605,6 @@ from app.automations.persistence import ( # noqa: E402, F401
AutomationTrigger,
)
-
engine = create_async_engine(
DATABASE_URL,
pool_size=30,
diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py
index ef1c9312a..48a095456 100644
--- a/surfsense_backend/app/routes/__init__.py
+++ b/surfsense_backend/app/routes/__init__.py
@@ -1,5 +1,7 @@
from fastapi import APIRouter
+from app.automations.api import router as automations_router
+
from .agent_action_log_route import router as agent_action_log_router
from .agent_flags_route import router as agent_flags_router
from .agent_permissions_route import router as agent_permissions_router
@@ -7,7 +9,6 @@ from .agent_revert_route import router as agent_revert_router
from .airtable_add_connector_route import (
router as airtable_add_connector_router,
)
-from app.automations.api import router as automations_router
from .chat_comments_routes import router as chat_comments_router
from .circleback_webhook_route import router as circleback_webhook_router
from .clickup_add_connector_route import router as clickup_add_connector_router
diff --git a/surfsense_backend/app/tasks/chat/streaming/context/deepagents_todos.py b/surfsense_backend/app/tasks/chat/streaming/context/deepagents_todos.py
index 0bbf4f0a5..b9cbf6506 100644
--- a/surfsense_backend/app/tasks/chat/streaming/context/deepagents_todos.py
+++ b/surfsense_backend/app/tasks/chat/streaming/context/deepagents_todos.py
@@ -19,9 +19,7 @@ def extract_todos_from_deepagents(command_output: Any) -> dict:
elif isinstance(command_output, dict):
if "todos" in command_output:
todos_data = command_output.get("todos", [])
- elif "update" in command_output and isinstance(
- command_output["update"], dict
- ):
+ elif "update" in command_output and isinstance(command_output["update"], dict):
todos_data = command_output["update"].get("todos", [])
return {"todos": todos_data}
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/auto_pin.py b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/auto_pin.py
index cb20eb011..af496cee7 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/auto_pin.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/auto_pin.py
@@ -69,17 +69,13 @@ async def resolve_initial_auto_pin(
"pin.requires_image_input": requires_image_input,
},
)
- return AutoPinResult(
- llm_config_id=pinned.resolved_llm_config_id, error=None
- )
+ return AutoPinResult(llm_config_id=pinned.resolved_llm_config_id, error=None)
except ValueError as pin_error:
# The "no vision-capable cfg" path raises a ValueError whose message
# we map to the friendly image-input SSE error so the user sees the
# same message regardless of whether the gate fired in the resolver or
# in ``llm_capability.assert_vision_capability_for_image_turn``.
- is_vision_failure = (
- requires_image_input and "vision-capable" in str(pin_error)
- )
+ is_vision_failure = requires_image_input and "vision-capable" in str(pin_error)
error_code = (
"MODEL_DOES_NOT_SUPPORT_IMAGE_INPUT"
if is_vision_failure
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/input_state.py b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/input_state.py
index fb171c244..f508571b0 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/input_state.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/input_state.py
@@ -207,9 +207,7 @@ async def _resolve_mentions_for_query(
try:
chip_objs.append(MentionedDocumentInfo.model_validate(raw))
except Exception:
- logger.debug(
- "stream_new_chat: dropping malformed mention chip %r", raw
- )
+ logger.debug("stream_new_chat: dropping malformed mention chip %r", raw)
resolved = await resolve_mentions(
session,
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/llm_capability.py b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/llm_capability.py
index ff5a56eec..9f4e5d2d8 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/llm_capability.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/llm_capability.py
@@ -48,9 +48,7 @@ def check_image_input_capability(
return None
model_label = agent_config.config_name or agent_config.model_name or "model"
- ot.add_event(
- "quota.denied", {"quota.code": "MODEL_DOES_NOT_SUPPORT_IMAGE_INPUT"}
- )
+ ot.add_event("quota.denied", {"quota.code": "MODEL_DOES_NOT_SUPPORT_IMAGE_INPUT"})
return (
(
f"The selected model ({model_label}) does not support "
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/orchestrator.py b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/orchestrator.py
index bca72b5ea..6d0853502 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/orchestrator.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/orchestrator.py
@@ -259,7 +259,8 @@ async def stream_new_chat(
if needs_premium_quota(agent_config, user_id):
premium_reservation = await reserve_premium(
- agent_config=agent_config, user_id=user_id # type: ignore[arg-type]
+ agent_config=agent_config,
+ user_id=user_id, # type: ignore[arg-type]
)
if not premium_reservation.allowed:
ot.add_event("quota.denied", {"quota.code": "PREMIUM_QUOTA_EXHAUSTED"})
@@ -492,7 +493,9 @@ async def stream_new_chat(
# --- Block 4: First SSE frames ---
- for sse in iter_initial_frames(streaming_service, turn_id=stream_result.turn_id):
+ for sse in iter_initial_frames(
+ streaming_service, turn_id=stream_result.turn_id
+ ):
yield sse
# --- Block 5: Persistence join + message-id frames ---
@@ -693,7 +696,9 @@ async def stream_new_chat(
fallback_commit_search_space_id=search_space_id,
fallback_commit_created_by_id=user_id,
fallback_commit_filesystem_mode=(
- filesystem_selection.mode if filesystem_selection else FilesystemMode.CLOUD
+ filesystem_selection.mode
+ if filesystem_selection
+ else FilesystemMode.CLOUD
),
fallback_commit_thread_id=chat_id,
runtime_context=runtime_context,
@@ -715,11 +720,7 @@ async def stream_new_chat(
title_emitted = True
# Account for the case where the task completed but produced no
# title — flip the flag anyway so we don't keep checking it.
- if (
- title_task is not None
- and title_task.done()
- and not title_emitted
- ):
+ if title_task is not None and title_task.done() and not title_emitted:
title_emitted = True
_perf_log.info(
@@ -811,9 +812,7 @@ async def stream_new_chat(
end_turn(str(chat_id))
if premium_reservation is not None and user_id:
- await release_premium(
- reservation=premium_reservation, user_id=user_id
- )
+ await release_premium(reservation=premium_reservation, user_id=user_id)
await close_session_and_clear_ai_responding(session, chat_id)
@@ -852,9 +851,9 @@ async def stream_new_chat(
# Break circular refs held by the agent graph, tools, and LLM
# wrappers so the GC can reclaim them in a single pass.
- agent = llm = connector_service = None # noqa: F841
- input_state = stream_result = None # noqa: F841
- session = None # noqa: F841
+ agent = llm = connector_service = None
+ input_state = stream_result = None
+ session = None
run_gc_pass(log_prefix="stream_new_chat", chat_id=chat_id)
close_chat_request_span(
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/runtime_context.py b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/runtime_context.py
index 1f11be1fe..cf1e8c3fb 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/runtime_context.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/runtime_context.py
@@ -30,9 +30,7 @@ def build_new_chat_runtime_context(
return SurfSenseContextSchema(
search_space_id=search_space_id,
mentioned_document_ids=list(mentioned_document_ids or []),
- mentioned_folder_ids=list(
- accepted_folder_ids or mentioned_folder_ids or []
- ),
+ mentioned_folder_ids=list(accepted_folder_ids or mentioned_folder_ids or []),
request_id=request_id,
turn_id=turn_id,
)
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/title_gen.py b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/title_gen.py
index 11312110f..7db45941b 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/title_gen.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/new_chat/title_gen.py
@@ -133,12 +133,8 @@ async def _generate_title(
# inherited Azure endpoint — see ``provider_api_base`` for the
# same bug repro on the image-gen / vision paths.
raw_model = getattr(llm, "model", "") or ""
- provider_prefix = (
- raw_model.split("/", 1)[0] if "/" in raw_model else None
- )
- provider_value = (
- agent_config.provider if agent_config is not None else None
- )
+ provider_prefix = raw_model.split("/", 1)[0] if "/" in raw_model else None
+ provider_value = agent_config.provider if agent_config is not None else None
title_api_base = resolve_api_base(
provider=provider_value,
provider_prefix=provider_prefix,
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/orchestrator.py b/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/orchestrator.py
index b67ac987e..e1b95aa63 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/orchestrator.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/orchestrator.py
@@ -15,14 +15,10 @@ building blocks under ``flows/shared/``. Mirrors ``stream_new_chat`` but:
from __future__ import annotations
import contextlib
-import gc
import logging
-import sys
import time
-import uuid as _uuid
from collections.abc import AsyncGenerator
from functools import partial
-from typing import Any
from uuid import UUID
import anyio
@@ -32,7 +28,7 @@ from app.agents.new_chat.chat_deepagent import create_surfsense_deep_agent
from app.agents.new_chat.filesystem_selection import FilesystemMode, FilesystemSelection
from app.agents.new_chat.middleware.busy_mutex import end_turn
from app.config import config as _app_config
-from app.db import ChatVisibility, async_session_maker, shielded_async_session
+from app.db import ChatVisibility, async_session_maker
from app.observability import otel as ot
from app.services.chat_session_state_service import set_ai_responding
from app.services.new_streaming_service import VercelStreamingService
@@ -89,7 +85,7 @@ from app.tasks.chat.streaming.flows.shared.terminal_error import (
)
from app.tasks.chat.streaming.shared.stream_result import StreamResult
from app.tasks.chat.streaming.shared.utils import resume_step_prefix
-from app.utils.perf import get_perf_logger, log_system_snapshot
+from app.utils.perf import get_perf_logger
logger = logging.getLogger(__name__)
_perf_log = get_perf_logger()
@@ -217,12 +213,11 @@ async def stream_resume_chat(
if needs_premium_quota(agent_config, user_id):
premium_reservation = await reserve_premium(
- agent_config=agent_config, user_id=user_id # type: ignore[arg-type]
+ agent_config=agent_config,
+ user_id=user_id, # type: ignore[arg-type]
)
if not premium_reservation.allowed:
- ot.add_event(
- "quota.denied", {"quota.code": "PREMIUM_QUOTA_EXHAUSTED"}
- )
+ ot.add_event("quota.denied", {"quota.code": "PREMIUM_QUOTA_EXHAUSTED"})
if requested_llm_config_id == 0:
try:
pinned_fb = await resolve_or_get_pinned_llm_config_id(
@@ -396,7 +391,9 @@ async def stream_resume_chat(
# --- First SSE frames ---
- for sse in iter_initial_frames(streaming_service, turn_id=stream_result.turn_id):
+ for sse in iter_initial_frames(
+ streaming_service, turn_id=stream_result.turn_id
+ ):
yield sse
# --- Assistant-shell persistence + id frame ---
@@ -517,7 +514,9 @@ async def stream_resume_chat(
fallback_commit_search_space_id=search_space_id,
fallback_commit_created_by_id=user_id,
fallback_commit_filesystem_mode=(
- filesystem_selection.mode if filesystem_selection else FilesystemMode.CLOUD
+ filesystem_selection.mode
+ if filesystem_selection
+ else FilesystemMode.CLOUD
),
fallback_commit_thread_id=chat_id,
runtime_context=runtime_context,
@@ -589,9 +588,7 @@ async def stream_resume_chat(
end_turn(str(chat_id))
if premium_reservation is not None and user_id:
- await release_premium(
- reservation=premium_reservation, user_id=user_id
- )
+ await release_premium(reservation=premium_reservation, user_id=user_id)
await close_session_and_clear_ai_responding(session, chat_id)
@@ -609,13 +606,11 @@ async def stream_resume_chat(
if not busy_error_raised:
with contextlib.suppress(Exception):
end_turn(str(chat_id))
- _perf_log.info(
- "[stream_resume] end_turn cleanup (chat_id=%s)", chat_id
- )
+ _perf_log.info("[stream_resume] end_turn cleanup (chat_id=%s)", chat_id)
- agent = llm = connector_service = None # noqa: F841
- stream_result = None # noqa: F841
- session = None # noqa: F841
+ agent = llm = connector_service = None
+ stream_result = None
+ session = None
run_gc_pass(log_prefix="stream_resume", chat_id=chat_id)
close_chat_request_span(
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/resume_routing.py b/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/resume_routing.py
index 300fbc9bd..7f4f67aac 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/resume_routing.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/resume_chat/resume_routing.py
@@ -47,9 +47,7 @@ async def build_resume_routing(
slice_decisions_by_tool_call,
)
- parent_state = await agent.aget_state(
- {"configurable": {"thread_id": str(chat_id)}}
- )
+ parent_state = await agent.aget_state({"configurable": {"thread_id": str(chat_id)}})
pending = collect_pending_tool_calls(parent_state)
_perf_log.info(
"[hitl_route] resume_entry chat_id=%s decisions=%d pending_subagents=%d",
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/shared/assistant_finalize.py b/surfsense_backend/app/tasks/chat/streaming/flows/shared/assistant_finalize.py
index d16f81ac7..be1f102f3 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/shared/assistant_finalize.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/shared/assistant_finalize.py
@@ -49,9 +49,7 @@ async def finalize_assistant_message(
was never assigned.
"""
if not (
- stream_result
- and stream_result.turn_id
- and stream_result.assistant_message_id
+ stream_result and stream_result.turn_id and stream_result.assistant_message_id
):
return
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/shared/finally_cleanup.py b/surfsense_backend/app/tasks/chat/streaming/flows/shared/finally_cleanup.py
index 8d425402f..f9454775e 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/shared/finally_cleanup.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/shared/finally_cleanup.py
@@ -39,9 +39,7 @@ async def close_session_and_clear_ai_responding(
async with shielded_async_session() as fresh_session:
await clear_ai_responding(fresh_session, chat_id)
except Exception:
- logger.warning(
- "Failed to clear AI responding state for thread %s", chat_id
- )
+ logger.warning("Failed to clear AI responding state for thread %s", chat_id)
with contextlib.suppress(Exception):
session.expunge_all()
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/shared/premium_quota.py b/surfsense_backend/app/tasks/chat/streaming/flows/shared/premium_quota.py
index 0ec40d275..cbf44764c 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/shared/premium_quota.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/shared/premium_quota.py
@@ -41,9 +41,7 @@ class PremiumReservation:
allowed: bool
-def needs_premium_quota(
- agent_config: AgentConfig | None, user_id: str | None
-) -> bool:
+def needs_premium_quota(agent_config: AgentConfig | None, user_id: str | None) -> bool:
return bool(agent_config is not None and user_id and agent_config.is_premium)
@@ -61,8 +59,10 @@ async def reserve_premium(
request_id = _uuid.uuid4().hex[:16]
litellm_params = agent_config.litellm_params or {}
base_model = (
- litellm_params.get("base_model") if isinstance(litellm_params, dict) else None
- ) or agent_config.model_name or ""
+ (litellm_params.get("base_model") if isinstance(litellm_params, dict) else None)
+ or agent_config.model_name
+ or ""
+ )
reserve_amount_micros = estimate_call_reserve_micros(
base_model=base_model,
quota_reserve_tokens=agent_config.quota_reserve_tokens,
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/shared/span.py b/surfsense_backend/app/tasks/chat/streaming/flows/shared/span.py
index 1e5169af1..74b9682ed 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/shared/span.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/shared/span.py
@@ -6,8 +6,7 @@ import contextlib
import sys
from typing import Any, Literal
-from app.observability import metrics as ot_metrics
-from app.observability import otel as ot
+from app.observability import metrics as ot_metrics, otel as ot
def open_chat_request_span(
diff --git a/surfsense_backend/app/tasks/chat/streaming/flows/shared/terminal_error.py b/surfsense_backend/app/tasks/chat/streaming/flows/shared/terminal_error.py
index c9db2caf2..b305dba23 100644
--- a/surfsense_backend/app/tasks/chat/streaming/flows/shared/terminal_error.py
+++ b/surfsense_backend/app/tasks/chat/streaming/flows/shared/terminal_error.py
@@ -15,8 +15,7 @@ from collections.abc import Iterator
from typing import Any, Literal
from app.agents.new_chat.errors import BusyError
-from app.observability import metrics as ot_metrics
-from app.observability import otel as ot
+from app.observability import metrics as ot_metrics, otel as ot
from app.services.new_streaming_service import VercelStreamingService
from app.tasks.chat.streaming.errors.classifier import classify_stream_exception
from app.tasks.chat.streaming.errors.emitter import emit_stream_terminal_error
diff --git a/surfsense_backend/tests/unit/automations/actions/agent_task/test_finalize.py b/surfsense_backend/tests/unit/automations/actions/agent_task/test_finalize.py
index bd49d764c..aa6c74549 100644
--- a/surfsense_backend/tests/unit/automations/actions/agent_task/test_finalize.py
+++ b/surfsense_backend/tests/unit/automations/actions/agent_task/test_finalize.py
@@ -72,7 +72,11 @@ def test_extract_returns_none_when_no_assistant_text_is_present() -> None:
anything?" rather than guess whether ``""`` means silence or empty
output. Empty-string contents are normalized to ``None`` too."""
no_ai = {"messages": [HumanMessage(content="just a question")]}
- only_tools = {"messages": [AIMessage(content=[{"type": "tool_use", "name": "x", "input": {}}])]}
+ only_tools = {
+ "messages": [
+ AIMessage(content=[{"type": "tool_use", "name": "x", "input": {}}])
+ ]
+ }
empty_string = {"messages": [AIMessage(content=" ")]}
assert extract_final_assistant_message(no_ai) is None
diff --git a/surfsense_backend/tests/unit/automations/runtime/test_retries.py b/surfsense_backend/tests/unit/automations/runtime/test_retries.py
index f0f12ca59..05fd02ab6 100644
--- a/surfsense_backend/tests/unit/automations/runtime/test_retries.py
+++ b/surfsense_backend/tests/unit/automations/runtime/test_retries.py
@@ -33,7 +33,9 @@ async def test_with_retries_returns_result_and_attempts_one_on_first_success() -
assert calls == 1
-async def test_with_retries_returns_attempt_count_when_succeeding_after_failures() -> None:
+async def test_with_retries_returns_attempt_count_when_succeeding_after_failures() -> (
+ None
+):
"""A coroutine that fails twice then succeeds returns ``attempts=3``
(the actual attempt that produced the result). Locks the contract
that the caller can distinguish first-try success from a recovery."""
diff --git a/surfsense_backend/tests/unit/automations/schemas/definition/test_envelope.py b/surfsense_backend/tests/unit/automations/schemas/definition/test_envelope.py
index c625b0ec9..d7b392a1d 100644
--- a/surfsense_backend/tests/unit/automations/schemas/definition/test_envelope.py
+++ b/surfsense_backend/tests/unit/automations/schemas/definition/test_envelope.py
@@ -11,7 +11,9 @@ from app.automations.schemas.definition.plan_step import PlanStep
pytestmark = pytest.mark.unit
-def test_automation_definition_accepts_minimal_valid_input_with_sensible_defaults() -> None:
+def test_automation_definition_accepts_minimal_valid_input_with_sensible_defaults() -> (
+ None
+):
"""A definition with just ``name`` + a one-step ``plan`` is valid and
fills in the rest with safe defaults so users don't have to write
out every section to get started."""
diff --git a/surfsense_backend/tests/unit/automations/templating/test_environment.py b/surfsense_backend/tests/unit/automations/templating/test_environment.py
index ec1c0ee40..64850c9c5 100644
--- a/surfsense_backend/tests/unit/automations/templating/test_environment.py
+++ b/surfsense_backend/tests/unit/automations/templating/test_environment.py
@@ -32,7 +32,9 @@ def test_environment_finalizes_datetime_output_to_iso_string() -> None:
when emitting ``inputs.fired_at`` and other datetime values."""
dt = datetime(2026, 5, 28, 14, 30, tzinfo=UTC)
- assert render_template("{{ moment }}", {"moment": dt}) == "2026-05-28T14:30:00+00:00"
+ assert (
+ render_template("{{ moment }}", {"moment": dt}) == "2026-05-28T14:30:00+00:00"
+ )
def test_environment_finalizes_none_output_to_empty_string() -> None:
diff --git a/surfsense_backend/tests/unit/automations/test_definition_types.py b/surfsense_backend/tests/unit/automations/test_definition_types.py
index 231e4fa97..2320b61d3 100644
--- a/surfsense_backend/tests/unit/automations/test_definition_types.py
+++ b/surfsense_backend/tests/unit/automations/test_definition_types.py
@@ -31,7 +31,7 @@ def test_action_definition_params_schema_reflects_params_model() -> None:
name="N",
description="D",
params_model=_Topic,
- build_handler=lambda _ctx: (lambda _p: {}), # type: ignore[arg-type,return-value]
+ build_handler=lambda _ctx: lambda _p: {}, # type: ignore[arg-type,return-value]
)
schema = definition.params_schema
diff --git a/surfsense_backend/tests/unit/automations/test_stores.py b/surfsense_backend/tests/unit/automations/test_stores.py
index e54062d64..d005d7be7 100644
--- a/surfsense_backend/tests/unit/automations/test_stores.py
+++ b/surfsense_backend/tests/unit/automations/test_stores.py
@@ -29,7 +29,9 @@ class _Params(BaseModel):
def _trigger(type_: str = "test_trigger") -> TriggerDefinition:
- return TriggerDefinition(type=type_, description="Test trigger.", params_model=_Params)
+ return TriggerDefinition(
+ type=type_, description="Test trigger.", params_model=_Params
+ )
def _action(type_: str = "test_action") -> ActionDefinition:
@@ -38,7 +40,7 @@ def _action(type_: str = "test_action") -> ActionDefinition:
name="Test",
description="Test action.",
params_model=_Params,
- build_handler=lambda _ctx: (lambda _p: {}), # type: ignore[arg-type,return-value]
+ build_handler=lambda _ctx: lambda _p: {}, # type: ignore[arg-type,return-value]
)
@@ -112,4 +114,4 @@ def test_all_triggers_returns_defensive_snapshot(
snapshot = all_triggers()
snapshot.pop("snapshot_test")
- assert get_trigger("snapshot_test") is not None
\ No newline at end of file
+ assert get_trigger("snapshot_test") is not None
diff --git a/surfsense_backend/tests/unit/automations/triggers/schedule/test_cron.py b/surfsense_backend/tests/unit/automations/triggers/schedule/test_cron.py
index 261e51b18..5c7580823 100644
--- a/surfsense_backend/tests/unit/automations/triggers/schedule/test_cron.py
+++ b/surfsense_backend/tests/unit/automations/triggers/schedule/test_cron.py
@@ -45,8 +45,12 @@ def test_compute_next_fire_at_respects_dst_offset_change() -> None:
winter_after = datetime(2026, 2, 15, 0, 0, tzinfo=UTC)
summer_after = datetime(2026, 4, 15, 0, 0, tzinfo=UTC)
- winter_fire = compute_next_fire_at("0 9 * * *", "America/New_York", after=winter_after)
- summer_fire = compute_next_fire_at("0 9 * * *", "America/New_York", after=summer_after)
+ winter_fire = compute_next_fire_at(
+ "0 9 * * *", "America/New_York", after=winter_after
+ )
+ summer_fire = compute_next_fire_at(
+ "0 9 * * *", "America/New_York", after=summer_after
+ )
assert winter_fire == datetime(2026, 2, 15, 14, 0, tzinfo=UTC)
assert summer_fire == datetime(2026, 4, 15, 13, 0, tzinfo=UTC)
diff --git a/surfsense_backend/tests/unit/tasks/chat/streaming/test_parallel_refactor_parity.py b/surfsense_backend/tests/unit/tasks/chat/streaming/test_parallel_refactor_parity.py
index eb24b4df8..ff4ca30df 100644
--- a/surfsense_backend/tests/unit/tasks/chat/streaming/test_parallel_refactor_parity.py
+++ b/surfsense_backend/tests/unit/tasks/chat/streaming/test_parallel_refactor_parity.py
@@ -33,7 +33,6 @@ import pytest
from app.agents.new_chat.context import SurfSenseContextSchema
from app.services.new_streaming_service import VercelStreamingService
-
from app.tasks.chat.stream_new_chat import (
stream_new_chat as old_stream_new_chat,
stream_resume_chat as old_stream_resume_chat,
@@ -152,7 +151,13 @@ class _FakeSurfsenseDoc:
"user_query, image_urls, docs, expected_title, expected_action",
[
("hello world", None, [], "Understanding your request", "Processing"),
- ("", ["data:image/png;base64,AAA"], [], "Understanding your request", "Processing"),
+ (
+ "",
+ ["data:image/png;base64,AAA"],
+ [],
+ "Understanding your request",
+ "Processing",
+ ),
("", None, [], "Understanding your request", "Processing"),
(
"doc question",
@@ -209,9 +214,10 @@ def test_initial_thinking_step_collapses_many_doc_names() -> None:
def test_image_capability_passes_without_images() -> None:
- assert check_image_input_capability(
- user_image_data_urls=None, agent_config=None
- ) is None
+ assert (
+ check_image_input_capability(user_image_data_urls=None, agent_config=None)
+ is None
+ )
def test_image_capability_passes_when_capability_unknown() -> None:
@@ -500,9 +506,7 @@ def test_can_recover_provider_rate_limit_rejects_non_rate_limit_exception() -> N
def test_spawn_set_ai_responding_bg_noop_without_user_id() -> None:
async def _run() -> set[asyncio.Task]:
background: set[asyncio.Task] = set()
- spawn_set_ai_responding_bg(
- chat_id=1, user_id=None, background_tasks=background
- )
+ spawn_set_ai_responding_bg(chat_id=1, user_id=None, background_tasks=background)
return background
bg = asyncio.run(_run())
diff --git a/surfsense_web/app/(home)/free/page.tsx b/surfsense_web/app/(home)/free/page.tsx
index 4512f3396..5cea9b6d2 100644
--- a/surfsense_web/app/(home)/free/page.tsx
+++ b/surfsense_web/app/(home)/free/page.tsx
@@ -221,10 +221,7 @@ export default async function FreeHubPage() {
{/* In-content ad: above the model table */}
-
By accessing or using the Service, you acknowledge that you have read and understood
- this Privacy Policy. If you do not agree with our policies and practices, do not use
- the Service. We may modify this policy from time to time; material changes will be
- reflected by updating the "Last updated" date above.
+ this Privacy Policy. If you do not agree with our policies and practices, do not use the
+ Service. We may modify this policy from time to time; material changes will be reflected
+ by updating the "Last updated" date above.
@@ -71,9 +71,9 @@ export default function PrivacyPolicy() {
Notion, Confluence, GitHub, and others) under the scopes you authorize.
- Billing Data includes information necessary to process payments
- (such as transaction identifiers and credit balances). Card details are handled by
- our payment processor and are not stored on our servers.
+ Billing Data includes information necessary to process payments (such
+ as transaction identifiers and credit balances). Card details are handled by our
+ payment processor and are not stored on our servers.
Technical Data includes internet protocol (IP) address, browser type
@@ -126,8 +126,8 @@ export default function PrivacyPolicy() {
incidents.
- To communicate with you about product updates, security notices, support requests,
- and (with your consent where required) marketing.
+ To communicate with you about product updates, security notices, support requests, and
+ (with your consent where required) marketing.
To serve and measure advertising on pages where ads are shown (currently, our free
@@ -141,8 +141,8 @@ export default function PrivacyPolicy() {
4. Cookies and Tracking Technologies
We and our partners use cookies, local storage, and similar technologies to operate the
- Service, remember your preferences, measure usage, and serve advertising. The
- categories include:
+ Service, remember your preferences, measure usage, and serve advertising. The categories
+ include:
@@ -179,9 +179,9 @@ export default function PrivacyPolicy() {
- Google, as a third-party vendor, uses cookies (including the DoubleClick DART
- cookie) to serve ads to you based on your visits to our Service and other websites
- on the Internet.
+ Google, as a third-party vendor, uses cookies (including the DoubleClick DART cookie)
+ to serve ads to you based on your visits to our Service and other websites on the
+ Internet.
Google's use of advertising cookies enables it and its partners to serve ads to you
@@ -195,14 +195,12 @@ export default function PrivacyPolicy() {
youronlinechoices.com (EU).
- For users in the European Economic Area, the United Kingdom, and Switzerland, we
- use a Google-certified Consent Management Platform to obtain your consent for
- personalized advertising before such cookies are set. You may change or withdraw
- your consent at any time through the consent banner.
-
-
- We do not knowingly serve personalized advertising to children. See Section 11.
+ For users in the European Economic Area, the United Kingdom, and Switzerland, we use a
+ Google-certified Consent Management Platform to obtain your consent for personalized
+ advertising before such cookies are set. You may change or withdraw your consent at
+ any time through the consent banner.
+
We do not knowingly serve personalized advertising to children. See Section 11.
For more information about how Google uses data when you use our Service, see{" "}
@@ -217,8 +215,8 @@ export default function PrivacyPolicy() {
6. Data Security
We implement technical and organizational measures designed to protect your personal
- data against accidental loss, unauthorized access, alteration, and disclosure. Access
- to personal data is limited to personnel who need it to operate the Service.
+ data against accidental loss, unauthorized access, alteration, and disclosure. Access to
+ personal data is limited to personnel who need it to operate the Service.
No system can be guaranteed to be fully secure. We cannot guarantee that personal data
@@ -232,10 +230,10 @@ export default function PrivacyPolicy() {
We retain personal data only for as long as necessary to provide the Service and to
comply with our legal, accounting, and reporting obligations. Account data is retained
- for the life of your account; you can request deletion at any time. Aggregated data
- that no longer identifies you may be retained indefinitely for analytics and product
- improvement purposes. Anonymous chat sessions on our free pages are not retained in
- any user-linked database.
+ for the life of your account; you can request deletion at any time. Aggregated data that
+ no longer identifies you may be retained indefinitely for analytics and product
+ improvement purposes. Anonymous chat sessions on our free pages are not retained in any
+ user-linked database.
@@ -243,8 +241,7 @@ export default function PrivacyPolicy() {
8. Third-Party Services
We rely on the following categories of third-party processors and providers to operate
- the Service. Each is bound by its own privacy policy, which we encourage you to
- review:
+ the Service. Each is bound by its own privacy policy, which we encourage you to review:
@@ -261,9 +258,9 @@ export default function PrivacyPolicy() {
Advertising: Google AdSense (see Section 5).
- Large language model providers: OpenAI, Anthropic, Google, and
- other LLM providers process the prompts and content you submit to the Service in
- order to generate responses.
+ Large language model providers: OpenAI, Anthropic, Google, and other
+ LLM providers process the prompts and content you submit to the Service in order to
+ generate responses.
Integration providers: When you explicitly connect a third-party
@@ -278,9 +275,7 @@ export default function PrivacyPolicy() {
-
- 9. Your Legal Rights (Including GDPR)
-
+
9. Your Legal Rights (Including GDPR)
Subject to applicable law, you have the following rights in relation to your personal
data:
@@ -314,17 +309,17 @@ export default function PrivacyPolicy() {
- The right to know what categories of personal information we have collected about
- you and how it is used and shared.
+ The right to know what categories of personal information we have collected about you
+ and how it is used and shared.
The right to delete personal information we have collected from you.
The right to correct inaccurate personal information.
The right to opt out of the "sale" or "sharing" of personal information for
cross-context behavioral advertising. We do not sell personal data; however,
- advertising cookies set by Google AdSense may be considered "sharing" under
- California law. To opt out, you can use the consent controls described in Section 5
- or enable a Global Privacy Control (GPC) signal in your browser, which we honor.
+ advertising cookies set by Google AdSense may be considered "sharing" under California
+ law. To opt out, you can use the consent controls described in Section 5 or enable a
+ Global Privacy Control (GPC) signal in your browser, which we honor.
The right not to be discriminated against for exercising your privacy rights.
@@ -337,33 +332,32 @@ export default function PrivacyPolicy() {
11. Children's Privacy
The Service is not directed to children under 13 (or under 16 in the EEA, UK, and
- Switzerland). We do not knowingly collect personal data from children. If you believe
- a child has provided us with personal data, please contact us and we will take steps
- to delete it. We do not knowingly serve personalized advertising to children.
+ Switzerland). We do not knowingly collect personal data from children. If you believe a
+ child has provided us with personal data, please contact us and we will take steps to
+ delete it. We do not knowingly serve personalized advertising to children.
12. Changes to This Policy
- We may update this Privacy Policy from time to time to reflect changes in our
- practices, technology, legal requirements, or for other operational reasons. When we
- make material changes, we will update the "Last updated" date at the top of this page
- and, where appropriate, provide additional notice (such as an in-product notification
- or email). Your continued use of the Service after the updated policy becomes
- effective constitutes your acceptance of the revised policy.
+ We may update this Privacy Policy from time to time to reflect changes in our practices,
+ technology, legal requirements, or for other operational reasons. When we make material
+ changes, we will update the "Last updated" date at the top of this page and, where
+ appropriate, provide additional notice (such as an in-product notification or email).
+ Your continued use of the Service after the updated policy becomes effective constitutes
+ your acceptance of the revised policy.
13. Contact Us
- If you have questions about this Privacy Policy or our privacy practices, or if you
- want to exercise any of your rights, please contact us at:
+ If you have questions about this Privacy Policy or our privacy practices, or if you want
+ to exercise any of your rights, please contact us at: