Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions multi-agent/api/flask/endpoints/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,39 @@ def save_blueprint(blueprint_raw=None, user_id="alice", metadata=None):
return jsonify({"status": "error", "error": str(e)}), 500


@blueprints_bp.route("/blueprint.update", methods=["PUT"])
@from_body({
"blueprint_id": fields.Str(data_key="blueprintId", required=True),
"blueprint_raw": fields.Str(data_key="blueprintRaw", required=True),
})
def update_blueprint(blueprint_id, blueprint_raw):
"""Update an existing blueprint in-place, keeping the same ID."""
try:
parsed = _extract_blueprint_data(
json_field_value=blueprint_raw,
field_name="blueprint_raw"
)

svc = current_app.container.blueprint_service
success = svc.update_draft(blueprint_id=blueprint_id, draft_dict=parsed)

if not success:
return jsonify({"status": "error", "error": "Failed to update blueprint"}), 500

return jsonify({
"status": "success",
"blueprint_id": blueprint_id,
}), 200

except BlueprintNotFoundError as e:
return jsonify({"status": "error", "error": str(e)}), 404
except BadRequest as e:
return jsonify({"status": "error", "error": str(e)}), 400
except Exception as e:
logger.exception(f"Unexpected error updating blueprint {blueprint_id}")
return jsonify({"status": "error", "error": str(e)}), 500


