mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 16:02:59 +00:00
refactor: one builder for an agent's run flow, and the agent page on the shared header
The run endpoint and evals build the agent's one-step flow through the same function. The agent page uses DetailPageHeader, whose error handler, tag and trigger context are now optional, and whose menu items keep their disabled state. The home row and the page share the agent's menu and delete. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
96d0a6e62b
commit
12faa3384f
@@ -23,8 +23,7 @@ use windmill_common::{
|
||||
};
|
||||
use windmill_queue::{push, PushArgs, PushIsolationLevel};
|
||||
|
||||
use crate::ai_evals::run::config_to_draft;
|
||||
use crate::ai_evals::subject::AgentDraft;
|
||||
use crate::ai_evals::run::{agent_step_flow, config_to_draft};
|
||||
use crate::db::{ApiAuthed, DB};
|
||||
use crate::jobs::{handle_chat_conversation_messages, set_flow_memory_id, RunJobQuery};
|
||||
|
||||
@@ -74,7 +73,14 @@ pub(crate) async fn run_agent(
|
||||
} else {
|
||||
path.to_string()
|
||||
};
|
||||
let flow_value = agent_flow(&config, chat)?;
|
||||
// A step memory id would replace the conversation's, and the agent would forget the turns
|
||||
// before; history comes from the conversation alone.
|
||||
let flow_value = agent_step_flow(
|
||||
&config,
|
||||
AGENT_NODE_ID,
|
||||
chat,
|
||||
&["memory_id", "previous_messages"],
|
||||
)?;
|
||||
|
||||
let push_authed = authed.clone().into();
|
||||
let (uuid, mut tx) = push(
|
||||
@@ -136,34 +142,3 @@ pub(crate) async fn run_agent(
|
||||
|
||||
Ok((StatusCode::CREATED, uuid.to_string()))
|
||||
}
|
||||
|
||||
/// The agent as a one-module flow reading the message and files from the run's inputs, shaped
|
||||
/// through `FlowValue` rather than trusted as raw JSON.
|
||||
fn agent_flow(config: &AgentDraft, chat: bool) -> Result<windmill_common::flows::FlowValue> {
|
||||
let mut input_transforms = match &config.input_transforms {
|
||||
serde_json::Value::Object(map) => map.clone(),
|
||||
_ => serde_json::Map::new(),
|
||||
};
|
||||
// A step memory id would replace the conversation's, and the agent would forget the turns
|
||||
// before; history comes from the conversation alone.
|
||||
for key in ["memory_id", "previous_messages"] {
|
||||
input_transforms.remove(key);
|
||||
}
|
||||
for key in ["user_message", "user_attachments"] {
|
||||
input_transforms.insert(
|
||||
key.to_string(),
|
||||
serde_json::json!({ "type": "javascript", "expr": format!("flow_input.{}", key) }),
|
||||
);
|
||||
}
|
||||
Ok(serde_json::from_value(serde_json::json!({
|
||||
"chat_input_enabled": chat,
|
||||
"modules": [{
|
||||
"id": AGENT_NODE_ID,
|
||||
"value": {
|
||||
"type": "aiagent",
|
||||
"tools": config.tools,
|
||||
"input_transforms": serde_json::Value::Object(input_transforms),
|
||||
}
|
||||
}]
|
||||
}))?)
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ fn build_run_flow(
|
||||
|
||||
/// The agent step, reading its case from the iteration rather than from the flow's arguments.
|
||||
fn agent_module(config: &AgentDraft) -> Result<serde_json::Value> {
|
||||
let flow = build_case_flow(config)?;
|
||||
let flow = agent_step_flow(config, AGENT_NODE_ID, false, &[])?;
|
||||
let mut value = serde_json::to_value(&flow.modules[0].value)?;
|
||||
if let Some(map) = value.as_object_mut() {
|
||||
let transforms = map
|
||||
@@ -269,34 +269,43 @@ fn agent_module(config: &AgentDraft) -> Result<serde_json::Value> {
|
||||
Ok(serde_json::json!({ "id": AGENT_NODE_ID, "value": value }))
|
||||
}
|
||||
|
||||
/// The agent step as a one-module flow, so the module shape is validated by deserializing
|
||||
/// through `FlowValue` rather than trusted as raw JSON.
|
||||
fn build_case_flow(config: &AgentDraft) -> Result<windmill_common::flows::FlowValue> {
|
||||
// The configuration runs exactly as authored: its own brain transforms are the module's, and
|
||||
// the case supplies the message and the attachments over the top.
|
||||
/// The agent as a one-step flow, validated by deserializing through `FlowValue` rather than
|
||||
/// trusted as raw JSON. Its own transforms are the step's, minus `without`, and the run's inputs
|
||||
/// supply the message and the attachments over the top. Always inlined, never a link to the
|
||||
/// resource: a linked step would resolve the agent when it runs, not as it was read here.
|
||||
pub(crate) fn agent_step_flow(
|
||||
config: &AgentDraft,
|
||||
id: &str,
|
||||
chat: bool,
|
||||
without: &[&str],
|
||||
) -> Result<windmill_common::flows::FlowValue> {
|
||||
let mut input_transforms = match &config.input_transforms {
|
||||
serde_json::Value::Object(map) => map.clone(),
|
||||
_ => serde_json::Map::new(),
|
||||
};
|
||||
for key in without {
|
||||
input_transforms.remove(*key);
|
||||
}
|
||||
for key in ["user_message", "user_attachments"] {
|
||||
input_transforms.insert(
|
||||
key.to_string(),
|
||||
serde_json::json!({ "type": "javascript", "expr": format!("flow_input.{}", key) }),
|
||||
);
|
||||
}
|
||||
|
||||
// Always inlined, never a link to the resource: a linked step would resolve the agent when
|
||||
// each case runs, which is the one thing a run of a named version must not do.
|
||||
let mut agent_value = serde_json::Map::new();
|
||||
agent_value.insert("type".to_string(), serde_json::json!("aiagent"));
|
||||
agent_value.insert("tools".to_string(), serde_json::json!(config.tools));
|
||||
agent_value.insert(
|
||||
"input_transforms".to_string(),
|
||||
serde_json::Value::Object(input_transforms),
|
||||
);
|
||||
Ok(serde_json::from_value(serde_json::json!({
|
||||
"modules": [{ "id": AGENT_NODE_ID, "value": serde_json::Value::Object(agent_value) }]
|
||||
}))?)
|
||||
let mut flow = serde_json::json!({
|
||||
"modules": [{
|
||||
"id": id,
|
||||
"value": {
|
||||
"type": "aiagent",
|
||||
"tools": config.tools,
|
||||
"input_transforms": serde_json::Value::Object(input_transforms),
|
||||
}
|
||||
}]
|
||||
});
|
||||
if chat {
|
||||
flow["chat_input_enabled"] = serde_json::json!(true);
|
||||
}
|
||||
Ok(serde_json::from_value(flow)?)
|
||||
}
|
||||
|
||||
/// How many times the agent has been saved, not the identity of the row holding that value: runs
|
||||
|
||||
@@ -6,16 +6,14 @@
|
||||
import type ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import type DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
|
||||
import InheritedLabels from '$lib/components/InheritedLabels.svelte'
|
||||
import { ResourceService, type ListableResource } from '$lib/gen'
|
||||
import type { ListableResource } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { isDeployable } from '$lib/utils_deployable'
|
||||
import { getDeployUiSettings } from '$lib/components/home/deploy_ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { FileUp, Pen, Shield, Trash } from 'lucide-svelte'
|
||||
import { Pen } from 'lucide-svelte'
|
||||
import Button from '../button/Button.svelte'
|
||||
import ChatFlowBadge from '$lib/components/flows/ChatFlowBadge.svelte'
|
||||
import { keepsManagedMemory } from '$lib/components/flows/agentFormFields'
|
||||
import { agentMenuItems, deleteAgent } from '$lib/components/flows/agentActions'
|
||||
import Row from './Row.svelte'
|
||||
|
||||
/**
|
||||
@@ -46,14 +44,8 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function deleteAgent(path: string): Promise<void> {
|
||||
try {
|
||||
await ResourceService.deleteResource({ workspace: $workspaceStore!, path })
|
||||
sendUserToast(`Deleted agent ${path}`)
|
||||
dispatch('change')
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not delete agent ${path}: ${err}`, true)
|
||||
}
|
||||
async function remove(path: string) {
|
||||
if (await deleteAgent($workspaceStore!, path)) dispatch('change')
|
||||
}
|
||||
|
||||
let editHref = $derived(`${base}/agents/edit/${agent.path}`)
|
||||
@@ -106,48 +98,20 @@
|
||||
</span>
|
||||
{/if}
|
||||
<Dropdown
|
||||
items={async () => {
|
||||
const { path } = agent
|
||||
return [
|
||||
{
|
||||
displayName: 'Permissions',
|
||||
icon: Shield,
|
||||
// A draft-only agent has no resource yet to hold permissions.
|
||||
disabled: !agent.canWrite || Boolean(agent.draft_only),
|
||||
action: () => {
|
||||
shareModal.openDrawer?.(path, 'resource')
|
||||
}
|
||||
},
|
||||
...(!agent.ws_specific &&
|
||||
!agent.draft_only &&
|
||||
isDeployable('resource', path, await getDeployUiSettings())
|
||||
? [
|
||||
{
|
||||
displayName: 'Deploy to prod/staging',
|
||||
icon: FileUp,
|
||||
action: () => {
|
||||
deploymentDrawer.openDrawer(path, 'resource')
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
displayName: 'Delete',
|
||||
icon: Trash,
|
||||
type: 'delete' as const,
|
||||
disabled: !agent.canWrite,
|
||||
action: (event) => {
|
||||
if (event?.shiftKey) {
|
||||
deleteAgent(path)
|
||||
} else {
|
||||
deleteConfirmedCallback = () => {
|
||||
deleteAgent(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
items={() =>
|
||||
agentMenuItems({
|
||||
path: agent.path,
|
||||
canWrite: agent.canWrite,
|
||||
draftOnly: Boolean(agent.draft_only),
|
||||
wsSpecific: agent.ws_specific,
|
||||
onPermissions: () => shareModal.openDrawer?.(agent.path, 'resource'),
|
||||
onDeploy: () => deploymentDrawer.openDrawer(agent.path, 'resource'),
|
||||
onDelete: (event) => {
|
||||
const { path } = agent
|
||||
if (event?.shiftKey) remove(path)
|
||||
else deleteConfirmedCallback = () => remove(path)
|
||||
}
|
||||
]
|
||||
}}
|
||||
})}
|
||||
on:open={() => {
|
||||
menuOpen = true
|
||||
}}
|
||||
|
||||
@@ -29,26 +29,33 @@
|
||||
type MenuItemButton = {
|
||||
label: string
|
||||
Icon: any
|
||||
onclick: () => void
|
||||
onclick: (e: MouseEvent) => void
|
||||
color?: 'red'
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const { triggersCount, triggersState } = $state(getContext<TriggerContext>('TriggerContext'))
|
||||
// An agent's page has no triggers, and so no context for them.
|
||||
const { triggersCount, triggersState } = $state(
|
||||
getContext<TriggerContext | undefined>('TriggerContext') ?? ({} as Partial<TriggerContext>)
|
||||
)
|
||||
|
||||
interface Props {
|
||||
mainButtons?: MainButton[]
|
||||
menuItems?: MenuItemButton[]
|
||||
summary?: string
|
||||
path?: string
|
||||
tag: string | undefined
|
||||
errorHandlerKind: 'flow' | 'script'
|
||||
scriptOrFlowPath: string
|
||||
errorHandlerMuted: boolean | undefined
|
||||
tag?: string | undefined
|
||||
/** Unset for what has no workspace error handler to mute, such as an agent. */
|
||||
errorHandlerKind?: 'flow' | 'script'
|
||||
scriptOrFlowPath?: string
|
||||
errorHandlerMuted?: boolean | undefined
|
||||
labels?: string[] | undefined
|
||||
inheritedLabels?: string[] | undefined
|
||||
onSaved?: (newPath: string) => void
|
||||
children?: import('svelte').Snippet
|
||||
trigger_badges?: import('svelte').Snippet
|
||||
/** Controls ahead of the menu, such as the way an agent's page runs it. */
|
||||
leading_actions?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -64,7 +71,8 @@
|
||||
inheritedLabels = undefined,
|
||||
onSaved,
|
||||
children,
|
||||
trigger_badges
|
||||
trigger_badges,
|
||||
leading_actions
|
||||
}: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -81,6 +89,7 @@
|
||||
}
|
||||
|
||||
async function toggleErrorHandler() {
|
||||
if (!errorHandlerKind || !scriptOrFlowPath) return
|
||||
const next = await toggleWorkspaceErrorHandler(
|
||||
errorHandlerKind,
|
||||
scriptOrFlowPath,
|
||||
@@ -99,7 +108,7 @@
|
||||
disabled: b.buttonProps.disabled,
|
||||
type: 'action' as const
|
||||
})),
|
||||
...(wide.current
|
||||
...(wide.current || !errorHandlerKind
|
||||
? []
|
||||
: [
|
||||
{
|
||||
@@ -113,6 +122,7 @@
|
||||
displayName: item.label,
|
||||
icon: item.Icon,
|
||||
action: item.onclick,
|
||||
disabled: item.disabled,
|
||||
type: item.color === 'red' ? ('delete' as const) : ('action' as const),
|
||||
separatorTop: i === 0 && !wide.current
|
||||
}))
|
||||
@@ -176,12 +186,13 @@
|
||||
{@render trigger_badges?.()}
|
||||
</div>
|
||||
<div class="flex gap-1 items-center pr-4">
|
||||
{@render leading_actions?.()}
|
||||
{#if allMenuItems.length > 0}
|
||||
{#key allMenuItems}
|
||||
<DropdownV2 items={allMenuItems} placement="bottom-end" size="md" />
|
||||
{/key}
|
||||
{/if}
|
||||
{#if wide.current}
|
||||
{#if wide.current && errorHandlerKind && scriptOrFlowPath}
|
||||
<ErrorHandlerToggleButton
|
||||
kind={errorHandlerKind}
|
||||
{scriptOrFlowPath}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { FileUp, Shield, Trash } from 'lucide-svelte'
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { Item } from '$lib/utils'
|
||||
import { isDeployable } from '$lib/utils_deployable'
|
||||
import { getDeployUiSettings } from '$lib/components/home/deploy_ui'
|
||||
|
||||
/** Deletes a saved agent, saying how it went. Resolves whether it was deleted. */
|
||||
export async function deleteAgent(workspace: string, path: string): Promise<boolean> {
|
||||
try {
|
||||
await ResourceService.deleteResource({ workspace, path })
|
||||
sendUserToast(`Deleted agent ${path}`)
|
||||
return true
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not delete agent ${path}: ${err}`, true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** An agent's menu, as its home row and its page both show it. */
|
||||
export async function agentMenuItems(agent: {
|
||||
path: string
|
||||
canWrite: boolean
|
||||
/** Never deployed: no resource yet to hold permissions or to deploy onward. */
|
||||
draftOnly?: boolean
|
||||
wsSpecific?: boolean
|
||||
onPermissions: () => void
|
||||
onDeploy: () => void
|
||||
onDelete: (event?: MouseEvent) => void
|
||||
}): Promise<Item[]> {
|
||||
const deployable =
|
||||
!agent.wsSpecific &&
|
||||
!agent.draftOnly &&
|
||||
isDeployable('resource', agent.path, await getDeployUiSettings())
|
||||
return [
|
||||
{
|
||||
displayName: 'Permissions',
|
||||
icon: Shield,
|
||||
disabled: !agent.canWrite || Boolean(agent.draftOnly),
|
||||
action: agent.onPermissions
|
||||
},
|
||||
...(deployable
|
||||
? [{ displayName: 'Deploy to prod/staging', icon: FileUp, action: agent.onDeploy }]
|
||||
: []),
|
||||
{
|
||||
displayName: 'Delete',
|
||||
icon: Trash,
|
||||
type: 'delete' as const,
|
||||
disabled: !agent.canWrite,
|
||||
action: agent.onDelete
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state'
|
||||
import { FileUp, FlaskConical, FormInput, MessageSquare, Pen, Shield, Trash } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { FlaskConical, FormInput, MessageSquare, Pen } from 'lucide-svelte'
|
||||
import { resource } from 'runed'
|
||||
import { base } from '$lib/base'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { copilotInfo } from '$lib/aiStore'
|
||||
import { Button } from '$lib/components/common'
|
||||
import AgentEvalsModal from '$lib/components/flows/content/AgentEvalsModal.svelte'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import SummaryPathDisplay from '$lib/components/SummaryPathDisplay.svelte'
|
||||
import DetailPageHeader from '$lib/components/details/DetailPageHeader.svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
|
||||
@@ -18,11 +16,8 @@
|
||||
import AgentConfigModal from '$lib/components/flows/content/AgentConfigModal.svelte'
|
||||
import RunForm from '$lib/components/RunForm.svelte'
|
||||
import { keepsManagedMemory } from '$lib/components/flows/agentFormFields'
|
||||
import { getDeployUiSettings } from '$lib/components/home/deploy_ui'
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { agentMenuItems, deleteAgent } from '$lib/components/flows/agentActions'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { isDeployable } from '$lib/utils_deployable'
|
||||
|
||||
/**
|
||||
* The deployed agent, as a flow's or a script's page shows theirs: a way to run it, a chat when
|
||||
@@ -44,16 +39,39 @@
|
||||
let deleteOpen = $state(false)
|
||||
let evalsModal: AgentEvalsModal | undefined = $state(undefined)
|
||||
|
||||
async function deleteAgent() {
|
||||
if (!ws) return
|
||||
try {
|
||||
await ResourceService.deleteResource({ workspace: ws, path })
|
||||
sendUserToast(`Deleted agent ${path}`)
|
||||
await goto(`${base}/?kind=agent`)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not delete agent ${path}: ${err}`, true)
|
||||
}
|
||||
async function remove() {
|
||||
if (ws && (await deleteAgent(ws, path))) await goto(`${base}/?kind=agent`)
|
||||
}
|
||||
|
||||
// Operators get no menu, as on a flow's or a script's page.
|
||||
const menuItems = resource(
|
||||
() =>
|
||||
[
|
||||
path,
|
||||
config != undefined && !$userStore?.operator,
|
||||
canEdit,
|
||||
agent?.state?.wsSpecific
|
||||
] as const,
|
||||
async ([path, shown, canWrite, wsSpecific]) =>
|
||||
shown
|
||||
? (
|
||||
await agentMenuItems({
|
||||
path,
|
||||
canWrite,
|
||||
wsSpecific,
|
||||
onPermissions: () => shareModal?.openDrawer?.(path, 'resource'),
|
||||
onDeploy: () => deploymentDrawer?.openDrawer(path, 'resource'),
|
||||
onDelete: () => (deleteOpen = true)
|
||||
})
|
||||
).map((item) => ({
|
||||
label: item.displayName,
|
||||
Icon: item.icon,
|
||||
onclick: (e: MouseEvent) => item.action?.(e),
|
||||
disabled: item.disabled,
|
||||
color: item.type === 'delete' ? ('red' as const) : undefined
|
||||
}))
|
||||
: []
|
||||
)
|
||||
</script>
|
||||
|
||||
<ShareModal bind:this={shareModal} />
|
||||
@@ -65,113 +83,79 @@
|
||||
on:canceled={() => (deleteOpen = false)}
|
||||
on:confirmed={() => {
|
||||
deleteOpen = false
|
||||
deleteAgent()
|
||||
remove()
|
||||
}}
|
||||
>
|
||||
<span>Every flow that links {path} will fail at its agent step once it is deleted.</span>
|
||||
</ConfirmationModal>
|
||||
|
||||
<main class="h-screen w-full flex flex-col">
|
||||
<!-- Laid out as `DetailPageHeader` is for flows and scripts, whose trigger and error handler
|
||||
controls an agent does not have. -->
|
||||
<div class="border-b">
|
||||
<div
|
||||
class="flex w-full flex-wrap md:flex-nowrap justify-end gap-x-2 gap-y-4 items-center min-h-12 py-2 md:py-0"
|
||||
>
|
||||
<div class="grow px-2 inline-flex items-center gap-4 min-w-0">
|
||||
<div class={twMerge('min-w-0', $userStore?.operator ? 'pl-10' : '')}>
|
||||
<SummaryPathDisplay summary={config ? agent?.state?.description : undefined} {path} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1 items-center pr-4">
|
||||
<!-- Only an agent that keeps the conversation can chat: without managed memory every
|
||||
message would be answered alone, so it is run from its inputs with nothing to switch. -->
|
||||
{#if chatAvailable && testPane?.mode}
|
||||
<ToggleButtonGroup
|
||||
bind:selected={
|
||||
() => testPane?.mode,
|
||||
(mode) => {
|
||||
if (testPane) testPane.mode = mode
|
||||
<DetailPageHeader
|
||||
summary={config ? agent?.state?.description : undefined}
|
||||
{path}
|
||||
menuItems={menuItems.current ?? []}
|
||||
mainButtons={[
|
||||
// Evaluating an agent builds datasets and runs against it: authoring, as editing is.
|
||||
...(canEdit
|
||||
? [
|
||||
{
|
||||
label: 'Evals',
|
||||
buttonProps: {
|
||||
variant: 'default',
|
||||
unifiedSize: 'md',
|
||||
startIcon: FlaskConical,
|
||||
title: 'Run this agent against a dataset of cases',
|
||||
onClick: () => evalsModal?.openModal()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
buttonProps: {
|
||||
variant: 'accent',
|
||||
unifiedSize: 'md',
|
||||
startIcon: Pen,
|
||||
href: `${base}/agents/edit/${path}`
|
||||
}
|
||||
}
|
||||
noWFull
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton
|
||||
size="md"
|
||||
value="chat"
|
||||
label="Chat"
|
||||
icon={MessageSquare}
|
||||
tooltip="Chat with the agent: each message runs it, and it remembers the conversation"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
size="md"
|
||||
value="form"
|
||||
label="Form"
|
||||
icon={FormInput}
|
||||
tooltip="Run the agent once, on the inputs in a form"
|
||||
{item}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
{#if config && !$userStore?.operator}
|
||||
<DropdownV2
|
||||
placement="bottom-end"
|
||||
size="md"
|
||||
items={async () => [
|
||||
{
|
||||
displayName: 'Permissions',
|
||||
icon: Shield,
|
||||
disabled: !canEdit,
|
||||
action: () => shareModal?.openDrawer?.(path, 'resource')
|
||||
},
|
||||
...(!agent?.state?.wsSpecific &&
|
||||
isDeployable('resource', path, await getDeployUiSettings())
|
||||
? [
|
||||
{
|
||||
displayName: 'Deploy to prod/staging',
|
||||
icon: FileUp,
|
||||
action: () => deploymentDrawer?.openDrawer(path, 'resource')
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
displayName: 'Delete',
|
||||
icon: Trash,
|
||||
type: 'delete' as const,
|
||||
disabled: !canEdit,
|
||||
action: () => (deleteOpen = true)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<!-- Evaluating an agent builds datasets and runs against it: authoring, as editing is. -->
|
||||
{#if canEdit}
|
||||
<Button
|
||||
variant="default"
|
||||
unifiedSize="md"
|
||||
startIcon={{ icon: FlaskConical }}
|
||||
title="Run this agent against a dataset of cases"
|
||||
onClick={() => evalsModal?.openModal()}
|
||||
>
|
||||
Evals
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if canEdit}
|
||||
<Button
|
||||
variant="accent"
|
||||
unifiedSize="md"
|
||||
startIcon={{ icon: Pen }}
|
||||
href="{base}/agents/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
>
|
||||
{#snippet leading_actions()}
|
||||
<!-- Only an agent that keeps the conversation can chat: without managed memory every
|
||||
message would be answered alone, so it is run from its inputs with nothing to switch. -->
|
||||
{#if chatAvailable && testPane?.mode}
|
||||
<ToggleButtonGroup
|
||||
bind:selected={
|
||||
() => testPane?.mode,
|
||||
(mode) => {
|
||||
if (testPane) testPane.mode = mode
|
||||
}
|
||||
}
|
||||
noWFull
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton
|
||||
size="md"
|
||||
value="chat"
|
||||
label="Chat"
|
||||
icon={MessageSquare}
|
||||
tooltip="Chat with the agent: each message runs it, and it remembers the conversation"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
size="md"
|
||||
value="form"
|
||||
label="Form"
|
||||
icon={FormInput}
|
||||
tooltip="Run the agent once, on the inputs in a form"
|
||||
{item}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</DetailPageHeader>
|
||||
<div class="flex-1 min-h-0">
|
||||
{#key `${ws}:${path}`}
|
||||
<AgentEditorHost
|
||||
|
||||
Reference in New Issue
Block a user