Context variables (#214)

* move prompts panel to variables

* variable shows both name and value

* added modal to add variables

* removed warning on edits

* adding or updating variables only uses the modal

* append variable context to agent instructions

* add dummy value to the variable values when downloading json

* fixed @variable mentions in the instruction editor

* change placeholder text for variables when json is imported
This commit is contained in:
arkml 2025-08-21 17:29:27 +05:30 committed by GitHub
parent 981cff3b3f
commit ab014f788c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 358 additions and 23 deletions

View file

@ -14,6 +14,9 @@ const sectionHeaderStyles = "block text-xs font-medium uppercase tracking-wider
// Enhanced textarea styles with improved states
const textareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-gray-400 dark:placeholder:text-gray-500";
// Value field styles without grey placeholder text
const valueTextareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-black dark:placeholder:text-white";
export function PromptConfig({
prompt,
agents,
@ -128,7 +131,7 @@ export function PromptConfig({
<div className="space-y-4">
<label className={sectionHeaderStyles}>
Prompt
Value
</label>
<Textarea
value={prompt.prompt}
@ -139,8 +142,8 @@ export function PromptConfig({
});
showSavedMessage();
}}
placeholder="Edit prompt here..."
className={`${textareaStyles} min-h-[200px]`}
placeholder="Enter variable value..."
className={`${valueTextareaStyles} min-h-[200px]`}
autoResize
/>
</div>

View file

@ -63,6 +63,9 @@ interface EntityListProps {
onAddAgent: (agent: Partial<z.infer<typeof WorkflowAgent>>) => void;
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
onAddPrompt: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
onUpdatePrompt: (name: string, prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
onAddPromptFromModal: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
onUpdatePromptFromModal: (name: string, prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
onAddPipeline: (pipeline: Partial<z.infer<typeof WorkflowPipeline>>) => void;
onAddAgentToPipeline: (pipelineName: string) => void;
onToggleAgent: (name: string) => void;
@ -97,6 +100,7 @@ const EmptyState: React.FC<EmptyStateProps> = ({ entity, hasFilteredItems }) =>
const ListItemWithMenu = ({
name,
value,
isSelected,
onClick,
disabled,
@ -110,6 +114,7 @@ const ListItemWithMenu = ({
isMocked,
}: {
name: string;
value?: string;
isSelected?: boolean;
onClick?: () => void;
disabled?: boolean;
@ -157,9 +162,34 @@ const ListItemWithMenu = ({
/>
) : icon}
</div>
<span className="text-xs">{name}</span>
{value ? (
<div className="flex-1 min-w-0 grid grid-cols-2 gap-2">
<Tooltip
content={name}
size="sm"
delay={500}
isDisabled={name.length <= 20}
>
<span className="text-xs font-medium truncate">
{name}
</span>
</Tooltip>
<Tooltip
content={value}
size="sm"
delay={500}
isDisabled={value.length <= 30}
>
<span className="text-xs text-zinc-600 dark:text-zinc-400 truncate">
{value}
</span>
</Tooltip>
</div>
) : (
<span className="text-xs">{name}</span>
)}
</div>
<div className="flex items-center gap-1">
<div className="flex items-center gap-1 shrink-0">
{statusLabel}
{isMocked && (
<Tooltip content="Mocked" size="sm" delay={500}>
@ -168,7 +198,9 @@ const ListItemWithMenu = ({
</div>
</Tooltip>
)}
{menuContent}
<div className="opacity-100">
{menuContent}
</div>
</div>
</div>
);
@ -464,6 +496,9 @@ export const EntityList = forwardRef<
onAddAgent,
onAddTool,
onAddPrompt,
onUpdatePrompt,
onAddPromptFromModal,
onUpdatePromptFromModal,
onAddPipeline,
onAddAgentToPipeline,
onToggleAgent,
@ -490,6 +525,8 @@ export const EntityList = forwardRef<
const [showAgentTypeModal, setShowAgentTypeModal] = useState(false);
const [showToolsModal, setShowToolsModal] = useState(false);
const [showDataSourcesModal, setShowDataSourcesModal] = useState(false);
const [showAddVariableModal, setShowAddVariableModal] = useState(false);
const [editingVariable, setEditingVariable] = useState<{name: string; value: string} | null>(null);
// State to track which toolkit's tools panel to open
const [selectedToolkitSlug, setSelectedToolkitSlug] = useState<string | null>(null);
@ -498,6 +535,11 @@ export const EntityList = forwardRef<
outputVisibility: agentType
});
};
const handleVariableClick = (prompt: z.infer<typeof WorkflowPrompt>) => {
setEditingVariable({ name: prompt.name, value: prompt.prompt });
setShowAddVariableModal(true);
};
const selectedRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [containerHeight, setContainerHeight] = useState<number>(0);
@ -1146,7 +1188,7 @@ export const EntityList = forwardRef<
)}
</button>
<PenLine className="w-4 h-4" />
<span>Prompts</span>
<span>Variables</span>
</div>
<Button
variant="secondary"
@ -1154,11 +1196,11 @@ export const EntityList = forwardRef<
onClick={(e) => {
e.stopPropagation();
setExpandedPanels(prev => ({ ...prev, prompts: true }));
onAddPrompt({});
setShowAddVariableModal(true);
}}
className={`group ${buttonClasses}`}
showHoverContent={true}
hoverContent="Add Prompt"
hoverContent="Add Variable"
>
<PlusIcon className="w-4 h-4" />
</Button>
@ -1174,8 +1216,9 @@ export const EntityList = forwardRef<
<ListItemWithMenu
key={`prompt-${index}`}
name={prompt.name}
value={prompt.prompt}
isSelected={selectedEntity?.type === "prompt" && selectedEntity.name === prompt.name}
onClick={() => onSelectPrompt(prompt.name)}
onClick={() => handleVariableClick(prompt)}
selectedRef={selectedEntity?.type === "prompt" && selectedEntity.name === prompt.name ? selectedRef : undefined}
icon={<ScrollText className="w-4 h-4 text-blue-600/70 dark:text-blue-500/70" />}
menuContent={
@ -1188,7 +1231,7 @@ export const EntityList = forwardRef<
))}
</div>
) : (
<EmptyState entity="prompts" hasFilteredItems={false} />
<EmptyState entity="variables" hasFilteredItems={false} />
)}
</div>
</div>
@ -1227,6 +1270,27 @@ export const EntityList = forwardRef<
useRagS3Uploads={useRagS3Uploads}
useRagScraping={useRagScraping}
/>
<AddVariableModal
isOpen={showAddVariableModal}
onClose={() => {
setShowAddVariableModal(false);
setEditingVariable(null);
}}
onConfirm={(name, value) => {
if (editingVariable) {
// Update existing variable using modal-specific handler
onUpdatePromptFromModal(editingVariable.name, { name, prompt: value });
} else {
// Add new variable using modal-specific handler
onAddPromptFromModal({ name, prompt: value });
}
setShowAddVariableModal(false);
setEditingVariable(null);
}}
initialName={editingVariable?.name}
initialValue={editingVariable?.value}
isEditing={!!editingVariable}
/>
</div>
);
});
@ -1922,4 +1986,134 @@ function AgentTypeModal({ isOpen, onClose, onConfirm, onCreatePipeline }: AgentT
</ModalContent>
</Modal>
);
}
interface AddVariableModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: (name: string, value: string) => void;
initialName?: string;
initialValue?: string;
isEditing?: boolean;
}
function AddVariableModal({ isOpen, onClose, onConfirm, initialName, initialValue, isEditing = false }: AddVariableModalProps) {
const [name, setName] = useState('');
const [value, setValue] = useState('');
const [errors, setErrors] = useState<{ name?: string; value?: string }>({});
// Initialize form with values when modal opens
useEffect(() => {
if (isOpen) {
setName(initialName || '');
setValue(initialValue || '');
setErrors({});
}
}, [isOpen, initialName, initialValue]);
const resetForm = () => {
setName('');
setValue('');
setErrors({});
};
const handleClose = () => {
resetForm();
onClose();
};
const handleConfirm = () => {
const newErrors: { name?: string; value?: string } = {};
if (!name.trim()) {
newErrors.name = 'Variable name is required';
}
if (!value.trim()) {
newErrors.value = 'Variable value is required';
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
onConfirm(name.trim(), value.trim());
resetForm();
};
return (
<Modal isOpen={isOpen} onClose={handleClose} size="md">
<ModalContent>
<ModalHeader>
<div className="flex items-center gap-2">
<PenLine className="w-5 h-5 text-indigo-600" />
<span>{isEditing ? 'Edit Variable' : 'Add Variable'}</span>
</div>
</ModalHeader>
<ModalBody className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Variable Name
</label>
<input
type="text"
value={name}
onChange={(e) => {
setName(e.target.value);
if (errors.name) setErrors(prev => ({ ...prev, name: undefined }));
}}
placeholder="Enter variable name (e.g., greeting_message)"
className={clsx(
"w-full px-3 py-2 border rounded-md text-sm",
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500",
"dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100",
errors.name ? "border-red-500" : "border-gray-300 dark:border-gray-600"
)}
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.name}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Variable Value
</label>
<textarea
value={value}
onChange={(e) => {
setValue(e.target.value);
if (errors.value) setErrors(prev => ({ ...prev, value: undefined }));
}}
placeholder="Enter the variable value..."
rows={4}
className={clsx(
"w-full px-3 py-2 border rounded-md text-sm resize-none",
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500",
"dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100",
errors.value ? "border-red-500" : "border-gray-300 dark:border-gray-600"
)}
/>
{errors.value && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.value}</p>
)}
</div>
</ModalBody>
<ModalFooter>
<Button
variant="secondary"
onClick={handleClose}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleConfirm}
>
{isEditing ? 'Update Variable' : 'Add Variable'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}

