mirror of
https://github.com/block/buzz.git
synced 2026-11-07 18:01:06 +00:00
feat: provider/model selection for personas and runtime-aware env injection (#794)
This commit is contained in:
@@ -66,6 +66,7 @@ export function PersonaDialog({
|
||||
const [systemPrompt, setSystemPrompt] = React.useState("");
|
||||
const [runtime, setRuntime] = React.useState("");
|
||||
const [model, setModel] = React.useState("");
|
||||
const [provider, setProvider] = React.useState("");
|
||||
const [namePoolText, setNamePoolText] = React.useState("");
|
||||
const [envVars, setEnvVars] = React.useState<EnvVarsValue>({});
|
||||
const [isImportingUpdate, setIsImportingUpdate] = React.useState(false);
|
||||
@@ -90,6 +91,7 @@ export function PersonaDialog({
|
||||
setSystemPrompt(initialValues.systemPrompt);
|
||||
setRuntime(initialValues.runtime ?? "");
|
||||
setModel(initialValues.model ?? "");
|
||||
setProvider(initialValues.provider ?? "");
|
||||
setNamePoolText(
|
||||
("namePool" in initialValues
|
||||
? (initialValues as { namePool?: string[] }).namePool
|
||||
@@ -216,6 +218,7 @@ export function PersonaDialog({
|
||||
setSystemPrompt("");
|
||||
setRuntime("");
|
||||
setModel("");
|
||||
setProvider("");
|
||||
setNamePoolText("");
|
||||
setImportErrorMessage(null);
|
||||
setIsImportingUpdate(false);
|
||||
@@ -240,6 +243,7 @@ export function PersonaDialog({
|
||||
systemPrompt,
|
||||
runtime: runtime.trim() || undefined,
|
||||
model: model.trim() || undefined,
|
||||
provider: provider.trim() || undefined,
|
||||
namePool: namePool.length > 0 ? namePool : undefined,
|
||||
envVars,
|
||||
};
|
||||
@@ -405,6 +409,27 @@ export function PersonaDialog({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium" htmlFor="persona-provider">
|
||||
LLM Provider
|
||||
</label>
|
||||
<Input
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
disabled={isPending}
|
||||
id="persona-provider"
|
||||
onChange={(event) => setProvider(event.target.value)}
|
||||
placeholder="e.g. databricks, anthropic, openai"
|
||||
spellCheck={false}
|
||||
value={provider}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Optional. Injected as the runtime's provider env var at agent
|
||||
creation time. Leave blank for auto-detection or provider-locked
|
||||
runtimes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label
|
||||
className="text-sm font-medium"
|
||||
|
||||
@@ -31,6 +31,7 @@ test("duplicatePersonaDialogState copies persona fields into a new draft", () =>
|
||||
systemPrompt: "Be direct.",
|
||||
runtime: "provider-a",
|
||||
model: "model-a",
|
||||
provider: null,
|
||||
isBuiltIn: false,
|
||||
isActive: true,
|
||||
createdAt: "2025-01-01T00:00:00Z",
|
||||
@@ -43,6 +44,7 @@ test("duplicatePersonaDialogState copies persona fields into a new draft", () =>
|
||||
systemPrompt: "Be direct.",
|
||||
runtime: "provider-a",
|
||||
model: "model-a",
|
||||
provider: undefined,
|
||||
namePool: [],
|
||||
envVars: {},
|
||||
});
|
||||
@@ -82,6 +84,7 @@ test("editPersonaDialogState preserves the persona id for updates", () => {
|
||||
systemPrompt: "Keep it weird.",
|
||||
runtime: null,
|
||||
model: null,
|
||||
provider: null,
|
||||
isBuiltIn: true,
|
||||
isActive: true,
|
||||
createdAt: "2025-01-01T00:00:00Z",
|
||||
@@ -98,6 +101,7 @@ test("editPersonaDialogState preserves the persona id for updates", () => {
|
||||
systemPrompt: "Keep it weird.",
|
||||
runtime: undefined,
|
||||
model: undefined,
|
||||
provider: undefined,
|
||||
namePool: [],
|
||||
envVars: {},
|
||||
});
|
||||
@@ -132,6 +136,8 @@ test("importPersonaDialogState maps parsed persona previews into create drafts",
|
||||
systemPrompt: "Imported prompt",
|
||||
runtime: null,
|
||||
model: "model-b",
|
||||
provider: null,
|
||||
namePool: [],
|
||||
sourceFile: "import.persona.json",
|
||||
});
|
||||
|
||||
@@ -142,5 +148,83 @@ test("importPersonaDialogState maps parsed persona previews into create drafts",
|
||||
systemPrompt: "Imported prompt",
|
||||
runtime: undefined,
|
||||
model: "model-b",
|
||||
provider: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Provider round-trip tests ─────────────────────────────────────────────────
|
||||
|
||||
test("editPersonaDialogState preserves provider=databricks", () => {
|
||||
const state = editPersonaDialogState({
|
||||
id: "persona-provider",
|
||||
displayName: "DB Agent",
|
||||
avatarUrl: null,
|
||||
systemPrompt: "Use databricks.",
|
||||
runtime: "goose",
|
||||
model: "dbrx",
|
||||
provider: "databricks",
|
||||
isBuiltIn: false,
|
||||
isActive: true,
|
||||
namePool: [],
|
||||
envVars: {},
|
||||
createdAt: "2025-01-01T00:00:00Z",
|
||||
updatedAt: "2025-01-02T00:00:00Z",
|
||||
});
|
||||
|
||||
assert.equal(state.initialValues.provider, "databricks");
|
||||
});
|
||||
|
||||
test("editPersonaDialogState maps provider=null to undefined", () => {
|
||||
const state = editPersonaDialogState({
|
||||
id: "persona-no-provider",
|
||||
displayName: "Plain",
|
||||
avatarUrl: null,
|
||||
systemPrompt: "No provider.",
|
||||
runtime: null,
|
||||
model: null,
|
||||
provider: null,
|
||||
isBuiltIn: false,
|
||||
isActive: true,
|
||||
namePool: [],
|
||||
envVars: {},
|
||||
createdAt: "2025-01-01T00:00:00Z",
|
||||
updatedAt: "2025-01-02T00:00:00Z",
|
||||
});
|
||||
|
||||
assert.equal(state.initialValues.provider, undefined);
|
||||
});
|
||||
|
||||
test("duplicatePersonaDialogState preserves provider=databricks", () => {
|
||||
const state = duplicatePersonaDialogState({
|
||||
id: "persona-dup-provider",
|
||||
displayName: "DB Agent",
|
||||
avatarUrl: null,
|
||||
systemPrompt: "Use databricks.",
|
||||
runtime: "goose",
|
||||
model: "dbrx",
|
||||
provider: "databricks",
|
||||
isBuiltIn: false,
|
||||
isActive: true,
|
||||
namePool: [],
|
||||
envVars: {},
|
||||
createdAt: "2025-01-01T00:00:00Z",
|
||||
updatedAt: "2025-01-02T00:00:00Z",
|
||||
});
|
||||
|
||||
assert.equal(state.initialValues.provider, "databricks");
|
||||
});
|
||||
|
||||
test("importPersonaDialogState preserves provider=anthropic", () => {
|
||||
const state = importPersonaDialogState({
|
||||
displayName: "Imported With Provider",
|
||||
avatarDataUrl: null,
|
||||
systemPrompt: "Anthropic agent.",
|
||||
runtime: "goose",
|
||||
model: "claude-sonnet",
|
||||
provider: "anthropic",
|
||||
namePool: [],
|
||||
sourceFile: "provider-test.persona.json",
|
||||
});
|
||||
|
||||
assert.equal(state.initialValues.provider, "anthropic");
|
||||
});
|
||||
|
||||
@@ -44,6 +44,7 @@ export function duplicatePersonaDialogState(
|
||||
systemPrompt: persona.systemPrompt,
|
||||
runtime: persona.runtime ?? undefined,
|
||||
model: persona.model ?? undefined,
|
||||
provider: persona.provider ?? undefined,
|
||||
// Carry envVars and namePool into the duplicate. Without this, a
|
||||
// duplicated persona that relies on an API key in env_vars would
|
||||
// silently fail at spawn until the user re-entered every credential.
|
||||
@@ -69,6 +70,7 @@ export function editPersonaDialogState(
|
||||
systemPrompt: persona.systemPrompt,
|
||||
runtime: persona.runtime ?? undefined,
|
||||
model: persona.model ?? undefined,
|
||||
provider: persona.provider ?? undefined,
|
||||
// Seed both namePool and envVars from the loaded persona so editing
|
||||
// unrelated fields doesn't submit an empty value that wipes them.
|
||||
// (Persona update treats Some(empty) as "clear all" intentionally;
|
||||
@@ -92,6 +94,7 @@ export function importPersonaDialogState(
|
||||
systemPrompt: persona.systemPrompt,
|
||||
runtime: persona.runtime ?? undefined,
|
||||
model: persona.model ?? undefined,
|
||||
provider: persona.provider ?? undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ type RawParsedPersonaPreview = {
|
||||
avatar_data_url: string | null;
|
||||
runtime: string | null;
|
||||
model: string | null;
|
||||
provider: string | null;
|
||||
name_pool?: string[];
|
||||
source_file: string;
|
||||
};
|
||||
@@ -33,6 +34,7 @@ export type ParsedPersonaPreview = {
|
||||
avatarDataUrl: string | null;
|
||||
runtime: string | null;
|
||||
model: string | null;
|
||||
provider: string | null;
|
||||
namePool: string[];
|
||||
sourceFile: string;
|
||||
};
|
||||
@@ -54,6 +56,7 @@ type RawPersona = {
|
||||
system_prompt: string;
|
||||
runtime?: string | null;
|
||||
model?: string | null;
|
||||
provider?: string | null;
|
||||
name_pool?: string[];
|
||||
is_builtin: boolean;
|
||||
is_active?: boolean;
|
||||
@@ -70,6 +73,7 @@ function fromRawPersona(persona: RawPersona): AgentPersona {
|
||||
systemPrompt: persona.system_prompt,
|
||||
runtime: persona.runtime ?? null,
|
||||
model: persona.model ?? null,
|
||||
provider: persona.provider ?? null,
|
||||
namePool: persona.name_pool ?? [],
|
||||
isBuiltIn: persona.is_builtin,
|
||||
isActive: persona.is_active ?? true,
|
||||
@@ -94,6 +98,7 @@ export async function createPersona(
|
||||
systemPrompt: input.systemPrompt,
|
||||
runtime: input.runtime,
|
||||
model: input.model,
|
||||
provider: input.provider,
|
||||
namePool: input.namePool ?? [],
|
||||
envVars: input.envVars ?? {},
|
||||
},
|
||||
@@ -113,6 +118,7 @@ export async function updatePersona(
|
||||
systemPrompt: input.systemPrompt,
|
||||
runtime: input.runtime,
|
||||
model: input.model,
|
||||
provider: input.provider,
|
||||
namePool: input.namePool ?? [],
|
||||
// Send envVars only when caller explicitly provided it; omitting
|
||||
// tells the backend "don't touch the stored env vars" so editing
|
||||
@@ -151,6 +157,7 @@ export async function parsePersonaFiles(
|
||||
avatarDataUrl: p.avatar_data_url,
|
||||
runtime: p.runtime,
|
||||
model: p.model,
|
||||
provider: p.provider,
|
||||
namePool: p.name_pool ?? [],
|
||||
sourceFile: p.source_file,
|
||||
})),
|
||||
|
||||
@@ -469,6 +469,8 @@ export type AgentPersona = {
|
||||
runtime: string | null;
|
||||
/** Opaque, harness-specific model identifier string. Sprout stores and passes through without interpretation. */
|
||||
model: string | null;
|
||||
/** LLM inference provider (e.g. "databricks", "anthropic"). Injected as the runtime's provider env var at spawn time. */
|
||||
provider: string | null;
|
||||
namePool: string[];
|
||||
isBuiltIn: boolean;
|
||||
isActive: boolean;
|
||||
@@ -487,6 +489,7 @@ export type CreatePersonaInput = {
|
||||
systemPrompt: string;
|
||||
runtime?: string;
|
||||
model?: string;
|
||||
provider?: string;
|
||||
namePool?: string[];
|
||||
envVars?: Record<string, string>;
|
||||
};
|
||||
@@ -498,6 +501,7 @@ export type UpdatePersonaInput = {
|
||||
systemPrompt: string;
|
||||
runtime?: string;
|
||||
model?: string;
|
||||
provider?: string;
|
||||
namePool?: string[];
|
||||
envVars?: Record<string, string>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user