Improves editing UI for tools in AGS (#5539)

<!-- Thank you for your contribution! Please review
https://microsoft.github.io/autogen/docs/Contribute before opening a
pull request. -->

<!-- Please add a reviewer to the assignee section when you create a PR.
If you don't have the access to it, we will shortly find a reviewer and
assign them to your PR. -->
<img width="1560" alt="image"
src="https://github.com/user-attachments/assets/da3d781f-2572-4bd7-802d-1d3900f6c6d9"
/>

## Why are these changes needed?

Improves  editing UI for tools in AGS
- add remove tools 
- add/remove imports 
- show source code section of FunctionTool in  in code editor

<!-- Please give a short summary of the change and the problem this
solves. -->

## Related issue number

<!-- For example: "Closes #1234" -->

## Checks

- [ ] I've included any doc changes needed for
https://microsoft.github.io/autogen/. See
https://microsoft.github.io/autogen/docs/Contribute#documentation to
build and test documentation locally.
- [ ] I've added tests (if relevant) corresponding to the changes
introduced in this PR.
- [ ] I've made sure all auto checks have passed.
This commit is contained in:
Victor Dibia
2025-02-17 17:24:43 -08:00
committed by GitHub
parent 9a10427a7d
commit e02db84586
12 changed files with 1088 additions and 545 deletions

View File

@@ -77,12 +77,20 @@ export type AgentMessageConfig =
| ToolCallMessageConfig
| ToolCallResultMessageConfig;
// Tool Configs
export interface FromModuleImport {
module: string;
imports: string[];
}
// Import can be either a string (direct import) or a FromModuleImport
export type Import = string | FromModuleImport;
// The complete FunctionToolConfig interface
export interface FunctionToolConfig {
source_code: string;
name: string;
description: string;
global_imports: any[]; // Sequence[Import] equivalent
global_imports: Import[];
has_cancellation_support: boolean;
}

View File

@@ -34,8 +34,7 @@ const PROVIDERS = {
// Models
OPENAI: "autogen_ext.models.openai.OpenAIChatCompletionClient",
AZURE_OPENAI:
"autogen_ext.models.azure_openai.AzureOpenAIChatCompletionClient",
AZURE_OPENAI: "autogen_ext.models.openai.AzureOpenAIChatCompletionClient",
// Tools
FUNCTION_TOOL: "autogen_core.tools.FunctionTool",

View File

@@ -1,11 +1,5 @@
//team/builder/builder.tsx
import React, {
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
DndContext,
useSensor,
@@ -26,7 +20,7 @@ import {
MiniMap,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { Button, Layout, message, Modal, Switch, Tooltip } from "antd";
import { Button, Layout, message, Switch, Tooltip } from "antd";
import {
Cable,
CheckCircle,
@@ -39,7 +33,7 @@ import {
} from "lucide-react";
import { useTeamBuilderStore } from "./store";
import { ComponentLibrary } from "./library";
import { ComponentTypes, Team, Session } from "../../../types/datamodel";
import { ComponentTypes, Team } from "../../../types/datamodel";
import { CustomNode, CustomEdge, DragItem } from "./types";
import { edgeTypes, nodeTypes } from "./nodes";
@@ -49,11 +43,8 @@ import TeamBuilderToolbar from "./toolbar";
import { MonacoEditor } from "../../monaco";
import { NodeEditor } from "./node-editor/node-editor";
import debounce from "lodash.debounce";
import { appContext } from "../../../../hooks/provider";
import { sessionAPI } from "../../playground/api";
import TestDrawer from "./testdrawer";
import { teamAPI, validationAPI, ValidationResponse } from "../api";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import { validationAPI, ValidationResponse } from "../api";
import { ValidationErrors } from "./validationerrors";
const { Sider, Content } = Layout;

View File

@@ -1,524 +0,0 @@
import React from "react";
import { Input, Select, Switch, InputNumber, Form, Button } from "antd";
import { Edit } from "lucide-react";
import {
isTeamComponent,
isAgentComponent,
isModelComponent,
isToolComponent,
isTerminationComponent,
isSelectorTeam,
isRoundRobinTeam,
isAssistantAgent,
isUserProxyAgent,
isWebSurferAgent,
isOpenAIModel,
isAzureOpenAIModel,
isFunctionTool,
isOrTermination,
isMaxMessageTermination,
isTextMentionTermination,
} from "../../../../types/guards";
import { Component, ComponentConfig } from "../../../../types/datamodel";
import DetailGroup from "./detailgroup";
const { TextArea } = Input;
const { Option } = Select;
interface NodeEditorFieldsProps {
component: Component<ComponentConfig>;
onNavigate: (componentType: string, id: string, parentField: string) => void;
}
export const NodeEditorFields: React.FC<NodeEditorFieldsProps> = ({
component,
onNavigate,
}) => {
const renderNestedComponentButton = (
label: string,
component: Component<ComponentConfig> | Component<ComponentConfig>[],
parentField: string
) => {
if (Array.isArray(component)) {
return (
<div className="space-y-2 mb-4">
<div className="flex justify-between items-center">
<span className="text-sm font-medium">{label}</span>
</div>
{component.map((item) => (
<Button
key={item.label}
onClick={() =>
onNavigate(item.component_type, item.label || "", parentField)
}
className="w-full flex justify-between items-center"
>
<span>{item.label}</span>
<Edit className="w-4 h-4" />
</Button>
))}
</div>
);
}
return component ? (
<div className="mb-4">
<Button
onClick={() =>
onNavigate(
component.component_type,
component.label || "",
parentField
)
}
className="w-full flex justify-between items-center"
>
<span>{label}</span>
<Edit className="w-4 h-4" />
</Button>
</div>
) : null;
};
const renderTeamFields = () => {
if (!component) return null;
if (isSelectorTeam(component)) {
return (
<>
<Form.Item
label="Selector Prompt"
name={["config", "selector_prompt"]}
rules={[{ required: true }]}
>
<TextArea rows={4} />
</Form.Item>
<Form.Item label="Max Turns" name={["config", "max_turns"]}>
<InputNumber min={1} />
</Form.Item>
<Form.Item
label="Allow Repeated Speaker"
name={["config", "allow_repeated_speaker"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
{component.config.model_client &&
renderNestedComponentButton(
"Model Client",
component.config.model_client,
"model_client"
)}
{component.config.termination_condition &&
renderNestedComponentButton(
"Termination Condition",
component.config.termination_condition,
"termination_condition"
)}
</>
);
}
if (isRoundRobinTeam(component)) {
return (
<>
<Form.Item label="Max Turns" name={["config", "max_turns"]}>
<InputNumber min={1} />
</Form.Item>
{component.config.termination_condition &&
renderNestedComponentButton(
"Termination Condition",
component.config.termination_condition,
"termination_condition"
)}
</>
);
}
return null;
};
const renderAgentFields = () => {
if (!component) return null;
if (isAssistantAgent(component)) {
return (
<>
<Form.Item
label="Name"
name={["config", "name"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Description"
name={["config", "description"]}
rules={[{ required: true }]}
>
<TextArea rows={4} />
</Form.Item>
<Form.Item label="System Message" name={["config", "system_message"]}>
<TextArea rows={4} />
</Form.Item>
<Form.Item
label="Reflect on Tool Use"
name={["config", "reflect_on_tool_use"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Tool Call Summary Format"
name={["config", "tool_call_summary_format"]}
>
<Input />
</Form.Item>
{component.config.model_client &&
renderNestedComponentButton(
"Model Client",
component.config.model_client,
"model_client"
)}
{component.config.tools &&
component.config.tools.length > 0 &&
renderNestedComponentButton(
"Tools",
component.config.tools,
"tools"
)}
</>
);
}
if (isUserProxyAgent(component)) {
return (
<>
<Form.Item
label="Name"
name={["config", "name"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Description"
name={["config", "description"]}
rules={[{ required: true }]}
>
<TextArea rows={4} />
</Form.Item>
</>
);
}
if (isWebSurferAgent(component)) {
return (
<>
<Form.Item
label="Name"
name={["config", "name"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Downloads Folder"
name={["config", "downloads_folder"]}
>
<Input />
</Form.Item>
<Form.Item label="Description" name={["config", "description"]}>
<TextArea rows={4} />
</Form.Item>
<Form.Item label="Start Page" name={["config", "start_page"]}>
<Input />
</Form.Item>
<Form.Item
label="Headless"
name={["config", "headless"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Animate Actions"
name={["config", "animate_actions"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Save Screenshots"
name={["config", "to_save_screenshots"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Use OCR"
name={["config", "use_ocr"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Browser Channel"
name={["config", "browser_channel"]}
>
<Input />
</Form.Item>
<Form.Item
label="Browser Data Directory"
name={["config", "browser_data_dir"]}
>
<Input />
</Form.Item>
<Form.Item
label="Resize Viewport"
name={["config", "to_resize_viewport"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
{component.config.model_client &&
renderNestedComponentButton(
"Model Client",
component.config.model_client,
"model_client"
)}
</>
);
}
return null;
};
const renderModelFields = () => {
if (!component) return null;
const createArgumentsFields = (
<>
<Form.Item label="Temperature" name={["config", "temperature"]}>
<InputNumber min={0} max={2} step={0.1} />
</Form.Item>
<Form.Item label="Max Tokens" name={["config", "max_tokens"]}>
<InputNumber min={1} />
</Form.Item>
<Form.Item label="Top P" name={["config", "top_p"]}>
<InputNumber min={0} max={1} step={0.1} />
</Form.Item>
<Form.Item
label="Frequency Penalty"
name={["config", "frequency_penalty"]}
>
<InputNumber min={-2} max={2} step={0.1} />
</Form.Item>
<Form.Item
label="Presence Penalty"
name={["config", "presence_penalty"]}
>
<InputNumber min={-2} max={2} step={0.1} />
</Form.Item>
<Form.Item label="Stop Sequences" name={["config", "stop"]}>
<Select mode="tags" />
</Form.Item>
</>
);
if (isOpenAIModel(component)) {
return (
<>
<Form.Item
label="Model"
name={["config", "model"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item label="API Key" name={["config", "api_key"]}>
<Input.Password />
</Form.Item>
<Form.Item label="Organization" name={["config", "organization"]}>
<Input />
</Form.Item>
<Form.Item label="Base URL" name={["config", "base_url"]}>
<Input />
</Form.Item>
<Form.Item label="Timeout" name={["config", "timeout"]}>
<InputNumber min={1} />
</Form.Item>
<Form.Item label="Max Retries" name={["config", "max_retries"]}>
<InputNumber min={0} />
</Form.Item>
{createArgumentsFields}
</>
);
}
if (isAzureOpenAIModel(component)) {
return (
<>
<Form.Item
label="Model"
name={["config", "model"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Azure Endpoint"
name={["config", "azure_endpoint"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Azure Deployment"
name={["config", "azure_deployment"]}
>
<Input />
</Form.Item>
<Form.Item
label="API Version"
name={["config", "api_version"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item label="Azure AD Token" name={["config", "azure_ad_token"]}>
<Input.Password />
</Form.Item>
{createArgumentsFields}
</>
);
}
return null;
};
const renderToolFields = () => {
if (!component) return null;
if (isFunctionTool(component)) {
return (
<>
<Form.Item
label="Name"
name={["config", "name"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Description"
name={["config", "description"]}
rules={[{ required: true }]}
>
<TextArea rows={4} />
</Form.Item>
<Form.Item
label="Source Code"
name={["config", "source_code"]}
rules={[{ required: true }]}
>
<TextArea rows={8} />
</Form.Item>
<Form.Item
label="Has Cancellation Support"
name={["config", "has_cancellation_support"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
</>
);
}
return null;
};
const renderTerminationFields = () => {
if (!component) return null;
if (isOrTermination(component)) {
return (
<>
<Form.Item
label="Number of Conditions"
name={["config", "conditions"]}
>
<InputNumber disabled />
</Form.Item>
{component.config.conditions &&
component.config.conditions.length > 0 &&
renderNestedComponentButton(
"Conditions",
component.config.conditions,
"conditions"
)}
</>
);
}
if (isMaxMessageTermination(component)) {
return (
<Form.Item
label="Max Messages"
name={["config", "max_messages"]}
rules={[{ required: true }]}
>
<InputNumber min={1} />
</Form.Item>
);
}
if (isTextMentionTermination(component)) {
return (
<Form.Item
label="Text"
name={["config", "text"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
);
}
return null;
};
// Common fields for all components
const commonFields = (
<DetailGroup title="Component Details">
<Form.Item label="Label" name="label">
<Input />
</Form.Item>
<Form.Item label="Description" name="description">
<TextArea rows={4} />
</Form.Item>
</DetailGroup>
);
// Component-specific fields
let specificFields = null;
if (isTeamComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">{renderTeamFields()}</DetailGroup>
);
} else if (isAgentComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">{renderAgentFields()}</DetailGroup>
);
} else if (isModelComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">{renderModelFields()}</DetailGroup>
);
} else if (isToolComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">{renderToolFields()}</DetailGroup>
);
} else if (isTerminationComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">
{renderTerminationFields()}
</DetailGroup>
);
}
return (
<>
{commonFields}
{specificFields}
</>
);
};
export default NodeEditorFields;

View File

@@ -0,0 +1,185 @@
// fields/agent-fields.tsx
import React from "react";
import { Form, Input, Switch } from "antd";
import {
isAssistantAgent,
isUserProxyAgent,
isWebSurferAgent,
} from "../../../../../types/guards";
import { NestedComponentButton } from "./fields";
import { NodeEditorFieldsProps } from "./fields";
const { TextArea } = Input;
export const AgentFields: React.FC<NodeEditorFieldsProps> = ({
component,
onNavigate,
workingCopy,
setWorkingCopy,
editPath,
updateComponentAtPath,
getCurrentComponent,
}) => {
if (!component) return null;
if (isAssistantAgent(component)) {
return (
<>
<Form.Item
label="Name"
name={["config", "name"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Description"
name={["config", "description"]}
rules={[{ required: true }]}
>
<TextArea rows={4} />
</Form.Item>
<Form.Item label="System Message" name={["config", "system_message"]}>
<TextArea rows={4} />
</Form.Item>
<Form.Item
label="Reflect on Tool Use"
name={["config", "reflect_on_tool_use"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Tool Call Summary Format"
name={["config", "tool_call_summary_format"]}
>
<Input />
</Form.Item>
{component.config.model_client && (
<NestedComponentButton
label="Model Client"
component={component.config.model_client}
parentField="model_client"
onNavigate={onNavigate}
/>
)}
{component.config.tools && component.config.tools.length > 0 && (
<NestedComponentButton
label="Tools"
component={component.config.tools}
parentField="tools"
onNavigate={onNavigate}
workingCopy={workingCopy}
setWorkingCopy={setWorkingCopy}
editPath={editPath}
updateComponentAtPath={updateComponentAtPath}
getCurrentComponent={getCurrentComponent}
/>
)}
</>
);
}
if (isUserProxyAgent(component)) {
return (
<>
<Form.Item
label="Name"
name={["config", "name"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Description"
name={["config", "description"]}
rules={[{ required: true }]}
>
<TextArea rows={4} />
</Form.Item>
</>
);
}
if (isWebSurferAgent(component)) {
return (
<>
<Form.Item
label="Name"
name={["config", "name"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Downloads Folder"
name={["config", "downloads_folder"]}
>
<Input />
</Form.Item>
<Form.Item label="Description" name={["config", "description"]}>
<TextArea rows={4} />
</Form.Item>
<Form.Item label="Start Page" name={["config", "start_page"]}>
<Input />
</Form.Item>
<Form.Item
label="Headless"
name={["config", "headless"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Animate Actions"
name={["config", "animate_actions"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Save Screenshots"
name={["config", "to_save_screenshots"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Use OCR"
name={["config", "use_ocr"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item label="Browser Channel" name={["config", "browser_channel"]}>
<Input />
</Form.Item>
<Form.Item
label="Browser Data Directory"
name={["config", "browser_data_dir"]}
>
<Input />
</Form.Item>
<Form.Item
label="Resize Viewport"
name={["config", "to_resize_viewport"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
{component.config.model_client && (
<NestedComponentButton
label="Model Client"
component={component.config.model_client}
parentField="model_client"
onNavigate={onNavigate}
/>
)}
</>
);
}
return null;
};
export default AgentFields;

View File

@@ -0,0 +1,282 @@
import React from "react";
import { Button, Form, Input } from "antd";
import {
Component,
ComponentConfig,
FunctionToolConfig,
} from "../../../../../types/datamodel";
import {
isTeamComponent,
isAgentComponent,
isModelComponent,
isToolComponent,
isTerminationComponent,
} from "../../../../../types/guards";
import DetailGroup from "../detailgroup";
import { TeamFields } from "./team-fields";
import { AgentFields } from "./agent-fields";
import { ModelFields } from "./model-fields";
import { ToolFields } from "./tool-fields";
import { TerminationFields } from "./termination-fields";
import { Edit, MinusCircle, PlusCircle } from "lucide-react";
import { EditPath } from "../node-editor";
const { TextArea } = Input;
export interface NodeEditorFieldsProps {
component: Component<ComponentConfig>;
onNavigate: (componentType: string, id: string, parentField: string) => void;
workingCopy: Component<ComponentConfig> | null;
setWorkingCopy: (component: Component<ComponentConfig> | null) => void;
editPath: EditPath[];
updateComponentAtPath: (
root: Component<ComponentConfig>,
path: EditPath[],
updates: Partial<Component<ComponentConfig>>
) => Component<ComponentConfig>;
getCurrentComponent: (
root: Component<ComponentConfig>
) => Component<ComponentConfig> | null;
}
const NodeEditorFields: React.FC<NodeEditorFieldsProps> = ({
component,
onNavigate,
workingCopy,
setWorkingCopy,
editPath,
updateComponentAtPath,
getCurrentComponent,
}) => {
let specificFields = null;
if (isTeamComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">
<TeamFields
component={component}
onNavigate={onNavigate}
workingCopy={workingCopy}
setWorkingCopy={setWorkingCopy}
editPath={editPath}
updateComponentAtPath={updateComponentAtPath}
getCurrentComponent={getCurrentComponent}
/>
</DetailGroup>
);
} else if (isAgentComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">
<AgentFields
component={component}
onNavigate={onNavigate}
workingCopy={workingCopy}
setWorkingCopy={setWorkingCopy}
editPath={editPath}
updateComponentAtPath={updateComponentAtPath}
getCurrentComponent={getCurrentComponent}
/>
</DetailGroup>
);
} else if (isModelComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">
<ModelFields component={component} />
</DetailGroup>
);
} else if (isToolComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">
<ToolFields
component={component}
workingCopy={workingCopy}
setWorkingCopy={setWorkingCopy}
editPath={editPath}
updateComponentAtPath={updateComponentAtPath}
getCurrentComponent={getCurrentComponent}
/>
</DetailGroup>
);
} else if (isTerminationComponent(component)) {
specificFields = (
<DetailGroup title="Configuration">
<TerminationFields component={component} onNavigate={onNavigate} />
</DetailGroup>
);
}
return (
<>
<DetailGroup title="Component Details">
<CommonFields />
</DetailGroup>
{specificFields}
</>
);
};
export default NodeEditorFields;
// // fields/common-fields.tsx
export const CommonFields: React.FC = () => {
return (
<>
<Form.Item label="Label" name="label">
<Input />
</Form.Item>
<Form.Item label="Description" name="description">
<TextArea rows={4} />
</Form.Item>
</>
);
};
interface NestedComponentButtonProps {
label: string;
component: Component<ComponentConfig> | Component<ComponentConfig>[];
parentField: string;
onNavigate: (componentType: string, id: string, parentField: string) => void;
workingCopy?: Component<ComponentConfig> | null;
setWorkingCopy?: (component: Component<ComponentConfig> | null) => void;
editPath?: any[];
updateComponentAtPath?: any;
getCurrentComponent?: any;
}
export const NestedComponentButton: React.FC<NestedComponentButtonProps> = ({
label,
component,
parentField,
onNavigate,
workingCopy,
setWorkingCopy,
editPath,
updateComponentAtPath,
getCurrentComponent,
}) => {
if (Array.isArray(component)) {
return (
<div className="space-y-2 mb-4">
<div className="flex justify-between items-center">
<span className="text-sm font-medium">{label}</span>
{parentField === "tools" && (
<Button
type="dashed"
size="small"
onClick={() => {
const blankTool: Component<FunctionToolConfig> = {
provider: "autogen_core.tools.FunctionTool",
component_type: "tool",
version: 1,
component_version: 1,
description:
"Create custom tools by wrapping standard Python functions.",
label: "New Tool",
config: {
source_code: "def new_function():\n pass",
name: "new_function",
description: "Description of the new function",
global_imports: [],
has_cancellation_support: false,
},
};
if (
workingCopy &&
setWorkingCopy &&
updateComponentAtPath &&
getCurrentComponent &&
editPath
) {
const currentTools =
component as Component<ComponentConfig>[];
const updatedTools = [...currentTools, blankTool];
const updatedCopy = updateComponentAtPath(
workingCopy,
editPath,
{
config: {
...getCurrentComponent(workingCopy)?.config,
tools: updatedTools,
},
}
);
setWorkingCopy(updatedCopy);
}
}}
icon={<PlusCircle className="w-4 h-4" />}
>
Add Tool
</Button>
)}
</div>
{component.map((item, index) => (
<div key={item.label} className="flex items-center gap-2">
<Button
onClick={() =>
onNavigate(item.component_type, item.label || "", parentField)
}
className="w-full flex justify-between items-center"
>
<span>{item.label}</span>
<Edit className="w-4 h-4" />
</Button>
{parentField === "tools" && (
<Button
type="text"
danger
icon={<MinusCircle className="w-4 h-4" />}
onClick={() => {
if (
workingCopy &&
setWorkingCopy &&
updateComponentAtPath &&
getCurrentComponent &&
editPath
) {
const currentTools =
component as Component<ComponentConfig>[];
const updatedTools = currentTools.filter(
(_, idx) => idx !== index
);
const updatedCopy = updateComponentAtPath(
workingCopy,
editPath,
{
config: {
...getCurrentComponent(workingCopy)?.config,
tools: updatedTools,
},
}
);
setWorkingCopy(updatedCopy);
}
}}
/>
)}
</div>
))}
</div>
);
}
return component ? (
<div className="mb-4">
<Button
onClick={() =>
onNavigate(
component.component_type,
component.label || "",
parentField
)
}
className="w-full flex justify-between items-center"
>
<span>{label}</span>
<Edit className="w-4 h-4" />
</Button>
</div>
) : null;
};

View File

@@ -0,0 +1,182 @@
// fields/model-fields.tsx
import React from "react";
import { Form, Input, InputNumber, Select } from "antd";
import { Component, ComponentConfig } from "../../../../../types/datamodel";
import { isOpenAIModel, isAzureOpenAIModel } from "../../../../../types/guards";
interface ModelFieldsProps {
component: Component<ComponentConfig>;
}
export const ModelFields: React.FC<ModelFieldsProps> = ({ component }) => {
if (!component) return null;
// Common arguments fields shared between OpenAI and Azure OpenAI models
const ArgumentsFields = () => (
<>
<Form.Item
label="Temperature"
name={["config", "temperature"]}
tooltip="Controls randomness in the model's output. Higher values (e.g., 0.8) make output more random, lower values (e.g., 0.2) make it more focused."
>
<InputNumber min={0} max={2} step={0.1} />
</Form.Item>
<Form.Item
label="Max Tokens"
name={["config", "max_tokens"]}
tooltip="Maximum length of the model's output in tokens"
>
<InputNumber min={1} />
</Form.Item>
<Form.Item
label="Top P"
name={["config", "top_p"]}
tooltip="Controls diversity via nucleus sampling. Lower values (e.g., 0.1) make output more focused, higher values (e.g., 0.9) make it more diverse."
>
<InputNumber min={0} max={1} step={0.1} />
</Form.Item>
<Form.Item
label="Frequency Penalty"
name={["config", "frequency_penalty"]}
tooltip="Decreases the model's likelihood to repeat the same information. Values range from -2.0 to 2.0."
>
<InputNumber min={-2} max={2} step={0.1} />
</Form.Item>
<Form.Item
label="Presence Penalty"
name={["config", "presence_penalty"]}
tooltip="Increases the model's likelihood to talk about new topics. Values range from -2.0 to 2.0."
>
<InputNumber min={-2} max={2} step={0.1} />
</Form.Item>
<Form.Item
label="Stop Sequences"
name={["config", "stop"]}
tooltip="Sequences where the model will stop generating further tokens"
>
<Select
mode="tags"
placeholder="Enter stop sequences"
style={{ width: "100%" }}
/>
</Form.Item>
</>
);
if (isOpenAIModel(component)) {
return (
<>
<Form.Item
label="Model"
name={["config", "model"]}
rules={[{ required: true }]}
tooltip="The name of the OpenAI model to use (e.g., gpt-4, gpt-3.5-turbo)"
>
<Input />
</Form.Item>
<Form.Item
label="API Key"
name={["config", "api_key"]}
tooltip="Your OpenAI API key"
>
<Input.Password />
</Form.Item>
<Form.Item
label="Organization"
name={["config", "organization"]}
tooltip="Optional: Your OpenAI organization ID"
>
<Input />
</Form.Item>
<Form.Item
label="Base URL"
name={["config", "base_url"]}
tooltip="Optional: Custom base URL for API requests"
>
<Input />
</Form.Item>
<Form.Item
label="Timeout"
name={["config", "timeout"]}
tooltip="Request timeout in seconds"
>
<InputNumber min={1} />
</Form.Item>
<Form.Item
label="Max Retries"
name={["config", "max_retries"]}
tooltip="Maximum number of retry attempts for failed requests"
>
<InputNumber min={0} />
</Form.Item>
<ArgumentsFields />
</>
);
}
if (isAzureOpenAIModel(component)) {
return (
<>
<Form.Item
label="Model"
name={["config", "model"]}
rules={[{ required: true }]}
tooltip="The name of the Azure OpenAI model deployment"
>
<Input />
</Form.Item>
<Form.Item
label="Azure Endpoint"
name={["config", "azure_endpoint"]}
rules={[{ required: true }]}
tooltip="Your Azure OpenAI service endpoint URL"
>
<Input />
</Form.Item>
<Form.Item
label="Azure Deployment"
name={["config", "azure_deployment"]}
tooltip="The name of your Azure OpenAI model deployment"
>
<Input />
</Form.Item>
<Form.Item
label="API Version"
name={["config", "api_version"]}
rules={[{ required: true }]}
tooltip="Azure OpenAI API version (e.g., 2023-05-15)"
>
<Input />
</Form.Item>
<Form.Item
label="Azure AD Token"
name={["config", "azure_ad_token"]}
tooltip="Optional: Azure Active Directory token for authentication"
>
<Input.Password />
</Form.Item>
<ArgumentsFields />
</>
);
}
return null;
};
export default ModelFields;

View File

@@ -0,0 +1,77 @@
import React from "react";
import { Form, Input, InputNumber, Switch } from "antd";
import { isSelectorTeam, isRoundRobinTeam } from "../../../../../types/guards";
import { NestedComponentButton, NodeEditorFieldsProps } from "./fields";
const { TextArea } = Input;
export const TeamFields: React.FC<NodeEditorFieldsProps> = ({
component,
onNavigate,
workingCopy,
setWorkingCopy,
editPath,
updateComponentAtPath,
getCurrentComponent,
}) => {
if (!component) return null;
if (isSelectorTeam(component)) {
return (
<>
<Form.Item
label="Selector Prompt"
name={["config", "selector_prompt"]}
rules={[{ required: true }]}
>
<TextArea rows={4} />
</Form.Item>
<Form.Item label="Max Turns" name={["config", "max_turns"]}>
<InputNumber min={1} />
</Form.Item>
<Form.Item
label="Allow Repeated Speaker"
name={["config", "allow_repeated_speaker"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
{component.config.model_client && (
<NestedComponentButton
label="Model Client"
component={component.config.model_client}
parentField="model_client"
onNavigate={onNavigate}
/>
)}
{component.config.termination_condition && (
<NestedComponentButton
label="Termination Condition"
component={component.config.termination_condition}
parentField="termination_condition"
onNavigate={onNavigate}
/>
)}
</>
);
}
if (isRoundRobinTeam(component)) {
return (
<>
<Form.Item label="Max Turns" name={["config", "max_turns"]}>
<InputNumber min={1} />
</Form.Item>
{component.config.termination_condition && (
<NestedComponentButton
label="Termination Condition"
component={component.config.termination_condition}
parentField="termination_condition"
onNavigate={onNavigate}
/>
)}
</>
);
}
return null;
};

View File

@@ -0,0 +1,69 @@
// fields/termination-fields.tsx
import React from "react";
import { Form, Input, InputNumber } from "antd";
import { Component, ComponentConfig } from "../../../../../types/datamodel";
import {
isOrTermination,
isMaxMessageTermination,
isTextMentionTermination,
} from "../../../../../types/guards";
import { NestedComponentButton } from "./fields";
interface TerminationFieldsProps {
component: Component<ComponentConfig>;
onNavigate: (componentType: string, id: string, parentField: string) => void;
}
export const TerminationFields: React.FC<TerminationFieldsProps> = ({
component,
onNavigate,
}) => {
if (!component) return null;
if (isOrTermination(component)) {
return (
<>
<Form.Item label="Number of Conditions" name={["config", "conditions"]}>
<InputNumber disabled />
</Form.Item>
{component.config.conditions &&
component.config.conditions.length > 0 && (
<NestedComponentButton
label="Conditions"
component={component.config.conditions}
parentField="conditions"
onNavigate={onNavigate}
/>
)}
</>
);
}
if (isMaxMessageTermination(component)) {
return (
<Form.Item
label="Max Messages"
name={["config", "max_messages"]}
rules={[{ required: true }]}
>
<InputNumber min={1} />
</Form.Item>
);
}
if (isTextMentionTermination(component)) {
return (
<Form.Item
label="Text"
name={["config", "text"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
);
}
return null;
};
export default TerminationFields;

View File

@@ -0,0 +1,265 @@
import React, { useRef, useState } from "react";
import { Form, Input, Switch, Select, Button, Space } from "antd";
import { PlusCircle, MinusCircle } from "lucide-react";
import { Import } from "../../../../../types/datamodel";
import { isFunctionTool } from "../../../../../types/guards";
import { MonacoEditor } from "../../../../monaco";
import { NodeEditorFieldsProps } from "./fields";
const { TextArea } = Input;
const { Option } = Select;
interface ImportState {
module: string;
imports: string;
}
export const ToolFields: React.FC<
Omit<NodeEditorFieldsProps, "onNavigate">
> = ({
component,
workingCopy,
setWorkingCopy,
editPath,
updateComponentAtPath,
getCurrentComponent,
}) => {
if (!component || !isFunctionTool(component)) return null;
const editorRef = useRef(null);
const [showAddImport, setShowAddImport] = useState(false);
const [importType, setImportType] = useState<"direct" | "fromModule">(
"direct"
);
const [directImport, setDirectImport] = useState("");
const [moduleImport, setModuleImport] = useState<ImportState>({
module: "",
imports: "",
});
const formatImport = (imp: Import): string => {
if (!imp) return "";
if (typeof imp === "string") {
return imp;
}
return `from ${imp.module} import ${imp.imports.join(", ")}`;
};
const handleAddImport = (form: { add: (value: string | Import) => void }) => {
if (importType === "direct" && directImport) {
form.add(directImport);
setDirectImport("");
} else if (
importType === "fromModule" &&
moduleImport.module &&
moduleImport.imports
) {
form.add({
module: moduleImport.module,
imports: moduleImport.imports
.split(",")
.map((i) => i.trim())
.filter((i) => i),
});
setModuleImport({ module: "", imports: "" });
}
setShowAddImport(false);
};
return (
<>
<Form.Item
label="Name"
name={["config", "name"]}
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
label="Description"
name={["config", "description"]}
rules={[{ required: true }]}
>
<TextArea rows={4} />
</Form.Item>
<Form.Item label="Global Imports">
<div className="space-y-2">
<Form.List name={["config", "global_imports"]}>
{(fields, { add, remove }) => (
<div className="space-y-2">
{/* Existing Imports */}
<div className="flex flex-wrap gap-2">
{fields.map((field) => (
<div
key={field.key}
className="flex items-center gap-2 bg-tertiary rounded px-2 py-1"
>
<Form.Item {...field} noStyle>
<Input type="hidden" />
</Form.Item>
<Form.Item
shouldUpdate={(prevValues, curValues) => {
const prevImport =
prevValues.config?.global_imports?.[field.name];
const curImport =
curValues.config?.global_imports?.[field.name];
return (
JSON.stringify(prevImport) !==
JSON.stringify(curImport)
);
}}
noStyle
>
{({ getFieldValue }) => {
const imp = getFieldValue([
"config",
"global_imports",
field.name,
]);
return (
<span className="text-sm">{formatImport(imp)}</span>
);
}}
</Form.Item>
<Button
type="text"
size="small"
className="flex items-center justify-center h-6 w-6 p-0"
onClick={() => remove(field.name)}
icon={<MinusCircle className="h-4 w-4" />}
/>
</div>
))}
</div>
{/* Add Import UI */}
{showAddImport ? (
<div className="border rounded p-3 space-y-3">
<Form.Item className="mb-2">
<Select
value={importType}
onChange={setImportType}
style={{ width: 200 }}
>
<Option value="direct">Direct Import</Option>
<Option value="fromModule">From Module Import</Option>
</Select>
</Form.Item>
{importType === "direct" ? (
<Space>
<Input
placeholder="Package name (e.g., os)"
className="w-64"
value={directImport}
onChange={(e) => setDirectImport(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && directImport) {
handleAddImport({ add });
}
}}
/>
<Button
onClick={() => handleAddImport({ add })}
disabled={!directImport}
>
Add
</Button>
</Space>
) : (
<Space direction="vertical" className="w-full">
<Input
placeholder="Module name (e.g., typing)"
className="w-64"
value={moduleImport.module}
onChange={(e) =>
setModuleImport((prev) => ({
...prev,
module: e.target.value,
}))
}
/>
<Space className="w-full">
<Input
placeholder="Import names (comma-separated)"
className="w-64"
value={moduleImport.imports}
onChange={(e) =>
setModuleImport((prev) => ({
...prev,
imports: e.target.value,
}))
}
onKeyDown={(e) => {
if (
e.key === "Enter" &&
moduleImport.module &&
moduleImport.imports
) {
handleAddImport({ add });
}
}}
/>
<Button
onClick={() => handleAddImport({ add })}
disabled={
!moduleImport.module || !moduleImport.imports
}
>
Add
</Button>
</Space>
</Space>
)}
</div>
) : (
<Button
type="dashed"
onClick={() => setShowAddImport(true)}
className="w-full"
>
<PlusCircle className="h-4 w-4 mr-2" />
Add Import
</Button>
)}
</div>
)}
</Form.List>
</div>
</Form.Item>
<Form.Item
label="Source Code"
name={["config", "source_code"]}
rules={[{ required: true }]}
>
<div className="h-96">
<Form.Item noStyle shouldUpdate>
{({ getFieldValue, setFieldValue }) => (
<MonacoEditor
value={getFieldValue(["config", "source_code"]) || ""}
editorRef={editorRef}
language="python"
onChange={(value) =>
setFieldValue(["config", "source_code"], value)
}
/>
)}
</Form.Item>
</div>
</Form.Item>
<Form.Item
label="Has Cancellation Support"
name={["config", "has_cancellation_support"]}
valuePropName="checked"
>
<Switch />
</Form.Item>
</>
);
};
export default ToolFields;

View File

@@ -3,10 +3,10 @@ import { Form, Button, Drawer, Breadcrumb } from "antd";
import { ChevronLeft } from "lucide-react";
import { Component, ComponentConfig } from "../../../../types/datamodel";
import { NodeEditorProps } from "../types";
import NodeEditorFields from "./fields";
import { isComponent } from "../../../../types/guards";
import NodeEditorFields from "./fields/fields";
interface EditPath {
export interface EditPath {
componentType: string;
id: string;
parentField: string;
@@ -48,6 +48,7 @@ export const NodeEditor: React.FC<
(acc, [key, value]) => {
// Skip nested component fields as they're handled by buttons
if (
key !== "global_imports" &&
typeof value === "object" &&
(Array.isArray(value) || value?.component_type)
) {
@@ -248,6 +249,7 @@ export const NodeEditor: React.FC<
<Drawer
title={
<div className="flex items-center gap-4">
{" "}
{editPath.length > 0 && (
<Button
onClick={navigateBack}
@@ -261,7 +263,7 @@ export const NodeEditor: React.FC<
</div>
}
placement="right"
width={400}
size="large"
onClose={onClose}
open={true}
className="node-editor-drawer"
@@ -275,9 +277,14 @@ export const NodeEditor: React.FC<
<NodeEditorFields
component={currentComponent}
onNavigate={navigateToComponent}
editPath={editPath}
workingCopy={workingCopy}
setWorkingCopy={setWorkingCopy}
updateComponentAtPath={updateComponentAtPath}
getCurrentComponent={getCurrentComponent}
/>
)}
<div className="flex justify-end gap-2 mt-4 absolute bottom-0 right-0 left-0 p-4 bg-white border-t">
<div className="flex justify-end gap-2 mt-4 absolute bottom-0 right-0 left-0 p-4 bg-primary border-t">
<Button onClick={onClose}>Cancel</Button>
<Button type="primary" onClick={handleFormSubmit}>
Save Changes

View File

@@ -234,6 +234,7 @@ export const TeamNode = memo<NodeProps<CustomNode>>((props) => {
<TruncatableText
content={component.description || component.label || ""}
textThreshold={150}
showFullscreen={false}
/>
</div>
{isSelectorTeam(component) && component.config.selector_prompt && (
@@ -242,6 +243,7 @@ export const TeamNode = memo<NodeProps<CustomNode>>((props) => {
<TruncatableText
content={component.config.selector_prompt}
textThreshold={150}
showFullscreen={false}
/>
</div>
)}