View file

@ -85,6 +85,9 @@ export type Action = {
} | {
type: "add_prompt";
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
} | {
type: "add_prompt_no_select";
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
} | {
type: "add_pipeline";
pipeline: Partial<z.infer<typeof WorkflowPipeline>>;
@ -143,6 +146,10 @@ export type Action = {
type: "update_prompt";
name: string;
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
} | {
type: "update_prompt_no_select";
name: string;
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
} | {
type: "toggle_agent";
name: string;
@ -391,10 +398,10 @@ function reducer(state: State, action: Action): State {
if (isLive) {
break;
}
let newPromptName = "New prompt";
let newPromptName = "New Variable";
if (draft.workflow?.prompts.some((prompt) => prompt.name === newPromptName)) {
newPromptName = `New prompt ${draft.workflow?.prompts.filter((prompt) =>
prompt.name.startsWith("New prompt")).length + 1}`;
newPromptName = `New Variable ${draft.workflow?.prompts.filter((prompt) =>
prompt.name.startsWith("New Variable")).length + 1}`;
}
draft.workflow?.prompts.push({
name: newPromptName,
@ -410,6 +417,26 @@ function reducer(state: State, action: Action): State {
draft.chatKey++;
break;
}
case "add_prompt_no_select": {
if (isLive) {
break;
}
let newPromptName = "New Variable";
if (draft.workflow?.prompts.some((prompt) => prompt.name === newPromptName)) {
newPromptName = `New Variable ${draft.workflow?.prompts.filter((prompt) =>
prompt.name.startsWith("New Variable")).length + 1}`;
}
draft.workflow?.prompts.push({
name: newPromptName,
type: "base_prompt",
prompt: "",
...action.prompt
});
// Don't set selection - this is the key difference
draft.pendingChanges = true;
draft.chatKey++;
break;
}
case "add_pipeline": {
if (isLive) {
break;
@ -751,6 +778,47 @@ function reducer(state: State, action: Action): State {
draft.pendingChanges = true;
draft.chatKey++;
break;
case "update_prompt_no_select":
if (isLive) {
break;
}
// update prompt data
draft.workflow.prompts = draft.workflow.prompts.map((prompt) =>
prompt.name === action.name ? { ...prompt, ...action.prompt } : prompt
);
// if the prompt is renamed
if (action.prompt.name && action.prompt.name !== action.name) {
// update this prompts references in other agents / prompts
draft.workflow.agents = draft.workflow.agents.map(agent => ({
...agent,
instructions: agent.instructions.replace(
`[@prompt:${action.name}](#mention)`,
`[@prompt:${action.prompt.name}](#mention)`
)
}));
draft.workflow.prompts = draft.workflow.prompts.map(prompt => ({
...prompt,
prompt: prompt.prompt.replace(
`[@prompt:${action.name}](#mention)`,
`[@prompt:${action.prompt.name}](#mention)`
)
}));
// if this is the selected prompt, update the selection
if (draft.selection?.type === "prompt" && draft.selection.name === action.name) {
draft.selection = {
type: "prompt",
name: action.prompt.name
};
}
}
// Don't set selection - this is the key difference
draft.pendingChanges = true;
draft.chatKey++;
break;
case "toggle_agent":
if (isLive) {
break;
@ -1074,6 +1142,15 @@ export function WorkflowEditor({
dispatch({ type: "update_prompt", name, prompt });
}
// Modal-specific handlers that don't auto-select
function handleAddPromptFromModal(prompt: Partial<z.infer<typeof WorkflowPrompt>>) {
dispatch({ type: "add_prompt_no_select", prompt });
}
function handleUpdatePromptFromModal(name: string, prompt: Partial<z.infer<typeof WorkflowPrompt>>) {
dispatch({ type: "update_prompt_no_select", name, prompt });
}
function handleDeletePrompt(name: string) {
if (window.confirm(`Are you sure you want to delete the prompt "${name}"?`)) {
dispatch({ type: "delete_prompt", name });
@ -1129,7 +1206,23 @@ export function WorkflowEditor({
// Remove handleCopyJSON and add handleDownloadJSON
function handleDownloadJSON() {
const workflow = state.present.workflow;
const json = JSON.stringify(workflow, null, 2);
// Create a copy of the workflow and replace variable values with dummy text
const workflowCopy = {
...workflow,
prompts: workflow.prompts.map(prompt => {
// If this is a variable (base_prompt type), replace its value with dummy text
if (prompt.type === 'base_prompt') {
return {
...prompt,
prompt: '<needs to be added>'
};
}
return prompt;
})
};
const json = JSON.stringify(workflowCopy, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
@ -1310,6 +1403,9 @@ export function WorkflowEditor({
onAddAgent={handleAddAgent}
onAddTool={handleAddTool}
onAddPrompt={handleAddPrompt}
onUpdatePrompt={handleUpdatePrompt}
onAddPromptFromModal={handleAddPromptFromModal}
onUpdatePromptFromModal={handleUpdatePromptFromModal}
onAddPipeline={handleAddPipeline}
onAddAgentToPipeline={handleAddAgentToPipeline}
onToggleAgent={handleToggleAgent}