Merge branch 'master' into aarushikansal-add-vector-store-support

This commit is contained in:
Nicholas Tindle
2024-08-04 22:18:26 -07:00
committed by GitHub
33 changed files with 1632 additions and 408 deletions

12
.github/CODEOWNERS vendored
View File

@@ -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
View File

@@ -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()

View File

@@ -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**

View File

@@ -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

View File

@@ -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",

View File

@@ -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 => {

View File

@@ -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>

View File

@@ -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'

View File

@@ -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">
&times;
<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">
&times;
</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">
&times;
</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;

View File

@@ -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 (

View 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;

View File

@@ -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>
);
};

View 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">
{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;

View File

@@ -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>
);
}

View File

@@ -33,7 +33,6 @@ input, textarea {
border-radius: 4px;
width: calc(100% - 18px);
box-sizing: border-box;
margin-top: 5px;
}
input::placeholder, textarea::placeholder {

View 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();

View File

@@ -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",

View 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 }

View 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 }

View 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 }

View File

@@ -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 */

View File

@@ -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[];
};

View File

@@ -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"

View File

@@ -8,3 +8,6 @@ REDDIT_CLIENT_ID=
REDDIT_CLIENT_SECRET=
REDDIT_USERNAME=
REDDIT_PASSWORD=
# Discord
DISCORD_BOT_TOKEN=

View File

@@ -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

View File

@@ -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)}"

View 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}")

View File

@@ -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)}"

View File

@@ -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

View File

@@ -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(

View File

@@ -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"

View File

@@ -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]

View File

@@ -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