Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update AGS to Use AgentChat Declarative Config Serialization #5261

Merged
merged 22 commits into from
Jan 31, 2025
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix drag drop error
victordibia committed Jan 30, 2025

Verified

This commit was signed with the committer’s verified signature.
alexcastilio Alex Castilio
commit f55d3dbb5c2bd103bb37d1f06386f426f64fa52c
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"id": "default_gallery",
"id": "gallery_default",
"name": "Default Component Gallery",
"url": null,
"metadata": {
"author": "AutoGen Team",
"created_at": "2025-01-29T21:27:14.029754",
"updated_at": "2025-01-30T05:27:43.594Z",
"created_at": "2025-01-29T22:13:58.687387",
"updated_at": "2025-01-29T22:13:58.715730",
"version": "1.0.0",
"description": "A default gallery containing basic components for human-in-loop conversations",
"tags": ["human-in-loop", "assistant"],
@@ -121,7 +121,7 @@
"version": 1,
"component_version": 1,
"description": "A group chat team that have participants takes turn to publish a message\n to all, using a ChatCompletion model to select the next speaker after each message.",
"label": "SelectorGroupChat_1738214891894",
"label": "Web Agents (Operator)",
"config": {
"participants": [
{
@@ -401,6 +401,18 @@
"model": "gpt-4o-mini"
}
},
{
"provider": "autogen_ext.models.openai.OpenAIChatCompletionClient",
"component_type": "model",
"version": 1,
"component_version": 1,
"description": "Example on how to use the OpenAIChatCopletionClient with local models (Ollama, vllm etc).",
"label": "Mistral-7B vllm",
"config": {
"model": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
"base_url": "http://localhost:1234/v1"
}
},
{
"provider": "autogen_ext.models.openai.OpenAIChatCompletionClient",
"component_type": "model",
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//team/builder/builder.tsx
import React, { useCallback, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
DndContext,
useSensor,
@@ -31,6 +31,7 @@ import "./builder.css";
import TeamBuilderToolbar from "./toolbar";
import { MonacoEditor } from "../../monaco";
import { NodeEditor } from "./node-editor/node-editor";
import debounce from "lodash.debounce";

const { Sider, Content } = Layout;

@@ -126,18 +127,27 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({

// Handle JSON changes
const handleJsonChange = useCallback(
(value: string) => {
debounce((value: string) => {
try {
const config = JSON.parse(value);
loadFromJson(config);
// dirty ?
// Always consider JSON edits as changes that should affect isDirty state
loadFromJson(config, false);
// Force history update even if nodes/edges appear same
useTeamBuilderStore.getState().addToHistory();
} catch (error) {
console.error("Invalid JSON:", error);
}
},
}, 1000),
[loadFromJson]
);

// Cleanup debounced function
useEffect(() => {
return () => {
handleJsonChange.cancel();
};
}, [handleJsonChange]);

// Handle save
const handleSave = useCallback(async () => {
try {
@@ -233,8 +243,7 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
const draggedItem = active.data.current.current;
const dropZoneId = over.id as string;

const [nodeId, zoneType] = dropZoneId.split("-zone")[0].split("-");

const [nodeId] = dropZoneId.split("@@@");
// Find target node
const targetNode = nodes.find((node) => node.id === nodeId);
if (!targetNode) return;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { memo } from "react";
import {
Handle,
Position,
@@ -63,34 +63,32 @@ interface DroppableZoneProps {
id: string; // Add this to make each zone uniquely identifiable
}

const DroppableZone: React.FC<DroppableZoneProps> = ({
accepts,
children,
className,
id,
}) => {
const { isOver, setNodeRef, active } = useDroppable({
id,
data: { accepts },
});

// Fix the data path to handle nested current objects
const isValidDrop =
isOver &&
active?.data?.current?.current?.type &&
accepts.includes(active.data.current.current.type);

return (
<div
ref={setNodeRef}
className={`droppable-zone p-2 ${isValidDrop ? "can-drop" : ""} ${
className || ""
}`}
>
{children}
</div>
);
};
const DroppableZone = memo<DroppableZoneProps>(
({ accepts, children, className, id }) => {
const { isOver, setNodeRef, active } = useDroppable({
id,
data: { accepts },
});

// Fix the data path to handle nested current objects
const isValidDrop =
isOver &&
active?.data?.current?.current?.type &&
accepts.includes(active.data.current.current.type);

return (
<div
ref={setNodeRef}
className={`droppable-zone p-2 ${isValidDrop ? "can-drop" : ""} ${
className || ""
}`}
>
{children}
</div>
);
}
);
DroppableZone.displayName = "DroppableZone";

// Base node layout component
interface BaseNodeProps extends NodeProps<CustomNode> {
@@ -103,80 +101,86 @@ interface BaseNodeProps extends NodeProps<CustomNode> {
onEditClick?: (id: string) => void;
}

const BaseNode: React.FC<BaseNodeProps> = ({
id,
data,
selected,
dragHandle,
icon: Icon,
children,
headerContent,
descriptionContent,
className,
onEditClick,
}) => {
const removeNode = useTeamBuilderStore((state) => state.removeNode);
const setSelectedNode = useTeamBuilderStore((state) => state.setSelectedNode);
const showDelete = data.type !== "team";

return (
<div
ref={dragHandle}
className={`
const BaseNode = memo<BaseNodeProps>(
({
id,
data,
selected,
dragHandle,
icon: Icon,
children,
headerContent,
descriptionContent,
className,
onEditClick,
}) => {
const removeNode = useTeamBuilderStore((state) => state.removeNode);
const setSelectedNode = useTeamBuilderStore(
(state) => state.setSelectedNode
);
const showDelete = data.type !== "team";

return (
<div
ref={dragHandle}
className={`
bg-white text-primary relative rounded-lg shadow-lg w-72
${selected ? "ring-2 ring-accent" : ""}
${className || ""}
transition-all duration-200
`}
>
<div className="border-b p-3 bg-gray-50 rounded-t-lg">
<div className="flex items-center justify-between min-w-0">
<div className="flex items-center gap-2 min-w-0 flex-1">
<Icon className="flex-shrink-0 w-5 h-5 text-gray-600" />
<span className="font-medium text-gray-800 truncate">
{data.component.label}
</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<span className="text-xs px-2 py-1 bg-gray-200 rounded text-gray-700">
{data.component.component_type}
</span>
<button
onClick={(e) => {
e.stopPropagation();
setSelectedNode(id);
}}
className="p-1 hover:bg-secondary rounded"
>
<Edit className="w-4 h-4 text-accent" />
</button>
{showDelete && (
<>
<button
onClick={(e) => {
console.log("remove node", id);
e.stopPropagation();
if (id) removeNode(id);
}}
className="p-1 hover:bg-red-100 rounded"
>
<Trash2Icon className="w-4 h-4 text-red-500" />
</button>
</>
)}
>
<div className="border-b p-3 bg-gray-50 rounded-t-lg">
<div className="flex items-center justify-between min-w-0">
<div className="flex items-center gap-2 min-w-0 flex-1">
<Icon className="flex-shrink-0 w-5 h-5 text-gray-600" />
<span className="font-medium text-gray-800 truncate">
{data.component.label}
</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<span className="text-xs px-2 py-1 bg-gray-200 rounded text-gray-700">
{data.component.component_type}
</span>
<button
onClick={(e) => {
e.stopPropagation();
setSelectedNode(id);
}}
className="p-1 hover:bg-secondary rounded"
>
<Edit className="w-4 h-4 text-accent" />
</button>
{showDelete && (
<>
<button
onClick={(e) => {
console.log("remove node", id);
e.stopPropagation();
if (id) removeNode(id);
}}
className="p-1 hover:bg-red-100 rounded"
>
<Trash2Icon className="w-4 h-4 text-red-500" />
</button>
</>
)}
</div>
</div>
{headerContent}
</div>

<div className="px-3 py-2 border-b text-sm text-gray-600">
{descriptionContent}
</div>
{headerContent}
</div>

<div className="px-3 py-2 border-b text-sm text-gray-600">
{descriptionContent}
<div className="p-3 space-y-2">{children}</div>
</div>
);
}
);

<div className="p-3 space-y-2">{children}</div>
</div>
);
};
BaseNode.displayName = "BaseNode";

// Reusable components
const NodeSection: React.FC<{
@@ -204,7 +208,7 @@ const ConnectionBadge: React.FC<{
);

// Team Node
export const TeamNode: React.FC<NodeProps<CustomNode>> = (props) => {
export const TeamNode = memo<NodeProps<CustomNode>>((props) => {
const component = props.data.component as Component<TeamConfig>;
const hasModel = isSelectorTeam(component) && !!component.config.model_client;
const participantCount = component.config.participants?.length || 0;
@@ -259,7 +263,7 @@ export const TeamNode: React.FC<NodeProps<CustomNode>> = (props) => {
{component.config.model_client.config.model}
</div>
)}
<DroppableZone id={`${props.id}-model-zone`} accepts={["model"]}>
<DroppableZone id={`${props.id}@@@model-zone`} accepts={["model"]}>
<div className="text-secondary text-xs my-1 text-center">
Drop model here
</div>
@@ -292,7 +296,7 @@ export const TeamNode: React.FC<NodeProps<CustomNode>> = (props) => {
<span>{participant.config.name}</span>
</div>
))}
<DroppableZone id={`${props.id}-agent-zone`} accepts={["agent"]}>
<DroppableZone id={`${props.id}@@@agent-zone`} accepts={["agent"]}>
<div className="text-secondary text-xs my-1 text-center">
Drop agents here
</div>
@@ -331,9 +335,11 @@ export const TeamNode: React.FC<NodeProps<CustomNode>> = (props) => {
</NodeSection>
</BaseNode>
);
};
});

TeamNode.displayName = "TeamNode";

export const AgentNode: React.FC<NodeProps<CustomNode>> = (props) => {
export const AgentNode = memo<NodeProps<CustomNode>>((props) => {
const component = props.data.component as Component<AgentConfig>;
const hasModel =
isAssistantAgent(component) && !!component.config.model_client;
@@ -391,7 +397,10 @@ export const AgentNode: React.FC<NodeProps<CustomNode>> = (props) => {
{component.config.model_client.config.model}
</div>
)}
<DroppableZone id={`${props.id}-model-zone`} accepts={["model"]}>
<DroppableZone
id={`${props.id}@@@model-zone`}
accepts={["model"]}
>
<div className="text-secondary text-xs my-1 text-center">
Drop model here
</div>
@@ -421,7 +430,10 @@ export const AgentNode: React.FC<NodeProps<CustomNode>> = (props) => {
))}
</div>
)}
<DroppableZone id={`${props.id}-tool-zone`} accepts={["tool"]}>
<DroppableZone
id={`${props.id}@@@tool-zone`}
accepts={["tool"]}
>
<div className="text-secondary text-xs my-1 text-center">
Drop tools here
</div>
@@ -433,134 +445,14 @@ export const AgentNode: React.FC<NodeProps<CustomNode>> = (props) => {
)}
</BaseNode>
);
};

// Model Node
export const ModelNode: React.FC<NodeProps<CustomNode>> = (props) => {
const component = props.data.component as Component<ModelConfig>;
const isOpenAI = isOpenAIModel(component);
const isAzure = isAzureOpenAIModel(component);

return (
<BaseNode
{...props}
icon={iconMap.model}
descriptionContent={
<div>
<div className="break-words"> {component.description}</div>
{isOpenAI && component.config.base_url && (
<div className="mt-1 text-xs">URL: {component.config.base_url}</div>
)}
{isAzure && (
<div className="mt-1 text-xs">
Endpoint: {component.config.azure_endpoint}
</div>
)}
</div>
}
>
<Handle
type="source"
position={Position.Right}
id={`${props.id}-model-output-handle`}
className="my-right-handle"
/>
<NodeSection title="Configuration">
<div className="text-sm">Model: {component.config.model}</div>
</NodeSection>
</BaseNode>
);
};

// Tool Node
export const ToolNode: React.FC<NodeProps<CustomNode>> = (props) => {
const component = props.data.component as Component<ToolConfig>;
const isFunctionToolType = isFunctionTool(component);

return (
<BaseNode
{...props}
icon={iconMap.tool}
descriptionContent={
<div
className=" "
title={component.description || component.config.name}
>
{" "}
{component.config.name}
</div>
}
>
<Handle
type="source"
position={Position.Right}
id={`${props.id}-tool-output-handle`}
className="my-right-handle"
/>
<NodeSection title="Configuration">
<div className="text-sm">{component.config.description}</div>
</NodeSection>

{isFunctionToolType && (
<NodeSection title="Content">
<div className="text-sm break-all">
<TruncatableText
content={component.config.source_code || ""}
textThreshold={150}
/>
</div>
</NodeSection>
)}
</BaseNode>
);
};

// Termination Node

// First, let's add the Termination Node component
export const TerminationNode: React.FC<NodeProps<CustomNode>> = (props) => {
const component = props.data.component as Component<TerminationConfig>;
const isMaxMessages = isMaxMessageTermination(component);
const isTextMention = isTextMentionTermination(component);
const isOr = isOrTermination(component);

return (
<BaseNode
{...props}
icon={iconMap.termination}
descriptionContent={
<div> {component.description || component.label}</div>
}
>
<Handle
type="source"
position={Position.Right}
id={`${props.id}-termination-output-handle`}
className="my-right-handle"
/>
});

<NodeSection title="Configuration">
<div className="text-sm">
{isMaxMessages && (
<div>Max Messages: {component.config.max_messages}</div>
)}
{isTextMention && <div>Text: {component.config.text}</div>}
{isOr && (
<div>OR Conditions: {component.config.conditions.length}</div>
)}
</div>
</NodeSection>
</BaseNode>
);
};
AgentNode.displayName = "AgentNode";

// Export all node types
export const nodeTypes = {
team: TeamNode,
agent: AgentNode,
model: ModelNode,
tool: ToolNode,
termination: TerminationNode,
};

const EDGE_STYLES = {
Original file line number Diff line number Diff line change
@@ -60,9 +60,13 @@ export interface TeamBuilderState {

// Sync with JSON
syncToJson: () => Component<TeamConfig> | null;
loadFromJson: (config: Component<TeamConfig>) => GraphState;
loadFromJson: (
config: Component<TeamConfig>,
isInitialLoad?: boolean
) => GraphState;
layoutNodes: () => void;
resetHistory: () => void;
addToHistory: () => void;
}

const buildTeamComponent = (
@@ -109,13 +113,6 @@ export const useTeamBuilderStore = create<TeamBuilderState>((set, get) => ({
let newNodes = [...state.nodes];
let newEdges = [...state.edges];

console.log(
"Adding node",
clonedComponent,
isTerminationComponent(clonedComponent),
targetNodeId
);

if (targetNodeId) {
const targetNode = state.nodes.find((n) => n.id === targetNodeId);

@@ -520,27 +517,46 @@ export const useTeamBuilderStore = create<TeamBuilderState>((set, get) => ({
});
},

loadFromJson: (config: Component<TeamConfig>) => {
loadFromJson: (
config: Component<TeamConfig>,
isInitialLoad: boolean = true
) => {
// Get graph representation of team config
const { nodes, edges } = convertTeamConfigToGraph(config);

// Apply layout to elements
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
nodes,
edges
);

// Update store with new state and reset history
set({
nodes: layoutedNodes,
edges: layoutedEdges,
originalComponent: config,
history: [{ nodes: layoutedNodes, edges: layoutedEdges }],
currentHistoryIndex: 0,
selectedNodeId: null,
});
if (isInitialLoad) {
// Initial load - reset history
set({
nodes: layoutedNodes,
edges: layoutedEdges,
originalComponent: config,
history: [{ nodes: layoutedNodes, edges: layoutedEdges }],
currentHistoryIndex: 0,
selectedNodeId: null,
});
} else {
// JSON edit - check if state actually changed
const currentState = get();
if (
!isEqual(layoutedNodes, currentState.nodes) ||
!isEqual(layoutedEdges, currentState.edges)
) {
set((state) => ({
nodes: layoutedNodes,
edges: layoutedEdges,
history: [
...state.history.slice(0, state.currentHistoryIndex + 1),
{ nodes: layoutedNodes, edges: layoutedEdges },
].slice(-MAX_HISTORY),
currentHistoryIndex: state.currentHistoryIndex + 1,
}));
}
}

// Return final graph state
return { nodes: layoutedNodes, edges: layoutedEdges };
},

@@ -550,4 +566,14 @@ export const useTeamBuilderStore = create<TeamBuilderState>((set, get) => ({
currentHistoryIndex: 0,
}));
},

addToHistory: () => {
set((state) => ({
history: [
...state.history.slice(0, state.currentHistoryIndex + 1),
{ nodes: state.nodes, edges: state.edges },
].slice(-MAX_HISTORY),
currentHistoryIndex: state.currentHistoryIndex + 1,
}));
},
}));