@blueprints_bp.route("/blueprint.info.get", methods=["GET"])
@from_query({
"blueprint_id": fields.Str(data_key="blueprintId", required=True)
Expand Down
11 changes: 7 additions & 4 deletions multi-agent/blueprints/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,15 @@ def get_blueprint_draft_doc(self, blueprint_id: str) -> BlueprintDocument:
"""Get blueprint document with metadata for sharing operations."""
return self._repo.load(blueprint_id)

def update_draft(self, *, blueprint_id: str, draft_dict: dict) -> bool: # NEW
def update_draft(self, *, blueprint_id: str, draft_dict: dict) -> bool:
draft = BlueprintDraft(**draft_dict)
rid_refs = list(RefWalker.external_rids(draft))
return self._repo.update(
blueprint_id=blueprint_id, spec=draft, rid_refs=rid_refs
)
try:
return self._repo.update(
blueprint_id=blueprint_id, spec=draft, rid_refs=rid_refs
)
except KeyError:
raise BlueprintNotFoundError(blueprint_id) from None

def load_resolved(self, blueprint_id: str) -> BlueprintSpec:
return self._resolver.resolve(self.load_draft(blueprint_id))
Expand Down
14 changes: 14 additions & 0 deletions ui/client/src/api/blueprints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,20 @@ export async function saveBlueprint(
return data;
}

/**
* Update an existing blueprint in-place (keeps the same ID)
*/
export async function updateBlueprint(
blueprintId: string,
blueprintRaw: string,
): Promise<SaveBlueprintResponse> {
const { data } = await axios.put<SaveBlueprintResponse>('/blueprints/blueprint.update', {
blueprintId,
blueprintRaw,
});
return data;
}

// ────────────────────────────────────────────────────────────────────────────────
// Blueprint Metadata & Sharing
// ────────────────────────────────────────────────────────────────────────────────
Expand Down
4 changes: 4 additions & 0 deletions ui/client/src/components/agentic-ai/AgentFlowGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ type AgentFlowGraphProps = {
selectedFlow: FlowObject | null;
setSelectedFlow: (flow: FlowObject | null) => void;
onValidationChange?: (isValid: boolean, validationResult: BlueprintValidationResult | null, isValidating: boolean) => void;
onFlowEdit?: (flow: FlowObject) => void;
};

export default function AgentFlowGraph({
selectedFlow,
setSelectedFlow,
onValidationChange,
onFlowEdit,
}: AgentFlowGraphProps): React.ReactElement {

const handleFlowSelect = (flow: FlowObject | null): void => {
Expand All @@ -41,9 +43,11 @@ export default function AgentFlowGraph({
selectedFlow={selectedFlow}
onFlowSelect={handleFlowSelect}
onFlowDelete={handleFlowDelete}
onFlowEdit={onFlowEdit}
onValidationChange={onValidationChange}
showActiveStatus={true}
showDeleteButton={true}
showEditButton={true}
height="100%"
graphProps={{
showBackground: true,
Expand Down
25 changes: 24 additions & 1 deletion ui/client/src/components/agentic-ai/WorkflowsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback } from "react";
import { motion } from "framer-motion";
import { Trash2, Users } from "lucide-react";
import { Trash2, Users, Pencil } from "lucide-react";
import { useAuth } from "@/contexts/AuthContext";
import { useShared } from "@/contexts/SharedContext";
import { Button } from "@/components/ui/button";
Expand All @@ -26,9 +26,11 @@ export interface WorkflowsPanelProps {
selectedFlow: FlowObject | null;
onFlowSelect: (flow: FlowObject | null) => void;
onFlowDelete?: (flow: FlowObject) => void;
onFlowEdit?: (flow: FlowObject) => void;
onValidationChange?: (isValid: boolean, validationResult: BlueprintValidationResult | null, isValidating: boolean) => void;
showActiveStatus?: boolean;
showDeleteButton?: boolean;
showEditButton?: boolean;
className?: string;
height?: string;
graphProps?: {
Expand All @@ -41,9 +43,11 @@ export default function WorkflowsPanel({
selectedFlow,
onFlowSelect,
onFlowDelete,
onFlowEdit,
onValidationChange,
showActiveStatus = false,
showDeleteButton = false,
showEditButton = false,
className = "",
height = "100%",
graphProps = {
Expand Down Expand Up @@ -193,6 +197,13 @@ export default function WorkflowsPanel({
setShowDeleteModal(true);
};

const handleEditClick = (flow: FlowObject, event: React.MouseEvent) => {
event.stopPropagation();
if (onFlowEdit) {
onFlowEdit(flow);
}
};

const handleShareClick = (flow: FlowObject, event: React.MouseEvent) => {
event.stopPropagation(); // Prevent flow selection when clicking share
openShareForItem({
Expand Down Expand Up @@ -302,6 +313,18 @@ export default function WorkflowsPanel({
Active
</span>
)}
{showEditButton && (
<SimpleTooltip content={<p>Edit this workflow</p>}>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 hover:bg-primary/20 hover:text-primary"
onClick={(e) => handleEditClick(flow, e)}
>
<Pencil className="h-3 w-3" />
</Button>
</SimpleTooltip>
)}
<SimpleTooltip content={<p>Share this workflow</p>}>
<Button
variant="ghost"
Expand Down
4 changes: 4 additions & 0 deletions ui/client/src/components/agentic-ai/graphs/GraphCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ interface GraphCanvasProps {
onAttachCondition?: (nodeId: string, condition: any) => void;
onRemoveCondition?: (nodeId: string, conditionRid: string) => void;
isGraphValid?: boolean;
isEditMode?: boolean;
}

const GraphCanvas: React.FC<GraphCanvasProps> = ({
Expand All @@ -153,6 +154,7 @@ const GraphCanvas: React.FC<GraphCanvasProps> = ({
onAttachCondition,
onRemoveCondition,
isGraphValid = false,
isEditMode = false,
}) => {
const [showYamlDebug, setShowYamlDebug] = useState(false);
const { primaryHex } = useTheme();
Expand Down Expand Up @@ -214,6 +216,7 @@ const GraphCanvas: React.FC<GraphCanvasProps> = ({
onSaveGraph={onSaveGraph}
onBack={onBack}
isGraphValid={isGraphValid}
isEditMode={isEditMode}
/>
<CardContent className="p-0 h-full relative">
{/* YAML Debug Panel */}
Expand Down Expand Up @@ -255,6 +258,7 @@ const GraphCanvas: React.FC<GraphCanvasProps> = ({
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
fitView
fitViewOptions={{ padding: 0.15, maxZoom: 1.2 }}
defaultViewport={{ x: 0, y: 0, zoom: 0.33 }}
connectionLineType={ConnectionLineType.SmoothStep}
defaultEdgeOptions={{
Expand Down
6 changes: 4 additions & 2 deletions ui/client/src/components/agentic-ai/graphs/GraphHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ interface GraphHeaderProps {
onSaveGraph: () => void;
onBack?: () => void;
isGraphValid?: boolean;
isEditMode?: boolean;
}

const GraphHeader: React.FC<GraphHeaderProps> = ({
onClearGraph,
onSaveGraph,
onBack,
isGraphValid = false,
isEditMode = false,
}) => {
return (
<CardHeader className="py-3 px-6 border-b border-gray-800">
Expand All @@ -27,7 +29,7 @@ const GraphHeader: React.FC<GraphHeaderProps> = ({
<Button
variant="ghost"
size="sm"
onClick={onBack}
onClick={() => onBack()}
className="flex items-center gap-2"
>
<ArrowLeft className="w-4 h-4" />
Expand Down Expand Up @@ -55,7 +57,7 @@ const GraphHeader: React.FC<GraphHeaderProps> = ({
className="bg-primary hover:bg-primary/80 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save className="h-4 w-4 mr-2" />
Save Workflow
{isEditMode ? "Update Workflow" : "Save Workflow"}
</Button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
Expand All @@ -19,16 +19,29 @@ interface SaveBlueprintModalProps {
onClose: () => void;
onSave: (name: string, description: string) => void;
isLoading?: boolean;
isEditMode?: boolean;
currentName?: string;
currentDescription?: string;
}

const SaveBlueprintModal: React.FC<SaveBlueprintModalProps> = ({
isOpen,
onClose,
onSave,
isLoading = false,
isEditMode = false,
currentName = "",
currentDescription = "",
}) => {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [name, setName] = useState(currentName);
const [description, setDescription] = useState(currentDescription);

useEffect(() => {
if (isOpen) {
setName(currentName);
setDescription(currentDescription);
}
}, [isOpen]);

const handleSave = () => {
if (!name.trim()) {
Expand All @@ -38,8 +51,8 @@ const SaveBlueprintModal: React.FC<SaveBlueprintModalProps> = ({
};

const handleClose = () => {
setName("");
setDescription("");
setName(isEditMode ? currentName : "");
setDescription(isEditMode ? currentDescription : "");
onClose();
};

Expand All @@ -49,7 +62,9 @@ const SaveBlueprintModal: React.FC<SaveBlueprintModalProps> = ({
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px] bg-gray-900 border-gray-700">
<DialogHeader>
<DialogTitle className="text-white">Save Workflow</DialogTitle>
<DialogTitle className="text-white">
{isEditMode ? "Update Workflow" : "Save Workflow"}
</DialogTitle>
</DialogHeader>

<div className="space-y-4 py-4">
Expand Down Expand Up @@ -103,10 +118,10 @@ const SaveBlueprintModal: React.FC<SaveBlueprintModalProps> = ({
{isLoading ? (
<div className="flex items-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
Saving...
{isEditMode ? "Updating..." : "Saving..."}
</div>
) : (
"Save Workflow"
isEditMode ? "Update Workflow" : "Save Workflow"
)}
</Button>
</UmamiTrack>
Expand Down
Loading