mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-12 20:45:20 +02:00
feat(web): expose model policies in automations
This commit is contained in:
parent
18606fe388
commit
32ab2b8713
7 changed files with 77 additions and 106 deletions
|
|
@ -130,7 +130,7 @@ export function AutomationBuilderForm({
|
|||
// data into state, so there's no flicker/loop and the user's pick is sticky.
|
||||
const resolvedModels = useMemo<BuilderModels>(
|
||||
() => ({
|
||||
agentLlmId: form.models.agentLlmId || eligibleModels.llm.defaultId || 0,
|
||||
chatModelId: form.models.chatModelId || eligibleModels.llm.defaultId || 0,
|
||||
imageConfigId: form.models.imageConfigId || eligibleModels.image.defaultId || 0,
|
||||
visionConfigId: form.models.visionConfigId || eligibleModels.vision.defaultId || 0,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { getProviderIcon } from "@/lib/provider-icons";
|
|||
import { Field } from "./form-field";
|
||||
|
||||
export interface AutomationModelSelection {
|
||||
agentLlmId: number;
|
||||
chatModelId: number;
|
||||
imageConfigId: number;
|
||||
visionConfigId: number;
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ interface AutomationModelFieldsProps {
|
|||
}
|
||||
|
||||
/**
|
||||
* Three eligible-only model pickers (Agent LLM / Image / Vision) for the
|
||||
* Three eligible-only model pickers (Chat / Image / Vision) for the
|
||||
* automation builder + chat approval card. Options come from
|
||||
* {@link useAutomationEligibleModels} (premium globals + BYOK only); selection
|
||||
* is validated + snapshotted onto `definition.models` at create time.
|
||||
|
|
@ -51,18 +51,18 @@ export function AutomationModelFields({
|
|||
errors,
|
||||
}: AutomationModelFieldsProps) {
|
||||
const { llm, image, vision, isLoading } = useAutomationEligibleModels();
|
||||
const rolesHref = `/dashboard/${searchSpaceId}/search-space-settings/roles`;
|
||||
const rolesHref = `/dashboard/${searchSpaceId}/search-space-settings/models`;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<ModelSelectField
|
||||
label="Agent model"
|
||||
label="Chat model"
|
||||
kind={llm}
|
||||
value={value.agentLlmId}
|
||||
value={value.chatModelId}
|
||||
isLoading={isLoading}
|
||||
rolesHref={rolesHref}
|
||||
error={errors?.agentLlmId}
|
||||
onChange={(id) => onChange({ agentLlmId: id })}
|
||||
error={errors?.chatModelId}
|
||||
onChange={(id) => onChange({ chatModelId: id })}
|
||||
/>
|
||||
<ModelSelectField
|
||||
label="Image model"
|
||||
|
|
|
|||
|
|
@ -57,9 +57,9 @@ export const createAutomationMutationAtom = atomWithMutation(() => ({
|
|||
task_count: variables.definition.plan.length,
|
||||
trigger_type: variables.triggers?.[0]?.type ?? "none",
|
||||
has_schedule: (variables.triggers?.length ?? 0) > 0,
|
||||
agent_llm_id: variables.definition.models?.agent_llm_id,
|
||||
image_generation_config_id: variables.definition.models?.image_generation_config_id,
|
||||
vision_llm_config_id: variables.definition.models?.vision_llm_config_id,
|
||||
chat_model_id: variables.definition.models?.chat_model_id,
|
||||
image_gen_model_id: variables.definition.models?.image_gen_model_id,
|
||||
vision_model_id: variables.definition.models?.vision_model_id,
|
||||
tags_count: variables.definition.metadata?.tags?.length,
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
|
|||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||
const eligibleModels = useAutomationEligibleModels();
|
||||
const [modelSelection, setModelSelection] = useState<AutomationModelSelection>({
|
||||
agentLlmId: 0,
|
||||
chatModelId: 0,
|
||||
imageConfigId: 0,
|
||||
visionConfigId: 0,
|
||||
});
|
||||
|
|
@ -121,7 +121,7 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
|
|||
// default. No effect seeds async hook data into state.
|
||||
const resolvedModels = useMemo<AutomationModelSelection>(
|
||||
() => ({
|
||||
agentLlmId: modelSelection.agentLlmId || eligibleModels.llm.defaultId || 0,
|
||||
chatModelId: modelSelection.chatModelId || eligibleModels.llm.defaultId || 0,
|
||||
imageConfigId: modelSelection.imageConfigId || eligibleModels.image.defaultId || 0,
|
||||
visionConfigId: modelSelection.visionConfigId || eligibleModels.vision.defaultId || 0,
|
||||
}),
|
||||
|
|
@ -133,7 +133,7 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
|
|||
]
|
||||
);
|
||||
const modelsResolved =
|
||||
resolvedModels.agentLlmId !== 0 &&
|
||||
resolvedModels.chatModelId !== 0 &&
|
||||
resolvedModels.imageConfigId !== 0 &&
|
||||
resolvedModels.visionConfigId !== 0;
|
||||
|
||||
|
|
@ -147,9 +147,9 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
|
|||
definition: {
|
||||
...baseDefinition,
|
||||
models: {
|
||||
agent_llm_id: resolvedModels.agentLlmId,
|
||||
image_generation_config_id: resolvedModels.imageConfigId,
|
||||
vision_llm_config_id: resolvedModels.visionConfigId,
|
||||
chat_model_id: resolvedModels.chatModelId,
|
||||
image_gen_model_id: resolvedModels.imageConfigId,
|
||||
vision_model_id: resolvedModels.visionConfigId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -162,9 +162,9 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
|
|||
trigger_type:
|
||||
(triggers[0] as { type?: string } | undefined)?.type ??
|
||||
(triggers.length ? undefined : "none"),
|
||||
agent_llm_id: resolvedModels.agentLlmId,
|
||||
image_generation_config_id: resolvedModels.imageConfigId,
|
||||
vision_llm_config_id: resolvedModels.visionConfigId,
|
||||
chat_model_id: resolvedModels.chatModelId,
|
||||
image_gen_model_id: resolvedModels.imageConfigId,
|
||||
vision_model_id: resolvedModels.visionConfigId,
|
||||
});
|
||||
onDecision({
|
||||
type: "edit",
|
||||
|
|
|
|||
|
|
@ -63,9 +63,9 @@ export type Inputs = z.infer<typeof inputs>;
|
|||
// Captured model snapshot (server-managed). Set at create time and preserved
|
||||
// across edits so runs are insulated from later chat/search-space model changes.
|
||||
export const automationModels = z.object({
|
||||
agent_llm_id: z.number().int().default(0),
|
||||
image_generation_config_id: z.number().int().default(0),
|
||||
vision_llm_config_id: z.number().int().default(0),
|
||||
chat_model_id: z.number().int().default(0),
|
||||
image_gen_model_id: z.number().int().default(0),
|
||||
vision_model_id: z.number().int().default(0),
|
||||
});
|
||||
export type AutomationModels = z.infer<typeof automationModels>;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,18 +3,11 @@
|
|||
import { useAtomValue } from "jotai";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
globalImageGenConfigsAtom,
|
||||
imageGenConfigsAtom,
|
||||
} from "@/atoms/image-gen-config/image-gen-config-query.atoms";
|
||||
import {
|
||||
globalNewLLMConfigsAtom,
|
||||
llmPreferencesAtom,
|
||||
newLLMConfigsAtom,
|
||||
} from "@/atoms/new-llm-config/new-llm-config-query.atoms";
|
||||
import {
|
||||
globalVisionLLMConfigsAtom,
|
||||
visionLLMConfigsAtom,
|
||||
} from "@/atoms/vision-llm-config/vision-llm-config-query.atoms";
|
||||
globalModelConnectionsAtom,
|
||||
modelConnectionsAtom,
|
||||
modelRolesAtom,
|
||||
} from "@/atoms/model-connections/model-connections-query.atoms";
|
||||
import type { ConnectionRead, ModelRead } from "@/contracts/types/model-connections.types";
|
||||
|
||||
/**
|
||||
* A single model the user may pick for an automation slot.
|
||||
|
|
@ -44,48 +37,40 @@ export interface AutomationEligibleModels {
|
|||
isLoading: boolean;
|
||||
}
|
||||
|
||||
interface GlobalConfigLike {
|
||||
id: number;
|
||||
name: string;
|
||||
model_name: string;
|
||||
provider: string;
|
||||
is_premium?: boolean;
|
||||
is_auto_mode?: boolean;
|
||||
}
|
||||
|
||||
interface UserConfigLike {
|
||||
id: number;
|
||||
name: string;
|
||||
model_name: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the eligible option list for one model kind: premium globals
|
||||
* (`is_premium === true`, never Auto mode) followed by all BYOK configs.
|
||||
* followed by all BYOK/search-space models.
|
||||
*/
|
||||
function buildKind(
|
||||
globals: GlobalConfigLike[] | undefined,
|
||||
byok: UserConfigLike[] | undefined,
|
||||
globals: ConnectionRead[] | undefined,
|
||||
byok: ConnectionRead[] | undefined,
|
||||
capability: "chat" | "image_gen" | "vision",
|
||||
prefId: number | null | undefined
|
||||
): EligibleModelKind {
|
||||
const premiumGlobals: EligibleModelOption[] = (globals ?? [])
|
||||
.filter((c) => c.is_premium === true && !c.is_auto_mode)
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
modelName: c.model_name,
|
||||
provider: c.provider,
|
||||
isBYOK: false,
|
||||
}));
|
||||
const toOption = (connection: ConnectionRead, model: ModelRead, isBYOK: boolean) => ({
|
||||
id: model.id,
|
||||
name: model.display_name || model.model_id,
|
||||
modelName: model.model_id,
|
||||
provider: connection.native_provider || connection.protocol,
|
||||
isBYOK,
|
||||
});
|
||||
|
||||
const byokOptions: EligibleModelOption[] = (byok ?? []).map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
modelName: c.model_name,
|
||||
provider: c.provider,
|
||||
isBYOK: true,
|
||||
}));
|
||||
const premiumGlobals: EligibleModelOption[] = (globals ?? []).flatMap((connection) =>
|
||||
connection.models
|
||||
.filter(
|
||||
(model) =>
|
||||
model.enabled &&
|
||||
Boolean(model.capabilities?.[capability]) &&
|
||||
String(model.billing_tier ?? "").toLowerCase() === "premium"
|
||||
)
|
||||
.map((model) => toOption(connection, model, false))
|
||||
);
|
||||
|
||||
const byokOptions: EligibleModelOption[] = (byok ?? []).flatMap((connection) =>
|
||||
connection.models
|
||||
.filter((model) => model.enabled && Boolean(model.capabilities?.[capability]))
|
||||
.map((model) => toOption(connection, model, true))
|
||||
);
|
||||
|
||||
const options = [...premiumGlobals, ...byokOptions];
|
||||
const byId = new Map<number, EligibleModelOption>(options.map((o) => [o.id, o]));
|
||||
|
|
@ -105,46 +90,32 @@ function buildKind(
|
|||
* (premium globals + user BYOK — never free globals or Auto mode), with a
|
||||
* default selection seeded from the search space's role preferences.
|
||||
*
|
||||
* Everything is derived during render from the existing config query atoms;
|
||||
* Everything is derived during render from the connection/model query atoms;
|
||||
* there are no effects, so option lists/maps keep stable references.
|
||||
*/
|
||||
export function useAutomationEligibleModels(): AutomationEligibleModels {
|
||||
const { data: llmUserConfigs, isLoading: llmUserLoading } = useAtomValue(newLLMConfigsAtom);
|
||||
const { data: llmGlobalConfigs, isLoading: llmGlobalLoading } =
|
||||
useAtomValue(globalNewLLMConfigsAtom);
|
||||
const { data: preferences, isLoading: prefsLoading } = useAtomValue(llmPreferencesAtom);
|
||||
const { data: imageGlobalConfigs, isLoading: imageGlobalLoading } =
|
||||
useAtomValue(globalImageGenConfigsAtom);
|
||||
const { data: imageUserConfigs, isLoading: imageUserLoading } = useAtomValue(imageGenConfigsAtom);
|
||||
const { data: visionGlobalConfigs, isLoading: visionGlobalLoading } = useAtomValue(
|
||||
globalVisionLLMConfigsAtom
|
||||
const { data: byokConnections, isLoading: byokLoading } = useAtomValue(modelConnectionsAtom);
|
||||
const { data: globalConnections, isLoading: globalLoading } = useAtomValue(
|
||||
globalModelConnectionsAtom
|
||||
);
|
||||
const { data: visionUserConfigs, isLoading: visionUserLoading } =
|
||||
useAtomValue(visionLLMConfigsAtom);
|
||||
const { data: roles, isLoading: rolesLoading } = useAtomValue(modelRolesAtom);
|
||||
|
||||
const llm = useMemo(
|
||||
() => buildKind(llmGlobalConfigs, llmUserConfigs, preferences?.agent_llm_id),
|
||||
[llmGlobalConfigs, llmUserConfigs, preferences?.agent_llm_id]
|
||||
() => buildKind(globalConnections, byokConnections, "chat", roles?.chat_model_id),
|
||||
[globalConnections, byokConnections, roles?.chat_model_id]
|
||||
);
|
||||
|
||||
const image = useMemo(
|
||||
() => buildKind(imageGlobalConfigs, imageUserConfigs, preferences?.image_generation_config_id),
|
||||
[imageGlobalConfigs, imageUserConfigs, preferences?.image_generation_config_id]
|
||||
() => buildKind(globalConnections, byokConnections, "image_gen", roles?.image_gen_model_id),
|
||||
[globalConnections, byokConnections, roles?.image_gen_model_id]
|
||||
);
|
||||
|
||||
const vision = useMemo(
|
||||
() => buildKind(visionGlobalConfigs, visionUserConfigs, preferences?.vision_llm_config_id),
|
||||
[visionGlobalConfigs, visionUserConfigs, preferences?.vision_llm_config_id]
|
||||
() => buildKind(globalConnections, byokConnections, "vision", roles?.vision_model_id),
|
||||
[globalConnections, byokConnections, roles?.vision_model_id]
|
||||
);
|
||||
|
||||
const isLoading =
|
||||
llmUserLoading ||
|
||||
llmGlobalLoading ||
|
||||
prefsLoading ||
|
||||
imageGlobalLoading ||
|
||||
imageUserLoading ||
|
||||
visionGlobalLoading ||
|
||||
visionUserLoading;
|
||||
const isLoading = byokLoading || globalLoading || rolesLoading;
|
||||
|
||||
return useMemo(() => ({ llm, image, vision, isLoading }), [llm, image, vision, isLoading]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export type BuilderExecution = z.infer<typeof builderExecutionSchema>;
|
|||
* later chat/search-space model changes.
|
||||
*/
|
||||
export const builderModelsSchema = z.object({
|
||||
agentLlmId: z.number().int(),
|
||||
chatModelId: z.number().int(),
|
||||
imageConfigId: z.number().int(),
|
||||
visionConfigId: z.number().int(),
|
||||
});
|
||||
|
|
@ -90,7 +90,7 @@ export const builderFormSchema = z.object({
|
|||
tags: z.array(z.string()),
|
||||
/** Carried through from an edited definition so we don't drop it. */
|
||||
goal: z.string().nullable(),
|
||||
/** Selected agent/image/vision models (``0`` = use the eligible default). */
|
||||
/** Selected chat/image/vision models (``0`` = use the eligible default). */
|
||||
models: builderModelsSchema,
|
||||
});
|
||||
export type BuilderForm = z.infer<typeof builderFormSchema>;
|
||||
|
|
@ -147,7 +147,7 @@ export function createEmptyForm(): BuilderForm {
|
|||
},
|
||||
tags: [],
|
||||
goal: null,
|
||||
models: { agentLlmId: 0, imageConfigId: 0, visionConfigId: 0 },
|
||||
models: { chatModelId: 0, imageConfigId: 0, visionConfigId: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -240,9 +240,9 @@ function buildDefinition(form: BuilderForm): AutomationDefinition {
|
|||
...(hasResolvedModels(form.models)
|
||||
? {
|
||||
models: {
|
||||
agent_llm_id: form.models.agentLlmId,
|
||||
image_generation_config_id: form.models.imageConfigId,
|
||||
vision_llm_config_id: form.models.visionConfigId,
|
||||
chat_model_id: form.models.chatModelId,
|
||||
image_gen_model_id: form.models.imageConfigId,
|
||||
vision_model_id: form.models.visionConfigId,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
|
@ -251,7 +251,7 @@ function buildDefinition(form: BuilderForm): AutomationDefinition {
|
|||
|
||||
/** True once every model slot holds a concrete (non-zero) id. */
|
||||
export function hasResolvedModels(models: BuilderModels): boolean {
|
||||
return models.agentLlmId !== 0 && models.imageConfigId !== 0 && models.visionConfigId !== 0;
|
||||
return models.chatModelId !== 0 && models.imageConfigId !== 0 && models.visionConfigId !== 0;
|
||||
}
|
||||
|
||||
/** The desired schedule trigger for this form, or ``null`` if none. */
|
||||
|
|
@ -500,9 +500,9 @@ function modelsFromDefinition(raw: unknown): BuilderModels {
|
|||
const m = asRecord(raw);
|
||||
const num = (value: unknown) => (typeof value === "number" ? value : 0);
|
||||
return {
|
||||
agentLlmId: num(m.agent_llm_id),
|
||||
imageConfigId: num(m.image_generation_config_id),
|
||||
visionConfigId: num(m.vision_llm_config_id),
|
||||
chatModelId: num(m.chat_model_id),
|
||||
imageConfigId: num(m.image_gen_model_id),
|
||||
visionConfigId: num(m.vision_model_id),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue