mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Merge branch 'master' into aarushikansal-add-vector-store-support
This commit is contained in:
12
.github/CODEOWNERS
vendored
12
.github/CODEOWNERS
vendored
@@ -1,5 +1,7 @@
|
||||
.github/workflows/ @Significant-Gravitas/devops
|
||||
autogpt/ @Significant-Gravitas/maintainers
|
||||
forge/ @Significant-Gravitas/forge-maintainers
|
||||
benchmark/ @Significant-Gravitas/benchmark-maintainers
|
||||
frontend/ @Significant-Gravitas/frontend-maintainers
|
||||
* @Significant-Gravitas/maintainers
|
||||
.github/workflows/ @Significant-Gravitas/devops
|
||||
forge/ @Significant-Gravitas/forge-maintainers
|
||||
benchmark/ @Significant-Gravitas/benchmark-maintainers
|
||||
frontend/ @Significant-Gravitas/frontend-maintainers
|
||||
rnd/infra @Significant-Gravitas/devops
|
||||
.github/CODEOWNERS @Significant-Gravitas/admins
|
||||
|
||||
2
cli.py
2
cli.py
@@ -69,6 +69,8 @@ d88P 888 "Y88888 "Y888 "Y88P" "Y8888P88 888 888
|
||||
bold=True,
|
||||
)
|
||||
)
|
||||
else:
|
||||
click.echo(click.style("🎉 Setup completed!\n", fg="green"))
|
||||
|
||||
|
||||
@cli.group()
|
||||
|
||||
@@ -9,7 +9,7 @@ This guide will help you setup the server and builder for the project.
|
||||
|
||||
<!-- The video is listed in the root Readme.md of the repo -->
|
||||
|
||||
We also offer this in video format. You can check it out [here](https://github.com/Significant-Gravitas/AutoGPT#how-to-get-started)
|
||||
We also offer this in video format. You can check it out [here](https://github.com/Significant-Gravitas/AutoGPT#how-to-get-started).
|
||||
|
||||
!!! warning
|
||||
**DO NOT FOLLOW ANY OUTSIDE TUTORIALS AS THEY WILL LIKELY BE OUT OF DATE**
|
||||
|
||||
@@ -2,7 +2,19 @@ This is the frontend for AutoGPT's next generation
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
Run the following installation once.
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn install
|
||||
# or
|
||||
pnpm install
|
||||
# or
|
||||
bun install
|
||||
```
|
||||
|
||||
Next, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
@@ -18,8 +30,12 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
For subsequent runs, you do not have to `npm install` again. Simply do `npm run dev`.
|
||||
|
||||
If the project is updated via git, you will need to `npm install` after each update.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Deploy
|
||||
|
||||
TODO
|
||||
TODO
|
||||
|
||||
@@ -11,11 +11,14 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-collapsible": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import React, { useState, useEffect, FC, memo, useCallback } from 'react';
|
||||
import React, { useState, useEffect, FC, memo, useCallback, useRef } from 'react';
|
||||
import { NodeProps, useReactFlow } from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import './customnode.css';
|
||||
import InputModalComponent from './InputModalComponent';
|
||||
import OutputModalComponent from './OutputModalComponent';
|
||||
import { BlockIORootSchema, NodeExecutionResult } from '@/lib/autogpt-server-api/types';
|
||||
import { BlockSchema } from '@/lib/types';
|
||||
import { beautifyString, setNestedProperty } from '@/lib/utils';
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import NodeHandle from './NodeHandle';
|
||||
import NodeInputField from './NodeInputField';
|
||||
import { Copy, Trash2 } from 'lucide-react';
|
||||
import { history } from './history';
|
||||
|
||||
export type CustomNodeData = {
|
||||
blockType: string;
|
||||
title: string;
|
||||
inputSchema: BlockSchema;
|
||||
outputSchema: BlockSchema;
|
||||
inputSchema: BlockIORootSchema;
|
||||
outputSchema: BlockIORootSchema;
|
||||
hardcodedValues: { [key: string]: any };
|
||||
setHardcodedValues: (values: { [key: string]: any }) => void;
|
||||
connections: Array<{ source: string; sourceHandle: string; target: string; targetHandle: string }>;
|
||||
@@ -41,6 +43,8 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
|
||||
const { getNode, setNodes, getEdges, setEdges } = useReactFlow();
|
||||
|
||||
const outputDataRef = useRef<HTMLDivElement>(null);
|
||||
const isInitialSetup = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.output_data || data.status) {
|
||||
@@ -56,6 +60,10 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
data.setIsAnyModalOpen?.(isModalOpen || isOutputModalOpen);
|
||||
}, [isModalOpen, isOutputModalOpen, data]);
|
||||
|
||||
useEffect(() => {
|
||||
isInitialSetup.current = false;
|
||||
}, []);
|
||||
|
||||
const toggleOutput = (checked: boolean) => {
|
||||
setIsOutputOpen(checked);
|
||||
};
|
||||
@@ -70,7 +78,7 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const generateOutputHandles = (schema: BlockSchema) => {
|
||||
const generateOutputHandles = (schema: BlockIORootSchema) => {
|
||||
if (!schema?.properties) return null;
|
||||
const keys = Object.keys(schema.properties);
|
||||
return keys.map((key) => (
|
||||
@@ -92,6 +100,16 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
current[keys[keys.length - 1]] = value;
|
||||
|
||||
console.log(`Updating hardcoded values for node ${id}:`, newValues);
|
||||
|
||||
if (!isInitialSetup.current) {
|
||||
history.push({
|
||||
type: 'UPDATE_INPUT',
|
||||
payload: { nodeId: id, oldValues: data.hardcodedValues, newValues },
|
||||
undo: () => data.setHardcodedValues(data.hardcodedValues),
|
||||
redo: () => data.setHardcodedValues(newValues),
|
||||
});
|
||||
}
|
||||
|
||||
data.setHardcodedValues(newValues);
|
||||
const errors = data.errors || {};
|
||||
// Remove error with the same key
|
||||
@@ -139,7 +157,9 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
|
||||
const handleOutputClick = () => {
|
||||
setIsOutputModalOpen(true);
|
||||
setModalValue(typeof data.output_data === 'object' ? JSON.stringify(data.output_data, null, 2) : data.output_data);
|
||||
setModalValue(
|
||||
data.output_data ? JSON.stringify(data.output_data, null, 2) : "[no output (yet)]"
|
||||
);
|
||||
};
|
||||
|
||||
const isTextTruncated = (element: HTMLElement | null): boolean => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
useNodesState,
|
||||
@@ -15,44 +15,20 @@ import ReactFlow, {
|
||||
import 'reactflow/dist/style.css';
|
||||
import CustomNode, { CustomNodeData } from './CustomNode';
|
||||
import './flow.css';
|
||||
import AutoGPTServerAPI, { Block, Graph, NodeExecutionResult, ObjectSchema } from '@/lib/autogpt-server-api';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { ChevronRight, ChevronLeft } from "lucide-react";
|
||||
import AutoGPTServerAPI, { Block, BlockIOSchema, Graph, NodeExecutionResult } from '@/lib/autogpt-server-api';
|
||||
import { Play, Undo2, Redo2} from "lucide-react";
|
||||
import { deepEquals, getTypeColor, removeEmptyStringsAndNulls, setNestedProperty } from '@/lib/utils';
|
||||
import { beautifyString } from '@/lib/utils';
|
||||
import { history } from './history';
|
||||
import { CustomEdge, CustomEdgeData } from './CustomEdge';
|
||||
import ConnectionLine from './ConnectionLine';
|
||||
import Ajv from 'ajv';
|
||||
import {Control, ControlPanel} from "@/components/edit/control/ControlPanel";
|
||||
import {SaveControl} from "@/components/edit/control/SaveControl";
|
||||
import {BlocksControl} from "@/components/edit/control/BlocksControl";
|
||||
|
||||
const Sidebar: React.FC<{ isOpen: boolean, availableNodes: Block[], addNode: (id: string, name: string) => void }> =
|
||||
({ isOpen, availableNodes, addNode }) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const filteredNodes = availableNodes.filter(node =>
|
||||
node.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`sidebar dark-theme ${isOpen ? 'open' : ''}`}>
|
||||
<h3>Nodes</h3>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search nodes..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{filteredNodes.map((node) => (
|
||||
<div key={node.id} className="sidebarNodeRowStyle dark-theme">
|
||||
<span>{beautifyString(node.name).replace(/Block$/, '')}</span>
|
||||
<Button onClick={() => addNode(node.id, node.name)}>Add</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// This is for the history, this is the minimum distance a block must move before it is logged
|
||||
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
||||
const MINIMUM_MOVE_BEFORE_LOG = 50;
|
||||
|
||||
const ajv = new Ajv({ strict: false, allErrors: true });
|
||||
|
||||
@@ -65,7 +41,6 @@ const FlowEditor: React.FC<{
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<CustomEdgeData>([]);
|
||||
const [nodeId, setNodeId] = useState<number>(1);
|
||||
const [availableNodes, setAvailableNodes] = useState<Block[]>([]);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
const [savedAgent, setSavedAgent] = useState<Graph | null>(null);
|
||||
const [agentDescription, setAgentDescription] = useState<string>('');
|
||||
const [agentName, setAgentName] = useState<string>('');
|
||||
@@ -75,6 +50,8 @@ const FlowEditor: React.FC<{
|
||||
|
||||
const apiUrl = process.env.AGPT_SERVER_URL!;
|
||||
const api = useMemo(() => new AutoGPTServerAPI(apiUrl), [apiUrl]);
|
||||
const initialPositionRef = useRef<{ [key: string]: { x: number; y: number } }>({});
|
||||
const isDragging = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
api.connectWebSocket()
|
||||
@@ -107,9 +84,97 @@ const FlowEditor: React.FC<{
|
||||
.then(graph => loadGraph(graph));
|
||||
}, [flowID, template, availableNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
const isUndo = (isMac ? event.metaKey : event.ctrlKey) && event.key === 'z';
|
||||
const isRedo = (isMac ? event.metaKey : event.ctrlKey) && (event.key === 'y' || (event.shiftKey && event.key === 'Z'));
|
||||
|
||||
if (isUndo) {
|
||||
event.preventDefault();
|
||||
handleUndo();
|
||||
}
|
||||
|
||||
if (isRedo) {
|
||||
event.preventDefault();
|
||||
handleRedo();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
const nodeTypes: NodeTypes = useMemo(() => ({ custom: CustomNode }), []);
|
||||
const edgeTypes: EdgeTypes = useMemo(() => ({ custom: CustomEdge }), []);
|
||||
|
||||
const onNodesChangeStart = (event: MouseEvent, node: Node) => {
|
||||
initialPositionRef.current[node.id] = { ...node.position };
|
||||
isDragging.current = true;
|
||||
};
|
||||
|
||||
const onNodesChangeEnd = (event: MouseEvent, node: Node | null) => {
|
||||
if (!node) return;
|
||||
|
||||
isDragging.current = false;
|
||||
const oldPosition = initialPositionRef.current[node.id];
|
||||
const newPosition = node.position;
|
||||
|
||||
// Calculate the movement distance
|
||||
if (!oldPosition || !newPosition) return;
|
||||
|
||||
const distanceMoved = Math.sqrt(
|
||||
Math.pow(newPosition.x - oldPosition.x, 2) +
|
||||
Math.pow(newPosition.y - oldPosition.y, 2)
|
||||
);
|
||||
|
||||
if (distanceMoved > MINIMUM_MOVE_BEFORE_LOG) { // Minimum movement threshold
|
||||
history.push({
|
||||
type: 'UPDATE_NODE_POSITION',
|
||||
payload: { nodeId: node.id, oldPosition, newPosition },
|
||||
undo: () => setNodes((nds) => nds.map(n => n.id === node.id ? { ...n, position: oldPosition } : n)),
|
||||
redo: () => setNodes((nds) => nds.map(n => n.id === node.id ? { ...n, position: newPosition } : n)),
|
||||
});
|
||||
}
|
||||
delete initialPositionRef.current[node.id];
|
||||
};
|
||||
|
||||
|
||||
const updateNodesOnEdgeChange = (edge: Edge<CustomEdgeData>, action: 'add' | 'remove') => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === edge.source || node.id === edge.target) {
|
||||
const connections = action === 'add'
|
||||
? [
|
||||
...node.data.connections,
|
||||
{
|
||||
source: edge.source,
|
||||
sourceHandle: edge.sourceHandle!,
|
||||
target: edge.target,
|
||||
targetHandle: edge.targetHandle!,
|
||||
}
|
||||
]
|
||||
: node.data.connections.filter(
|
||||
(conn) =>
|
||||
!(conn.source === edge.source && conn.target === edge.target && conn.sourceHandle === edge.sourceHandle && conn.targetHandle === edge.targetHandle)
|
||||
);
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
connections,
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getOutputType = (id: string, handleId: string) => {
|
||||
const node = nodes.find((node) => node.id === id);
|
||||
if (!node) return 'unknown';
|
||||
@@ -117,8 +182,9 @@ const FlowEditor: React.FC<{
|
||||
const outputSchema = node.data.outputSchema;
|
||||
if (!outputSchema) return 'unknown';
|
||||
|
||||
const outputType = outputSchema.properties[handleId].type;
|
||||
return outputType;
|
||||
const outputHandle = outputSchema.properties[handleId];
|
||||
if (!("type" in outputHandle)) return "unknown";
|
||||
return outputHandle.type;
|
||||
}
|
||||
|
||||
const getNodePos = (id: string) => {
|
||||
@@ -143,40 +209,62 @@ const FlowEditor: React.FC<{
|
||||
);
|
||||
}, [setNodes]);
|
||||
|
||||
const onConnect: OnConnect = (connection: Connection) => {
|
||||
const edgeColor = getTypeColor(getOutputType(connection.source!, connection.sourceHandle!));
|
||||
const sourcePos = getNodePos(connection.source!)
|
||||
console.log('sourcePos', sourcePos);
|
||||
setEdges((eds) => addEdge({
|
||||
type: 'custom',
|
||||
markerEnd: { type: MarkerType.ArrowClosed, strokeWidth: 2, color: edgeColor },
|
||||
data: { edgeColor, sourcePos },
|
||||
...connection
|
||||
}, eds));
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === connection.target || node.id === connection.source) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
connections: [
|
||||
...node.data.connections,
|
||||
{
|
||||
source: connection.source,
|
||||
sourceHandle: connection.sourceHandle,
|
||||
target: connection.target,
|
||||
targetHandle: connection.targetHandle,
|
||||
} as { source: string; sourceHandle: string; target: string; targetHandle: string },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
const onConnect: OnConnect = useCallback(
|
||||
(connection: Connection) => {
|
||||
const edgeColor = getTypeColor(getOutputType(connection.source!, connection.sourceHandle!));
|
||||
const sourcePos = getNodePos(connection.source!)
|
||||
console.log('sourcePos', sourcePos);
|
||||
const newEdge = {
|
||||
id: `${connection.source}_${connection.sourceHandle}_${connection.target}_${connection.targetHandle}`,
|
||||
type: 'custom',
|
||||
markerEnd: { type: MarkerType.ArrowClosed, strokeWidth: 2, color: edgeColor },
|
||||
data: { edgeColor, sourcePos },
|
||||
...connection
|
||||
};
|
||||
|
||||
setEdges((eds) => {
|
||||
const newEdges = addEdge(newEdge, eds);
|
||||
history.push({
|
||||
type: 'ADD_EDGE',
|
||||
payload: newEdge,
|
||||
undo: () => {
|
||||
setEdges((prevEdges) => prevEdges.filter(edge => edge.id !== newEdge.id));
|
||||
updateNodesOnEdgeChange(newEdge, 'remove');
|
||||
},
|
||||
redo: () => {
|
||||
setEdges((prevEdges) => addEdge(newEdge, prevEdges));
|
||||
updateNodesOnEdgeChange(newEdge, 'add');
|
||||
}
|
||||
});
|
||||
updateNodesOnEdgeChange(newEdge, 'add');
|
||||
return newEdges;
|
||||
});
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === connection.target || node.id === connection.source) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
connections: [
|
||||
...node.data.connections,
|
||||
{
|
||||
source: connection.source,
|
||||
sourceHandle: connection.sourceHandle,
|
||||
target: connection.target,
|
||||
targetHandle: connection.targetHandle,
|
||||
} as { source: string; sourceHandle: string; target: string; targetHandle: string },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
clearNodesStatusAndOutput(); // Clear status and output on connection change
|
||||
}
|
||||
},
|
||||
[nodes]
|
||||
);
|
||||
|
||||
const onEdgesDelete = useCallback(
|
||||
(edgesToDelete: Edge<CustomEdgeData>[]) => {
|
||||
@@ -191,8 +279,8 @@ const FlowEditor: React.FC<{
|
||||
(edge) =>
|
||||
edge.source === conn.source &&
|
||||
edge.target === conn.target &&
|
||||
edge.sourceHandle === conn.sourceHandle &&
|
||||
edge.targetHandle === conn.targetHandle
|
||||
edge.sourceHandle === edge.sourceHandle &&
|
||||
edge.targetHandle === edge.targetHandle
|
||||
)
|
||||
),
|
||||
},
|
||||
@@ -203,13 +291,13 @@ const FlowEditor: React.FC<{
|
||||
[setNodes, clearNodesStatusAndOutput]
|
||||
);
|
||||
|
||||
const addNode = (blockId: string, nodeType: string) => {
|
||||
const addNode = useCallback((blockId: string, nodeType: string) => {
|
||||
const nodeSchema = availableNodes.find(node => node.id === blockId);
|
||||
if (!nodeSchema) {
|
||||
console.error(`Schema not found for block ID: ${blockId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const newNode: Node<CustomNodeData> = {
|
||||
id: nodeId.toString(),
|
||||
type: 'custom',
|
||||
@@ -220,12 +308,12 @@ const FlowEditor: React.FC<{
|
||||
inputSchema: nodeSchema.inputSchema,
|
||||
outputSchema: nodeSchema.outputSchema,
|
||||
hardcodedValues: {},
|
||||
setHardcodedValues: (values: { [key: string]: any }) => {
|
||||
setNodes((nds) => nds.map((node) =>
|
||||
node.id === newNode.id
|
||||
? { ...node, data: { ...node.data, hardcodedValues: values } }
|
||||
: node
|
||||
));
|
||||
setHardcodedValues: (values) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) =>
|
||||
node.id === newNode.id ? { ...node, data: { ...node.data, hardcodedValues: values } } : node
|
||||
)
|
||||
);
|
||||
},
|
||||
connections: [],
|
||||
isOutputOpen: false,
|
||||
@@ -240,10 +328,26 @@ const FlowEditor: React.FC<{
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
setNodes((nds) => [...nds, newNode]);
|
||||
setNodeId((prevId) => prevId + 1);
|
||||
clearNodesStatusAndOutput(); // Clear status and output when a new node is added
|
||||
|
||||
history.push({
|
||||
type: 'ADD_NODE',
|
||||
payload: newNode,
|
||||
undo: () => setNodes((nds) => nds.filter(node => node.id !== newNode.id)),
|
||||
redo: () => setNodes((nds) => [...nds, newNode])
|
||||
});
|
||||
}, [nodeId, availableNodes]);
|
||||
|
||||
|
||||
const handleUndo = () => {
|
||||
history.undo();
|
||||
};
|
||||
|
||||
const handleRedo = () => {
|
||||
history.redo();
|
||||
};
|
||||
|
||||
function loadGraph(graph: Graph) {
|
||||
@@ -318,13 +422,18 @@ const FlowEditor: React.FC<{
|
||||
return {};
|
||||
}
|
||||
|
||||
const getNestedData = (schema: ObjectSchema, values: { [key: string]: any }): { [key: string]: any } => {
|
||||
const getNestedData = (
|
||||
schema: BlockIOSchema, values: { [key: string]: any }
|
||||
): { [key: string]: any } => {
|
||||
let inputData: { [key: string]: any } = {};
|
||||
|
||||
if (schema.properties) {
|
||||
if ("properties" in schema) {
|
||||
Object.keys(schema.properties).forEach((key) => {
|
||||
if (values[key] !== undefined) {
|
||||
if (schema.properties[key].type === 'object') {
|
||||
if (
|
||||
"properties" in schema.properties[key]
|
||||
|| "additionalProperties" in schema.properties[key]
|
||||
) {
|
||||
inputData[key] = getNestedData(schema.properties[key], values[key]);
|
||||
} else {
|
||||
inputData[key] = values[key];
|
||||
@@ -333,7 +442,7 @@ const FlowEditor: React.FC<{
|
||||
});
|
||||
}
|
||||
|
||||
if (schema.additionalProperties) {
|
||||
if ("additionalProperties" in schema) {
|
||||
inputData = { ...inputData, ...values };
|
||||
}
|
||||
|
||||
@@ -526,8 +635,6 @@ const FlowEditor: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
|
||||
|
||||
const handleKeyDown = useCallback((event: KeyboardEvent) => {
|
||||
if (isAnyModalOpen) return; // Prevent copy/paste if any modal is open
|
||||
|
||||
@@ -596,23 +703,26 @@ const FlowEditor: React.FC<{
|
||||
clearNodesStatusAndOutput();
|
||||
}, [clearNodesStatusAndOutput]);
|
||||
|
||||
const editorControls: Control[] = [
|
||||
{
|
||||
label: 'Undo',
|
||||
icon: <Undo2 />,
|
||||
onClick: handleUndo,
|
||||
},
|
||||
{
|
||||
label: 'Redo',
|
||||
icon: <Redo2 />,
|
||||
onClick: handleRedo,
|
||||
},
|
||||
{
|
||||
label: 'Run',
|
||||
icon: <Play />,
|
||||
onClick: runAgent,
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleSidebar}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: isSidebarOpen ? '350px' : '10px',
|
||||
zIndex: 10000,
|
||||
backgroundColor: 'black',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{isSidebarOpen ? <ChevronLeft className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Sidebar isOpen={isSidebarOpen} availableNodes={availableNodes} addNode={addNode} />
|
||||
<ReactFlow
|
||||
nodes={nodes.map(node => ({ ...node, data: { ...node.data, setIsAnyModalOpen } }))}
|
||||
edges={edges.map(edge => ({...edge, data: { ...edge.data, clearNodesStatusAndOutput } }))}
|
||||
@@ -625,31 +735,19 @@ const FlowEditor: React.FC<{
|
||||
onNodesDelete={onNodesDelete}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
deleteKeyCode={["Backspace", "Delete"]}
|
||||
onNodeDragStart={onNodesChangeStart}
|
||||
onNodeDragStop={onNodesChangeEnd}
|
||||
>
|
||||
<div style={{ position: 'absolute', right: 10, zIndex: 4 }}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Agent Name"
|
||||
value={agentName}
|
||||
onChange={(e) => setAgentName(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Agent Description"
|
||||
value={agentDescription}
|
||||
onChange={(e) => setAgentDescription(e.target.value)}
|
||||
/>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}> {/* Added gap for spacing */}
|
||||
<Button onClick={() => saveAgent(savedAgent?.is_template)}>
|
||||
Save {savedAgent?.is_template ? "Template" : "Agent"}
|
||||
</Button>
|
||||
{!savedAgent?.is_template &&
|
||||
<Button onClick={runAgent}>Save & Run Agent</Button>
|
||||
}
|
||||
{!savedAgent &&
|
||||
<Button onClick={() => saveAgent(true)}>Save as Template</Button>
|
||||
}
|
||||
</div>
|
||||
<div className={"flex flex-row absolute z-10 gap-2"}>
|
||||
<ControlPanel controls={editorControls} >
|
||||
<BlocksControl blocks={availableNodes} addBlock={addNode} />
|
||||
<SaveControl
|
||||
agentMeta={savedAgent}
|
||||
onSave={saveAgent}
|
||||
onDescriptionChange={setAgentDescription}
|
||||
onNameChange={setAgentName}
|
||||
/>
|
||||
</ControlPanel>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BlockSchema } from "@/lib/types";
|
||||
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
|
||||
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
|
||||
import { FC } from "react";
|
||||
import { Handle, Position } from "reactflow";
|
||||
@@ -6,7 +6,7 @@ import SchemaTooltip from "./SchemaTooltip";
|
||||
|
||||
type HandleProps = {
|
||||
keyName: string,
|
||||
schema: BlockSchema,
|
||||
schema: BlockIOSchema,
|
||||
isConnected: boolean,
|
||||
isRequired?: boolean,
|
||||
side: 'left' | 'right'
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Cross2Icon, PlusIcon } from "@radix-ui/react-icons";
|
||||
import { beautifyString } from "@/lib/utils";
|
||||
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
|
||||
import { FC, useState } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
|
||||
type BlockInputFieldProps = {
|
||||
keyName: string
|
||||
schema: any
|
||||
schema: BlockIOSchema
|
||||
parentKey?: string
|
||||
value: string | Array<string> | { [key: string]: string }
|
||||
handleInputClick: (key: string) => void
|
||||
@@ -13,147 +15,132 @@ type BlockInputFieldProps = {
|
||||
errors?: { [key: string]: string } | string | null
|
||||
}
|
||||
|
||||
const NodeInputField: FC<BlockInputFieldProps> =
|
||||
({ keyName: key, schema, parentKey = '', value, handleInputClick, handleInputChange, errors }) => {
|
||||
const [newKey, setNewKey] = useState<string>('');
|
||||
const [newValue, setNewValue] = useState<string>('');
|
||||
const [keyValuePairs, setKeyValuePairs] = useState<{ key: string, value: string }[]>([]);
|
||||
const NodeInputField: FC<BlockInputFieldProps> = ({
|
||||
keyName: key,
|
||||
schema,
|
||||
parentKey = '',
|
||||
value,
|
||||
handleInputClick,
|
||||
handleInputChange,
|
||||
errors
|
||||
}) => {
|
||||
const fullKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
const error = typeof errors === 'string' ? errors : errors?.[key] ?? "";
|
||||
const displayKey = schema.title || beautifyString(key);
|
||||
|
||||
const fullKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
const error = typeof errors === 'string' ? errors : errors?.[key] ?? "";
|
||||
const displayKey = schema.title || beautifyString(key);
|
||||
const [keyValuePairs, _setKeyValuePairs] = useState<{ key: string, value: string }[]>(
|
||||
"additionalProperties" in schema && value
|
||||
? Object.entries(value).map(([key, value]) => ({ key: key, value: value }))
|
||||
: []
|
||||
);
|
||||
|
||||
const handleAddProperty = () => {
|
||||
if (newKey && newValue) {
|
||||
const newPairs = [...keyValuePairs, { key: newKey, value: newValue }];
|
||||
setKeyValuePairs(newPairs);
|
||||
setNewKey('');
|
||||
setNewValue('');
|
||||
const expectedFormat = newPairs.reduce((acc, pair) => ({ ...acc, [pair.key]: pair.value }), {});
|
||||
handleInputChange('expected_format', expectedFormat);
|
||||
}
|
||||
};
|
||||
function setKeyValuePairs(newKVPairs: typeof keyValuePairs): void {
|
||||
_setKeyValuePairs(newKVPairs);
|
||||
handleInputChange(
|
||||
fullKey,
|
||||
newKVPairs.reduce((obj, { key, value }) => ({ ...obj, [key]: value }), {})
|
||||
);
|
||||
}
|
||||
|
||||
const renderClickableInput = (value: string | null = null, placeholder: string = "", secret: boolean = false) => {
|
||||
const className = `clickable-input ${error ? 'border-error' : ''}`
|
||||
const renderClickableInput = (value: string | null = null, placeholder: string = "", secret: boolean = false) => {
|
||||
const className = `clickable-input ${error ? 'border-error' : ''}`;
|
||||
|
||||
// if secret is true, then the input field will be a password field if the value is not null
|
||||
return secret ? (
|
||||
<div className={className} onClick={() => handleInputClick(fullKey)}>
|
||||
{value ? <span>********</span> : <i className="text-gray-500">{placeholder}</i>}
|
||||
</div>
|
||||
) : (
|
||||
<div className={className} onClick={() => handleInputClick(fullKey)}>
|
||||
{value || <i className="text-gray-500">{placeholder}</i>}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
return secret ? (
|
||||
<div className={className} onClick={() => handleInputClick(fullKey)}>
|
||||
{value ? <span>********</span> : <i className="text-gray-500">{placeholder}</i>}
|
||||
</div>
|
||||
) : (
|
||||
<div className={className} onClick={() => handleInputClick(fullKey)}>
|
||||
{value || <i className="text-gray-500">{placeholder}</i>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (schema.type === 'object' && schema.properties) {
|
||||
return (
|
||||
<div key={fullKey} className="object-input">
|
||||
<strong>{displayKey}:</strong>
|
||||
{Object.entries(schema.properties).map(([propKey, propSchema]: [string, any]) => (
|
||||
<div key={`${fullKey}.${propKey}`} className="nested-input">
|
||||
<NodeInputField
|
||||
keyName={propKey}
|
||||
schema={propSchema}
|
||||
parentKey={fullKey}
|
||||
value={(value as { [key: string]: string })[propKey]}
|
||||
handleInputClick={handleInputClick}
|
||||
handleInputChange={handleInputChange}
|
||||
errors={errors}
|
||||
if ("properties" in schema) {
|
||||
return (
|
||||
<div key={fullKey} className="object-input">
|
||||
<strong>{displayKey}:</strong>
|
||||
{Object.entries(schema.properties).map(([propKey, propSchema]) => (
|
||||
<div key={`${fullKey}.${propKey}`} className="nested-input">
|
||||
<NodeInputField
|
||||
keyName={propKey}
|
||||
schema={propSchema}
|
||||
parentKey={fullKey}
|
||||
value={(value as { [key: string]: string })[propKey]}
|
||||
handleInputClick={handleInputClick}
|
||||
handleInputChange={handleInputChange}
|
||||
errors={errors}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.type === 'object' && schema.additionalProperties) {
|
||||
return (
|
||||
<div key={fullKey}>
|
||||
<div>
|
||||
{keyValuePairs.map(({ key, value }, index) => (
|
||||
<div key={index} className="flex items-center w-[325px] space-x-2 mb-2">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Key"
|
||||
value={key}
|
||||
onChange={(e) => setKeyValuePairs(
|
||||
keyValuePairs.toSpliced(index, 1, {
|
||||
key: e.target.value, value: value
|
||||
})
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.type === 'object' && schema.additionalProperties) {
|
||||
const objectValue = value || {};
|
||||
return (
|
||||
<div key={fullKey} className="object-input">
|
||||
<strong>{displayKey}:</strong>
|
||||
{Object.entries(objectValue).map(([propKey, propValue]: [string, any]) => (
|
||||
<div key={`${fullKey}.${propKey}`} className="nested-input">
|
||||
<div className="clickable-input" onClick={() => handleInputClick(`${fullKey}.${propKey}`)}>
|
||||
{beautifyString(propKey)}: {typeof propValue === 'object' ? JSON.stringify(propValue, null, 2) : propValue}
|
||||
</div>
|
||||
<Button onClick={() => handleInputChange(`${fullKey}.${propKey}`, undefined)} className="array-item-remove">
|
||||
×
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Value"
|
||||
value={value}
|
||||
onChange={(e) => setKeyValuePairs(
|
||||
keyValuePairs.toSpliced(index, 1, {
|
||||
key: key, value: e.target.value
|
||||
})
|
||||
)}
|
||||
/>
|
||||
<Button variant="ghost" className="px-2"
|
||||
onClick={() => setKeyValuePairs(keyValuePairs.toSpliced(index, 1))}
|
||||
>
|
||||
<Cross2Icon />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{key === 'expected_format' && (
|
||||
<div className="nested-input">
|
||||
{keyValuePairs.map((pair, index) => (
|
||||
<div key={index} className="key-value-input">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Key"
|
||||
value={beautifyString(pair.key)}
|
||||
onChange={(e) => {
|
||||
const newPairs = [...keyValuePairs];
|
||||
newPairs[index].key = e.target.value;
|
||||
setKeyValuePairs(newPairs);
|
||||
const expectedFormat = newPairs.reduce((acc, pair) => ({ ...acc, [pair.key]: pair.value }), {});
|
||||
handleInputChange('expected_format', expectedFormat);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Value"
|
||||
value={beautifyString(pair.value)}
|
||||
onChange={(e) => {
|
||||
const newPairs = [...keyValuePairs];
|
||||
newPairs[index].value = e.target.value;
|
||||
setKeyValuePairs(newPairs);
|
||||
const expectedFormat = newPairs.reduce((acc, pair) => ({ ...acc, [pair.key]: pair.value }), {});
|
||||
handleInputChange('expected_format', expectedFormat);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="key-value-input">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Key"
|
||||
value={newKey}
|
||||
onChange={(e) => setNewKey(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Value"
|
||||
value={newValue}
|
||||
onChange={(e) => setNewValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleAddProperty}>Add Property</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button className="w-full"
|
||||
onClick={() => setKeyValuePairs(
|
||||
keyValuePairs.concat({ key: "", value: "" })
|
||||
)}
|
||||
>
|
||||
<PlusIcon className="mr-2" /> Add Property
|
||||
</Button>
|
||||
</div>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ("anyOf" in schema) {
|
||||
const types = schema.anyOf.map(s => "type" in s ? s.type : undefined);
|
||||
if (types.includes('string') && types.includes('null')) {
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey} (optional)`)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.anyOf) {
|
||||
const types = schema.anyOf.map((s: any) => s.type);
|
||||
if (types.includes('string') && types.includes('null')) {
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey} (optional)`)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.allOf) {
|
||||
return (
|
||||
<div key={fullKey} className="object-input">
|
||||
<strong>{displayKey}:</strong>
|
||||
{schema.allOf[0].properties && Object.entries(schema.allOf[0].properties).map(([propKey, propSchema]: [string, any]) => (
|
||||
if ("allOf" in schema) {
|
||||
return (
|
||||
<div key={fullKey} className="object-input">
|
||||
<strong>{displayKey}:</strong>
|
||||
{"properties" in schema.allOf[0] &&
|
||||
Object.entries(schema.allOf[0].properties).map(([propKey, propSchema]) => (
|
||||
<div key={`${fullKey}.${propKey}`} className="nested-input">
|
||||
<NodeInputField
|
||||
keyName={propKey}
|
||||
@@ -166,15 +153,16 @@ const NodeInputField: FC<BlockInputFieldProps> =
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.oneOf) {
|
||||
return (
|
||||
<div key={fullKey} className="object-input">
|
||||
<strong>{displayKey}:</strong>
|
||||
{schema.oneOf[0].properties && Object.entries(schema.oneOf[0].properties).map(([propKey, propSchema]: [string, any]) => (
|
||||
if ("oneOf" in schema) {
|
||||
return (
|
||||
<div key={fullKey} className="object-input">
|
||||
<strong>{displayKey}:</strong>
|
||||
{"properties" in schema.oneOf[0] &&
|
||||
Object.entries(schema.oneOf[0].properties).map(([propKey, propSchema]) => (
|
||||
<div key={`${fullKey}.${propKey}`} className="nested-input">
|
||||
<NodeInputField
|
||||
keyName={propKey}
|
||||
@@ -187,110 +175,120 @@ const NodeInputField: FC<BlockInputFieldProps> =
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (schema.type) {
|
||||
case 'string':
|
||||
if (schema.enum) {
|
||||
if (!("type" in schema)) {
|
||||
console.warn(`Schema for input ${key} does not specify a type:`, schema);
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${beautifyString(displayKey)} (Complex)`)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
<select
|
||||
value={value as string || ''}
|
||||
onChange={(e) => handleInputChange(fullKey, e.target.value)}
|
||||
className="select-input"
|
||||
>
|
||||
<option value="">Select {displayKey}</option>
|
||||
{schema.enum.map((option: string) => (
|
||||
<option key={option} value={option}>
|
||||
{beautifyString(option)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
else if (schema.secret) {
|
||||
return (<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey}`, true)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>)
|
||||
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey}`)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case 'boolean':
|
||||
switch (schema.type) {
|
||||
case 'string':
|
||||
if (schema.enum) {
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
<select
|
||||
value={value === undefined ? '' : value.toString()}
|
||||
onChange={(e) => handleInputChange(fullKey, e.target.value === 'true')}
|
||||
value={value as string || ''}
|
||||
onChange={(e) => handleInputChange(fullKey, e.target.value)}
|
||||
className="select-input"
|
||||
>
|
||||
<option value="">Select {displayKey}</option>
|
||||
<option value="true">True</option>
|
||||
<option value="false">False</option>
|
||||
{schema.enum.map((option: string) => (
|
||||
<option key={option} value={option}>
|
||||
{beautifyString(option)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
case 'number':
|
||||
case 'integer':
|
||||
}
|
||||
|
||||
if (schema.secret) {
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
<Input
|
||||
type="number"
|
||||
value={value as string || ''}
|
||||
onChange={(e) => handleInputChange(fullKey, parseFloat(e.target.value))}
|
||||
className={`number-input ${error ? 'border-error' : ''}`}
|
||||
/>
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey}`, true)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
case 'array':
|
||||
if (schema.items && schema.items.type === 'string') {
|
||||
const arrayValues = value as Array<string> || [];
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{arrayValues.map((item: string, index: number) => (
|
||||
<div key={`${fullKey}.${index}`} className="array-item-container">
|
||||
<Input
|
||||
type="text"
|
||||
value={item}
|
||||
onChange={(e) => handleInputChange(`${fullKey}.${index}`, e.target.value)}
|
||||
className="array-item-input"
|
||||
/>
|
||||
<Button onClick={() => handleInputChange(`${fullKey}.${index}`, '')} className="array-item-remove">
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button onClick={() => handleInputChange(fullKey, [...arrayValues, ''])} className="array-item-add">
|
||||
Add Item
|
||||
</Button>
|
||||
{error && <span className="error-message ml-2">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${displayKey}`)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
case 'boolean':
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
<select
|
||||
value={value === undefined ? '' : value.toString()}
|
||||
onChange={(e) => handleInputChange(fullKey, e.target.value === 'true')}
|
||||
className="select-input"
|
||||
>
|
||||
<option value="">Select {displayKey}</option>
|
||||
<option value="true">True</option>
|
||||
<option value="false">False</option>
|
||||
</select>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
<Input
|
||||
type="number"
|
||||
value={value as string || ''}
|
||||
onChange={(e) => handleInputChange(fullKey, parseFloat(e.target.value))}
|
||||
className={`number-input ${error ? 'border-error' : ''}`}
|
||||
/>
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
case 'array':
|
||||
if (schema.items && schema.items.type === 'string') {
|
||||
const arrayValues = value as Array<string> || [];
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${beautifyString(displayKey)} (Complex)`)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
{arrayValues.map((item: string, index: number) => (
|
||||
<div key={`${fullKey}.${index}`} className="array-item-container">
|
||||
<Input
|
||||
type="text"
|
||||
value={item}
|
||||
onChange={(e) => handleInputChange(`${fullKey}.${index}`, e.target.value)}
|
||||
className="array-item-input"
|
||||
/>
|
||||
<Button onClick={() => handleInputChange(`${fullKey}.${index}`, '')} className="array-item-remove">
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button onClick={() => handleInputChange(fullKey, [...arrayValues, ''])} className="array-item-add">
|
||||
Add Item
|
||||
</Button>
|
||||
{error && <span className="error-message ml-2">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
console.warn(`Schema for input ${key} specifies unknown type:`, schema);
|
||||
return (
|
||||
<div key={fullKey} className="input-container">
|
||||
{renderClickableInput(value as string, schema.placeholder || `Enter ${beautifyString(displayKey)} (Complex)`)}
|
||||
{error && <span className="error-message">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default NodeInputField;
|
||||
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { BlockSchema } from "@/lib/types";
|
||||
import { BlockIOSchema } from "@/lib/autogpt-server-api/types";
|
||||
import { Info } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
const SchemaTooltip: React.FC<{ schema: BlockSchema }> = ({ schema }) => {
|
||||
const SchemaTooltip: React.FC<{ schema: BlockIOSchema }> = ({ schema }) => {
|
||||
if (!schema.description) return null;
|
||||
|
||||
return (
|
||||
|
||||
61
rnd/autogpt_builder/src/components/edit/ControlPanel.tsx
Normal file
61
rnd/autogpt_builder/src/components/edit/ControlPanel.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Represents a control element for the ControlPanel Component.
|
||||
* @type {Object} Control
|
||||
* @property {React.ReactNode} icon - The icon of the control from lucide-react https://lucide.dev/icons/
|
||||
* @property {string} label - The label of the control, to be leveraged by ToolTip.
|
||||
* @property {onclick} onClick - The function to be executed when the control is clicked.
|
||||
*/
|
||||
export type Control = {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface ControlPanelProps {
|
||||
controls: Control[];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* ControlPanel component displays a panel with controls as icons with the ability to take in children.
|
||||
* @param {Object} ControlPanelProps - The properties of the control panel component.
|
||||
* @param {Array} ControlPanelProps.controls - An array of control objects representing actions to be preformed.
|
||||
* @param {Array} ControlPanelProps.children - The child components of the control panel.
|
||||
* @returns The rendered control panel component.
|
||||
*/
|
||||
export const ControlPanel= ( {controls, children}: ControlPanelProps) => {
|
||||
return (
|
||||
<aside className="hidden w-14 flex-col sm:flex">
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col items-center gap-4 px-2 sm:py-5 rounded-radius">
|
||||
{controls.map((control, index) => (
|
||||
<Tooltip key={index} delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => control.onClick()}
|
||||
>
|
||||
{control.icon}
|
||||
<span className="sr-only">{control.label}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{control.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
<Separator />
|
||||
{children}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
export default ControlPanel;
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useState } from "react";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ToyBrick } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { beautifyString } from "@/lib/utils";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Block } from '@/lib/autogpt-server-api';
|
||||
import { PlusIcon } from "@radix-ui/react-icons";
|
||||
|
||||
interface BlocksControlProps {
|
||||
blocks: Block[];
|
||||
addBlock: (id: string, name: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A React functional component that displays a control for managing blocks.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} BlocksControlProps - The properties for the BlocksControl component.
|
||||
* @param {Block[]} BlocksControlProps.blocks - An array of blocks to be displayed and filtered.
|
||||
* @param {(id: string, name: string) => void} BlocksControlProps.addBlock - A function to call when a block is added.
|
||||
* @returns The rendered BlocksControl component.
|
||||
*/
|
||||
export const BlocksControl: React.FC<BlocksControlProps> = ({ blocks, addBlock }) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const filteredBlocks = blocks.filter((block: Block) =>
|
||||
block.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger className="hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:text-white">
|
||||
<ToyBrick className="size-4"/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="right" sideOffset={15} align="start" className="w-80 p-0">
|
||||
<Card className="border-none shadow-none">
|
||||
<CardHeader className="p-4">
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<Label htmlFor="search-blocks">Blocks</Label>
|
||||
</div>
|
||||
<Input
|
||||
id="search-blocks"
|
||||
type="text"
|
||||
placeholder="Search blocks..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="p-1">
|
||||
<ScrollArea className="h-[60vh]">
|
||||
{filteredBlocks.map((block) => (
|
||||
<Card
|
||||
key={block.id}
|
||||
className="m-2"
|
||||
>
|
||||
<div className="flex items-center justify-between m-3">
|
||||
<div className="flex-1 min-w-0 mr-2">
|
||||
<span className="font-medium truncate block">{beautifyString(block.name)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => addBlock(block.id, block.name)}
|
||||
aria-label="Add block"
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Represents a control element for the ControlPanel Component.
|
||||
* @type {Object} Control
|
||||
* @property {React.ReactNode} icon - The icon of the control from lucide-react https://lucide.dev/icons/
|
||||
* @property {string} label - The label of the control, to be leveraged by ToolTip.
|
||||
* @property {onclick} onClick - The function to be executed when the control is clicked.
|
||||
*/
|
||||
export type Control = {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface ControlPanelProps {
|
||||
controls: Control[];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* ControlPanel component displays a panel with controls as icons with the ability to take in children.
|
||||
* @param {Object} ControlPanelProps - The properties of the control panel component.
|
||||
* @param {Array} ControlPanelProps.controls - An array of control objects representing actions to be preformed.
|
||||
* @param {Array} ControlPanelProps.children - The child components of the control panel.
|
||||
* @returns The rendered control panel component.
|
||||
*/
|
||||
export const ControlPanel= ( {controls, children}: ControlPanelProps) => {
|
||||
return (
|
||||
<aside className="hidden w-14 flex-col sm:flex">
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col items-center gap-4 px-2 sm:py-5 rounded-radius">
|
||||
{children}
|
||||
<Separator />
|
||||
{controls.map((control, index) => (
|
||||
<Tooltip key={index} delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => control.onClick()}
|
||||
>
|
||||
{control.icon}
|
||||
<span className="sr-only">{control.label}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{control.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
export default ControlPanel;
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from "react";
|
||||
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
|
||||
import {Card, CardContent, CardFooter} from "@/components/ui/card";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {GraphMeta} from "@/lib/autogpt-server-api";
|
||||
import {Label} from "@/components/ui/label";
|
||||
import {Save} from "lucide-react";
|
||||
|
||||
interface SaveControlProps {
|
||||
agentMeta: GraphMeta | null;
|
||||
onSave: (isTemplate: boolean | undefined) => void;
|
||||
onNameChange: (name: string) => void;
|
||||
onDescriptionChange: (description: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A SaveControl component to be used within the ControlPanel. It allows the user to save the agent / template.
|
||||
* @param {Object} SaveControlProps - The properties of the SaveControl component.
|
||||
* @param {GraphMeta | null} SaveControlProps.agentMeta - The agent's metadata, or null if creating a new agent.
|
||||
* @param {(isTemplate: boolean | undefined) => void} SaveControlProps.onSave - Function to save the agent or template.
|
||||
* @param {(name: string) => void} SaveControlProps.onNameChange - Function to handle name changes.
|
||||
* @param {(description: string) => void} SaveControlProps.onDescriptionChange - Function to handle description changes.
|
||||
* @returns The SaveControl component.
|
||||
*/
|
||||
export const SaveControl= ({
|
||||
agentMeta,
|
||||
onSave,
|
||||
onNameChange,
|
||||
onDescriptionChange
|
||||
}: SaveControlProps) => {
|
||||
|
||||
/**
|
||||
* Note for improvement:
|
||||
* At the moment we are leveraging onDescriptionChange and onNameChange to handle the changes in the description and name of the agent.
|
||||
* We should migrate this to be handled with form controls and a form library.
|
||||
*/
|
||||
|
||||
// Determines if we're saving a template or an agent
|
||||
let isTemplate = agentMeta?.is_template ? true : undefined;
|
||||
const handleSave = () => {
|
||||
onSave(isTemplate);
|
||||
};
|
||||
|
||||
const getType = () => {
|
||||
return agentMeta?.is_template ? 'template' : 'agent';
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover >
|
||||
<PopoverTrigger
|
||||
className="hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:text-white"
|
||||
>
|
||||
<Save className="size-4"/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="right" sideOffset={15} align="start">
|
||||
<Card className="border-none shadow-none">
|
||||
<CardContent className="p-4">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="name">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="Enter your agent name"
|
||||
className="col-span-3"
|
||||
defaultValue={agentMeta?.name || ''}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
/>
|
||||
<Label htmlFor="description">
|
||||
Description
|
||||
</Label>
|
||||
<Input
|
||||
id="description"
|
||||
placeholder="Your agent description"
|
||||
className="col-span-3"
|
||||
defaultValue={agentMeta?.description || ''}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col items-stretch gap-2 ">
|
||||
<Button className="w-full" onClick={handleSave}>
|
||||
Save {getType()}
|
||||
</Button>
|
||||
{!agentMeta && (
|
||||
<Button variant="secondary" className="w-full" onClick={() => {
|
||||
isTemplate = true;
|
||||
handleSave();
|
||||
}}>
|
||||
Save as Template
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +33,6 @@ input, textarea {
|
||||
border-radius: 4px;
|
||||
width: calc(100% - 18px);
|
||||
box-sizing: border-box;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
input::placeholder, textarea::placeholder {
|
||||
|
||||
89
rnd/autogpt_builder/src/components/history.ts
Normal file
89
rnd/autogpt_builder/src/components/history.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
// history.ts
|
||||
import { CustomNodeData } from './CustomNode';
|
||||
import { CustomEdgeData } from './CustomEdge';
|
||||
import { Edge } from 'reactflow';
|
||||
|
||||
type ActionType =
|
||||
| 'ADD_NODE'
|
||||
| 'DELETE_NODE'
|
||||
| 'ADD_EDGE'
|
||||
| 'DELETE_EDGE'
|
||||
| 'UPDATE_NODE'
|
||||
| 'MOVE_NODE'
|
||||
| 'UPDATE_INPUT'
|
||||
| 'UPDATE_NODE_POSITION';
|
||||
|
||||
type AddNodePayload = { node: CustomNodeData };
|
||||
type DeleteNodePayload = { nodeId: string };
|
||||
type AddEdgePayload = { edge: Edge<CustomEdgeData> };
|
||||
type DeleteEdgePayload = { edgeId: string };
|
||||
type UpdateNodePayload = { nodeId: string; newData: Partial<CustomNodeData> };
|
||||
type MoveNodePayload = { nodeId: string; position: { x: number; y: number } };
|
||||
type UpdateInputPayload = { nodeId: string; oldValues: { [key: string]: any }; newValues: { [key: string]: any } };
|
||||
type UpdateNodePositionPayload = { nodeId: string; oldPosition: { x: number; y: number }; newPosition: { x: number; y: number } };
|
||||
|
||||
type ActionPayload =
|
||||
| AddNodePayload
|
||||
| DeleteNodePayload
|
||||
| AddEdgePayload
|
||||
| DeleteEdgePayload
|
||||
| UpdateNodePayload
|
||||
| MoveNodePayload
|
||||
| UpdateInputPayload
|
||||
| UpdateNodePositionPayload;
|
||||
|
||||
type Action = {
|
||||
type: ActionType;
|
||||
payload: ActionPayload;
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
};
|
||||
|
||||
class History {
|
||||
private past: Action[] = [];
|
||||
private future: Action[] = [];
|
||||
|
||||
push(action: Action) {
|
||||
this.past.push(action);
|
||||
this.future = [];
|
||||
}
|
||||
|
||||
undo() {
|
||||
const action = this.past.pop();
|
||||
if (action) {
|
||||
action.undo();
|
||||
this.future.push(action);
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
const action = this.future.pop();
|
||||
if (action) {
|
||||
action.redo();
|
||||
this.past.push(action);
|
||||
}
|
||||
}
|
||||
|
||||
canUndo(): boolean {
|
||||
return this.past.length > 0;
|
||||
}
|
||||
|
||||
canRedo(): boolean {
|
||||
return this.future.length > 0;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.past = [];
|
||||
this.future = [];
|
||||
}
|
||||
|
||||
getHistoryState() {
|
||||
return {
|
||||
past: [...this.past],
|
||||
future: [...this.future],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const history = new History();
|
||||
@@ -5,20 +5,20 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-gray-300",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 dark:focus-visible:ring-neutral-300",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-gray-900 text-gray-50 shadow hover:bg-gray-900/90 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/90",
|
||||
"bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90",
|
||||
destructive:
|
||||
"bg-red-500 text-gray-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/90",
|
||||
"bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90",
|
||||
outline:
|
||||
"border border-gray-200 bg-white shadow-sm hover:bg-gray-100 hover:text-gray-900 dark:border-gray-800 dark:bg-gray-950 dark:hover:bg-gray-800 dark:hover:text-gray-50",
|
||||
"border border-neutral-200 bg-white shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
|
||||
secondary:
|
||||
"bg-gray-100 text-gray-900 shadow-sm hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80",
|
||||
ghost: "hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:text-white",
|
||||
link: "text-gray-900 underline-offset-4 hover:underline dark:text-gray-50",
|
||||
"bg-neutral-100 text-neutral-900 shadow-sm hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80",
|
||||
ghost: "hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50",
|
||||
link: "text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
|
||||
11
rnd/autogpt_builder/src/components/ui/collapsible.tsx
Normal file
11
rnd/autogpt_builder/src/components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
48
rnd/autogpt_builder/src/components/ui/scroll-area.tsx
Normal file
48
rnd/autogpt_builder/src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-neutral-200 dark:bg-neutral-800" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
31
rnd/autogpt_builder/src/components/ui/separator.tsx
Normal file
31
rnd/autogpt_builder/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-neutral-200 dark:bg-neutral-800",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
@@ -3,15 +3,56 @@ export type Block = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: ObjectSchema;
|
||||
outputSchema: ObjectSchema;
|
||||
inputSchema: BlockIORootSchema;
|
||||
outputSchema: BlockIORootSchema;
|
||||
};
|
||||
|
||||
export type ObjectSchema = {
|
||||
type: string;
|
||||
properties: { [key: string]: any };
|
||||
additionalProperties?: { type: string };
|
||||
export type BlockIORootSchema = {
|
||||
type: "object";
|
||||
properties: { [key: string]: BlockIOSchema };
|
||||
required?: string[];
|
||||
additionalProperties?: { type: string };
|
||||
}
|
||||
|
||||
export type BlockIOSchema = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
} & (BlockIOSimpleTypeSchema | BlockIOCombinedTypeSchema);
|
||||
|
||||
type BlockIOSimpleTypeSchema = {
|
||||
type: "object";
|
||||
properties: { [key: string]: BlockIOSchema };
|
||||
required?: string[];
|
||||
additionalProperties?: { type: string };
|
||||
} | {
|
||||
type: "array";
|
||||
items?: BlockIOSimpleTypeSchema;
|
||||
} | {
|
||||
type: "string";
|
||||
enum?: string[];
|
||||
secret?: true;
|
||||
default?: string;
|
||||
} | {
|
||||
type: "integer" | "number";
|
||||
default?: number;
|
||||
} | {
|
||||
type: "boolean";
|
||||
default?: boolean;
|
||||
} | {
|
||||
type: "null";
|
||||
};
|
||||
|
||||
// At the time of writing, combined schemas only occur on the first nested level in a
|
||||
// block schema. It is typed this way to make the use of these objects less tedious.
|
||||
type BlockIOCombinedTypeSchema = {
|
||||
allOf: [BlockIOSimpleTypeSchema];
|
||||
} | {
|
||||
anyOf: BlockIOSimpleTypeSchema[];
|
||||
default?: string | number | boolean | null;
|
||||
} | {
|
||||
oneOf: BlockIOSimpleTypeSchema[];
|
||||
default?: string | number | boolean | null;
|
||||
};
|
||||
|
||||
/* Mirror of autogpt_server/data/graph.py:Node */
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export type BlockSchema = {
|
||||
type: string;
|
||||
properties: { [key: string]: any };
|
||||
required?: string[];
|
||||
enum?: string[];
|
||||
items?: BlockSchema;
|
||||
additionalProperties?: { type: string };
|
||||
title?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
allOf?: any[];
|
||||
anyOf?: any[];
|
||||
oneOf?: any[];
|
||||
};
|
||||
@@ -224,6 +224,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@radix-ui/number@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.0.tgz#1e95610461a09cdf8bb05c152e76ca1278d5da46"
|
||||
integrity sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==
|
||||
|
||||
"@radix-ui/primitive@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
|
||||
@@ -246,6 +251,20 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-collapsible@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz#4d49ddcc7b7d38f6c82f1fd29674f6fab5353e77"
|
||||
integrity sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-id" "1.1.0"
|
||||
"@radix-ui/react-presence" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-collection@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed"
|
||||
@@ -447,6 +466,28 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.1.0"
|
||||
|
||||
"@radix-ui/react-scroll-area@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz#50b24b0fc9ada151d176395bcf47b2ec68feada5"
|
||||
integrity sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==
|
||||
dependencies:
|
||||
"@radix-ui/number" "1.1.0"
|
||||
"@radix-ui/primitive" "1.1.0"
|
||||
"@radix-ui/react-compose-refs" "1.1.0"
|
||||
"@radix-ui/react-context" "1.1.0"
|
||||
"@radix-ui/react-direction" "1.1.0"
|
||||
"@radix-ui/react-presence" "1.1.0"
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.0"
|
||||
|
||||
"@radix-ui/react-separator@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.0.tgz#ee0f4d86003b0e3ea7bc6ccab01ea0adee32663e"
|
||||
integrity sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.0.0"
|
||||
|
||||
"@radix-ui/react-slot@1.1.0", "@radix-ui/react-slot@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84"
|
||||
|
||||
@@ -8,3 +8,6 @@ REDDIT_CLIENT_ID=
|
||||
REDDIT_CLIENT_SECRET=
|
||||
REDDIT_USERNAME=
|
||||
REDDIT_PASSWORD=
|
||||
|
||||
# Discord
|
||||
DISCORD_BOT_TOKEN=
|
||||
@@ -53,7 +53,7 @@ We use the Poetry to manage the dependencies. To set up the project, follow thes
|
||||
|
||||
```sh
|
||||
poetry run prisma migrate dev
|
||||
```
|
||||
```
|
||||
|
||||
## Running The Server
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Generic, TypeVar
|
||||
from typing import Any, Generic, List, TypeVar
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from autogpt_server.data.model import SchemaField
|
||||
from autogpt_server.util.mock import MockObject
|
||||
|
||||
|
||||
@@ -181,3 +182,138 @@ class OutputBlock(ObjectLookupBase[Any]):
|
||||
|
||||
def block_id(self) -> str:
|
||||
return "363ae599-353e-4804-937e-b2ee3cef3da4"
|
||||
|
||||
|
||||
class DictionaryAddEntryBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
dictionary: dict | None = SchemaField(
|
||||
default=None,
|
||||
description="The dictionary to add the entry to. If not provided, a new dictionary will be created.",
|
||||
placeholder='{"key1": "value1", "key2": "value2"}',
|
||||
)
|
||||
key: str = SchemaField(
|
||||
description="The key for the new entry.", placeholder="new_key"
|
||||
)
|
||||
value: Any = SchemaField(
|
||||
description="The value for the new entry.", placeholder="new_value"
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
updated_dictionary: dict = SchemaField(
|
||||
description="The dictionary with the new entry added."
|
||||
)
|
||||
error: str = SchemaField(description="Error message if the operation failed.")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="31d1064e-7446-4693-a7d4-65e5ca1180d1",
|
||||
description="Adds a new key-value pair to a dictionary. If no dictionary is provided, a new one is created.",
|
||||
categories={BlockCategory.BASIC},
|
||||
input_schema=DictionaryAddEntryBlock.Input,
|
||||
output_schema=DictionaryAddEntryBlock.Output,
|
||||
test_input=[
|
||||
{
|
||||
"dictionary": {"existing_key": "existing_value"},
|
||||
"key": "new_key",
|
||||
"value": "new_value",
|
||||
},
|
||||
{"key": "first_key", "value": "first_value"},
|
||||
],
|
||||
test_output=[
|
||||
(
|
||||
"updated_dictionary",
|
||||
{"existing_key": "existing_value", "new_key": "new_value"},
|
||||
),
|
||||
("updated_dictionary", {"first_key": "first_value"}),
|
||||
],
|
||||
)
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
try:
|
||||
# If no dictionary is provided, create a new one
|
||||
if input_data.dictionary is None:
|
||||
updated_dict = {}
|
||||
else:
|
||||
# Create a copy of the input dictionary to avoid modifying the original
|
||||
updated_dict = input_data.dictionary.copy()
|
||||
|
||||
# Add the new key-value pair
|
||||
updated_dict[input_data.key] = input_data.value
|
||||
|
||||
yield "updated_dictionary", updated_dict
|
||||
except Exception as e:
|
||||
yield "error", f"Failed to add entry to dictionary: {str(e)}"
|
||||
|
||||
|
||||
class ListAddEntryBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
list: List[Any] | None = SchemaField(
|
||||
default=None,
|
||||
description="The list to add the entry to. If not provided, a new list will be created.",
|
||||
placeholder='[1, "string", {"key": "value"}]',
|
||||
)
|
||||
entry: Any = SchemaField(
|
||||
description="The entry to add to the list. Can be of any type (string, int, dict, etc.).",
|
||||
placeholder='{"new_key": "new_value"}',
|
||||
)
|
||||
position: int | None = SchemaField(
|
||||
default=None,
|
||||
description="The position to insert the new entry. If not provided, the entry will be appended to the end of the list.",
|
||||
placeholder="0",
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
updated_list: List[Any] = SchemaField(
|
||||
description="The list with the new entry added."
|
||||
)
|
||||
error: str = SchemaField(description="Error message if the operation failed.")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="aeb08fc1-2fc1-4141-bc8e-f758f183a822",
|
||||
description="Adds a new entry to a list. The entry can be of any type. If no list is provided, a new one is created.",
|
||||
categories={BlockCategory.BASIC},
|
||||
input_schema=ListAddEntryBlock.Input,
|
||||
output_schema=ListAddEntryBlock.Output,
|
||||
test_input=[
|
||||
{
|
||||
"list": [1, "string", {"existing_key": "existing_value"}],
|
||||
"entry": {"new_key": "new_value"},
|
||||
"position": 1,
|
||||
},
|
||||
{"entry": "first_entry"},
|
||||
{"list": ["a", "b", "c"], "entry": "d"},
|
||||
],
|
||||
test_output=[
|
||||
(
|
||||
"updated_list",
|
||||
[
|
||||
1,
|
||||
{"new_key": "new_value"},
|
||||
"string",
|
||||
{"existing_key": "existing_value"},
|
||||
],
|
||||
),
|
||||
("updated_list", ["first_entry"]),
|
||||
("updated_list", ["a", "b", "c", "d"]),
|
||||
],
|
||||
)
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
try:
|
||||
# If no list is provided, create a new one
|
||||
if input_data.list is None:
|
||||
updated_list = []
|
||||
else:
|
||||
# Create a copy of the input list to avoid modifying the original
|
||||
updated_list = input_data.list.copy()
|
||||
|
||||
# Add the new entry
|
||||
if input_data.position is None:
|
||||
updated_list.append(input_data.entry)
|
||||
else:
|
||||
updated_list.insert(input_data.position, input_data.entry)
|
||||
|
||||
yield "updated_list", updated_list
|
||||
except Exception as e:
|
||||
yield "error", f"Failed to add entry to list: {str(e)}"
|
||||
|
||||
205
rnd/autogpt_server/autogpt_server/blocks/discordblock.py
Normal file
205
rnd/autogpt_server/autogpt_server/blocks/discordblock.py
Normal file
@@ -0,0 +1,205 @@
|
||||
import asyncio
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
from pydantic import Field
|
||||
|
||||
from autogpt_server.data.block import Block, BlockOutput, BlockSchema
|
||||
from autogpt_server.data.model import BlockSecret, SecretField
|
||||
|
||||
|
||||
class DiscordReaderBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
discord_bot_token: BlockSecret = SecretField(
|
||||
key="discord_bot_token", description="Discord bot token"
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
message_content: str = Field(description="The content of the message received")
|
||||
channel_name: str = Field(
|
||||
description="The name of the channel the message was received from"
|
||||
)
|
||||
username: str = Field(
|
||||
description="The username of the user who sent the message"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="d3f4g5h6-1i2j-3k4l-5m6n-7o8p9q0r1s2t", # Unique ID for the node
|
||||
input_schema=DiscordReaderBlock.Input, # Assign input schema
|
||||
output_schema=DiscordReaderBlock.Output, # Assign output schema
|
||||
test_input={"discord_bot_token": "test_token"},
|
||||
test_output=[
|
||||
(
|
||||
"message_content",
|
||||
"Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.",
|
||||
),
|
||||
("channel_name", "general"),
|
||||
("username", "test_user"),
|
||||
],
|
||||
test_mock={
|
||||
"run_bot": lambda token: asyncio.Future() # Create a Future object for mocking
|
||||
},
|
||||
)
|
||||
|
||||
async def run_bot(self, token: str):
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
|
||||
client = discord.Client(intents=intents)
|
||||
|
||||
self.output_data = None
|
||||
self.channel_name = None
|
||||
self.username = None
|
||||
|
||||
@client.event
|
||||
async def on_ready():
|
||||
print(f"Logged in as {client.user}")
|
||||
|
||||
@client.event
|
||||
async def on_message(message):
|
||||
if message.author == client.user:
|
||||
return
|
||||
|
||||
self.output_data = message.content
|
||||
self.channel_name = message.channel.name
|
||||
self.username = message.author.name
|
||||
|
||||
if message.attachments:
|
||||
attachment = message.attachments[0] # Process the first attachment
|
||||
if attachment.filename.endswith((".txt", ".py")):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(attachment.url) as response:
|
||||
file_content = await response.text()
|
||||
self.output_data += f"\n\nFile from user: {attachment.filename}\nContent: {file_content}"
|
||||
|
||||
await client.close()
|
||||
|
||||
await client.start(token)
|
||||
|
||||
def run(self, input_data: "DiscordReaderBlock.Input") -> BlockOutput:
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
future = self.run_bot(input_data.discord_bot_token.get_secret_value())
|
||||
|
||||
# If it's a Future (mock), set the result
|
||||
if isinstance(future, asyncio.Future):
|
||||
future.set_result(
|
||||
{
|
||||
"output_data": "Hello!\n\nFile from user: example.txt\nContent: This is the content of the file.",
|
||||
"channel_name": "general",
|
||||
"username": "test_user",
|
||||
}
|
||||
)
|
||||
|
||||
result = loop.run_until_complete(future)
|
||||
|
||||
# For testing purposes, use the mocked result
|
||||
if isinstance(result, dict):
|
||||
self.output_data = result.get("output_data")
|
||||
self.channel_name = result.get("channel_name")
|
||||
self.username = result.get("username")
|
||||
|
||||
if (
|
||||
self.output_data is None
|
||||
or self.channel_name is None
|
||||
or self.username is None
|
||||
):
|
||||
raise ValueError("No message, channel name, or username received.")
|
||||
|
||||
yield "message_content", self.output_data
|
||||
yield "channel_name", self.channel_name
|
||||
yield "username", self.username
|
||||
|
||||
except discord.errors.LoginFailure as login_err:
|
||||
raise ValueError(f"Login error occurred: {login_err}")
|
||||
except Exception as e:
|
||||
raise ValueError(f"An error occurred: {e}")
|
||||
|
||||
|
||||
class DiscordMessageSenderBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
discord_bot_token: BlockSecret = SecretField(
|
||||
key="discord_bot_token", description="Discord bot token"
|
||||
)
|
||||
message_content: str = Field(description="The content of the message received")
|
||||
channel_name: str = Field(
|
||||
description="The name of the channel the message was received from"
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
status: str = Field(
|
||||
description="The status of the operation (e.g., 'Message sent', 'Error')"
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="h1i2j3k4-5l6m-7n8o-9p0q-r1s2t3u4v5w6", # Unique ID for the node
|
||||
input_schema=DiscordMessageSenderBlock.Input, # Assign input schema
|
||||
output_schema=DiscordMessageSenderBlock.Output, # Assign output schema
|
||||
test_input={
|
||||
"discord_bot_token": "YOUR_DISCORD_BOT_TOKEN",
|
||||
"channel_name": "general",
|
||||
"message_content": "Hello, Discord!",
|
||||
},
|
||||
test_output=[("status", "Message sent")],
|
||||
test_mock={
|
||||
"send_message": lambda token, channel_name, message_content: asyncio.Future()
|
||||
},
|
||||
)
|
||||
|
||||
async def send_message(self, token: str, channel_name: str, message_content: str):
|
||||
intents = discord.Intents.default()
|
||||
intents.guilds = True # Required for fetching guild/channel information
|
||||
client = discord.Client(intents=intents)
|
||||
|
||||
@client.event
|
||||
async def on_ready():
|
||||
print(f"Logged in as {client.user}")
|
||||
for guild in client.guilds:
|
||||
for channel in guild.text_channels:
|
||||
if channel.name == channel_name:
|
||||
# Split message into chunks if it exceeds 2000 characters
|
||||
for chunk in self.chunk_message(message_content):
|
||||
await channel.send(chunk)
|
||||
self.output_data = "Message sent"
|
||||
await client.close()
|
||||
return
|
||||
|
||||
self.output_data = "Channel not found"
|
||||
await client.close()
|
||||
|
||||
await client.start(token)
|
||||
|
||||
def chunk_message(self, message: str, limit: int = 2000) -> list:
|
||||
"""Splits a message into chunks not exceeding the Discord limit."""
|
||||
return [message[i : i + limit] for i in range(0, len(message), limit)]
|
||||
|
||||
def run(self, input_data: "DiscordMessageSenderBlock.Input") -> BlockOutput:
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
future = self.send_message(
|
||||
input_data.discord_bot_token.get_secret_value(),
|
||||
input_data.channel_name,
|
||||
input_data.message_content,
|
||||
)
|
||||
|
||||
# If it's a Future (mock), set the result
|
||||
if isinstance(future, asyncio.Future):
|
||||
future.set_result("Message sent")
|
||||
|
||||
result = loop.run_until_complete(future)
|
||||
|
||||
# For testing purposes, use the mocked result
|
||||
if isinstance(result, str):
|
||||
self.output_data = result
|
||||
|
||||
if self.output_data is None:
|
||||
raise ValueError("No status message received.")
|
||||
|
||||
yield "status", self.output_data
|
||||
|
||||
except discord.errors.LoginFailure as login_err:
|
||||
raise ValueError(f"Login error occurred: {login_err}")
|
||||
except Exception as e:
|
||||
raise ValueError(f"An error occurred: {e}")
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import NamedTuple
|
||||
from typing import List, NamedTuple
|
||||
|
||||
import anthropic
|
||||
import ollama
|
||||
@@ -8,7 +8,7 @@ import openai
|
||||
from groq import Groq
|
||||
|
||||
from autogpt_server.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from autogpt_server.data.model import BlockSecret, SecretField
|
||||
from autogpt_server.data.model import BlockSecret, SchemaField, SecretField
|
||||
from autogpt_server.util import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -409,3 +409,127 @@ class TextSummarizerBlock(Block):
|
||||
).send(None)[
|
||||
1
|
||||
] # Get the first yielded value
|
||||
|
||||
|
||||
class MessageRole(str, Enum):
|
||||
SYSTEM = "system"
|
||||
USER = "user"
|
||||
ASSISTANT = "assistant"
|
||||
|
||||
|
||||
class Message(BlockSchema):
|
||||
role: MessageRole
|
||||
content: str
|
||||
|
||||
|
||||
class AdvancedLlmCallBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
messages: List[Message] = SchemaField(
|
||||
description="List of messages in the conversation.", min_items=1
|
||||
)
|
||||
model: LlmModel = SchemaField(
|
||||
default=LlmModel.GPT4_TURBO,
|
||||
description="The language model to use for the conversation.",
|
||||
)
|
||||
api_key: BlockSecret = SecretField(
|
||||
value="", description="API key for the chosen language model provider."
|
||||
)
|
||||
max_tokens: int | None = SchemaField(
|
||||
default=None,
|
||||
description="The maximum number of tokens to generate in the chat completion.",
|
||||
ge=1,
|
||||
)
|
||||
|
||||
class Output(BlockSchema):
|
||||
response: str = SchemaField(
|
||||
description="The model's response to the conversation."
|
||||
)
|
||||
error: str = SchemaField(description="Error message if the API call failed.")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="c3d4e5f6-g7h8-i9j0-k1l2-m3n4o5p6q7r8",
|
||||
description="Advanced LLM call that takes a list of messages and sends them to the language model.",
|
||||
categories={BlockCategory.LLM},
|
||||
input_schema=AdvancedLlmCallBlock.Input,
|
||||
output_schema=AdvancedLlmCallBlock.Output,
|
||||
test_input={
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Who won the world series in 2020?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "The Los Angeles Dodgers won the World Series in 2020.",
|
||||
},
|
||||
{"role": "user", "content": "Where was it played?"},
|
||||
],
|
||||
"model": LlmModel.GPT4_TURBO,
|
||||
"api_key": "test_api_key",
|
||||
},
|
||||
test_output=(
|
||||
"response",
|
||||
"The 2020 World Series was played at Globe Life Field in Arlington, Texas.",
|
||||
),
|
||||
test_mock={
|
||||
"llm_call": lambda *args, **kwargs: "The 2020 World Series was played at Globe Life Field in Arlington, Texas."
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def llm_call(
|
||||
api_key: str,
|
||||
model: LlmModel,
|
||||
messages: List[dict[str, str]],
|
||||
max_tokens: int | None = None,
|
||||
) -> str:
|
||||
provider = model.metadata.provider
|
||||
|
||||
if provider == "openai":
|
||||
openai.api_key = api_key
|
||||
response = openai.chat.completions.create(
|
||||
model=model.value,
|
||||
messages=messages, # type: ignore
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
return response.choices[0].message.content or ""
|
||||
elif provider == "anthropic":
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
response = client.messages.create(
|
||||
model=model.value, max_tokens=max_tokens or 4096, messages=messages # type: ignore
|
||||
)
|
||||
return response.content[0].text if response.content else ""
|
||||
elif provider == "groq":
|
||||
client = Groq(api_key=api_key)
|
||||
response = client.chat.completions.create(
|
||||
model=model.value,
|
||||
messages=messages, # type: ignore
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
return response.choices[0].message.content or ""
|
||||
elif provider == "ollama":
|
||||
response = ollama.chat(
|
||||
model=model.value, messages=messages, stream=False # type: ignore
|
||||
)
|
||||
return response["message"]["content"]
|
||||
else:
|
||||
raise ValueError(f"Unsupported LLM provider: {provider}")
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
try:
|
||||
api_key = (
|
||||
input_data.api_key.get_secret_value()
|
||||
or LlmApiKeys[input_data.model.metadata.provider].get_secret_value()
|
||||
)
|
||||
|
||||
messages = [message.model_dump() for message in input_data.messages]
|
||||
|
||||
response = self.llm_call(
|
||||
api_key=api_key,
|
||||
model=input_data.model,
|
||||
messages=messages,
|
||||
max_tokens=input_data.max_tokens,
|
||||
)
|
||||
|
||||
yield "response", response
|
||||
except Exception as e:
|
||||
yield "error", f"Error calling LLM: {str(e)}"
|
||||
|
||||
@@ -154,3 +154,33 @@ class TextFormatterBlock(Block):
|
||||
texts=input_data.texts,
|
||||
**input_data.named_texts,
|
||||
)
|
||||
|
||||
|
||||
class TextCombinerBlock(Block):
|
||||
class Input(BlockSchema):
|
||||
input1: str = Field(description="First text input", default="a")
|
||||
input2: str = Field(description="Second text input", default="b")
|
||||
|
||||
class Output(BlockSchema):
|
||||
output: str = Field(description="Combined text")
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="e30a4d42-7b7d-4e6a-b36e-1f9b8e3b7d85",
|
||||
description="This block combines multiple input texts into a single output text.",
|
||||
categories={BlockCategory.TEXT},
|
||||
input_schema=TextCombinerBlock.Input,
|
||||
output_schema=TextCombinerBlock.Output,
|
||||
test_input=[
|
||||
{"input1": "Hello world I like ", "input2": "cake and to go for walks"},
|
||||
{"input1": "This is a test. ", "input2": "Let's see how it works."},
|
||||
],
|
||||
test_output=[
|
||||
("output", "Hello world I like cake and to go for walks"),
|
||||
("output", "This is a test. Let's see how it works."),
|
||||
],
|
||||
)
|
||||
|
||||
def run(self, input_data: Input) -> BlockOutput:
|
||||
combined_text = (input_data.input1 or "") + (input_data.input2 or "")
|
||||
yield "output", combined_text
|
||||
|
||||
@@ -105,6 +105,8 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
|
||||
medium_api_key: str = Field(default="", description="Medium API key")
|
||||
medium_author_id: str = Field(default="", description="Medium author ID")
|
||||
|
||||
discord_bot_token: str = Field(default="", description="Discord bot token")
|
||||
|
||||
# Add more secret fields as needed
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
|
||||
28
rnd/autogpt_server/poetry.lock
generated
28
rnd/autogpt_server/poetry.lock
generated
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "agpt"
|
||||
@@ -25,7 +25,7 @@ requests = "*"
|
||||
sentry-sdk = "^1.40.4"
|
||||
|
||||
[package.extras]
|
||||
benchmark = ["agbenchmark @ file:///Users/aarushi/autogpt/AutoGPT/benchmark"]
|
||||
benchmark = ["agbenchmark @ file:///home/bently/Desktop/autogpt-ui/AutoGPT/benchmark"]
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
@@ -329,7 +329,7 @@ watchdog = "4.0.0"
|
||||
webdriver-manager = "^4.0.1"
|
||||
|
||||
[package.extras]
|
||||
benchmark = ["agbenchmark @ file:///Users/aarushi/autogpt/AutoGPT/benchmark"]
|
||||
benchmark = ["agbenchmark @ file:///home/bently/Desktop/autogpt-ui/AutoGPT/benchmark"]
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
@@ -1073,6 +1073,26 @@ wrapt = ">=1.10,<2"
|
||||
[package.extras]
|
||||
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
|
||||
|
||||
[[package]]
|
||||
name = "discord-py"
|
||||
version = "2.4.0"
|
||||
description = "A Python wrapper for the Discord API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "discord.py-2.4.0-py3-none-any.whl", hash = "sha256:b8af6711c70f7e62160bfbecb55be699b5cb69d007426759ab8ab06b1bd77d1d"},
|
||||
{file = "discord_py-2.4.0.tar.gz", hash = "sha256:d07cb2a223a185873a1d0ee78b9faa9597e45b3f6186df21a95cec1e9bcdc9a5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.7.4,<4"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (==4.4.0)", "sphinx-inline-tabs (==2023.4.21)", "sphinxcontrib-applehelp (==1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (==2.0.1)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport (==1.2.4)", "typing-extensions (>=4.3,<5)"]
|
||||
speed = ["Brotli", "aiodns (>=1.1)", "cchardet (==2.1.7)", "orjson (>=3.5.4)"]
|
||||
test = ["coverage[toml]", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "typing-extensions (>=4.3,<5)", "tzdata"]
|
||||
voice = ["PyNaCl (>=1.3.0,<1.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
@@ -6399,4 +6419,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "b9a68db94e5721bfc916e583626e6da66a3469451d179bf9ee117d0a31b865f2"
|
||||
content-hash = "9991857e7076d3bfcbae7af6c2cec54dc943167a3adceb5a0ebf74d80c05778f"
|
||||
|
||||
@@ -39,6 +39,7 @@ ollama = "^0.3.0"
|
||||
feedparser = "^6.0.11"
|
||||
python-dotenv = "^1.0.1"
|
||||
expiringdict = "^1.2.2"
|
||||
discord-py = "^2.4.0"
|
||||
|
||||
autogpt-libs = { path = "../autogpt_libs", develop = true }
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
34
setup.sh
34
setup.sh
@@ -10,21 +10,35 @@ else
|
||||
if ! command -v python3 &> /dev/null
|
||||
then
|
||||
echo "python3 could not be found"
|
||||
echo "Installing python3 using pyenv..."
|
||||
if ! command -v pyenv &> /dev/null
|
||||
then
|
||||
echo "pyenv could not be found"
|
||||
echo "Installing pyenv..."
|
||||
curl https://pyenv.run | bash
|
||||
echo "Install python3 using pyenv ([y]/n)?"
|
||||
read response
|
||||
if [[ "$response" == "y" || -z "$response" ]]; then
|
||||
echo "Installing python3..."
|
||||
if ! command -v pyenv &> /dev/null
|
||||
then
|
||||
echo "pyenv could not be found"
|
||||
echo "Installing pyenv..."
|
||||
curl https://pyenv.run | bash
|
||||
fi
|
||||
pyenv install 3.11.5
|
||||
pyenv global 3.11.5
|
||||
else
|
||||
echo "Aborting setup"
|
||||
exit 1
|
||||
fi
|
||||
pyenv install 3.11.5
|
||||
pyenv global 3.11.5
|
||||
fi
|
||||
|
||||
if ! command -v poetry &> /dev/null
|
||||
then
|
||||
echo "poetry could not be found"
|
||||
echo "Installing poetry..."
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
echo "Install poetry using official installer ([y]/n)?"
|
||||
read response
|
||||
if [[ "$response" == "y" || -z "$response" ]]; then
|
||||
echo "Installing poetry..."
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
else
|
||||
echo "Aborting setup"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user