Skip to content

Commit b1b51ad

Browse files
authored
Merge pull request #1450 from MODSetter/dev_mod
feat: updated automations UI
2 parents 7972901 + 80daf46 commit b1b51ad

18 files changed

Lines changed: 1156 additions & 308 deletions

File tree

surfsense_backend/app/agents/multi_agent_chat/main_agent/tools/automation/create.py

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@
3232
)
3333
from app.automations.schemas.api import AutomationCreate
3434
from app.automations.services.automation import AutomationService
35-
from app.automations.services.model_policy import get_automation_model_eligibility
36-
from app.db import SearchSpace, User, async_session_maker
35+
from app.db import User, async_session_maker
3736
from app.utils.content_utils import extract_text_content
3837

3938
from .prompt import build_draft_prompt
@@ -99,26 +98,11 @@ async def create_automation(intent: str, runtime: ToolRuntime) -> dict[str, Any]
9998
declined. Acknowledge once and stop — do NOT retry or pitch
10099
variants without a fresh user request.
101100
"""
102-
# --- 0. Eligibility gate (fail fast, before drafting + HITL) ---
103-
# Automations may only use premium or BYOK models. Check up front so we
104-
# don't make the user draft + approve a card that can't be saved.
105-
async with async_session_maker() as session:
106-
search_space = await session.get(SearchSpace, search_space_id)
107-
if search_space is None:
108-
return {
109-
"status": "error",
110-
"message": "search space not found in this session",
111-
}
112-
eligibility = get_automation_model_eligibility(search_space)
113-
if not eligibility["allowed"]:
114-
reasons = " ".join(v["reason"] for v in eligibility["violations"])
115-
return {
116-
"status": "error",
117-
"message": (
118-
f"{reasons} Update the search space's model settings to a "
119-
"premium or your own (BYOK) model, then try again."
120-
),
121-
}
101+
# Models are chosen per-automation on the approval card (premium/BYOK
102+
# selectors) and validated when persisted by ``AutomationService.create``
103+
# — so there's no fail-fast search-space eligibility gate here. The
104+
# search space's current chat/role model selection no longer constrains
105+
# whether an automation can be drafted or saved.
122106

123107
# --- 1. Draft via sub-LLM ---
124108
prompt = build_draft_prompt(search_space_id=search_space_id, intent=intent)

surfsense_backend/app/automations/services/automation.py

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from app.automations.services.model_policy import (
2323
AutomationModelPolicyError,
2424
assert_automation_models_billable,
25+
assert_models_billable,
2526
get_automation_model_eligibility,
2627
)
2728
from app.automations.triggers import get_trigger
@@ -43,16 +44,23 @@ async def create(self, payload: AutomationCreate) -> Automation:
4344
await self._authorize(
4445
payload.search_space_id, Permission.AUTOMATIONS_CREATE.value
4546
)
46-
search_space = await self._assert_models_billable(payload.search_space_id)
47-
48-
# Snapshot the search space's current (already-validated) model prefs onto
49-
# the definition so runs are insulated from later chat/search-space model
50-
# changes. Captured ids are guaranteed billable by the check above.
51-
payload.definition.models = AutomationModels(
52-
agent_llm_id=search_space.agent_llm_id or 0,
53-
image_generation_config_id=search_space.image_generation_config_id or 0,
54-
vision_llm_config_id=search_space.vision_llm_config_id or 0,
55-
)
47+
48+
# Capture the model profile onto the definition so runs are insulated
49+
# from later chat/search-space model changes. Two sources:
50+
# 1. Explicit per-automation selection in ``payload.definition.models``
51+
# (manual builder + chat approval card). Validate the chosen ids.
52+
# 2. Fallback (no selection): snapshot the search space's current prefs.
53+
# Either way the captured ids are guaranteed billable (premium/BYOK).
54+
selected_models = payload.definition.models
55+
if selected_models is not None:
56+
self._assert_selected_models_billable(selected_models)
57+
else:
58+
search_space = await self._assert_models_billable(payload.search_space_id)
59+
payload.definition.models = AutomationModels(
60+
agent_llm_id=search_space.agent_llm_id or 0,
61+
image_generation_config_id=search_space.image_generation_config_id or 0,
62+
vision_llm_config_id=search_space.vision_llm_config_id or 0,
63+
)
5664

5765
automation = Automation(
5866
search_space_id=payload.search_space_id,
@@ -122,13 +130,22 @@ async def update(self, automation_id: int, patch: AutomationUpdate) -> Automatio
122130
automation.status = data["status"]
123131
if "definition" in data:
124132
new_def = patch.definition.model_dump(mode="json", by_alias=True)
125-
# Preserve the captured model snapshot across edits so a definition
126-
# change never silently re-binds the automation to the current chat
127-
# model selection. Backend-managed; survives whether or not the
128-
# client round-trips ``models``.
133+
# Model snapshot handling on edit:
134+
# * absent in the patch -> preserve the captured snapshot
135+
# (a non-model definition change never silently re-binds the
136+
# automation to the current chat/search-space selection).
137+
# * unchanged from the snapshot -> keep as-is, no re-validation
138+
# (so editing an automation whose captured model later drifted
139+
# out of premium isn't blocked by an unrelated name/schedule edit).
140+
# * genuinely changed -> validate the new selection (422 on a
141+
# non-billable pick), then accept it.
129142
existing_models = (automation.definition or {}).get("models")
130-
if existing_models is not None:
131-
new_def["models"] = existing_models
143+
provided_models = new_def.get("models")
144+
if provided_models is None:
145+
if existing_models is not None:
146+
new_def["models"] = existing_models
147+
elif provided_models != existing_models:
148+
self._assert_selected_models_billable(patch.definition.models)
132149
automation.definition = new_def
133150
automation.version += 1
134151

@@ -199,6 +216,22 @@ async def _assert_models_billable(self, search_space_id: int) -> SearchSpace:
199216
raise HTTPException(status_code=422, detail=str(exc)) from exc
200217
return search_space
201218

219+
def _assert_selected_models_billable(self, models: AutomationModels) -> None:
220+
"""Reject creation when an explicitly selected model isn't billable.
221+
222+
Used when the client supplies ``definition.models`` (per-automation
223+
selection from the builder or chat approval card). Same policy as the
224+
search-space path: premium global or BYOK only, no free/Auto.
225+
"""
226+
try:
227+
assert_models_billable(
228+
agent_llm_id=models.agent_llm_id,
229+
image_generation_config_id=models.image_generation_config_id,
230+
vision_llm_config_id=models.vision_llm_config_id,
231+
)
232+
except AutomationModelPolicyError as exc:
233+
raise HTTPException(status_code=422, detail=str(exc)) from exc
234+
202235
async def _authorize(self, search_space_id: int, permission: str) -> None:
203236
await check_permission(
204237
self.session,

0 commit comments

Comments
 (0)